Finished nextjs appdir adaptations

main
Štěpán Škorpil 2023-01-06 23:06:03 +01:00
rodzic b8f5d28dd5
commit 3a02c39109
28 zmienionych plików z 177 dodań i 108 usunięć

Wyświetl plik

@ -1,10 +1,11 @@
FROM node:18-bullseye AS prebuild FROM node:18-bullseye AS prebuild
FROM prebuild AS build FROM prebuild AS build
WORKDIR /srv WORKDIR /srv
COPY application/package*.json ./ COPY application/package*.json ./
COPY application/yarn.lock ./
RUN yarn install RUN yarn install
COPY application/. . COPY application/. .
RUN chmod -R uog+r .
RUN yarn build RUN yarn build
FROM build as dev FROM build as dev
@ -12,13 +13,9 @@ CMD yarn dev
FROM prebuild AS prod FROM prebuild AS prod
RUN groupadd -g 1001 nodejs RUN groupadd -g 1001 nodejs
RUN useradd -u 1001 -g 1001 nextjs RUN useradd -m -u 1001 -g 1001 nextjs
USER nextjs USER nextjs
EXPOSE 3000 EXPOSE 3000
WORKDIR /srv WORKDIR /srv
COPY --from=build /srv/node_modules ./node_modules COPY --from=build --chown=nextjs:nodejs /srv/. ./
COPY --from=build /srv/package*.json ./ CMD yarn build && yarn start
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

Wyświetl plik

@ -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'}/>
</>
}

Wyświetl plik

@ -6,7 +6,7 @@ import createConfig from '../../config/createConfig'
export default async function Page (): Promise<ReactElement> { export default async function Page (): Promise<ReactElement> {
const clientConfig = createConfig().get('client') const clientConfig = createConfig().get('client')
return ( return (
<Layout title={'People'} description={'Search people on Fediverse'} config={clientConfig}> <Layout title={'People'} config={clientConfig}>
<FeedSearch /> <FeedSearch />
</Layout> </Layout>
) )

Wyświetl plik

@ -1,11 +1,6 @@
import { ReactElement } from 'react' import React, { ReactElement } from 'react'
import HtmlHead from '../components/layout/HtmlHead'
export default function Head (): ReactElement { export default function Head (): ReactElement {
return ( return <HtmlHead />
<>
<title></title>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<link rel="icon" href="/favicon.ico" />
</>
)
} }

Wyświetl plik

@ -1,4 +1,7 @@
import React, { ReactElement } from 'react' 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 ({ export default function RootLayout ({
children children
@ -6,9 +9,16 @@ export default function RootLayout ({
children: React.ReactNode children: React.ReactNode
}): ReactElement { }): ReactElement {
return ( return (
<html> <html>
<head /> <body>
<body>{children}</body> <div className="container">
</html> <NavBar/>
<main>
{children}
</main>
<Footer/>
</div>
</body>
</html>
) )
} }

Wyświetl plik

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

Wyświetl plik

@ -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'}/>
</>
}

Wyświetl plik

@ -6,7 +6,7 @@ import createConfig from '../../config/createConfig'
export default async function Page (): Promise<ReactElement> { export default async function Page (): Promise<ReactElement> {
const clientConfig = createConfig().get('client') const clientConfig = createConfig().get('client')
return ( return (
<Layout title={'Servers'} description={'Search Fediverse servers'} config={clientConfig}> <Layout title={'Servers'} config={clientConfig}>
<NodeSearch /> <NodeSearch />
</Layout> </Layout>
) )

Wyświetl plik

@ -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'}/>
</>
}

Wyświetl plik

@ -1,6 +1,5 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import Accordion from '../../components/accordion/Accordion' import Accordion from '../../components/accordion/Accordion'
import AccordionItem from '../../components/accordion/AccordionItem'
import MastodonNoindexOptout from '../../components/optout/MastodonNoindexOptout' import MastodonNoindexOptout from '../../components/optout/MastodonNoindexOptout'
import MastodonSuggestingOptout from '../../components/optout/MastodonSuggestingOptout' import MastodonSuggestingOptout from '../../components/optout/MastodonSuggestingOptout'
import RobotsTxtOptout from '../../components/optout/RobotsTxtOptout' import RobotsTxtOptout from '../../components/optout/RobotsTxtOptout'
@ -11,7 +10,7 @@ import createConfig from '../../config/createConfig'
export default async function Page (): Promise<ReactElement> { export default async function Page (): Promise<ReactElement> {
const clientConfig = createConfig().get('client') const clientConfig = createConfig().get('client')
return ( 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&apos;t want to be listed here? There are several ways to opt-out from our index:</p> <p>You don&apos;t want to be listed here? There are several ways to opt-out from our index:</p>
<Accordion> <Accordion>
<MastodonNoindexOptout/> <MastodonNoindexOptout/>

Wyświetl plik

@ -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'}/>
</>
}

Wyświetl plik

@ -1,13 +1,12 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import NodeSearch from '../../components/node/NodeSearch'
import Layout from '../../components/server/Layout' import Layout from '../../components/server/Layout'
import Stats from "../../components/stats/Stats"; import Stats from '../../components/stats/Stats'
import createConfig from '../../config/createConfig' import createConfig from '../../config/createConfig'
export default async function Page (): Promise<ReactElement> { export default async function Page (): Promise<ReactElement> {
const clientConfig = createConfig().get('client') const clientConfig = createConfig().get('client')
return ( return (
<Layout title={'Stats'} description={'Fediverse stats'} config={clientConfig}> <Layout title={'Stats'} config={clientConfig}>
<Stats /> <Stats />
</Layout> </Layout>
) )

Wyświetl plik

@ -1,11 +1,9 @@
import React from 'react' import React, { ReactElement } from 'react'
const Spinner: React.FC = () => { export default function Spinner (): ReactElement {
return ( return (
<div className="spinner-border" role="status"> <div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span> <span className="visually-hidden">Loading...</span>
</div> </div>
) )
} }
export default Spinner

Wyświetl plik

@ -6,10 +6,11 @@ import SearchInput from '../form/SearchInput'
import SubmitButton from '../form/SubmitButton' import SubmitButton from '../form/SubmitButton'
export default function FeedForm ( export default function FeedForm (
{ onSubmit, onQueryChange, query }: { { onSubmit, onQueryChange, query, loading }: {
onSubmit: () => void onSubmit: () => void
onQueryChange: (query: FeedQueryInput) => void onQueryChange: (query: FeedQueryInput) => void
query: FeedQueryInput query: FeedQueryInput
loading?: boolean
} }
): ReactElement { ): ReactElement {
const handleQueryChange = (event): void => { const handleQueryChange = (event): void => {
@ -41,6 +42,7 @@ export default function FeedForm (
<SubmitButton <SubmitButton
faIcon={faSearch} faIcon={faSearch}
label={'Search'} label={'Search'}
loading={loading}
id={'search-feeds-button'} id={'search-feeds-button'}
/> />
</div> </div>

Wyświetl plik

@ -7,7 +7,7 @@ import Badge from './badges/Badge'
export default function FeedPlaceholder (): ReactElement { export default function FeedPlaceholder (): ReactElement {
const greyDotBlob = 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==' const greyDotBlob = 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=='
return ( 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"> <div className="card-body">
<h3 className={'card-title with-emoji display-name placeholder-glow'}> <h3 className={'card-title with-emoji display-name placeholder-glow'}>
<a><span className="placeholder col-4"></span></a> <a><span className="placeholder col-4"></span></a>

Wyświetl plik

@ -16,7 +16,6 @@ export default function FeedResults ({ feeds }: { feeds: ListFeedsItemFragment[]
return (<div className={'grid'}> return (<div className={'grid'}>
{ {
feeds.map((feed, index) => { feeds.map((feed, index) => {
console.info('feed', feed)
return (<FeedResult key={index} feed={feed} />) return (<FeedResult key={index} feed={feed} />)
}) })
} }

Wyświetl plik

@ -1,6 +1,6 @@
'use client' 'use client'
import { useQuery } from '@apollo/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 React, { ReactElement, useEffect, useState } from 'react'
import { z } from 'zod' import { z } from 'zod'
import { FeedQueryInput, ListFeedsDocument } from '../../graphql/generated/types' import { FeedQueryInput, ListFeedsDocument } from '../../graphql/generated/types'
@ -27,7 +27,6 @@ export default function FeedSearch (): ReactElement {
const matomo = useMatomo() const matomo = useMatomo()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const pathname = usePathname() const pathname = usePathname()
const router = useRouter()
const routerQuery = feedQueryInputSchema.parse(Object.fromEntries(searchParams)) const routerQuery = feedQueryInputSchema.parse(Object.fromEntries(searchParams))
const [page, setPage] = useState<number>(0) const [page, setPage] = useState<number>(0)
const [query, setQuery] = useState<FeedQueryInput>(routerQuery) const [query, setQuery] = useState<FeedQueryInput>(routerQuery)
@ -39,7 +38,7 @@ export default function FeedSearch (): ReactElement {
} }
}) })
useEffect((): void => { useEffect((): void => {
router.push(`${pathname ?? ''}?${createUrlSearchParams(query).toString()}`) window.history.replaceState({}, '', `${pathname ?? ''}?${createUrlSearchParams(query).toString()}`)
matomo.trackEvent({ matomo.trackEvent({
category: 'feeds', category: 'feeds',
action: 'new-search' action: 'new-search'
@ -95,7 +94,7 @@ export default function FeedSearch (): ReactElement {
} }
return <> return <>
<FeedForm query={query} onQueryChange={handleQueryChange} onSubmit={handleSearchSubmit}/> <FeedForm query={query} onQueryChange={handleQueryChange} onSubmit={handleSearchSubmit} loading={loading || pageLoading}/>
<FeedInfo show={query.search === ''}> <FeedInfo show={query.search === ''}>
<Loader loading={loading || pageLoading} showBottom={true} placeholder={(<FeedPlaceholder/>)}> <Loader loading={loading || pageLoading} showBottom={true} placeholder={(<FeedPlaceholder/>)}>
<FeedResults feeds={data?.listFeeds?.items}/> <FeedResults feeds={data?.listFeeds?.items}/>

Wyświetl plik

@ -2,13 +2,25 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
export default function SubmitButton ({ faIcon, label, id }: { export default function SubmitButton ({ faIcon, label, id, loading, loadingLabel }: {
faIcon: IconProp faIcon: IconProp
label: string label: string
loadingLabel?: string
loading?: boolean
id?: string id?: string
}): ReactElement { }): ReactElement {
loadingLabel = loadingLabel ?? label
return <button type={'submit'} className={'btn btn-primary'} id={id}> return <button type={'submit'} className={'btn btn-primary'} id={id}>
<FontAwesomeIcon icon={faIcon}/> {
<span>{label}</span> 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> </button>
} }

Wyświetl plik

@ -1,18 +1,13 @@
'use client' 'use client'
import React, { ReactElement, ReactNode, useEffect } from 'react' import React, { ReactElement, ReactNode, useEffect } from 'react'
import Head from 'next/head'
import { useMatomo } from '../../hooks/MatomoHook' import { useMatomo } from '../../hooks/MatomoHook'
import Footer from './Footer'
import NavBar from './NavBar'
export default function ClientLayout ({ export default function ClientLayout ({
children, children,
title, title
description
}: { }: {
children?: ReactNode children?: ReactNode
title: string title: string
description: string
}): ReactElement { }): ReactElement {
const matomo = useMatomo() const matomo = useMatomo()
useEffect(() => { useEffect(() => {
@ -20,24 +15,8 @@ export default function ClientLayout ({
}, []) }, [])
return ( return (
<div className={'container'}> <div className={'container'}>
<Head> <h1>{title}</h1>
<title>{title}</title> {children}
<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> </div>
) )
} }

Wyświetl plik

@ -1,9 +1,10 @@
import Link from 'next/link'
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
export default function Footer (): ReactElement { export default function Footer (): ReactElement {
return ( return (
<footer className={'text-center mt-5'}> <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> <p>©{(new Date()).getFullYear()} <a href={'https://skorpil.cz'}>Štěpán Škorpil</a></p>
</footer> </footer>
) )

Wyświetl plik

@ -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"/>
</>
}

Wyświetl plik

@ -6,10 +6,11 @@ import SearchInput from '../form/SearchInput'
import SubmitButton from '../form/SubmitButton' import SubmitButton from '../form/SubmitButton'
export default function NodeForm ( export default function NodeForm (
{ onSubmit, onQueryChange, query }: { { onSubmit, onQueryChange, query, loading }: {
onSubmit: () => void onSubmit: () => void
onQueryChange: (query: NodeQueryInput) => void onQueryChange: (query: NodeQueryInput) => void
query: NodeQueryInput query: NodeQueryInput
loading?: boolean
} }
): ReactElement { ): ReactElement {
const handleQueryChange = (event): void => { const handleQueryChange = (event): void => {
@ -42,6 +43,7 @@ export default function NodeForm (
label={'Search'} label={'Search'}
faIcon={faSearch} faIcon={faSearch}
id={'search-nodes-button'} id={'search-nodes-button'}
loading={loading}
/> />
</div> </div>
</form> </form>

Wyświetl plik

@ -1,23 +1,30 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import SoftwareBadgePlaceholder from '../SoftwareBadgePlaceholder' import SoftwareBadgePlaceholder from '../SoftwareBadgePlaceholder'
export default function NodePlaceholder (): ReactElement { const Row = (): ReactElement => <tr>
<td className={'placeholder-glow'}><span className={'placeholder col-10'}/></td>
<td>
<div><SoftwareBadgePlaceholder /></div>
<div className={'placeholder-glow'}><span className={'placeholder col-5'}/></div>
</td>
<td className={'text-end placeholder-glow'}><span className={'placeholder col-3'}/></td>
<td className={'text-end placeholder-glow'}><span className={'placeholder col-3'}/></td>
<td className={'text-end placeholder-glow'}><span className={'placeholder col-3'}/></td>
<td className={'text-end placeholder-glow'}><span className={'placeholder col-3'}/></td>
<td className={'text-end placeholder-glow'}><span className={'placeholder col-3'}/></td>
<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 ( return (
<tbody> <tbody className={'placeholder-wrapper'} aria-hidden="true">
<tr> {[...Array(rowCount).keys()].map(key => {
<td className={'placeholder-glow'}><span className={'placeholder col-10'}/></td> return <Row key={key}/>
<td> })}
<div><SoftwareBadgePlaceholder /></div>
<div className={'placeholder-glow'}><span className={'placeholder col-5'}/></div>
</td>
<td className={'text-end placeholder-glow'}><span className={'placeholder col-3'}/></td>
<td className={'text-end placeholder-glow'}><span className={'placeholder col-3'}/></td>
<td className={'text-end placeholder-glow'}><span className={'placeholder col-3'}/></td>
<td className={'text-end placeholder-glow'}><span className={'placeholder col-3'}/></td>
<td className={'text-end placeholder-glow'}><span className={'placeholder col-3'}/></td>
<td className={' placeholder-glow'}><span className={'placeholder col-6'}/></td>
<td className={' placeholder-glow'}><span className={'placeholder col-6'}/></td>
</tr>
</tbody> </tbody>
) )
} }

Wyświetl plik

@ -1,6 +1,6 @@
'use client' 'use client'
import { useQuery } from '@apollo/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 React, { ReactElement, useEffect, useState } from 'react'
import { z } from 'zod' import { z } from 'zod'
import { import {
@ -38,7 +38,7 @@ export default function NodeSearch (): ReactElement {
const matomo = useMatomo() const matomo = useMatomo()
const pathname = usePathname() const pathname = usePathname()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const router = useRouter() const [lastRowCount, setLastRowCount] = useState<number>(1)
let routerQuery: NodeQueryInput let routerQuery: NodeQueryInput
try { try {
routerQuery = nodeQueryInputSchema.parse(Object.fromEntries(searchParams)) routerQuery = nodeQueryInputSchema.parse(Object.fromEntries(searchParams))
@ -49,10 +49,10 @@ export default function NodeSearch (): ReactElement {
sortWay: SortingWayEnum.Desc sortWay: SortingWayEnum.Desc
} }
} }
console.log('Router query', routerQuery)
const [query, setQuery] = useState<NodeQueryInput>(routerQuery) const [query, setQuery] = useState<NodeQueryInput>(routerQuery)
const [page, setPage] = useState<number>(0) 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, { const { loading, error, data, fetchMore, refetch } = useQuery(ListNodesDocument, {
variables: { variables: {
query, 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(() => { useEffect(() => {
matomo.trackEvent({ matomo.trackEvent({
category: 'nodes', category: 'nodes',
@ -75,7 +83,7 @@ export default function NodeSearch (): ReactElement {
}) })
}, [page]) }, [page])
useEffect((): void => { useEffect((): void => {
router.push(`${pathname ?? ''}?${createUrlSearchParams(query).toString()}`) window.history.replaceState({}, '', `${pathname ?? ''}?${createUrlSearchParams(query).toString()}`)
matomo.trackEvent({ matomo.trackEvent({
category: 'nodes', category: 'nodes',
action: 'new-search' action: 'new-search'
@ -89,17 +97,17 @@ export default function NodeSearch (): ReactElement {
} }
const handleSearchSubmit = async (): Promise<void> => { const handleSearchSubmit = async (): Promise<void> => {
setPageLoading(true) setPageLoading('submit')
setQuery(query) setQuery(query)
setPage(0) setPage(0)
await refetch({ paging: { page: 0 } }) await refetch({ paging: { page: 0 } })
setPageLoading(false) setPageLoading(undefined)
} }
const handleLoadMore = async (): Promise<void> => { const handleLoadMore = async (): Promise<void> => {
setPage(page + 1) setPage(page + 1)
console.info('Loading next page', { query, page }) console.info('Loading next page', { query, page })
setPageLoading(true) setPageLoading('more')
await fetchMore({ await fetchMore({
variables: { variables: {
paging: { page: page + 1 } paging: { page: page + 1 }
@ -121,7 +129,7 @@ export default function NodeSearch (): ReactElement {
return fetchMoreResult return fetchMoreResult
} }
}) })
setPageLoading(false) setPageLoading(undefined)
} }
const toggleSort = (sortBy: NodeSortingByEnum): void => { const toggleSort = (sortBy: NodeSortingByEnum): void => {
@ -145,15 +153,18 @@ export default function NodeSearch (): ReactElement {
return ( return (
<> <>
<NodeForm query={query} onQueryChange={handleQueryChange} onSubmit={handleSearchSubmit}/> <NodeForm query={query} onQueryChange={handleQueryChange} onSubmit={handleSearchSubmit} loading={loading || pageLoading !== undefined}/>
<ResponsiveTable> <ResponsiveTable>
<NodeHeader onSortToggle={toggleSort} query={query}/> <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}/> <NodeResults nodes={data?.listNodes?.items}/>
</Loader> </Loader>
</ResponsiveTable> </ResponsiveTable>
<LoadMoreButton onClick={handleLoadMore} <LoadMoreButton onClick={handleLoadMore}
show={!loading && !pageLoading && data?.listNodes?.paging?.hasNext === true}/> show={!loading && pageLoading === undefined && data?.listNodes?.paging?.hasNext === true}/>
<ErrorMessage message={error?.message}/> <ErrorMessage message={error?.message}/>
</> </>
) )

Wyświetl plik

@ -1,25 +1,21 @@
import React, { ReactElement, ReactNode } from 'react' import React, { ReactElement, ReactNode } from 'react'
import ClientConfig from '../../config/ClientConfig' import ClientConfig from '../../config/ClientConfig'
import 'server-only' import 'server-only'
import '../../styles/global.scss'
import ClientLayout from '../layout/ClientLayout' import ClientLayout from '../layout/ClientLayout'
import ClientProviders from '../layout/ClientProviders' import ClientProviders from '../layout/ClientProviders'
export default function Layout ({ export default function Layout ({
children, children,
config, config,
title, title
description
}: { }: {
children?: ReactNode children?: ReactNode
config: ClientConfig config: ClientConfig
title: string title: string
description: string
}): ReactElement { }): ReactElement {
console.log('Layout')
return ( return (
<ClientProviders config={config}> <ClientProviders config={config}>
<ClientLayout title={title} description={description}> <ClientLayout title={title}>
{children} {children}
</ClientLayout> </ClientLayout>
</ClientProviders> </ClientProviders>

Wyświetl plik

@ -1,6 +1,6 @@
'use client' 'use client'
import { useQuery } from '@apollo/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 React, { ReactElement, useEffect, useState } from 'react'
import { import {
ListStatsDocument, ListStatsDocument,
@ -30,7 +30,6 @@ export default function Stats (): ReactElement {
}) })
const pathname = usePathname() const pathname = usePathname()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const router = useRouter()
const matomo = useMatomo() const matomo = useMatomo()
let routerQuery: StatsQueryInput let routerQuery: StatsQueryInput
try { try {
@ -42,7 +41,6 @@ export default function Stats (): ReactElement {
sortWay: SortingWayEnum.Desc sortWay: SortingWayEnum.Desc
} }
} }
console.log('Router query', routerQuery)
const [query, setQuery] = useState<StatsQueryInput>(routerQuery) const [query, setQuery] = useState<StatsQueryInput>(routerQuery)
const { loading, error, data } = useQuery(ListStatsDocument, { const { loading, error, data } = useQuery(ListStatsDocument, {
variables: { variables: {
@ -59,7 +57,7 @@ export default function Stats (): ReactElement {
setLastSum(sum) setLastSum(sum)
}, [data]) }, [data])
useEffect(() => { useEffect(() => {
router.push(`${pathname ?? ''}?${createUrlSearchParams(query).toString()}`) window.history.replaceState({}, '', `${pathname ?? ''}?${createUrlSearchParams(query).toString()}`)
matomo.trackEvent({ matomo.trackEvent({
category: 'stats', category: 'stats',
action: 'new-search' action: 'new-search'

Wyświetl plik

@ -24,8 +24,11 @@ const Row = (): ReactElement => <tr>
</tr> </tr>
export default function StatsPlaceholder ({ rowCount }: { rowCount?: number }): ReactElement { export default function StatsPlaceholder ({ rowCount }: { rowCount?: number }): ReactElement {
return <tbody> if (rowCount === undefined || rowCount <= 0) {
{[...Array(rowCount ?? 1).keys()].map(key => { rowCount = 1
}
return <tbody className="placeholder-wrapper" aria-hidden="true">
{[...Array(rowCount).keys()].map(key => {
return <Row key={key}/> return <Row key={key}/>
})} })}
</tbody> </tbody>

Wyświetl plik

@ -137,4 +137,6 @@ table.stats {
} }
} }
.placeholder-wrapper{
cursor: wait;
}