Files
picpeak/frontend/src/pages/ClientAccessPage.tsx
T
Luca 0b4b5cf46e fix(branding): theme-aware logo across all login / auth entry pages
Extends the dark-logo fix to the customer login, customer accept-invite,
customer reset-password, and gallery client-access pages — they all rendered
only the light logo on the themed (possibly dark) surface.

Also makes the login-page pick frame-aware: a framed login logo sits on a
fixed cream plate, so the light (dark-ink) logo always reads there; only the
frameless logo sits on the themed page background and uses the dark variant.
This corrects the admin login too (was unconditionally swapping when dark).

Customer/gallery pages read isDark from usePublicDarkMode (branding_force_
color_mode + OS fallback), matching CustomerLayout.
2026-06-03 16:56:15 +02:00

208 lines
7.6 KiB
TypeScript

import React, { useState } from 'react';
import { useParams, useSearchParams, useNavigate, Link } from 'react-router-dom';
import { AlertCircle, Lock } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, Input, Button, Loading } from '../components/common';
import { useGalleryAuth } from '../contexts';
import { useGalleryInfo } from '../hooks/useGallery';
import { usePublicSettings } from '../hooks/usePublicSettings';
import { usePublicDarkMode } from '../hooks/usePublicDarkMode';
import { buildResourceUrl } from '../utils/url';
export const ClientAccessPage: React.FC = () => {
const { slug } = useParams<{ slug: string }>();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { isAuthenticated, isClient, clientLogin, isLoading: authLoading } = useGalleryAuth();
const { t } = useTranslation();
const [pin, setPin] = useState('');
const [isLoggingIn, setIsLoggingIn] = useState(false);
const [loginError, setLoginError] = useState<string | null>(null);
const { data: galleryInfo, isLoading: isLoadingInfo, error: infoError } = useGalleryInfo(slug);
const { data: settingsData } = usePublicSettings();
// Theme-aware logo: the page background follows the themed
// --color-background (dark when branding_force_color_mode / OS is dark),
// so pick the dark logo variant accordingly.
const { isDark } = usePublicDarkMode();
const lightLogo = settingsData?.branding_logo_url?.trim();
const darkLogo = settingsData?.branding_logo_url_dark?.trim();
const brandLogo = isDark ? (darkLogo || lightLogo) : (lightLogo || darkLogo);
// If already authenticated as client, redirect to gallery
React.useEffect(() => {
if (isAuthenticated && isClient && slug) {
navigate(`/gallery/${slug}`, { replace: true });
}
}, [isAuthenticated, isClient, slug, navigate]);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!pin.trim()) {
setLoginError(t('clientAccess.enterPin'));
return;
}
if (!slug) {
setLoginError(t('errors.galleryNotFound'));
return;
}
try {
setIsLoggingIn(true);
setLoginError(null);
await clientLogin(slug, pin);
navigate(`/gallery/${slug}`, { replace: true });
} catch (error: any) {
const statusCode = error.response?.status;
if (statusCode === 401) {
setLoginError(t('clientAccess.invalidPin'));
} else if (statusCode === 423) {
setLoginError(t('auth.tooManyAttempts'));
} else {
setLoginError(t('clientAccess.loginFailed'));
}
} finally {
setIsLoggingIn(false);
}
};
if (isLoadingInfo || authLoading) {
return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--color-background, #fafafa)' }}>
<div className="min-h-screen flex items-center justify-center">
<Loading size="lg" text={t('gallery.loading')} />
</div>
</div>
);
}
if (infoError || !galleryInfo) {
return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--color-background, #fafafa)' }}>
<div className="min-h-screen flex flex-col">
{brandLogo && (
<div className="p-8 text-center">
<img
src={buildResourceUrl(brandLogo)}
alt={settingsData.branding_company_name || 'Company Logo'}
className="h-16 w-auto object-contain mx-auto"
/>
</div>
)}
<div className="flex-1 flex items-center justify-center">
<Card className="max-w-md w-full mx-4">
<CardContent className="text-center py-12">
<AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">{t('errors.galleryNotFound')}</h2>
<p className="text-neutral-600">{t('errors.galleryNotFoundMessage')}</p>
</CardContent>
</Card>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--color-background, #fafafa)' }}>
<div className="min-h-screen flex flex-col">
{/* Logo */}
{brandLogo && (
<div className="p-8 text-center">
<img
src={buildResourceUrl(brandLogo)}
alt={settingsData.branding_company_name || 'Company Logo'}
className="h-16 w-auto object-contain mx-auto"
/>
</div>
)}
<div className="flex-1 flex items-center justify-center px-4">
<Card className="max-w-md w-full">
<CardContent className="p-8">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-amber-100 dark:bg-amber-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
<Lock className="w-8 h-8 text-amber-600 dark:text-amber-400" />
</div>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{t('clientAccess.title')}
</h1>
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-2">
{galleryInfo.event_name}
</p>
<p className="text-xs text-neutral-500 dark:text-neutral-500 mt-1">
{t('clientAccess.description')}
</p>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<Input
type="password"
label={t('clientAccess.pinLabel')}
placeholder={t('clientAccess.pinPlaceholder')}
value={pin}
onChange={(e) => {
setPin(e.target.value);
setLoginError(null);
}}
error={loginError || undefined}
leftIcon={<Lock className="w-5 h-5" />}
autoFocus
/>
<Button
type="submit"
variant="primary"
className="w-full"
isLoading={isLoggingIn}
disabled={isLoggingIn}
>
{t('clientAccess.loginButton')}
</Button>
</form>
<div className="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-700 text-center">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{t('clientAccess.guestHint')}{' '}
<Link
to={`/gallery/${slug}`}
className="text-primary-600 dark:text-primary-400 hover:underline"
>
{t('clientAccess.guestLink')}
</Link>
</p>
</div>
</CardContent>
</Card>
</div>
{/* Footer */}
<div className="p-8 text-center">
<div className="flex items-center justify-center gap-4">
<Link
to="/impressum"
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors"
>
{t('legal.impressum')}
</Link>
<span className="text-xs text-neutral-400">|</span>
<Link
to="/datenschutz"
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors"
>
{t('legal.datenschutz')}
</Link>
</div>
<p className="text-xs mt-2 text-neutral-500">
Powered by <span className="font-semibold">PicPeak</span>
</p>
</div>
</div>
</div>
);
};