feat: implement tenant OAuth flow and guest achievements

This commit is contained in:
2025-09-25 08:32:37 +02:00
parent ef6203c603
commit b22d91ed32
84 changed files with 5984 additions and 1399 deletions

View File

@@ -1,6 +1,7 @@
import React from 'react';
import React from "react";
import { Page } from './_util';
import { useParams } from 'react-router-dom';
import { LegalMarkdown } from '../components/legal-markdown';
export default function LegalPage() {
const { page } = useParams();
@@ -9,42 +10,44 @@ export default function LegalPage() {
const [body, setBody] = React.useState('');
React.useEffect(() => {
async function load() {
setLoading(true);
const res = await fetch(`/api/v1/legal/${encodeURIComponent(page || '')}?lang=de`, { headers: { 'Cache-Control': 'no-store' }});
if (res.ok) {
const j = await res.json();
setTitle(j.title || '');
setBody(j.body_markdown || '');
}
setLoading(false);
if (!page) {
return;
}
if (page) load();
const controller = new AbortController();
async function loadLegal() {
try {
setLoading(true);
const res = await fetch(`/api/v1/legal/${encodeURIComponent(page)}?lang=de`, {
headers: { 'Cache-Control': 'no-store' },
signal: controller.signal,
});
if (!res.ok) {
throw new Error('failed');
}
const data = await res.json();
setTitle(data.title || '');
setBody(data.body_markdown || '');
} catch (error) {
if (!controller.signal.aborted) {
console.error('Failed to load legal page', error);
setTitle('');
setBody('');
}
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}
loadLegal();
return () => controller.abort();
}, [page]);
return (
<Page title={title || `Rechtliches: ${page}` }>
{loading ? <p>Lädt</p> : <Markdown md={body} />}
{loading ? <p>Laedt...</p> : <LegalMarkdown markdown={body} />}
</Page>
);
}
function Markdown({ md }: { md: string }) {
// Tiny, safe Markdown: paragraphs + basic bold/italic + links; no external dependency
const html = React.useMemo(() => {
let s = md
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// bold **text**
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
// italic *text*
s = s.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
// links [text](url)
s = s.replace(/\[(.+?)\]\((https?:[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1<\/a>');
// paragraphs
s = s.split(/\n{2,}/).map(p => `<p>${p.replace(/\n/g, '<br/>')}<\/p>`).join('\n');
return s;
}, [md]);
return <div className="prose prose-sm dark:prose-invert" dangerouslySetInnerHTML={{ __html: html }} />;
}