mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-23 12:43:48 -04:00
(admin)(blog) very simple post editor
This commit is contained in:
parent
15c20ea07c
commit
64735cac1b
2
Makefile
2
Makefile
@ -10,7 +10,7 @@ install:
|
|||||||
-cp -n .env.dist .env
|
-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
|
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
|
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 install
|
||||||
pnpm run-file server/migrate.ts
|
pnpm run-file server/migrate.ts
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Columnist from 'avris-columnist';
|
import Columnist from 'avris-columnist';
|
||||||
|
|
||||||
import type { Post } from '~/server/blog.ts';
|
import type { Post } from '~/src/blog/metadata.ts';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
posts: string[] | Post[];
|
posts: string[] | Post[];
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import type { RouteLocationRaw } from 'vue-router';
|
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';
|
import type { Post } from '~/src/blog/metadata.ts';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
post: Post;
|
post: Post;
|
||||||
|
56
pages/admin/blog/editor.vue
Normal file
56
pages/admin/blog/editor.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useDebounce, useLocalStorage } from '@vueuse/core';
|
||||||
|
import marked from 'marked';
|
||||||
|
|
||||||
|
import { extractMetadata, type Post } from '~/src/blog/metadata.ts';
|
||||||
|
import parseMarkdown, { type MarkdownInfo } from '~/src/parseMarkdown.ts';
|
||||||
|
|
||||||
|
const content = useLocalStorage(
|
||||||
|
'admin/blog/editor',
|
||||||
|
'# title\n<small>YYYY-MM-DD | author</small>\n\n\n\ncontent',
|
||||||
|
{ initOnMounted: true },
|
||||||
|
);
|
||||||
|
const debouncedContent = useDebounce(content, 500, { maxWait: 5_000 });
|
||||||
|
|
||||||
|
const post = ref<Post | undefined>();
|
||||||
|
const parsed = ref<MarkdownInfo | undefined>();
|
||||||
|
|
||||||
|
const { $translator: translator } = useNuxtApp();
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
|
watch(debouncedContent, async () => {
|
||||||
|
const metadata = extractMetadata(config, debouncedContent.value);
|
||||||
|
post.value = metadata ? { slug: 'slug', ...metadata } : undefined;
|
||||||
|
const markdown = marked(debouncedContent.value);
|
||||||
|
parsed.value = await parseMarkdown(markdown, translator);
|
||||||
|
});
|
||||||
|
|
||||||
|
const moderationAsyncData = await useFetch('/api/admin/moderation', { pick: ['blog'] });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page>
|
||||||
|
<NotFound v-if="!$isGranted('panel')" />
|
||||||
|
<template v-else>
|
||||||
|
<p>
|
||||||
|
<nuxt-link to="/admin">
|
||||||
|
<Icon v="user-cog" />
|
||||||
|
<T>admin.header</T>
|
||||||
|
</nuxt-link>
|
||||||
|
</p>
|
||||||
|
<h2>
|
||||||
|
<Icon v="pen-nib" />
|
||||||
|
Blog editor
|
||||||
|
</h2>
|
||||||
|
<div v-html="moderationAsyncData.data.value?.blog"></div>
|
||||||
|
|
||||||
|
<textarea v-model="content" class="form-control"></textarea>
|
||||||
|
<template v-if="post">
|
||||||
|
<hr>
|
||||||
|
<BlogEntry class="col-12 col-sm-6 col-md-4" :post details />
|
||||||
|
</template>
|
||||||
|
<hr>
|
||||||
|
<Spelling v-if="parsed?.content" :text="parsed.content" />
|
||||||
|
</template>
|
||||||
|
</Page>
|
||||||
|
</template>
|
@ -263,6 +263,14 @@ const impersonate = async (email: string) => {
|
|||||||
{ name: 'awaiting', count: stats.locales[locale].names.awaiting, warning: 1, danger: 16 },
|
{ name: 'awaiting', count: stats.locales[locale].names.awaiting, warning: 1, danger: 16 },
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
|
<AdminDashboardCard
|
||||||
|
v-if="config.links.enabled && config.links.blog"
|
||||||
|
:base-url="url"
|
||||||
|
icon="pen-nib"
|
||||||
|
header="Blog"
|
||||||
|
>
|
||||||
|
<nuxt-link :to="{ name: 'admin-blog-editor' }" class="btn btn-sm btn-secondary text-white">Editor</nuxt-link>
|
||||||
|
</AdminDashboardCard>
|
||||||
<AdminDashboardCard
|
<AdminDashboardCard
|
||||||
v-if="stats && $isGranted('translations', locale) && (stats.locales[locale].translations.missing > 0 || stats.locales[locale].translations.awaitingApproval > 0) || stats && $isGranted('code', locale) && stats.locales[locale].translations.awaitingMerge > 0"
|
v-if="stats && $isGranted('translations', locale) && (stats.locales[locale].translations.missing > 0 || stats.locales[locale].translations.awaitingApproval > 0) || stats && $isGranted('code', locale) && stats.locales[locale].translations.awaitingMerge > 0"
|
||||||
v-show="!filterAttention || stats.locales[locale].translations.missing || stats.locales[locale].translations.awaitingApproval || stats?.locales[locale].translations.awaitingMerge"
|
v-show="!filterAttention || stats.locales[locale].translations.missing || stats.locales[locale].translations.awaitingApproval || stats?.locales[locale].translations.awaitingMerge"
|
||||||
|
@ -26,5 +26,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
rulesSources: md.render(await fs.readFile(`${dir}/rules-sources.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')),
|
timesheets: md.render(await fs.readFile(`${dir}/timesheets.md`, 'utf-8')),
|
||||||
expenses: md.render(await fs.readFile(`${dir}/expenses.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')),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -3,68 +3,10 @@ import fs from 'node:fs/promises';
|
|||||||
import { defineCachedFunction } from 'nitropack/runtime';
|
import { defineCachedFunction } from 'nitropack/runtime';
|
||||||
import SQL from 'sql-template-strings';
|
import SQL from 'sql-template-strings';
|
||||||
|
|
||||||
import type { Config } from '~/locale/config.ts';
|
|
||||||
import type { Database } from '~/server/db.ts';
|
import type { Database } from '~/server/db.ts';
|
||||||
import type { UserRow } from '~/server/express/user.ts';
|
import type { UserRow } from '~/server/express/user.ts';
|
||||||
import { rootDir } from '~/server/paths.ts';
|
import { rootDir } from '~/server/paths.ts';
|
||||||
|
import { extractMetadata, type Post } from '~/src/blog/metadata.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(/^<small>(\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(/<img src="([^"]+)" class="hero([^"]*)" alt="([^"]*)"/))
|
|
||||||
.filter((x) => !!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 };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPosts = defineCachedFunction(async (): Promise<Post[]> => {
|
export const getPosts = defineCachedFunction(async (): Promise<Post[]> => {
|
||||||
const dir = `${rootDir}/data/blog`;
|
const dir = `${rootDir}/data/blog`;
|
||||||
|
59
src/blog/metadata.ts
Normal file
59
src/blog/metadata.ts
Normal file
@ -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(/^<small>(\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(/<img src="([^"]+)" class="hero([^"]*)" alt="([^"]*)"/))
|
||||||
|
.filter((x) => !!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 };
|
||||||
|
};
|
@ -13,7 +13,7 @@ import { loadSumlFromBase } from '../../server/loader.ts';
|
|||||||
import parseMarkdown from '../../src/parseMarkdown.ts';
|
import parseMarkdown from '../../src/parseMarkdown.ts';
|
||||||
import { Translator } from '../../src/translator.ts';
|
import { Translator } from '../../src/translator.ts';
|
||||||
|
|
||||||
import { extractMetadata } from '~/server/blog.ts';
|
import { extractMetadata } from '~/src/blog/metadata.ts';
|
||||||
|
|
||||||
const validator = new HtmlValidate({
|
const validator = new HtmlValidate({
|
||||||
extends: [
|
extends: [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user