kopia lustrzana https://github.com/Stopka/fedisearch
Reskinned to bootstrap, shorted char limit in feed search
rodzic
1c0fd9f21e
commit
8091609456
|
@ -1,11 +1,19 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
webpack (config) {
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: ['@svgr/webpack']
|
||||||
|
})
|
||||||
|
|
||||||
|
return config
|
||||||
|
},
|
||||||
async redirects () {
|
async redirects () {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: '/',
|
source: '/',
|
||||||
destination: '/feeds',
|
destination: '/feeds',
|
||||||
permanent: true,
|
permanent: true
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
Plik diff jest za duży
Load Diff
|
@ -15,18 +15,25 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@datapunt/matomo-tracker-js": "^0.5.1",
|
"@datapunt/matomo-tracker-js": "^0.5.1",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.17",
|
||||||
"@hookform/resolvers": "^2.8.5",
|
"@hookform/resolvers": "^2.8.5",
|
||||||
"@prisma/client": "^3.6.0",
|
"@prisma/client": "^3.6.0",
|
||||||
|
"@svgr/webpack": "^6.2.1",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
|
"bootstrap": "^5.1.3",
|
||||||
"next": "^12.0.7",
|
"next": "^12.0.7",
|
||||||
|
"node-cache": "^5.1.2",
|
||||||
"npmlog": "^6.0.0",
|
"npmlog": "^6.0.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"sass": "^1.45.1",
|
"sass": "^1.45.1",
|
||||||
"striptags": "^3.2.0",
|
"striptags": "^3.2.0",
|
||||||
"typescript-collections": "^1.3.3",
|
"typescript-collections": "^1.3.3",
|
||||||
"zod": "^3.11.6",
|
"zod": "^3.11.6"
|
||||||
"node-cache": "^5.1.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/eslint-plugin-next": "^12.0.7",
|
"@next/eslint-plugin-next": "^12.0.7",
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import striptags from 'striptags'
|
||||||
|
import Avatar from './Avatar'
|
||||||
|
import SoftwareBadge from './badges/SoftwareBadge'
|
||||||
|
import FeedTypeBadge from './badges/FeedTypeBadge'
|
||||||
|
import CreatedAtBadge from './badges/CreatedAtBadge'
|
||||||
|
import LastPostAtBadge from './badges/LastPostAtBadge'
|
||||||
|
import BotBadge from './badges/BotBadge'
|
||||||
|
import { FeedResponseField, FeedResponseItem } from '../types/FeedResponse'
|
||||||
|
import ParentFeed from './ParentFeed'
|
||||||
|
import StatusesCountBadge from './badges/StatusesCountBadge'
|
||||||
|
import FollowersBadge from './badges/FollowersBadge'
|
||||||
|
import FollowingBadge from './badges/FollowingBadge'
|
||||||
|
|
||||||
|
const FeedResult: React.FC<{ feed: FeedResponseItem }> = ({ feed }) => {
|
||||||
|
const fallbackEmojiImage = '/emoji.svg'
|
||||||
|
|
||||||
|
const handleEmojiImageError = (event) => {
|
||||||
|
event.target.src = fallbackEmojiImage
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.querySelectorAll('.with-emoji img').forEach(element => {
|
||||||
|
if (element.attributes['data-error-handler']) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
element.addEventListener('error', handleEmojiImageError)
|
||||||
|
element.setAttribute('data-error-handler', 'attached')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card feed g-col-12 mb-3">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className={'card-title with-emoji display-name'}>
|
||||||
|
<a href={feed.url}
|
||||||
|
dangerouslySetInnerHTML={{ __html: striptags(feed.displayName !== '' ? feed.displayName : feed.name, ['img']) }}
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<Avatar url={feed.avatar}/>
|
||||||
|
<div className={'address'}>
|
||||||
|
<span>{feed.name}@{feed.node.domain}</span>
|
||||||
|
<ParentFeed feed={feed.parentFeed}/>
|
||||||
|
</div>
|
||||||
|
<SoftwareBadge softwareName={feed.node.softwareName}/>
|
||||||
|
<div className={'badges'}>
|
||||||
|
<FeedTypeBadge type={feed.type}/>
|
||||||
|
<FollowersBadge followers={feed.followersCount} />
|
||||||
|
<FollowingBadge following={feed.followingCount} />
|
||||||
|
<StatusesCountBadge statusesCount={feed.statusesCount}/>
|
||||||
|
<CreatedAtBadge createdAt={feed.createdAt}/>
|
||||||
|
<LastPostAtBadge lastStatusAt={feed.lastStatusAt}/>
|
||||||
|
<BotBadge bot={feed.bot}/>
|
||||||
|
</div>
|
||||||
|
{feed.fields.length > 0
|
||||||
|
? (
|
||||||
|
<div className={'table-responsive fields'}>
|
||||||
|
<table className={'table'}>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
feed.fields.map((field: FeedResponseField, index: number): React.ReactNode => {
|
||||||
|
return (
|
||||||
|
<tr key={index}>
|
||||||
|
<th className={'with-emoji table-active'}
|
||||||
|
dangerouslySetInnerHTML={{ __html: striptags(field.name, ['a', 'strong', 'em', 'img']) }}/>
|
||||||
|
<td className={'with-emoji'}
|
||||||
|
dangerouslySetInnerHTML={{ __html: striptags(field.value, ['a', 'strong', 'em', 'img']) }}/>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: ''}
|
||||||
|
<div className={'description with-emoji'}
|
||||||
|
dangerouslySetInnerHTML={{ __html: striptags(feed.description, ['img', 'p', 'strong', 'em', 'br', 'a']) }}/>
|
||||||
|
</div>
|
||||||
|
</section>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FeedResult
|
|
@ -1,25 +1,23 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Result from './Result'
|
import FeedResult from './FeedResult'
|
||||||
import { FeedResponseItem } from '../types/FeedResponse'
|
import { FeedResponseItem } from '../types/FeedResponse'
|
||||||
|
|
||||||
const Results:React.FC<{feeds:FeedResponseItem[]}> = ({ feeds }) => {
|
const FeedResults:React.FC<{feeds:FeedResponseItem[]}> = ({ feeds }) => {
|
||||||
if (feeds.length === 0) {
|
if (feeds.length === 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>Nothing found</h2>
|
|
||||||
<p className={'no-results'}>We have no results for your query.</p>
|
<p className={'no-results'}>We have no results for your query.</p>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (<>
|
return (<div className={'grid'}>
|
||||||
<h2>Results</h2>
|
|
||||||
{
|
{
|
||||||
feeds.map((feed, index) => {
|
feeds.map((feed, index) => {
|
||||||
console.info('feed', feed)
|
console.info('feed', feed)
|
||||||
return (<Result key={index} feed={feed}/>)
|
return (<FeedResult key={index} feed={feed}/>)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</>)
|
</div>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Results
|
export default FeedResults
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
|
|
||||||
const Footer:React.FC = () => {
|
const Footer:React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<footer>
|
<footer className={'text-center mt-5'}>
|
||||||
©{(new Date()).getFullYear()} <a href={'https://skorpil.cz'}>Štěpán Škorpil</a>
|
©{(new Date()).getFullYear()} <a href={'https://skorpil.cz'}>Štěpán Škorpil</a>
|
||||||
</footer>
|
</footer>
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Head from 'next/head'
|
||||||
import Footer from './Footer'
|
import Footer from './Footer'
|
||||||
import getMatomo from '../lib/getMatomo'
|
import getMatomo from '../lib/getMatomo'
|
||||||
import { UserOptions } from '@datapunt/matomo-tracker-js/es/types'
|
import { UserOptions } from '@datapunt/matomo-tracker-js/es/types'
|
||||||
import NavItem from './NavItem'
|
import NavBar from './NavBar'
|
||||||
|
|
||||||
export const siteTitle = 'FediSearch'
|
export const siteTitle = 'FediSearch'
|
||||||
export const siteDescription = 'Search people on Fediverse'
|
export const siteDescription = 'Search people on Fediverse'
|
||||||
|
@ -24,49 +24,14 @@ const Layout: React.FC<{ matomoConfig: UserOptions, children: React.ReactNode }>
|
||||||
<meta property="og:image" content="/fedisearch.png"/>
|
<meta property="og:image" content="/fedisearch.png"/>
|
||||||
<meta property="og:type" content="website"/>
|
<meta property="og:type" content="website"/>
|
||||||
</Head>
|
</Head>
|
||||||
<header>
|
<div className="container">
|
||||||
<h1><a href={'/'}>FediSearch</a></h1>
|
<NavBar />
|
||||||
<a href={'/'} className={'logo'}>
|
|
||||||
<img
|
|
||||||
src="/fedisearch.svg"
|
|
||||||
alt={'FediSearch logo'}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<NavItem path={'/feeds'} label={'Search people'} icon={(
|
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="user"
|
|
||||||
className="svg-inline--fa fa-user fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 448 512">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"/>
|
|
||||||
</svg>
|
|
||||||
)} />
|
|
||||||
<NavItem path={'/nodes'} label={'Search servers'} icon={(
|
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="server"
|
|
||||||
className="svg-inline--fa fa-server fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 512 512">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M480 160H32c-17.673 0-32-14.327-32-32V64c0-17.673 14.327-32 32-32h448c17.673 0 32 14.327 32 32v64c0 17.673-14.327 32-32 32zm-48-88c-13.255 0-24 10.745-24 24s10.745 24 24 24 24-10.745 24-24-10.745-24-24-24zm-64 0c-13.255 0-24 10.745-24 24s10.745 24 24 24 24-10.745 24-24-10.745-24-24-24zm112 248H32c-17.673 0-32-14.327-32-32v-64c0-17.673 14.327-32 32-32h448c17.673 0 32 14.327 32 32v64c0 17.673-14.327 32-32 32zm-48-88c-13.255 0-24 10.745-24 24s10.745 24 24 24 24-10.745 24-24-10.745-24-24-24zm-64 0c-13.255 0-24 10.745-24 24s10.745 24 24 24 24-10.745 24-24-10.745-24-24-24zm112 248H32c-17.673 0-32-14.327-32-32v-64c0-17.673 14.327-32 32-32h448c17.673 0 32 14.327 32 32v64c0 17.673-14.327 32-32 32zm-48-88c-13.255 0-24 10.745-24 24s10.745 24 24 24 24-10.745 24-24-10.745-24-24-24zm-64 0c-13.255 0-24 10.745-24 24s10.745 24 24 24 24-10.745 24-24-10.745-24-24-24z"/>
|
|
||||||
</svg>
|
|
||||||
)} />
|
|
||||||
<NavItem path={'/stats'} label={'Index stats'} icon={(
|
|
||||||
|
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="chart-pie"
|
|
||||||
className="svg-inline--fa fa-chart-pie fa-w-17" role="img"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 544 512">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M527.79 288H290.5l158.03 158.03c6.04 6.04 15.98 6.53 22.19.68 38.7-36.46 65.32-85.61 73.13-140.86 1.34-9.46-6.51-17.85-16.06-17.85zm-15.83-64.8C503.72 103.74 408.26 8.28 288.8.04 279.68-.59 272 7.1 272 16.24V240h223.77c9.14 0 16.82-7.68 16.19-16.8zM224 288V50.71c0-9.55-8.39-17.4-17.84-16.06C86.99 51.49-4.1 155.6.14 280.37 4.5 408.51 114.83 513.59 243.03 511.98c50.4-.63 96.97-16.87 135.26-44.03 7.9-5.6 8.42-17.23 1.57-24.08L224 288z"/>
|
|
||||||
</svg>
|
|
||||||
)} />
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<main>
|
<main>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
<Footer/>
|
<Footer/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,45 +1,59 @@
|
||||||
import React, { ReactNode } from 'react'
|
import React, { ReactNode } from 'react'
|
||||||
|
import Spinner from './Spinner'
|
||||||
|
|
||||||
const Loader:React.FC<{ children: ReactNode, loading: boolean, hideContent?:boolean, table?:number, showTop?:boolean, showBottom?:boolean }> = ({ showTop, showBottom, hideContent, children, table, loading }) => {
|
const Loader: React.FC<{ children: ReactNode, loading: boolean, hideContent?: boolean, table?: number, showTop?: boolean, showBottom?: boolean }> = ({
|
||||||
|
showTop,
|
||||||
|
showBottom,
|
||||||
|
hideContent,
|
||||||
|
children,
|
||||||
|
table,
|
||||||
|
loading
|
||||||
|
}) => {
|
||||||
const className = 'loader' + (loading ? ' -loading' : '')
|
const className = 'loader' + (loading ? ' -loading' : '')
|
||||||
const loaderVisual = (
|
|
||||||
<div className={'loader-visualisation'}>
|
const spinner = (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className='loader-graphics' width="34" height="34">
|
<div className={'d-flex justify-content-center'}>
|
||||||
<path className="rail" d="m 16.977523,0.24095147 c -9.2629169,0 -16.73280045,7.51449143 -16.73280045,16.77740253 0,9.262912 7.46988355,16.777403 16.73280045,16.777403 9.262917,0 16.777413,-7.514491 16.777413,-16.777403 0,-9.2629111 -7.514496,-16.77740253 -16.777413,-16.77740253 z m 0,4.14972823 c 6.966927,0 12.627682,5.6607523 12.627682,12.6276743 0,6.966923 -5.660755,12.583053 -12.627682,12.583053 -6.966937,0 -12.5830596,-5.61613 -12.5830596,-12.583053 0,-6.966922 5.6161226,-12.6276743 12.5830596,-12.6276743 z" />
|
<Spinner/>
|
||||||
<path className="train" d="M 31.677259,17.003529 A 14.680208,14.680199 0 0 1 20.796571,31.183505" />
|
|
||||||
</svg>
|
|
||||||
<span>Loading...</span>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (table) {
|
if (table) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showTop && loading
|
{showTop && loading
|
||||||
? (
|
? (
|
||||||
|
<tbody>
|
||||||
<tr className={className}>
|
<tr className={className}>
|
||||||
<td colSpan={table}>{loaderVisual}</td>
|
<td colSpan={table}>
|
||||||
|
{spinner}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
)
|
)
|
||||||
: ''}
|
: ''}
|
||||||
{hideContent && loading ? '' : children}
|
{hideContent && loading ? '' : children}
|
||||||
{showBottom && loading
|
{showBottom && loading
|
||||||
? (
|
? (
|
||||||
|
<tbody>
|
||||||
<tr className={className}>
|
<tr className={className}>
|
||||||
<td colSpan={table}>{loaderVisual}</td>
|
<td colSpan={table}>
|
||||||
|
<div className={'d-flex justify-content-center'}>
|
||||||
|
<Spinner/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
)
|
)
|
||||||
: ''}
|
: ''}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<>
|
||||||
{showTop && loading ? loaderVisual : ''}
|
{showTop && loading ? spinner : ''}
|
||||||
<div className={'loader-content'}>
|
|
||||||
{hideContent && loading ? '' : children}
|
{hideContent && loading ? '' : children}
|
||||||
</div>
|
{showBottom && loading ? spinner : ''}
|
||||||
{showBottom && loading ? loaderVisual : ''}
|
</>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import NavItem from './NavItem'
|
||||||
|
import { faUser, faServer, faChartPie } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
const NavBar:React.FC = () => {
|
||||||
|
const [showMenu, setShowMenu] = useState<boolean>(false)
|
||||||
|
return (
|
||||||
|
<nav className="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
||||||
|
<div className="container-fluid">
|
||||||
|
<a className="navbar-brand" href={'/'}>
|
||||||
|
<img
|
||||||
|
src="/fedisearch.svg"
|
||||||
|
alt={'FediSearch logo'}
|
||||||
|
className="d-inline-block align-text-top logo"
|
||||||
|
/>
|
||||||
|
<span>FediSearch</span>
|
||||||
|
</a>
|
||||||
|
<button className="navbar-toggler" type="button" aria-controls="navbarSupportedContent"
|
||||||
|
aria-expanded="false" aria-label="Toggle navigation" onClick={() => setShowMenu(!showMenu)}>
|
||||||
|
<span className="navbar-toggler-icon"/>
|
||||||
|
</button>
|
||||||
|
<div className={'collapse navbar-collapse' + (showMenu ? ' show' : '')}>
|
||||||
|
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<NavItem path={'/feeds'} label={'People'} icon={faUser} />
|
||||||
|
<NavItem path={'/nodes'} label={'Servers'} icon={faServer} />
|
||||||
|
<NavItem path={'/stats'} label={'Stats'} icon={faChartPie} />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavBar
|
|
@ -1,14 +1,17 @@
|
||||||
import React, { FC, ReactElement } from 'react'
|
import React, { FC } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
import { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
const NavItem:FC<{path:string, label:string, icon:ReactElement}> = ({ path, label, icon }) => {
|
const NavItem:FC<{path:string, label:string, icon:IconProp}> = ({ path, label, icon }) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const active = router.pathname === path
|
||||||
return (
|
return (
|
||||||
<li>
|
<li className={'nav-item'}>
|
||||||
<Link href={path}>
|
<Link href={path}>
|
||||||
<a className={router.pathname === path ? 'active' : ''}>
|
<a className={'nav-link' + (active ? ' active' : '')} aria-current={active ? 'page' : undefined}>
|
||||||
{icon}
|
<FontAwesomeIcon icon={icon} className={'margin-right'} />
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -5,9 +5,9 @@ const ProgressBar: React.FC<{ percents: number, way?: 'left' | 'right' | 'top' |
|
||||||
percents = Math.round(percents)
|
percents = Math.round(percents)
|
||||||
color = color ?? 'var(--accent-color)'
|
color = color ?? 'var(--accent-color)'
|
||||||
return (
|
return (
|
||||||
<div className={'progressbar'} style={{
|
<div className="progress justify-content-end">
|
||||||
background: `linear-gradient(to ${way}, ${color} ${percents}%, transparent ${percents}%`
|
<div className="progress-bar" role="progressbar" style={{ width: `${percents}%` }} />
|
||||||
}}/>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
import React, { useEffect } from 'react'
|
|
||||||
import striptags from 'striptags'
|
|
||||||
import Avatar from './Avatar'
|
|
||||||
import SoftwareBadge from './badges/SoftwareBadge'
|
|
||||||
import FeedTypeBadge from './badges/FeedTypeBadge'
|
|
||||||
import SubscriptionsBadge from './badges/SubscriptionsBadge'
|
|
||||||
import CreatedAtBadge from './badges/CreatedAtBadge'
|
|
||||||
import LastPostAtBadge from './badges/LastPostAtBadge'
|
|
||||||
import BotBadge from './badges/BotBadge'
|
|
||||||
import { FeedResponseField, FeedResponseItem } from '../types/FeedResponse'
|
|
||||||
import ParentFeed from './ParentFeed'
|
|
||||||
import StatusesCountBadge from './badges/StatusesCountBadge'
|
|
||||||
|
|
||||||
const Result:React.FC<{ feed:FeedResponseItem }> = ({ feed }) => {
|
|
||||||
const fallbackEmojiImage = '/emoji.svg'
|
|
||||||
|
|
||||||
const handleEmojiImageError = (event) => {
|
|
||||||
event.target.src = fallbackEmojiImage
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.querySelectorAll('.with-emoji img').forEach(element => {
|
|
||||||
if (element.attributes['data-error-handler']) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
element.addEventListener('error', handleEmojiImageError)
|
|
||||||
element.setAttribute('data-error-handler', 'attached')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className={'feed'}>
|
|
||||||
<h3 className={'display-name with-emoji'}>
|
|
||||||
<a href={feed.url}
|
|
||||||
dangerouslySetInnerHTML={{ __html: striptags(feed.displayName !== '' ? feed.displayName : feed.name, ['img']) }}
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
<Avatar url={feed.avatar}/>
|
|
||||||
<div className={'address'}>
|
|
||||||
<span>{feed.name}@{feed.node.domain}</span>
|
|
||||||
<ParentFeed feed={feed.parentFeed}/>
|
|
||||||
</div>
|
|
||||||
<div className={'badges'}>
|
|
||||||
<SoftwareBadge softwareName={feed.node.softwareName}/>
|
|
||||||
<FeedTypeBadge type={feed.type}/>
|
|
||||||
<SubscriptionsBadge followingCount={feed.followingCount} followersCount={feed.followersCount}/>
|
|
||||||
<StatusesCountBadge statusesCount={feed.statusesCount}/>
|
|
||||||
<CreatedAtBadge createdAt={feed.createdAt}/>
|
|
||||||
<LastPostAtBadge lastStatusAt={feed.lastStatusAt}/>
|
|
||||||
<BotBadge bot={feed.bot}/>
|
|
||||||
</div>
|
|
||||||
{feed.fields.length > 0
|
|
||||||
? (
|
|
||||||
<table className={'fields'}>
|
|
||||||
{
|
|
||||||
feed.fields.map((field:FeedResponseField, index:number):React.ReactNode => {
|
|
||||||
return (
|
|
||||||
<tr key={index}>
|
|
||||||
<th className={'with-emoji'}
|
|
||||||
dangerouslySetInnerHTML={{ __html: striptags(field.name, ['a', 'strong', 'em', 'img']) }}/>
|
|
||||||
<td className={'with-emoji'}
|
|
||||||
dangerouslySetInnerHTML={{ __html: striptags(field.value, ['a', 'strong', 'em', 'img']) }}/>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</table>
|
|
||||||
)
|
|
||||||
: ''}
|
|
||||||
<div className={'description with-emoji'}
|
|
||||||
dangerouslySetInnerHTML={{ __html: striptags(feed.description, ['img', 'p', 'strong', 'em', 'br', 'a']) }}/>
|
|
||||||
</section>)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Result
|
|
|
@ -1,5 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Sort } from '../types/Sort'
|
import { Sort } from '../types/Sort'
|
||||||
|
import { faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
const SortToggle: React.FC<{
|
const SortToggle: React.FC<{
|
||||||
onToggle:(StatsRequestSortBy)=>void,
|
onToggle:(StatsRequestSortBy)=>void,
|
||||||
|
@ -11,23 +13,13 @@ const SortToggle: React.FC<{
|
||||||
<span>{children}</span>
|
<span>{children}</span>
|
||||||
{sort.sortBy === field && sort.sortWay === 'asc'
|
{sort.sortBy === field && sort.sortWay === 'asc'
|
||||||
? (
|
? (
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sort-up"
|
<FontAwesomeIcon icon={faSortUp} className={'margin-left'} />
|
||||||
className="sort-icon svg-inline--fa fa-sort-up fa-w-10" role="img" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 320 512">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M279 224H41c-21.4 0-32.1-25.9-17-41L143 64c9.4-9.4 24.6-9.4 33.9 0l119 119c15.2 15.1 4.5 41-16.9 41z"/>
|
|
||||||
</svg>
|
|
||||||
)
|
)
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
{sort.sortBy === field && sort.sortWay === 'desc'
|
{sort.sortBy === field && sort.sortWay === 'desc'
|
||||||
? (
|
? (
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sort-down"
|
<FontAwesomeIcon icon={faSortDown} className={'margin-left'} />
|
||||||
className="sort-icon svg-inline--fa fa-sort-down fa-w-10" role="img" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 320 512">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41z"/>
|
|
||||||
</svg>
|
|
||||||
)
|
)
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Spinner: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="spinner-border" role="status">
|
||||||
|
<span className="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Spinner
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
|
||||||
|
const Badge:React.FC<{ faIcon:IconProp, label:string, value:string|number|null, className?:string, showUnknown?:boolean }> = ({ faIcon, label, value, className, showUnknown }) => {
|
||||||
|
if (value === null && showUnknown !== true) {
|
||||||
|
return (<></>)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={'badge bg-secondary ' + className} title={label}>
|
||||||
|
<FontAwesomeIcon icon={faIcon} className={'margin-right'}/>
|
||||||
|
<span className="visually-hidden">{label}:</span>
|
||||||
|
<span className={'value'}>{value === null ? '?' : value}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default Badge
|
|
@ -1,17 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { faRobot } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import Badge from './Badge'
|
||||||
|
|
||||||
const BotBadge:React.FC<{ bot: boolean | null}> = ({ bot }) => {
|
const BotBadge:React.FC<{ bot: boolean | null}> = ({ bot }) => {
|
||||||
return (
|
return (
|
||||||
<div className={' badge bot'} title={'Bot'}>
|
<Badge faIcon={faRobot}
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="robot"
|
label={'Bot'}
|
||||||
className="svg-inline--fa fa-robot fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg"
|
value={bot !== null ? (bot ? 'Yes' : 'No') : null}
|
||||||
viewBox="0 0 640 512">
|
className={'bot'}
|
||||||
<path fill="currentColor"
|
/>
|
||||||
d="M32,224H64V416H32A31.96166,31.96166,0,0,1,0,384V256A31.96166,31.96166,0,0,1,32,224Zm512-48V448a64.06328,64.06328,0,0,1-64,64H160a64.06328,64.06328,0,0,1-64-64V176a79.974,79.974,0,0,1,80-80H288V32a32,32,0,0,1,64,0V96H464A79.974,79.974,0,0,1,544,176ZM264,256a40,40,0,1,0-40,40A39.997,39.997,0,0,0,264,256Zm-8,128H192v32h64Zm96,0H288v32h64ZM456,256a40,40,0,1,0-40,40A39.997,39.997,0,0,0,456,256Zm-8,128H384v32h64ZM640,256V384a31.96166,31.96166,0,0,1-32,32H576V224h32A31.96166,31.96166,0,0,1,640,256Z"/>
|
|
||||||
</svg>
|
|
||||||
<span className={'label'}>Bot</span>
|
|
||||||
<span className={'value'}>{bot !== null ? (bot ? 'Yes' : 'No') : '?'}</span>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export default BotBadge
|
export default BotBadge
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { faCalendarPlus } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import Badge from './Badge'
|
||||||
|
|
||||||
const CreatedAtBadge:React.FC<{ createdAt: string | null }> = ({ createdAt }) => {
|
const CreatedAtBadge:React.FC<{ createdAt: string | null }> = ({ createdAt }) => {
|
||||||
return (
|
return (
|
||||||
<div className={'badge created-at'} title={'Created at'}>
|
<Badge faIcon={faCalendarPlus}
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="calendar-plus"
|
label={'Created at'}
|
||||||
className="svg-inline--fa fa-calendar-plus fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg"
|
value={createdAt !== null ? (new Date(createdAt)).toLocaleDateString() : null}
|
||||||
viewBox="0 0 448 512">
|
className={'created-at'}
|
||||||
<path fill="currentColor"
|
/>
|
||||||
d="M336 292v24c0 6.6-5.4 12-12 12h-76v76c0 6.6-5.4 12-12 12h-24c-6.6 0-12-5.4-12-12v-76h-76c-6.6 0-12-5.4-12-12v-24c0-6.6 5.4-12 12-12h76v-76c0-6.6 5.4-12 12-12h24c6.6 0 12 5.4 12 12v76h76c6.6 0 12 5.4 12 12zm112-180v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h48V12c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v52h128V12c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v52h48c26.5 0 48 21.5 48 48zm-48 346V160H48v298c0 3.3 2.7 6 6 6h340c3.3 0 6-2.7 6-6z"/>
|
|
||||||
</svg>
|
|
||||||
<span className={'label'}>Created at</span>
|
|
||||||
<span className={'value'}>{createdAt !== null ? (new Date(createdAt)).toLocaleDateString() : '?'}</span>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export default CreatedAtBadge
|
export default CreatedAtBadge
|
||||||
|
|
|
@ -1,33 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { faRss, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import Badge from './Badge'
|
||||||
|
|
||||||
const FeedTypeBadge:React.FC<{ type: 'account' | 'channel' }> = ({ type }) => {
|
const FeedTypeBadge:React.FC<{ type: 'account' | 'channel' }> = ({ type }) => {
|
||||||
if (type === 'channel') {
|
|
||||||
return (
|
return (
|
||||||
<div className={'badge feed-type'} title={'Feed type'}>
|
<Badge faIcon={type === 'channel' ? faRss : faUser}
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="rss"
|
label={'Feed type'}
|
||||||
className="feed-type svg-inline--fa fa-rss fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg"
|
value={type === 'channel' ? 'Channel' : 'Account'}
|
||||||
viewBox="0 0 448 512">
|
className={'feed-type'}
|
||||||
<path fill="currentColor"
|
/>
|
||||||
d="M128.081 415.959c0 35.369-28.672 64.041-64.041 64.041S0 451.328 0 415.959s28.672-64.041 64.041-64.041 64.04 28.673 64.04 64.041zm175.66 47.25c-8.354-154.6-132.185-278.587-286.95-286.95C7.656 175.765 0 183.105 0 192.253v48.069c0 8.415 6.49 15.472 14.887 16.018 111.832 7.284 201.473 96.702 208.772 208.772.547 8.397 7.604 14.887 16.018 14.887h48.069c9.149.001 16.489-7.655 15.995-16.79zm144.249.288C439.596 229.677 251.465 40.445 16.503 32.01 7.473 31.686 0 38.981 0 48.016v48.068c0 8.625 6.835 15.645 15.453 15.999 191.179 7.839 344.627 161.316 352.465 352.465.353 8.618 7.373 15.453 15.999 15.453h48.068c9.034-.001 16.329-7.474 16.005-16.504z"/>
|
|
||||||
<title>Channel</title>
|
|
||||||
</svg>
|
|
||||||
<span className={'label'}>Feed type</span>
|
|
||||||
<span className={'value'}>Channel</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={'badge feed-type'} title={'Feed type'}>
|
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="user"
|
|
||||||
className="feed-type svg-inline--fa fa-user fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 448 512">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"/>
|
|
||||||
<title>Account</title>
|
|
||||||
</svg>
|
|
||||||
<span className={'label'}>Feed type</span>
|
|
||||||
<span className={'value'}>Account</span>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { faUserFriends } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import React from 'react'
|
||||||
|
import Badge from './Badge'
|
||||||
|
|
||||||
|
const FollowersBadge:React.FC<{ followers: number|null}> = ({ followers }) => {
|
||||||
|
return (
|
||||||
|
<Badge faIcon={faUserFriends}
|
||||||
|
label={'Followers'}
|
||||||
|
value={followers}
|
||||||
|
className={'followers'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default FollowersBadge
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { faEye } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import React from 'react'
|
||||||
|
import Badge from './Badge'
|
||||||
|
|
||||||
|
const FollowingBadge:React.FC<{ following: number|null}> = ({ following }) => {
|
||||||
|
return (
|
||||||
|
<Badge faIcon={faEye}
|
||||||
|
label={'Following'}
|
||||||
|
value={following}
|
||||||
|
className={'following'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default FollowingBadge
|
|
@ -1,17 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import Badge from './Badge'
|
||||||
|
import { faCalendarCheck } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
const LastPostAtBadge:React.FC<{ lastStatusAt: string | null }> = ({ lastStatusAt }) => {
|
const LastPostAtBadge:React.FC<{ lastStatusAt: string | null }> = ({ lastStatusAt }) => {
|
||||||
return (
|
return (
|
||||||
<div className={'badge last-status-at'} title={'Last status at'}>
|
<Badge faIcon={faCalendarCheck}
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="calendar-check"
|
label={'Last status at'}
|
||||||
className="svg-inline--fa fa-calendar-check fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg"
|
value={lastStatusAt !== null ? (new Date(lastStatusAt)).toLocaleDateString() : null}
|
||||||
viewBox="0 0 448 512">
|
className={'last-status-at'}
|
||||||
<path fill="currentColor"
|
/>
|
||||||
d="M400 64h-48V12c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v52H160V12c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v52H48C21.49 64 0 85.49 0 112v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V112c0-26.51-21.49-48-48-48zm-6 400H54a6 6 0 0 1-6-6V160h352v298a6 6 0 0 1-6 6zm-52.849-200.65L198.842 404.519c-4.705 4.667-12.303 4.637-16.971-.068l-75.091-75.699c-4.667-4.705-4.637-12.303.068-16.971l22.719-22.536c4.705-4.667 12.303-4.637 16.97.069l44.104 44.461 111.072-110.181c4.705-4.667 12.303-4.637 16.971.068l22.536 22.718c4.667 4.705 4.636 12.303-.069 16.97z"/>
|
|
||||||
</svg>
|
|
||||||
<span className={'label'}>Last status at</span>
|
|
||||||
<span className={'value'}>{lastStatusAt !== null ? (new Date(lastStatusAt)).toLocaleDateString() : '?'}</span>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ const SoftwareBadge:React.FC<{softwareName:string|null}> = ({ softwareName }) =>
|
||||||
event.target.src = fallbackImage
|
event.target.src = fallbackImage
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div className={'badge software-name'} title={'Software name'}>
|
return (<div className={'software-name'} title={'Software name'}>
|
||||||
<img className={'icon'}
|
<img className={'icon'}
|
||||||
src={softwareName !== null ? `/software/${softwareName}.svg` : fallbackImage}
|
src={softwareName !== null ? `/software/${softwareName}.svg` : fallbackImage}
|
||||||
alt={softwareName}
|
alt={softwareName}
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { faCommentAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import Badge from './Badge'
|
||||||
|
|
||||||
const StatusesCountBadge:React.FC<{ statusesCount: number | null }> = ({ statusesCount }) => {
|
const StatusesCountBadge:React.FC<{ statusesCount: number | null }> = ({ statusesCount }) => {
|
||||||
return (
|
return (
|
||||||
<div className={'badge last-status-at'} title={'Status count'}>
|
<Badge faIcon={faCommentAlt}
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="comment-alt"
|
label={'Status count'}
|
||||||
className="svg-inline--fa fa-comment-alt fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg"
|
value={statusesCount}
|
||||||
viewBox="0 0 512 512">
|
className={'last-status-at'}
|
||||||
<path fill="currentColor"
|
/>
|
||||||
d="M448 0H64C28.7 0 0 28.7 0 64v288c0 35.3 28.7 64 64 64h96v84c0 9.8 11.2 15.5 19.1 9.7L304 416h144c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64z"/>
|
|
||||||
</svg>
|
|
||||||
<span className={'label'}>Status count</span>
|
|
||||||
<span className={'value'}>{statusesCount !== null ? statusesCount : '?'}</span>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
const SubscriptionsBadge:React.FC<{ followingCount: number | null, followersCount: number | null }> = (
|
|
||||||
{ followingCount, followersCount }
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<div className={'badge subscriptions'}>
|
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye"
|
|
||||||
className="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 576 512">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"/>
|
|
||||||
<title>Subscriptions</title>
|
|
||||||
</svg>
|
|
||||||
<div className={'following'} title={'Following'}>
|
|
||||||
<span className={'label'}>Following</span>
|
|
||||||
<span className={'value'}>{followingCount !== null ? followingCount : '?'}</span>
|
|
||||||
</div>
|
|
||||||
<div className={'followers'} title={'Followers'}>
|
|
||||||
<span className={'label'}>Followers</span>
|
|
||||||
<span className={'value'}>{followersCount !== null ? followersCount : '?'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SubscriptionsBadge
|
|
|
@ -3,11 +3,13 @@ import React, { useEffect, useState } from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { feedResponseSchema } from '../types/FeedResponse'
|
import { feedResponseSchema } from '../types/FeedResponse'
|
||||||
import Loader from '../components/Loader'
|
import Loader from '../components/Loader'
|
||||||
import Results from '../components/Results'
|
import FeedResults from '../components/FeedResults'
|
||||||
import Layout, { siteTitle } from '../components/Layout'
|
import Layout, { siteTitle } from '../components/Layout'
|
||||||
import { matomoConfig } from '../lib/matomoConfig'
|
import { matomoConfig } from '../lib/matomoConfig'
|
||||||
import getMatomo from '../lib/getMatomo'
|
import getMatomo from '../lib/getMatomo'
|
||||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faSearch, faAngleDoubleDown } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
let source = axios.CancelToken.source()
|
let source = axios.CancelToken.source()
|
||||||
|
|
||||||
|
@ -49,7 +51,7 @@ const Feeds:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = (
|
||||||
setResults([])
|
setResults([])
|
||||||
setHasMore(false)
|
setHasMore(false)
|
||||||
setLoaded(false)
|
setLoaded(false)
|
||||||
if (query.length < 3) {
|
if (query.length < 1) {
|
||||||
console.info('Query too short.')
|
console.info('Query too short.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -108,9 +110,9 @@ const Feeds:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = (
|
||||||
<Head>
|
<Head>
|
||||||
<title>{siteTitle}</title>
|
<title>{siteTitle}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<h1>Search people</h1>
|
<h1>People</h1>
|
||||||
<form onSubmit={handleSearchSubmit}>
|
<form onSubmit={handleSearchSubmit}>
|
||||||
<label htmlFor={'query'}>Search on fediverse</label>
|
<div className="input-group mb-3">
|
||||||
<input
|
<input
|
||||||
name={'query'}
|
name={'query'}
|
||||||
id={'query'}
|
id={'query'}
|
||||||
|
@ -118,38 +120,34 @@ const Feeds:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = (
|
||||||
onChange={handleQueryChange}
|
onChange={handleQueryChange}
|
||||||
onBlur={handleQueryChange}
|
onBlur={handleQueryChange}
|
||||||
value={query}
|
value={query}
|
||||||
placeholder={'Search on fediverse'}
|
placeholder={'Search people on Fediverse'}
|
||||||
|
className="form-control"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
aria-label="Search people on Fediverse"
|
||||||
|
aria-describedby="search-button"
|
||||||
/>
|
/>
|
||||||
<button type={'submit'}>
|
<button type={'submit'} id={'search-button'} className={'btn btn-primary'}>
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="search"
|
<FontAwesomeIcon icon={faSearch} className={'margin-right'}/>
|
||||||
className="svg-inline--fa fa-search fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 512 512">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z" />
|
|
||||||
<title>Search</title>
|
|
||||||
</svg>
|
|
||||||
<span>Search</span>
|
<span>Search</span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<Loader loading={loading} showBottom={true}>
|
<Loader loading={loading} showBottom={true}>
|
||||||
{
|
{
|
||||||
loaded
|
loaded
|
||||||
? <Results feeds={results}/>
|
? <FeedResults feeds={results}/>
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
</Loader>
|
</Loader>
|
||||||
{hasMore && !loading
|
{hasMore && !loading
|
||||||
? (
|
? (
|
||||||
<button className={'next-page'} onClick={handleLoadMore}>
|
<div className={'d-flex justify-content-center'}>
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="angle-double-down"
|
<button className={'btn btn-secondary'} onClick={handleLoadMore}>
|
||||||
className="svg-inline--fa fa-angle-double-down fa-w-10" role="img"
|
<FontAwesomeIcon icon={faAngleDoubleDown} className={'margin-right'}/>
|
||||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M143 256.3L7 120.3c-9.4-9.4-9.4-24.6 0-33.9l22.6-22.6c9.4-9.4 24.6-9.4 33.9 0l96.4 96.4 96.4-96.4c9.4-9.4 24.6-9.4 33.9 0L313 86.3c9.4 9.4 9.4 24.6 0 33.9l-136 136c-9.4 9.5-24.6 9.5-34 .1zm34 192l136-136c9.4-9.4 9.4-24.6 0-33.9l-22.6-22.6c-9.4-9.4-24.6-9.4-33.9 0L160 352.1l-96.4-96.4c-9.4-9.4-24.6-9.4-33.9 0L7 278.3c-9.4 9.4-9.4 24.6 0 33.9l136 136c9.4 9.5 24.6 9.5 34 .1z"/>
|
|
||||||
</svg>
|
|
||||||
<span>Load more</span>
|
<span>Load more</span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
: ''}
|
: ''}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -11,6 +11,8 @@ import SoftwareBadge from '../components/badges/SoftwareBadge'
|
||||||
import SortToggle from '../components/SortToggle'
|
import SortToggle from '../components/SortToggle'
|
||||||
import { StatsRequestSortBy } from '../types/StatsRequest'
|
import { StatsRequestSortBy } from '../types/StatsRequest'
|
||||||
import { Sort } from '../types/Sort'
|
import { Sort } from '../types/Sort'
|
||||||
|
import { faSearch, faAngleDoubleDown } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
let source = axios.CancelToken.source()
|
let source = axios.CancelToken.source()
|
||||||
|
|
||||||
|
@ -130,33 +132,32 @@ const Nodes:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = (
|
||||||
</Head>
|
</Head>
|
||||||
<h1>Search servers</h1>
|
<h1>Search servers</h1>
|
||||||
<form onSubmit={handleSearchSubmit}>
|
<form onSubmit={handleSearchSubmit}>
|
||||||
<label htmlFor={'query'}>Search on fediverse</label>
|
<div className={'input-group mb-3'}>
|
||||||
<input
|
<input
|
||||||
name={'query'}
|
name={'query'}
|
||||||
id={'query'}
|
id={'query'}
|
||||||
type={'search'}
|
type={'search'}
|
||||||
|
className={'form-control'}
|
||||||
onChange={handleQueryChange}
|
onChange={handleQueryChange}
|
||||||
onBlur={handleQueryChange}
|
onBlur={handleQueryChange}
|
||||||
value={query}
|
value={query}
|
||||||
placeholder={'Search on fediverse'}
|
placeholder={'Search servers on fediverse'}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
aria-label="Search servers on fediverse"
|
||||||
|
aria-describedby="search-nodes-button"
|
||||||
/>
|
/>
|
||||||
<button type={'submit'}>
|
<button type={'submit'} className={'btn btn-primary'} id={'search-nodes-button'}>
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="search"
|
<FontAwesomeIcon icon={faSearch}/>
|
||||||
className="svg-inline--fa fa-search fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 512 512">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z" />
|
|
||||||
<title>Search</title>
|
|
||||||
</svg>
|
|
||||||
<span>Search</span>
|
<span>Search</span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<Loader loading={loading} showBottom={true}>
|
<Loader loading={loading} showBottom={true}>
|
||||||
{
|
{
|
||||||
loaded
|
loaded
|
||||||
? (
|
? (
|
||||||
<table>
|
<div className="table-responsive">
|
||||||
|
<table className={'table table-dark table-striped table-bordered nodes'}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowSpan={2}>
|
<th rowSpan={2}>
|
||||||
|
@ -187,17 +188,17 @@ const Nodes:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = (
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={'number-cell'}>
|
<th className={'text-end'}>
|
||||||
<SortToggle onToggle={toggleSort} field={'totalUserCount'} sort={sort}>
|
<SortToggle onToggle={toggleSort} field={'totalUserCount'} sort={sort}>
|
||||||
Total
|
Total
|
||||||
</SortToggle>
|
</SortToggle>
|
||||||
</th>
|
</th>
|
||||||
<th className={'number-cell'}>
|
<th className={'text-end'}>
|
||||||
<SortToggle onToggle={toggleSort} field={'monthActiveUserCount'} sort={sort}>
|
<SortToggle onToggle={toggleSort} field={'monthActiveUserCount'} sort={sort}>
|
||||||
Month active
|
Month active
|
||||||
</SortToggle>
|
</SortToggle>
|
||||||
</th>
|
</th>
|
||||||
<th className={'number-cell'}>
|
<th className={'text-end'}>
|
||||||
<SortToggle onToggle={toggleSort} field={'halfYearActiveUserCount'} sort={sort}>
|
<SortToggle onToggle={toggleSort} field={'halfYearActiveUserCount'} sort={sort}>
|
||||||
Half year active
|
Half year active
|
||||||
</SortToggle>
|
</SortToggle>
|
||||||
|
@ -211,12 +212,14 @@ const Nodes:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>{node.domain}</td>
|
<td>{node.domain}</td>
|
||||||
<td>
|
<td>
|
||||||
<div title={'Name'}><SoftwareBadge softwareName={node.softwareName}/></div>
|
<div title={'Name'}><SoftwareBadge
|
||||||
<div title={'Version'}>{node.softwareVersion ?? ''}</div></td>
|
softwareName={node.softwareName}/></div>
|
||||||
<td className={'number-cell'}>{node.totalUserCount ?? '?'}</td>
|
<div title={'Version'}>{node.softwareVersion ?? ''}</div>
|
||||||
<td className={'number-cell'}>{node.monthActiveUserCount ?? '?'}</td>
|
</td>
|
||||||
<td className={'number-cell'}>{node.halfYearActiveUserCount ?? '?'}</td>
|
<td className={'text-end'}>{node.totalUserCount ?? '?'}</td>
|
||||||
<td className={'number-cell'}>{node.statusesCount ?? '?'}</td>
|
<td className={'text-end'}>{node.monthActiveUserCount ?? '?'}</td>
|
||||||
|
<td className={'text-end'}>{node.halfYearActiveUserCount ?? '?'}</td>
|
||||||
|
<td className={'text-end'}>{node.statusesCount ?? '?'}</td>
|
||||||
<td>{node.openRegistrations === null ? '?' : (node.openRegistrations ? 'Opened' : 'Closed')}</td>
|
<td>{node.openRegistrations === null ? '?' : (node.openRegistrations ? 'Opened' : 'Closed')}</td>
|
||||||
<td>{node.refreshedAt ? (new Date(node.refreshedAt)).toLocaleDateString() : 'Never'}</td>
|
<td>{node.refreshedAt ? (new Date(node.refreshedAt)).toLocaleDateString() : 'Never'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -229,21 +232,19 @@ const Nodes:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = (
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
</Loader>
|
</Loader>
|
||||||
{hasMore && !loading
|
{hasMore && !loading
|
||||||
? (
|
? (
|
||||||
<button className={'next-page'} onClick={handleLoadMore}>
|
<div className={'d-flex justify-content-center'}>
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="angle-double-down"
|
<button className={'btn btn-secondary'} onClick={handleLoadMore}>
|
||||||
className="svg-inline--fa fa-angle-double-down fa-w-10" role="img"
|
<FontAwesomeIcon icon={faAngleDoubleDown} className={'margin-right'} />
|
||||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M143 256.3L7 120.3c-9.4-9.4-9.4-24.6 0-33.9l22.6-22.6c9.4-9.4 24.6-9.4 33.9 0l96.4 96.4 96.4-96.4c9.4-9.4 24.6-9.4 33.9 0L313 86.3c9.4 9.4 9.4 24.6 0 33.9l-136 136c-9.4 9.5-24.6 9.5-34 .1zm34 192l136-136c9.4-9.4 9.4-24.6 0-33.9l-22.6-22.6c-9.4-9.4-24.6-9.4-33.9 0L160 352.1l-96.4-96.4c-9.4-9.4-24.6-9.4-33.9 0L7 278.3c-9.4 9.4-9.4 24.6 0 33.9l136 136c9.4 9.5 24.6 9.5 34 .1z"/>
|
|
||||||
</svg>
|
|
||||||
<span>Load more</span>
|
<span>Load more</span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
: ''}
|
: ''}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -96,7 +96,8 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
<title>{'Stats | ' + siteTitle}</title>
|
<title>{'Stats | ' + siteTitle}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<h1>Index stats</h1>
|
<h1>Index stats</h1>
|
||||||
<table>
|
<div className="table-responsive">
|
||||||
|
<table className={'table table-dark table-striped table-bordered stats'}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
|
@ -104,17 +105,17 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
Software name
|
Software name
|
||||||
</SortToggle>
|
</SortToggle>
|
||||||
</th>
|
</th>
|
||||||
<th className={'number-cell'}>
|
<th className={'text-end'}>
|
||||||
<SortToggle onToggle={toggleSort} field={'nodeCount'} sort={sort}>
|
<SortToggle onToggle={toggleSort} field={'nodeCount'} sort={sort}>
|
||||||
Instance count
|
Instance count
|
||||||
</SortToggle>
|
</SortToggle>
|
||||||
</th>
|
</th>
|
||||||
<th className={'number-cell'}>
|
<th className={'text-end'}>
|
||||||
<SortToggle onToggle={toggleSort} field={'accountCount'} sort={sort}>
|
<SortToggle onToggle={toggleSort} field={'accountCount'} sort={sort}>
|
||||||
Account count
|
Account count
|
||||||
</SortToggle>
|
</SortToggle>
|
||||||
</th>
|
</th>
|
||||||
<th className={'number-cell'}>
|
<th className={'text-end'}>
|
||||||
<SortToggle onToggle={toggleSort} field={'channelCount'} sort={sort}>
|
<SortToggle onToggle={toggleSort} field={'channelCount'} sort={sort}>
|
||||||
Channel count
|
Channel count
|
||||||
</SortToggle>
|
</SortToggle>
|
||||||
|
@ -123,7 +124,9 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
</thead>
|
</thead>
|
||||||
<Loader loading={loading} hideContent={!loaded} table={4} showTop={true}>
|
<Loader loading={loading} hideContent={!loaded} table={4} showTop={true}>
|
||||||
{stats === null
|
{stats === null
|
||||||
? (<tr><td colSpan={4}><em>Failed to load stats data!</em></td></tr>)
|
? (<tr>
|
||||||
|
<td colSpan={4}><em>Failed to load stats data!</em></td>
|
||||||
|
</tr>)
|
||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -135,17 +138,17 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
<td>
|
<td>
|
||||||
<SoftwareBadge softwareName={software.name}/>
|
<SoftwareBadge softwareName={software.name}/>
|
||||||
</td>
|
</td>
|
||||||
<td className={'number-cell'}>
|
<td className={'text-end'}>
|
||||||
<span>{software.nodeCount}</span>
|
<span>{software.nodeCount}</span>
|
||||||
<ProgressBar way={'left'}
|
<ProgressBar way={'left'}
|
||||||
percents={100 * software.nodeCount / max.nodeCount}/>
|
percents={100 * software.nodeCount / max.nodeCount}/>
|
||||||
</td>
|
</td>
|
||||||
<td className={'number-cell'}>
|
<td className={'text-end'}>
|
||||||
<span>{software.accountCount}</span>
|
<span>{software.accountCount}</span>
|
||||||
<ProgressBar way={'left'}
|
<ProgressBar way={'left'}
|
||||||
percents={100 * software.accountCount / max.accountCount}/>
|
percents={100 * software.accountCount / max.accountCount}/>
|
||||||
</td>
|
</td>
|
||||||
<td className={'number-cell'}>
|
<td className={'text-end'}>
|
||||||
<span>{software.channelCount}</span>
|
<span>{software.channelCount}</span>
|
||||||
<ProgressBar way={'left'}
|
<ProgressBar way={'left'}
|
||||||
percents={100 * software.channelCount / max.channelCount}/>
|
percents={100 * software.channelCount / max.channelCount}/>
|
||||||
|
@ -155,10 +158,13 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
: (
|
: (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td><em>Not recognized</em></td>
|
<td><em>Not recognized</em></td>
|
||||||
<td className={'number-cell'}><span>{software.nodeCount}</span></td>
|
<td className={'text-end'}><span>{software.nodeCount}</span>
|
||||||
<td className={'number-cell'}><span>{software.accountCount}</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td className={'number-cell'}><span>{software.channelCount}</span>
|
<td className={'text-end'}>
|
||||||
|
<span>{software.accountCount}</span>
|
||||||
|
</td>
|
||||||
|
<td className={'text-end'}>
|
||||||
|
<span>{software.channelCount}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
@ -168,9 +174,9 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Summary</th>
|
<th>Summary</th>
|
||||||
<th className={'number-cell'}>{sum.nodeCount}</th>
|
<th className={'text-end'}>{sum.nodeCount}</th>
|
||||||
<th className={'number-cell'}>{sum.accountCount}</th>
|
<th className={'text-end'}>{sum.accountCount}</th>
|
||||||
<th className={'number-cell'}>{sum.channelCount}</th>
|
<th className={'text-end'}>{sum.channelCount}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</>
|
</>
|
||||||
|
@ -178,6 +184,7 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
}
|
}
|
||||||
</Loader>
|
</Loader>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,344 +1,66 @@
|
||||||
:root {
|
@import "./variables";
|
||||||
--shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.4);
|
@import '~bootstrap/scss/bootstrap.scss';
|
||||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
||||||
--main-bg-color: #212224;
|
|
||||||
--main-fg-color: #ededed;
|
|
||||||
--accent-color: #00a2fe;
|
|
||||||
--front-bg-color: #3a3c40;
|
|
||||||
--front-fg-color: rgb(156, 156, 156);
|
|
||||||
//@media (prefers-color-scheme: light) {
|
|
||||||
// --main-bg-color: #ededed;
|
|
||||||
// --main-fg-color: #212224;
|
|
||||||
// --front-bg-color: rgb(204, 204, 204);
|
|
||||||
// --front-fg-color: #3a3c40;
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
.svg-inline--fa,
|
||||||
width: 100%;
|
.with-emoji .emoji {
|
||||||
text-align: left;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 1em;
|
max-width: 1em;
|
||||||
max-height: 1em;
|
max-height: 1em;
|
||||||
display: inline;
|
height: 2em;
|
||||||
|
|
||||||
|
&.margin-right {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
td, th {
|
&.margin-left {
|
||||||
border: 1px solid var(--front-bg-color);
|
|
||||||
border-radius: 0.3em;
|
|
||||||
padding: 0.3em;
|
|
||||||
|
|
||||||
&.number-cell {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progressbar {
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
height: 2px;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-toggle{
|
|
||||||
svg{
|
|
||||||
max-width: 1em;
|
|
||||||
max-height: 1em;
|
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background-color: var(--front-bg-color);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
.software-name > .icon {
|
||||||
textarea,
|
|
||||||
button {
|
|
||||||
background-color: var(--front-bg-color);
|
|
||||||
border-radius: 2em;
|
|
||||||
border: none;
|
|
||||||
box-shadow: var(--shadow-inset);
|
|
||||||
height: 2em;
|
|
||||||
font-size: 20px;
|
|
||||||
color: var(--main-fg-color);
|
|
||||||
padding: 1em;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
outline: 1px solid var(--front-fg-color);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: var(--front-fg-color);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
color: var(--main-bg-color);
|
|
||||||
padding: 0 1em;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
vertical-align: middle;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
max-width: 1em;
|
|
||||||
max-height: 1em;
|
|
||||||
fill: var(--main-bg-color);
|
|
||||||
margin-right: 0.3em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:focus {
|
|
||||||
outline: 1px solid var(--main-fg-color) !important;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:focus-visible {
|
|
||||||
outline: 2px solid var(--main-fg-color);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
|
||||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-size: 18px;
|
|
||||||
background-color: var(--main-bg-color);
|
|
||||||
color: var(--main-fg-color);
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--accent-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: grid;
|
|
||||||
padding: 1em;
|
|
||||||
grid-template-columns: [start] auto [end];
|
|
||||||
grid-template-rows: [start] auto [message] auto [form] auto [main] auto [end];
|
|
||||||
max-width: 790px;
|
|
||||||
margin: 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
grid-gap: 1em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
header {
|
|
||||||
grid-column: start / end;
|
|
||||||
grid-row: start / message;
|
|
||||||
display: grid;
|
|
||||||
|
|
||||||
grid-template-columns: [start] 4em [text] auto [end];
|
|
||||||
grid-template-rows: [start] auto [end];
|
|
||||||
grid-gap: 1em;
|
|
||||||
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 20px;
|
|
||||||
|
|
||||||
width: auto;
|
|
||||||
margin: 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
height: fit-content;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
grid-column: text/ end;
|
|
||||||
grid-row: start/end;
|
|
||||||
margin: 0;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
grid-column: start/ text;
|
|
||||||
grid-row: start/end;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=search] {
|
|
||||||
background-color: var(--front-bg-color);
|
|
||||||
border-radius: 2em 0 0 2em;
|
|
||||||
border: none;
|
|
||||||
box-shadow: var(--shadow-inset);
|
|
||||||
height: 2em;
|
|
||||||
font-size: 20px;
|
|
||||||
color: var(--main-fg-color);
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: var(--front-fg-color);
|
|
||||||
border-radius: 0 2em 2em 0;
|
|
||||||
border: none;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
height: 2em;
|
|
||||||
font-size: 20px;
|
|
||||||
color: var(--main-bg-color);
|
|
||||||
padding: 0 0.5em 0 0.3em;
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
vertical-align: baseline;
|
|
||||||
width: 1em;
|
|
||||||
max-width: 1em;
|
|
||||||
max-height: 1em;
|
|
||||||
fill: var(--main-bg-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
ul {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: inline;
|
|
||||||
margin: 0.5em;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
a {
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
max-width: 1em;
|
|
||||||
max-height: 1em;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover svg {
|
|
||||||
outline: 1px solid var(--accent-color);
|
|
||||||
outline-offset: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--main-fg-color);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
max-width: 1.5em;
|
max-width: 1.5em;
|
||||||
max-height: 1.5em;
|
max-height: 1.5em;
|
||||||
position: relative;
|
padding-right: 0.5em;
|
||||||
top: 0.25em;
|
}
|
||||||
|
|
||||||
outline-color: var(--main-fg-color);
|
.navbar {
|
||||||
}
|
.navbar-brand {
|
||||||
}
|
.logo {
|
||||||
}
|
height: 1.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader {
|
table.stats {
|
||||||
text-align: center;
|
.software-name {
|
||||||
|
.icon {
|
||||||
.loader-visualisation {
|
max-height: 2em;
|
||||||
margin: 1em 0;
|
max-width: 2em;
|
||||||
|
|
||||||
svg {
|
|
||||||
animation: spin 2s linear infinite;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
.rail {
|
|
||||||
color: var(--main-bg-color);
|
|
||||||
fill: var(--front-bg-color);
|
|
||||||
stroke: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.train {
|
|
||||||
fill: none;
|
|
||||||
stroke: var(--accent-color);
|
|
||||||
stroke-width: 2.2583456;
|
|
||||||
stroke-linecap: round
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
.progress {
|
||||||
margin-left: 0.5em;
|
height: 0.5em;
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-content {
|
|
||||||
transition: 1s top, left;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feed {
|
.feed .card-body{
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 1em;
|
|
||||||
border-radius: 1em;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
background-color: var(--front-bg-color);
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: [start] min(20%) [main] auto [end];
|
grid-template-columns: [start] min(15%) [main] auto [end];
|
||||||
grid-template-rows: [start] auto [address] auto [badges];
|
grid-template-rows: [start] auto [address] auto [software] auto [badges];
|
||||||
column-gap: 1em;
|
column-gap: 1em;
|
||||||
row-gap: 1em;
|
row-gap: 1em;
|
||||||
box-shadow: var(--shadow);
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
align-self: start;
|
align-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
border-radius: 0.5em;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
grid-column: start /main;
|
grid-column: start /main;
|
||||||
grid-row: span 3;
|
grid-row: span 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -353,58 +75,23 @@ img {
|
||||||
grid-row: address;
|
grid-row: address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.software-name{
|
||||||
|
grid-column: main /end;
|
||||||
|
grid-row: software;
|
||||||
|
}
|
||||||
|
|
||||||
.badges {
|
.badges {
|
||||||
grid-column: main /end;
|
grid-column: main /end;
|
||||||
grid-row: badges;
|
grid-row: badges;
|
||||||
|
|
||||||
.label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
img, svg {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1em;
|
|
||||||
max-height: 1em;
|
|
||||||
display: inline;
|
|
||||||
margin-right: 0.3em;
|
|
||||||
vertical-align: baseline;
|
|
||||||
fill: var(--main-fg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge{
|
.badge{
|
||||||
display: inline-block;
|
margin-right: 0.5em;
|
||||||
margin-right: 1em;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscriptions {
|
|
||||||
& > * {
|
|
||||||
display: inline;
|
|
||||||
margin-right: 0.3em;
|
|
||||||
|
|
||||||
&:last-child::before {
|
|
||||||
content: '/';
|
|
||||||
display: inline;
|
|
||||||
margin-right: 0.3em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
grid-column: start / end;
|
grid-column: start / end;
|
||||||
grid-row: span 1;
|
grid-row: span 1;
|
||||||
|
|
||||||
td, th {
|
|
||||||
border: 1px solid var(--main-bg-color);
|
|
||||||
border-radius: 0.3em;
|
|
||||||
padding: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background-color: var(--main-bg-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
|
@ -429,25 +116,14 @@ img {
|
||||||
grid-column-start: start;
|
grid-column-start: start;
|
||||||
margin-top: -1em;
|
margin-top: -1em;
|
||||||
}
|
}
|
||||||
|
.badges{
|
||||||
|
margin-top: -1em;
|
||||||
|
}
|
||||||
.avatar {
|
.avatar {
|
||||||
grid-row-start: badges;
|
grid-row-start: software;
|
||||||
|
grid-row: span 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.with-emoji {
|
|
||||||
img {
|
|
||||||
max-width: 1em;
|
|
||||||
max-height: 1em;
|
|
||||||
display: inline;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
color: var(--front-fg-color);
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
// Darkly 5.1.3
|
||||||
|
// Bootswatch
|
||||||
|
|
||||||
|
$theme: "darkly" !default;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Color system
|
||||||
|
//
|
||||||
|
|
||||||
|
$white: #fff !default;
|
||||||
|
$gray-100: #f8f9fa !default;
|
||||||
|
$gray-200: #ebebeb !default;
|
||||||
|
$gray-300: #dee2e6 !default;
|
||||||
|
$gray-400: #ced4da !default;
|
||||||
|
$gray-500: #adb5bd !default;
|
||||||
|
$gray-600: #888 !default;
|
||||||
|
$gray-700: #444 !default;
|
||||||
|
$gray-800: #303030 !default;
|
||||||
|
$gray-900: #222 !default;
|
||||||
|
$black: #000 !default;
|
||||||
|
|
||||||
|
$blue: #375a7f !default;
|
||||||
|
$indigo: #6610f2 !default;
|
||||||
|
$purple: #6f42c1 !default;
|
||||||
|
$pink: #e83e8c !default;
|
||||||
|
$red: #e74c3c !default;
|
||||||
|
$orange: #fd7e14 !default;
|
||||||
|
$yellow: #f39c12 !default;
|
||||||
|
$green: #00bc8c !default;
|
||||||
|
$teal: #20c997 !default;
|
||||||
|
$cyan: #3498db !default;
|
||||||
|
|
||||||
|
$primary: $cyan !default;
|
||||||
|
$secondary: $gray-700 !default;
|
||||||
|
$success: $green !default;
|
||||||
|
$info: $cyan !default;
|
||||||
|
$warning: $yellow !default;
|
||||||
|
$danger: $red !default;
|
||||||
|
$light: $gray-500 !default;
|
||||||
|
$dark: $gray-800 !default;
|
||||||
|
|
||||||
|
$min-contrast-ratio: 1.9 !default;
|
||||||
|
|
||||||
|
// Body
|
||||||
|
|
||||||
|
$body-bg: $gray-900 !default;
|
||||||
|
$body-color: $white !default;
|
||||||
|
|
||||||
|
// Links
|
||||||
|
|
||||||
|
$link-color: $primary !default;
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
|
||||||
|
// stylelint-disable-next-line value-keyword-case
|
||||||
|
$font-family-sans-serif: Lato, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
|
||||||
|
$h1-font-size: 3rem !default;
|
||||||
|
$h2-font-size: 2.5rem !default;
|
||||||
|
$h3-font-size: 2rem !default;
|
||||||
|
$text-muted: $gray-600 !default;
|
||||||
|
|
||||||
|
// Tables
|
||||||
|
|
||||||
|
$table-border-color: $gray-700 !default;
|
||||||
|
|
||||||
|
$table-bg-scale: 0 !default;
|
||||||
|
|
||||||
|
// Forms
|
||||||
|
|
||||||
|
$input-bg: $white !default;
|
||||||
|
$input-color: $gray-800 !default;
|
||||||
|
$input-border-color: $body-bg !default;
|
||||||
|
$input-group-addon-color: $gray-500 !default;
|
||||||
|
$input-group-addon-bg: $gray-700 !default;
|
||||||
|
|
||||||
|
$form-check-input-bg: $white !default;
|
||||||
|
$form-check-input-border: none !default;
|
||||||
|
|
||||||
|
$form-file-button-color: $white !default;
|
||||||
|
|
||||||
|
// Dropdowns
|
||||||
|
|
||||||
|
$dropdown-bg: $gray-900 !default;
|
||||||
|
$dropdown-border-color: $gray-700 !default;
|
||||||
|
$dropdown-divider-bg: $gray-700 !default;
|
||||||
|
$dropdown-link-color: $white !default;
|
||||||
|
$dropdown-link-hover-color: $white !default;
|
||||||
|
$dropdown-link-hover-bg: $primary !default;
|
||||||
|
|
||||||
|
// Navs
|
||||||
|
|
||||||
|
$nav-link-padding-x: 2rem !default;
|
||||||
|
$nav-link-disabled-color: $gray-500 !default;
|
||||||
|
$nav-tabs-border-color: $gray-700 !default;
|
||||||
|
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color transparent !default;
|
||||||
|
$nav-tabs-link-active-color: $white !default;
|
||||||
|
$nav-tabs-link-active-border-color: $nav-tabs-border-color $nav-tabs-border-color transparent !default;
|
||||||
|
|
||||||
|
// Navbar
|
||||||
|
|
||||||
|
$navbar-padding-y: 1rem !default;
|
||||||
|
$navbar-dark-color: rgba($white, .6) !default;
|
||||||
|
$navbar-dark-hover-color: $white !default;
|
||||||
|
$navbar-light-color: rgba($gray-900, .7) !default;
|
||||||
|
$navbar-light-hover-color: $gray-900 !default;
|
||||||
|
$navbar-light-active-color: $gray-900 !default;
|
||||||
|
$navbar-light-toggler-border-color: rgba($gray-900, .1) !default;
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
|
||||||
|
$pagination-color: $white !default;
|
||||||
|
$pagination-bg: $primary !default;
|
||||||
|
$pagination-border-width: 0 !default;
|
||||||
|
$pagination-border-color: transparent !default;
|
||||||
|
$pagination-hover-color: $white !default;
|
||||||
|
$pagination-hover-bg: lighten($primary, 10%) !default;
|
||||||
|
$pagination-hover-border-color: transparent !default;
|
||||||
|
$pagination-active-bg: $pagination-hover-bg !default;
|
||||||
|
$pagination-active-border-color: transparent !default;
|
||||||
|
$pagination-disabled-color: $white !default;
|
||||||
|
$pagination-disabled-bg: darken($primary, 15%) !default;
|
||||||
|
$pagination-disabled-border-color: transparent !default;
|
||||||
|
|
||||||
|
// Cards
|
||||||
|
|
||||||
|
$card-cap-bg: $gray-700 !default;
|
||||||
|
$card-bg: $gray-800 !default;
|
||||||
|
|
||||||
|
// Popovers
|
||||||
|
|
||||||
|
$popover-bg: $gray-800 !default;
|
||||||
|
$popover-header-bg: $gray-700 !default;
|
||||||
|
|
||||||
|
// Toasts
|
||||||
|
|
||||||
|
$toast-background-color: $gray-700 !default;
|
||||||
|
$toast-header-background-color: $gray-800 !default;
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
|
||||||
|
$modal-content-bg: $gray-800 !default;
|
||||||
|
$modal-content-border-color: $gray-700 !default;
|
||||||
|
$modal-header-border-color: $gray-700 !default;
|
||||||
|
|
||||||
|
// Progress bars
|
||||||
|
|
||||||
|
$progress-bg: $gray-700 !default;
|
||||||
|
|
||||||
|
// List group
|
||||||
|
|
||||||
|
$list-group-color: $body-color !default;
|
||||||
|
$list-group-bg: $gray-800 !default;
|
||||||
|
$list-group-border-color: $gray-700 !default;
|
||||||
|
$list-group-hover-bg: $gray-700 !default;
|
||||||
|
$list-group-action-hover-color: $list-group-color !default;
|
||||||
|
$list-group-action-active-bg: $gray-900 !default;
|
||||||
|
|
||||||
|
// Breadcrumbs
|
||||||
|
|
||||||
|
$breadcrumb-padding-y: .375rem !default;
|
||||||
|
$breadcrumb-padding-x: .75rem !default;
|
||||||
|
$breadcrumb-bg: $gray-700 !default;
|
||||||
|
$breadcrumb-border-radius: .25rem !default;
|
||||||
|
|
||||||
|
// Close
|
||||||
|
|
||||||
|
$btn-close-color: $white !default;
|
||||||
|
$btn-close-opacity: .4 !default;
|
||||||
|
$btn-close-hover-opacity: 1 !default;
|
||||||
|
|
||||||
|
// Code
|
||||||
|
|
||||||
|
$pre-color: inherit !default;
|
|
@ -18,6 +18,7 @@ export const feedResponseItemSchema = z.object({
|
||||||
bot: z.boolean().nullable(),
|
bot: z.boolean().nullable(),
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
|
|
||||||
displayName: z.string(),
|
displayName: z.string(),
|
||||||
fields: z.array(feedResponseFieldSchema).nullable(),
|
fields: z.array(feedResponseFieldSchema).nullable(),
|
||||||
followersCount: z.number().nullable(),
|
followersCount: z.number().nullable(),
|
||||||
|
|
Ładowanie…
Reference in New Issue