tfrere HF Staff commited on
Commit
6ca591a
·
2 Parent(s): d6c8ff488e27c3

Merge branch 'feature/install-modal'

Browse files
src/assets/reachy-mini-access-point-QR-code.png ADDED

Git LFS Details

  • SHA256: deb7d1a6526187f7513a446e062c7fbd5312943efdf6556aee468fa83d627350
  • Pointer size: 129 Bytes
  • Size of remote file: 1.87 kB
src/components/InstallModal.jsx ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo } from 'react';
2
+ import {
3
+ Avatar,
4
+ Box,
5
+ Button,
6
+ Chip,
7
+ Dialog,
8
+ DialogContent,
9
+ IconButton,
10
+ Link,
11
+ Typography,
12
+ } from '@mui/material';
13
+ import CloseIcon from '@mui/icons-material/Close';
14
+ import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
15
+ import VerifiedIcon from '@mui/icons-material/Verified';
16
+ import DownloadIcon from '@mui/icons-material/Download';
17
+ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
18
+ import ComputerIcon from '@mui/icons-material/Computer';
19
+ import BlockIcon from '@mui/icons-material/Block';
20
+
21
+ function InstallModal({ open, onClose, app }) {
22
+ // Detect Linux users
23
+ const isLinux = useMemo(() => {
24
+ if (typeof navigator === 'undefined') return false;
25
+ const platform = navigator.platform?.toLowerCase() || '';
26
+ const userAgent = navigator.userAgent?.toLowerCase() || '';
27
+ return platform.includes('linux') || userAgent.includes('linux');
28
+ }, []);
29
+
30
+ if (!app) return null;
31
+
32
+ const appName = app.name || app.id?.split('/').pop();
33
+ const cardData = app.cardData || {};
34
+ const emoji = cardData.emoji || '📦';
35
+ const description = cardData.short_description || app.description || 'No description';
36
+ const deepLinkUrl = `reachymini://install/${appName}`;
37
+ const spaceUrl = `https://huggingface.co/spaces/${app.id}`;
38
+
39
+ const author = app.id?.split('/')?.[0] || app.author || null;
40
+ const isOfficial = app.isOfficial;
41
+ const likes = app.likes || 0;
42
+ const lastModified = app.lastModified || app.createdAt || null;
43
+ const formattedDate = lastModified
44
+ ? new Date(lastModified).toLocaleDateString('en-US', {
45
+ month: 'short',
46
+ day: 'numeric',
47
+ year: 'numeric',
48
+ })
49
+ : null;
50
+
51
+ const handleInstall = () => {
52
+ window.location.href = deepLinkUrl;
53
+ setTimeout(() => onClose(), 500);
54
+ };
55
+
56
+ return (
57
+ <Dialog
58
+ open={open}
59
+ onClose={onClose}
60
+ PaperProps={{
61
+ sx: {
62
+ borderRadius: '20px',
63
+ maxWidth: 520,
64
+ minWidth: { xs: 'auto', sm: 480 },
65
+ width: '100%',
66
+ mx: 2,
67
+ overflow: 'visible',
68
+ bgcolor: '#fff',
69
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
70
+ },
71
+ }}
72
+ >
73
+ {/* Close button outside modal */}
74
+ <IconButton
75
+ onClick={onClose}
76
+ disableRipple
77
+ sx={{
78
+ position: 'absolute',
79
+ top: 0,
80
+ right: -44,
81
+ color: 'rgba(255,255,255,0.8)',
82
+ p: 0.5,
83
+ '&:hover': { color: '#fff', bgcolor: 'transparent' },
84
+ }}
85
+ >
86
+ <CloseIcon sx={{ fontSize: 24 }} />
87
+ </IconButton>
88
+
89
+ <DialogContent sx={{ p: 0, overflow: 'hidden', borderRadius: '20px' }}>
90
+ {/* Header */}
91
+ <Box sx={{ p: 3 }}>
92
+
93
+ {/* App row */}
94
+ <Box sx={{ display: 'flex', gap: 2.5 }}>
95
+ {/* Emoji */}
96
+ <Box
97
+ sx={{
98
+ width: 72,
99
+ height: 72,
100
+ borderRadius: '18px',
101
+ bgcolor: '#f5f5f5',
102
+ display: 'flex',
103
+ alignItems: 'center',
104
+ justifyContent: 'center',
105
+ fontSize: 36,
106
+ flexShrink: 0,
107
+ }}
108
+ >
109
+ {emoji}
110
+ </Box>
111
+
112
+ {/* Info */}
113
+ <Box sx={{ flex: 1, minWidth: 0, pr: 4 }}>
114
+ {/* Title */}
115
+ <Typography
116
+ sx={{
117
+ fontSize: 20,
118
+ fontWeight: 700,
119
+ color: '#1a1a1a',
120
+ mb: 1,
121
+ lineHeight: 1.2,
122
+ }}
123
+ >
124
+ {appName}
125
+ </Typography>
126
+
127
+ {/* Meta row */}
128
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}>
129
+ {author && (
130
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
131
+ <Avatar
132
+ sx={{
133
+ width: 20,
134
+ height: 20,
135
+ bgcolor: '#e0e0e0',
136
+ fontSize: 10,
137
+ fontWeight: 600,
138
+ color: '#666',
139
+ }}
140
+ >
141
+ {author.charAt(0).toUpperCase()}
142
+ </Avatar>
143
+ <Typography sx={{ fontSize: 13, color: '#666', fontFamily: 'monospace' }}>
144
+ {author}
145
+ </Typography>
146
+ </Box>
147
+ )}
148
+ {isOfficial && (
149
+ <Chip
150
+ icon={<VerifiedIcon sx={{ fontSize: 14 }} />}
151
+ label="Official"
152
+ size="small"
153
+ sx={{
154
+ height: 24,
155
+ bgcolor: 'rgba(255, 149, 0, 0.1)',
156
+ color: '#FF9500',
157
+ fontWeight: 600,
158
+ fontSize: 11,
159
+ '& .MuiChip-icon': { color: '#FF9500', ml: 0.5 },
160
+ '& .MuiChip-label': { px: 1 },
161
+ }}
162
+ />
163
+ )}
164
+ </Box>
165
+
166
+ {/* Stats row */}
167
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1 }}>
168
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
169
+ <FavoriteBorderIcon sx={{ fontSize: 14, color: '#999' }} />
170
+ <Typography sx={{ fontSize: 12, color: '#999' }}>{likes}</Typography>
171
+ </Box>
172
+ {formattedDate && (
173
+ <Typography sx={{ fontSize: 12, color: '#aaa' }}>
174
+ Updated {formattedDate}
175
+ </Typography>
176
+ )}
177
+ </Box>
178
+
179
+ {/* Description - intégrée au bloc info */}
180
+ <Typography
181
+ sx={{
182
+ fontSize: 13.5,
183
+ color: '#666',
184
+ lineHeight: 1.6,
185
+ mt: 1.5,
186
+ }}
187
+ >
188
+ {description}
189
+ </Typography>
190
+ </Box>
191
+ </Box>
192
+ </Box>
193
+
194
+ {/* Divider */}
195
+ <Box sx={{ height: 1, bgcolor: '#f0f0f0', mx: 3 }} />
196
+
197
+ {/* Desktop App Requirement Block */}
198
+ <Box sx={{ p: 3 }}>
199
+ {isLinux ? (
200
+ // Linux: Not supported message
201
+ <Box
202
+ sx={{
203
+ display: 'flex',
204
+ alignItems: 'flex-start',
205
+ gap: 2,
206
+ p: 2.5,
207
+ borderRadius: '14px',
208
+ bgcolor: 'rgba(0, 0, 0, 0.03)',
209
+ border: '1px solid rgba(0, 0, 0, 0.08)',
210
+ }}
211
+ >
212
+ <Box
213
+ sx={{
214
+ width: 44,
215
+ height: 44,
216
+ borderRadius: '12px',
217
+ bgcolor: 'rgba(0, 0, 0, 0.05)',
218
+ display: 'flex',
219
+ alignItems: 'center',
220
+ justifyContent: 'center',
221
+ flexShrink: 0,
222
+ }}
223
+ >
224
+ <BlockIcon sx={{ fontSize: 24, color: '#999' }} />
225
+ </Box>
226
+ <Box sx={{ flex: 1 }}>
227
+ <Typography
228
+ sx={{
229
+ fontSize: 14,
230
+ fontWeight: 700,
231
+ color: '#1a1a1a',
232
+ mb: 0.75,
233
+ }}
234
+ >
235
+ Linux not supported yet
236
+ </Typography>
237
+ <Typography
238
+ sx={{
239
+ fontSize: 13,
240
+ color: '#666',
241
+ lineHeight: 1.6,
242
+ }}
243
+ >
244
+ The desktop app is currently available for <strong>macOS</strong> and <strong>Windows</strong> only.
245
+ Linux support is coming soon!
246
+ </Typography>
247
+ </Box>
248
+ </Box>
249
+ ) : (
250
+ // macOS/Windows: Normal install flow
251
+ <Box
252
+ sx={{
253
+ display: 'flex',
254
+ alignItems: 'flex-start',
255
+ gap: 2,
256
+ p: 2.5,
257
+ borderRadius: '14px',
258
+ bgcolor: 'rgba(255, 149, 0, 0.05)',
259
+ border: '1px solid rgba(255, 149, 0, 0.12)',
260
+ }}
261
+ >
262
+ <Box
263
+ sx={{
264
+ width: 44,
265
+ height: 44,
266
+ borderRadius: '12px',
267
+ bgcolor: 'rgba(255, 149, 0, 0.1)',
268
+ display: 'flex',
269
+ alignItems: 'center',
270
+ justifyContent: 'center',
271
+ flexShrink: 0,
272
+ }}
273
+ >
274
+ <ComputerIcon sx={{ fontSize: 24, color: '#FF9500' }} />
275
+ </Box>
276
+ <Box sx={{ flex: 1 }}>
277
+ <Typography
278
+ sx={{
279
+ fontSize: 14,
280
+ fontWeight: 700,
281
+ color: '#1a1a1a',
282
+ mb: 0.75,
283
+ }}
284
+ >
285
+ Reachy Mini Desktop App required
286
+ </Typography>
287
+ <Typography
288
+ sx={{
289
+ fontSize: 13,
290
+ color: '#666',
291
+ lineHeight: 1.6,
292
+ mb: 1.5,
293
+ }}
294
+ >
295
+ No robot? <Link
296
+ href="#"
297
+ sx={{
298
+ color: '#FF9500',
299
+ fontWeight: 600,
300
+ textDecoration: 'none',
301
+ '&:hover': { textDecoration: 'underline' },
302
+ }}
303
+ >
304
+ Try it in simulation mode
305
+ </Link> – no hardware needed!
306
+ </Typography>
307
+ <Link
308
+ href="/download"
309
+ sx={{
310
+ display: 'inline-block',
311
+ fontSize: 13,
312
+ fontWeight: 600,
313
+ color: '#FF9500',
314
+ textDecoration: 'none',
315
+ '&:hover': { textDecoration: 'underline' },
316
+ }}
317
+ >
318
+ Download the desktop app →
319
+ </Link>
320
+ </Box>
321
+ </Box>
322
+ )}
323
+ </Box>
324
+
325
+ {/* Actions */}
326
+ <Box sx={{ px: 3, pb: 3, display: 'flex', gap: 2 }}>
327
+ <Button
328
+ component={Link}
329
+ href={spaceUrl}
330
+ target="_blank"
331
+ rel="noopener noreferrer"
332
+ variant="outlined"
333
+ sx={{
334
+ flex: isLinux ? 1 : 1,
335
+ py: 1.5,
336
+ borderRadius: '12px',
337
+ borderColor: '#ddd',
338
+ color: '#555',
339
+ fontWeight: 600,
340
+ fontSize: 14,
341
+ textTransform: 'none',
342
+ textDecoration: 'none',
343
+ gap: 0.75,
344
+ '&:hover': { borderColor: '#bbb', bgcolor: '#fafafa' },
345
+ }}
346
+ >
347
+ App Page
348
+ <OpenInNewIcon sx={{ fontSize: 16 }} />
349
+ </Button>
350
+
351
+ {!isLinux && (
352
+ <Button
353
+ variant="contained"
354
+ onClick={handleInstall}
355
+ startIcon={<DownloadIcon sx={{ fontSize: 20 }} />}
356
+ sx={{
357
+ flex: 1.5,
358
+ py: 1.5,
359
+ borderRadius: '12px',
360
+ bgcolor: '#FF9500',
361
+ fontWeight: 600,
362
+ fontSize: 14,
363
+ textTransform: 'none',
364
+ boxShadow: 'none',
365
+ '&:hover': {
366
+ bgcolor: '#e68600',
367
+ boxShadow: '0 4px 12px rgba(255, 149, 0, 0.35)',
368
+ },
369
+ }}
370
+ >
371
+ Install
372
+ </Button>
373
+ )}
374
+ </Box>
375
+ </DialogContent>
376
+ </Dialog>
377
+ );
378
+ }
379
+
380
+ export default InstallModal;
src/pages/Apps.jsx CHANGED
@@ -11,19 +11,21 @@ import {
11
  CircularProgress,
12
  Link,
13
  IconButton,
 
14
  } from '@mui/material';
15
  import SearchIcon from '@mui/icons-material/Search';
16
  import CloseIcon from '@mui/icons-material/Close';
17
  import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
18
  import AccessTimeIcon from '@mui/icons-material/AccessTime';
19
- import OpenInNewIcon from '@mui/icons-material/OpenInNew';
20
  import VerifiedIcon from '@mui/icons-material/Verified';
 
21
  import Layout from '../components/Layout';
22
  import ReachiesCarousel from '../components/ReachiesCarousel';
23
  import { useApps } from '../context/AppsContext';
 
24
 
25
  // App Card Component
26
- function AppCard({ app }) {
27
  const isOfficial = app.isOfficial;
28
  const cardData = app.cardData || {};
29
  const author = app.id?.split('/')?.[0] || app.author || null;
@@ -35,14 +37,14 @@ function AppCard({ app }) {
35
  ? new Date(lastModified).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
36
  : null;
37
 
38
- const spaceUrl = `https://huggingface.co/spaces/${app.id}`;
 
 
 
 
39
 
40
  return (
41
  <Box
42
- component={Link}
43
- href={spaceUrl}
44
- target="_blank"
45
- rel="noopener noreferrer"
46
  sx={{
47
  display: 'flex',
48
  flexDirection: 'column',
@@ -51,9 +53,7 @@ function AppCard({ app }) {
51
  overflow: 'hidden',
52
  bgcolor: '#ffffff',
53
  border: isOfficial ? '1px solid rgba(59, 130, 246, 0.25)' : '1px solid rgba(0, 0, 0, 0.08)',
54
- cursor: 'pointer',
55
  transition: 'all 0.2s ease',
56
- textDecoration: 'none',
57
  '&:hover': {
58
  transform: 'translateY(-4px)',
59
  boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)',
@@ -203,9 +203,10 @@ function AppCard({ app }) {
203
  {cardData.short_description || app.description || 'No description'}
204
  </Typography>
205
 
206
- {/* Date + Open Link */}
207
  <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
208
- {formattedDate && (
 
209
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
210
  <AccessTimeIcon sx={{ fontSize: 14, color: '#999' }} />
211
  <Typography
@@ -218,14 +219,31 @@ function AppCard({ app }) {
218
  {formattedDate}
219
  </Typography>
220
  </Box>
 
 
221
  )}
222
 
223
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: '#FF9500' }}>
224
- <Typography sx={{ fontSize: 12, fontWeight: 600 }}>
225
- View on HF
226
- </Typography>
227
- <OpenInNewIcon sx={{ fontSize: 14 }} />
228
- </Box>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  </Box>
230
  </Box>
231
  </Box>
@@ -238,6 +256,20 @@ export default function Apps() {
238
  const { apps, loading, error } = useApps();
239
  const [searchQuery, setSearchQuery] = useState('');
240
  const [officialOnly, setOfficialOnly] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
  // Filter apps based on search and official toggle
243
  const filteredApps = useMemo(() => {
@@ -509,11 +541,19 @@ export default function Apps() {
509
  <AppCard
510
  key={app.id || index}
511
  app={app}
 
512
  />
513
  ))}
514
  </Box>
515
  )}
516
  </Container>
 
 
 
 
 
 
 
517
  </Layout>
518
  );
519
  }
 
11
  CircularProgress,
12
  Link,
13
  IconButton,
14
+ Button,
15
  } from '@mui/material';
16
  import SearchIcon from '@mui/icons-material/Search';
17
  import CloseIcon from '@mui/icons-material/Close';
18
  import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
19
  import AccessTimeIcon from '@mui/icons-material/AccessTime';
 
20
  import VerifiedIcon from '@mui/icons-material/Verified';
21
+ import DownloadIcon from '@mui/icons-material/Download';
22
  import Layout from '../components/Layout';
23
  import ReachiesCarousel from '../components/ReachiesCarousel';
24
  import { useApps } from '../context/AppsContext';
25
+ import InstallModal from '../components/InstallModal';
26
 
27
  // App Card Component
28
+ function AppCard({ app, onInstallClick }) {
29
  const isOfficial = app.isOfficial;
30
  const cardData = app.cardData || {};
31
  const author = app.id?.split('/')?.[0] || app.author || null;
 
37
  ? new Date(lastModified).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
38
  : null;
39
 
40
+ const handleInstallClick = (e) => {
41
+ e.preventDefault();
42
+ e.stopPropagation();
43
+ onInstallClick?.(app);
44
+ };
45
 
46
  return (
47
  <Box
 
 
 
 
48
  sx={{
49
  display: 'flex',
50
  flexDirection: 'column',
 
53
  overflow: 'hidden',
54
  bgcolor: '#ffffff',
55
  border: isOfficial ? '1px solid rgba(59, 130, 246, 0.25)' : '1px solid rgba(0, 0, 0, 0.08)',
 
56
  transition: 'all 0.2s ease',
 
57
  '&:hover': {
58
  transform: 'translateY(-4px)',
59
  boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)',
 
203
  {cardData.short_description || app.description || 'No description'}
204
  </Typography>
205
 
206
+ {/* Date + Install Button */}
207
  <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
208
+ {/* Date */}
209
+ {formattedDate ? (
210
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
211
  <AccessTimeIcon sx={{ fontSize: 14, color: '#999' }} />
212
  <Typography
 
219
  {formattedDate}
220
  </Typography>
221
  </Box>
222
+ ) : (
223
+ <Box />
224
  )}
225
 
226
+ {/* Install Button */}
227
+ <Button
228
+ variant="text"
229
+ size="small"
230
+ endIcon={<DownloadIcon sx={{ fontSize: 14 }} />}
231
+ onClick={handleInstallClick}
232
+ sx={{
233
+ py: 0.5,
234
+ px: 1.5,
235
+ borderRadius: '8px',
236
+ color: '#FF9500',
237
+ fontWeight: 600,
238
+ fontSize: 12,
239
+ textTransform: 'none',
240
+ '&:hover': {
241
+ bgcolor: 'rgba(255, 149, 0, 0.08)',
242
+ },
243
+ }}
244
+ >
245
+ Install
246
+ </Button>
247
  </Box>
248
  </Box>
249
  </Box>
 
256
  const { apps, loading, error } = useApps();
257
  const [searchQuery, setSearchQuery] = useState('');
258
  const [officialOnly, setOfficialOnly] = useState(false);
259
+
260
+ // Install modal state
261
+ const [installModalOpen, setInstallModalOpen] = useState(false);
262
+ const [selectedApp, setSelectedApp] = useState(null);
263
+
264
+ const handleInstallClick = (app) => {
265
+ setSelectedApp(app);
266
+ setInstallModalOpen(true);
267
+ };
268
+
269
+ const handleCloseInstallModal = () => {
270
+ setInstallModalOpen(false);
271
+ setSelectedApp(null);
272
+ };
273
 
274
  // Filter apps based on search and official toggle
275
  const filteredApps = useMemo(() => {
 
541
  <AppCard
542
  key={app.id || index}
543
  app={app}
544
+ onInstallClick={handleInstallClick}
545
  />
546
  ))}
547
  </Box>
548
  )}
549
  </Container>
550
+
551
+ {/* Install Modal */}
552
+ <InstallModal
553
+ open={installModalOpen}
554
+ onClose={handleCloseInstallModal}
555
+ app={selectedApp}
556
+ />
557
  </Layout>
558
  );
559
  }