From 1833e784ea5dad0db9f092892c0770eeb983abe6 Mon Sep 17 00:00:00 2001 From: Kevin Stadler Date: Fri, 13 Sep 2024 13:08:14 +0200 Subject: [PATCH] improvements: switch to typesense backend - move type-based model.ts to /lib - add 'later' field to database - rename pages and switch them all to the typesense backend --- README.md | 5 + .../[language]}/page.tsx | 0 app/publication/[id]/page.tsx | 28 +++-- .../instantsearch.tsx | 2 +- app/{instantsearch => search}/page.tsx | 0 app/{translator => translators}/page.tsx | 0 app/works/[...args]/page.tsx | 58 +++------- components/app-header.tsx | 6 +- lib/data.ts | 23 ++-- {types => lib}/model.ts | 25 ++--- readme.md | 102 ------------------ scripts/tsv-to-json.py | 7 +- 12 files changed, 67 insertions(+), 189 deletions(-) create mode 100644 README.md rename app/{language => languages/[language]}/page.tsx (100%) rename app/{instantsearch => search}/instantsearch.tsx (99%) rename app/{instantsearch => search}/page.tsx (100%) rename app/{translator => translators}/page.tsx (100%) rename {types => lib}/model.ts (78%) delete mode 100644 readme.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ffcdbb --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# thomas-bernhard-global + +bugs / open questions + +- [ ] how should languages be encoded in urls (queries with special characters like `portuguese (brazil)` are currently also broken/unreliable) diff --git a/app/language/page.tsx b/app/languages/[language]/page.tsx similarity index 100% rename from app/language/page.tsx rename to app/languages/[language]/page.tsx diff --git a/app/publication/[id]/page.tsx b/app/publication/[id]/page.tsx index 1ece7e5..69f8eb7 100644 --- a/app/publication/[id]/page.tsx +++ b/app/publication/[id]/page.tsx @@ -1,13 +1,13 @@ import { notFound } from "next/navigation"; -import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; -import { getLaterEditions, getPublication, getSameLanguagePublications } from "@/app/data"; import { AppLink } from "@/components/app-link"; import { BernhardWorkLink } from "@/components/bernhard-links"; import { InlineList } from "@/components/inline-list"; import { MainContent } from "@/components/main-content"; import { ClickablePublicationThumbnail, PublicationCover } from "@/components/publication-cover"; -import type { Publication, Translator } from "@/types/model"; +import { getPublication, getSameLanguagePublications } from "@/lib/data"; +import type { Publication, Translator } from "@/lib/model"; interface PublicationPageProps { params: { @@ -32,13 +32,22 @@ function translatorIndices(pub: Publication): Array<[Translator, Array]> }, []); } -export default function PublicationPage(props: PublicationPageProps) { - const t = useTranslations("PublicationPage"); - const pub = getPublication(props.params.id); +export default async function PublicationPage(props: PublicationPageProps) { + const t = await getTranslations("PublicationPage"); + const pub = await getPublication(props.params.id); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!pub) { return notFound(); } - const later = getLaterEditions(pub); + + const later = + // TODO database should just return null instead of empty arrays wherever possible + pub.later?.length === 0 + ? undefined + : pub.later?.map((id) => { + return getPublication(id); + }); + const translatorInfo = translatorIndices(pub); // don't show translator/translation indices when all translations are authored by all translators const showIndices = translatorInfo.some(([_t, is]) => { @@ -104,7 +113,8 @@ export default function PublicationPage(props: PublicationPageProps) { <>

{t("later_editions")}

- {later.map((p) => { + {later.map(async (pp) => { + const p = await pp; return (
@@ -120,7 +130,7 @@ export default function PublicationPage(props: PublicationPageProps) { {t("more_in")} {pub.language}
- {getSameLanguagePublications(pub).map((p) => { + {(await getSameLanguagePublications(pub)).map((p) => { return ; })}
diff --git a/app/instantsearch/instantsearch.tsx b/app/search/instantsearch.tsx similarity index 99% rename from app/instantsearch/instantsearch.tsx rename to app/search/instantsearch.tsx index f7e2b73..5e8c36c 100644 --- a/app/instantsearch/instantsearch.tsx +++ b/app/search/instantsearch.tsx @@ -9,7 +9,7 @@ import TypesenseInstantSearchAdapter, { type SearchClient } from "typesense-inst import { ClickablePublicationThumbnail } from "@/components/publication-cover"; import { collectionName } from "@/lib/data"; -import type { Publication } from "@/types/model"; +import type { Publication } from "@/lib/model"; const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({ server: { diff --git a/app/instantsearch/page.tsx b/app/search/page.tsx similarity index 100% rename from app/instantsearch/page.tsx rename to app/search/page.tsx diff --git a/app/translator/page.tsx b/app/translators/page.tsx similarity index 100% rename from app/translator/page.tsx rename to app/translators/page.tsx diff --git a/app/works/[...args]/page.tsx b/app/works/[...args]/page.tsx index 3c2430c..1cacde7 100644 --- a/app/works/[...args]/page.tsx +++ b/app/works/[...args]/page.tsx @@ -1,12 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +"use client"; import { useTranslations } from "next-intl"; -// import { useEffect, useState } from "react"; -import { getPublications, getWorks } from "@/app/data"; import { AppNavLink } from "@/components/app-nav-link"; import { MainContent } from "@/components/main-content"; import { ClickablePublicationThumbnail } from "@/components/publication-cover"; import { PublicationGrid } from "@/components/publication-grid"; -import { Category } from "@/types/model"; +import { getPublications } from "@/lib/data"; +import { type Category, otherCategories } from "@/lib/model"; interface WorkPageProps { params: { @@ -14,66 +16,30 @@ interface WorkPageProps { }; } -export default function WorkPage(props: WorkPageProps) { +// eslint-disable-next-line @next/next/no-async-client-component +export default async function WorkPage(props: WorkPageProps) { const catt = useTranslations("BernhardCategories"); const _t = useTranslations("WorkPage"); const category = props.params.args[0]; - // const [category, _setCategory] = useState(props.params.args[0]); - // const [workId, setWorkId] = useState(props.params.args[1]); - // query result based on category - // const [works, _setWorks] = useState>(getWorks(category)); - const works = getWorks(category); - const publications = getPublications({ erstpublikation: true }, category, "", 0, 0); - // // query result based on id (and language?) - // const [publications, setPublications] = useState>([]); - - // useEffect(() => { - // setPublications(getPublications({ erstpublikation: true })); - // }, [workId]); - - //if (false) { - // //publicationTypes.includes(category)) { - // // setWorks(getWorks(category)); - // if (workId) { - // const work = getWork(workId); - // if (!work) { - // return notFound(); - // } - // } - // } + const publications = await getPublications({ q: "*", filter: { erstpublikation: true } }); return (
- {Object.keys(Category).map((c) => { + {otherCategories.map((c) => { return ( - { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - catt(c as any) - } + {catt(c)} ); })}
- { - // category ? catt(category as any) : null - } -
    - {works.map((w) => { - return ( -
  • - - {w.title} {w.year ? `(${String(w.year)})` : null} - -
  • - ); - })} -
+ {category} +
    {publications.map((p) => { diff --git a/components/app-header.tsx b/components/app-header.tsx index 3741cf0..490bb31 100644 --- a/components/app-header.tsx +++ b/components/app-header.tsx @@ -16,15 +16,15 @@ export function AppHeader(): ReactNode { label: t("links.works"), }, languages: { - href: createHref({ pathname: "/language" }), + href: createHref({ pathname: "/languages" }), label: t("links.languages"), }, translators: { - href: createHref({ pathname: "/translator" }), + href: createHref({ pathname: "/translators" }), label: t("links.translators"), }, search: { - href: createHref({ pathname: "/instantsearch" }), + href: createHref({ pathname: "/search" }), label: t("links.search"), }, } satisfies Record; diff --git a/lib/data.ts b/lib/data.ts index 5638d93..62a2cc7 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -1,8 +1,7 @@ import { Client } from "typesense"; import type { SearchResponse } from "typesense/lib/Typesense/Documents"; -import page from "@/app/(index)/page"; -import type { Publication } from "@/types/model"; +import type { Publication } from "@/lib/model"; const perPage = 16; @@ -59,6 +58,10 @@ export async function getFaceted( }) as unknown as Promise>; } +export async function getPublication(id: string) { + return collection.documents(id).retrieve() as Promise; +} + export async function getPublications( args: SearchArgs = searchDefaults, ): Promise> { @@ -72,8 +75,7 @@ export async function getPublications( per_page: args.per_page, }) .then((r) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return r.hits!.map((h) => { + return r.hits?.map((h) => { return h.document; }) as unknown as Array; }); @@ -82,8 +84,13 @@ export async function getPublications( // get 4 publications, ideally in the same language but excluding the publication itself *and* its // children (because they will already be listed separately anyway) export async function getSameLanguagePublications(pub: Publication) { - return collection.documents().search({ - q: "*", - filter_by: `language: ${pub.language} && id :!= [ ${[pub.id, ...(pub.later ?? [])].join()} ]`, - }) as unknown as Promise>; + return ( + await collection.documents().search({ + q: "*", + per_page: 4, + filter_by: `language: ${pub.language} && id :!= [ ${[pub.id, ...(pub.later ?? [])].join()} ]`, + }) + ).hits?.map((r) => { + return r.document; + }) as unknown as Array; } diff --git a/types/model.ts b/lib/model.ts similarity index 78% rename from types/model.ts rename to lib/model.ts index 0e368c7..60a843b 100755 --- a/types/model.ts +++ b/lib/model.ts @@ -1,21 +1,14 @@ // these end up in public URLs as slugs, so come up with good names! -export enum Category { - novels = "novels", - novellas = "novellas & short prose", - autobiography = "autobiography", - fragments = "fragments", +export const otherCategories = [ + "prose", + "drama", + "poetry", + "letterspeechinterview", + "adaptations", +] as const; +export const proseCategories = ["novels", "novellas", "autobiography", "fragments"] as const; - drama = "drama & libretti", - poetry = "poetry", - letterspeechinterview = "letters, speeches, interviews", - adaptations = "adaptations", -} - -export type Prose = - | Category.autobiography - | Category.fragments - | Category.novellas - | Category.novels; +export type Category = (typeof otherCategories)[number] | (typeof proseCategories)[number]; /** Publication contains one or more translated works. */ export interface Publication { diff --git a/readme.md b/readme.md deleted file mode 100644 index 862bfd2..0000000 --- a/readme.md +++ /dev/null @@ -1,102 +0,0 @@ -# app template - -template repository for next.js apps. - -## how to run - -prerequisites: - -- [node.js v20](https://nodejs.org/en/download) -- [pnpm v9](https://pnpm.io/installation) - -> [!TIP] -> -> you can use `pnpm` to install the required node.js version with `pnpm env use 20 --global` - -set required environment variables in `.env.local`: - -```bash -cp .env.local.example .env.local -``` - -also, set environment variables required by [validation](./.github/workflows/validate.yml) and -[deployment](./.github/workflows/build-deploy.yml) github actions. use -["variables"](https://github.com/acdh-oeaw/template-app-next/settings/variables/actions) for every -environment variable prefixed with `NEXT_PUBLIC_`, and -["secrets"](https://github.com/acdh-oeaw/template-app-next/settings/secrets/actions) for all others. - -the default template accepts the following variables: - -- `NEXT_PUBLIC_REDMINE_ID` (required): service issue for this application in the acdh-ch - [redmine](https://redmine.acdh.oeaw.ac.at) issue tracker. -- `NEXT_PUBLIC_APP_BASE_URL` (required): the base url for this application. the default of - "http://localhost:3000" should be fine for local development. -- `NEXT_PUBLIC_BOTS` (required): whether this website can be indexed by web crawlers like the google - bot. supported values are "disabled" and "enabled", defaults to "disabled". -- `NEXT_PUBLIC_MATOMO_BASE_URL` and `NEXT_PUBLIC_MATOMO_ID` (optional): set these to support - client-side analytics with matomo. -- `NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION` (optional): set this to verify site ownership for google - search console. - -when adding new environment variables, don't forget to add them to `.env.local.example` as well. - -install dependencies: - -```bash -pnpm install -``` - -run a development server on [http://localhost:3000](http://localhost:3000): - -```bash -pnpm run dev -``` - -> [!TIP] -> -> this template supports developing in containers. when opening the project in your editor, you -> should be prompted to re-open it in a devcontainer. - -## how to deploy - -- ask a sysadmin to create a new acdh-ch kubernetes project. -- create a new namespace in that project via [rancher](https://rancher.acdh-dev.oeaw.ac.at), and set - the `KUBE_NAMESPACE` github variable to that namespace -- adjust the [`app_name`](./.github/workflows/build-deploy.yml#L36), which will be the name of the - deployment in the above namespace. -- set the `PUBLIC_URL` github variable to the application's public url (e.g. - "https://my-app.acdh-ch-dev.oeaw.ac.at"), and set the `KUBE_INGRESS_BASE_DOMAIN` to the public - url's base domain (e.g. "acdh-ch-dev.oeaw.ac.at"). `PUBLIC_URL` should match - `NEXT_PUBLIC_APP_BASE_URL`. -- if you haven't yet, create a service issue in the acdh-ch - [redmine](https://redmine.acdh.oeaw.ac.at) issue tracker, and set the `SERVICE_ID` github variable - to the issue number. this should match the `NEXT_PUBLIC_REDMINE_ID` variable in your `.env.local` - file. -- ensure required build args (prefixed with `NEXT_PUBLIC_`) are referenced in both the - [`Dockerfile`](./Dockerfile), as well as the [validation](./.github/workflows/validate.yml) and - [deployment](./.github/workflows/build-deploy.yml) pipelines, and set as - [github variables](https://github.com/acdh-oeaw/template-app-next/settings/variables/actions). -- ensure required runtime environment variables are referenced in the - [validation](./.github/workflows/validate.yml) and - [deployment](./.github/workflows/build-deploy.yml) pipelines, and set as - [github secrets](https://github.com/acdh-oeaw/template-app-next/settings/secrets/actions). github - secrets need to be prefixed with `K8S_SECRET_` to be automatically copied to the runtime - environment. in case you need secrets in the docker build context, you can - [mount a secret in the Dockerfile](https://docs.docker.com/build/building/secrets/). -- ensure both the github repository, as well as the - [package registry](https://github.com/orgs/acdh-oeaw/packages/container/my-app/settings) is set to - public. - -if everything is set up correctly, every git push to the `main` branch will create a new deployment -if the validation pipeline passes. - -you can reference the [template repository](https://github.com/acdh-oeaw/template-app-next) for a -working setup. - -## template variants - -- `variant/with-commitlint` branch: enables `commitlint` and runs it as a git hook, and as part of - the validation workflow in a github action. -- `variant/with-single-locale` branch: adjusts the template to support a single pre-configured - locale only, and removes internationalised routing. ui strings are still managed in the - [`messages`](./messages) folder to make it easy to activate full i18n support later if needed. diff --git a/scripts/tsv-to-json.py b/scripts/tsv-to-json.py index b5d3120..2bd23c3 100755 --- a/scripts/tsv-to-json.py +++ b/scripts/tsv-to-json.py @@ -201,7 +201,7 @@ def workkey(pub, i): logger.error(f"{pub['Signatur']} does not have a numeric year ('{pub['year']}')") year = None - assets = [ { 'id': pub['Signatur']} ] if os.path.isfile(f'../public/covers/{pub["Signatur"]}.jpg') else None + assets = [ { 'id': pub['Signatur']} ] if os.path.isfile(f'../public/covers/{pub["Signatur"]}.jpg') else [] if len(pub['more']): assets += [ { 'id': name } for name in pub['more'].split(', ') ] @@ -228,10 +228,9 @@ def workkey(pub, i): if pub['parents']: for par in pub['parents']: try: - publications[par]['children'].append(pub['signatur']) + publications[par]['later'].append(pub['id']) except KeyError: - continue - + logger.warning(f"{pub['id']} was previously published in {par} but couldn't find a record for {par}") print(f"extracted {len(publications)} of {len(data)} publications")