photo visibility for demo events, hardened the demo mode. fixed dark/light mode toggle and notification bell toggle. fixed photo upload page sizes & header visibility.

This commit is contained in:
Codex Agent
2025-12-18 21:14:24 +01:00
parent 7c4067b32b
commit 53ec427e6e
25 changed files with 965 additions and 102 deletions

View File

@@ -41,6 +41,7 @@ import { compressPhoto, formatBytes } from '../lib/image';
import { useGuestIdentity } from '../context/GuestIdentityContext';
import { useEventData } from '../hooks/useEventData';
import { isTaskModeEnabled } from '../lib/engagement';
import { getDeviceId } from '../lib/device';
interface Task {
id: number;
@@ -130,6 +131,7 @@ export default function UploadPage() {
const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined;
const uploadsRequireApproval =
(event?.guest_upload_visibility as 'immediate' | 'review' | undefined) !== 'immediate';
const demoReadOnly = Boolean(event?.demo_read_only);
const taskIdParam = searchParams.get('task');
const emotionSlug = searchParams.get('emotion') || '';
@@ -154,9 +156,11 @@ export default function UploadPage() {
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadError, setUploadError] = useState<string | null>(null);
const [uploadWarning, setUploadWarning] = useState<string | null>(null);
const [immersiveMode, setImmersiveMode] = useState(false);
const [immersiveMode, setImmersiveMode] = useState(true);
const [showCelebration, setShowCelebration] = useState(false);
const [showHeroOverlay, setShowHeroOverlay] = useState(true);
const kpiChipsRef = useRef<HTMLDivElement | null>(null);
const navSentinelRef = useRef<HTMLDivElement | null>(null);
const [errorDialog, setErrorDialog] = useState<UploadErrorDialog | null>(null);
const [taskDetailsExpanded, setTaskDetailsExpanded] = useState(false);
@@ -172,15 +176,35 @@ const [canUpload, setCanUpload] = useState(true);
useEffect(() => {
if (typeof document === 'undefined') return undefined;
const className = 'guest-immersive';
if (immersiveMode) {
document.body.classList.add(className);
} else {
document.body.classList.remove(className);
}
document.body.classList.add(className);
document.body.classList.add('guest-nav-visible'); // show nav by default on upload page
return () => {
document.body.classList.remove(className);
document.body.classList.remove('guest-nav-visible');
};
}, [immersiveMode]);
}, []);
const updateNavVisibility = useCallback(() => {
if (typeof document === 'undefined') {
return;
}
// nav is always visible on upload page unless user explicitly toggles immersive off via button
document.body.classList.add('guest-nav-visible');
}, []);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
// ensure nav remains visible; hide only when immersive toggled off via the menu button
updateNavVisibility();
return () => {
document.body.classList.remove('guest-nav-visible');
};
}, [updateNavVisibility]);
const [showPrimer, setShowPrimer] = useState<boolean>(() => {
if (typeof window === 'undefined') return false;
@@ -330,6 +354,13 @@ const [canUpload, setCanUpload] = useState(true);
if (!eventKey) return;
const checkLimits = async () => {
if (demoReadOnly) {
setCanUpload(false);
setUploadError(t('upload.demoReadOnly', 'Uploads sind in der Demo deaktiviert.'));
setUploadWarning(null);
return;
}
try {
const pkg = await getEventPackage(eventKey);
setEventPackage(pkg);
@@ -374,7 +405,7 @@ const [canUpload, setCanUpload] = useState(true);
};
checkLimits();
}, [eventKey, t]);
}, [demoReadOnly, eventKey, t]);
const stopStream = useCallback(() => {
if (streamRef.current) {
@@ -1111,7 +1142,7 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
<>
<div
ref={cameraShellRef as unknown as React.RefObject<HTMLDivElement>}
className="relative flex min-h-screen flex-col gap-4 pb-[calc(env(safe-area-inset-bottom,0px)+12px)] pt-3"
className="relative flex min-h-screen flex-col gap-4 pb-[calc(env(safe-area-inset-bottom,0px)+72px)] pt-3"
style={bodyFont ? { fontFamily: bodyFont } : undefined}
>
{taskFloatingCard}
@@ -1130,9 +1161,9 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
ref={cameraViewportRef}
className="relative w-full"
style={{
height: 'calc(100vh - 160px)',
minHeight: '70vh',
maxHeight: '90vh',
height: 'clamp(60vh, calc(100vh - 220px), 82vh)',
minHeight: '60vh',
maxHeight: '88vh',
}}
>
<video
@@ -1402,17 +1433,20 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
</div>
{socialChips.length > 0 && (
<div className="mt-4 flex gap-3 overflow-x-auto pb-2">
{socialChips.map((chip) => (
<div
key={chip.id}
className="shrink-0 rounded-full border border-white/15 bg-white/80 px-4 py-2 text-xs font-semibold text-slate-800 shadow dark:border-white/10 dark:bg-white/10 dark:text-white"
>
<span className="block text-[10px] uppercase tracking-wide opacity-70">{chip.label}</span>
<span className="text-sm">{chip.value}</span>
</div>
))}
</div>
<>
<div ref={navSentinelRef} data-testid="nav-visibility-sentinel" className="h-px w-full" />
<div ref={kpiChipsRef} data-testid="upload-kpi-chips" className="mt-4 flex gap-3 overflow-x-auto pb-2">
{socialChips.map((chip) => (
<div
key={chip.id}
className="shrink-0 rounded-full border border-white/15 bg-white/80 px-4 py-2 text-xs font-semibold text-slate-800 shadow dark:border-white/10 dark:bg-white/10 dark:text-white"
>
<span className="block text-[10px] uppercase tracking-wide opacity-70">{chip.label}</span>
<span className="text-sm">{chip.value}</span>
</div>
))}
</div>
</>
)}
{permissionState !== 'granted' && renderPermissionNotice()}