pull/7/head
Gabi Purcaru 2022-12-20 13:50:49 +00:00
rodzic 33f32df9b1
commit 034bb70261
16 zmienionych plików z 2518 dodań i 236 usunięć

Wyświetl plik

@ -1,3 +1,6 @@
{
"extends": "next/core-web-vitals"
}
"extends": [
"next/core-web-vitals",
"prettier"
]
}

6
.prettierrc 100644
Wyświetl plik

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}

Wyświetl plik

@ -0,0 +1,246 @@
import React, { useState } from "react";
import sanitizeHtml from 'sanitize-html';
type AccountDetails = {
id: string, // IMPORTANT: this is int64 so will overflow Javascript's number type
acct: string,
};
async function usernameToId(handle: string): Promise<{ id: number, domain: string }> {
const match = handle.match(/^(.+)@(.+)$/);
if (!match || match.length < 2) {
throw new Error(`Incorrect handle: ${handle}`);
}
const domain = match[2];
const username = match[1];
let response = await fetch(`https://${domain}/api/v1/accounts/lookup?acct=${username}`);
const { id } = await response.json();
return { id, domain };
}
function getDomain(handle) {
const match = handle.match(/^(.+)@(.+)$/);
if (!match || match.length < 2) {
throw new Error(`Incorrect handle: ${handle}`);
}
const domain = match[2];
return domain;
}
async function accountFollows(handle: string): Promise<Array<AccountDetails>> {
let id, domain;
try {
({ id, domain } = await usernameToId(handle));
} catch (e) {
return [];
}
let nextPage: string | undefined = `https://${domain}/api/v1/accounts/${id}/following`;
let data: Array<AccountDetails> = [];
while (nextPage && data.length <= 50) {
console.log(`Get page: ${nextPage}`);
let response;
let page;
try {
response = await fetch(nextPage);
page = await response.json();
} catch (e) {
console.log(e);
break;
}
if (!page.map) {
break;
}
page = page.map(entry => {
if (entry.acct && !/@/.test(entry.acct)) {
// make sure the domain is always there
entry.acct = `${entry.acct}@${domain}`;
};
return entry;
})
data = [...data, ...page];
nextPage = getNextPage(response.headers.get('Link'));
}
return data;
}
async function accountFofs(handle: string, setProgress): Promise<Array<AccountDetails>> {
console.log('Start');
const directFollows = await accountFollows(handle);
setProgress([0, directFollows.length]);
console.log(`Direct follows: ${directFollows.length}`);
let progress = 0;
const indirectFollowLists = await Promise.all(
directFollows.map(
async ({ acct }) => {
const follows = await accountFollows(acct);
progress++;
setProgress([progress, directFollows.length]);
return follows.map(account => ({...account, followed_by: [acct]}));
}
),
);
let indirectFollows = [].concat([], ...indirectFollowLists);
const indirectFollowMap = new Map();
const directFollowIds = new Set(directFollows.map(({ acct }) => acct));
indirectFollows.filter(
// exclude direct follows
({ acct }) => !directFollowIds.has(acct)
).map(account => {
const acct = account.acct;
if (indirectFollowMap.has(acct)) {
const otherAccount = indirectFollowMap.get(acct);
account.followed_by = [...account.followed_by, ...otherAccount.followed_by];
}
indirectFollowMap.set(acct, account);
});
return Array.from(indirectFollowMap.values()).sort((a, b) => {
if (a.followed_by.length != b.followed_by.length) {
return b.followed_by.length - a.followed_by.length;
}
return b.followers_count - a.followers_count;
});
}
function getNextPage(linkHeader: string | undefined): string | undefined {
if (!linkHeader) {
return undefined;
}
// 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"
const match = linkHeader.match(/<(.+)>; rel="next"/);
if (match && match.length > 0) {
return match[1];
}
return undefined;
}
export function Content({ }) {
const [handle, setHandle] = useState("");
const [follows, setfollows] = useState<Array<AccountDetails>>([]);
const [isLoading, setLoading] = useState(false);
const [isDone, setDone] = useState(false);
const [domain, setDomain] = useState();
const [[numLoaded, totalToLoad], setProgress] = useState([0, 0]);
console.log(follows.length);
async function search(handle: string) {
if (!/@/.test(handle)) {
return;
}
setLoading(true);
setDomain(getDomain(handle));
setfollows(await accountFofs(handle, setProgress));
setLoading(false);
setDone(true);
}
return <section className="bg-gray-50 dark:bg-gray-800" id="searchForm">
<div className="px-4 py-8 mx-auto space-y-12 lg:space-y-20 lg:py-24 lg:px-6">
<form onSubmit={() => search(handle)}>
<div className="form-group mb-6 text-4xl ml-8">
<label htmlFor="mastodonHandle" className="form-label inline-block mb-2 text-gray-700">Your Mastodon handle:</label>
<input type="text" value={handle} onChange={e => setHandle(e.target.value)} className="form-control
block
w-80
px-3
py-1.5
text-base
font-normal
text-gray-700
bg-white bg-clip-padding
border border-solid border-gray-300
rounded
transition
ease-in-out
m-0
focus:text-gray-700 focus:bg-white focus:border-green-600 focus:outline-none" id="mastodonHandle"
aria-describedby="mastodonHandleHelp" placeholder="johnmastodon@mas.to" />
<small id="mastodonHandleHelp" className="block mt-1 text-xs text-gray-600">Be sure to include the full handle, including the domain.</small>
<button type="submit" className="
px-6
py-2.5
bg-green-600
text-white
font-medium
text-xs
leading-tight
uppercase
rounded
shadow-md
hover:bg-green-700 hover:shadow-lg
focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0
active:bg-green-800 active:shadow-lg
transition
duration-150
ease-in-out">
Search
{isLoading ?
<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>
: null}
</button>
{isLoading ?
<p className="text-sm">Loaded {numLoaded} from {totalToLoad}...</p>
: null}
</div>
</form>
{isDone ?
<div className="w-9/12 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, 100).map(account => <AccountDetails key={account.acct} account={account} mainDomain={domain} />)}
</ul>
</div>
</div>
: null}
</div>
</section>;
}
function AccountDetails({ account, mainDomain }) {
const { avatar_static, display_name, acct, note, followers_count, followed_by } = account;
let formatter = Intl.NumberFormat('en', { notation: 'compact' });
let numfollows = formatter.format(followers_count);
console.log(account);
return (
<li className="py-3 sm:py-4">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<img className="w-8 h-8 rounded-full" src={avatar_static} alt={display_name} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate dark:text-white">
{display_name}
</p>
<p className="text-sm text-gray-500 truncate dark:text-gray-400">
{acct} | {numfollows} follows
</p>
<br />
<small className="text-sm" dangerouslySetInnerHTML={{ __html: sanitizeHtml(note) }}></small>
<br />
<small className="text-xs text-gray-800">
Followed by{' '}
{followed_by.map((handle, idx) => (
<><span className="font-semibold">{handle.replace(/@.+/, '')}</span>{idx === followed_by.length - 1 ? '' : ', '}</>
))}
</small>
</div>
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white">
<a href={`https://${mainDomain}/@${acct}`} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Follow
</a>
</div>
</div>
</li>
);
}

48
components/FAQ.tsx 100644
Wyświetl plik

@ -0,0 +1,48 @@
import React, { useState } from "react";
export function FAQ({ }) {
return <section className="bg-white dark:bg-gray-900 mt-12">
<div className="max-w-screen-xl px-4 pb-8 mx-auto lg:pb-24 lg:px-6 ">
<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 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?">
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
Mastodon instances.
</FAQItem>
<FAQItem title="Help! The search got stuck.">
Don&apos;t worry. The list of suggestions will load in 30 seconds or so. Sometimes it gets stuck because one or more of the queries
made to Mastodon time out. This is not a problem, because the rest of the queries will work as expected.
</FAQItem>
<FAQItem title="How can I contribute with suggestions?">
Click the &quot;Fork me on Github&quot; link on the top right, and open up an issue.
</FAQItem>
</div>
</div>
</div>
</section>;
}
function FAQItem({ defaultSelected, title, children }) {
const [selected, setSelected] = useState(defaultSelected);
return (<>
<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-white`} 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">
{children}
</div>
</div> : null}
</>);
}

Wyświetl plik

@ -0,0 +1,16 @@
import React from "react";
export default function Footer({ }) {
return <footer className="bg-white dark:bg-gray-800">
<div className="max-w-screen-xl p-4 py-6 mx-auto lg:py-16 md:p-8 lg:p-10">
<hr className="my-6 border-gray-200 sm:mx-auto dark:border-gray-700 lg:my-8" />
<div className="text-center">
<span className="flex items-center justify-center mb-5 text-2xl font-semibold text-gray-700 dark:text-white">
Followgraph for Mastodon, built by&nbsp; <a href="https://mastodon.online/@gabipurcaru" target="_blank" rel="noreferrer" className="font-bold text-gray-900">@gabipurcaru@mastodon.online</a>.
</span>
<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>;
}

Wyświetl plik

@ -0,0 +1,39 @@
import React from "react";
export default function Header() {
return (<header className="fixed w-full">
<nav className="bg-white border-gray-200 py-2.5 dark:bg-gray-900">
<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">
<ul className="flex flex-col mt-4 font-medium lg:flex-row lg:space-x-8 lg:mt-0">
<MenuItem link="#" selected>Home</MenuItem>
<MenuItem link="#">FAQ</MenuItem>
<MenuItem link="#">Fork me on GitHub</MenuItem>
</ul>
</div>
</div>
</nav>
</header>);
function Logo({ }) {
return (<a href="#" className="flex items-center">
<svg className="w-12 h-12 mr-4" 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 }) {
return (<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-400 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>);
}

Wyświetl plik

@ -0,0 +1,41 @@
import Image from 'next/image'
import React from "react";
export default function Hero({ }) {
return <section className="bg-white dark:bg-gray-900">
<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">
<div className="mr-auto place-self-center lg:col-span-7">
<h1 className="max-w-2xl mb-4 text-4xl font-extrabold leading-none tracking-tight md:text-5xl xl:text-6xl dark:text-white">
Find Mastodon <br /> followers.
</h1>
<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">
This tool allows you to expand your connection graph and find new people to follow. It works by
looking up your &quot;follows&apos; follows&quot;. <br />
Note, this may take a while and potentially time out, since it will try to load all the people in your extended network.
</p>
<div className="space-y-4 sm:flex sm:space-y-0 sm:space-x-4">
<a href="#" 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:text-gray-400" 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 px-5 py-3 text-sm font-medium text-center text-gray-900 border border-gray-200 rounded-lg sm:w-auto hover:bg-green-400 focus:ring-4 focus:ring-gray-100 dark:text-white dark:border-gray-700 dark:hover:bg-gray-700 dark:focus:ring-gray-800 bg-green-300 ">
<svg className="w-4 h-4 mr-2 text-gray-500 dark:text-gray-400" 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>
</section>;
}

2045
package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -15,9 +15,20 @@
"@types/react-dom": "18.0.9",
"eslint": "8.30.0",
"eslint-config-next": "13.0.7",
"mastodon": "^1.2.2",
"next": "13.0.7",
"node-fetch": "^3.3.0",
"oauth": "^0.10.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-paginate": "^8.1.4",
"sanitize-html": "^2.8.0",
"typescript": "4.9.4"
},
"devDependencies": {
"autoprefixer": "^10.4.13",
"eslint-config-prettier": "^8.5.0",
"postcss": "^8.4.20",
"tailwindcss": "^3.2.4"
}
}

Wyświetl plik

@ -0,0 +1,33 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
import fetch from 'node-fetch';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Array<AccountDetails>>
) {
const domain = 'mastodon.online';
// const credentials = await createApp(domain);
// console.log(credentials);
// const url = createOauthUrl(credentials, domain);
// console.log(url);
// const token = "COETdTghIFAL6hNGbRGiHbxUeU4WjuNyL1X2lu9KT0o";
// const { token } = req.query;
// const acct = 'gabipurcaru@mastodon.online';
// const follows = await accountFollows(acct, token, domain);
// console.log(follows.length);
// const id = 109265004581756329;
const follows = await accountFofs('gabipurcaru@mastodon.online');
console.log(follows.length);
res.status(200).json(follows); //follows.map(({ acct }) => ({ acct })));
}

Wyświetl plik

@ -1,13 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}

Wyświetl plik

@ -1,123 +1,27 @@
import { Content } from './../components/Content';
import { FAQ } from './../components/FAQ';
import Footer from './../components/Footer';
import Hero from './../components/Hero';
import Header from './../components/Header';
import Head from 'next/head'
import Image from 'next/image'
import { Inter } from '@next/font/google'
import styles from '../styles/Home.module.css'
const inter = Inter({ subsets: ['latin'] })
export default function Home() {
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<title>Mastodon Followgraph</title>
<meta name="description" content="Find people to follow by expanding your follow graph." />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<div className={styles.description}>
<p>
Get started by editing&nbsp;
<code className={styles.code}>pages/index.tsx</code>
</p>
<div>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className={styles.vercelLogo}
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div>
<Header />
<Hero />
<Content />
<div className={styles.center}>
<Image
className={styles.logo}
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
<div className={styles.thirteen}>
<Image
src="/thirteen.svg"
alt="13"
width={40}
height={31}
priority
/>
</div>
</div>
<FAQ />
<div className={styles.grid}>
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2 className={inter.className}>
Docs <span>-&gt;</span>
</h2>
<p className={inter.className}>
Find in-depth information about Next.js features and&nbsp;API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2 className={inter.className}>
Learn <span>-&gt;</span>
</h2>
<p className={inter.className}>
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2 className={inter.className}>
Templates <span>-&gt;</span>
</h2>
<p className={inter.className}>
Discover and deploy boilerplate example Next.js&nbsp;projects.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2 className={inter.className}>
Deploy <span>-&gt;</span>
</h2>
<p className={inter.className}>
Instantly deploy your Next.js site to a shareable URL
with&nbsp;Vercel.
</p>
</a>
</div>
</main>
<Footer /></div>
</>
)
}

Wyświetl plik

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/hero.png 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.3 MiB

Wyświetl plik

@ -1,107 +1,5 @@
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
@tailwind base;
@tailwind components;
@tailwind utilities;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient(
from 180deg at 50% 50%,
#16abff33 0deg,
#0885ff33 55deg,
#54d6ff33 120deg,
#0071ff33 160deg,
transparent 360deg
);
--secondary-glow: radial-gradient(
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
--tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient(
#00000080,
#00000040,
#00000030,
#00000020,
#00000010,
#00000010,
#00000080
);
--callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rbg(--foreground-rgb);
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
html { scroll-behavior: smooth; }

11
tailwind.config.js 100644
Wyświetl plik

@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}