tfrere HF Staff commited on
Commit
acb9889
·
0 Parent(s):

feat: initial static gallery of research-article-template spaces

Browse files
Files changed (2) hide show
  1. README.md +16 -0
  2. index.html +518 -0
README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Research Article Template Gallery
3
+ emoji: "\U0001F4DA"
4
+ colorFrom: yellow
5
+ colorTo: purple
6
+ sdk: static
7
+ pinned: false
8
+ tags:
9
+ - research-article-template
10
+ ---
11
+
12
+ # Research Article Template - Spaces Gallery
13
+
14
+ A gallery of all HuggingFace Spaces built with the [research-article-template](https://huggingface.co/spaces/tfrere/research-article-template).
15
+
16
+ Data is fetched live from the HuggingFace API.
index.html ADDED
@@ -0,0 +1,518 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Research Article Template - Spaces Gallery</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <script
14
+ crossorigin
15
+ src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"
16
+ ></script>
17
+ <script
18
+ crossorigin
19
+ src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"
20
+ ></script>
21
+ <script
22
+ crossorigin
23
+ src="https://unpkg.com/@mui/material@5/umd/material-ui.production.min.js"
24
+ ></script>
25
+ <script
26
+ crossorigin
27
+ src="https://unpkg.com/@babel/standalone@7/babel.min.js"
28
+ ></script>
29
+ <style>
30
+ * {
31
+ margin: 0;
32
+ padding: 0;
33
+ box-sizing: border-box;
34
+ }
35
+ body {
36
+ background: #0d1117;
37
+ min-height: 100vh;
38
+ }
39
+ </style>
40
+ </head>
41
+ <body>
42
+ <div id="root"></div>
43
+ <script type="text/babel">
44
+ const { useState, useEffect, useMemo } = React;
45
+ const {
46
+ createTheme,
47
+ ThemeProvider,
48
+ CssBaseline,
49
+ Container,
50
+ Box,
51
+ Typography,
52
+ Paper,
53
+ Table,
54
+ TableBody,
55
+ TableCell,
56
+ TableContainer,
57
+ TableHead,
58
+ TableRow,
59
+ TableSortLabel,
60
+ TextField,
61
+ InputAdornment,
62
+ Chip,
63
+ Link,
64
+ CircularProgress,
65
+ Grid,
66
+ Card,
67
+ CardContent,
68
+ } = MaterialUI;
69
+
70
+ const API_URL =
71
+ "https://huggingface.co/api/spaces?filter=research-article-template&limit=100&sort=likes&direction=-1";
72
+
73
+ const theme = createTheme({
74
+ palette: {
75
+ mode: "dark",
76
+ primary: { main: "#FFD21E" },
77
+ background: { default: "#0d1117", paper: "#161b22" },
78
+ divider: "#30363d",
79
+ text: { primary: "#e6edf3", secondary: "#8b949e" },
80
+ },
81
+ typography: {
82
+ fontFamily: "'Inter', sans-serif",
83
+ h3: { fontWeight: 800, letterSpacing: "-0.02em" },
84
+ },
85
+ shape: { borderRadius: 12 },
86
+ components: {
87
+ MuiPaper: {
88
+ styleOverrides: { root: { backgroundImage: "none" } },
89
+ },
90
+ MuiTableCell: {
91
+ styleOverrides: { root: { borderColor: "#30363d" } },
92
+ },
93
+ },
94
+ });
95
+
96
+ function StatCard({ label, value, emoji }) {
97
+ return (
98
+ <Card
99
+ variant="outlined"
100
+ sx={{
101
+ height: "100%",
102
+ border: "1px solid #30363d",
103
+ bgcolor: "#161b22",
104
+ }}
105
+ >
106
+ <CardContent sx={{ textAlign: "center", py: 3 }}>
107
+ <Typography
108
+ variant="body2"
109
+ color="text.secondary"
110
+ gutterBottom
111
+ sx={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: 1 }}
112
+ >
113
+ {label}
114
+ </Typography>
115
+ <Typography variant="h4" fontWeight={700} sx={{ mt: 0.5 }}>
116
+ <span style={{ marginRight: 8 }}>{emoji}</span>
117
+ {value}
118
+ </Typography>
119
+ </CardContent>
120
+ </Card>
121
+ );
122
+ }
123
+
124
+ function App() {
125
+ const [spaces, setSpaces] = useState([]);
126
+ const [loading, setLoading] = useState(true);
127
+ const [error, setError] = useState(null);
128
+ const [search, setSearch] = useState("");
129
+ const [orderBy, setOrderBy] = useState("likes");
130
+ const [order, setOrder] = useState("desc");
131
+
132
+ useEffect(() => {
133
+ fetch(API_URL)
134
+ .then((r) => {
135
+ if (!r.ok) throw new Error("API request failed");
136
+ return r.json();
137
+ })
138
+ .then((data) => {
139
+ setSpaces(data);
140
+ setLoading(false);
141
+ })
142
+ .catch((err) => {
143
+ setError(err.message);
144
+ setLoading(false);
145
+ });
146
+ }, []);
147
+
148
+ const stats = useMemo(() => {
149
+ if (!spaces.length)
150
+ return { total: 0, likes: 0, authors: 0, latest: null };
151
+ const authors = new Set(spaces.map((s) => s.id.split("/")[0]));
152
+ const latest = spaces.reduce((a, b) =>
153
+ new Date(a.createdAt) > new Date(b.createdAt) ? a : b
154
+ );
155
+ return {
156
+ total: spaces.length,
157
+ likes: spaces.reduce((sum, s) => sum + (s.likes || 0), 0),
158
+ authors: authors.size,
159
+ latest,
160
+ };
161
+ }, [spaces]);
162
+
163
+ const sorted = useMemo(() => {
164
+ let result = spaces;
165
+ if (search) {
166
+ const q = search.toLowerCase();
167
+ result = result.filter((s) => s.id.toLowerCase().includes(q));
168
+ }
169
+ return [...result].sort((a, b) => {
170
+ let av, bv;
171
+ if (orderBy === "likes") {
172
+ av = a.likes;
173
+ bv = b.likes;
174
+ } else if (orderBy === "createdAt") {
175
+ av = new Date(a.createdAt).getTime();
176
+ bv = new Date(b.createdAt).getTime();
177
+ } else {
178
+ av = a.id.toLowerCase();
179
+ bv = b.id.toLowerCase();
180
+ }
181
+ if (av < bv) return order === "asc" ? -1 : 1;
182
+ if (av > bv) return order === "asc" ? 1 : -1;
183
+ return 0;
184
+ });
185
+ }, [spaces, search, orderBy, order]);
186
+
187
+ const handleSort = (prop) => {
188
+ if (orderBy === prop) {
189
+ setOrder(order === "desc" ? "asc" : "desc");
190
+ } else {
191
+ setOrderBy(prop);
192
+ setOrder(prop === "id" ? "asc" : "desc");
193
+ }
194
+ };
195
+
196
+ const fmtDate = (d) =>
197
+ new Date(d).toLocaleDateString("en-US", {
198
+ year: "numeric",
199
+ month: "short",
200
+ day: "numeric",
201
+ });
202
+
203
+ if (loading) {
204
+ return (
205
+ <Box
206
+ display="flex"
207
+ flexDirection="column"
208
+ justifyContent="center"
209
+ alignItems="center"
210
+ minHeight="100vh"
211
+ gap={2}
212
+ >
213
+ <CircularProgress sx={{ color: "#FFD21E" }} />
214
+ <Typography color="text.secondary">
215
+ Loading spaces...
216
+ </Typography>
217
+ </Box>
218
+ );
219
+ }
220
+
221
+ if (error) {
222
+ return (
223
+ <Box
224
+ display="flex"
225
+ justifyContent="center"
226
+ alignItems="center"
227
+ minHeight="100vh"
228
+ >
229
+ <Typography color="error">Failed to load data: {error}</Typography>
230
+ </Box>
231
+ );
232
+ }
233
+
234
+ return (
235
+ <Container maxWidth="lg" sx={{ py: { xs: 3, md: 6 } }}>
236
+ <Box textAlign="center" mb={5}>
237
+ <Typography
238
+ variant="h3"
239
+ sx={{
240
+ mb: 1.5,
241
+ fontSize: { xs: "1.8rem", md: "3rem" },
242
+ background: "linear-gradient(135deg, #FFD21E 0%, #FF9D00 100%)",
243
+ WebkitBackgroundClip: "text",
244
+ WebkitTextFillColor: "transparent",
245
+ }}
246
+ >
247
+ Research Article Template
248
+ </Typography>
249
+ <Typography
250
+ variant="h6"
251
+ color="text.secondary"
252
+ fontWeight={400}
253
+ sx={{ fontSize: { xs: "0.95rem", md: "1.25rem" } }}
254
+ >
255
+ All {stats.total} HuggingFace Spaces built with the template
256
+ </Typography>
257
+ </Box>
258
+
259
+ <Grid container spacing={2} sx={{ mb: 4 }}>
260
+ <Grid item xs={6} sm={3}>
261
+ <StatCard
262
+ label="Spaces"
263
+ value={stats.total}
264
+ emoji={"\u{1F4E6}"}
265
+ />
266
+ </Grid>
267
+ <Grid item xs={6} sm={3}>
268
+ <StatCard
269
+ label="Total Likes"
270
+ value={stats.likes.toLocaleString()}
271
+ emoji={"\u2764\uFE0F"}
272
+ />
273
+ </Grid>
274
+ <Grid item xs={6} sm={3}>
275
+ <StatCard
276
+ label="Authors"
277
+ value={stats.authors}
278
+ emoji={"\u{1F465}"}
279
+ />
280
+ </Grid>
281
+ <Grid item xs={6} sm={3}>
282
+ <StatCard
283
+ label="Latest"
284
+ value={
285
+ stats.latest
286
+ ? fmtDate(stats.latest.createdAt)
287
+ : "-"
288
+ }
289
+ emoji={"\u{1F4C5}"}
290
+ />
291
+ </Grid>
292
+ </Grid>
293
+
294
+ <TextField
295
+ fullWidth
296
+ placeholder="Search spaces..."
297
+ variant="outlined"
298
+ size="small"
299
+ value={search}
300
+ onChange={(e) => setSearch(e.target.value)}
301
+ sx={{
302
+ mb: 3,
303
+ "& .MuiOutlinedInput-root": {
304
+ borderRadius: 2,
305
+ bgcolor: "#161b22",
306
+ "& fieldset": { borderColor: "#30363d" },
307
+ "&:hover fieldset": { borderColor: "#FFD21E" },
308
+ "&.Mui-focused fieldset": { borderColor: "#FFD21E" },
309
+ },
310
+ }}
311
+ InputProps={{
312
+ startAdornment: (
313
+ <InputAdornment position="start">
314
+ <span style={{ fontSize: 18 }}>{"\u{1F50D}"}</span>
315
+ </InputAdornment>
316
+ ),
317
+ }}
318
+ />
319
+
320
+ <TableContainer
321
+ component={Paper}
322
+ variant="outlined"
323
+ sx={{ border: "1px solid #30363d", bgcolor: "#0d1117" }}
324
+ >
325
+ <Table size="small">
326
+ <TableHead>
327
+ <TableRow sx={{ bgcolor: "#161b22" }}>
328
+ <TableCell sx={{ fontWeight: 600, width: 50 }}>
329
+ #
330
+ </TableCell>
331
+ <TableCell sx={{ fontWeight: 600 }}>
332
+ <TableSortLabel
333
+ active={orderBy === "id"}
334
+ direction={orderBy === "id" ? order : "asc"}
335
+ onClick={() => handleSort("id")}
336
+ >
337
+ Space
338
+ </TableSortLabel>
339
+ </TableCell>
340
+ <TableCell sx={{ fontWeight: 600, width: 120 }} align="right">
341
+ <TableSortLabel
342
+ active={orderBy === "likes"}
343
+ direction={orderBy === "likes" ? order : "desc"}
344
+ onClick={() => handleSort("likes")}
345
+ >
346
+ Likes
347
+ </TableSortLabel>
348
+ </TableCell>
349
+ <TableCell
350
+ sx={{
351
+ fontWeight: 600,
352
+ width: 140,
353
+ display: { xs: "none", sm: "table-cell" },
354
+ }}
355
+ >
356
+ <TableSortLabel
357
+ active={orderBy === "createdAt"}
358
+ direction={orderBy === "createdAt" ? order : "desc"}
359
+ onClick={() => handleSort("createdAt")}
360
+ >
361
+ Created
362
+ </TableSortLabel>
363
+ </TableCell>
364
+ </TableRow>
365
+ </TableHead>
366
+ <TableBody>
367
+ {sorted.map((space, i) => {
368
+ const parts = space.id.split("/");
369
+ const author = parts[0];
370
+ const name = parts[1];
371
+ const isPopular = space.likes >= 50;
372
+
373
+ return (
374
+ <TableRow
375
+ key={space.id}
376
+ hover
377
+ sx={{
378
+ "&:hover": { bgcolor: "rgba(255,210,30,0.03)" },
379
+ cursor: "pointer",
380
+ }}
381
+ onClick={() =>
382
+ window.open(
383
+ "https://huggingface.co/spaces/" + space.id,
384
+ "_blank"
385
+ )
386
+ }
387
+ >
388
+ <TableCell>
389
+ <Typography variant="body2" color="text.secondary">
390
+ {i + 1}
391
+ </Typography>
392
+ </TableCell>
393
+ <TableCell>
394
+ <Box>
395
+ <Typography
396
+ variant="caption"
397
+ color="text.secondary"
398
+ sx={{ display: "block", lineHeight: 1.3 }}
399
+ >
400
+ {author}
401
+ </Typography>
402
+ <Link
403
+ href={
404
+ "https://huggingface.co/spaces/" + space.id
405
+ }
406
+ target="_blank"
407
+ rel="noopener"
408
+ underline="hover"
409
+ onClick={(e) => e.stopPropagation()}
410
+ sx={{
411
+ color: "#58a6ff",
412
+ fontWeight: 500,
413
+ fontSize: "0.9rem",
414
+ }}
415
+ >
416
+ {name}
417
+ </Link>
418
+ </Box>
419
+ </TableCell>
420
+ <TableCell align="right">
421
+ {space.likes > 0 ? (
422
+ <Chip
423
+ label={
424
+ "\u2764\uFE0F " +
425
+ space.likes.toLocaleString()
426
+ }
427
+ size="small"
428
+ sx={{
429
+ bgcolor: isPopular
430
+ ? "rgba(255,210,30,0.12)"
431
+ : "rgba(255,255,255,0.05)",
432
+ color: isPopular ? "#FFD21E" : "#8b949e",
433
+ fontWeight: isPopular ? 600 : 400,
434
+ height: 24,
435
+ "& .MuiChip-label": {
436
+ px: 1,
437
+ fontSize: "0.8rem",
438
+ },
439
+ }}
440
+ />
441
+ ) : (
442
+ <Typography
443
+ variant="body2"
444
+ color="text.disabled"
445
+ sx={{ fontSize: "0.8rem" }}
446
+ >
447
+ 0
448
+ </Typography>
449
+ )}
450
+ </TableCell>
451
+ <TableCell
452
+ sx={{
453
+ display: { xs: "none", sm: "table-cell" },
454
+ }}
455
+ >
456
+ <Typography
457
+ variant="body2"
458
+ color="text.secondary"
459
+ sx={{ fontSize: "0.8rem" }}
460
+ >
461
+ {fmtDate(space.createdAt)}
462
+ </Typography>
463
+ </TableCell>
464
+ </TableRow>
465
+ );
466
+ })}
467
+ {sorted.length === 0 && (
468
+ <TableRow>
469
+ <TableCell colSpan={4} align="center" sx={{ py: 4 }}>
470
+ <Typography color="text.secondary">
471
+ No spaces found matching "{search}"
472
+ </Typography>
473
+ </TableCell>
474
+ </TableRow>
475
+ )}
476
+ </TableBody>
477
+ </Table>
478
+ </TableContainer>
479
+
480
+ <Typography
481
+ variant="body2"
482
+ color="text.disabled"
483
+ sx={{ textAlign: "center", mt: 4, fontSize: "0.75rem" }}
484
+ >
485
+ Data fetched live from the{" "}
486
+ <Link
487
+ href="https://huggingface.co/docs/hub/api"
488
+ target="_blank"
489
+ rel="noopener"
490
+ color="inherit"
491
+ underline="hover"
492
+ >
493
+ HuggingFace API
494
+ </Link>
495
+ {" \u2022 "}
496
+ <Link
497
+ href="https://github.com/tfrere/research-article-template"
498
+ target="_blank"
499
+ rel="noopener"
500
+ color="inherit"
501
+ underline="hover"
502
+ >
503
+ research-article-template
504
+ </Link>
505
+ </Typography>
506
+ </Container>
507
+ );
508
+ }
509
+
510
+ ReactDOM.createRoot(document.getElementById("root")).render(
511
+ <ThemeProvider theme={theme}>
512
+ <CssBaseline />
513
+ <App />
514
+ </ThemeProvider>
515
+ );
516
+ </script>
517
+ </body>
518
+ </html>