From f4f40f7a0ceb1a24706f10bef37b20c1dfc0d05e Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 5 Dec 2025 18:44:11 +0100 Subject: [PATCH] =?UTF-8?q?=C3=BCberarbeitung=20der=20kamera-seite:=20deut?= =?UTF-8?q?lich=20aufger=C3=A4umter=20und=20mehr=20platz=20f=C3=BCr=20das?= =?UTF-8?q?=20Video/Bild?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 11 + package.json | 1 + resources/js/guest/pages/UploadPage.tsx | 355 ++++++++++++++---------- 3 files changed, 219 insertions(+), 148 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e9be4c..fe6a1ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@types/react": "^19.0.3", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.6.0", + "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -5544,6 +5545,16 @@ "node": ">=6" } }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", diff --git a/package.json b/package.json index e23aa2e..ba601c2 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@types/react": "^19.0.3", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.6.0", + "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/resources/js/guest/pages/UploadPage.tsx b/resources/js/guest/pages/UploadPage.tsx index 035cace..ca5c87e 100644 --- a/resources/js/guest/pages/UploadPage.tsx +++ b/resources/js/guest/pages/UploadPage.tsx @@ -20,15 +20,16 @@ import { Camera, ChevronDown, Grid3X3, + Menu, ImagePlus, Info, Loader2, RotateCcw, + Timer, Sparkles, + FlipHorizontal, Zap, ZapOff, - Maximize2, - Minimize2, } from 'lucide-react'; import { getEventPackage, type EventPackage } from '../services/eventApi'; import { useTranslation, type TranslateFn } from '../i18n/useTranslation'; @@ -147,7 +148,8 @@ export default function UploadPage() { const [uploadProgress, setUploadProgress] = useState(0); const [uploadError, setUploadError] = useState(null); const [uploadWarning, setUploadWarning] = useState(null); -const [immersiveMode, setImmersiveMode] = useState(false); +const [immersiveMode, setImmersiveMode] = useState(true); +const [showCelebration, setShowCelebration] = useState(false); const [errorDialog, setErrorDialog] = useState(null); const [taskDetailsExpanded, setTaskDetailsExpanded] = useState(false); @@ -193,6 +195,11 @@ const [canUpload, setCanUpload] = useState(true); return Number.isFinite(parsed) ? parsed : null; }, [taskIdParam]); + const tasksUrl = useMemo(() => { + if (!eventKey) return '/tasks'; + return `/e/${encodeURIComponent(eventKey)}/tasks`; + }, [eventKey]); + // Load preferences from storage useEffect(() => { if (typeof window === 'undefined') return; @@ -473,6 +480,25 @@ const [canUpload, setCanUpload] = useState(true); setPreferences((prev) => ({ ...prev, flashPreferred: !prev.flashPreferred })); }, []); + const triggerConfetti = useCallback(async () => { + if (typeof window === 'undefined') return; + const prefersReducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches; + if (prefersReducedMotion) return; + + try { + const { default: confetti } = await import('canvas-confetti'); + confetti({ + particleCount: 70, + spread: 65, + origin: { x: 0.5, y: 0.35 }, + ticks: 160, + scalar: 0.9, + }); + } catch (error) { + console.warn('Confetti could not start', error); + } + }, []); + const resetCountdownTimer = useCallback(() => { if (countdownTimerRef.current) { window.clearInterval(countdownTimerRef.current); @@ -638,6 +664,11 @@ const [canUpload, setCanUpload] = useState(true); if (task?.id) { markCompleted(task.id); } + setShowCelebration(true); + if (typeof window !== 'undefined') { + window.setTimeout(() => setShowCelebration(false), 1800); + } + void triggerConfetti(); try { const raw = localStorage.getItem('my-photo-ids'); const arr: number[] = raw ? JSON.parse(raw) : []; @@ -647,6 +678,9 @@ const [canUpload, setCanUpload] = useState(true); } catch (error) { console.warn('Failed to persist my-photo-ids', error); } + await new Promise((resolve) => { + window.setTimeout(resolve, 420); + }); stopStream(); navigateAfterUpload(photoId); } catch (error: unknown) { @@ -678,7 +712,7 @@ const [canUpload, setCanUpload] = useState(true); } finally { setStatusMessage(''); } - }, [emotionSlug, markCompleted, navigateAfterUpload, reviewPhoto, eventKey, stopStream, task, canUpload, t, identity.name]); + }, [emotionSlug, markCompleted, navigateAfterUpload, reviewPhoto, eventKey, stopStream, task, canUpload, t, identity.name, triggerConfetti]); const handleGalleryPick = useCallback(async (event: React.ChangeEvent) => { if (!canUpload) return; @@ -795,7 +829,7 @@ const [canUpload, setCanUpload] = useState(true); ) : null; - const limitStatusSection = limitCards.length > 0 ? ( -
-
-
-

- {t('upload.limitSummary.title')} -

-

- {t('upload.limitSummary.subtitle')} -

-
- - {t('upload.limitSummary.badgeLabel')} - -
-
- {limitCards.map((card) => { - const styles = LIMIT_CARD_STYLES[card.tone]; - return ( -
-
-
-

- {card.label} -

-

{card.valueLabel}

-
- - {card.badgeLabel} - -
- {card.progress !== null && ( -
-
-
- )} -

{card.description}

-
- ); - })} -
-
- ) : null; - const dialogToneIconClass: Record, string> = { danger: 'text-rose-500', warning: 'text-amber-500', @@ -939,17 +920,14 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[ if (!canUpload) { return renderWithDialog( - <> - {limitStatusSection} - - - - {t('upload.limitReached') - .replace('{used}', `${eventPackage?.used_photos || 0}`) - .replace('{max}', `${eventPackage?.package?.max_photos || 0}`)} - - - + + + + {t('upload.limitReached') + .replace('{used}', `${eventPackage?.used_photos || 0}`) + .replace('{max}', `${eventPackage?.package?.max_photos || 0}`)} + + ); } @@ -1035,12 +1013,20 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[ ); }; + const isCountdownActive = mode === 'countdown'; + const countdownProgress = preferences.countdownEnabled && preferences.countdownSeconds > 0 + ? Math.max(0, Math.min(1, countdownValue / preferences.countdownSeconds)) + : 0; + const countdownDegrees = Math.round(countdownProgress * 360); + const controlIconButtonBase = + 'flex h-10 w-10 items-center justify-center rounded-full border border-white/25 bg-white/10 text-white shadow-sm backdrop-blur transition hover:border-white/40 hover:bg-white/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60'; + return renderWithDialog( <> -
+
{taskFloatingCard}
@@ -1090,6 +1076,98 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
)} +
+
+ + + + + {preferences.facingMode === 'user' && ( + + )} + + + + +
+
+ {mode === 'uploading' && (
@@ -1126,106 +1204,88 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[ )} -
- - - {preferences.facingMode === 'user' && ( + {showCelebration && ( +
+
+ +

+ {t('upload.taskInfo.completed', 'Aufgabe gelöst!')} +

+
- )} - - -
+
+ )} -
+
+ {mode === 'review' && reviewPhoto ? (
-
) : ( - +
+ {!isCountdownActive && mode !== 'uploading' && ( + + )} + {isCountdownActive && ( +
+ )} +
+ +
)} +