kopia lustrzana https://github.com/Stopka/fedisearch
Finished nextjs appdir adaptations
rodzic
b8f5d28dd5
commit
3a02c39109
13
Dockerfile
13
Dockerfile
|
@ -1,10 +1,11 @@
|
|||
FROM node:18-bullseye AS prebuild
|
||||
|
||||
FROM prebuild AS build
|
||||
WORKDIR /srv
|
||||
COPY application/package*.json ./
|
||||
COPY application/yarn.lock ./
|
||||
RUN yarn install
|
||||
COPY application/. .
|
||||
RUN chmod -R uog+r .
|
||||
RUN yarn build
|
||||
|
||||
FROM build as dev
|
||||
|
@ -12,13 +13,9 @@ CMD yarn dev
|
|||
|
||||
FROM prebuild AS prod
|
||||
RUN groupadd -g 1001 nodejs
|
||||
RUN useradd -u 1001 -g 1001 nextjs
|
||||
RUN useradd -m -u 1001 -g 1001 nextjs
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
WORKDIR /srv
|
||||
COPY --from=build /srv/node_modules ./node_modules
|
||||
COPY --from=build /srv/package*.json ./
|
||||
COPY --from=build /srv/next.config.js ./
|
||||
COPY --from=build --chown=nextjs:nodejs /srv/src/.next ./src/.next
|
||||
COPY --from=build /srv/src/public ./src/public
|
||||
CMD yarn start
|
||||
COPY --from=build --chown=nextjs:nodejs /srv/. ./
|
||||
CMD yarn build && yarn start
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import HtmlHead from '../../components/layout/HtmlHead'
|
||||
|
||||
export default function Head (): ReactElement {
|
||||
return <>
|
||||
<HtmlHead title={'People'} description={'Search people on Fediverse'}/>
|
||||
</>
|
||||
}
|
|
@ -6,7 +6,7 @@ import createConfig from '../../config/createConfig'
|
|||
export default async function Page (): Promise<ReactElement> {
|
||||
const clientConfig = createConfig().get('client')
|
||||
return (
|
||||
<Layout title={'People'} description={'Search people on Fediverse'} config={clientConfig}>
|
||||
<Layout title={'People'} config={clientConfig}>
|
||||
<FeedSearch />
|
||||
</Layout>
|
||||
)
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import { ReactElement } from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
import HtmlHead from '../components/layout/HtmlHead'
|
||||
|
||||
export default function Head (): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<title></title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</>
|
||||
)
|
||||
return <HtmlHead />
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import Footer from '../components/layout/Footer'
|
||||
import NavBar from '../components/layout/NavBar'
|
||||
import '../styles/global.scss'
|
||||
|
||||
export default function RootLayout ({
|
||||
children
|
||||
|
@ -7,8 +10,15 @@ export default function RootLayout ({
|
|||
}): ReactElement {
|
||||
return (
|
||||
<html>
|
||||
<head />
|
||||
<body>{children}</body>
|
||||
<body>
|
||||
<div className="container">
|
||||
<NavBar/>
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<Footer/>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
|
||||
export default function Loading (): ReactElement {
|
||||
console.log('Loading')
|
||||
return <div className={'container'}>
|
||||
<h1 className={'placeholder-glow'} aria-hidden={true}><span className={'placeholder col-4'}/></h1>
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import HtmlHead from '../../components/layout/HtmlHead'
|
||||
|
||||
export default function Head (): ReactElement {
|
||||
return <>
|
||||
<HtmlHead title={'Servers'} description={'Search Fediverse servers'}/>
|
||||
</>
|
||||
}
|
|
@ -6,7 +6,7 @@ import createConfig from '../../config/createConfig'
|
|||
export default async function Page (): Promise<ReactElement> {
|
||||
const clientConfig = createConfig().get('client')
|
||||
return (
|
||||
<Layout title={'Servers'} description={'Search Fediverse servers'} config={clientConfig}>
|
||||
<Layout title={'Servers'} config={clientConfig}>
|
||||
<NodeSearch />
|
||||
</Layout>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import HtmlHead from '../../components/layout/HtmlHead'
|
||||
|
||||
export default function Head (): ReactElement {
|
||||
return <>
|
||||
<HtmlHead title={'Opt out'} description={'How to opt out from our index'}/>
|
||||
</>
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import Accordion from '../../components/accordion/Accordion'
|
||||
import AccordionItem from '../../components/accordion/AccordionItem'
|
||||
import MastodonNoindexOptout from '../../components/optout/MastodonNoindexOptout'
|
||||
import MastodonSuggestingOptout from '../../components/optout/MastodonSuggestingOptout'
|
||||
import RobotsTxtOptout from '../../components/optout/RobotsTxtOptout'
|
||||
|
@ -11,7 +10,7 @@ import createConfig from '../../config/createConfig'
|
|||
export default async function Page (): Promise<ReactElement> {
|
||||
const clientConfig = createConfig().get('client')
|
||||
return (
|
||||
<Layout title={'Opt out'} description={'What to do to opt out from the index'} config={clientConfig}>
|
||||
<Layout title={'Opt out'} config={clientConfig}>
|
||||
<p>You don't want to be listed here? There are several ways to opt-out from our index:</p>
|
||||
<Accordion>
|
||||
<MastodonNoindexOptout/>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import HtmlHead from '../../components/layout/HtmlHead'
|
||||
|
||||
export default function Head (): ReactElement {
|
||||
return <>
|
||||
<HtmlHead title={'Stats'} description={'Index statistics'}/>
|
||||
</>
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import NodeSearch from '../../components/node/NodeSearch'
|
||||
import Layout from '../../components/server/Layout'
|
||||
import Stats from "../../components/stats/Stats";
|
||||
import Stats from '../../components/stats/Stats'
|
||||
import createConfig from '../../config/createConfig'
|
||||
|
||||
export default async function Page (): Promise<ReactElement> {
|
||||
const clientConfig = createConfig().get('client')
|
||||
return (
|
||||
<Layout title={'Stats'} description={'Fediverse stats'} config={clientConfig}>
|
||||
<Layout title={'Stats'} config={clientConfig}>
|
||||
<Stats />
|
||||
</Layout>
|
||||
)
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
|
||||
const Spinner: React.FC = () => {
|
||||
export default function Spinner (): ReactElement {
|
||||
return (
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Spinner
|
||||
|
|
|
@ -6,10 +6,11 @@ import SearchInput from '../form/SearchInput'
|
|||
import SubmitButton from '../form/SubmitButton'
|
||||
|
||||
export default function FeedForm (
|
||||
{ onSubmit, onQueryChange, query }: {
|
||||
{ onSubmit, onQueryChange, query, loading }: {
|
||||
onSubmit: () => void
|
||||
onQueryChange: (query: FeedQueryInput) => void
|
||||
query: FeedQueryInput
|
||||
loading?: boolean
|
||||
}
|
||||
): ReactElement {
|
||||
const handleQueryChange = (event): void => {
|
||||
|
@ -41,6 +42,7 @@ export default function FeedForm (
|
|||
<SubmitButton
|
||||
faIcon={faSearch}
|
||||
label={'Search'}
|
||||
loading={loading}
|
||||
id={'search-feeds-button'}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@ import Badge from './badges/Badge'
|
|||
export default function FeedPlaceholder (): ReactElement {
|
||||
const greyDotBlob = 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=='
|
||||
return (
|
||||
<section className="card feed g-col-12 mb-3" aria-hidden="true">
|
||||
<section className="card feed g-col-12 mb-3 placeholder-wrapper" aria-hidden="true">
|
||||
<div className="card-body">
|
||||
<h3 className={'card-title with-emoji display-name placeholder-glow'}>
|
||||
<a><span className="placeholder col-4"></span></a>
|
||||
|
|
|
@ -16,7 +16,6 @@ export default function FeedResults ({ feeds }: { feeds: ListFeedsItemFragment[]
|
|||
return (<div className={'grid'}>
|
||||
{
|
||||
feeds.map((feed, index) => {
|
||||
console.info('feed', feed)
|
||||
return (<FeedResult key={index} feed={feed} />)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { usePathname, useSearchParams } from 'next/navigation'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { z } from 'zod'
|
||||
import { FeedQueryInput, ListFeedsDocument } from '../../graphql/generated/types'
|
||||
|
@ -27,7 +27,6 @@ export default function FeedSearch (): ReactElement {
|
|||
const matomo = useMatomo()
|
||||
const searchParams = useSearchParams()
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const routerQuery = feedQueryInputSchema.parse(Object.fromEntries(searchParams))
|
||||
const [page, setPage] = useState<number>(0)
|
||||
const [query, setQuery] = useState<FeedQueryInput>(routerQuery)
|
||||
|
@ -39,7 +38,7 @@ export default function FeedSearch (): ReactElement {
|
|||
}
|
||||
})
|
||||
useEffect((): void => {
|
||||
router.push(`${pathname ?? ''}?${createUrlSearchParams(query).toString()}`)
|
||||
window.history.replaceState({}, '', `${pathname ?? ''}?${createUrlSearchParams(query).toString()}`)
|
||||
matomo.trackEvent({
|
||||
category: 'feeds',
|
||||
action: 'new-search'
|
||||
|
@ -95,7 +94,7 @@ export default function FeedSearch (): ReactElement {
|
|||
}
|
||||
|
||||
return <>
|
||||
<FeedForm query={query} onQueryChange={handleQueryChange} onSubmit={handleSearchSubmit}/>
|
||||
<FeedForm query={query} onQueryChange={handleQueryChange} onSubmit={handleSearchSubmit} loading={loading || pageLoading}/>
|
||||
<FeedInfo show={query.search === ''}>
|
||||
<Loader loading={loading || pageLoading} showBottom={true} placeholder={(<FeedPlaceholder/>)}>
|
||||
<FeedResults feeds={data?.listFeeds?.items}/>
|
||||
|
|
|
@ -2,13 +2,25 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'
|
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React, { ReactElement } from 'react'
|
||||
|
||||
export default function SubmitButton ({ faIcon, label, id }: {
|
||||
export default function SubmitButton ({ faIcon, label, id, loading, loadingLabel }: {
|
||||
faIcon: IconProp
|
||||
label: string
|
||||
loadingLabel?: string
|
||||
loading?: boolean
|
||||
id?: string
|
||||
}): ReactElement {
|
||||
loadingLabel = loadingLabel ?? label
|
||||
return <button type={'submit'} className={'btn btn-primary'} id={id}>
|
||||
{
|
||||
loading === true
|
||||
? <>
|
||||
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span>{loadingLabel}</span>
|
||||
</>
|
||||
: <>
|
||||
<FontAwesomeIcon icon={faIcon}/>
|
||||
<span>{label}</span>
|
||||
</>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
'use client'
|
||||
import React, { ReactElement, ReactNode, useEffect } from 'react'
|
||||
import Head from 'next/head'
|
||||
import { useMatomo } from '../../hooks/MatomoHook'
|
||||
import Footer from './Footer'
|
||||
import NavBar from './NavBar'
|
||||
|
||||
export default function ClientLayout ({
|
||||
children,
|
||||
title,
|
||||
description
|
||||
title
|
||||
}: {
|
||||
children?: ReactNode
|
||||
title: string
|
||||
description: string
|
||||
}): ReactElement {
|
||||
const matomo = useMatomo()
|
||||
useEffect(() => {
|
||||
|
@ -20,24 +15,8 @@ export default function ClientLayout ({
|
|||
}, [])
|
||||
return (
|
||||
<div className={'container'}>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<link rel="icon" href="/fedisearch.png"/>
|
||||
<meta name="description" content={description}/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta property="og:title" content={title}/>
|
||||
<meta property="og:description" content={description}/>
|
||||
<meta property="og:image" content="/fedisearch.png"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
</Head>
|
||||
<div className="container">
|
||||
<NavBar />
|
||||
<main>
|
||||
<h1>{title}</h1>
|
||||
{children}
|
||||
</main>
|
||||
<Footer/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import Link from 'next/link'
|
||||
import React, { ReactElement } from 'react'
|
||||
|
||||
export default function Footer (): ReactElement {
|
||||
return (
|
||||
<footer className={'text-center mt-5'}>
|
||||
<p><a href={'/optout'}>How to opt-out</a></p>
|
||||
<p><Link href={'/optout'}>How to opt-out</Link></p>
|
||||
<p>©{(new Date()).getFullYear()} <a href={'https://skorpil.cz'}>Štěpán Škorpil</a></p>
|
||||
</footer>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
|
||||
export default function ({ title, description }: {
|
||||
title?: string
|
||||
description?: string
|
||||
}): ReactElement {
|
||||
const pageName = 'FediSearch'
|
||||
const htmlTitle = (title !== undefined ? `${title} | ` : '') + pageName
|
||||
description = description ?? 'Search on Fediverse'
|
||||
return <>
|
||||
<title>{htmlTitle}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta name="description" content={description}/>
|
||||
<meta property="og:image" content="/fedisearch.png"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta property="og:title" content={title ?? pageName}/>
|
||||
<meta property="og:description" content={description}/>
|
||||
<link rel="icon" href="/fedisearch.png"/>
|
||||
</>
|
||||
}
|
|
@ -6,10 +6,11 @@ import SearchInput from '../form/SearchInput'
|
|||
import SubmitButton from '../form/SubmitButton'
|
||||
|
||||
export default function NodeForm (
|
||||
{ onSubmit, onQueryChange, query }: {
|
||||
{ onSubmit, onQueryChange, query, loading }: {
|
||||
onSubmit: () => void
|
||||
onQueryChange: (query: NodeQueryInput) => void
|
||||
query: NodeQueryInput
|
||||
loading?: boolean
|
||||
}
|
||||
): ReactElement {
|
||||
const handleQueryChange = (event): void => {
|
||||
|
@ -42,6 +43,7 @@ export default function NodeForm (
|
|||
label={'Search'}
|
||||
faIcon={faSearch}
|
||||
id={'search-nodes-button'}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import SoftwareBadgePlaceholder from '../SoftwareBadgePlaceholder'
|
||||
|
||||
export default function NodePlaceholder (): ReactElement {
|
||||
return (
|
||||
<tbody>
|
||||
<tr>
|
||||
const Row = (): ReactElement => <tr>
|
||||
<td className={'placeholder-glow'}><span className={'placeholder col-10'}/></td>
|
||||
<td>
|
||||
<div><SoftwareBadgePlaceholder /></div>
|
||||
|
@ -18,6 +15,16 @@ export default function NodePlaceholder (): ReactElement {
|
|||
<td className={' placeholder-glow'}><span className={'placeholder col-6'}/></td>
|
||||
<td className={' placeholder-glow'}><span className={'placeholder col-6'}/></td>
|
||||
</tr>
|
||||
|
||||
export default function NodePlaceholder ({ rowCount }: { rowCount?: number }): ReactElement {
|
||||
if (rowCount === undefined || rowCount <= 0) {
|
||||
rowCount = 1
|
||||
}
|
||||
return (
|
||||
<tbody className={'placeholder-wrapper'} aria-hidden="true">
|
||||
{[...Array(rowCount).keys()].map(key => {
|
||||
return <Row key={key}/>
|
||||
})}
|
||||
</tbody>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { usePathname, useSearchParams } from 'next/navigation'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
|
@ -38,7 +38,7 @@ export default function NodeSearch (): ReactElement {
|
|||
const matomo = useMatomo()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const [lastRowCount, setLastRowCount] = useState<number>(1)
|
||||
let routerQuery: NodeQueryInput
|
||||
try {
|
||||
routerQuery = nodeQueryInputSchema.parse(Object.fromEntries(searchParams))
|
||||
|
@ -49,10 +49,10 @@ export default function NodeSearch (): ReactElement {
|
|||
sortWay: SortingWayEnum.Desc
|
||||
}
|
||||
}
|
||||
console.log('Router query', routerQuery)
|
||||
const [query, setQuery] = useState<NodeQueryInput>(routerQuery)
|
||||
const [page, setPage] = useState<number>(0)
|
||||
const [pageLoading, setPageLoading] = useState<boolean>(false)
|
||||
const [pageLoading, setPageLoading] = useState<undefined | 'sort' | 'submit' | 'more'>(undefined)
|
||||
|
||||
const { loading, error, data, fetchMore, refetch } = useQuery(ListNodesDocument, {
|
||||
variables: {
|
||||
query,
|
||||
|
@ -62,6 +62,14 @@ export default function NodeSearch (): ReactElement {
|
|||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const items = data?.listNodes?.items
|
||||
if (items === undefined) {
|
||||
return
|
||||
}
|
||||
setLastRowCount(items.length)
|
||||
}, [data])
|
||||
|
||||
useEffect(() => {
|
||||
matomo.trackEvent({
|
||||
category: 'nodes',
|
||||
|
@ -75,7 +83,7 @@ export default function NodeSearch (): ReactElement {
|
|||
})
|
||||
}, [page])
|
||||
useEffect((): void => {
|
||||
router.push(`${pathname ?? ''}?${createUrlSearchParams(query).toString()}`)
|
||||
window.history.replaceState({}, '', `${pathname ?? ''}?${createUrlSearchParams(query).toString()}`)
|
||||
matomo.trackEvent({
|
||||
category: 'nodes',
|
||||
action: 'new-search'
|
||||
|
@ -89,17 +97,17 @@ export default function NodeSearch (): ReactElement {
|
|||
}
|
||||
|
||||
const handleSearchSubmit = async (): Promise<void> => {
|
||||
setPageLoading(true)
|
||||
setPageLoading('submit')
|
||||
setQuery(query)
|
||||
setPage(0)
|
||||
await refetch({ paging: { page: 0 } })
|
||||
setPageLoading(false)
|
||||
setPageLoading(undefined)
|
||||
}
|
||||
|
||||
const handleLoadMore = async (): Promise<void> => {
|
||||
setPage(page + 1)
|
||||
console.info('Loading next page', { query, page })
|
||||
setPageLoading(true)
|
||||
setPageLoading('more')
|
||||
await fetchMore({
|
||||
variables: {
|
||||
paging: { page: page + 1 }
|
||||
|
@ -121,7 +129,7 @@ export default function NodeSearch (): ReactElement {
|
|||
return fetchMoreResult
|
||||
}
|
||||
})
|
||||
setPageLoading(false)
|
||||
setPageLoading(undefined)
|
||||
}
|
||||
|
||||
const toggleSort = (sortBy: NodeSortingByEnum): void => {
|
||||
|
@ -145,15 +153,18 @@ export default function NodeSearch (): ReactElement {
|
|||
|
||||
return (
|
||||
<>
|
||||
<NodeForm query={query} onQueryChange={handleQueryChange} onSubmit={handleSearchSubmit}/>
|
||||
<NodeForm query={query} onQueryChange={handleQueryChange} onSubmit={handleSearchSubmit} loading={loading || pageLoading !== undefined}/>
|
||||
<ResponsiveTable>
|
||||
<NodeHeader onSortToggle={toggleSort} query={query}/>
|
||||
<Loader loading={loading || pageLoading} showBottom={true} placeholder={(<NodePlaceholder/>)}>
|
||||
<Loader
|
||||
loading={loading || pageLoading !== undefined}
|
||||
showBottom={true}
|
||||
placeholder={(<NodePlaceholder rowCount={pageLoading === 'more' ? 1 : lastRowCount}/>)}>
|
||||
<NodeResults nodes={data?.listNodes?.items}/>
|
||||
</Loader>
|
||||
</ResponsiveTable>
|
||||
<LoadMoreButton onClick={handleLoadMore}
|
||||
show={!loading && !pageLoading && data?.listNodes?.paging?.hasNext === true}/>
|
||||
show={!loading && pageLoading === undefined && data?.listNodes?.paging?.hasNext === true}/>
|
||||
<ErrorMessage message={error?.message}/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,25 +1,21 @@
|
|||
import React, { ReactElement, ReactNode } from 'react'
|
||||
import ClientConfig from '../../config/ClientConfig'
|
||||
import 'server-only'
|
||||
import '../../styles/global.scss'
|
||||
import ClientLayout from '../layout/ClientLayout'
|
||||
import ClientProviders from '../layout/ClientProviders'
|
||||
|
||||
export default function Layout ({
|
||||
children,
|
||||
config,
|
||||
title,
|
||||
description
|
||||
title
|
||||
}: {
|
||||
children?: ReactNode
|
||||
config: ClientConfig
|
||||
title: string
|
||||
description: string
|
||||
}): ReactElement {
|
||||
console.log('Layout')
|
||||
return (
|
||||
<ClientProviders config={config}>
|
||||
<ClientLayout title={title} description={description}>
|
||||
<ClientLayout title={title}>
|
||||
{children}
|
||||
</ClientLayout>
|
||||
</ClientProviders>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { usePathname, useSearchParams } from 'next/navigation'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import {
|
||||
ListStatsDocument,
|
||||
|
@ -30,7 +30,6 @@ export default function Stats (): ReactElement {
|
|||
})
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const matomo = useMatomo()
|
||||
let routerQuery: StatsQueryInput
|
||||
try {
|
||||
|
@ -42,7 +41,6 @@ export default function Stats (): ReactElement {
|
|||
sortWay: SortingWayEnum.Desc
|
||||
}
|
||||
}
|
||||
console.log('Router query', routerQuery)
|
||||
const [query, setQuery] = useState<StatsQueryInput>(routerQuery)
|
||||
const { loading, error, data } = useQuery(ListStatsDocument, {
|
||||
variables: {
|
||||
|
@ -59,7 +57,7 @@ export default function Stats (): ReactElement {
|
|||
setLastSum(sum)
|
||||
}, [data])
|
||||
useEffect(() => {
|
||||
router.push(`${pathname ?? ''}?${createUrlSearchParams(query).toString()}`)
|
||||
window.history.replaceState({}, '', `${pathname ?? ''}?${createUrlSearchParams(query).toString()}`)
|
||||
matomo.trackEvent({
|
||||
category: 'stats',
|
||||
action: 'new-search'
|
||||
|
|
|
@ -24,8 +24,11 @@ const Row = (): ReactElement => <tr>
|
|||
</tr>
|
||||
|
||||
export default function StatsPlaceholder ({ rowCount }: { rowCount?: number }): ReactElement {
|
||||
return <tbody>
|
||||
{[...Array(rowCount ?? 1).keys()].map(key => {
|
||||
if (rowCount === undefined || rowCount <= 0) {
|
||||
rowCount = 1
|
||||
}
|
||||
return <tbody className="placeholder-wrapper" aria-hidden="true">
|
||||
{[...Array(rowCount).keys()].map(key => {
|
||||
return <Row key={key}/>
|
||||
})}
|
||||
</tbody>
|
||||
|
|
|
@ -137,4 +137,6 @@ table.stats {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.placeholder-wrapper{
|
||||
cursor: wait;
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue