Spaces:
Running
Running
copy files
Browse files- .eslintrc.json +3 -0
- .github/workflows/build.yml +27 -0
- .github/workflows/prettier.yml +26 -0
- .gitignore +36 -0
- .prettierrc +6 -0
- Dockerfile +8 -0
- LICENSE.md +21 -0
- components/Content.tsx +530 -0
- components/FAQ.tsx +124 -0
- components/Footer.tsx +43 -0
- components/Header.tsx +82 -0
- components/Hero.tsx +74 -0
- components/Spinner.tsx +22 -0
- next.config.js +6 -0
- package-lock.json +0 -0
- package.json +38 -0
- pages/_app.tsx +10 -0
- pages/_document.tsx +13 -0
- pages/index.tsx +46 -0
- postcss.config.js +6 -0
- public/android-chrome-192x192.png +0 -0
- public/android-chrome-512x512.png +0 -0
- public/apple-touch-icon.png +0 -0
- public/favicon-16x16.png +0 -0
- public/favicon-32x32.png +0 -0
- public/favicon.ico +0 -0
- public/googledfe269244d136c58.html +1 -0
- public/hero.png +0 -0
- public/ldjson-logo.jpg +0 -0
- public/site.webmanifest +19 -0
- styles/Home.module.css +278 -0
- styles/globals.css +7 -0
- tailwind.config.js +11 -0
- tsconfig.json +20 -0
.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'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 "Fork me on Github" 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
|
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 "follows'
|
15 |
+
follows". <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 |
+
}
|