kopia lustrzana https://github.com/Stopka/fedisearch
Added node search page
rodzic
39e21960ff
commit
84a0460596
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/feeds',
|
||||
permanent: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
|
@ -35,7 +35,7 @@ const Layout: React.FC<{ matomoConfig: UserOptions, children: React.ReactNode }>
|
|||
</header>
|
||||
<nav>
|
||||
<ul>
|
||||
<NavItem path={'/'} label={'Search people'} icon={(
|
||||
<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">
|
||||
|
@ -43,6 +43,14 @@ const Layout: React.FC<{ matomoConfig: UserOptions, children: React.ReactNode }>
|
|||
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"
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react'
|
||||
import { StatsRequest, StatsRequestSortBy } from '../types/StatsRequest'
|
||||
import { Sort } from '../types/Sort'
|
||||
|
||||
const SortToggle: React.FC<{
|
||||
onToggle:(StatsRequestSortBy)=>void,
|
||||
field:StatsRequestSortBy,
|
||||
sort: StatsRequest
|
||||
field:string,
|
||||
sort: Sort
|
||||
}> = ({ onToggle, field, sort, children }) => {
|
||||
return (
|
||||
<a className={'sort-toggle'} href={'#'} onClick={() => onToggle(field)}>
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import prisma from '../../lib/prisma'
|
||||
import { pageLimit } from '../../lib/pageLimit'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { nodeRequestSchema } from '../../types/NodeRequest'
|
||||
import { NodeResponse } from '../../types/NodeResponse'
|
||||
|
||||
const handleFeedSearch = async (req: NextApiRequest, res: NextApiResponse<NodeResponse>): Promise<void> => {
|
||||
console.info('Searching nodes', { query: req.query })
|
||||
|
||||
const nodeRequest = nodeRequestSchema.parse(req.query)
|
||||
const phrases = (nodeRequest.search ?? '').trim().split(/[\s+]+/)
|
||||
nodeRequest.sortBy = nodeRequest.sortBy ?? 'refreshedAt'
|
||||
nodeRequest.sortWay = nodeRequest.sortWay ?? 'desc'
|
||||
const order = {}
|
||||
order[nodeRequest.sortBy] = nodeRequest.sortWay
|
||||
const nodes = await prisma.node.findMany({
|
||||
where: {
|
||||
AND: phrases.map(phrase => {
|
||||
return {
|
||||
OR: [
|
||||
{
|
||||
domain: {
|
||||
contains: phrase,
|
||||
mode: 'insensitive'
|
||||
}
|
||||
},
|
||||
{
|
||||
softwareName: {
|
||||
contains: phrase,
|
||||
mode: 'insensitive'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
NOT: {
|
||||
softwareName: null
|
||||
}
|
||||
},
|
||||
take: pageLimit + 1,
|
||||
skip: (nodeRequest.page ?? 0) * pageLimit,
|
||||
orderBy: [order]
|
||||
})
|
||||
|
||||
res.status(200)
|
||||
.json({
|
||||
hasMore: typeof nodes[pageLimit] !== 'undefined',
|
||||
nodes: nodes.slice(0, pageLimit).map(node => {
|
||||
return {
|
||||
softwareName: node.softwareName,
|
||||
softwareVersion: node.softwareVersion,
|
||||
totalUserCount: node.totalUserCount,
|
||||
monthActiveUserCount: node.monthActiveUserCount,
|
||||
halfYearActiveUserCount: node.halfYearActiveUserCount,
|
||||
statusesCount: node.statusesCount,
|
||||
openRegistrations: node.openRegistrations,
|
||||
refreshedAt: node.refreshedAt ? node.refreshedAt.toISOString() : null,
|
||||
domain: node.domain
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default handleFeedSearch
|
|
@ -11,7 +11,7 @@ import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
|||
|
||||
let source = axios.CancelToken.source()
|
||||
|
||||
const Home: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)
|
||||
|
@ -108,6 +108,7 @@ const Home:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = ({
|
|||
<Head>
|
||||
<title>{siteTitle}</title>
|
||||
</Head>
|
||||
<h1>Search people</h1>
|
||||
<form onSubmit={handleSearchSubmit}>
|
||||
<label htmlFor={'query'}>Search on fediverse</label>
|
||||
<input
|
||||
|
@ -164,4 +165,4 @@ export const getServerSideProps:GetServerSideProps = async (context) => {
|
|||
}
|
||||
}
|
||||
|
||||
export default Home
|
||||
export default Feeds
|
|
@ -0,0 +1,262 @@
|
|||
import Head from 'next/head'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import Loader from '../components/Loader'
|
||||
import Layout, { siteTitle } from '../components/Layout'
|
||||
import { matomoConfig } from '../lib/matomoConfig'
|
||||
import getMatomo from '../lib/getMatomo'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import { nodeResponseSchema } from '../types/NodeResponse'
|
||||
import SoftwareBadge from '../components/badges/SoftwareBadge'
|
||||
import SortToggle from '../components/SortToggle'
|
||||
import { StatsRequestSortBy } from '../types/StatsRequest'
|
||||
import { Sort } from '../types/Sort'
|
||||
|
||||
let source = axios.CancelToken.source()
|
||||
|
||||
const Nodes:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = ({ matomoConfig }) => {
|
||||
const [query, setQuery] = useState('')
|
||||
const [submitted, setSubmitted] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [results, setResults] = useState([])
|
||||
const [page, setPage] = useState(0)
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [sort, setSort] = useState<Sort>({
|
||||
sortBy: 'refreshedAt', sortWay: 'desc'
|
||||
})
|
||||
|
||||
const search = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
console.info('Retrieving results', { query, page })
|
||||
source = axios.CancelToken.source()
|
||||
const response = await axios.get('/api/node', {
|
||||
params: { search: query, page, sortBy: sort.sortBy, sortWay: sort.sortWay },
|
||||
cancelToken: source.token
|
||||
})
|
||||
const responseData = await nodeResponseSchema.parseAsync(response.data)
|
||||
setHasMore(responseData.hasMore)
|
||||
setResults([
|
||||
...(page > 0 ? results : []),
|
||||
...responseData.nodes
|
||||
])
|
||||
setLoaded(true)
|
||||
} catch (e) {
|
||||
console.warn('Search failed', e)
|
||||
setLoaded(true)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const loadNewQueryResults = () => {
|
||||
console.info('Cancelling searches')
|
||||
source.cancel('New query on the way')
|
||||
setResults([])
|
||||
setHasMore(false)
|
||||
setLoaded(false)
|
||||
console.info('Loading new query search', { query, page })
|
||||
setLoading(true)
|
||||
setTimeout(search)
|
||||
getMatomo(matomoConfig).trackEvent({
|
||||
category: 'nodes',
|
||||
action: 'new-search'
|
||||
})
|
||||
}
|
||||
|
||||
const loadNextPageResults = () => {
|
||||
setHasMore(false)
|
||||
if (page === 0) {
|
||||
return
|
||||
}
|
||||
console.info('Loading next page', { query, page })
|
||||
setTimeout(search)
|
||||
getMatomo(matomoConfig).trackEvent({
|
||||
category: 'nodes',
|
||||
action: 'next-page',
|
||||
customDimensions: [
|
||||
{
|
||||
value: page.toString(),
|
||||
id: 1
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const handleQueryChange = (event) => {
|
||||
const value = event.target.value
|
||||
console.info('Query changed', { query: value })
|
||||
setQuery(value)
|
||||
setPage(0)
|
||||
}
|
||||
|
||||
const handleSearchSubmit = event => {
|
||||
event.preventDefault()
|
||||
setQuery(query)
|
||||
setSubmitted(new Date())
|
||||
setPage(0)
|
||||
}
|
||||
|
||||
const handleLoadMore = event => {
|
||||
event.preventDefault()
|
||||
setPage(page + 1)
|
||||
}
|
||||
|
||||
const toggleSort = (sortBy: StatsRequestSortBy) => {
|
||||
const sortWay = sort.sortBy === sortBy && sort.sortWay === 'asc' ? 'desc' : 'asc'
|
||||
getMatomo(matomoConfig).trackEvent({
|
||||
category: 'nodes',
|
||||
action: 'sort',
|
||||
customDimensions: [
|
||||
{
|
||||
value: `${sortBy} ${sortWay}`,
|
||||
id: 2
|
||||
}
|
||||
]
|
||||
})
|
||||
setSort({
|
||||
sortBy: sortBy,
|
||||
sortWay: sortWay
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(loadNewQueryResults, [query, submitted, sort])
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps:GetServerSideProps = async (context) => {
|
||||
console.info('Loading matomo config', matomoConfig)
|
||||
return {
|
||||
props: {
|
||||
matomoConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Nodes
|
|
@ -8,8 +8,10 @@ import axios from 'axios'
|
|||
import { StatsResponse, statsResponseSchema } from '../types/StatsResponse'
|
||||
import SoftwareBadge from '../components/badges/SoftwareBadge'
|
||||
import ProgressBar from '../components/ProgressBar'
|
||||
import { StatsRequest, StatsRequestSortBy } from '../types/StatsRequest'
|
||||
import { StatsRequestSortBy } from '../types/StatsRequest'
|
||||
import SortToggle from '../components/SortToggle'
|
||||
import getMatomo from '../lib/getMatomo'
|
||||
import { Sort } from '../types/Sort'
|
||||
|
||||
let source = axios.CancelToken.source()
|
||||
|
||||
|
@ -17,14 +19,25 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
|||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [loaded, setLoaded] = useState<boolean>(false)
|
||||
const [stats, setStats] = useState<StatsResponse | null>(null)
|
||||
const [sort, setSort] = useState<StatsRequest>({
|
||||
const [sort, setSort] = useState<Sort>({
|
||||
sortBy: 'nodeCount', sortWay: 'desc'
|
||||
})
|
||||
|
||||
const toggleSort = (sortBy: StatsRequestSortBy) => {
|
||||
const sortWay = sort.sortBy === sortBy && sort.sortWay === 'asc' ? 'desc' : 'asc'
|
||||
getMatomo(matomoConfig).trackEvent({
|
||||
category: 'stats',
|
||||
action: 'sort',
|
||||
customDimensions: [
|
||||
{
|
||||
value: `${sortBy} ${sortWay}`,
|
||||
id: 2
|
||||
}
|
||||
]
|
||||
})
|
||||
setSort({
|
||||
sortBy: sortBy,
|
||||
sortWay: sort.sortBy === sortBy && sort.sortWay === 'asc' ? 'desc' : 'asc'
|
||||
sortWay: sortWay
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin-bottom: 1em;
|
||||
|
||||
img {
|
||||
max-width: 1em;
|
||||
|
@ -29,7 +30,6 @@ table {
|
|||
border: 1px solid var(--front-bg-color);
|
||||
border-radius: 0.3em;
|
||||
padding: 0.3em;
|
||||
word-break: break-word;
|
||||
|
||||
&.number-cell {
|
||||
text-align: right;
|
||||
|
@ -223,6 +223,8 @@ form {
|
|||
fill: var(--main-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
nav {
|
||||
|
@ -344,13 +346,11 @@ img {
|
|||
grid-column: main / end;
|
||||
grid-row: start;
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.address {
|
||||
grid-column: main / end;
|
||||
grid-row: address;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.badges {
|
||||
|
@ -400,7 +400,6 @@ img {
|
|||
border: 1px solid var(--main-bg-color);
|
||||
border-radius: 0.3em;
|
||||
padding: 0.3em;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
th {
|
||||
|
@ -411,7 +410,6 @@ img {
|
|||
.description {
|
||||
grid-column: start/end;
|
||||
grid-row: span 1;
|
||||
word-break: break-word;
|
||||
|
||||
p {
|
||||
margin: 0 0 1em 0;
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { z } from 'zod'
|
||||
import { preserveUndefined, stringToInt, transform } from '../lib/transform'
|
||||
|
||||
export const statsRequestSortBySchema = z.enum([
|
||||
'softwareName',
|
||||
'softwareVersion',
|
||||
'totalUserCount',
|
||||
'monthActiveUserCount',
|
||||
'halfYearActiveUserCount',
|
||||
'statusesCount',
|
||||
'openRegistrations',
|
||||
'refreshedAt',
|
||||
'domain'
|
||||
])
|
||||
|
||||
export const statsRequestSortWaySchema = z.enum([
|
||||
'asc',
|
||||
'desc'
|
||||
])
|
||||
|
||||
export const nodeRequestSchema = z.object({
|
||||
sortBy: z.optional(statsRequestSortBySchema),
|
||||
sortWay: z.optional(statsRequestSortWaySchema),
|
||||
search: z.string().optional(),
|
||||
page: transform(
|
||||
z.string().optional(),
|
||||
preserveUndefined(stringToInt),
|
||||
z.number().gte(0).optional()
|
||||
)
|
||||
})
|
||||
|
||||
export type NodeRequest = z.infer<typeof nodeRequestSchema>
|
|
@ -0,0 +1,21 @@
|
|||
import { z } from 'zod'
|
||||
|
||||
export const nodeResponseItemSchema = z.object({
|
||||
softwareName: z.string().nullable(),
|
||||
softwareVersion: z.string().nullable(),
|
||||
totalUserCount: z.number().nullable(),
|
||||
monthActiveUserCount: z.number().nullable(),
|
||||
halfYearActiveUserCount: z.number().nullable(),
|
||||
statusesCount: z.number().nullable(),
|
||||
openRegistrations: z.boolean().nullable(),
|
||||
refreshedAt: z.string().nullable(),
|
||||
domain: z.string()
|
||||
})
|
||||
|
||||
export const nodeResponseSchema = z.object({
|
||||
hasMore: z.boolean(),
|
||||
nodes: z.array(nodeResponseItemSchema)
|
||||
})
|
||||
|
||||
export type NodeResponse = z.infer<typeof nodeResponseSchema>
|
||||
export type NodeResponseItem = z.infer<typeof nodeResponseItemSchema>
|
|
@ -0,0 +1,4 @@
|
|||
export type Sort = {
|
||||
sortBy: string,
|
||||
sortWay: 'asc' | 'desc'
|
||||
}
|
Ładowanie…
Reference in New Issue