From 2309f8ce2e3e9bab1666cd92a8211ecddcb2de76 Mon Sep 17 00:00:00 2001 From: Mehmet Date: Sat, 14 Dec 2024 16:07:45 +0100 Subject: [PATCH 1/2] feat: added human friendly ui to terms pages --- .changeset/eighty-comics-love.md | 5 + .../src/app/termen/[...slug]/middleware.ts | 26 +++++ apps/web/src/app/termen/[...slug]/page.tsx | 97 +++++++++++++++++++ apps/web/src/app/termen/[...slug]/route.ts | 60 ------------ .../download/[extension]/[...slug]/route.ts | 30 ++++++ .../web/src/app/termen/find-term-in-format.ts | 23 +++++ .../termen/get-headers-with-content-type.ts | 13 +++ .../src/app/termen/get-slug-from-params.ts | 7 ++ .../web/src/app/termen/get-valid-extension.ts | 11 +++ .../src/app/termen/handler/[...slug]/route.ts | 40 ++++++++ apps/web/src/middleware.ts | 7 ++ 11 files changed, 259 insertions(+), 60 deletions(-) create mode 100644 .changeset/eighty-comics-love.md create mode 100644 apps/web/src/app/termen/[...slug]/middleware.ts create mode 100644 apps/web/src/app/termen/[...slug]/page.tsx delete mode 100644 apps/web/src/app/termen/[...slug]/route.ts create mode 100644 apps/web/src/app/termen/download/[extension]/[...slug]/route.ts create mode 100644 apps/web/src/app/termen/find-term-in-format.ts create mode 100644 apps/web/src/app/termen/get-headers-with-content-type.ts create mode 100644 apps/web/src/app/termen/get-slug-from-params.ts create mode 100644 apps/web/src/app/termen/get-valid-extension.ts create mode 100644 apps/web/src/app/termen/handler/[...slug]/route.ts diff --git a/.changeset/eighty-comics-love.md b/.changeset/eighty-comics-love.md new file mode 100644 index 00000000..668ed812 --- /dev/null +++ b/.changeset/eighty-comics-love.md @@ -0,0 +1,5 @@ +--- +"web": minor +--- + +Added human friendly Term page ui diff --git a/apps/web/src/app/termen/[...slug]/middleware.ts b/apps/web/src/app/termen/[...slug]/middleware.ts new file mode 100644 index 00000000..5593a1a1 --- /dev/null +++ b/apps/web/src/app/termen/[...slug]/middleware.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from 'next/server'; + +interface Middleware { + predicate(request: NextRequest): boolean; + handler(request: NextRequest): any; +} + +export const termenMiddleware: Middleware = { + predicate: (request) => request.nextUrl.pathname.startsWith('/termen'), + handler: (request) => { + const handlerHeaders = ['application/rdf+xml', 'text/turtle', 'application/json']; + const acceptHeader = request.headers.get('accept'); + + const shouldRewrite = + acceptHeader === null ? false : handlerHeaders.some((handlerHeader) => acceptHeader?.includes(handlerHeader)); + + if (shouldRewrite) { + const slug = request.nextUrl.pathname.split('/').toSpliced(0, 2).join('/'); + const url = new URL(`/termen/handler/${slug}`, process.env.NEXT_PUBLIC_WEB_URL); + + return NextResponse.rewrite(url); + } + + return NextResponse.next(); + }, +}; diff --git a/apps/web/src/app/termen/[...slug]/page.tsx b/apps/web/src/app/termen/[...slug]/page.tsx new file mode 100644 index 00000000..f8510833 --- /dev/null +++ b/apps/web/src/app/termen/[...slug]/page.tsx @@ -0,0 +1,97 @@ +import { resolveCmsImage } from '@/common/resolve-cms-image'; +import { Button } from '@/components/button'; +import { Chip } from '@/components/chip'; +import { Container } from '@/components/container'; +import { Pill } from '@/components/pill'; +import { Typography } from '@/components/typography'; +import { db } from '@/drizzle/db'; +import { files, filesRelatedMorphs, terms } from '@/drizzle/schema'; +import { and, eq } from 'drizzle-orm'; +import { notFound } from 'next/navigation'; +import path from 'path'; +import { findTermInFormat } from '../find-term-in-format'; +import { getSlugFromParams } from '../get-slug-from-params'; + +interface FindFieldArgs { + input: Record; + key: string; + value: string; +} + +function findNodeWithField({ input, key, value }: FindFieldArgs): Record | null { + let result: Record | null = null; + + function search(obj: Record) { + if (typeof obj !== 'object' || obj === null) return; + + for (const inputKey in obj) { + if (inputKey === key && obj[inputKey] === value) { + result = obj; + return; + } + + if (typeof obj[inputKey] === 'object') { + search(obj[inputKey]); + if (result) return; + } + } + } + + search(input); + + return result; +} + +export default async function TermenPage({ params }: { params: { slug: string[] } }) { + const slug = getSlugFromParams(params.slug); + const term = await findTermInFormat({ slug, extension: '.json' }); + + const json = await await fetch(resolveCmsImage(term.files as any), { + method: 'GET', + }).then((res) => res.json() as Record); + + const rootNode = findNodeWithField({ + input: json, + key: '@id', + value: `https://regels.overheid.nl/termen${slug}`, + }); + + const nlPrefLabelNode = findNodeWithField({ + input: rootNode?.['http://www.w3.org/2004/02/skos/core#prefLabel'] || {}, + key: '@language', + value: 'nl', + }); + + const nlDefinitionNode = findNodeWithField({ + input: rootNode?.['http://www.w3.org/2004/02/skos/core#definition'] || {}, + key: '@language', + value: 'nl', + }); + + const nlPrefLabel = nlPrefLabelNode?.['@value']; + + if (!nlPrefLabel) return notFound(); + + return ( + +
+ {nlPrefLabel} + +
+ Definitie + {nlDefinitionNode?.['@value']} + Download +
+ + + +
+
+ ); +} diff --git a/apps/web/src/app/termen/[...slug]/route.ts b/apps/web/src/app/termen/[...slug]/route.ts deleted file mode 100644 index 43ec2887..00000000 --- a/apps/web/src/app/termen/[...slug]/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { resolveCmsImage } from '@/common/resolve-cms-image'; -import { db } from '@/drizzle/db'; -import { files, filesRelatedMorphs, terms } from '@/drizzle/schema'; -import { and, eq } from 'drizzle-orm'; -import { NextRequest } from 'next/server'; -import path from 'path'; - -export async function GET(req: NextRequest, { params }: { params: { slug: string[] } }) { - const slug = '/' + params.slug.join('/'); - - const extension = (() => { - if (!path.extname(slug)) return '.json'; - - const extensions = ['json', 'rdf', 'ttl']; - - const extension = extensions.find((value) => slug.endsWith('.' + value)); - - if (extension) return '.' + extension; - - const accepts = req.headers.get('accept'); - - if (accepts?.includes('application/rdf+xml')) return '.rdf'; - - if (accepts?.includes('text/turtle')) return '.ttl'; - - if (accepts?.includes('application/json')) return '.json'; - })(); - - if (!extension) return new Response('Not found', { status: 404 }); - - const pureSlug = path.join(path.dirname(slug), path.basename(slug, path.extname(slug))); - - const [term] = await db - .select() - .from(terms) - .leftJoin( - filesRelatedMorphs, - and(eq(terms.id, filesRelatedMorphs.relatedId), eq(filesRelatedMorphs.relatedType, 'api::term.term')) - ) - .leftJoin(files, and(eq(files.id, filesRelatedMorphs.fileId))) - .where(and(eq(terms.slug, pureSlug), eq(files.ext, extension))) - .limit(1); - - const fetchResponse = await fetch(resolveCmsImage(term.files as any), { - method: 'GET', - }); - - const headers = new Headers(); - - if (extension === '.ttl') headers.set('content-type', 'text/turtle'); - - if (extension === '.json') headers.set('content-type', 'application/json'); - - if (extension === '.rdf') headers.set('content-type', 'application/rdf+xml'); - - return new Response(fetchResponse.body, { - headers, - status: fetchResponse.status, - }); -} diff --git a/apps/web/src/app/termen/download/[extension]/[...slug]/route.ts b/apps/web/src/app/termen/download/[extension]/[...slug]/route.ts new file mode 100644 index 00000000..a6f8b9c7 --- /dev/null +++ b/apps/web/src/app/termen/download/[extension]/[...slug]/route.ts @@ -0,0 +1,30 @@ +import { findTermInFormat, FindTermInFormatArgs } from '@/app/termen/find-term-in-format'; +import { getHeadersWithContentTypes } from '@/app/termen/get-headers-with-content-type'; +import { getSlugFromParams } from '@/app/termen/get-slug-from-params'; +import { getValidExtension } from '@/app/termen/get-valid-extension'; +import { notFoundResponse } from '@/common/not-found-response'; +import { resolveCmsImage } from '@/common/resolve-cms-image'; +import slugify from '@sindresorhus/slugify'; +import { NextRequest } from 'next/server'; + +export async function GET(req: NextRequest, { params }: { params: { slug: string[]; extension: string } }) { + const extension = getValidExtension(params.extension); + + if (extension === null) return notFoundResponse(req); + + const slug = getSlugFromParams(params.slug); + const term = await findTermInFormat({ slug, extension }); + + const headers = getHeadersWithContentTypes(extension); + + headers.set('Content-Disposition', `attachment; filename="${slugify(slug)}${extension}"`); + + const fetchResponse = await fetch(resolveCmsImage(term.files as any), { + method: 'GET', + }); + + return new Response(fetchResponse.body, { + headers, + status: fetchResponse.status, + }); +} diff --git a/apps/web/src/app/termen/find-term-in-format.ts b/apps/web/src/app/termen/find-term-in-format.ts new file mode 100644 index 00000000..3e679816 --- /dev/null +++ b/apps/web/src/app/termen/find-term-in-format.ts @@ -0,0 +1,23 @@ +import { db } from '@/drizzle/db'; +import { files, filesRelatedMorphs, terms } from '@/drizzle/schema'; +import { and, eq } from 'drizzle-orm'; + +export interface FindTermInFormatArgs { + slug: string; + extension: '.json' | '.rdf' | '.ttl'; +} + +export async function findTermInFormat({ extension, slug }: FindTermInFormatArgs) { + const [term] = await db + .select() + .from(terms) + .leftJoin( + filesRelatedMorphs, + and(eq(terms.id, filesRelatedMorphs.relatedId), eq(filesRelatedMorphs.relatedType, 'api::term.term')) + ) + .leftJoin(files, and(eq(files.id, filesRelatedMorphs.fileId))) + .where(and(eq(terms.slug, slug), eq(files.ext, extension))) + .limit(1); + + return term; +} diff --git a/apps/web/src/app/termen/get-headers-with-content-type.ts b/apps/web/src/app/termen/get-headers-with-content-type.ts new file mode 100644 index 00000000..476556bc --- /dev/null +++ b/apps/web/src/app/termen/get-headers-with-content-type.ts @@ -0,0 +1,13 @@ +import { FindTermInFormatArgs } from './find-term-in-format'; + +export function getHeadersWithContentTypes(extension: FindTermInFormatArgs['extension']) { + const headers = new Headers(); + + if (extension === '.ttl') headers.set('content-type', 'text/turtle'); + + if (extension === '.json') headers.set('content-type', 'application/json'); + + if (extension === '.rdf') headers.set('content-type', 'application/rdf+xml'); + + return headers; +} diff --git a/apps/web/src/app/termen/get-slug-from-params.ts b/apps/web/src/app/termen/get-slug-from-params.ts new file mode 100644 index 00000000..526c331e --- /dev/null +++ b/apps/web/src/app/termen/get-slug-from-params.ts @@ -0,0 +1,7 @@ +import path from 'path'; + +export function getSlugFromParams(param: string[]) { + const slug = '/' + param.join('/'); + + return path.join(path.dirname(slug), path.basename(slug, path.extname(slug))); +} diff --git a/apps/web/src/app/termen/get-valid-extension.ts b/apps/web/src/app/termen/get-valid-extension.ts new file mode 100644 index 00000000..6409e07a --- /dev/null +++ b/apps/web/src/app/termen/get-valid-extension.ts @@ -0,0 +1,11 @@ +import { FindTermInFormatArgs } from './find-term-in-format'; + +export function getValidExtension(extension: string): FindTermInFormatArgs['extension'] | null { + if (extension === 'ttl') return '.ttl'; + + if (extension === 'rdf') return '.rdf'; + + if (extension === 'json') return '.json'; + + return null; +} diff --git a/apps/web/src/app/termen/handler/[...slug]/route.ts b/apps/web/src/app/termen/handler/[...slug]/route.ts new file mode 100644 index 00000000..0e828368 --- /dev/null +++ b/apps/web/src/app/termen/handler/[...slug]/route.ts @@ -0,0 +1,40 @@ +import { resolveCmsImage } from '@/common/resolve-cms-image'; +import { db } from '@/drizzle/db'; +import { files, filesRelatedMorphs, terms } from '@/drizzle/schema'; +import { and, eq, param } from 'drizzle-orm'; +import { NextRequest } from 'next/server'; +import path from 'path'; +import { getSlugFromParams } from '../../get-slug-from-params'; +import { findTermInFormat, FindTermInFormatArgs } from '../../find-term-in-format'; +import { getHeadersWithContentTypes } from '../../get-headers-with-content-type'; + +export async function GET(req: NextRequest, { params }: { params: { slug: string[] } }) { + const slug = getSlugFromParams(params.slug); + + const extension = ((): FindTermInFormatArgs['extension'] | null => { + const accepts = req.headers.get('accept'); + + if (accepts?.includes('application/rdf+xml')) return '.rdf'; + + if (accepts?.includes('text/turtle')) return '.ttl'; + + if (accepts?.includes('application/json')) return '.json'; + + return null; + })(); + + if (!extension) return new Response('Not found', { status: 404 }); + + const term = await findTermInFormat({ slug, extension }); + + const fetchResponse = await fetch(resolveCmsImage(term.files as any), { + method: 'GET', + }); + + const headers = getHeadersWithContentTypes(extension); + + return new Response(fetchResponse.body, { + headers, + status: fetchResponse.status, + }); +} diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 5f73d22c..2bb87e35 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { termenMiddleware } from './app/termen/[...slug]/middleware'; export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], @@ -11,4 +12,10 @@ export default function middleware(req: NextRequest) { if (host === 'waardelijsten.localhost' || host === 'waardelijsten.regels.overheid.nl') { return NextResponse.rewrite(new URL(`/waardelijsten${url.pathname}`, url)); } + + const middlewares = [termenMiddleware]; + + const middlewareToRun = middlewares.find((middleware) => middleware.predicate(req)); + + if (middlewareToRun) return middlewareToRun.handler(req); } From 8baa311ddef81dfa9c65f68f14c1854c4bcb09f5 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Sun, 15 Dec 2024 16:10:46 +0100 Subject: [PATCH 2/2] added label and scopeNote --- apps/web/src/app/termen/[...slug]/page.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/web/src/app/termen/[...slug]/page.tsx b/apps/web/src/app/termen/[...slug]/page.tsx index f8510833..f23572ca 100644 --- a/apps/web/src/app/termen/[...slug]/page.tsx +++ b/apps/web/src/app/termen/[...slug]/page.tsx @@ -62,6 +62,18 @@ export default async function TermenPage({ params }: { params: { slug: string[] value: 'nl', }); + const nlLabelNode = findNodeWithField({ + input: rootNode?.['http://www.w3.org/2000/01/rdf-schema#label'] || {}, + key: '@language', + value: 'nl', + }); + + const nlScopeNoteNode = findNodeWithField({ + input: rootNode?.['http://www.w3.org/2004/02/skos/core#scopeNote'] || {}, + key: '@language', + value: 'nl', + }); + const nlDefinitionNode = findNodeWithField({ input: rootNode?.['http://www.w3.org/2004/02/skos/core#definition'] || {}, key: '@language', @@ -78,8 +90,12 @@ export default async function TermenPage({ params }: { params: { slug: string[] {nlPrefLabel} + Label + {nlLabelNode?.['@value']} Definitie {nlDefinitionNode?.['@value']} + Scope notitie + {nlScopeNoteNode?.['@value']} Download