diff --git a/Makefile b/Makefile index 1faecb84e..bd618699d 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ install: -cp -n .env.dist .env if [ ! -d "${KEYS_DIR}" ]; then mkdir -p ${KEYS_DIR}; openssl genrsa -out ${KEYS_DIR}/private.pem 2048; openssl rsa -in ${KEYS_DIR}/private.pem -outform PEM -pubout -out ${KEYS_DIR}/public.pem; fi mkdir -p moderation - touch moderation/sus.txt moderation/rules-users.md moderation/rules-terminology.md moderation/rules-sources.md moderation/timesheets.md moderation/expenses.md + touch moderation/sus.txt moderation/rules-users.md moderation/rules-terminology.md moderation/rules-sources.md moderation/timesheets.md moderation/expenses.md moderation/blog.md pnpm install pnpm run-file server/migrate.ts diff --git a/components/blog/BlogEntriesList.vue b/components/blog/BlogEntriesList.vue index 0c9623c04..15ac3d2ad 100644 --- a/components/blog/BlogEntriesList.vue +++ b/components/blog/BlogEntriesList.vue @@ -1,7 +1,7 @@ + + diff --git a/pages/admin/index.vue b/pages/admin/index.vue index 1fdd3b779..27f55011c 100644 --- a/pages/admin/index.vue +++ b/pages/admin/index.vue @@ -263,6 +263,14 @@ const impersonate = async (email: string) => { { name: 'awaiting', count: stats.locales[locale].names.awaiting, warning: 1, danger: 16 }, ]" /> + + Editor + { rulesSources: md.render(await fs.readFile(`${dir}/rules-sources.md`, 'utf-8')), timesheets: md.render(await fs.readFile(`${dir}/timesheets.md`, 'utf-8')), expenses: md.render(await fs.readFile(`${dir}/expenses.md`, 'utf-8')), + blog: md.render(await fs.readFile(`${dir}/blog.md`, 'utf-8')), }; }); diff --git a/server/blog.ts b/server/blog.ts index 42c2e7cb0..d2e49c237 100644 --- a/server/blog.ts +++ b/server/blog.ts @@ -3,68 +3,10 @@ import fs from 'node:fs/promises'; import { defineCachedFunction } from 'nitropack/runtime'; import SQL from 'sql-template-strings'; -import type { Config } from '~/locale/config.ts'; import type { Database } from '~/server/db.ts'; import type { UserRow } from '~/server/express/user.ts'; import { rootDir } from '~/server/paths.ts'; - -export interface PostMetadata { - title: string; - date: string; - authors: string[]; - hero: { src: string; alt: string; class: string } | undefined; -} - -export interface Post extends PostMetadata { - slug: string; -} - -export const extractMetadata = (config: Config, content: string): PostMetadata | undefined => { - const lines = content.split('\n').filter((l) => !!l); - - const title = lines[0].match(/^# (.*)$/)?.[1]; - - const secondLineMatch = lines[1].match(/^(\d\d\d\d-\d\d-\d\d) \| ([^|]*).*<\/small>$/); - const date = secondLineMatch?.[1]; - const authors = secondLineMatch?.[2].split(',').map((a) => { - a = a.trim(); - const teamName = config.contact.team?.route; - if (teamName && a.startsWith(teamName)) { - return teamName; - } - const m = a.match(/^\[([^\]]+)]/); - if (m) { - return m[1]; - } - return a; - }) ?? []; - - let hero = undefined; - const classHeroImages = lines - .map((x) => x.match(/([^ !!x); - if (classHeroImages.length) { - hero = { - src: classHeroImages[0][1], - alt: classHeroImages[0][3], - class: classHeroImages[0][2].replace('d-none', ''), - }; - } else { - const images = lines.map((x) => x.match(/^!\[([^\]]*)]\(([^)]+)\)$/)).filter((x) => !!x); - if (images.length) { - hero = { - src: images[0][2], - alt: images[0][1], - class: '', - }; - } - } - - if (title === undefined || date === undefined) { - return undefined; - } - return { title, date, authors, hero }; -}; +import { extractMetadata, type Post } from '~/src/blog/metadata.ts'; export const getPosts = defineCachedFunction(async (): Promise => { const dir = `${rootDir}/data/blog`; diff --git a/src/blog/metadata.ts b/src/blog/metadata.ts new file mode 100644 index 000000000..25d27be66 --- /dev/null +++ b/src/blog/metadata.ts @@ -0,0 +1,59 @@ +import type { Config } from '~/locale/config.ts'; + +export interface PostMetadata { + title: string; + date: string; + authors: string[]; + hero: { src: string; alt: string; class: string } | undefined; +} + +export interface Post extends PostMetadata { + slug: string; +} + +export const extractMetadata = (config: Config, content: string): PostMetadata | undefined => { + const lines = content.split('\n').filter((l) => !!l); + + const title = lines[0]?.match(/^# (.*)$/)?.[1]; + + const secondLineMatch = lines[1]?.match(/^(\d\d\d\d-\d\d-\d\d) \| ([^|]*).*<\/small>$/); + const date = secondLineMatch?.[1]; + const authors = secondLineMatch?.[2].split(',').map((a) => { + a = a.trim(); + const teamName = config.contact.team?.route; + if (teamName && a.startsWith(teamName)) { + return teamName; + } + const m = a.match(/^\[([^\]]+)]/); + if (m) { + return m[1]; + } + return a; + }) ?? []; + + let hero = undefined; + const classHeroImages = lines + .map((x) => x.match(/([^ !!x); + if (classHeroImages.length) { + hero = { + src: classHeroImages[0][1], + alt: classHeroImages[0][3], + class: classHeroImages[0][2].replace('d-none', ''), + }; + } else { + const images = lines.map((x) => x.match(/^!\[([^\]]*)]\(([^)]+)\)$/)).filter((x) => !!x); + if (images.length) { + hero = { + src: images[0][2], + alt: images[0][1], + class: '', + }; + } + } + + if (title === undefined || date === undefined) { + return undefined; + } + return { title, date, authors, hero }; +}; diff --git a/test/locales/blog.test.ts b/test/locales/blog.test.ts index 8af9f5e6a..da3c264b2 100644 --- a/test/locales/blog.test.ts +++ b/test/locales/blog.test.ts @@ -13,7 +13,7 @@ import { loadSumlFromBase } from '../../server/loader.ts'; import parseMarkdown from '../../src/parseMarkdown.ts'; import { Translator } from '../../src/translator.ts'; -import { extractMetadata } from '~/server/blog.ts'; +import { extractMetadata } from '~/src/blog/metadata.ts'; const validator = new HtmlValidate({ extends: [