feat(cms): redirect legal links to external URL when configured

This commit is contained in:
Luca
2026-05-03 23:06:16 +02:00
parent a4e3d10fb0
commit c5bba505ac
2 changed files with 63 additions and 13 deletions
@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { Calendar, Clock, Download, LogOut } from 'lucide-react'; import { Calendar, Clock, Download, LogOut } from 'lucide-react';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -9,6 +10,7 @@ import { DynamicFavicon } from '../common/DynamicFavicon';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
import { useGuestIdentityOptional } from '../../contexts/GuestIdentityContext'; import { useGuestIdentityOptional } from '../../contexts/GuestIdentityContext';
import { buildResourceUrl } from '../../utils/url'; import { buildResourceUrl } from '../../utils/url';
import { cmsService, type PublicCMSPage } from '../../services/cms.service';
import type { HeaderStyleType } from '../../types/theme.types'; import type { HeaderStyleType } from '../../types/theme.types';
interface GalleryLayoutProps { interface GalleryLayoutProps {
@@ -62,6 +64,22 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
const { theme } = useTheme(); const { theme } = useTheme();
const guestIdentity = useGuestIdentityOptional(); const guestIdentity = useGuestIdentityOptional();
// Footer legal-link config. Cached aggressively because the toggle state
// changes rarely and the gallery footer renders on every page view.
// Failures fall back to the internal /impressum and /datenschutz routes.
const { data: impressumPage } = useQuery<PublicCMSPage>({
queryKey: ['public-cms', 'impressum'],
queryFn: () => cmsService.getPublicPage('impressum'),
staleTime: 5 * 60 * 1000,
retry: false,
});
const { data: datenschutzPage } = useQuery<PublicCMSPage>({
queryKey: ['public-cms', 'datenschutz'],
queryFn: () => cmsService.getPublicPage('datenschutz'),
staleTime: 5 * 60 * 1000,
retry: false,
});
// Determine header style - use prop first (from event data), then theme, then fall back to 'standard' // Determine header style - use prop first (from event data), then theme, then fall back to 'standard'
const headerStyle: HeaderStyleType = headerStyleProp || theme.headerStyle || 'standard'; const headerStyle: HeaderStyleType = headerStyleProp || theme.headerStyle || 'standard';
const isHeroHeader = headerStyle === 'hero'; const isHeroHeader = headerStyle === 'hero';
@@ -592,19 +610,41 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
)} )}
{/* Legal Links */} {/* Legal Links */}
<div className="mt-4 flex items-center justify-center gap-4 flex-wrap"> <div className="mt-4 flex items-center justify-center gap-4 flex-wrap">
<Link {impressumPage?.use_external_url && impressumPage.external_url ? (
to="/impressum" <a
className="text-xs text-muted-theme hover:text-theme transition-colors" href={impressumPage.external_url}
> target="_blank"
{t('legal.impressum')} rel="noopener noreferrer"
</Link> className="text-xs text-muted-theme hover:text-theme transition-colors"
>
{t('legal.impressum')}
</a>
) : (
<Link
to="/impressum"
className="text-xs text-muted-theme hover:text-theme transition-colors"
>
{t('legal.impressum')}
</Link>
)}
<span className="text-xs text-muted-theme">|</span> <span className="text-xs text-muted-theme">|</span>
<Link {datenschutzPage?.use_external_url && datenschutzPage.external_url ? (
to="/datenschutz" <a
className="text-xs text-muted-theme hover:text-theme transition-colors" href={datenschutzPage.external_url}
> target="_blank"
{t('legal.datenschutz')} rel="noopener noreferrer"
</Link> className="text-xs text-muted-theme hover:text-theme transition-colors"
>
{t('legal.datenschutz')}
</a>
) : (
<Link
to="/datenschutz"
className="text-xs text-muted-theme hover:text-theme transition-colors"
>
{t('legal.datenschutz')}
</Link>
)}
{guestIdentity?.identity && ( {guestIdentity?.identity && (
<> <>
<span className="text-xs text-muted-theme">|</span> <span className="text-xs text-muted-theme">|</span>
+11 -1
View File
@@ -44,7 +44,17 @@ export const LegalPage: React.FC = () => {
} }
}, [page?.title]); }, [page?.title]);
if (isLoading) { // External-URL override: full-page redirect so the visitor lands on the
// operator's own canonical legal page. Use replace() so the back button
// returns to the gallery instead of looping back through the redirect.
const willRedirect = !!(page?.use_external_url && page?.external_url);
useEffect(() => {
if (willRedirect && page?.external_url) {
window.location.replace(page.external_url);
}
}, [willRedirect, page?.external_url]);
if (isLoading || willRedirect) {
return ( return (
<div className="min-h-screen bg-neutral-50 flex items-center justify-center"> <div className="min-h-screen bg-neutral-50 flex items-center justify-center">
<Loading size="lg" text="Loading..." /> <Loading size="lg" text="Loading..." />