Reskinned to bootstrap, shorted char limit in feed search

main
Štěpán Škorpil 2022-02-05 11:55:34 +01:00
rodzic 1c0fd9f21e
commit 8091609456
30 zmienionych plików z 4201 dodań i 1433 usunięć

Wyświetl plik

@ -1,11 +1,19 @@
module.exports = {
async redirects() {
webpack (config) {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack']
})
return config
},
async redirects () {
return [
{
source: '/',
destination: '/feeds',
permanent: true,
},
permanent: true
}
]
},
}
}

3975
application/package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -15,18 +15,25 @@
},
"dependencies": {
"@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",
"@prisma/client": "^3.6.0",
"@svgr/webpack": "^6.2.1",
"axios": "^0.21.1",
"bootstrap": "^5.1.3",
"next": "^12.0.7",
"node-cache": "^5.1.2",
"npmlog": "^6.0.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"sass": "^1.45.1",
"striptags": "^3.2.0",
"typescript-collections": "^1.3.3",
"zod": "^3.11.6",
"node-cache": "^5.1.2"
"zod": "^3.11.6"
},
"devDependencies": {
"@next/eslint-plugin-next": "^12.0.7",

Wyświetl plik

@ -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

Wyświetl plik

@ -1,25 +1,23 @@
import React from 'react'
import Result from './Result'
import FeedResult from './FeedResult'
import { FeedResponseItem } from '../types/FeedResponse'
const Results:React.FC<{feeds:FeedResponseItem[]}> = ({ feeds }) => {
const FeedResults:React.FC<{feeds:FeedResponseItem[]}> = ({ feeds }) => {
if (feeds.length === 0) {
return (
<>
<h2>Nothing found</h2>
<p className={'no-results'}>We have no results for your query.</p>
</>
)
}
return (<>
<h2>Results</h2>
return (<div className={'grid'}>
{
feeds.map((feed, index) => {
console.info('feed', feed)
return (<Result key={index} feed={feed}/>)
return (<FeedResult key={index} feed={feed}/>)
})
}
</>)
</div>)
}
export default Results
export default FeedResults

Wyświetl plik

@ -2,7 +2,7 @@ import React from 'react'
const Footer:React.FC = () => {
return (
<footer>
<footer className={'text-center mt-5'}>
©{(new Date()).getFullYear()} <a href={'https://skorpil.cz'}>Štěpán Škorpil</a>
</footer>
)

Wyświetl plik

@ -3,7 +3,7 @@ import Head from 'next/head'
import Footer from './Footer'
import getMatomo from '../lib/getMatomo'
import { UserOptions } from '@datapunt/matomo-tracker-js/es/types'
import NavItem from './NavItem'
import NavBar from './NavBar'
export const siteTitle = 'FediSearch'
export const siteDescription = 'Search people on Fediverse'
@ -24,48 +24,13 @@ const Layout: React.FC<{ matomoConfig: UserOptions, children: React.ReactNode }>
<meta property="og:image" content="/fedisearch.png"/>
<meta property="og:type" content="website"/>
</Head>
<header>
<h1><a href={'/'}>FediSearch</a></h1>
<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>
{children}
</main>
<Footer/>
<div className="container">
<NavBar />
<main>
{children}
</main>
<Footer/>
</div>
</div>
)
}

Wyświetl plik

@ -1,45 +1,59 @@
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 loaderVisual = (
<div className={'loader-visualisation'}>
<svg xmlns="http://www.w3.org/2000/svg" className='loader-graphics' width="34" height="34">
<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" />
<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>
const spinner = (
<div className={'d-flex justify-content-center'}>
<Spinner/>
</div>
)
if (table) {
return (
<>
{showTop && loading
? (
<tr className={className}>
<td colSpan={table}>{loaderVisual}</td>
</tr>
)
: ''}
{ hideContent && loading ? '' : children}
{showBottom && loading
? (
<tr className={className}>
<td colSpan={table}>{loaderVisual}</td>
</tr>
)
: ''}
</>
<>
{showTop && loading
? (
<tbody>
<tr className={className}>
<td colSpan={table}>
{spinner}
</td>
</tr>
</tbody>
)
: ''}
{hideContent && loading ? '' : children}
{showBottom && loading
? (
<tbody>
<tr className={className}>
<td colSpan={table}>
<div className={'d-flex justify-content-center'}>
<Spinner/>
</div>
</td>
</tr>
</tbody>
)
: ''}
</>
)
}
return (
<div className={className}>
{showTop && loading ? loaderVisual : ''}
<div className={'loader-content'}>
{hideContent && loading ? '' : children}
</div>
{showBottom && loading ? loaderVisual : ''}
</div>
<>
{showTop && loading ? spinner : ''}
{hideContent && loading ? '' : children}
{showBottom && loading ? spinner : ''}
</>
)
}

Wyświetl plik

@ -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

Wyświetl plik

@ -1,14 +1,17 @@
import React, { FC, ReactElement } from 'react'
import React, { FC } from 'react'
import Link from 'next/link'
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 active = router.pathname === path
return (
<li>
<li className={'nav-item'}>
<Link href={path}>
<a className={router.pathname === path ? 'active' : ''}>
{icon}
<a className={'nav-link' + (active ? ' active' : '')} aria-current={active ? 'page' : undefined}>
<FontAwesomeIcon icon={icon} className={'margin-right'} />
<span>{label}</span>
</a>
</Link>

Wyświetl plik

@ -5,9 +5,9 @@ const ProgressBar: React.FC<{ percents: number, way?: 'left' | 'right' | 'top' |
percents = Math.round(percents)
color = color ?? 'var(--accent-color)'
return (
<div className={'progressbar'} style={{
background: `linear-gradient(to ${way}, ${color} ${percents}%, transparent ${percents}%`
}}/>
<div className="progress justify-content-end">
<div className="progress-bar" role="progressbar" style={{ width: `${percents}%` }} />
</div>
)
}

Wyświetl plik

@ -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

Wyświetl plik

@ -1,5 +1,7 @@
import React from 'react'
import { Sort } from '../types/Sort'
import { faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
const SortToggle: React.FC<{
onToggle:(StatsRequestSortBy)=>void,
@ -11,23 +13,13 @@ const SortToggle: React.FC<{
<span>{children}</span>
{sort.sortBy === field && sort.sortWay === 'asc'
? (
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sort-up"
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>
<FontAwesomeIcon icon={faSortUp} className={'margin-left'} />
)
: ''
}
{sort.sortBy === field && sort.sortWay === 'desc'
? (
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sort-down"
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>
<FontAwesomeIcon icon={faSortDown} className={'margin-left'} />
)
: ''
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -1,17 +1,14 @@
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 (
<div className={' badge bot'} title={'Bot'}>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="robot"
className="svg-inline--fa fa-robot fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512">
<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>
<Badge faIcon={faRobot}
label={'Bot'}
value={bot !== null ? (bot ? 'Yes' : 'No') : null}
className={'bot'}
/>
)
}
export default BotBadge

Wyświetl plik

@ -1,17 +1,14 @@
import React from 'react'
import { faCalendarPlus } from '@fortawesome/free-solid-svg-icons'
import Badge from './Badge'
const CreatedAtBadge:React.FC<{ createdAt: string | null }> = ({ createdAt }) => {
return (
<div className={'badge created-at'} title={'Created at'}>
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="calendar-plus"
className="svg-inline--fa fa-calendar-plus fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512">
<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>
<Badge faIcon={faCalendarPlus}
label={'Created at'}
value={createdAt !== null ? (new Date(createdAt)).toLocaleDateString() : null}
className={'created-at'}
/>
)
}
export default CreatedAtBadge

Wyświetl plik

@ -1,33 +1,14 @@
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 }) => {
if (type === 'channel') {
return (
<div className={'badge feed-type'} title={'Feed type'}>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="rss"
className="feed-type svg-inline--fa fa-rss fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512">
<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>
<Badge faIcon={type === 'channel' ? faRss : faUser}
label={'Feed type'}
value={type === 'channel' ? 'Channel' : 'Account'}
className={'feed-type'}
/>
)
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -1,17 +1,14 @@
import React from 'react'
import Badge from './Badge'
import { faCalendarCheck } from '@fortawesome/free-solid-svg-icons'
const LastPostAtBadge:React.FC<{ lastStatusAt: string | null }> = ({ lastStatusAt }) => {
return (
<div className={'badge last-status-at'} title={'Last status at'}>
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="calendar-check"
className="svg-inline--fa fa-calendar-check fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512">
<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>
<Badge faIcon={faCalendarCheck}
label={'Last status at'}
value={lastStatusAt !== null ? (new Date(lastStatusAt)).toLocaleDateString() : null}
className={'last-status-at'}
/>
)
}

Wyświetl plik

@ -7,7 +7,7 @@ const SoftwareBadge:React.FC<{softwareName:string|null}> = ({ softwareName }) =>
event.target.src = fallbackImage
}
return (<div className={'badge software-name'} title={'Software name'}>
return (<div className={'software-name'} title={'Software name'}>
<img className={'icon'}
src={softwareName !== null ? `/software/${softwareName}.svg` : fallbackImage}
alt={softwareName}

Wyświetl plik

@ -1,17 +1,14 @@
import React from 'react'
import { faCommentAlt } from '@fortawesome/free-solid-svg-icons'
import Badge from './Badge'
const StatusesCountBadge:React.FC<{ statusesCount: number | null }> = ({ statusesCount }) => {
return (
<div className={'badge last-status-at'} title={'Status count'}>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="comment-alt"
className="svg-inline--fa fa-comment-alt fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512">
<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>
<Badge faIcon={faCommentAlt}
label={'Status count'}
value={statusesCount}
className={'last-status-at'}
/>
)
}

Wyświetl plik

@ -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

Wyświetl plik

@ -3,15 +3,17 @@ import React, { useEffect, useState } from 'react'
import axios from 'axios'
import { feedResponseSchema } from '../types/FeedResponse'
import Loader from '../components/Loader'
import Results from '../components/Results'
import FeedResults from '../components/FeedResults'
import Layout, { siteTitle } from '../components/Layout'
import { matomoConfig } from '../lib/matomoConfig'
import getMatomo from '../lib/getMatomo'
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()
const Feeds:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = ({ matomoConfig }) => {
const Feeds: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = ({ matomoConfig }) => {
const [query, setQuery] = useState('')
const [submitted, setSubmitted] = useState(null)
const [loading, setLoading] = useState(false)
@ -49,7 +51,7 @@ const Feeds:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = (
setResults([])
setHasMore(false)
setLoaded(false)
if (query.length < 3) {
if (query.length < 1) {
console.info('Query too short.')
return
}
@ -108,55 +110,51 @@ const Feeds:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = (
<Head>
<title>{siteTitle}</title>
</Head>
<h1>Search people</h1>
<h1>People</h1>
<form onSubmit={handleSearchSubmit}>
<label htmlFor={'query'}>Search on fediverse</label>
<input
name={'query'}
id={'query'}
type={'search'}
onChange={handleQueryChange}
onBlur={handleQueryChange}
value={query}
placeholder={'Search on fediverse'}
autoFocus={true}
/>
<button type={'submit'}>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="search"
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>
</button>
<div className="input-group mb-3">
<input
name={'query'}
id={'query'}
type={'search'}
onChange={handleQueryChange}
onBlur={handleQueryChange}
value={query}
placeholder={'Search people on Fediverse'}
className="form-control"
autoFocus={true}
aria-label="Search people on Fediverse"
aria-describedby="search-button"
/>
<button type={'submit'} id={'search-button'} className={'btn btn-primary'}>
<FontAwesomeIcon icon={faSearch} className={'margin-right'}/>
<span>Search</span>
</button>
</div>
</form>
<Loader loading={loading} showBottom={true}>
{
loaded
? <Results feeds={results}/>
? <FeedResults feeds={results}/>
: ''
}
</Loader>
{hasMore && !loading
? (
<button className={'next-page'} onClick={handleLoadMore}>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="angle-double-down"
className="svg-inline--fa fa-angle-double-down fa-w-10" role="img"
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>
</button>
<div className={'d-flex justify-content-center'}>
<button className={'btn btn-secondary'} onClick={handleLoadMore}>
<FontAwesomeIcon icon={faAngleDoubleDown} className={'margin-right'}/>
<span>Load more</span>
</button>
</div>
)
: ''}
</Layout>
)
}
export const getServerSideProps:GetServerSideProps = async (context) => {
export const getServerSideProps: GetServerSideProps = async (context) => {
console.info('Loading matomo config', matomoConfig)
return {
props: {

Wyświetl plik

@ -11,10 +11,12 @@ import SoftwareBadge from '../components/badges/SoftwareBadge'
import SortToggle from '../components/SortToggle'
import { StatsRequestSortBy } from '../types/StatsRequest'
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()
const Nodes:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = ({ matomoConfig }) => {
const Nodes: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = ({ matomoConfig }) => {
const [query, setQuery] = useState('')
const [submitted, setSubmitted] = useState(null)
const [loading, setLoading] = useState(false)
@ -124,133 +126,132 @@ const Nodes:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = (
useEffect(loadNextPageResults, [page])
return (
<Layout matomoConfig={matomoConfig}>
<Head>
<title>{siteTitle}</title>
</Head>
<h1>Search servers</h1>
<form onSubmit={handleSearchSubmit}>
<label htmlFor={'query'}>Search on fediverse</label>
<input
name={'query'}
id={'query'}
type={'search'}
onChange={handleQueryChange}
onBlur={handleQueryChange}
value={query}
placeholder={'Search on fediverse'}
autoFocus={true}
/>
<button type={'submit'}>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="search"
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>
</button>
</form>
<Loader loading={loading} showBottom={true}>
{
loaded
? (
<table>
<thead>
<tr>
<th rowSpan={2}>
<SortToggle onToggle={toggleSort} field={'domain'} sort={sort}>
Domain
</SortToggle>
</th>
<th rowSpan={2}>
<SortToggle onToggle={toggleSort} field={'softwareName'} sort={sort}>
Software
</SortToggle>
</th>
<th colSpan={3}>User count</th>
<th rowSpan={2} className={'number-cell'}>
<SortToggle onToggle={toggleSort} field={'statusesCount'} sort={sort}>
Statuses
</SortToggle>
</th>
<th rowSpan={2}>
<SortToggle onToggle={toggleSort} field={'openRegistrations'} sort={sort}>
Registrations
</SortToggle>
</th>
<th rowSpan={2}>
<SortToggle onToggle={toggleSort} field={'refreshedAt'} sort={sort}>
Last refreshed
</SortToggle>
</th>
</tr>
<tr>
<th className={'number-cell'}>
<SortToggle onToggle={toggleSort} field={'totalUserCount'} sort={sort}>
Total
</SortToggle>
</th>
<th className={'number-cell'}>
<SortToggle onToggle={toggleSort} field={'monthActiveUserCount'} sort={sort}>
Month active
</SortToggle>
</th>
<th className={'number-cell'}>
<SortToggle onToggle={toggleSort} field={'halfYearActiveUserCount'} sort={sort}>
Half year active
</SortToggle>
</th>
</tr>
</thead>
<tbody>
{results.length
? results.map((node, index) => {
return (
<tr key={index}>
<td>{node.domain}</td>
<td>
<div title={'Name'}><SoftwareBadge softwareName={node.softwareName}/></div>
<div title={'Version'}>{node.softwareVersion ?? ''}</div></td>
<td className={'number-cell'}>{node.totalUserCount ?? '?'}</td>
<td className={'number-cell'}>{node.monthActiveUserCount ?? '?'}</td>
<td className={'number-cell'}>{node.halfYearActiveUserCount ?? '?'}</td>
<td className={'number-cell'}>{node.statusesCount ?? '?'}</td>
<td>{node.openRegistrations === null ? '?' : (node.openRegistrations ? 'Opened' : 'Closed')}</td>
<td>{node.refreshedAt ? (new Date(node.refreshedAt)).toLocaleDateString() : 'Never'}</td>
</tr>
)
})
: (
<tr>
<td colSpan={9}>No servers found</td>
</tr>
)}
</tbody>
</table>
)
: ''
}
</Loader>
{hasMore && !loading
? (
<button className={'next-page'} onClick={handleLoadMore}>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="angle-double-down"
className="svg-inline--fa fa-angle-double-down fa-w-10" role="img"
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>
</button>
)
: ''}
</Layout>
<Layout matomoConfig={matomoConfig}>
<Head>
<title>{siteTitle}</title>
</Head>
<h1>Search servers</h1>
<form onSubmit={handleSearchSubmit}>
<div className={'input-group mb-3'}>
<input
name={'query'}
id={'query'}
type={'search'}
className={'form-control'}
onChange={handleQueryChange}
onBlur={handleQueryChange}
value={query}
placeholder={'Search servers on fediverse'}
autoFocus={true}
aria-label="Search servers on fediverse"
aria-describedby="search-nodes-button"
/>
<button type={'submit'} className={'btn btn-primary'} id={'search-nodes-button'}>
<FontAwesomeIcon icon={faSearch}/>
<span>Search</span>
</button>
</div>
</form>
<Loader loading={loading} showBottom={true}>
{
loaded
? (
<div className="table-responsive">
<table className={'table table-dark table-striped table-bordered nodes'}>
<thead>
<tr>
<th rowSpan={2}>
<SortToggle onToggle={toggleSort} field={'domain'} sort={sort}>
Domain
</SortToggle>
</th>
<th rowSpan={2}>
<SortToggle onToggle={toggleSort} field={'softwareName'} sort={sort}>
Software
</SortToggle>
</th>
<th colSpan={3}>User count</th>
<th rowSpan={2} className={'number-cell'}>
<SortToggle onToggle={toggleSort} field={'statusesCount'} sort={sort}>
Statuses
</SortToggle>
</th>
<th rowSpan={2}>
<SortToggle onToggle={toggleSort} field={'openRegistrations'} sort={sort}>
Registrations
</SortToggle>
</th>
<th rowSpan={2}>
<SortToggle onToggle={toggleSort} field={'refreshedAt'} sort={sort}>
Last refreshed
</SortToggle>
</th>
</tr>
<tr>
<th className={'text-end'}>
<SortToggle onToggle={toggleSort} field={'totalUserCount'} sort={sort}>
Total
</SortToggle>
</th>
<th className={'text-end'}>
<SortToggle onToggle={toggleSort} field={'monthActiveUserCount'} sort={sort}>
Month active
</SortToggle>
</th>
<th className={'text-end'}>
<SortToggle onToggle={toggleSort} field={'halfYearActiveUserCount'} sort={sort}>
Half year active
</SortToggle>
</th>
</tr>
</thead>
<tbody>
{results.length
? results.map((node, index) => {
return (
<tr key={index}>
<td>{node.domain}</td>
<td>
<div title={'Name'}><SoftwareBadge
softwareName={node.softwareName}/></div>
<div title={'Version'}>{node.softwareVersion ?? ''}</div>
</td>
<td className={'text-end'}>{node.totalUserCount ?? '?'}</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.refreshedAt ? (new Date(node.refreshedAt)).toLocaleDateString() : 'Never'}</td>
</tr>
)
})
: (
<tr>
<td colSpan={9}>No servers found</td>
</tr>
)}
</tbody>
</table>
</div>
)
: ''
}
</Loader>
{hasMore && !loading
? (
<div className={'d-flex justify-content-center'}>
<button className={'btn btn-secondary'} onClick={handleLoadMore}>
<FontAwesomeIcon icon={faAngleDoubleDown} className={'margin-right'} />
<span>Load more</span>
</button>
</div>
)
: ''}
</Layout>
)
}
export const getServerSideProps:GetServerSideProps = async (context) => {
export const getServerSideProps: GetServerSideProps = async (context) => {
console.info('Loading matomo config', matomoConfig)
return {
props: {

Wyświetl plik

@ -96,88 +96,95 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
<title>{'Stats | ' + siteTitle}</title>
</Head>
<h1>Index stats</h1>
<table>
<thead>
<tr>
<th>
<SortToggle onToggle={toggleSort} field={'softwareName'} sort={sort}>
Software name
</SortToggle>
</th>
<th className={'number-cell'}>
<SortToggle onToggle={toggleSort} field={'nodeCount'} sort={sort}>
Instance count
</SortToggle>
</th>
<th className={'number-cell'}>
<SortToggle onToggle={toggleSort} field={'accountCount'} sort={sort}>
Account count
</SortToggle>
</th>
<th className={'number-cell'}>
<SortToggle onToggle={toggleSort} field={'channelCount'} sort={sort}>
Channel count
</SortToggle>
</th>
</tr>
</thead>
<Loader loading={loading} hideContent={!loaded} table={4} showTop={true}>
{stats === null
? (<tr><td colSpan={4}><em>Failed to load stats data!</em></td></tr>)
: (
<>
<tbody>
{
stats.softwares.map((software, index) => {
return software.name !== null
? (
<tr key={index}>
<td>
<SoftwareBadge softwareName={software.name}/>
</td>
<td className={'number-cell'}>
<span>{software.nodeCount}</span>
<ProgressBar way={'left'}
percents={100 * software.nodeCount / max.nodeCount}/>
</td>
<td className={'number-cell'}>
<span>{software.accountCount}</span>
<ProgressBar way={'left'}
percents={100 * software.accountCount / max.accountCount}/>
</td>
<td className={'number-cell'}>
<span>{software.channelCount}</span>
<ProgressBar way={'left'}
percents={100 * software.channelCount / max.channelCount}/>
</td>
</tr>
)
: (
<tr key={index}>
<td><em>Not recognized</em></td>
<td className={'number-cell'}><span>{software.nodeCount}</span></td>
<td className={'number-cell'}><span>{software.accountCount}</span>
</td>
<td className={'number-cell'}><span>{software.channelCount}</span>
</td>
</tr>
)
})
}
</tbody>
<tfoot>
<tr>
<th>Summary</th>
<th className={'number-cell'}>{sum.nodeCount}</th>
<th className={'number-cell'}>{sum.accountCount}</th>
<th className={'number-cell'}>{sum.channelCount}</th>
</tr>
</tfoot>
</>
)
}
</Loader>
</table>
<div className="table-responsive">
<table className={'table table-dark table-striped table-bordered stats'}>
<thead>
<tr>
<th>
<SortToggle onToggle={toggleSort} field={'softwareName'} sort={sort}>
Software name
</SortToggle>
</th>
<th className={'text-end'}>
<SortToggle onToggle={toggleSort} field={'nodeCount'} sort={sort}>
Instance count
</SortToggle>
</th>
<th className={'text-end'}>
<SortToggle onToggle={toggleSort} field={'accountCount'} sort={sort}>
Account count
</SortToggle>
</th>
<th className={'text-end'}>
<SortToggle onToggle={toggleSort} field={'channelCount'} sort={sort}>
Channel count
</SortToggle>
</th>
</tr>
</thead>
<Loader loading={loading} hideContent={!loaded} table={4} showTop={true}>
{stats === null
? (<tr>
<td colSpan={4}><em>Failed to load stats data!</em></td>
</tr>)
: (
<>
<tbody>
{
stats.softwares.map((software, index) => {
return software.name !== null
? (
<tr key={index}>
<td>
<SoftwareBadge softwareName={software.name}/>
</td>
<td className={'text-end'}>
<span>{software.nodeCount}</span>
<ProgressBar way={'left'}
percents={100 * software.nodeCount / max.nodeCount}/>
</td>
<td className={'text-end'}>
<span>{software.accountCount}</span>
<ProgressBar way={'left'}
percents={100 * software.accountCount / max.accountCount}/>
</td>
<td className={'text-end'}>
<span>{software.channelCount}</span>
<ProgressBar way={'left'}
percents={100 * software.channelCount / max.channelCount}/>
</td>
</tr>
)
: (
<tr key={index}>
<td><em>Not recognized</em></td>
<td className={'text-end'}><span>{software.nodeCount}</span>
</td>
<td className={'text-end'}>
<span>{software.accountCount}</span>
</td>
<td className={'text-end'}>
<span>{software.channelCount}</span>
</td>
</tr>
)
})
}
</tbody>
<tfoot>
<tr>
<th>Summary</th>
<th className={'text-end'}>{sum.nodeCount}</th>
<th className={'text-end'}>{sum.accountCount}</th>
<th className={'text-end'}>{sum.channelCount}</th>
</tr>
</tfoot>
</>
)
}
</Loader>
</table>
</div>
</Layout>
)
}

Wyświetl plik

@ -1,344 +1,66 @@
:root {
--shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.4);
--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;
//}
}
@import "./variables";
@import '~bootstrap/scss/bootstrap.scss';
table {
width: 100%;
text-align: left;
margin-bottom: 1em;
.svg-inline--fa,
.with-emoji .emoji {
max-width: 1em;
max-height: 1em;
height: 2em;
img {
max-width: 1em;
max-height: 1em;
display: inline;
&.margin-right {
margin-right: 0.5em;
}
td, th {
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;
vertical-align: baseline;
}
}
}
th {
background-color: var(--front-bg-color);
&.margin-left {
margin-left: 0.5em;
}
}
input,
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;
}
.software-name > .icon {
max-width: 1.5em;
max-height: 1.5em;
padding-right: 0.5em;
}
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;
}
.navbar {
.navbar-brand {
.logo {
grid-column: start/ text;
grid-row: start/end;
img {
height: 100%;
}
height: 1.5rem;
margin-right: 0.5rem;
}
}
}
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);
table.stats {
.software-name {
.icon {
max-height: 2em;
max-width: 2em;
}
}
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-height: 1.5em;
position: relative;
top: 0.25em;
outline-color: var(--main-fg-color);
}
}
}
}
.progress {
height: 0.5em;
}
}
.loader {
text-align: center;
.loader-visualisation {
margin: 1em 0;
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 {
margin-left: 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;
padding: 1em;
border-radius: 1em;
margin-bottom: 1em;
background-color: var(--front-bg-color);
display: grid;
grid-template-columns: [start] min(20%) [main] auto [end];
grid-template-rows: [start] auto [address] auto [badges];
grid-template-columns: [start] min(15%) [main] auto [end];
grid-template-rows: [start] auto [address] auto [software] auto [badges];
column-gap: 1em;
row-gap: 1em;
box-shadow: var(--shadow);
position: relative;
> * {
align-self: start;
}
.avatar {
border-radius: 0.5em;
width: 100%;
grid-column: start /main;
grid-row: span 3;
grid-row: span 4;
}
@ -353,58 +75,23 @@ img {
grid-row: address;
}
.software-name{
grid-column: main /end;
grid-row: software;
}
.badges {
grid-column: main /end;
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 {
display: inline-block;
margin-right: 1em;
white-space: nowrap;
}
.subscriptions {
& > * {
display: inline;
margin-right: 0.3em;
&:last-child::before {
content: '/';
display: inline;
margin-right: 0.3em;
}
}
.badge{
margin-right: 0.5em;
}
}
.fields {
grid-column: start / end;
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 {
@ -429,25 +116,14 @@ img {
grid-column-start: start;
margin-top: -1em;
}
.badges{
margin-top: -1em;
}
.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;
}
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -18,6 +18,7 @@ export const feedResponseItemSchema = z.object({
bot: z.boolean().nullable(),
createdAt: z.string(),
description: z.string(),
displayName: z.string(),
fields: z.array(feedResponseFieldSchema).nullable(),
followersCount: z.number().nullable(),