kopia lustrzana https://github.com/gabipurcaru/followgraph
initial commit
rodzic
33f32df9b1
commit
034bb70261
|
@ -1,3 +1,6 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"prettier"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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'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 "Fork me on Github" 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}
|
||||
</>);
|
||||
}
|
|
@ -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 <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>;
|
||||
}
|
|
@ -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>);
|
||||
}
|
|
@ -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 "follows' follows". <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>;
|
||||
}
|
Plik diff jest za duży
Load Diff
11
package.json
11
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 })));
|
||||
}
|
|
@ -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' })
|
||||
}
|
122
pages/index.tsx
122
pages/index.tsx
|
@ -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
|
||||
<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>-></span>
|
||||
</h2>
|
||||
<p className={inter.className}>
|
||||
Find in-depth information about Next.js features and 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>-></span>
|
||||
</h2>
|
||||
<p className={inter.className}>
|
||||
Learn about Next.js in an interactive course with 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>-></span>
|
||||
</h2>
|
||||
<p className={inter.className}>
|
||||
Discover and deploy boilerplate example Next.js 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>-></span>
|
||||
</h2>
|
||||
<p className={inter.className}>
|
||||
Instantly deploy your Next.js site to a shareable URL
|
||||
with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<Footer /></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 1.3 MiB |
|
@ -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; }
|
|
@ -0,0 +1,11 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
Ładowanie…
Reference in New Issue