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:
sandrodachandClaude Opus 5.5 committed 2026-10-28 23:10:27 +01:00
1 parent a62b204e14
commit d6c70d7a8e
17 files changed
+318 -2

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
import React from 'react';
import { Brain, ExternalLink } from 'lucide-react';
import { APP_VERSION } from '../version';
import { ReadingModal } from './ReadingModal';
// Project facts shown in "Über MindSift"; change here when the repository or license changes
export const PROJECT_REPO_URL = 'https://git.dsit.me/sandrodach/MindSift';
export const PROJECT_LICENSE = 'Noch nicht festgelegt (keine LICENSE-Datei im Repository)';
export const AboutModal: React.FC<{ onClose: () => void }> = ({ onClose }) => (
<ReadingModal title="Über MindSift" onClose={onClose}>
<div className="space-y-4 text-xs text-muted leading-relaxed">
<div className="flex items-center gap-3">
<div className="w-9 h-9 bg-accent text-on-accent rounded-btn flex items-center justify-center shrink-0">
<Brain className="w-5 h-5" />
</div>
<div>
<p className="text-sm font-bold text-main">MindSift</p>
<p>Der Kopf frei in drei Sekunden.</p>
</div>
</div>
<p>
Die meisten Notiz- und Aufgaben-Apps verlangen Struktur im falschen Moment. MindSift trennt deshalb{' '}
<strong className="text-main">Erfassen</strong> und <strong className="text-main">Organisieren</strong>: Gedanken
landen sofort im Quick Dump und werden später mit einem Tastendruck zu Aufgabe, Notiz oder Listeneintrag. Alles läuft
selbst gehostet in einem Container mit einer SQLite-Datei – deine Daten bleiben bei dir.
</p>
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5">
<dt className="font-semibold text-main">Version</dt>
<dd className="font-mono">{APP_VERSION}</dd>
<dt className="font-semibold text-main">Lizenz</dt>
<dd>{PROJECT_LICENSE}</dd>
<dt className="font-semibold text-main">Quellcode</dt>
<dd>
<a href={PROJECT_REPO_URL} target="_blank" rel="noopener noreferrer" className="text-accent hover:underline inline-flex items-center gap-1">
{PROJECT_REPO_URL.replace(/^https:\/\//, '')}
<ExternalLink className="w-3 h-3" />
</a>
</dd>
</dl>
<p className="text-[11px] text-faint">
Impressum und Datenschutz dieser Instanz verantwortet ihr Betreiber; die Links stehen in den Kontoeinstellungen und auf der Anmeldeseite.
</p>
</div>
</ReadingModal>
);
+63
View File
@@ -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>
);
};
+37
View File
@@ -0,0 +1,37 @@
import React, { useEffect } from 'react';
import { X } from 'lucide-react';
interface ReadingModalProps {
title: string;
onClose: () => void;
children: React.ReactNode;
}
// Slim modal for reading text (legal notices, about); Escape or backdrop click closes it
export const ReadingModal: React.FC<ReadingModalProps> = ({ title, onClose, children }) => {
useEffect(() => {
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose();
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs"
onClick={(e) => e.target === e.currentTarget && onClose()}
role="dialog"
aria-modal="true"
aria-label={title}
>
<div className="panel-surface w-full max-w-2xl max-h-[85vh] flex flex-col shadow-panel">
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<h2 className="text-sm font-bold text-main">{title}</h2>
<button type="button" onClick={onClose} className="btn-icon" title="Schließen">
<X className="w-4 h-4" />
</button>
</div>
<div className="p-4 overflow-y-auto text-sm text-main">{children}</div>
</div>
</div>
);
};