tfrere HF Staff Cursor commited on
Commit
9ce28e1
·
1 Parent(s): 7d49524

fix: replace programmatic like with redirect to HF Space page

Browse files

HuggingFace intentionally blocks programmatic likes via API tokens to
prevent spam. Replace the failing POST /api/spaces/{id}/like call with
a redirect to the Space page on huggingface.co where users can like
natively. Likes are refreshed automatically when the tab regains focus.
Also remove the now-unnecessary write-discussions OAuth scope.

Co-authored-by: Cursor <cursoragent@cursor.com>

README.md CHANGED
@@ -6,8 +6,6 @@ colorTo: purple
6
  sdk: docker
7
  pinned: true
8
  hf_oauth: true
9
- hf_oauth_scopes:
10
- - write-discussions
11
  thumbnail: >-
12
  https://cdn-uploads.huggingface.co/production/uploads/671faa3a541a76b548647676/XWNDlOu0R4fHXR0kCW3Wd.png
13
  short_description: All about Reachy Mini, from building to getting started
 
6
  sdk: docker
7
  pinned: true
8
  hf_oauth: true
 
 
9
  thumbnail: >-
10
  https://cdn-uploads.huggingface.co/production/uploads/671faa3a541a76b548647676/XWNDlOu0R4fHXR0kCW3Wd.png
11
  short_description: All about Reachy Mini, from building to getting started
src/components/InstallModal.jsx CHANGED
@@ -21,7 +21,7 @@ import ComputerIcon from '@mui/icons-material/Computer';
21
  import { useAuth } from '../context/AuthContext';
22
 
23
  function InstallModal({ open, onClose, app }) {
24
- const { isLoggedIn, isSpaceLiked, toggleLike } = useAuth();
25
  // Detect Linux users
26
  const isLinux = useMemo(() => {
27
  if (typeof navigator === 'undefined') return false;
@@ -171,14 +171,19 @@ function InstallModal({ open, onClose, app }) {
171
  {/* Stats row */}
172
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1 }}>
173
  <Tooltip
174
- title={isLoggedIn ? '' : 'Sign in to like this app'}
175
  arrow
176
  placement="top"
177
- disableHoverListener={isLoggedIn}
178
  >
179
  <Box
180
  component="button"
181
- onClick={() => toggleLike(app.id)}
 
 
 
 
 
 
182
  sx={{
183
  display: 'flex',
184
  alignItems: 'center',
 
21
  import { useAuth } from '../context/AuthContext';
22
 
23
  function InstallModal({ open, onClose, app }) {
24
+ const { isLoggedIn, isSpaceLiked, openSpaceForLike, login } = useAuth();
25
  // Detect Linux users
26
  const isLinux = useMemo(() => {
27
  if (typeof navigator === 'undefined') return false;
 
171
  {/* Stats row */}
172
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1 }}>
173
  <Tooltip
174
+ title={isLoggedIn ? 'Like on Hugging Face' : 'Sign in to like this app'}
175
  arrow
176
  placement="top"
 
177
  >
178
  <Box
179
  component="button"
180
+ onClick={() => {
181
+ if (!isLoggedIn) {
182
+ login();
183
+ return;
184
+ }
185
+ openSpaceForLike(app.id);
186
+ }}
187
  sx={{
188
  display: 'flex',
189
  alignItems: 'center',
src/context/AuthContext.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
2
  import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
3
 
4
  const AuthContext = createContext(null);
@@ -43,11 +43,10 @@ async function fetchUserLikedSpaces(username) {
43
 
44
  // Provider component
45
  export function AuthProvider({ children }) {
46
- const [user, setUser] = useState(null); // { name, avatarUrl, accessToken }
47
  const [likedSpaceIds, setLikedSpaceIds] = useState(new Set());
48
  const [isLoading, setIsLoading] = useState(true);
49
  const [oauthConfig, setOauthConfig] = useState(null); // { clientId, scopes }
50
- const pendingLikes = useRef(new Set()); // Track in-flight like/unlike requests
51
 
52
  // On mount: fetch OAuth config + check if user just completed OAuth redirect
53
  useEffect(() => {
@@ -65,12 +64,11 @@ export function AuthProvider({ children }) {
65
  // Check if user just completed OAuth redirect
66
  const oauthResult = await oauthHandleRedirectIfPresent();
67
  if (oauthResult) {
68
- const { accessToken, userInfo } = oauthResult;
69
  const userData = {
70
  name: userInfo.name,
71
  preferredUsername: userInfo.preferred_username || userInfo.name,
72
  avatarUrl: userInfo.picture,
73
- accessToken: accessToken,
74
  };
75
  setUser(userData);
76
 
@@ -124,66 +122,31 @@ export function AuthProvider({ children }) {
124
  [likedSpaceIds]
125
  );
126
 
127
- // Toggle like/unlike with optimistic update
128
- const toggleLike = useCallback(
129
- async (spaceId) => {
130
- if (!user) {
131
- // Not logged in -> trigger login
132
- login();
133
- return;
134
- }
135
-
136
- const spaceIdLower = spaceId?.toLowerCase();
137
- if (!spaceIdLower) return;
138
-
139
- // Prevent duplicate requests for the same space
140
- if (pendingLikes.current.has(spaceIdLower)) return;
141
- pendingLikes.current.add(spaceIdLower);
142
-
143
- const wasLiked = likedSpaceIds.has(spaceIdLower);
144
- const method = wasLiked ? 'DELETE' : 'POST';
145
-
146
- // Optimistic update
147
- setLikedSpaceIds((prev) => {
148
- const next = new Set(prev);
149
- if (wasLiked) {
150
- next.delete(spaceIdLower);
151
- } else {
152
- next.add(spaceIdLower);
153
- }
154
- return next;
155
- });
156
-
157
- try {
158
- const response = await fetch(`${HF_API}/api/spaces/${spaceId}/like`, {
159
- method,
160
- headers: {
161
- Authorization: `Bearer ${user.accessToken}`,
162
- },
163
- });
164
-
165
- if (!response.ok) {
166
- throw new Error(`Like API returned ${response.status}`);
167
- }
168
- } catch (err) {
169
- console.error(`[Auth] Failed to ${wasLiked ? 'unlike' : 'like'} ${spaceId}:`, err);
170
- // Revert optimistic update on error
171
- setLikedSpaceIds((prev) => {
172
- const reverted = new Set(prev);
173
- if (wasLiked) {
174
- reverted.add(spaceIdLower);
175
- } else {
176
- reverted.delete(spaceIdLower);
177
- }
178
- return reverted;
179
- });
180
- } finally {
181
- pendingLikes.current.delete(spaceIdLower);
182
- }
183
  },
184
- [user, likedSpaceIds, login]
185
  );
186
 
 
 
 
 
 
 
 
 
 
 
187
  return (
188
  <AuthContext.Provider
189
  value={{
@@ -195,7 +158,8 @@ export function AuthProvider({ children }) {
195
  login,
196
  logout,
197
  isSpaceLiked,
198
- toggleLike,
 
199
  }}
200
  >
201
  {children}
 
1
+ import { createContext, useContext, useState, useEffect, useCallback } from 'react';
2
  import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
3
 
4
  const AuthContext = createContext(null);
 
43
 
44
  // Provider component
45
  export function AuthProvider({ children }) {
46
+ const [user, setUser] = useState(null); // { name, avatarUrl }
47
  const [likedSpaceIds, setLikedSpaceIds] = useState(new Set());
48
  const [isLoading, setIsLoading] = useState(true);
49
  const [oauthConfig, setOauthConfig] = useState(null); // { clientId, scopes }
 
50
 
51
  // On mount: fetch OAuth config + check if user just completed OAuth redirect
52
  useEffect(() => {
 
64
  // Check if user just completed OAuth redirect
65
  const oauthResult = await oauthHandleRedirectIfPresent();
66
  if (oauthResult) {
67
+ const { userInfo } = oauthResult;
68
  const userData = {
69
  name: userInfo.name,
70
  preferredUsername: userInfo.preferred_username || userInfo.name,
71
  avatarUrl: userInfo.picture,
 
72
  };
73
  setUser(userData);
74
 
 
122
  [likedSpaceIds]
123
  );
124
 
125
+ /**
126
+ * Open the HF Space page so the user can like it natively.
127
+ * HuggingFace intentionally blocks programmatic likes via API tokens
128
+ * to prevent spam ("To prevent spam usage, it is not possible to like a
129
+ * repository from a script." huggingface_hub SDK).
130
+ * Reading likes works fine; writing requires the native HF session.
131
+ */
132
+ const openSpaceForLike = useCallback(
133
+ (spaceId) => {
134
+ if (!spaceId) return;
135
+ window.open(`${HF_API}/spaces/${spaceId}`, '_blank', 'noopener,noreferrer');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  },
137
+ []
138
  );
139
 
140
+ /**
141
+ * Re-fetch user's liked spaces (call after user returns from HF page).
142
+ */
143
+ const refreshLikes = useCallback(async () => {
144
+ if (!user?.preferredUsername) return;
145
+ const likes = await fetchUserLikedSpaces(user.preferredUsername);
146
+ setLikedSpaceIds(likes);
147
+ console.log(`[Auth] Refreshed likes: ${likes.size} liked spaces`);
148
+ }, [user]);
149
+
150
  return (
151
  <AuthContext.Provider
152
  value={{
 
158
  login,
159
  logout,
160
  isSpaceLiked,
161
+ openSpaceForLike,
162
+ refreshLikes,
163
  }}
164
  >
165
  {children}
src/pages/Apps.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useCallback } from 'react';
2
  import {
3
  Box,
4
  Container,
@@ -30,7 +30,7 @@ import { useAuth } from '../context/AuthContext';
30
  import InstallModal from '../components/InstallModal';
31
 
32
  // App Card Component
33
- function AppCard({ app, onInstallClick, isLiked, onToggleLike, isLoggedIn }) {
34
  const isOfficial = app.isOfficial;
35
  const isPythonApp = app.extra?.isPythonApp !== false; // Default to true for backwards compatibility
36
  const cardData = app.extra?.cardData || {};
@@ -175,19 +175,18 @@ function AppCard({ app, onInstallClick, isLiked, onToggleLike, isLoggedIn }) {
175
  )}
176
  </Box>
177
 
178
- {/* Likes - Interactive */}
179
  <Tooltip
180
- title={isLoggedIn ? '' : 'Sign in to like this app'}
181
  arrow
182
  placement="top"
183
- disableHoverListener={isLoggedIn}
184
  >
185
  <Box
186
  component="button"
187
  onClick={(e) => {
188
  e.preventDefault();
189
  e.stopPropagation();
190
- onToggleLike?.(app.id);
191
  }}
192
  sx={{
193
  display: 'flex',
@@ -334,7 +333,7 @@ function AppCard({ app, onInstallClick, isLiked, onToggleLike, isLoggedIn }) {
334
  export default function Apps() {
335
  // Get apps from context (cached globally)
336
  const { apps, loading, error } = useApps();
337
- const { user, isLoggedIn, isOAuthAvailable, login, logout, isSpaceLiked, toggleLike } = useAuth();
338
  const [searchQuery, setSearchQuery] = useState('');
339
  const [officialOnly, setOfficialOnly] = useState(false);
340
 
@@ -352,13 +351,30 @@ export default function Apps() {
352
  setSelectedApp(null);
353
  };
354
 
355
- const handleToggleLike = useCallback(
 
356
  (spaceId) => {
357
- toggleLike(spaceId);
 
 
 
 
358
  },
359
- [toggleLike]
360
  );
361
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  // Filter apps based on search and official toggle
363
  const filteredApps = useMemo(() => {
364
  let result = apps;
@@ -698,7 +714,7 @@ export default function Apps() {
698
  app={app}
699
  onInstallClick={handleInstallClick}
700
  isLiked={isSpaceLiked(app.id)}
701
- onToggleLike={handleToggleLike}
702
  isLoggedIn={isLoggedIn}
703
  />
704
  ))}
 
1
+ import { useState, useMemo, useCallback, useEffect } from 'react';
2
  import {
3
  Box,
4
  Container,
 
30
  import InstallModal from '../components/InstallModal';
31
 
32
  // App Card Component
33
+ function AppCard({ app, onInstallClick, isLiked, onLikeClick, isLoggedIn }) {
34
  const isOfficial = app.isOfficial;
35
  const isPythonApp = app.extra?.isPythonApp !== false; // Default to true for backwards compatibility
36
  const cardData = app.extra?.cardData || {};
 
175
  )}
176
  </Box>
177
 
178
+ {/* Likes - Click opens HF Space page to like natively */}
179
  <Tooltip
180
+ title={isLoggedIn ? 'Like on Hugging Face' : 'Sign in to like this app'}
181
  arrow
182
  placement="top"
 
183
  >
184
  <Box
185
  component="button"
186
  onClick={(e) => {
187
  e.preventDefault();
188
  e.stopPropagation();
189
+ onLikeClick?.(app.id);
190
  }}
191
  sx={{
192
  display: 'flex',
 
333
  export default function Apps() {
334
  // Get apps from context (cached globally)
335
  const { apps, loading, error } = useApps();
336
+ const { user, isLoggedIn, isOAuthAvailable, login, logout, isSpaceLiked, openSpaceForLike, refreshLikes } = useAuth();
337
  const [searchQuery, setSearchQuery] = useState('');
338
  const [officialOnly, setOfficialOnly] = useState(false);
339
 
 
351
  setSelectedApp(null);
352
  };
353
 
354
+ // Handle like click: if logged in, open HF page; if not, prompt login
355
+ const handleLikeClick = useCallback(
356
  (spaceId) => {
357
+ if (!isLoggedIn) {
358
+ login();
359
+ return;
360
+ }
361
+ openSpaceForLike(spaceId);
362
  },
363
+ [isLoggedIn, login, openSpaceForLike]
364
  );
365
 
366
+ // Refresh likes when the tab regains focus (user may have liked on HF)
367
+ useEffect(() => {
368
+ if (!isLoggedIn) return;
369
+ const handleVisibilityChange = () => {
370
+ if (document.visibilityState === 'visible') {
371
+ refreshLikes();
372
+ }
373
+ };
374
+ document.addEventListener('visibilitychange', handleVisibilityChange);
375
+ return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
376
+ }, [isLoggedIn, refreshLikes]);
377
+
378
  // Filter apps based on search and official toggle
379
  const filteredApps = useMemo(() => {
380
  let result = apps;
 
714
  app={app}
715
  onInstallClick={handleInstallClick}
716
  isLiked={isSpaceLiked(app.id)}
717
+ onLikeClick={handleLikeClick}
718
  isLoggedIn={isLoggedIn}
719
  />
720
  ))}