Skip to content

Commit

Permalink
Merge pull request #702 from MinBZK/terms-ui
Browse files Browse the repository at this point in the history
Added human friendly ui to terms pages
  • Loading branch information
onursagir authored Dec 15, 2024
2 parents 9c6aa31 + 8baa311 commit 4ee7090
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 60 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-comics-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"web": minor
---

Added human friendly Term page ui
26 changes: 26 additions & 0 deletions apps/web/src/app/termen/[...slug]/middleware.ts
Original file line number Diff line number Diff line change
@@ -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();
},
};
113 changes: 113 additions & 0 deletions apps/web/src/app/termen/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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<string, any>;
key: string;
value: string;
}

function findNodeWithField({ input, key, value }: FindFieldArgs): Record<string, any> | null {
let result: Record<string, any> | null = null;

function search(obj: Record<string, any>) {
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<string, any>);

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 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',
value: 'nl',
});

const nlPrefLabel = nlPrefLabelNode?.['@value'];

if (!nlPrefLabel) return notFound();

return (
<Container>
<div className="flex items-end gap-x-4">
<Typography variant="h1">{nlPrefLabel}</Typography>
<Pill label="Term" className="justify-self-start" />
</div>
<Typography variant="h2">Label</Typography>
<Typography>{nlLabelNode?.['@value']}</Typography>
<Typography variant="h2">Definitie</Typography>
<Typography>{nlDefinitionNode?.['@value']}</Typography>
<Typography variant="h2">Scope notitie</Typography>
<Typography>{nlScopeNoteNode?.['@value']}</Typography>
<Typography variant="h3">Download</Typography>
<div className="mt-4 flex gap-x-2">
<Button component="a" href={`/termen/download/rdf${slug}`}>
RDF/XML
</Button>
<Button component="a" href={`/termen/download/ttl${slug}`}>
Turtle
</Button>
<Button component="a" href={`/termen/download/json${slug}`}>
JSON-LD
</Button>
</div>
</Container>
);
}
60 changes: 0 additions & 60 deletions apps/web/src/app/termen/[...slug]/route.ts

This file was deleted.

30 changes: 30 additions & 0 deletions apps/web/src/app/termen/download/[extension]/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
23 changes: 23 additions & 0 deletions apps/web/src/app/termen/find-term-in-format.ts
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 13 additions & 0 deletions apps/web/src/app/termen/get-headers-with-content-type.ts
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 7 additions & 0 deletions apps/web/src/app/termen/get-slug-from-params.ts
Original file line number Diff line number Diff line change
@@ -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)));
}
11 changes: 11 additions & 0 deletions apps/web/src/app/termen/get-valid-extension.ts
Original file line number Diff line number Diff line change
@@ -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;
}
40 changes: 40 additions & 0 deletions apps/web/src/app/termen/handler/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
7 changes: 7 additions & 0 deletions apps/web/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -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).*)'],
Expand All @@ -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);
}

0 comments on commit 4ee7090

Please sign in to comment.