kopia lustrzana https://github.com/gabipurcaru/followgraph
Porównaj commity
4 Commity
606f9cbead
...
f93b146c31
Autor | SHA1 | Data |
---|---|---|
Gabi Purcaru | f93b146c31 | |
Gabi Purcaru | 11924c8030 | |
Gabi Purcaru | 5c9079f72e | |
Gabi Purcaru | 189d2c33fe |
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState } from 'react'
|
import { Spinner } from './Spinner'
|
||||||
|
import React, { useState, memo, useRef } from 'react'
|
||||||
import sanitizeHtml from 'sanitize-html'
|
import sanitizeHtml from 'sanitize-html'
|
||||||
import debounce from 'debounce'
|
import debounce from 'debounce'
|
||||||
|
|
||||||
|
@ -11,7 +12,11 @@ type AccountDetails = {
|
||||||
id: string
|
id: string
|
||||||
acct: string
|
acct: string
|
||||||
followed_by: Set<string> // list of handles
|
followed_by: Set<string> // list of handles
|
||||||
|
followers_count: number
|
||||||
discoverable: boolean
|
discoverable: boolean
|
||||||
|
display_name: string
|
||||||
|
note: string
|
||||||
|
avatar_static: string
|
||||||
}
|
}
|
||||||
|
|
||||||
async function usernameToId(
|
async function usernameToId(
|
||||||
|
@ -166,6 +171,23 @@ function getNextPage(linkHeader: string | null): string | null {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchesSearch(account: AccountDetails, search: string): boolean {
|
||||||
|
if (/^\s*$/.test(search)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const sanitizedSearch = search.replace(/^\s+|\s+$/, '').toLocaleLowerCase()
|
||||||
|
if (account.acct.toLocaleLowerCase().includes(sanitizedSearch)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (account.display_name.toLocaleLowerCase().includes(sanitizedSearch)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (account.note.toLocaleLowerCase().includes(sanitizedSearch)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
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>>([])
|
||||||
|
@ -264,16 +286,10 @@ export function Content({}) {
|
||||||
ease-in-out"
|
ease-in-out"
|
||||||
>
|
>
|
||||||
Search
|
Search
|
||||||
{isLoading ? (
|
<Spinner
|
||||||
<svg
|
visible={isLoading}
|
||||||
className="w-4 h-4 ml-2 fill-white animate-spin inline"
|
className="w-4 h-4 ml-2 fill-white"
|
||||||
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>
|
</button>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
@ -313,24 +329,7 @@ export function Content({}) {
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{isDone || follows.length > 0 ? (
|
{isDone || follows.length > 0 ? (
|
||||||
<div className="flex-col lg:flex items-center justify-center">
|
<Results follows={follows} domain={domain} />
|
||||||
<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>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<ErrorLog errors={errors} />
|
<ErrorLog errors={errors} />
|
||||||
|
@ -339,87 +338,96 @@ export function Content({}) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccountDetails({ account, mainDomain }) {
|
const AccountDetails = memo(
|
||||||
const {
|
({
|
||||||
avatar_static,
|
account,
|
||||||
display_name,
|
mainDomain,
|
||||||
acct,
|
}: {
|
||||||
note,
|
account: AccountDetails
|
||||||
followers_count,
|
mainDomain: string
|
||||||
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="px-4 py-3 pb-7 sm:px-0 sm:py-4">
|
<li className="px-4 py-3 pb-7 sm:px-0 sm:py-4">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row">
|
<div className="flex flex-col gap-4 sm:flex-row">
|
||||||
<div className="flex-shrink-0 m-auto">
|
<div className="flex-shrink-0 m-auto">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
className="w-16 h-16 sm:w-8 sm:h-8 rounded-full"
|
className="w-16 h-16 sm:w-8 sm:h-8 rounded-full"
|
||||||
src={avatar_static}
|
src={avatar_static}
|
||||||
alt={display_name}
|
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">
|
||||||
{display_name}
|
{display_name}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row text-sm text-gray-500 dark:text-gray-400">
|
<div className="flex flex-col sm:flex-row text-sm text-gray-500 dark:text-gray-400">
|
||||||
<span className="truncate">{acct}</span>
|
<span className="truncate">{acct}</span>
|
||||||
<span className="sm:inline hidden whitespace-pre"> | </span>
|
<span className="sm:inline hidden whitespace-pre"> | </span>
|
||||||
<span>{numFollowers} followers</span>
|
<span>{numFollowers} followers</span>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<small
|
||||||
|
className="text-sm dark:text-gray-200"
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitizeHtml(note) }}
|
||||||
|
></small>
|
||||||
|
<br />
|
||||||
|
<small className="text-xs text-gray-800 dark:text-gray-400">
|
||||||
|
Followed by{' '}
|
||||||
|
{followed_by.size < 9 || expandedFollowers ? (
|
||||||
|
Array.from<string>(followed_by.values()).map((handle, idx) => (
|
||||||
|
<React.Fragment key={handle}>
|
||||||
|
<span className="font-semibold">
|
||||||
|
{handle.replace(/@.+/, '')}
|
||||||
|
</span>
|
||||||
|
{idx === followed_by.size - 1 ? '.' : ', '}
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => setExpandedFollowers(true)}
|
||||||
|
className="font-semibold"
|
||||||
|
>
|
||||||
|
{followed_by.size} of your contacts
|
||||||
|
</button>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex m-auto text-base font-semibold text-gray-900 dark:text-white">
|
||||||
|
<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
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
|
||||||
<small
|
|
||||||
className="text-sm dark:text-gray-200"
|
|
||||||
dangerouslySetInnerHTML={{ __html: sanitizeHtml(note) }}
|
|
||||||
></small>
|
|
||||||
<br />
|
|
||||||
<small className="text-xs text-gray-800 dark:text-gray-400">
|
|
||||||
Followed by{' '}
|
|
||||||
{followed_by.size < 9 || expandedFollowers ? (
|
|
||||||
Array.from<string>(followed_by.values()).map((handle, idx) => (
|
|
||||||
<React.Fragment key={handle}>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{handle.replace(/@.+/, '')}
|
|
||||||
</span>
|
|
||||||
{idx === followed_by.size - 1 ? '.' : ', '}
|
|
||||||
</React.Fragment>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={() => setExpandedFollowers(true)}
|
|
||||||
className="font-semibold"
|
|
||||||
>
|
|
||||||
{followed_by.size} of your contacts
|
|
||||||
</button>
|
|
||||||
.
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex m-auto text-base font-semibold text-gray-900 dark:text-white">
|
</li>
|
||||||
<a
|
)
|
||||||
href={`https://${mainDomain}/@${acct.replace(
|
}
|
||||||
'@' + mainDomain,
|
)
|
||||||
''
|
AccountDetails.displayName = 'AccountDetails'
|
||||||
)}`}
|
|
||||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Follow
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ErrorLog({ errors }: { errors: Array<string> }) {
|
function ErrorLog({ errors }: { errors: Array<string> }) {
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
@ -444,3 +452,99 @@ function ErrorLog({ errors }: { errors: Array<string> }) {
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Results({
|
||||||
|
domain,
|
||||||
|
follows,
|
||||||
|
}: {
|
||||||
|
domain: string
|
||||||
|
follows: Array<AccountDetails>
|
||||||
|
}) {
|
||||||
|
let [search, setSearch] = useState<string>('')
|
||||||
|
const [isLoading, setLoading] = useState(false)
|
||||||
|
const updateSearch = useRef(
|
||||||
|
debounce((s: string) => {
|
||||||
|
setLoading(false)
|
||||||
|
setSearch(s)
|
||||||
|
}, 1500)
|
||||||
|
).current
|
||||||
|
|
||||||
|
follows = follows.filter((acc) => matchesSearch(acc, search)).slice(0, 500)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-col lg:flex items-center justify-center">
|
||||||
|
<div className="max-w-4xl">
|
||||||
|
<div className="w-full mb-4 dark:text-gray-200">
|
||||||
|
<label>
|
||||||
|
<div className="mb-2">
|
||||||
|
<Spinner
|
||||||
|
visible={isLoading}
|
||||||
|
className="w-4 h-4 mr-1 fill-gray-400"
|
||||||
|
/>
|
||||||
|
Search:
|
||||||
|
</div>
|
||||||
|
<SearchInput
|
||||||
|
onChange={(s) => {
|
||||||
|
setLoading(true)
|
||||||
|
updateSearch(s)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="content-center px-2 sm:px-8 py-4 bg-white border rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div className="flow-root">
|
||||||
|
{follows.length === 0 ? (
|
||||||
|
<p className="text-gray-700 dark:text-gray-200">
|
||||||
|
No results found.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className="divide-y divide-gray-200 dark:divide-gray-700"
|
||||||
|
>
|
||||||
|
{follows.map((account) => (
|
||||||
|
<AccountDetails
|
||||||
|
key={account.acct}
|
||||||
|
account={account}
|
||||||
|
mainDomain={domain}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchInput({ onChange }: { onChange: (s: string) => void }) {
|
||||||
|
let [search, setSearchInputValue] = useState<string>('')
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="London"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchInputValue(e.target.value)
|
||||||
|
onChange(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-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"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -71,8 +71,9 @@ export function FAQ({}) {
|
||||||
</FAQItem>
|
</FAQItem>
|
||||||
|
|
||||||
<FAQItem title="Can I download the list of accounts as CSV?">
|
<FAQItem title="Can I download the list of accounts as CSV?">
|
||||||
While it would be a useful feature, Followgraph does <em>not</em> plan to offer this functionality
|
While it would be a useful feature, Followgraph does <em>not</em>{' '}
|
||||||
as it would facilitate inorganic and potentially malicious behaviour.
|
plan to offer this functionality as it would facilitate inorganic
|
||||||
|
and potentially malicious behaviour.
|
||||||
</FAQItem>
|
</FAQItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,12 +17,15 @@ export default function Footer({}) {
|
||||||
@gabipurcaru@mastodon.online
|
@gabipurcaru@mastodon.online
|
||||||
</Link>
|
</Link>
|
||||||
.<br />
|
.<br />
|
||||||
<Link
|
<span className="text-sm font-bold text-gray-900 dark:text-gray-400 ">
|
||||||
href="/donate"
|
<Link href="/privacy" className="underline">
|
||||||
className="font-bold text-gray-900 dark:text-gray-400"
|
Privacy
|
||||||
>
|
</Link>{' '}
|
||||||
Donate
|
|{' '}
|
||||||
</Link>
|
<Link href="/donate" className="underline">
|
||||||
|
Donate
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="block text-sm text-center text-gray-500 dark:text-gray-400">
|
<span className="block text-sm text-center text-gray-500 dark:text-gray-400">
|
||||||
Built with{' '}
|
Built with{' '}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export default function Header({ selected }: { selected: 'home' | 'donate' }) {
|
export default function Header({
|
||||||
|
selected,
|
||||||
|
}: {
|
||||||
|
selected: 'home' | 'donate' | 'privacy'
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<header className="fixed w-full">
|
<header className="fixed w-full">
|
||||||
<nav className="bg-white border-gray-200 py-2.5 dark:bg-gray-900">
|
<nav className="bg-white border-gray-200 py-2.5 dark:bg-gray-900">
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react'
|
||||||
|
export function Spinner({
|
||||||
|
visible,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
visible: boolean
|
||||||
|
className: string
|
||||||
|
}) {
|
||||||
|
if (!visible) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={className + ' 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -27,6 +27,7 @@
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.8",
|
||||||
"@types/sanitize-html": "^2.8.0",
|
"@types/sanitize-html": "^2.8.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
@ -387,6 +388,34 @@
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/typography": {
|
||||||
|
"version": "0.5.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.8.tgz",
|
||||||
|
"integrity": "sha512-xGQEp8KXN8Sd8m6R4xYmwxghmswrd0cPnNI2Lc6fmrC3OojysTBJJGSIVwPV56q4t6THFUK3HJ0EaWwpglSxWw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.castarray": "^4.4.0",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
|
"postcss-selector-parser": "6.0.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=3.0.0 || insiders"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||||
|
"version": "6.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||||
|
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"cssesc": "^3.0.0",
|
||||||
|
"util-deprecate": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/json5": {
|
"node_modules/@types/json5": {
|
||||||
"version": "0.0.29",
|
"version": "0.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||||
|
@ -2773,6 +2802,18 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.castarray": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
|
@ -4518,6 +4559,30 @@
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@tailwindcss/typography": {
|
||||||
|
"version": "0.5.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.8.tgz",
|
||||||
|
"integrity": "sha512-xGQEp8KXN8Sd8m6R4xYmwxghmswrd0cPnNI2Lc6fmrC3OojysTBJJGSIVwPV56q4t6THFUK3HJ0EaWwpglSxWw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"lodash.castarray": "^4.4.0",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
|
"postcss-selector-parser": "6.0.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"postcss-selector-parser": {
|
||||||
|
"version": "6.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||||
|
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"cssesc": "^3.0.0",
|
||||||
|
"util-deprecate": "^1.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/json5": {
|
"@types/json5": {
|
||||||
"version": "0.0.29",
|
"version": "0.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||||
|
@ -6206,6 +6271,18 @@
|
||||||
"p-locate": "^5.0.0"
|
"p-locate": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lodash.castarray": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"lodash.merge": {
|
"lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.8",
|
||||||
"@types/sanitize-html": "^2.8.0",
|
"@types/sanitize-html": "^2.8.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
import Footer from './../components/Footer'
|
||||||
|
import Donate from './../components/Donate'
|
||||||
|
import Header from './../components/Header'
|
||||||
|
import Head from 'next/head'
|
||||||
|
|
||||||
|
export default function Privacy() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Followgraph for Mastodon</title>
|
||||||
|
<meta name="description" content="Privacy policy" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{`{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"url": "https://followgraph.vercel.app/",
|
||||||
|
"image": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"@id": "https://followgraph.vercel.app/#/schema/ImageObject/FollowGraphThumbnail",
|
||||||
|
"url": "/ldjson-logo.jpg",
|
||||||
|
"contentUrl": "/ldjson-logo.jpg",
|
||||||
|
"caption": "Followgraph for Mastodon",
|
||||||
|
"width": 345,
|
||||||
|
"height": 345
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
</script>
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
<div>
|
||||||
|
<Header selected="privacy" />
|
||||||
|
<section className="pt-32 dark:bg-gray-800">
|
||||||
|
<div className="prose dark:prose-invert max-w-5xl px-10">
|
||||||
|
<h1>tl;dr</h1>
|
||||||
|
<p>
|
||||||
|
Please refer to the full Privacy Policy below. The short summary
|
||||||
|
is that Followgraph is a static website which{' '}
|
||||||
|
<strong>
|
||||||
|
sets no cookies and requires no authentication. The only data
|
||||||
|
gathered is through{' '}
|
||||||
|
<a href="https://vercel.com/analytics">Vercel Analytics</a>
|
||||||
|
</strong>
|
||||||
|
, which gathers aggregated visitor and demographic statistics
|
||||||
|
about site visitors in a privacy-sensitive way.
|
||||||
|
</p>
|
||||||
|
<h1>Privacy Policy for Followgraph for Mastodon</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
At Followgraph for Mastodon, accessible from
|
||||||
|
https://followgraph.vercel.app/, one of our main priorities is the
|
||||||
|
privacy of our visitors. This Privacy Policy document contains
|
||||||
|
types of information that is collected and recorded by Followgraph
|
||||||
|
for Mastodon and how we use it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you have additional questions or require more information about
|
||||||
|
our Privacy Policy, do not hesitate to contact us.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Log Files</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Followgraph for Mastodon follows a standard procedure of using log
|
||||||
|
files. These files log visitors when they visit websites. All
|
||||||
|
hosting companies do this and a part of hosting services'
|
||||||
|
analytics. The information collected by log files include internet
|
||||||
|
protocol (IP) addresses, browser type, Internet Service Provider
|
||||||
|
(ISP), date and time stamp, referring/exit pages, and possibly the
|
||||||
|
number of clicks. These are not linked to any information that is
|
||||||
|
personally identifiable. The purpose of the information is for
|
||||||
|
analyzing trends, administering the site, tracking users'
|
||||||
|
movement on the website, and gathering demographic information.
|
||||||
|
Our Privacy Policy was created with the help of the{' '}
|
||||||
|
<a href="https://www.privacypolicyonline.com/privacy-policy-generator/">
|
||||||
|
Privacy Policy Generator
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Third Party Privacy Policies</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Followgraph for Mastodon's Privacy Policy does not apply to
|
||||||
|
other websites. In particular, Followgraph uses Vercel Analytics
|
||||||
|
to gather basic information about site usage. You can find Vercel
|
||||||
|
Analytics' Privacy Policy here:
|
||||||
|
https://vercel.com/legal/privacy-policy.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Children's Information</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Another part of our priority is adding protection for children
|
||||||
|
while using the internet. We encourage parents and guardians to
|
||||||
|
observe, participate in, and/or monitor and guide their online
|
||||||
|
activity.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Followgraph for Mastodon does not knowingly collect any Personal
|
||||||
|
Identifiable Information from children under the age of 13. If you
|
||||||
|
think that your child provided this kind of information on our
|
||||||
|
website, we strongly encourage you to contact us immediately and
|
||||||
|
we will do our best efforts to promptly remove such information
|
||||||
|
from our records.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Online Privacy Policy Only</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This Privacy Policy applies only to our online activities and is
|
||||||
|
valid for visitors to our website with regards to the information
|
||||||
|
that they shared and/or collect in Followgraph for Mastodon. This
|
||||||
|
policy is not applicable to any information collected offline or
|
||||||
|
via channels other than this website.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Consent</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
By using our website, you hereby consent to our Privacy Policy and
|
||||||
|
agree to its Terms and Conditions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -7,5 +7,5 @@ module.exports = {
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require('@tailwindcss/typography')],
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue