feat(legal): imprint & privacy notices, "Über MindSift"
- LEGAL_IMPRINT_/LEGAL_PRIVACY_ URL and Markdown (inline or *_FILE);
only http(s) URLs accepted
- /auth/status exposes legal info; public GET /legal/{imprint|privacy}
- Links on login/setup screen and in the settings footer: URL opens a new
tab, Markdown a reading modal; hidden when nothing is configured
- "Über MindSift" modal (version, license, repository) in settings and help
- Docs and compose/env examples
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a62b204e14
commit
d6c70d7a8e
17 files changed
+318
-2
No files matched your search
@@ -0,0 +1,63 @@
|
||||
import React, { useState } from 'react';
|
||||
import { AuthStatus } from '../lib/types';
|
||||
import { api } from '../lib/api';
|
||||
import { MarkdownViewer } from '../lib/markdown';
|
||||
import { ReadingModal } from './ReadingModal';
|
||||
|
||||
type LegalType = 'imprint' | 'privacy';
|
||||
const TITLES: Record<LegalType, string> = { imprint: 'Impressum', privacy: 'Datenschutz' };
|
||||
|
||||
interface LegalLinksProps {
|
||||
legal?: AuthStatus['legal'];
|
||||
className?: string;
|
||||
children?: React.ReactNode; // further footer links, e.g. "Über MindSift"
|
||||
}
|
||||
|
||||
// Impressum / Datenschutz links: an external URL opens in a new tab, Markdown in a reading modal.
|
||||
// Links without URL or text are not shown at all.
|
||||
export const LegalLinks: React.FC<LegalLinksProps> = ({ legal, className = '', children }) => {
|
||||
const [open, setOpen] = useState<{ type: LegalType; content: string } | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const entries = (['imprint', 'privacy'] as LegalType[])
|
||||
.map((type) => ({
|
||||
type,
|
||||
url: type === 'imprint' ? legal?.imprintUrl : legal?.privacyUrl,
|
||||
hasContent: type === 'imprint' ? legal?.hasImprintContent : legal?.hasPrivacyContent,
|
||||
}))
|
||||
.filter((e) => e.url || e.hasContent);
|
||||
|
||||
if (entries.length === 0 && !children) return null;
|
||||
|
||||
const openText = async (type: LegalType) => {
|
||||
setError(null);
|
||||
try {
|
||||
setOpen({ type, content: await api.getLegalText(type) });
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Text konnte nicht geladen werden');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-[11px] text-muted ${className}`}>
|
||||
{entries.map((e) =>
|
||||
e.url ? (
|
||||
<a key={e.type} href={e.url} target="_blank" rel="noopener noreferrer" className="hover:text-main underline-offset-2 hover:underline">
|
||||
{TITLES[e.type]}
|
||||
</a>
|
||||
) : (
|
||||
<button key={e.type} type="button" onClick={() => openText(e.type)} className="hover:text-main underline-offset-2 hover:underline">
|
||||
{TITLES[e.type]}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
{children}
|
||||
{error && <span className="text-status-err">{error}</span>}
|
||||
{open && (
|
||||
<ReadingModal title={TITLES[open.type]} onClose={() => setOpen(null)}>
|
||||
<MarkdownViewer content={open.content} />
|
||||
</ReadingModal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user