feat(cms): admin UI for external imprint/privacy URL
This commit is contained in:
@@ -2187,7 +2187,13 @@
|
|||||||
"lastUpdated": "Zuletzt aktualisiert:",
|
"lastUpdated": "Zuletzt aktualisiert:",
|
||||||
"impressum": "Impressum",
|
"impressum": "Impressum",
|
||||||
"datenschutz": "Datenschutzerklärung",
|
"datenschutz": "Datenschutzerklärung",
|
||||||
"pageUpdated": "Seite erfolgreich aktualisiert"
|
"pageUpdated": "Seite erfolgreich aktualisiert",
|
||||||
|
"useExternalUrl": "Externe URL verwenden",
|
||||||
|
"useExternalUrlHelp": "Besucher werden auf eine externe Seite weitergeleitet, anstatt die internen Inhalte anzuzeigen. Titel und Inhalt bleiben als Fallback gespeichert.",
|
||||||
|
"externalUrl": "Externe URL",
|
||||||
|
"externalUrlPlaceholder": "https://example.com/impressum",
|
||||||
|
"externalUrlInvalid": "Muss eine gültige https://-URL sein",
|
||||||
|
"externalUrlActive": "Externe URL ist aktiv — interne Inhalte bleiben gespeichert, werden Besuchern aber nicht angezeigt."
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"eventNameRequired": "Veranstaltungsname ist erforderlich",
|
"eventNameRequired": "Veranstaltungsname ist erforderlich",
|
||||||
|
|||||||
@@ -1736,7 +1736,13 @@
|
|||||||
"lastUpdated": "Last updated:",
|
"lastUpdated": "Last updated:",
|
||||||
"impressum": "Legal Notice",
|
"impressum": "Legal Notice",
|
||||||
"datenschutz": "Privacy Policy",
|
"datenschutz": "Privacy Policy",
|
||||||
"pageUpdated": "Page updated successfully"
|
"pageUpdated": "Page updated successfully",
|
||||||
|
"useExternalUrl": "Use external URL",
|
||||||
|
"useExternalUrlHelp": "Redirect visitors to an external page instead of showing the internal content. The internal title and content stay saved as a fallback.",
|
||||||
|
"externalUrl": "External URL",
|
||||||
|
"externalUrlPlaceholder": "https://example.com/impressum",
|
||||||
|
"externalUrlInvalid": "Must be a valid https:// URL",
|
||||||
|
"externalUrlActive": "External URL is active — internal content is preserved but not shown to visitors."
|
||||||
},
|
},
|
||||||
"eventTypes": {
|
"eventTypes": {
|
||||||
"title": "Event Types",
|
"title": "Event Types",
|
||||||
|
|||||||
@@ -182,6 +182,31 @@ export const CMSPage: React.FC = () => {
|
|||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUseExternalUrlChange = (val: boolean) => {
|
||||||
|
setEditForm(prev => ({ ...prev, use_external_url: val }));
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExternalUrlChange = (val: string) => {
|
||||||
|
setEditForm(prev => ({ ...prev, external_url: val }));
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inline URL validation. Empty input while typing is not an error
|
||||||
|
// (avoid flagging mid-edit). Backend re-validates on save regardless.
|
||||||
|
const externalUrlError = useMemo(() => {
|
||||||
|
if (!editForm.use_external_url) return null;
|
||||||
|
const v = (editForm.external_url || '').trim();
|
||||||
|
if (!v) return null;
|
||||||
|
try {
|
||||||
|
const u = new URL(v);
|
||||||
|
if (u.protocol !== 'https:') return t('cms.externalUrlInvalid');
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return t('cms.externalUrlInvalid');
|
||||||
|
}
|
||||||
|
}, [editForm.use_external_url, editForm.external_url, t]);
|
||||||
|
|
||||||
// Per-page logo upload (#324). Only meaningful for the customisable
|
// Per-page logo upload (#324). Only meaningful for the customisable
|
||||||
// error pages right now, but harmless if exposed for any slug.
|
// error pages right now, but harmless if exposed for any slug.
|
||||||
const logoInputRef = useRef<HTMLInputElement>(null);
|
const logoInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -607,6 +632,41 @@ export const CMSPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* External URL override */}
|
||||||
|
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 p-4 bg-neutral-50 dark:bg-neutral-800/40">
|
||||||
|
<label className="flex items-start gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mt-1 h-4 w-4 rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
checked={!!editForm.use_external_url}
|
||||||
|
onChange={(e) => handleUseExternalUrlChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="flex-1">
|
||||||
|
<span className="block text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
||||||
|
{t('cms.useExternalUrl')}
|
||||||
|
</span>
|
||||||
|
<span className="block text-xs text-neutral-500 dark:text-neutral-400 mt-1">
|
||||||
|
{t('cms.useExternalUrlHelp')}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{editForm.use_external_url && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
label={t('cms.externalUrl')}
|
||||||
|
value={editForm.external_url || ''}
|
||||||
|
onChange={(e) => handleExternalUrlChange(e.target.value)}
|
||||||
|
placeholder={t('cms.externalUrlPlaceholder')}
|
||||||
|
error={externalUrlError || undefined}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
|
||||||
|
{t('cms.externalUrlActive')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export interface CMSPage {
|
|||||||
content_en: string;
|
content_en: string;
|
||||||
content_de: string;
|
content_de: string;
|
||||||
logo_url: string | null;
|
logo_url: string | null;
|
||||||
|
use_external_url: boolean;
|
||||||
|
external_url: string | null;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,6 +18,8 @@ export interface PublicCMSPage {
|
|||||||
content: string;
|
content: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
logo_url: string | null;
|
logo_url: string | null;
|
||||||
|
use_external_url: boolean;
|
||||||
|
external_url: string | null;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user