severo HF staff commited on
Commit
b369d17
1 Parent(s): da80fef

copy files

Browse files
.eslintrc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "extends": ["next/core-web-vitals", "prettier"]
3
+ }
.github/workflows/build.yml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3
+
4
+ name: Verify Next.js Build
5
+ on:
6
+ push:
7
+ branches: ['main']
8
+ pull_request:
9
+ branches: ['main']
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+
15
+ strategy:
16
+ matrix:
17
+ node-version: [19.x]
18
+
19
+ steps:
20
+ - uses: actions/checkout@v3
21
+ - name: Use Node.js ${{ matrix.node-version }}
22
+ uses: actions/setup-node@v3
23
+ with:
24
+ node-version: ${{ matrix.node-version }}
25
+ cache: 'npm'
26
+ - run: npm ci
27
+ - run: npm run build
.github/workflows/prettier.yml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Run Prettier
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ prettier:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v2
15
+ with:
16
+ # Make sure the actual branch is checked out when running on pull requests
17
+ ref: ${{ github.head_ref }}
18
+ # This is important to fetch the changes to the previous commit
19
+ fetch-depth: 0
20
+
21
+ - name: Prettify code
22
+ uses: creyD/[email protected]
23
+ with:
24
+ # This part is also where you can pass other options, for example:
25
+ prettier_options: --write **/*.{js,md}
26
+ only_changed: True
.gitignore ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # next.js
12
+ /.next/
13
+ /out/
14
+
15
+ # production
16
+ /build
17
+
18
+ # misc
19
+ .DS_Store
20
+ *.pem
21
+
22
+ # debug
23
+ npm-debug.log*
24
+ yarn-debug.log*
25
+ yarn-error.log*
26
+ .pnpm-debug.log*
27
+
28
+ # local env files
29
+ .env*.local
30
+
31
+ # vercel
32
+ .vercel
33
+
34
+ # typescript
35
+ *.tsbuildinfo
36
+ next-env.d.ts
.prettierrc ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "trailingComma": "es5",
3
+ "tabWidth": 2,
4
+ "semi": false,
5
+ "singleQuote": true
6
+ }
Dockerfile ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ FROM node:19
2
+ WORKDIR /usr/src/app
3
+ COPY package.json package-lock.json ./
4
+ RUN npm ci
5
+ RUN npm rebuild
6
+ COPY . ./
7
+ RUN npm run build
8
+ RUN npm run start
LICENSE.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Gabi Purcaru
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
components/Content.tsx ADDED
@@ -0,0 +1,530 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Spinner } from './Spinner'
2
+ import React, { useState, memo, useRef } from 'react'
3
+ import debounce from 'debounce'
4
+ import { cp } from 'fs'
5
+
6
+ const usersCache = new Map<string, AccountDetails>()
7
+
8
+ type AccountDetails = {
9
+ user: string
10
+ fullname: string
11
+ // isFollowing: boolean
12
+ // type: "user" | "org"
13
+ // isPro: boolean
14
+ avatarUrl: string
15
+ followed_by: Set<string> // list of usernames
16
+ followers_count: number
17
+ details: string
18
+ }
19
+
20
+ async function accountFollows(
21
+ handle: string,
22
+ limit: number,
23
+ logError: (x: string) => void
24
+ ): Promise<Array<AccountDetails>> {
25
+ let nextPage:
26
+ | string
27
+ | null = `https://huggingface.co/api/users/${handle}/following`
28
+ let data: Array<AccountDetails> = []
29
+ while (nextPage && data.length <= limit) {
30
+ console.log(`Get page: ${nextPage}`)
31
+ let response
32
+ let page
33
+ try {
34
+ response = await fetch(nextPage)
35
+ if (response.status !== 200) {
36
+ throw new Error('HTTP request failed')
37
+ }
38
+ page = await response.json()
39
+ } catch (e) {
40
+ logError(`Error while retrieving follows for ${handle}.`)
41
+ break
42
+ }
43
+ if (!page.map) {
44
+ break
45
+ }
46
+ page = page.slice(0, limit)
47
+ // const newData = await Promise.all(
48
+ // page.map(async (account) => {
49
+ // const user = account.user
50
+ // if (!usersCache.has(user)) {
51
+ // const details = await accountDetails(user, logError)
52
+ // // const followers_count = await accountFollowersCount(user, logError)
53
+ // usersCache.set(user, { ...account, details })
54
+ // }
55
+ // return usersCache.get(user)
56
+ // })
57
+ // )
58
+ // data = [...data, ...newData]
59
+ data = [...data, ...page]
60
+ nextPage = getNextPage(response.headers.get('Link'))
61
+ }
62
+ return data
63
+ }
64
+
65
+ // async function accountFollowersCount(
66
+ // handle: string,
67
+ // logError: (x: string) => void
68
+ // ): Promise<number> {
69
+ // let nextPage:
70
+ // | string
71
+ // | null = `https://huggingface.co/api/users/${handle}/followers`
72
+ // let count = 0
73
+ // while (nextPage) {
74
+ // console.log(`Get page: ${nextPage}`)
75
+ // let response
76
+ // let page
77
+ // try {
78
+ // response = await fetch(nextPage)
79
+ // if (response.status !== 200) {
80
+ // throw new Error('HTTP request failed')
81
+ // }
82
+ // page = await response.json()
83
+ // } catch (e) {
84
+ // logError(`Error while retrieving followers for ${handle}.`)
85
+ // break
86
+ // }
87
+ // if (!page.map) {
88
+ // break
89
+ // }
90
+ // count += page.length
91
+ // nextPage = getNextPage(response.headers.get('Link'))
92
+ // }
93
+ // return count
94
+ // }
95
+
96
+ async function accountDetails(
97
+ handle: string,
98
+ logError: (x: string) => void
99
+ ): Promise<string> {
100
+ let page
101
+ try {
102
+ let response = await fetch(
103
+ `https://huggingface.co/api/users/${handle}/overview`
104
+ )
105
+
106
+ if (response.status !== 200) {
107
+ throw new Error('HTTP request failed')
108
+ }
109
+ let page = await response.json()
110
+ return page?.details ?? ''
111
+ } catch (e) {
112
+ logError(`Error while retrieving details for ${handle}.`)
113
+ }
114
+ return ''
115
+ }
116
+
117
+ async function accountFofs(
118
+ handle: string,
119
+ setProgress: (x: Array<number>) => void,
120
+ setFollows: (x: Array<AccountDetails>) => void,
121
+ logError: (x: string) => void
122
+ ): Promise<void> {
123
+ const directFollows = await accountFollows(handle, 2000, logError)
124
+ setProgress([0, directFollows.length])
125
+ let progress = 0
126
+
127
+ const directFollowIds = new Set(directFollows.map(({ user }) => user))
128
+ directFollowIds.add(handle)
129
+
130
+ const indirectFollowLists: Array<Array<AccountDetails>> = []
131
+
132
+ const updateList = debounce(() => {
133
+ let indirectFollows: Array<AccountDetails> = [].concat(
134
+ [],
135
+ ...indirectFollowLists
136
+ )
137
+ const indirectFollowMap = new Map()
138
+
139
+ indirectFollows
140
+ .filter(
141
+ // exclude direct follows
142
+ ({ user }) => !directFollowIds.has(user)
143
+ )
144
+ .map((account) => {
145
+ const acct = account.user
146
+ if (indirectFollowMap.has(acct)) {
147
+ const otherAccount = indirectFollowMap.get(acct)
148
+ account.followed_by = new Set([
149
+ ...Array.from(account.followed_by.values()),
150
+ ...otherAccount.followed_by,
151
+ ])
152
+ }
153
+ indirectFollowMap.set(acct, account)
154
+ })
155
+
156
+ const list = Array.from(indirectFollowMap.values()).sort((a, b) => {
157
+ if (a.followed_by.size != b.followed_by.size) {
158
+ return b.followed_by.size - a.followed_by.size
159
+ }
160
+ return b.followers_count - a.followers_count
161
+ })
162
+
163
+ setFollows(list)
164
+ }, 2000)
165
+
166
+ await Promise.all(
167
+ directFollows.map(async ({ user }) => {
168
+ const follows = await accountFollows(user, 200, logError)
169
+ progress++
170
+ setProgress([progress, directFollows.length])
171
+ indirectFollowLists.push(
172
+ follows.map((account) => ({ ...account, followed_by: new Set([user]) }))
173
+ )
174
+ updateList()
175
+ })
176
+ )
177
+
178
+ updateList.flush()
179
+ }
180
+
181
+ function getNextPage(linkHeader: string | null): string | null {
182
+ if (!linkHeader) {
183
+ return null
184
+ }
185
+ // Example header:
186
+ // Link: <https://mastodon.example/api/v1/accounts/1/follows?limit=2&max_id=7628164>; rel="next", <https://mastodon.example/api/v1/accounts/1/follows?limit=2&since_id=7628165>; rel="prev"
187
+ const match = linkHeader.match(/<(.+)>; rel="next"/)
188
+ if (match && match.length > 0) {
189
+ return match[1]
190
+ }
191
+ return null
192
+ }
193
+
194
+ function matchesSearch(account: AccountDetails, search: string): boolean {
195
+ if (/^\s*$/.test(search)) {
196
+ return true
197
+ }
198
+ const sanitizedSearch = search.replace(/^\s+|\s+$/, '').toLocaleLowerCase()
199
+ if (account.user.toLocaleLowerCase().includes(sanitizedSearch)) {
200
+ return true
201
+ }
202
+ if (account.fullname.toLocaleLowerCase().includes(sanitizedSearch)) {
203
+ return true
204
+ }
205
+ return false
206
+ }
207
+
208
+ export function Content({}) {
209
+ const [handle, setHandle] = useState('')
210
+ const [follows, setFollows] = useState<Array<AccountDetails>>([])
211
+ const [isLoading, setLoading] = useState(false)
212
+ const [isDone, setDone] = useState(false)
213
+ const [[numLoaded, totalToLoad], setProgress] = useState<Array<number>>([
214
+ 0, 0,
215
+ ])
216
+ const [errors, setErrors] = useState<Array<string>>([])
217
+
218
+ async function search(handle: string) {
219
+ setErrors([])
220
+ setLoading(true)
221
+ setDone(false)
222
+ setFollows([])
223
+ setProgress([0, 0])
224
+ await accountFofs(handle, setProgress, setFollows, (error) =>
225
+ setErrors((e) => [...e, error])
226
+ )
227
+ setLoading(false)
228
+ setDone(true)
229
+ }
230
+
231
+ return (
232
+ <section className="bg-gray-50 dark:bg-gray-800" id="searchForm">
233
+ <div className="px-4 py-8 mx-auto space-y-12 lg:space-y-20 lg:py-24 max-w-screen-xl">
234
+ <form
235
+ onSubmit={(e) => {
236
+ search(handle)
237
+ e.preventDefault()
238
+ return false
239
+ }}
240
+ >
241
+ <div className="form-group mb-6 text-4xl lg:ml-16">
242
+ <label
243
+ htmlFor="huggingFaceHandle"
244
+ className="form-label inline-block mb-2 text-gray-700 dark:text-gray-200"
245
+ >
246
+ Your Hugging Face username:
247
+ </label>
248
+ <input
249
+ type="text"
250
+ value={handle}
251
+ onChange={(e) => setHandle(e.target.value)}
252
+ className="form-control
253
+ block
254
+ w-80
255
+ px-3
256
+ py-1.5
257
+ text-base
258
+ font-normal
259
+ text-gray-700
260
+ bg-white bg-clip-padding
261
+ border border-solid border-gray-300
262
+ rounded
263
+ transition
264
+ ease-in-out
265
+ m-0
266
+ focus:text-gray-900 focus:bg-white focus:border-green-600 focus:outline-none
267
+ dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-gray-200 dark:focus:bg-gray-900 dark:focus:text-gray-200
268
+ "
269
+ id="huggingFaceHandle"
270
+ aria-describedby="huggingFaceHandleHelp"
271
+ placeholder="merve"
272
+ />
273
+
274
+ <button
275
+ type="submit"
276
+ className="
277
+ px-6
278
+ py-2.5
279
+ bg-green-600
280
+ text-white
281
+ font-medium
282
+ text-xs
283
+ leading-tight
284
+ uppercase
285
+ rounded
286
+ shadow-md
287
+ hover:bg-green-700 hover:shadow-lg
288
+ focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0
289
+ active:bg-green-800 active:shadow-lg
290
+ transition
291
+ duration-150
292
+ ease-in-out"
293
+ >
294
+ Search
295
+ <Spinner
296
+ visible={isLoading}
297
+ className="w-4 h-4 ml-2 fill-white"
298
+ />
299
+ </button>
300
+
301
+ {isLoading ? (
302
+ <p className="text-sm dark:text-gray-400">
303
+ Loaded {numLoaded} of {totalToLoad}...
304
+ </p>
305
+ ) : null}
306
+
307
+ {isDone && follows.length === 0 ? (
308
+ <div
309
+ className="flex p-4 mt-4 max-w-full sm:max-w-xl text-sm text-gray-700 bg-gray-100 rounded-lg dark:bg-gray-700 dark:text-gray-300"
310
+ role="alert"
311
+ >
312
+ <svg
313
+ aria-hidden="true"
314
+ className="flex-shrink-0 inline w-5 h-5 mr-3"
315
+ fill="currentColor"
316
+ viewBox="0 0 20 20"
317
+ xmlns="http://www.w3.org/2000/svg"
318
+ >
319
+ <path
320
+ fill-rule="evenodd"
321
+ d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
322
+ clip-rule="evenodd"
323
+ ></path>
324
+ </svg>
325
+ <span className="sr-only">Info</span>
326
+ <div>
327
+ <span className="font-medium">No results found.</span> Please
328
+ double check for typos in the username, and ensure that you
329
+ follow at least a few people to seed the search. Otherwise,
330
+ try again later as Hugging Face may throttle requests.
331
+ </div>
332
+ </div>
333
+ ) : null}
334
+ </div>
335
+ </form>
336
+
337
+ {isDone || follows.length > 0 ? <Results follows={follows} /> : null}
338
+
339
+ <ErrorLog errors={errors} />
340
+ </div>
341
+ </section>
342
+ )
343
+ }
344
+
345
+ const AccountDetails = memo(({ account }: { account: AccountDetails }) => {
346
+ const {
347
+ avatarUrl,
348
+ fullname,
349
+ user,
350
+ followed_by,
351
+ // followers_count,
352
+ // details
353
+ } = account
354
+ // let formatter = Intl.NumberFormat('en', { notation: 'compact' })
355
+ // let numFollowers = formatter.format(followers_count)
356
+
357
+ const [expandedFollowers, setExpandedFollowers] = useState(false)
358
+
359
+ return (
360
+ <li className="px-4 py-3 pb-7 sm:px-0 sm:py-4">
361
+ <div className="flex flex-col gap-4 sm:flex-row">
362
+ <div className="flex-shrink-0 m-auto">
363
+ {/* eslint-disable-next-line @next/next/no-img-element */}
364
+ <img
365
+ className="w-16 h-16 sm:w-8 sm:h-8 rounded-full"
366
+ src={avatarUrl}
367
+ alt={fullname}
368
+ />
369
+ </div>
370
+ <div className="flex-1 min-w-0">
371
+ <p className="text-sm font-medium text-gray-900 truncate dark:text-white">
372
+ {fullname}
373
+ </p>
374
+ {/* <div className="flex flex-col sm:flex-row text-sm text-gray-500 dark:text-gray-400">
375
+ <span className="truncate">{user}</span>
376
+ <span className="sm:inline hidden whitespace-pre"> | </span>
377
+ <span>{numFollowers} followers</span>
378
+ </div> */}
379
+ {/* <br />
380
+ <small className="text-sm dark:text-gray-200">{details}</small> */}
381
+ <br />
382
+ <small className="text-xs text-gray-800 dark:text-gray-400">
383
+ Followed by{' '}
384
+ {followed_by.size < 9 || expandedFollowers ? (
385
+ Array.from<string>(followed_by.values()).map((handle, idx) => (
386
+ <React.Fragment key={handle}>
387
+ <span className="font-semibold">
388
+ {handle.replace(/@.+/, '')}
389
+ </span>
390
+ {idx === followed_by.size - 1 ? '.' : ', '}
391
+ </React.Fragment>
392
+ ))
393
+ ) : (
394
+ <>
395
+ <button
396
+ onClick={() => setExpandedFollowers(true)}
397
+ className="font-semibold"
398
+ >
399
+ {followed_by.size} of your contacts
400
+ </button>
401
+ .
402
+ </>
403
+ )}
404
+ </small>
405
+ </div>
406
+ <div className="inline-flex m-auto text-base font-semibold text-gray-900 dark:text-white">
407
+ <a
408
+ href={`https://huggingface.co/${user}`}
409
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
410
+ target="_blank"
411
+ rel="noreferrer"
412
+ >
413
+ Follow
414
+ </a>
415
+ </div>
416
+ </div>
417
+ </li>
418
+ )
419
+ })
420
+ AccountDetails.displayName = 'AccountDetails'
421
+
422
+ function ErrorLog({ errors }: { errors: Array<string> }) {
423
+ const [expanded, setExpanded] = useState(false)
424
+ return (
425
+ <>
426
+ {errors.length > 0 ? (
427
+ <div className="text-sm text-gray-600 dark:text-gray-200 border border-solid border-gray-200 dark:border-gray-700 rounded p-4 max-w-4xl mx-auto">
428
+ Found{' '}
429
+ <button className="font-bold" onClick={() => setExpanded(!expanded)}>
430
+ {errors.length} warnings
431
+ </button>
432
+ {expanded ? ':' : '.'}
433
+ {expanded
434
+ ? errors.map((err) => (
435
+ <p key={err} className="text-xs">
436
+ {err}
437
+ </p>
438
+ ))
439
+ : null}
440
+ </div>
441
+ ) : null}
442
+ </>
443
+ )
444
+ }
445
+
446
+ function Results({ follows }: { follows: Array<AccountDetails> }) {
447
+ let [search, setSearch] = useState<string>('')
448
+ const [isLoading, setLoading] = useState(false)
449
+ const updateSearch = useRef(
450
+ debounce((s: string) => {
451
+ setLoading(false)
452
+ setSearch(s)
453
+ }, 1500)
454
+ ).current
455
+
456
+ follows = follows.filter((acc) => matchesSearch(acc, search)).slice(0, 500)
457
+
458
+ return (
459
+ <div className="flex-col lg:flex items-center justify-center">
460
+ <div className="max-w-4xl">
461
+ <div className="w-full mb-4 dark:text-gray-200">
462
+ <label>
463
+ <div className="mb-2">
464
+ <Spinner
465
+ visible={isLoading}
466
+ className="w-4 h-4 mr-1 fill-gray-400"
467
+ />
468
+ Search:
469
+ </div>
470
+ <SearchInput
471
+ onChange={(s) => {
472
+ setLoading(true)
473
+ updateSearch(s)
474
+ }}
475
+ />
476
+ </label>
477
+ </div>
478
+ <div className="content-center px-2 sm:px-8 py-4 bg-white border rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700">
479
+ <div className="flow-root">
480
+ {follows.length === 0 ? (
481
+ <p className="text-gray-700 dark:text-gray-200">
482
+ No results found.
483
+ </p>
484
+ ) : null}
485
+ <ul
486
+ role="list"
487
+ className="divide-y divide-gray-200 dark:divide-gray-700"
488
+ >
489
+ {follows.map((account) => (
490
+ <AccountDetails key={account.user} account={account} />
491
+ ))}
492
+ </ul>
493
+ </div>
494
+ </div>
495
+ </div>
496
+ </div>
497
+ )
498
+ }
499
+
500
+ function SearchInput({ onChange }: { onChange: (s: string) => void }) {
501
+ let [search, setSearchInputValue] = useState<string>('')
502
+ return (
503
+ <input
504
+ type="text"
505
+ placeholder="Loubna"
506
+ value={search}
507
+ onChange={(e) => {
508
+ setSearchInputValue(e.target.value)
509
+ onChange(e.target.value)
510
+ }}
511
+ className="
512
+ form-control
513
+ block
514
+ w-80
515
+ px-3
516
+ py-1.5
517
+ text-base
518
+ font-normal
519
+ text-gray-700
520
+ bg-white bg-clip-padding
521
+ border border-solid border-gray-300
522
+ rounded
523
+ transition
524
+ ease-in-out
525
+ m-0
526
+ focus:text-gray-900 focus:bg-white focus:border-green-600 focus:outline-none
527
+ dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-gray-200 dark:focus:bg-gray-900 dark:focus:text-gray-200"
528
+ />
529
+ )
530
+ }
components/FAQ.tsx ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react'
2
+ export function FAQ({}) {
3
+ return (
4
+ <section className="bg-white dark:bg-gray-900 pt-12">
5
+ <div
6
+ className="max-w-screen-xl px-4 pb-8 mx-auto lg:pb-24 lg:px-6"
7
+ id="faq"
8
+ >
9
+ <h2 className="mb-6 text-3xl font-extrabold tracking-tight text-center text-gray-900 lg:mb-8 lg:text-3xl dark:text-white">
10
+ Frequently asked questions
11
+ </h2>
12
+ <div className="max-w-screen-md mx-auto">
13
+ <div
14
+ id="accordion-flush"
15
+ data-accordion="collapse"
16
+ data-active-classes="bg-white dark:bg-gray-900 text-gray-900 dark:text-white"
17
+ data-inactive-classes="text-gray-500 dark:text-gray-400"
18
+ >
19
+ <FAQItem
20
+ defaultSelected
21
+ title="How does Followgraph for Hugging Face work?"
22
+ >
23
+ Followgraph looks up all the people you follow on Hugging Face,
24
+ and then the people <em>they</em> follow. Then it sorts them by
25
+ the number of mutuals, or otherwise by how popular those accounts
26
+ are.
27
+ <br />
28
+ It then shows the list with Hugging Face links to follow them.
29
+ </FAQItem>
30
+
31
+ <FAQItem title="Do I need to grant Followgraph any permissions?">
32
+ Not at all! Followgraph uses public APIs to fetch potential people
33
+ you can follow on Hugging Face. In fact, it only does
34
+ inauthenticated network requests.
35
+ </FAQItem>
36
+
37
+ <FAQItem title="Help! The search got stuck.">
38
+ Don&apos;t worry. The list of suggestions will load in 30 seconds
39
+ or so. Sometimes it gets stuck because one or more of the queries
40
+ made to Hugging Face time out. This is not a problem, because the
41
+ rest of the queries will work as expected.
42
+ </FAQItem>
43
+
44
+ <FAQItem title="Why don't I see any results?">
45
+ Make sure you have no typos in the Hugging Face handle, and make
46
+ sure you follow at least a few people to seed the search.
47
+ </FAQItem>
48
+
49
+ <FAQItem title="How can I contribute with suggestions?">
50
+ Click the &quot;Fork me on Github&quot; link on the top right, and
51
+ open up an issue.
52
+ </FAQItem>
53
+
54
+ <FAQItem title="Why is this not a core Hugging Face feature?">
55
+ Well, maybe it should be. In the meantime, you can use this
56
+ website.
57
+ </FAQItem>
58
+
59
+ <FAQItem title="Can I download the list of accounts as CSV?">
60
+ While it would be a useful feature, Followgraph does <em>not</em>{' '}
61
+ plan to offer this functionality as it would facilitate inorganic
62
+ and potentially malicious behaviour.
63
+ </FAQItem>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </section>
68
+ )
69
+ }
70
+
71
+ function FAQItem({
72
+ defaultSelected,
73
+ title,
74
+ children,
75
+ }: {
76
+ defaultSelected?: boolean
77
+ title: string
78
+ children: React.ReactNode
79
+ }) {
80
+ const [selected, setSelected] = useState(defaultSelected)
81
+ return (
82
+ <>
83
+ <h3 id="accordion-flush-heading-1">
84
+ <button
85
+ type="button"
86
+ onClick={() => setSelected(!selected)}
87
+ className={`flex items-center justify-between w-full py-5 font-medium text-left text-gray-${
88
+ selected ? 900 : 500
89
+ } bg-white border-b border-gray-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-${
90
+ selected ? 200 : 400
91
+ }`}
92
+ data-accordion-target="#accordion-flush-body-1"
93
+ aria-expanded="true"
94
+ aria-controls="accordion-flush-body-1"
95
+ >
96
+ <span>{title}</span>
97
+ <svg
98
+ data-accordion-icon
99
+ className="w-6 h-6 rotate-180 shrink-0"
100
+ fill="currentColor"
101
+ viewBox="0 0 20 20"
102
+ xmlns="http://www.w3.org/2000/svg"
103
+ >
104
+ <path
105
+ fillRule="evenodd"
106
+ d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
107
+ clipRule="evenodd"
108
+ />
109
+ </svg>
110
+ </button>
111
+ </h3>
112
+ {selected ? (
113
+ <div
114
+ id="accordion-flush-body-1"
115
+ aria-labelledby="accordion-flush-heading-1"
116
+ >
117
+ <div className="py-5 border-b border-gray-200 dark:border-gray-700 dark:text-gray-300">
118
+ {children}
119
+ </div>
120
+ </div>
121
+ ) : null}
122
+ </>
123
+ )
124
+ }
components/Footer.tsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from 'next/link'
2
+ import React from 'react'
3
+ export default function Footer({}) {
4
+ return (
5
+ <footer className="bg-white dark:bg-gray-800">
6
+ <div className="max-w-screen-xl p-4 py-6 mx-auto lg:py-16 md:p-8 lg:p-10">
7
+ <hr className="my-6 border-gray-200 sm:mx-auto dark:border-gray-700 lg:my-8" />
8
+ <div className="text-center">
9
+ <div className="mb-5 lg:text-2xl font-semibold text-gray-700 dark:text-white text-lg">
10
+ Followgraph for Hugging Face, forked from&nbsp;
11
+ <Link
12
+ href="https://github.com/gabipurcaru/followgraph"
13
+ target="_blank"
14
+ rel="noreferrer"
15
+ className="font-bold text-gray-900 dark:text-gray-400"
16
+ >
17
+ gabipurcaru/followgraph
18
+ </Link>
19
+ </div>
20
+ <span className="block text-sm text-center text-gray-500 dark:text-gray-400">
21
+ Built with{' '}
22
+ <Link
23
+ href="https://flowbite.com"
24
+ className="text-purple-600 hover:underline dark:text-purple-500"
25
+ rel="nofollow noopener noreferrer"
26
+ >
27
+ Flowbite
28
+ </Link>{' '}
29
+ and{' '}
30
+ <Link
31
+ href="https://tailwindcss.com"
32
+ className="text-purple-600 hover:underline dark:text-purple-500"
33
+ rel="nofollow noopener noreferrer"
34
+ >
35
+ Tailwind CSS
36
+ </Link>
37
+ .
38
+ </span>
39
+ </div>
40
+ </div>
41
+ </footer>
42
+ )
43
+ }
components/Header.tsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from 'next/link'
2
+ import React from 'react'
3
+
4
+ export default function Header({ selected }: { selected: 'home' }) {
5
+ return (
6
+ <header className="fixed w-full">
7
+ <nav className="bg-white border-gray-200 py-2.5 dark:bg-gray-900">
8
+ <div className="flex flex-wrap items-center justify-between max-w-screen-xl px-4 mx-auto">
9
+ <Logo />
10
+
11
+ <div
12
+ className="items-center justify-between hidden w-full lg:flex lg:w-auto lg:order-1"
13
+ id="mobile-menu-2"
14
+ >
15
+ <ul className="flex flex-col mt-4 font-medium lg:flex-row lg:space-x-8 lg:mt-0">
16
+ <MenuItem link="/" selected={selected == 'home'}>
17
+ Home
18
+ </MenuItem>
19
+ <MenuItem link="/#faq">FAQ</MenuItem>
20
+ <MenuItem link="https://github.com/severo/hf-followgraph">
21
+ Fork me on GitHub
22
+ </MenuItem>
23
+ </ul>
24
+ </div>
25
+ </div>
26
+ </nav>
27
+ </header>
28
+ )
29
+
30
+ function Logo({}) {
31
+ return (
32
+ <Link href="/" className="flex items-center">
33
+ <svg
34
+ className="w-12 h-12 mr-4 dark:fill-white"
35
+ xmlns="http://www.w3.org/2000/svg"
36
+ shape-rendering="geometricPrecision"
37
+ text-rendering="geometricPrecision"
38
+ image-rendering="optimizeQuality"
39
+ fill-rule="evenodd"
40
+ clip-rule="evenodd"
41
+ viewBox="0 0 512 342.68"
42
+ >
43
+ <path d="M3.59 300.55a3.59 3.59 0 0 1-3.59-3.6c0-1.02.14-2.03.38-3.03 5.77-45.65 41.51-57.84 66.87-64.42 12.69-3.29 44.26-15.82 31.68-33.04-7.05-9.67-13.44-16.47-19.83-26.68-4.61-6.81-7.04-12.88-7.04-17.75 0-5.19 2.75-11.27 8.26-12.64-.73-10.45-.97-24.2-.48-35.39 1.75-19.2 15.52-33.35 33.31-39.62 7.05-2.68 3.64-13.38 11.42-13.62 18.24-.49 48.14 15.07 59.82 27.71 6.81 7.3 11.18 17.02 11.91 29.91l-.73 32.22c3.4.98 5.59 3.16 6.57 6.56.97 3.89 0 9.25-3.41 16.79 0 .24-.24.24-.24.48-7.51 12.37-15.33 20.56-23.92 32.03-3.84 5.11-3.09 10.01.1 14.41-4.48 2.62-8.85 5.62-13.06 9.16-16.76 14.07-29.68 35.08-34.13 68.61l-.5 2.9c-.24 1.9-.38 3.48-.38 4.66 0 1.48.13 2.93.37 4.35H3.59zM428 174.68c46.41 0 84 37.62 84 84 0 46.4-37.62 84-84 84-46.4 0-84-37.62-84-84 0-46.41 37.61-84 84-84zm-13.25 49.44c-.03-3.91-.39-6.71 4.46-6.64l15.7.19c5.07-.03 6.42 1.58 6.36 6.33v21.43h21.3c3.91-.04 6.7-.4 6.63 4.45l-.19 15.71c.03 5.07-1.58 6.42-6.32 6.36h-21.42v21.41c.06 4.75-1.29 6.36-6.36 6.33l-15.7.18c-4.85.08-4.49-2.72-4.46-6.63v-21.29h-21.43c-4.75.06-6.35-1.29-6.32-6.36l-.19-15.71c-.08-4.85 2.72-4.49 6.62-4.45h21.32v-21.31zm-261.98 76.47c-2.43 0-4.39-1.96-4.39-4.39 0-1.25.17-2.48.47-3.7 7.03-55.71 40.42-67.83 71.33-75.78 14.84-3.82 44.44-18.71 40.85-37.91-7.49-6.94-14.92-16.53-16.21-30.83l-.9.02c-2.07-.03-4.08-.5-5.95-1.56-4.13-2.35-6.4-6.85-7.49-11.99-2.29-15.67-2.86-23.67 5.49-27.17l.07-.02c-1.04-19.34 2.23-47.79-17.63-53.8 39.21-48.44 84.41-74.8 118.34-31.7 37.81 1.98 54.67 55.54 31.19 85.52h-.99c8.36 3.5 7.1 12.49 5.49 27.17-1.09 5.14-3.36 9.64-7.49 11.99-1.87 1.06-3.87 1.53-5.95 1.56l-.9-.02c-1.29 14.3-8.74 23.89-16.23 30.83-1.01 5.43.63 10.52 3.84 15.11-14.05 17.81-22.44 40.31-22.44 64.76 0 14.89 3.11 29.07 8.73 41.91H152.77z" />
44
+ </svg>
45
+ <span className="self-center text-xl font-semibold whitespace-nowrap dark:text-white">
46
+ Followgraph for Hugging Face
47
+ </span>
48
+ </Link>
49
+ )
50
+ }
51
+ }
52
+
53
+ function MenuItem({
54
+ link,
55
+ children,
56
+ selected,
57
+ }: {
58
+ link: string
59
+ children: string | React.ReactElement
60
+ selected?: boolean
61
+ }) {
62
+ return (
63
+ <li>
64
+ {selected ? (
65
+ <Link
66
+ href={link}
67
+ className="block py-2 pl-3 pr-4 text-white bg-purple-700 rounded lg:bg-transparent lg:text-purple-700 lg:p-0 dark:text-white"
68
+ aria-current="page"
69
+ >
70
+ {children}
71
+ </Link>
72
+ ) : (
73
+ <Link
74
+ href={link}
75
+ className="block py-2 pl-3 pr-4 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-purple-700 lg:p-0 dark:text-gray-300 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700"
76
+ >
77
+ {children}
78
+ </Link>
79
+ )}
80
+ </li>
81
+ )
82
+ }
components/Hero.tsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Image from 'next/image'
2
+ import React from 'react'
3
+
4
+ export default function Hero({}) {
5
+ return (
6
+ <section className="bg-white dark:bg-gray-900">
7
+ <div className="grid max-w-screen-xl px-4 pt-20 pb-8 mx-auto lg:gap-8 xl:gap-0 lg:py-16 lg:grid-cols-12 lg:pt-28 lg:px-20">
8
+ <div className="mr-auto place-self-center lg:col-span-7">
9
+ <h1 className="max-w-2xl mb-4 text-4xl font-extrabold leading-none tracking-tight md:text-5xl xl:text-6xl dark:text-white">
10
+ Find awesome people <br /> on Hugging Face.
11
+ </h1>
12
+ <p className="max-w-2xl mb-6 font-light text-gray-500 lg:mb-8 md:text-lg lg:text-xl dark:text-gray-400">
13
+ This tool allows you to expand your connection graph and find new
14
+ people to follow. It works by looking up your &quot;follows&apos;
15
+ follows&quot;. <br /> <br />
16
+ Your extended network is a treasure trove!
17
+ </p>
18
+
19
+ <div className="space-y-4 sm:flex sm:space-y-0 sm:space-x-4 ">
20
+ <a
21
+ href="https://github.com/severo/hf-followgraph"
22
+ className="inline-flex items-center justify-center w-full px-5 py-3 text-sm font-medium text-center text-gray-900 border border-gray-200 rounded-lg sm:w-auto hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 dark:text-white dark:border-gray-700 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
23
+ >
24
+ <svg
25
+ className="w-4 h-4 mr-2 text-gray-500 dark:fill-gray-300"
26
+ xmlns="http://www.w3.org/2000/svg"
27
+ viewBox="0 0 496 512"
28
+ >
29
+ {/* Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) */}
30
+ <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
31
+ </svg>{' '}
32
+ View on GitHub
33
+ </a>
34
+
35
+ <a
36
+ href="#searchForm"
37
+ className="
38
+ inline-flex items-center justify-center w-full
39
+ px-5 py-3 text-sm font-medium text-center
40
+ text-gray-900 border
41
+ border-gray-200 rounded-lg sm:w-auto hover:bg-green-400
42
+ focus:ring-4
43
+ focus:ring-gray-100 dark:text-gray-200
44
+ bg-green-500
45
+
46
+ dark:bg-green-700 dark:hover:bg-green-600
47
+ dark:focus:ring-gray-800
48
+ dark:border-gray-700 "
49
+ >
50
+ <svg
51
+ className="w-4 h-4 mr-2 dark:fill-gray-300"
52
+ xmlns="http://www.w3.org/2000/svg"
53
+ viewBox="0 0 512 512"
54
+ >
55
+ {/* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
56
+ <path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352c79.5 0 144-64.5 144-144s-64.5-144-144-144S64 128.5 64 208s64.5 144 144 144z" />
57
+ </svg>
58
+ Use now
59
+ </a>
60
+ </div>
61
+ </div>
62
+ <div className="hidden lg:mt-0 lg:col-span-5 lg:flex">
63
+ <Image
64
+ src="/hero.png"
65
+ alt="Picture of people at a party"
66
+ width={500}
67
+ height={500}
68
+ priority
69
+ />
70
+ </div>
71
+ </div>
72
+ </section>
73
+ )
74
+ }
components/Spinner.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ export function Spinner({
3
+ visible,
4
+ className,
5
+ }: {
6
+ visible: boolean
7
+ className: string
8
+ }) {
9
+ if (!visible) {
10
+ return null
11
+ }
12
+ return (
13
+ <svg
14
+ className={className + ' animate-spin inline'}
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ viewBox="0 0 512 512"
17
+ >
18
+ {/*! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
19
+ <path d="M304 48c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zm0 416c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zM48 304c26.5 0 48-21.5 48-48s-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48zm464-48c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zM142.9 437c18.7-18.7 18.7-49.1 0-67.9s-49.1-18.7-67.9 0s-18.7 49.1 0 67.9s49.1 18.7 67.9 0zm0-294.2c18.7-18.7 18.7-49.1 0-67.9S93.7 56.2 75 75s-18.7 49.1 0 67.9s49.1 18.7 67.9 0zM369.1 437c18.7 18.7 49.1 18.7 67.9 0s18.7-49.1 0-67.9s-49.1-18.7-67.9 0s-18.7 49.1 0 67.9z" />
20
+ </svg>
21
+ )
22
+ }
next.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ }
5
+
6
+ module.exports = nextConfig
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "hf-followgraph",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint",
10
+ "prettier": "prettier --check . --config .prettierrc",
11
+ "prettier:fix": "prettier --write . --config .prettierrc"
12
+ },
13
+ "dependencies": {
14
+ "@next/font": "13.0.7",
15
+ "@types/node": "18.11.17",
16
+ "@types/react": "18.0.26",
17
+ "@types/react-dom": "18.0.9",
18
+ "@vercel/analytics": "^0.1.6",
19
+ "debounce": "^1.2.1",
20
+ "eslint": "8.30.0",
21
+ "eslint-config-next": "13.0.7",
22
+ "next": "13.0.7",
23
+ "node-fetch": "^3.3.0",
24
+ "oauth": "^0.10.0",
25
+ "react": "18.2.0",
26
+ "react-dom": "18.2.0",
27
+ "react-paginate": "^8.1.4",
28
+ "typescript": "4.9.4"
29
+ },
30
+ "devDependencies": {
31
+ "@tailwindcss/typography": "^0.5.8",
32
+ "autoprefixer": "^10.4.13",
33
+ "eslint-config-prettier": "^8.5.0",
34
+ "postcss": "^8.4.20",
35
+ "prettier": "2.8.1",
36
+ "tailwindcss": "^3.2.4"
37
+ }
38
+ }
pages/_app.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import '../styles/globals.css'
2
+ import type { AppProps } from 'next/app'
3
+
4
+ export default function App({ Component, pageProps }: AppProps) {
5
+ return (
6
+ <>
7
+ <Component {...pageProps} />
8
+ </>
9
+ )
10
+ }
pages/_document.tsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Html, Head, Main, NextScript } from 'next/document'
2
+
3
+ export default function Document() {
4
+ return (
5
+ <Html lang="en">
6
+ <Head />
7
+ <body>
8
+ <Main />
9
+ <NextScript />
10
+ </body>
11
+ </Html>
12
+ )
13
+ }
pages/index.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Content } from './../components/Content'
2
+ import { FAQ } from './../components/FAQ'
3
+ import Footer from './../components/Footer'
4
+ import Hero from './../components/Hero'
5
+ import Header from './../components/Header'
6
+ import Head from 'next/head'
7
+
8
+ export default function Home() {
9
+ return (
10
+ <>
11
+ <Head>
12
+ <title>Followgraph for Hugging Face</title>
13
+ <meta
14
+ name="description"
15
+ content="Find people to follow on Hugging Face by expanding your follow graph."
16
+ />
17
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
18
+ <script type="application/ld+json">
19
+ {`{
20
+ "@context": "https://schema.org",
21
+ "@type": "WebSite",
22
+ "url": "https://followgraph.vercel.app/",
23
+ "image": {
24
+ "@type": "ImageObject",
25
+ "@id": "https://followgraph.vercel.app/#/schema/ImageObject/FollowGraphThumbnail",
26
+ "url": "/ldjson-logo.jpg",
27
+ "contentUrl": "/ldjson-logo.jpg",
28
+ "caption": "Followgraph for Hugging Face",
29
+ "width": 345,
30
+ "height": 345
31
+ }
32
+ }`}
33
+ </script>
34
+ <link rel="icon" href="/favicon.ico" />
35
+ </Head>
36
+ <div>
37
+ <Header selected="home" />
38
+ <Hero />
39
+ <Content />
40
+ <FAQ />
41
+
42
+ <Footer />
43
+ </div>
44
+ </>
45
+ )
46
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
public/android-chrome-192x192.png ADDED
public/android-chrome-512x512.png ADDED
public/apple-touch-icon.png ADDED
public/favicon-16x16.png ADDED
public/favicon-32x32.png ADDED
public/favicon.ico ADDED
public/googledfe269244d136c58.html ADDED
@@ -0,0 +1 @@
 
 
1
+ google-site-verification: googledfe269244d136c58.html
public/hero.png ADDED
public/ldjson-logo.jpg ADDED
public/site.webmanifest ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "",
3
+ "short_name": "",
4
+ "icons": [
5
+ {
6
+ "src": "/android-chrome-192x192.png",
7
+ "sizes": "192x192",
8
+ "type": "image/png"
9
+ },
10
+ {
11
+ "src": "/android-chrome-512x512.png",
12
+ "sizes": "512x512",
13
+ "type": "image/png"
14
+ }
15
+ ],
16
+ "theme_color": "#ffffff",
17
+ "background_color": "#ffffff",
18
+ "display": "standalone"
19
+ }
styles/Home.module.css ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .main {
2
+ display: flex;
3
+ flex-direction: column;
4
+ justify-content: space-between;
5
+ align-items: center;
6
+ padding: 6rem;
7
+ min-height: 100vh;
8
+ }
9
+
10
+ .description {
11
+ display: inherit;
12
+ justify-content: inherit;
13
+ align-items: inherit;
14
+ font-size: 0.85rem;
15
+ max-width: var(--max-width);
16
+ width: 100%;
17
+ z-index: 2;
18
+ font-family: var(--font-mono);
19
+ }
20
+
21
+ .description a {
22
+ display: flex;
23
+ justify-content: center;
24
+ align-items: center;
25
+ gap: 0.5rem;
26
+ }
27
+
28
+ .description p {
29
+ position: relative;
30
+ margin: 0;
31
+ padding: 1rem;
32
+ background-color: rgba(var(--callout-rgb), 0.5);
33
+ border: 1px solid rgba(var(--callout-border-rgb), 0.3);
34
+ border-radius: var(--border-radius);
35
+ }
36
+
37
+ .code {
38
+ font-weight: 700;
39
+ font-family: var(--font-mono);
40
+ }
41
+
42
+ .grid {
43
+ display: grid;
44
+ grid-template-columns: repeat(4, minmax(25%, auto));
45
+ width: var(--max-width);
46
+ max-width: 100%;
47
+ }
48
+
49
+ .card {
50
+ padding: 1rem 1.2rem;
51
+ border-radius: var(--border-radius);
52
+ background: rgba(var(--card-rgb), 0);
53
+ border: 1px solid rgba(var(--card-border-rgb), 0);
54
+ transition: background 200ms, border 200ms;
55
+ }
56
+
57
+ .card span {
58
+ display: inline-block;
59
+ transition: transform 200ms;
60
+ }
61
+
62
+ .card h2 {
63
+ font-weight: 600;
64
+ margin-bottom: 0.7rem;
65
+ }
66
+
67
+ .card p {
68
+ margin: 0;
69
+ opacity: 0.6;
70
+ font-size: 0.9rem;
71
+ line-height: 1.5;
72
+ max-width: 30ch;
73
+ }
74
+
75
+ .center {
76
+ display: flex;
77
+ justify-content: center;
78
+ align-items: center;
79
+ position: relative;
80
+ padding: 4rem 0;
81
+ }
82
+
83
+ .center::before {
84
+ background: var(--secondary-glow);
85
+ border-radius: 50%;
86
+ width: 480px;
87
+ height: 360px;
88
+ margin-left: -400px;
89
+ }
90
+
91
+ .center::after {
92
+ background: var(--primary-glow);
93
+ width: 240px;
94
+ height: 180px;
95
+ z-index: -1;
96
+ }
97
+
98
+ .center::before,
99
+ .center::after {
100
+ content: '';
101
+ left: 50%;
102
+ position: absolute;
103
+ filter: blur(45px);
104
+ transform: translateZ(0);
105
+ }
106
+
107
+ .logo,
108
+ .thirteen {
109
+ position: relative;
110
+ }
111
+
112
+ .thirteen {
113
+ display: flex;
114
+ justify-content: center;
115
+ align-items: center;
116
+ width: 75px;
117
+ height: 75px;
118
+ padding: 25px 10px;
119
+ margin-left: 16px;
120
+ transform: translateZ(0);
121
+ border-radius: var(--border-radius);
122
+ overflow: hidden;
123
+ box-shadow: 0px 2px 8px -1px #0000001a;
124
+ }
125
+
126
+ .thirteen::before,
127
+ .thirteen::after {
128
+ content: '';
129
+ position: absolute;
130
+ z-index: -1;
131
+ }
132
+
133
+ /* Conic Gradient Animation */
134
+ .thirteen::before {
135
+ animation: 6s rotate linear infinite;
136
+ width: 200%;
137
+ height: 200%;
138
+ background: var(--tile-border);
139
+ }
140
+
141
+ /* Inner Square */
142
+ .thirteen::after {
143
+ inset: 0;
144
+ padding: 1px;
145
+ border-radius: var(--border-radius);
146
+ background: linear-gradient(
147
+ to bottom right,
148
+ rgba(var(--tile-start-rgb), 1),
149
+ rgba(var(--tile-end-rgb), 1)
150
+ );
151
+ background-clip: content-box;
152
+ }
153
+
154
+ /* Enable hover only on non-touch devices */
155
+ @media (hover: hover) and (pointer: fine) {
156
+ .card:hover {
157
+ background: rgba(var(--card-rgb), 0.1);
158
+ border: 1px solid rgba(var(--card-border-rgb), 0.15);
159
+ }
160
+
161
+ .card:hover span {
162
+ transform: translateX(4px);
163
+ }
164
+ }
165
+
166
+ @media (prefers-reduced-motion) {
167
+ .thirteen::before {
168
+ animation: none;
169
+ }
170
+
171
+ .card:hover span {
172
+ transform: none;
173
+ }
174
+ }
175
+
176
+ /* Mobile */
177
+ @media (max-width: 700px) {
178
+ .content {
179
+ padding: 4rem;
180
+ }
181
+
182
+ .grid {
183
+ grid-template-columns: 1fr;
184
+ margin-bottom: 120px;
185
+ max-width: 320px;
186
+ text-align: center;
187
+ }
188
+
189
+ .card {
190
+ padding: 1rem 2.5rem;
191
+ }
192
+
193
+ .card h2 {
194
+ margin-bottom: 0.5rem;
195
+ }
196
+
197
+ .center {
198
+ padding: 8rem 0 6rem;
199
+ }
200
+
201
+ .center::before {
202
+ transform: none;
203
+ height: 300px;
204
+ }
205
+
206
+ .description {
207
+ font-size: 0.8rem;
208
+ }
209
+
210
+ .description a {
211
+ padding: 1rem;
212
+ }
213
+
214
+ .description p,
215
+ .description div {
216
+ display: flex;
217
+ justify-content: center;
218
+ position: fixed;
219
+ width: 100%;
220
+ }
221
+
222
+ .description p {
223
+ align-items: center;
224
+ inset: 0 0 auto;
225
+ padding: 2rem 1rem 1.4rem;
226
+ border-radius: 0;
227
+ border: none;
228
+ border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
229
+ background: linear-gradient(
230
+ to bottom,
231
+ rgba(var(--background-start-rgb), 1),
232
+ rgba(var(--callout-rgb), 0.5)
233
+ );
234
+ background-clip: padding-box;
235
+ backdrop-filter: blur(24px);
236
+ }
237
+
238
+ .description div {
239
+ align-items: flex-end;
240
+ pointer-events: none;
241
+ inset: auto 0 0;
242
+ padding: 2rem;
243
+ height: 200px;
244
+ background: linear-gradient(
245
+ to bottom,
246
+ transparent 0%,
247
+ rgb(var(--background-end-rgb)) 40%
248
+ );
249
+ z-index: 1;
250
+ }
251
+ }
252
+
253
+ /* Tablet and Smaller Desktop */
254
+ @media (min-width: 701px) and (max-width: 1120px) {
255
+ .grid {
256
+ grid-template-columns: repeat(2, 50%);
257
+ }
258
+ }
259
+
260
+ @media (prefers-color-scheme: dark) {
261
+ .vercelLogo {
262
+ filter: invert(1);
263
+ }
264
+
265
+ .logo,
266
+ .thirteen img {
267
+ filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
268
+ }
269
+ }
270
+
271
+ @keyframes rotate {
272
+ from {
273
+ transform: rotate(360deg);
274
+ }
275
+ to {
276
+ transform: rotate(0deg);
277
+ }
278
+ }
styles/globals.css ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ html {
6
+ scroll-behavior: smooth;
7
+ }
tailwind.config.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ './pages/**/*.{js,ts,jsx,tsx}',
5
+ './components/**/*.{js,ts,jsx,tsx}',
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [require('@tailwindcss/typography')],
11
+ }
tsconfig.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": false,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "module": "esnext",
12
+ "moduleResolution": "node",
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "jsx": "preserve",
16
+ "incremental": true
17
+ },
18
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19
+ "exclude": ["node_modules"]
20
+ }