kopia lustrzana https://github.com/gabipurcaru/followgraph
Add prettier package and apply config to all files (#11)
rodzic
1a368e963f
commit
8141375a2b
|
@ -1,6 +1,3 @@
|
||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["next/core-web-vitals", "prettier"]
|
||||||
"next/core-web-vitals",
|
}
|
||||||
"prettier"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,4 +3,4 @@
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true
|
"singleQuote": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,78 +1,88 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html'
|
||||||
import debounce from 'debounce'
|
import debounce from 'debounce'
|
||||||
|
|
||||||
type AccountDetails = {
|
type AccountDetails = {
|
||||||
id: string, // IMPORTANT: this is int64 so will overflow Javascript's number type
|
id: string // IMPORTANT: this is int64 so will overflow Javascript's number type
|
||||||
acct: string,
|
acct: string
|
||||||
followed_by: Set<string>, // list of handles
|
followed_by: Set<string> // list of handles
|
||||||
};
|
}
|
||||||
|
|
||||||
async function usernameToId(handle: string): Promise<{ id: number, domain: string }> {
|
async function usernameToId(
|
||||||
const match = handle.match(/^(.+)@(.+)$/);
|
handle: string
|
||||||
|
): Promise<{ id: number; domain: string }> {
|
||||||
|
const match = handle.match(/^(.+)@(.+)$/)
|
||||||
if (!match || match.length < 2) {
|
if (!match || match.length < 2) {
|
||||||
throw new Error(`Incorrect handle: ${handle}`);
|
throw new Error(`Incorrect handle: ${handle}`)
|
||||||
}
|
}
|
||||||
const domain = match[2];
|
const domain = match[2]
|
||||||
const username = match[1];
|
const username = match[1]
|
||||||
let response = await fetch(`https://${domain}/api/v1/accounts/lookup?acct=${username}`);
|
let response = await fetch(
|
||||||
|
`https://${domain}/api/v1/accounts/lookup?acct=${username}`
|
||||||
|
)
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
throw new Error('HTTP request failed');
|
throw new Error('HTTP request failed')
|
||||||
}
|
}
|
||||||
const { id } = await response.json();
|
const { id } = await response.json()
|
||||||
return { id, domain };
|
return { id, domain }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDomain(handle: string) {
|
function getDomain(handle: string) {
|
||||||
const match = handle.match(/^(.+)@(.+)$/);
|
const match = handle.match(/^(.+)@(.+)$/)
|
||||||
if (!match || match.length < 2) {
|
if (!match || match.length < 2) {
|
||||||
throw new Error(`Incorrect handle: ${handle}`);
|
throw new Error(`Incorrect handle: ${handle}`)
|
||||||
}
|
}
|
||||||
const domain = match[2];
|
const domain = match[2]
|
||||||
return domain;
|
return domain
|
||||||
}
|
}
|
||||||
|
|
||||||
async function accountFollows(handle: string, limit: number, logError: (x: string) => void): Promise<Array<AccountDetails>> {
|
async function accountFollows(
|
||||||
let id, domain: string;
|
handle: string,
|
||||||
|
limit: number,
|
||||||
|
logError: (x: string) => void
|
||||||
|
): Promise<Array<AccountDetails>> {
|
||||||
|
let id, domain: string
|
||||||
try {
|
try {
|
||||||
({ id, domain } = await usernameToId(handle));
|
;({ id, domain } = await usernameToId(handle))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(`Cannot find handle ${handle}.`);
|
logError(`Cannot find handle ${handle}.`)
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextPage: string | null = `https://${domain}/api/v1/accounts/${id}/following`;
|
let nextPage:
|
||||||
let data: Array<AccountDetails> = [];
|
| string
|
||||||
|
| null = `https://${domain}/api/v1/accounts/${id}/following`
|
||||||
|
let data: Array<AccountDetails> = []
|
||||||
while (nextPage && data.length <= limit) {
|
while (nextPage && data.length <= limit) {
|
||||||
console.log(`Get page: ${nextPage}`);
|
console.log(`Get page: ${nextPage}`)
|
||||||
let response;
|
let response
|
||||||
let page;
|
let page
|
||||||
try {
|
try {
|
||||||
response = await fetch(nextPage);
|
response = await fetch(nextPage)
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
throw new Error('HTTP request failed');
|
throw new Error('HTTP request failed')
|
||||||
}
|
}
|
||||||
page = await response.json();
|
page = await response.json()
|
||||||
console.log(response.statusText);
|
console.log(response.statusText)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(`Error while retrieving followers for ${handle}.`)
|
logError(`Error while retrieving followers for ${handle}.`)
|
||||||
console.log('eeeee', e);
|
console.log('eeeee', e)
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
if (!page.map) {
|
if (!page.map) {
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
page = page.map((entry: AccountDetails) => {
|
page = page.map((entry: AccountDetails) => {
|
||||||
if (entry.acct && !/@/.test(entry.acct)) {
|
if (entry.acct && !/@/.test(entry.acct)) {
|
||||||
// make sure the domain is always there
|
// make sure the domain is always there
|
||||||
entry.acct = `${entry.acct}@${domain}`;
|
entry.acct = `${entry.acct}@${domain}`
|
||||||
};
|
}
|
||||||
return entry;
|
return entry
|
||||||
})
|
})
|
||||||
data = [...data, ...page];
|
data = [...data, ...page]
|
||||||
nextPage = getNextPage(response.headers.get('Link'));
|
nextPage = getNextPage(response.headers.get('Link'))
|
||||||
}
|
}
|
||||||
return data;
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
async function accountFofs(
|
async function accountFofs(
|
||||||
|
@ -81,103 +91,127 @@ async function accountFofs(
|
||||||
setFollows: (x: Array<AccountDetails>) => void,
|
setFollows: (x: Array<AccountDetails>) => void,
|
||||||
logError: (x: string) => void
|
logError: (x: string) => void
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const directFollows = await accountFollows(handle, 2000, logError);
|
const directFollows = await accountFollows(handle, 2000, logError)
|
||||||
setProgress([0, directFollows.length]);
|
setProgress([0, directFollows.length])
|
||||||
let progress = 0;
|
let progress = 0
|
||||||
|
|
||||||
const directFollowIds = new Set(directFollows.map(({ acct }) => acct));
|
const directFollowIds = new Set(directFollows.map(({ acct }) => acct))
|
||||||
directFollowIds.add(handle.replace(/^@/, ''));
|
directFollowIds.add(handle.replace(/^@/, ''))
|
||||||
|
|
||||||
const indirectFollowLists: Array<Array<AccountDetails>> = [];
|
const indirectFollowLists: Array<Array<AccountDetails>> = []
|
||||||
|
|
||||||
const updateList = debounce(() => {
|
const updateList = debounce(() => {
|
||||||
let indirectFollows: Array<AccountDetails> = [].concat([], ...indirectFollowLists);
|
let indirectFollows: Array<AccountDetails> = [].concat(
|
||||||
const indirectFollowMap = new Map();
|
[],
|
||||||
|
...indirectFollowLists
|
||||||
|
)
|
||||||
|
const indirectFollowMap = new Map()
|
||||||
|
|
||||||
indirectFollows.filter(
|
indirectFollows
|
||||||
// exclude direct follows
|
.filter(
|
||||||
({ acct }) => !directFollowIds.has(acct)
|
// exclude direct follows
|
||||||
).map(account => {
|
({ acct }) => !directFollowIds.has(acct)
|
||||||
const acct = account.acct;
|
)
|
||||||
if (indirectFollowMap.has(acct)) {
|
.map((account) => {
|
||||||
const otherAccount = indirectFollowMap.get(acct);
|
const acct = account.acct
|
||||||
account.followed_by = new Set([...Array.from(account.followed_by.values()), ...otherAccount.followed_by]);
|
if (indirectFollowMap.has(acct)) {
|
||||||
}
|
const otherAccount = indirectFollowMap.get(acct)
|
||||||
indirectFollowMap.set(acct, account);
|
account.followed_by = new Set([
|
||||||
});
|
...Array.from(account.followed_by.values()),
|
||||||
|
...otherAccount.followed_by,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
indirectFollowMap.set(acct, account)
|
||||||
|
})
|
||||||
|
|
||||||
const list = Array.from(indirectFollowMap.values()).sort((a, b) => {
|
const list = Array.from(indirectFollowMap.values()).sort((a, b) => {
|
||||||
if (a.followed_by.size != b.followed_by.size) {
|
if (a.followed_by.size != b.followed_by.size) {
|
||||||
return b.followed_by.size - a.followed_by.size;
|
return b.followed_by.size - a.followed_by.size
|
||||||
}
|
}
|
||||||
return b.followers_count - a.followers_count;
|
return b.followers_count - a.followers_count
|
||||||
});
|
})
|
||||||
|
|
||||||
setFollows(list);
|
setFollows(list)
|
||||||
}, 2000);
|
}, 2000)
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
directFollows.map(
|
directFollows.map(async ({ acct }) => {
|
||||||
async ({ acct }) => {
|
const follows = await accountFollows(acct, 200, logError)
|
||||||
const follows = await accountFollows(acct, 200, logError);
|
progress++
|
||||||
progress++;
|
setProgress([progress, directFollows.length])
|
||||||
setProgress([progress, directFollows.length]);
|
indirectFollowLists.push(
|
||||||
indirectFollowLists.push(follows.map(account => ({ ...account, followed_by: new Set([acct]) })));
|
follows.map((account) => ({ ...account, followed_by: new Set([acct]) }))
|
||||||
updateList();
|
)
|
||||||
}
|
updateList()
|
||||||
),
|
})
|
||||||
);
|
)
|
||||||
|
|
||||||
updateList.flush();
|
updateList.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNextPage(linkHeader: string | null): string | null {
|
function getNextPage(linkHeader: string | null): string | null {
|
||||||
if (!linkHeader) {
|
if (!linkHeader) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
// Example header:
|
// Example header:
|
||||||
// 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"
|
// 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"
|
||||||
const match = linkHeader.match(/<(.+)>; rel="next"/);
|
const match = linkHeader.match(/<(.+)>; rel="next"/)
|
||||||
if (match && match.length > 0) {
|
if (match && match.length > 0) {
|
||||||
return match[1];
|
return match[1]
|
||||||
}
|
}
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Content({ }) {
|
export function Content({}) {
|
||||||
const [handle, setHandle] = useState("");
|
const [handle, setHandle] = useState('')
|
||||||
const [follows, setFollows] = useState<Array<AccountDetails>>([]);
|
const [follows, setFollows] = useState<Array<AccountDetails>>([])
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false)
|
||||||
const [isDone, setDone] = useState(false);
|
const [isDone, setDone] = useState(false)
|
||||||
const [domain, setDomain] = useState<string>("");
|
const [domain, setDomain] = useState<string>('')
|
||||||
const [[numLoaded, totalToLoad], setProgress] = useState<Array<number>>([0, 0]);
|
const [[numLoaded, totalToLoad], setProgress] = useState<Array<number>>([
|
||||||
const [errors, setErrors] = useState<Array<string>>([]);
|
0, 0,
|
||||||
|
])
|
||||||
|
const [errors, setErrors] = useState<Array<string>>([])
|
||||||
|
|
||||||
async function search(handle: string) {
|
async function search(handle: string) {
|
||||||
if (!/@/.test(handle)) {
|
if (!/@/.test(handle)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
setErrors([]);
|
setErrors([])
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
setDone(false);
|
setDone(false)
|
||||||
setFollows([]);
|
setFollows([])
|
||||||
setProgress([0, 0]);
|
setProgress([0, 0])
|
||||||
setDomain(getDomain(handle));
|
setDomain(getDomain(handle))
|
||||||
await accountFofs(handle, setProgress, setFollows, error => setErrors(e => [...e, error]));
|
await accountFofs(handle, setProgress, setFollows, (error) =>
|
||||||
setLoading(false);
|
setErrors((e) => [...e, error])
|
||||||
setDone(true);
|
)
|
||||||
|
setLoading(false)
|
||||||
|
setDone(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <section className="bg-gray-50 dark:bg-gray-800" id="searchForm">
|
return (
|
||||||
<div className="px-4 py-8 mx-auto space-y-12 lg:space-y-20 lg:py-24 lg:px-6">
|
<section className="bg-gray-50 dark:bg-gray-800" id="searchForm">
|
||||||
<form onSubmit={e => {
|
<div className="px-4 py-8 mx-auto space-y-12 lg:space-y-20 lg:py-24 lg:px-6">
|
||||||
search(handle);
|
<form
|
||||||
e.preventDefault();
|
onSubmit={(e) => {
|
||||||
return false;
|
search(handle)
|
||||||
}}>
|
e.preventDefault()
|
||||||
<div className="form-group mb-6 text-4xl lg:ml-16">
|
return false
|
||||||
<label htmlFor="mastodonHandle" className="form-label inline-block mb-2 text-gray-700 dark:text-gray-200">Your Mastodon handle:</label>
|
}}
|
||||||
<input type="text" value={handle} onChange={e => setHandle(e.target.value)} className="form-control
|
>
|
||||||
|
<div className="form-group mb-6 text-4xl lg:ml-16">
|
||||||
|
<label
|
||||||
|
htmlFor="mastodonHandle"
|
||||||
|
className="form-label inline-block mb-2 text-gray-700 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
Your Mastodon handle:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={handle}
|
||||||
|
onChange={(e) => setHandle(e.target.value)}
|
||||||
|
className="form-control
|
||||||
block
|
block
|
||||||
w-80
|
w-80
|
||||||
px-3
|
px-3
|
||||||
|
@ -193,11 +227,21 @@ export function Content({ }) {
|
||||||
m-0
|
m-0
|
||||||
focus:text-gray-900 focus:bg-white focus:border-green-600 focus:outline-none
|
focus:text-gray-900 focus:bg-white focus:border-green-600 focus:outline-none
|
||||||
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
|
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
|
||||||
" id="mastodonHandle"
|
"
|
||||||
aria-describedby="mastodonHandleHelp" placeholder="johnmastodon@mas.to" />
|
id="mastodonHandle"
|
||||||
<small id="mastodonHandleHelp" className="block mt-1 text-xs text-gray-600 dark:text-gray-300">Be sure to include the full handle, including the domain.</small>
|
aria-describedby="mastodonHandleHelp"
|
||||||
|
placeholder="johnmastodon@mas.to"
|
||||||
|
/>
|
||||||
|
<small
|
||||||
|
id="mastodonHandleHelp"
|
||||||
|
className="block mt-1 text-xs text-gray-600 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Be sure to include the full handle, including the domain.
|
||||||
|
</small>
|
||||||
|
|
||||||
<button type="submit" className="
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="
|
||||||
px-6
|
px-6
|
||||||
py-2.5
|
py-2.5
|
||||||
bg-green-600
|
bg-green-600
|
||||||
|
@ -213,61 +257,107 @@ export function Content({ }) {
|
||||||
active:bg-green-800 active:shadow-lg
|
active:bg-green-800 active:shadow-lg
|
||||||
transition
|
transition
|
||||||
duration-150
|
duration-150
|
||||||
ease-in-out">
|
ease-in-out"
|
||||||
Search
|
>
|
||||||
{isLoading ?
|
Search
|
||||||
<svg className="w-4 h-4 ml-2 fill-white animate-spin inline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">{/*! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}<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" /></svg>
|
{isLoading ? (
|
||||||
: null}
|
<svg
|
||||||
</button>
|
className="w-4 h-4 ml-2 fill-white animate-spin inline"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
>
|
||||||
|
{/*! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
|
||||||
|
<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" />
|
||||||
|
</svg>
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
|
||||||
{isLoading ?
|
{isLoading ? (
|
||||||
<p className="text-sm dark:text-gray-400">Loaded {numLoaded} of {totalToLoad}...</p>
|
<p className="text-sm dark:text-gray-400">
|
||||||
: null}
|
Loaded {numLoaded} of {totalToLoad}...
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{isDone && follows.length === 0 ?
|
{isDone && follows.length === 0 ? (
|
||||||
<div 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" role="alert">
|
<div
|
||||||
<svg aria-hidden="true" className="flex-shrink-0 inline w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" 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" clip-rule="evenodd"></path></svg>
|
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"
|
||||||
<span className="sr-only">Info</span>
|
role="alert"
|
||||||
<div>
|
>
|
||||||
<span className="font-medium">No results found.</span> Please double check for typos in the handle, and ensure that you follow at least a few people
|
<svg
|
||||||
to seed the search. Otherwise, try again later as Mastodon may throttle requests.
|
aria-hidden="true"
|
||||||
|
className="flex-shrink-0 inline w-5 h-5 mr-3"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
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"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span className="sr-only">Info</span>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">No results found.</span> Please
|
||||||
|
double check for typos in the handle, and ensure that you
|
||||||
|
follow at least a few people to seed the search. Otherwise,
|
||||||
|
try again later as Mastodon may throttle requests.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{isDone || follows.length > 0 ? (
|
||||||
|
<div className="flex-col lg:flex items-center justify-center">
|
||||||
|
<div className="max-w-4xl content-center px-2 sm:px-8 py-4 bg-white border rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div className="flow-root">
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className="divide-y divide-gray-200 dark:divide-gray-700"
|
||||||
|
>
|
||||||
|
{follows.slice(0, 500).map((account) => (
|
||||||
|
<AccountDetails
|
||||||
|
key={account.acct}
|
||||||
|
account={account}
|
||||||
|
mainDomain={domain}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
|
|
||||||
{isDone || follows.length > 0 ?
|
|
||||||
<div className="flex-col lg:flex items-center justify-center">
|
|
||||||
<div className="max-w-4xl content-center px-2 sm:px-8 py-4 bg-white border rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700">
|
|
||||||
<div className="flow-root">
|
|
||||||
<ul role="list" className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{follows.slice(0, 500).map(account => <AccountDetails key={account.acct} account={account} mainDomain={domain} />)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
: null}
|
|
||||||
|
|
||||||
<ErrorLog errors={errors} />
|
<ErrorLog errors={errors} />
|
||||||
</div>
|
</div>
|
||||||
</section>;
|
</section>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccountDetails({ account, mainDomain }) {
|
function AccountDetails({ account, mainDomain }) {
|
||||||
const { avatar_static, display_name, acct, note, followers_count, followed_by } = account;
|
const {
|
||||||
let formatter = Intl.NumberFormat('en', { notation: 'compact' });
|
avatar_static,
|
||||||
let numFollowers = formatter.format(followers_count);
|
display_name,
|
||||||
|
acct,
|
||||||
|
note,
|
||||||
|
followers_count,
|
||||||
|
followed_by,
|
||||||
|
} = account
|
||||||
|
let formatter = Intl.NumberFormat('en', { notation: 'compact' })
|
||||||
|
let numFollowers = formatter.format(followers_count)
|
||||||
|
|
||||||
const [expandedFollowers, setExpandedFollowers] = useState(false);
|
const [expandedFollowers, setExpandedFollowers] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="py-3 sm:py-4">
|
<li className="py-3 sm:py-4">
|
||||||
<div className="flex flex-col sm:flex-row items-center space-x-4">
|
<div className="flex flex-col sm:flex-row items-center space-x-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<img className="w-16 h-16 sm:w-8 sm:h-8 rounded-full" src={avatar_static} alt={display_name} />
|
<img
|
||||||
|
className="w-16 h-16 sm:w-8 sm:h-8 rounded-full"
|
||||||
|
src={avatar_static}
|
||||||
|
alt={display_name}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-gray-900 truncate dark:text-white">
|
<p className="text-sm font-medium text-gray-900 truncate dark:text-white">
|
||||||
|
@ -277,36 +367,73 @@ function AccountDetails({ account, mainDomain }) {
|
||||||
{acct} | {numFollowers} followers
|
{acct} | {numFollowers} followers
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<small className="text-sm dark:text-gray-200" dangerouslySetInnerHTML={{ __html: sanitizeHtml(note) }}></small>
|
<small
|
||||||
|
className="text-sm dark:text-gray-200"
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitizeHtml(note) }}
|
||||||
|
></small>
|
||||||
<br />
|
<br />
|
||||||
<small className="text-xs text-gray-800 dark:text-gray-400">
|
<small className="text-xs text-gray-800 dark:text-gray-400">
|
||||||
Followed by{' '}
|
Followed by{' '}
|
||||||
{followed_by.size < 9 || expandedFollowers ?
|
{followed_by.size < 9 || expandedFollowers ? (
|
||||||
|
|
||||||
Array.from<string>(followed_by.values()).map((handle, idx) => (
|
Array.from<string>(followed_by.values()).map((handle, idx) => (
|
||||||
<><span className="font-semibold">{handle.replace(/@.+/, '')}</span>{idx === followed_by.size - 1 ? '.' : ', '}</>
|
<>
|
||||||
|
<span className="font-semibold">
|
||||||
|
{handle.replace(/@.+/, '')}
|
||||||
|
</span>
|
||||||
|
{idx === followed_by.size - 1 ? '.' : ', '}
|
||||||
|
</>
|
||||||
))
|
))
|
||||||
: <>
|
) : (
|
||||||
<button onClick={() => setExpandedFollowers(true)} className="font-semibold">{followed_by.size} of your contacts</button>.
|
<>
|
||||||
</>}
|
<button
|
||||||
|
onClick={() => setExpandedFollowers(true)}
|
||||||
|
className="font-semibold"
|
||||||
|
>
|
||||||
|
{followed_by.size} of your contacts
|
||||||
|
</button>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white my-4 sm:my-0">
|
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white my-4 sm:my-0">
|
||||||
<a href={`https://${mainDomain}/@${acct.replace('@'+mainDomain, '')}`} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" target="_blank" rel="noreferrer">
|
<a
|
||||||
|
href={`https://${mainDomain}/@${acct.replace(
|
||||||
|
'@' + mainDomain,
|
||||||
|
''
|
||||||
|
)}`}
|
||||||
|
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
Follow
|
Follow
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ErrorLog({ errors }: { errors: Array<string> }) {
|
function ErrorLog({ errors }: { errors: Array<string> }) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false)
|
||||||
return (<>
|
return (
|
||||||
{errors.length > 0 ? <div className="text-sm text-gray-600 dark:text-gray-200 lg:ml-12 border border-solid border-gray-200 dark:border-gray-700 rounded p-4">
|
<>
|
||||||
Found <button className="font-bold" onClick={() => setExpanded(!expanded)}>{errors.length} warnings</button>{expanded ? ':' : '.'}
|
{errors.length > 0 ? (
|
||||||
{expanded ? errors.map(err => <p key={err} className="text-xs">{err}</p>) : null}
|
<div className="text-sm text-gray-600 dark:text-gray-200 lg:ml-12 border border-solid border-gray-200 dark:border-gray-700 rounded p-4">
|
||||||
</div> : null}
|
Found{' '}
|
||||||
</>);
|
<button className="font-bold" onClick={() => setExpanded(!expanded)}>
|
||||||
}
|
{errors.length} warnings
|
||||||
|
</button>
|
||||||
|
{expanded ? ':' : '.'}
|
||||||
|
{expanded
|
||||||
|
? errors.map((err) => (
|
||||||
|
<p key={err} className="text-xs">
|
||||||
|
{err}
|
||||||
|
</p>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -1,62 +1,127 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
export function FAQ({ }) {
|
export function FAQ({}) {
|
||||||
return <section className="bg-white dark:bg-gray-900 pt-12">
|
return (
|
||||||
<div className="max-w-screen-xl px-4 pb-8 mx-auto lg:pb-24 lg:px-6" id="faq">
|
<section className="bg-white dark:bg-gray-900 pt-12">
|
||||||
<h2 className="mb-6 text-3xl font-extrabold tracking-tight text-center text-gray-900 lg:mb-8 lg:text-3xl dark:text-white">Frequently asked questions</h2>
|
<div
|
||||||
<div className="max-w-screen-md mx-auto">
|
className="max-w-screen-xl px-4 pb-8 mx-auto lg:pb-24 lg:px-6"
|
||||||
<div id="accordion-flush" data-accordion="collapse" data-active-classes="bg-white dark:bg-gray-900 text-gray-900 dark:text-white" data-inactive-classes="text-gray-500 dark:text-gray-400">
|
id="faq"
|
||||||
<FAQItem defaultSelected title="How does this work?">
|
>
|
||||||
The tool looks up all the people you follow, and then the people <em>they</em> follow. Then
|
<h2 className="mb-6 text-3xl font-extrabold tracking-tight text-center text-gray-900 lg:mb-8 lg:text-3xl dark:text-white">
|
||||||
it sorts them by the number of mutuals, or otherwise by how popular those accounts are.
|
Frequently asked questions
|
||||||
</FAQItem>
|
</h2>
|
||||||
|
<div className="max-w-screen-md mx-auto">
|
||||||
|
<div
|
||||||
|
id="accordion-flush"
|
||||||
|
data-accordion="collapse"
|
||||||
|
data-active-classes="bg-white dark:bg-gray-900 text-gray-900 dark:text-white"
|
||||||
|
data-inactive-classes="text-gray-500 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<FAQItem defaultSelected title="How does this work?">
|
||||||
|
The tool looks up all the people you follow, and then the people{' '}
|
||||||
|
<em>they</em> follow. Then it sorts them by the number of mutuals,
|
||||||
|
or otherwise by how popular those accounts are.
|
||||||
|
</FAQItem>
|
||||||
|
|
||||||
<FAQItem title="Do I need to grant the app any permissions?">
|
<FAQItem title="Do I need to grant the app any permissions?">
|
||||||
Not at all! This app uses public APIs to fetch potential people you can follow on Mastodon. In fact, it only does inauthenticated network requests to various
|
Not at all! This app uses public APIs to fetch potential people
|
||||||
Mastodon instances.
|
you can follow on Mastodon. In fact, it only does inauthenticated
|
||||||
</FAQItem>
|
network requests to various Mastodon instances.
|
||||||
|
</FAQItem>
|
||||||
|
|
||||||
<FAQItem title="Help! The search got stuck.">
|
<FAQItem title="Help! The search got stuck.">
|
||||||
Don't worry. The list of suggestions will load in 30 seconds or so. Sometimes it gets stuck because one or more of the queries
|
Don't worry. The list of suggestions will load in 30 seconds
|
||||||
made to Mastodon time out. This is not a problem, because the rest of the queries will work as expected.
|
or so. Sometimes it gets stuck because one or more of the queries
|
||||||
</FAQItem>
|
made to Mastodon time out. This is not a problem, because the rest
|
||||||
|
of the queries will work as expected.
|
||||||
|
</FAQItem>
|
||||||
|
|
||||||
<FAQItem title="Why don't I see any results?">
|
<FAQItem title="Why don't I see any results?">
|
||||||
There could be a few reasons:
|
There could be a few reasons:
|
||||||
<ul className="list-disc ml-4">
|
<ul className="list-disc ml-4">
|
||||||
<li>This tool only works if your list of follows is public. If you've opted to hide your social graph, you will not see
|
<li>
|
||||||
any results here.</li>
|
This tool only works if your list of follows is public. If
|
||||||
<li>Due to the high volume of requests, sometimes Mastodon throttles this tool. If that's the case, try again a bit later.</li>
|
you've opted to hide your social graph, you will not see
|
||||||
<li>Make sure you have no typos in the Mastodon handle, and make sure you follow at least a few people to seed the search.</li>
|
any results here.
|
||||||
</ul>
|
</li>
|
||||||
</FAQItem>
|
<li>
|
||||||
|
Due to the high volume of requests, sometimes Mastodon
|
||||||
|
throttles this tool. If that's the case, try again a bit
|
||||||
|
later.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Make sure you have no typos in the Mastodon handle, and make
|
||||||
|
sure you follow at least a few people to seed the search.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</FAQItem>
|
||||||
|
|
||||||
<FAQItem title="How can I contribute with suggestions?">
|
<FAQItem title="How can I contribute with suggestions?">
|
||||||
Click the "Fork me on Github" link on the top right, and open up an issue.
|
Click the "Fork me on Github" link on the top right, and
|
||||||
</FAQItem>
|
open up an issue.
|
||||||
|
</FAQItem>
|
||||||
|
|
||||||
<FAQItem title="Why is this not a core Mastodon feature?">
|
<FAQItem title="Why is this not a core Mastodon feature?">
|
||||||
Well, maybe it should be. In the meantime, you can use this website.
|
Well, maybe it should be. In the meantime, you can use this
|
||||||
</FAQItem>
|
website.
|
||||||
|
</FAQItem>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>;
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FAQItem({ defaultSelected, title, children }: { defaultSelected?: boolean, title: string, children: React.ReactNode }) {
|
function FAQItem({
|
||||||
const [selected, setSelected] = useState(defaultSelected);
|
defaultSelected,
|
||||||
return (<>
|
title,
|
||||||
<h3 id="accordion-flush-heading-1">
|
children,
|
||||||
<button type="button" onClick={() => setSelected(!selected)} className={`flex items-center justify-between w-full py-5 font-medium text-left text-gray-${selected ? 900 : 500} bg-white border-b border-gray-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-${selected ? 200 : 400}`} data-accordion-target="#accordion-flush-body-1" aria-expanded="true" aria-controls="accordion-flush-body-1">
|
}: {
|
||||||
<span>{title}</span>
|
defaultSelected?: boolean
|
||||||
<svg data-accordion-icon className="w-6 h-6 rotate-180 shrink-0" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" 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" clipRule="evenodd" /></svg>
|
title: string
|
||||||
</button>
|
children: React.ReactNode
|
||||||
</h3>
|
}) {
|
||||||
{selected ?
|
const [selected, setSelected] = useState(defaultSelected)
|
||||||
<div id="accordion-flush-body-1" aria-labelledby="accordion-flush-heading-1">
|
return (
|
||||||
<div className="py-5 border-b border-gray-200 dark:border-gray-700 dark:text-gray-300">
|
<>
|
||||||
{children}
|
<h3 id="accordion-flush-heading-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelected(!selected)}
|
||||||
|
className={`flex items-center justify-between w-full py-5 font-medium text-left text-gray-${
|
||||||
|
selected ? 900 : 500
|
||||||
|
} bg-white border-b border-gray-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-${
|
||||||
|
selected ? 200 : 400
|
||||||
|
}`}
|
||||||
|
data-accordion-target="#accordion-flush-body-1"
|
||||||
|
aria-expanded="true"
|
||||||
|
aria-controls="accordion-flush-body-1"
|
||||||
|
>
|
||||||
|
<span>{title}</span>
|
||||||
|
<svg
|
||||||
|
data-accordion-icon
|
||||||
|
className="w-6 h-6 rotate-180 shrink-0"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
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"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</h3>
|
||||||
|
{selected ? (
|
||||||
|
<div
|
||||||
|
id="accordion-flush-body-1"
|
||||||
|
aria-labelledby="accordion-flush-heading-1"
|
||||||
|
>
|
||||||
|
<div className="py-5 border-b border-gray-200 dark:border-gray-700 dark:text-gray-300">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> : null}
|
) : null}
|
||||||
</>);
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,41 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
export default function Footer({ }) {
|
export default function Footer({}) {
|
||||||
return <footer className="bg-white dark:bg-gray-800">
|
return (
|
||||||
<div className="max-w-screen-xl p-4 py-6 mx-auto lg:py-16 md:p-8 lg:p-10">
|
<footer className="bg-white dark:bg-gray-800">
|
||||||
<hr className="my-6 border-gray-200 sm:mx-auto dark:border-gray-700 lg:my-8" />
|
<div className="max-w-screen-xl p-4 py-6 mx-auto lg:py-16 md:p-8 lg:p-10">
|
||||||
<div className="text-center">
|
<hr className="my-6 border-gray-200 sm:mx-auto dark:border-gray-700 lg:my-8" />
|
||||||
<div className="mb-5 lg:text-2xl font-semibold text-gray-700 dark:text-white text-lg">
|
<div className="text-center">
|
||||||
Followgraph for Mastodon, built by <a href="https://mastodon.online/@gabipurcaru" target="_blank" rel="noreferrer" className="font-bold text-gray-900 dark:text-gray-400">@gabipurcaru@mastodon.online</a>.
|
<div className="mb-5 lg:text-2xl font-semibold text-gray-700 dark:text-white text-lg">
|
||||||
</div>
|
Followgraph for Mastodon, built by {' '}
|
||||||
<span className="block text-sm text-center text-gray-500 dark:text-gray-400">Built with <a href="https://flowbite.com" className="text-purple-600 hover:underline dark:text-purple-500">Flowbite</a> and <a href="https://tailwindcss.com" className="text-purple-600 hover:underline dark:text-purple-500">Tailwind CSS</a>.
|
<a
|
||||||
</span>
|
href="https://mastodon.online/@gabipurcaru"
|
||||||
|
target="_blank"
|
||||||
</div>
|
rel="noreferrer"
|
||||||
|
className="font-bold text-gray-900 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
@gabipurcaru@mastodon.online
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
<span className="block text-sm text-center text-gray-500 dark:text-gray-400">
|
||||||
|
Built with{' '}
|
||||||
|
<a
|
||||||
|
href="https://flowbite.com"
|
||||||
|
className="text-purple-600 hover:underline dark:text-purple-500"
|
||||||
|
>
|
||||||
|
Flowbite
|
||||||
|
</a>{' '}
|
||||||
|
and{' '}
|
||||||
|
<a
|
||||||
|
href="https://tailwindcss.com"
|
||||||
|
className="text-purple-600 hover:underline dark:text-purple-500"
|
||||||
|
>
|
||||||
|
Tailwind CSS
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>;
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,80 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
return (<header className="fixed w-full">
|
return (
|
||||||
<nav className="bg-white border-gray-200 py-2.5 dark:bg-gray-900">
|
<header className="fixed w-full">
|
||||||
<div className="flex flex-wrap items-center justify-between max-w-screen-xl px-4 mx-auto">
|
<nav className="bg-white border-gray-200 py-2.5 dark:bg-gray-900">
|
||||||
<Logo />
|
<div className="flex flex-wrap items-center justify-between max-w-screen-xl px-4 mx-auto">
|
||||||
|
<Logo />
|
||||||
|
|
||||||
<div className="items-center justify-between hidden w-full lg:flex lg:w-auto lg:order-1" id="mobile-menu-2">
|
<div
|
||||||
<ul className="flex flex-col mt-4 font-medium lg:flex-row lg:space-x-8 lg:mt-0">
|
className="items-center justify-between hidden w-full lg:flex lg:w-auto lg:order-1"
|
||||||
<MenuItem link="#" selected>Home</MenuItem>
|
id="mobile-menu-2"
|
||||||
<MenuItem link="#faq">FAQ</MenuItem>
|
>
|
||||||
<MenuItem link="https://github.com/gabipurcaru/followgraph">Fork me on GitHub</MenuItem>
|
<ul className="flex flex-col mt-4 font-medium lg:flex-row lg:space-x-8 lg:mt-0">
|
||||||
</ul>
|
<MenuItem link="#" selected>
|
||||||
</div>
|
Home
|
||||||
</div>
|
</MenuItem>
|
||||||
</nav>
|
<MenuItem link="#faq">FAQ</MenuItem>
|
||||||
</header>);
|
<MenuItem link="https://github.com/gabipurcaru/followgraph">
|
||||||
|
Fork me on GitHub
|
||||||
|
</MenuItem>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
|
||||||
function Logo({ }) {
|
function Logo({}) {
|
||||||
return (<a href="#" className="flex items-center">
|
return (
|
||||||
<svg className="w-12 h-12 mr-4 dark:fill-white" xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 342.68"><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" /></svg>
|
<a href="#" className="flex items-center">
|
||||||
<span className="self-center text-xl font-semibold whitespace-nowrap dark:text-white">Followgraph for Mastodon</span>
|
<svg
|
||||||
</a>);
|
className="w-12 h-12 mr-4 dark:fill-white"
|
||||||
}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
shape-rendering="geometricPrecision"
|
||||||
|
text-rendering="geometricPrecision"
|
||||||
|
image-rendering="optimizeQuality"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
viewBox="0 0 512 342.68"
|
||||||
|
>
|
||||||
|
<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" />
|
||||||
|
</svg>
|
||||||
|
<span className="self-center text-xl font-semibold whitespace-nowrap dark:text-white">
|
||||||
|
Followgraph for Mastodon
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuItem({ link, children, selected }: { link: string, children: string | React.ReactElement, selected?: boolean }) {
|
function MenuItem({
|
||||||
return (<li>
|
link,
|
||||||
{selected ?
|
children,
|
||||||
<a href={link} 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" aria-current="page">
|
selected,
|
||||||
{children}
|
}: {
|
||||||
</a>
|
link: string
|
||||||
:
|
children: string | React.ReactElement
|
||||||
<a href={link} 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">
|
selected?: boolean
|
||||||
{children}
|
}) {
|
||||||
</a>
|
return (
|
||||||
}
|
<li>
|
||||||
</li>);
|
{selected ? (
|
||||||
|
<a
|
||||||
|
href={link}
|
||||||
|
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"
|
||||||
|
aria-current="page"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={link}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,40 @@
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
export default function Hero({ }) {
|
export default function Hero({}) {
|
||||||
return <section className="bg-white dark:bg-gray-900">
|
return (
|
||||||
<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">
|
<section className="bg-white dark:bg-gray-900">
|
||||||
<div className="mr-auto place-self-center lg:col-span-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">
|
||||||
<h1 className="max-w-2xl mb-4 text-4xl font-extrabold leading-none tracking-tight md:text-5xl xl:text-6xl dark:text-white">
|
<div className="mr-auto place-self-center lg:col-span-7">
|
||||||
Find awesome people <br /> on Mastodon.
|
<h1 className="max-w-2xl mb-4 text-4xl font-extrabold leading-none tracking-tight md:text-5xl xl:text-6xl dark:text-white">
|
||||||
</h1>
|
Find awesome people <br /> on Mastodon.
|
||||||
<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">
|
</h1>
|
||||||
This tool allows you to expand your connection graph and find new people to follow. It works by
|
<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">
|
||||||
looking up your "follows' follows". <br /> <br/>
|
This tool allows you to expand your connection graph and find new
|
||||||
Your extended network is a treasure trove!
|
people to follow. It works by looking up your "follows'
|
||||||
</p>
|
follows". <br /> <br />
|
||||||
|
Your extended network is a treasure trove!
|
||||||
<div className="space-y-4 sm:flex sm:space-y-0 sm:space-x-4 ">
|
</p>
|
||||||
<a href="https://github.com/gabipurcaru/followgraph" 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">
|
|
||||||
<svg className="w-4 h-4 mr-2 text-gray-500 dark:fill-gray-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512">{
|
|
||||||
/* Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) */
|
|
||||||
}<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" /></svg> View on GitHub
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="#searchForm" className="
|
<div className="space-y-4 sm:flex sm:space-y-0 sm:space-x-4 ">
|
||||||
|
<a
|
||||||
|
href="https://github.com/gabipurcaru/followgraph"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mr-2 text-gray-500 dark:fill-gray-300"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 496 512"
|
||||||
|
>
|
||||||
|
{/* Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) */}
|
||||||
|
<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" />
|
||||||
|
</svg>{' '}
|
||||||
|
View on GitHub
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#searchForm"
|
||||||
|
className="
|
||||||
inline-flex items-center justify-center w-full
|
inline-flex items-center justify-center w-full
|
||||||
px-5 py-3 text-sm font-medium text-center
|
px-5 py-3 text-sm font-medium text-center
|
||||||
text-gray-900 border
|
text-gray-900 border
|
||||||
|
@ -32,22 +45,30 @@ export default function Hero({ }) {
|
||||||
|
|
||||||
dark:bg-green-700 dark:hover:bg-green-600
|
dark:bg-green-700 dark:hover:bg-green-600
|
||||||
dark:focus:ring-gray-800
|
dark:focus:ring-gray-800
|
||||||
dark:border-gray-700 ">
|
dark:border-gray-700 "
|
||||||
|
>
|
||||||
<svg className="w-4 h-4 mr-2 dark:fill-gray-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">{/* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}<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" /></svg>
|
<svg
|
||||||
Use now
|
className="w-4 h-4 mr-2 dark:fill-gray-300"
|
||||||
</a>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
>
|
||||||
|
{/* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
|
||||||
|
<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" />
|
||||||
|
</svg>
|
||||||
|
Use now
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden lg:mt-0 lg:col-span-5 lg:flex">
|
||||||
|
<Image
|
||||||
|
src="/hero.png"
|
||||||
|
alt="Picture of people at a party"
|
||||||
|
width={500}
|
||||||
|
height={500}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden lg:mt-0 lg:col-span-5 lg:flex">
|
</section>
|
||||||
<Image
|
)
|
||||||
src="/hero.png"
|
|
||||||
alt="Picture of people at a party"
|
|
||||||
width={500}
|
|
||||||
height={500}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"postcss": "^8.4.20",
|
"postcss": "^8.4.20",
|
||||||
|
"prettier": "2.8.1",
|
||||||
"tailwindcss": "^3.2.4"
|
"tailwindcss": "^3.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3440,6 +3441,21 @@
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin-prettier.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
@ -6608,6 +6624,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
|
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
|
||||||
},
|
},
|
||||||
|
"prettier": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"prop-types": {
|
"prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
|
|
@ -6,7 +6,9 @@
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"prettier": "prettier --check . --config .prettierrc",
|
||||||
|
"prettier:fix": "prettier --write . --config .prettierrc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/font": "13.0.7",
|
"@next/font": "13.0.7",
|
||||||
|
@ -32,6 +34,7 @@
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"postcss": "^8.4.20",
|
"postcss": "^8.4.20",
|
||||||
|
"prettier": "2.8.1",
|
||||||
"tailwindcss": "^3.2.4"
|
"tailwindcss": "^3.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import '../styles/globals.css'
|
import '../styles/globals.css'
|
||||||
import { Analytics } from '@vercel/analytics/react';
|
import { Analytics } from '@vercel/analytics/react'
|
||||||
import type { AppProps } from 'next/app'
|
import type { AppProps } from 'next/app'
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
return (<>
|
return (
|
||||||
<Component {...pageProps} />
|
<>
|
||||||
<Analytics />
|
<Component {...pageProps} />
|
||||||
</>);
|
<Analytics />
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Content } from './../components/Content';
|
import { Content } from './../components/Content'
|
||||||
import { FAQ } from './../components/FAQ';
|
import { FAQ } from './../components/FAQ'
|
||||||
import Footer from './../components/Footer';
|
import Footer from './../components/Footer'
|
||||||
import Hero from './../components/Hero';
|
import Hero from './../components/Hero'
|
||||||
import Header from './../components/Header';
|
import Header from './../components/Header'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
@ -10,7 +10,10 @@ export default function Home() {
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Followgraph for Mastodon</title>
|
<title>Followgraph for Mastodon</title>
|
||||||
<meta name="description" content="Find people to follow on Mastodon by expanding your follow graph." />
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Find people to follow on Mastodon by expanding your follow graph."
|
||||||
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
google-site-verification: googledfe269244d136c58.html
|
google-site-verification: googledfe269244d136c58.html
|
||||||
|
|
|
@ -1 +1,19 @@
|
||||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
|
|
|
@ -2,4 +2,6 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
html { scroll-behavior: smooth; }
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
'./pages/**/*.{js,ts,jsx,tsx}',
|
||||||
"./components/**/*.{js,ts,jsx,tsx}",
|
'./components/**/*.{js,ts,jsx,tsx}',
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
|
|
Ładowanie…
Reference in New Issue