diff --git a/Dockerfile b/Dockerfile index 7d07ff4..e0d98a5 100644 --- a/Dockerfile +++ b/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 diff --git a/application/src/app/feeds/head.tsx b/application/src/app/feeds/head.tsx new file mode 100644 index 0000000..1884fe4 --- /dev/null +++ b/application/src/app/feeds/head.tsx @@ -0,0 +1,8 @@ +import React, { ReactElement } from 'react' +import HtmlHead from '../../components/layout/HtmlHead' + +export default function Head (): ReactElement { + return <> + + > +} diff --git a/application/src/app/feeds/page.tsx b/application/src/app/feeds/page.tsx index b0c12bc..cc2d82a 100644 --- a/application/src/app/feeds/page.tsx +++ b/application/src/app/feeds/page.tsx @@ -6,7 +6,7 @@ import createConfig from '../../config/createConfig' export default async function Page (): Promise { const clientConfig = createConfig().get('client') return ( - + ) diff --git a/application/src/app/head.tsx b/application/src/app/head.tsx index db9659e..dcda320 100644 --- a/application/src/app/head.tsx +++ b/application/src/app/head.tsx @@ -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 ( - <> - - - - > - ) + return } diff --git a/application/src/app/layout.tsx b/application/src/app/layout.tsx index e33333e..a8ff970 100644 --- a/application/src/app/layout.tsx +++ b/application/src/app/layout.tsx @@ -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 ( - - - {children} - + + + + + + {children} + + + + + ) } diff --git a/application/src/app/loading.tsx b/application/src/app/loading.tsx new file mode 100644 index 0000000..42602cd --- /dev/null +++ b/application/src/app/loading.tsx @@ -0,0 +1,8 @@ +import React, { ReactElement } from 'react' + +export default function Loading (): ReactElement { + console.log('Loading') + return + + +} diff --git a/application/src/app/nodes/head.tsx b/application/src/app/nodes/head.tsx new file mode 100644 index 0000000..65050c0 --- /dev/null +++ b/application/src/app/nodes/head.tsx @@ -0,0 +1,8 @@ +import React, { ReactElement } from 'react' +import HtmlHead from '../../components/layout/HtmlHead' + +export default function Head (): ReactElement { + return <> + + > +} diff --git a/application/src/app/nodes/page.tsx b/application/src/app/nodes/page.tsx index 6d3002d..7b84166 100644 --- a/application/src/app/nodes/page.tsx +++ b/application/src/app/nodes/page.tsx @@ -6,7 +6,7 @@ import createConfig from '../../config/createConfig' export default async function Page (): Promise { const clientConfig = createConfig().get('client') return ( - + ) diff --git a/application/src/app/optout/head.tsx b/application/src/app/optout/head.tsx new file mode 100644 index 0000000..a806a59 --- /dev/null +++ b/application/src/app/optout/head.tsx @@ -0,0 +1,8 @@ +import React, { ReactElement } from 'react' +import HtmlHead from '../../components/layout/HtmlHead' + +export default function Head (): ReactElement { + return <> + + > +} diff --git a/application/src/app/optout/page.tsx b/application/src/app/optout/page.tsx index 39fa63a..c2d4565 100644 --- a/application/src/app/optout/page.tsx +++ b/application/src/app/optout/page.tsx @@ -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 { const clientConfig = createConfig().get('client') return ( - + You don't want to be listed here? There are several ways to opt-out from our index: diff --git a/application/src/app/stats/head.tsx b/application/src/app/stats/head.tsx new file mode 100644 index 0000000..7a48a5d --- /dev/null +++ b/application/src/app/stats/head.tsx @@ -0,0 +1,8 @@ +import React, { ReactElement } from 'react' +import HtmlHead from '../../components/layout/HtmlHead' + +export default function Head (): ReactElement { + return <> + + > +} diff --git a/application/src/app/stats/page.tsx b/application/src/app/stats/page.tsx index 11de63d..544683f 100644 --- a/application/src/app/stats/page.tsx +++ b/application/src/app/stats/page.tsx @@ -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 { const clientConfig = createConfig().get('client') return ( - + ) diff --git a/application/src/components/Spinner.tsx b/application/src/components/Spinner.tsx index 44a295f..2e34d69 100644 --- a/application/src/components/Spinner.tsx +++ b/application/src/components/Spinner.tsx @@ -1,11 +1,9 @@ -import React from 'react' +import React, { ReactElement } from 'react' -const Spinner: React.FC = () => { +export default function Spinner (): ReactElement { return ( Loading... ) } - -export default Spinner diff --git a/application/src/components/feed/FeedForm.tsx b/application/src/components/feed/FeedForm.tsx index 064ddea..ca9439c 100644 --- a/application/src/components/feed/FeedForm.tsx +++ b/application/src/components/feed/FeedForm.tsx @@ -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 ( diff --git a/application/src/components/feed/FeedPlaceholder.tsx b/application/src/components/feed/FeedPlaceholder.tsx index 629e80c..f38b777 100644 --- a/application/src/components/feed/FeedPlaceholder.tsx +++ b/application/src/components/feed/FeedPlaceholder.tsx @@ -7,7 +7,7 @@ import Badge from './badges/Badge' export default function FeedPlaceholder (): ReactElement { const greyDotBlob = 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==' return ( - + diff --git a/application/src/components/feed/FeedResults.tsx b/application/src/components/feed/FeedResults.tsx index 4f1b3e9..cc4f2a4 100644 --- a/application/src/components/feed/FeedResults.tsx +++ b/application/src/components/feed/FeedResults.tsx @@ -16,7 +16,6 @@ export default function FeedResults ({ feeds }: { feeds: ListFeedsItemFragment[] return ( { feeds.map((feed, index) => { - console.info('feed', feed) return () }) } diff --git a/application/src/components/feed/FeedSearch.tsx b/application/src/components/feed/FeedSearch.tsx index 97ab458..d6eda80 100644 --- a/application/src/components/feed/FeedSearch.tsx +++ b/application/src/components/feed/FeedSearch.tsx @@ -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(0) const [query, setQuery] = useState(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 <> - + )}> diff --git a/application/src/components/form/SubmitButton.tsx b/application/src/components/form/SubmitButton.tsx index c223247..e6039ec 100644 --- a/application/src/components/form/SubmitButton.tsx +++ b/application/src/components/form/SubmitButton.tsx @@ -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 - - {label} + { + loading === true + ? <> + + {loadingLabel} + > + : <> + + {label} + > + } } diff --git a/application/src/components/layout/ClientLayout.tsx b/application/src/components/layout/ClientLayout.tsx index 89f7016..9838ae2 100644 --- a/application/src/components/layout/ClientLayout.tsx +++ b/application/src/components/layout/ClientLayout.tsx @@ -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 ( - - {title} - - - - - - - - - - - - {title} - {children} - - - + {title} + {children} ) } diff --git a/application/src/components/layout/Footer.tsx b/application/src/components/layout/Footer.tsx index b294b7e..57d48e9 100644 --- a/application/src/components/layout/Footer.tsx +++ b/application/src/components/layout/Footer.tsx @@ -1,9 +1,10 @@ +import Link from 'next/link' import React, { ReactElement } from 'react' export default function Footer (): ReactElement { return ( ) diff --git a/application/src/components/layout/HtmlHead.tsx b/application/src/components/layout/HtmlHead.tsx new file mode 100644 index 0000000..5d85534 --- /dev/null +++ b/application/src/components/layout/HtmlHead.tsx @@ -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 <> + {htmlTitle} + + + + + + + + > +} diff --git a/application/src/components/node/NodeForm.tsx b/application/src/components/node/NodeForm.tsx index 64df506..52f8ddf 100644 --- a/application/src/components/node/NodeForm.tsx +++ b/application/src/components/node/NodeForm.tsx @@ -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} /> diff --git a/application/src/components/node/NodePlaceholder.tsx b/application/src/components/node/NodePlaceholder.tsx index adf033c..3684346 100644 --- a/application/src/components/node/NodePlaceholder.tsx +++ b/application/src/components/node/NodePlaceholder.tsx @@ -1,23 +1,30 @@ import React, { ReactElement } from 'react' import SoftwareBadgePlaceholder from '../SoftwareBadgePlaceholder' -export default function NodePlaceholder (): ReactElement { +const Row = (): ReactElement => + + + + + + + + + + + + + + +export default function NodePlaceholder ({ rowCount }: { rowCount?: number }): ReactElement { + if (rowCount === undefined || rowCount <= 0) { + rowCount = 1 + } return ( - - - - - - - - - - - - - - - + + {[...Array(rowCount).keys()].map(key => { + return + })} ) } diff --git a/application/src/components/node/NodeSearch.tsx b/application/src/components/node/NodeSearch.tsx index fcb0e9c..94c35c6 100644 --- a/application/src/components/node/NodeSearch.tsx +++ b/application/src/components/node/NodeSearch.tsx @@ -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(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(routerQuery) const [page, setPage] = useState(0) - const [pageLoading, setPageLoading] = useState(false) + const [pageLoading, setPageLoading] = useState(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 => { - setPageLoading(true) + setPageLoading('submit') setQuery(query) setPage(0) await refetch({ paging: { page: 0 } }) - setPageLoading(false) + setPageLoading(undefined) } const handleLoadMore = async (): Promise => { 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 ( <> - + - )}> + )}> + show={!loading && pageLoading === undefined && data?.listNodes?.paging?.hasNext === true}/> > ) diff --git a/application/src/components/server/Layout.tsx b/application/src/components/server/Layout.tsx index 875f15b..93a6b85 100644 --- a/application/src/components/server/Layout.tsx +++ b/application/src/components/server/Layout.tsx @@ -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 ( - + {children} diff --git a/application/src/components/stats/Stats.tsx b/application/src/components/stats/Stats.tsx index 5c2a9e5..0c1d4ad 100644 --- a/application/src/components/stats/Stats.tsx +++ b/application/src/components/stats/Stats.tsx @@ -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(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' diff --git a/application/src/components/stats/StatsPlaceholder.tsx b/application/src/components/stats/StatsPlaceholder.tsx index a199c99..5848d77 100644 --- a/application/src/components/stats/StatsPlaceholder.tsx +++ b/application/src/components/stats/StatsPlaceholder.tsx @@ -24,8 +24,11 @@ const Row = (): ReactElement => export default function StatsPlaceholder ({ rowCount }: { rowCount?: number }): ReactElement { - return - {[...Array(rowCount ?? 1).keys()].map(key => { + if (rowCount === undefined || rowCount <= 0) { + rowCount = 1 + } + return + {[...Array(rowCount).keys()].map(key => { return })} diff --git a/application/src/styles/global.scss b/application/src/styles/global.scss index 5fc7179..9a9acbf 100644 --- a/application/src/styles/global.scss +++ b/application/src/styles/global.scss @@ -137,4 +137,6 @@ table.stats { } } - +.placeholder-wrapper{ + cursor: wait; +}
You don't want to be listed here? There are several ways to opt-out from our index: