Spaces:
Sleeping
Sleeping
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 |
+
}
|