footer.stats.keys.cardsquotation.colon
diff --git a/pages/admin/index.vue b/pages/admin/index.vue
index 98ca1fc8b..d0f543fa2 100644
--- a/pages/admin/index.vue
+++ b/pages/admin/index.vue
@@ -9,7 +9,7 @@
admin.header
-
+
Stats calculated at:
@@ -31,34 +31,34 @@
-
+
{{ name }}
(not published yet)
;
+}
+
+export const fetchStats = async (db: Database): Promise => {
+ const maxId = (await db.get<{ maxId: StatRow['id'] | null }>('SELECT MAX(id) AS maxId FROM stats'))!.maxId;
+
+ if (maxId === null) {
+ return null;
+ }
+
+ let overall: ({ users: number } & OverallStatsData) | null = null;
+ const locales: Record = {};
+
+ for (const statsRow of await db.all>(SQL`SELECT locale, users, data FROM stats WHERE id = ${maxId}`)) {
+ const stats = {
+ users: statsRow.users,
+ ...JSON.parse(statsRow.data),
+ };
+ if (statsRow.locale === '_') {
+ overall = stats;
+ } else {
+ locales[statsRow.locale] = stats;
+ }
+ }
+
+ return {
+ calculatedAt: decodeTime(maxId) / 1000,
+ overall: overall!,
+ locales,
+ };
+};
diff --git a/server/api/admin/stats-public.get.ts b/server/api/admin/stats-public.get.ts
new file mode 100644
index 000000000..718a3a58e
--- /dev/null
+++ b/server/api/admin/stats-public.get.ts
@@ -0,0 +1,67 @@
+import { fetchStats } from '~/server/admin.ts';
+
+interface PublicStats {
+ calculatedAt: number;
+ overall: PublicLocaleStats;
+ current: Partial;
+}
+
+interface PublicLocaleStats {
+ users: number;
+ cards: number;
+ pageViews: number;
+ visitors: number;
+ online: number;
+ visitDuration?: number;
+ uptime?: number;
+ responseTime?: number;
+}
+
+export default defineEventHandler(async () => {
+ const db = useDatabase();
+ const statsAll = await fetchStats(db);
+ if (statsAll === null) {
+ throw createError({
+ status: 404,
+ statusMessage: 'Not Found',
+ });
+ }
+
+ const stats: PublicStats = {
+ calculatedAt: statsAll.calculatedAt,
+ overall: {
+ users: statsAll.overall.users,
+ cards: 0,
+ pageViews: statsAll.overall.plausible?.pageviews || 0,
+ visitors: statsAll.overall.plausible?.visitors || 0,
+ online: statsAll.overall.plausible?.realTimeVisitors || 0,
+ },
+ current: {},
+ };
+
+ for (const [locale, localeStats] of Object.entries(statsAll.locales)) {
+ stats.overall.cards += localeStats.users;
+ if (locale === global.config.locale) {
+ stats.current = {
+ cards: localeStats.users,
+ };
+ }
+ if (localeStats.plausible) {
+ stats.overall.pageViews += localeStats.plausible.pageviews;
+ stats.overall.visitors += localeStats.plausible.visitors;
+ stats.overall.online += localeStats.plausible.realTimeVisitors;
+ if (locale === global.config.locale) {
+ stats.current.pageViews = localeStats.plausible.pageviews;
+ stats.current.visitors = localeStats.plausible.visitors;
+ stats.current.online = localeStats.plausible.realTimeVisitors;
+ stats.current.visitDuration = localeStats.plausible.visit_duration;
+ }
+ }
+ if (localeStats.heartbeat && locale === global.config.locale) {
+ stats.current.uptime = localeStats.heartbeat.uptime;
+ stats.current.responseTime = localeStats.heartbeat.avgResponseTime;
+ }
+ }
+
+ return stats;
+});
diff --git a/server/api/admin/stats.get.ts b/server/api/admin/stats.get.ts
new file mode 100644
index 000000000..a498812d1
--- /dev/null
+++ b/server/api/admin/stats.get.ts
@@ -0,0 +1,28 @@
+import { fetchStats } from '~/server/admin.ts';
+
+export default defineEventHandler(async (event) => {
+ const { isGranted } = await useAuthentication(event);
+ if (!isGranted('panel')) {
+ throw createError({
+ status: 401,
+ statusMessage: 'Unauthorised',
+ });
+ }
+
+ const db = useDatabase();
+ const stats = await fetchStats(db);
+ if (stats === null) {
+ throw createError({
+ status: 404,
+ statusMessage: 'Not Found',
+ });
+ }
+
+ for (const locale of Object.keys(stats.locales)) {
+ if (!isGranted('panel', locale)) {
+ delete stats.locales[locale];
+ }
+ }
+
+ return stats;
+});
diff --git a/server/express/admin.ts b/server/express/admin.ts
index 8bde25e50..fa02f026b 100644
--- a/server/express/admin.ts
+++ b/server/express/admin.ts
@@ -1,7 +1,6 @@
import fs from 'fs';
import { Router } from 'express';
-import type { Request } from 'express';
import markdownit from 'markdown-it';
import SQL from 'sql-template-strings';
import Suml from 'suml';
@@ -23,6 +22,8 @@ import { profilesSnapshot } from './profile.ts';
import { loadCurrentUser } from './user.ts';
import type { UserRow } from './user.ts';
+import type { StatRow } from '~/server/admin.ts';
+
interface BanProposalRow {
id: string;
userId: string;
@@ -31,13 +32,6 @@ interface BanProposalRow {
bannedReason: string;
}
-interface StatRow {
- id: string;
- locale: string;
- users: number;
- data: string;
-}
-
interface UserMessageRow {
id: string;
userId: string;
@@ -171,112 +165,6 @@ router.get('/admin/users', handleErrorAsync(async (req, res) => {
});
}));
-const fetchStats = async (req: Request): Promise => {
- const maxId = (await req.db.get<{ maxId: StatRow['id'] | null }>('SELECT MAX(id) AS maxId FROM stats'))!.maxId;
-
- if (maxId == null) {
- return {
- _: {},
- };
- }
-
- const stats: any = {
- calculatedAt: decodeTime(maxId) / 1000,
- };
-
- for (const statsRow of await req.db.all>(SQL`SELECT locale, users, data FROM stats WHERE id = ${maxId}`)) {
- stats[statsRow.locale] = {
- users: statsRow.users,
- ...JSON.parse(statsRow.data),
- };
- }
-
- return stats;
-};
-
-router.get('/admin/stats', handleErrorAsync(async (req, res) => {
- if (!req.isGranted('panel')) {
- return res.status(401).json({ error: 'Unauthorised' });
- }
-
- const stats = await fetchStats(req);
-
- for (const locale of Object.keys(stats)) {
- if (locale === '_' || locale === 'calculatedAt') {
- continue;
- }
-
- if (!req.isGranted('panel', locale)) {
- delete stats[locale];
- }
- }
-
- return res.json(stats);
-}));
-
-interface Stats {
- calculatedAt: number;
- overall: LocaleStats;
- current: Partial;
-}
-
-interface LocaleStats {
- users: number;
- cards: number;
- pageViews?: number;
- visitors?: number;
- online?: number;
- visitDuration?: number;
- uptime?: number;
- responseTime?: number;
-}
-
-router.get('/admin/stats-public', handleErrorAsync(async (req, res) => {
- const statsAll = await fetchStats(req);
-
- const stats: Stats = {
- calculatedAt: statsAll.calculatedAt,
- overall: {
- users: statsAll._.users,
- cards: 0,
- pageViews: statsAll._.plausible?.pageviews || 0,
- visitors: statsAll._.plausible?.visitors || 0,
- online: statsAll._.plausible?.realTimeVisitors || 0,
- },
- current: {},
- };
-
- for (const [locale, localeStats] of Object.entries(statsAll) as any) {
- if (locale === '_' || locale === 'calculatedAt') {
- continue;
- }
-
- stats.overall.cards += localeStats.users;
- if (locale === global.config.locale) {
- stats.current = {
- cards: localeStats.users,
- };
- }
- if (localeStats.plausible) {
- stats.overall.pageViews += localeStats.plausible.pageviews;
- stats.overall.visitors += localeStats.plausible.visitors;
- stats.overall.online += localeStats.plausible.realTimeVisitors;
- if (locale === global.config.locale) {
- stats.current.pageViews = localeStats.plausible.pageviews;
- stats.current.visitors = localeStats.plausible.visitors;
- stats.current.online = localeStats.plausible.realTimeVisitors;
- stats.current.visitDuration = localeStats.plausible.visit_duration;
- }
- }
- if (localeStats.heartbeat && locale === global.config.locale) {
- stats.current.uptime = localeStats.heartbeat.uptime;
- stats.current.responseTime = localeStats.heartbeat.avgResponseTime;
- }
- }
-
- return res.json(stats);
-}));
-
router.get('/admin/stats/users-chart/:locale', handleErrorAsync(async (req, res) => {
if (!req.isGranted('users') && !req.isGranted('community')) {
return res.status(401).json({ error: 'Unauthorised' });
diff --git a/server/index.ts b/server/index.ts
index 16c28e85d..1e84adb88 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -12,7 +12,6 @@ import SQL from 'sql-template-strings';
import buildLocaleList from '../src/buildLocaleList.ts';
import { longtimeCookieSetting } from '../src/cookieSettings.ts';
import formatError from '../src/error.ts';
-import { isGrantedForUser } from '../src/helpers.ts';
import type { User } from '../src/user.ts';
import './globals.ts';
@@ -118,9 +117,7 @@ router.use(async function (req, res, next) {
const authentication = await useAuthentication(getH3Event(req));
req.rawUser = authentication.rawUser;
req.user = authentication.user;
- req.isGranted = (area: string = '', locale = global.config.locale): boolean => {
- return !!req.user && isGrantedForUser(req.user, locale, area);
- };
+ req.isGranted = authentication.isGranted;
req.locales = buildLocaleList(global.config.locale, global.config.locale === '_');
req.db = new LazyDatabase();
req.isUserAllowedToPost = async (): Promise => {
diff --git a/server/utils/useAuthentication.ts b/server/utils/useAuthentication.ts
index 32460782b..58c119e5a 100644
--- a/server/utils/useAuthentication.ts
+++ b/server/utils/useAuthentication.ts
@@ -1,6 +1,7 @@
import type { H3Event } from 'h3';
import jwt from '~/server/jwt.ts';
+import { isGrantedForUser } from '~/src/helpers.ts';
import type { User } from '~/src/user.ts';
export default async (event: H3Event) => {
@@ -17,5 +18,8 @@ export default async (event: H3Event) => {
}
const user = rawUser?.authenticated ? rawUser : null;
- return { rawUser, user };
+ const isGranted = (area: string = '', locale = global.config.locale): boolean => {
+ return !!user && isGrantedForUser(user, locale, area);
+ };
+ return { rawUser, user, isGranted };
};
diff --git a/src/stats.ts b/src/stats.ts
index 03bebf15b..a503ce2c8 100644
--- a/src/stats.ts
+++ b/src/stats.ts
@@ -109,13 +109,13 @@ const deduplicateAdminMail = (projectDir: string, type: string, seconds: number)
return true;
};
-interface Stats {
+export interface LocaleStats {
locale: string;
users: number;
data: OverallStatsData | LocaleStatsData;
}
-interface OverallStatsData {
+export interface OverallStatsData {
admins: number;
userReports: number;
bansPending: number;
@@ -125,7 +125,7 @@ interface OverallStatsData {
linksQueue: number;
}
-interface LocaleStatsData {
+export interface LocaleStatsData {
nouns: { approved: number; awaiting: number };
inclusive: { approved: number; awaiting: number };
terms: { approved: number; awaiting: number };
@@ -140,7 +140,7 @@ export const calculateStats = async (
db: Database,
allLocales: Record,
projectDir: string,
-): Promise => {
+): Promise => {
const id = ulid();
const heartbeat = await checkHeartbeat();
From 4367e0f054544867c668e19281395a6ed87ca22d Mon Sep 17 00:00:00 2001
From: Valentyne Stigloher
Date: Tue, 31 Dec 2024 00:33:22 +0100
Subject: [PATCH 02/10] (ts) migrate /api/admin/all-locales from express to h3
---
server/api/admin/all-locales.get.ts | 24 ++++++++++++++++++++++++
server/express/admin.ts | 22 ----------------------
2 files changed, 24 insertions(+), 22 deletions(-)
create mode 100644 server/api/admin/all-locales.get.ts
diff --git a/server/api/admin/all-locales.get.ts b/server/api/admin/all-locales.get.ts
new file mode 100644
index 000000000..aa7ef543f
--- /dev/null
+++ b/server/api/admin/all-locales.get.ts
@@ -0,0 +1,24 @@
+import fs from 'fs';
+
+import Suml from 'suml';
+
+import type { Config } from '~/locale/config.ts';
+import buildLocaleList from '~/src/buildLocaleList.ts';
+
+export default defineEventHandler(async (event) => {
+ const { isGranted } = await useAuthentication(event);
+ if (!isGranted('panel')) {
+ throw createError({
+ status: 401,
+ statusMessage: 'Unauthorised',
+ });
+ }
+
+ return Object.fromEntries(Object.entries(buildLocaleList(global.config.locale, true))
+ .map(([locale, localeDescription]) => {
+ return [locale, {
+ ...localeDescription,
+ config: new Suml().parse(fs.readFileSync(`./locale/${locale}/config.suml`).toString()) as Config,
+ }];
+ }));
+});
diff --git a/server/express/admin.ts b/server/express/admin.ts
index fa02f026b..336be6b26 100644
--- a/server/express/admin.ts
+++ b/server/express/admin.ts
@@ -3,13 +3,9 @@ import fs from 'fs';
import { Router } from 'express';
import markdownit from 'markdown-it';
import SQL from 'sql-template-strings';
-import Suml from 'suml';
import { encodeTime, decodeTime, ulid } from 'ulid';
-import type { Config } from '../../locale/config.ts';
import allLocales from '../../locale/locales.ts';
-import type { LocaleDescription } from '../../locale/locales.ts';
-import buildLocaleList from '../../src/buildLocaleList.ts';
import { buildDict, now, shuffle, handleErrorAsync, filterObjectKeys } from '../../src/helpers.ts';
import auditLog from '../audit.ts';
import avatar from '../avatar.ts';
@@ -195,24 +191,6 @@ router.get('/admin/stats/users-chart/:locale', handleErrorAsync(async (req, res)
return res.json(incrementsChart);
}));
-type LocalDescriptionWithConfig = LocaleDescription & { config?: Config };
-
-router.get('/admin/all-locales', handleErrorAsync(async (req, res) => {
- if (!req.isGranted('panel')) {
- return res.status(401).json({ error: 'Unauthorised' });
- }
-
- const locales: Record = buildLocaleList(global.config.locale, true);
- for (const locale in locales) {
- if (!locales.hasOwnProperty(locale)) {
- continue;
- }
- locales[locale].config = new Suml().parse(fs.readFileSync(`./locale/${locale}/config.suml`).toString()) as Config;
- }
-
- return res.json(locales);
-}));
-
const normalise = (s: string): string => s.trim().toLowerCase();
const fetchUserByUsername = async (db: Database, username: string) => {
From 9280d797ba1f4f47a5729cf6ec7f8d155391dd6c Mon Sep 17 00:00:00 2001
From: Valentyne Stigloher
Date: Tue, 31 Dec 2024 00:49:22 +0100
Subject: [PATCH 03/10] (ts) migrate pages/admin/index.vue to composition API
with typescript
---
pages/admin/index.vue | 121 +++++++++++++++++++-----------------------
server/admin.ts | 4 ++
2 files changed, 59 insertions(+), 66 deletions(-)
diff --git a/pages/admin/index.vue b/pages/admin/index.vue
index d0f543fa2..1fdd3b779 100644
--- a/pages/admin/index.vue
+++ b/pages/admin/index.vue
@@ -1,3 +1,57 @@
+
+
@@ -12,7 +66,7 @@
Stats calculated at:
-
+
{{ $datetime(stats.calculatedAt) }}
@@ -247,68 +301,3 @@
-
-
diff --git a/server/admin.ts b/server/admin.ts
index 704f9ca87..a5191429d 100644
--- a/server/admin.ts
+++ b/server/admin.ts
@@ -1,9 +1,13 @@
import SQL from 'sql-template-strings';
import { decodeTime } from 'ulid';
+import type { Config } from '~/locale/config.ts';
+import type { LocaleDescription } from '~/locale/locales.ts';
import type { Database } from '~/server/db.ts';
import type { LocaleStatsData, OverallStatsData } from '~/src/stats.ts';
+export type LocalDescriptionWithConfig = Omit & { config: Config };
+
export interface StatRow {
id: string;
locale: string;
From 77ecbeefcbd4bf43203942f3f997ce33dc0ec99e Mon Sep 17 00:00:00 2001
From: Valentyne Stigloher
Date: Tue, 31 Dec 2024 00:56:39 +0100
Subject: [PATCH 04/10] (ts) migrate components/AdminDashboardCard.vue to
composition API with typescript
---
components/AdminDashboardCard.vue | 89 ++++++++++++++++---------------
1 file changed, 46 insertions(+), 43 deletions(-)
diff --git a/components/AdminDashboardCard.vue b/components/AdminDashboardCard.vue
index 0eb46dad2..08a1c4ddb 100644
--- a/components/AdminDashboardCard.vue
+++ b/components/AdminDashboardCard.vue
@@ -1,3 +1,42 @@
+
+
@@ -8,10 +47,14 @@
{{ header }}
- -
+
-
{{ count }} {{ name || '' }}
@@ -24,43 +67,3 @@
-
-
From 5c28fc7701da2d7c6bb2b7a41bccb9c610a7e545 Mon Sep 17 00:00:00 2001
From: Valentyne Stigloher
Date: Wed, 1 Jan 2025 22:14:06 +0100
Subject: [PATCH 05/10] (ts) migrate /api/admin/moderation from express to h3
---
server/api/admin/moderation.get.ts | 30 ++++++++++++++++++++++++++++++
server/express/admin.ts | 25 -------------------------
2 files changed, 30 insertions(+), 25 deletions(-)
create mode 100644 server/api/admin/moderation.get.ts
diff --git a/server/api/admin/moderation.get.ts b/server/api/admin/moderation.get.ts
new file mode 100644
index 000000000..1400038de
--- /dev/null
+++ b/server/api/admin/moderation.get.ts
@@ -0,0 +1,30 @@
+import fs from 'node:fs/promises';
+
+import markdownit from 'markdown-it';
+
+import { rootDir } from '~/server/paths.ts';
+
+const md = markdownit({ html: true });
+
+export default defineEventHandler(async (event) => {
+ const { isGranted } = await useAuthentication(event);
+ if (!isGranted('panel')) {
+ throw createError({
+ status: 401,
+ statusMessage: 'Unauthorised',
+ });
+ }
+
+ const dir = `${rootDir}/moderation`;
+
+ return {
+ susRegexes: (await fs.readFile(`${dir}/sus.txt`, 'utf-8'))
+ .split('\n')
+ .filter((x) => !!x && !x.startsWith('#')),
+ rulesUsers: md.render(await fs.readFile(`${dir}/rules-users.md`, 'utf-8')),
+ rulesTerminology: md.render(await fs.readFile(`${dir}/rules-terminology.md`, 'utf-8')),
+ 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')),
+ };
+});
diff --git a/server/express/admin.ts b/server/express/admin.ts
index 336be6b26..fa0d5f0cd 100644
--- a/server/express/admin.ts
+++ b/server/express/admin.ts
@@ -1,7 +1,4 @@
-import fs from 'fs';
-
import { Router } from 'express';
-import markdownit from 'markdown-it';
import SQL from 'sql-template-strings';
import { encodeTime, decodeTime, ulid } from 'ulid';
@@ -12,7 +9,6 @@ import avatar from '../avatar.ts';
import { archiveBan, liftBan } from '../ban.ts';
import type { Database } from '../db.ts';
import mailer from '../mailer.ts';
-import { rootDir } from '../paths.ts';
import { profilesSnapshot } from './profile.ts';
import { loadCurrentUser } from './user.ts';
@@ -495,27 +491,6 @@ router.post('/admin/overwrite-sensitive/:username', handleErrorAsync(async (req,
return res.json(req.body.sensitive);
}));
-const md = markdownit({ html: true });
-
-router.get('/admin/moderation', handleErrorAsync(async (req, res) => {
- if (!req.isGranted('panel')) {
- return res.status(401).json({ error: 'Unauthorised' });
- }
-
- const dir = `${rootDir}/moderation`;
-
- return res.json({
- susRegexes: fs.readFileSync(`${dir}/sus.txt`).toString('utf-8')
- .split('\n')
- .filter((x) => !!x && !x.startsWith('#')),
- rulesUsers: md.render(fs.readFileSync(`${dir}/rules-users.md`).toString('utf-8')),
- rulesTerminology: md.render(fs.readFileSync(`${dir}/rules-terminology.md`).toString('utf-8')),
- rulesSources: md.render(fs.readFileSync(`${dir}/rules-sources.md`).toString('utf-8')),
- timesheets: md.render(fs.readFileSync(`${dir}/timesheets.md`).toString('utf-8')),
- expenses: md.render(fs.readFileSync(`${dir}/expenses.md`).toString('utf-8')),
- });
-}));
-
router.post('/admin/set-notification-frequency', handleErrorAsync(async (req, res) => {
if (!req.isGranted()) {
return res.status(401).json({ error: 'Unauthorised' });
From be0a4ae61aa19941e9927b6899a1d26d1cb7425d Mon Sep 17 00:00:00 2001
From: Valentyne Stigloher
Date: Wed, 1 Jan 2025 22:43:16 +0100
Subject: [PATCH 06/10] (refactor) move blog components into components/blog
---
components/{ => blog}/BlogEntriesList.vue | 3 +--
components/{ => blog}/BlogReactionSection.vue | 0
2 files changed, 1 insertion(+), 2 deletions(-)
rename components/{ => blog}/BlogEntriesList.vue (98%)
rename components/{ => blog}/BlogReactionSection.vue (100%)
diff --git a/components/BlogEntriesList.vue b/components/blog/BlogEntriesList.vue
similarity index 98%
rename from components/BlogEntriesList.vue
rename to components/blog/BlogEntriesList.vue
index b20a6dd20..d826403f5 100644
--- a/components/BlogEntriesList.vue
+++ b/components/blog/BlogEntriesList.vue
@@ -2,8 +2,7 @@
import Columnist from 'avris-columnist';
import type { RouteLocationRaw } from 'vue-router';
-import useConfig from '../composables/useConfig.ts';
-
+import useConfig from '~/composables/useConfig.ts';
import type { Post } from '~/server/blog.ts';
const props = defineProps<{
diff --git a/components/BlogReactionSection.vue b/components/blog/BlogReactionSection.vue
similarity index 100%
rename from components/BlogReactionSection.vue
rename to components/blog/BlogReactionSection.vue
From 15c20ea07c0ad04231564c47c8b33aa5f2efd2e2 Mon Sep 17 00:00:00 2001
From: Valentyne Stigloher
Date: Wed, 1 Jan 2025 22:50:26 +0100
Subject: [PATCH 07/10] (refactor) extract for a single thumbnail
of a blog entry
---
components/blog/BlogEntriesList.vue | 49 ++-------------------------
components/blog/BlogEntry.vue | 51 +++++++++++++++++++++++++++++
2 files changed, 53 insertions(+), 47 deletions(-)
create mode 100644 components/blog/BlogEntry.vue
diff --git a/components/blog/BlogEntriesList.vue b/components/blog/BlogEntriesList.vue
index d826403f5..0c9623c04 100644
--- a/components/blog/BlogEntriesList.vue
+++ b/components/blog/BlogEntriesList.vue
@@ -1,8 +1,6 @@
+
+
+
+
+
+
+
+
+
+
+
+
From 64735cac1bada0418568df3cc013c9234905d9d0 Mon Sep 17 00:00:00 2001
From: Valentyne Stigloher
Date: Wed, 1 Jan 2025 23:26:34 +0100
Subject: [PATCH 08/10] (admin)(blog) very simple post editor
---
Makefile | 2 +-
components/blog/BlogEntriesList.vue | 2 +-
components/blog/BlogEntry.vue | 2 +-
pages/admin/blog/editor.vue | 56 +++++++++++++++++++++++++++
pages/admin/index.vue | 8 ++++
server/api/admin/moderation.get.ts | 1 +
server/blog.ts | 60 +----------------------------
src/blog/metadata.ts | 59 ++++++++++++++++++++++++++++
test/locales/blog.test.ts | 2 +-
9 files changed, 129 insertions(+), 63 deletions(-)
create mode 100644 pages/admin/blog/editor.vue
create mode 100644 src/blog/metadata.ts
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 @@
+
+
+
+
+
+
+
+
+ admin.header
+
+
+
+
+ Blog editor
+
+
+
+
+
+
+
+
+
+
+
+
+
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: [
From 02d2e92fdbc0e03cf1646f074de671a4e11991dd Mon Sep 17 00:00:00 2001
From: Andrea Vos
Date: Thu, 2 Jan 2025 13:43:39 +0100
Subject: [PATCH 09/10] CR fixes
---
pages/admin/blog/editor.vue | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/pages/admin/blog/editor.vue b/pages/admin/blog/editor.vue
index 5829888a5..2b9619a8b 100644
--- a/pages/admin/blog/editor.vue
+++ b/pages/admin/blog/editor.vue
@@ -44,13 +44,15 @@ const moderationAsyncData = await useFetch('/api/admin/moderation', { pick: ['bl
-
+
-
+
+
+
From 98472dc6353647346ba43d4d15b456abc3883630 Mon Sep 17 00:00:00 2001
From: Valentyne Stigloher
Date: Thu, 2 Jan 2025 14:18:53 +0100
Subject: [PATCH 10/10] (admin) hide blog editor card when filtering for
attention
---
pages/admin/index.vue | 1 +
1 file changed, 1 insertion(+)
diff --git a/pages/admin/index.vue b/pages/admin/index.vue
index 27f55011c..fcc0d8a50 100644
--- a/pages/admin/index.vue
+++ b/pages/admin/index.vue
@@ -265,6 +265,7 @@ const impersonate = async (email: string) => {
/>