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

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> {
const clientConfig = createConfig().get('client')
return (
<Layout title={'People'} description={'Search people on Fediverse'} config={clientConfig}>
<Layout title={'People'} config={clientConfig}>
<FeedSearch />
</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 {
return (
<>
<title></title>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<link rel="icon" href="/favicon.ico" />
</>
)
return <HtmlHead />
}

Wyświetl plik

@ -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
@ -6,9 +9,16 @@ export default function RootLayout ({
children: React.ReactNode
}): ReactElement {
return (
<html>
<head />
<body>{children}</body>
</html>
<html>
<body>
<div className="container">
<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> {
const clientConfig = createConfig().get('client')
return (
<Layout title={'Servers'} description={'Search Fediverse servers'} config={clientConfig}>
<Layout title={'Servers'} config={clientConfig}>
<NodeSearch />
</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 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&apos;t want to be listed here? There are several ways to opt-out from our index:</p>
<Accordion>
<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 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>
)

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 (
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
)
}
export default Spinner

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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>
<h1>{title}</h1>
{children}
</div>
)
}

Wyświetl plik

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

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

Wyświetl plik

@ -1,23 +1,30 @@
import React, { ReactElement } from 'react'
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 (
<tbody>
<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>
<tbody className={'placeholder-wrapper'} aria-hidden="true">
{[...Array(rowCount).keys()].map(key => {
return <Row key={key}/>
})}
</tbody>
)
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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