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 debounce from 'debounce'
|
||||
|
||||
|
@ -11,7 +12,11 @@ type AccountDetails = {
|
|||
id: string
|
||||
acct: string
|
||||
followed_by: Set<string> // list of handles
|
||||
followers_count: number
|
||||
discoverable: boolean
|
||||
display_name: string
|
||||
note: string
|
||||
avatar_static: string
|
||||
}
|
||||
|
||||
async function usernameToId(
|
||||
|
@ -166,6 +171,23 @@ function getNextPage(linkHeader: string | null): string | 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({}) {
|
||||
const [handle, setHandle] = useState('')
|
||||
const [follows, setFollows] = useState<Array<AccountDetails>>([])
|
||||
|
@ -264,16 +286,10 @@ export function Content({}) {
|
|||
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}
|
||||
<Spinner
|
||||
visible={isLoading}
|
||||
className="w-4 h-4 ml-2 fill-white"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isLoading ? (
|
||||
|
@ -313,24 +329,7 @@ export function Content({}) {
|
|||
</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>
|
||||
<Results follows={follows} domain={domain} />
|
||||
) : null}
|
||||
|
||||
<ErrorLog errors={errors} />
|
||||
|
@ -339,87 +338,96 @@ export function Content({}) {
|
|||
)
|
||||
}
|
||||
|
||||
function AccountDetails({ account, mainDomain }) {
|
||||
const {
|
||||
avatar_static,
|
||||
display_name,
|
||||
acct,
|
||||
note,
|
||||
followers_count,
|
||||
followed_by,
|
||||
} = account
|
||||
let formatter = Intl.NumberFormat('en', { notation: 'compact' })
|
||||
let numFollowers = formatter.format(followers_count)
|
||||
const AccountDetails = memo(
|
||||
({
|
||||
account,
|
||||
mainDomain,
|
||||
}: {
|
||||
account: AccountDetails
|
||||
mainDomain: string
|
||||
}) => {
|
||||
const {
|
||||
avatar_static,
|
||||
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 (
|
||||
<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-shrink-0 m-auto">
|
||||
|
||||
<img
|
||||
className="w-16 h-16 sm:w-8 sm: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>
|
||||
<div className="flex flex-col sm:flex-row text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="truncate">{acct}</span>
|
||||
<span className="sm:inline hidden whitespace-pre"> | </span>
|
||||
<span>{numFollowers} followers</span>
|
||||
return (
|
||||
<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-shrink-0 m-auto">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
className="w-16 h-16 sm:w-8 sm: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>
|
||||
<div className="flex flex-col sm:flex-row text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="truncate">{acct}</span>
|
||||
<span className="sm:inline hidden whitespace-pre"> | </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>
|
||||
<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>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
)
|
||||
AccountDetails.displayName = 'AccountDetails'
|
||||
|
||||
function ErrorLog({ errors }: { errors: Array<string> }) {
|
||||
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 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
|
||||
as it would facilitate inorganic and potentially malicious behaviour.
|
||||
While it would be a useful feature, Followgraph does <em>not</em>{' '}
|
||||
plan to offer this functionality as it would facilitate inorganic
|
||||
and potentially malicious behaviour.
|
||||
</FAQItem>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,12 +17,15 @@ export default function Footer({}) {
|
|||
@gabipurcaru@mastodon.online
|
||||
</Link>
|
||||
.<br />
|
||||
<Link
|
||||
href="/donate"
|
||||
className="font-bold text-gray-900 dark:text-gray-400"
|
||||
>
|
||||
Donate
|
||||
</Link>
|
||||
<span className="text-sm font-bold text-gray-900 dark:text-gray-400 ">
|
||||
<Link href="/privacy" className="underline">
|
||||
Privacy
|
||||
</Link>{' '}
|
||||
|{' '}
|
||||
<Link href="/donate" className="underline">
|
||||
Donate
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<span className="block text-sm text-center text-gray-500 dark:text-gray-400">
|
||||
Built with{' '}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
export default function Header({ selected }: { selected: 'home' | 'donate' }) {
|
||||
export default function Header({
|
||||
selected,
|
||||
}: {
|
||||
selected: 'home' | 'donate' | 'privacy'
|
||||
}) {
|
||||
return (
|
||||
<header className="fixed w-full">
|
||||
<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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/sanitize-html": "^2.8.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
|
@ -387,6 +388,34 @@
|
|||
"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": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
|
@ -2773,6 +2802,18 @@
|
|||
"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": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
|
@ -4518,6 +4559,30 @@
|
|||
"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": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
|
@ -6206,6 +6271,18 @@
|
|||
"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": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"typescript": "4.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/sanitize-html": "^2.8.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"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: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue