Refine guest upload camera UI
This commit is contained in:
@@ -127,14 +127,18 @@ export default function BottomNav() {
|
|||||||
<Link
|
<Link
|
||||||
to={`${base}/upload`}
|
to={`${base}/upload`}
|
||||||
aria-label={labels.upload}
|
aria-label={labels.upload}
|
||||||
className={`relative flex ${compact ? 'h-12 w-12' : 'h-16 w-16'} items-center justify-center rounded-full text-white shadow-2xl transition ${
|
className={`relative flex ${compact ? 'h-12 w-12' : 'h-16 w-16'} items-center justify-center rounded-full text-white shadow-2xl transition-all duration-300 ${
|
||||||
isUploadActive ? 'scale-105' : 'hover:scale-105'
|
isUploadActive
|
||||||
|
? 'translate-y-6 scale-75 opacity-0 pointer-events-none'
|
||||||
|
: 'hover:scale-105'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
background: `radial-gradient(circle at 20% 20%, ${branding.secondaryColor}, ${branding.primaryColor})`,
|
background: `radial-gradient(circle at 20% 20%, ${branding.secondaryColor}, ${branding.primaryColor})`,
|
||||||
boxShadow: `0 20px 35px ${branding.primaryColor}44`,
|
boxShadow: `0 20px 35px ${branding.primaryColor}44`,
|
||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
}}
|
}}
|
||||||
|
tabIndex={isUploadActive ? -1 : 0}
|
||||||
|
aria-hidden={isUploadActive}
|
||||||
>
|
>
|
||||||
<Camera className="h-6 w-6" aria-hidden />
|
<Camera className="h-6 w-6" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, butt
|
|||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
className="relative rounded-full bg-white/15 p-2 text-current transition hover:bg-white/30"
|
className="relative flex h-10 w-10 items-center justify-center rounded-2xl border border-white/25 bg-white/15 text-current shadow-lg shadow-black/20 backdrop-blur transition hover:border-white/40 hover:bg-white/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60"
|
||||||
aria-label={open ? t('header.notifications.close', 'Benachrichtigungen schließen') : t('header.notifications.open', 'Benachrichtigungen anzeigen')}
|
aria-label={open ? t('header.notifications.close', 'Benachrichtigungen schließen') : t('header.notifications.open', 'Benachrichtigungen anzeigen')}
|
||||||
>
|
>
|
||||||
<Bell className="h-5 w-5" aria-hidden />
|
<Bell className="h-5 w-5" aria-hidden />
|
||||||
|
|||||||
@@ -121,7 +121,11 @@ export function SettingsSheet() {
|
|||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={handleOpenChange}>
|
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="h-9 w-9 rounded-md">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10 rounded-2xl border border-white/25 bg-white/15 text-current shadow-lg shadow-black/20 backdrop-blur transition hover:border-white/40 hover:bg-white/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60 dark:border-white/10 dark:bg-white/10 dark:hover:bg-white/15"
|
||||||
|
>
|
||||||
<Settings className="h-5 w-5" />
|
<Settings className="h-5 w-5" />
|
||||||
<span className="sr-only">{t('settings.sheet.openLabel')}</span>
|
<span className="sr-only">{t('settings.sheet.openLabel')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
Check,
|
||||||
Camera,
|
Camera,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Grid3X3,
|
Grid3X3,
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
Timer,
|
Timer,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
FlipHorizontal,
|
FlipHorizontal,
|
||||||
|
X,
|
||||||
Zap,
|
Zap,
|
||||||
ZapOff,
|
ZapOff,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -1237,7 +1239,11 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
|||||||
const countdownDegrees = Math.round(countdownProgress * 360);
|
const countdownDegrees = Math.round(countdownProgress * 360);
|
||||||
const controlIconButtonBase =
|
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';
|
'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';
|
||||||
const cameraControlsInset = 'calc(env(safe-area-inset-bottom, 0px) + 72px)';
|
const actionIconButtonBase =
|
||||||
|
'flex h-14 w-14 items-center justify-center rounded-2xl border border-white/25 bg-white/10 text-white shadow-lg shadow-black/25 backdrop-blur transition hover:border-white/40 hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60 disabled:pointer-events-none disabled:opacity-50';
|
||||||
|
const actionDockClass =
|
||||||
|
'rounded-[28px] border border-white/15 bg-white/10 p-4 shadow-2xl backdrop-blur';
|
||||||
|
const cameraControlsInset = 'calc(env(safe-area-inset-bottom, 0px) + 104px)';
|
||||||
|
|
||||||
return renderWithDialog(
|
return renderWithDialog(
|
||||||
<>
|
<>
|
||||||
@@ -1398,45 +1404,6 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
|||||||
<span className="sr-only">{t('upload.controls.toggleMirror')}</span>
|
<span className="sr-only">{t('upload.controls.toggleMirror')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className={cn(
|
|
||||||
controlIconButtonBase,
|
|
||||||
preferences.flashPreferred && 'border-white bg-white text-black'
|
|
||||||
)}
|
|
||||||
onClick={handleToggleFlashPreference}
|
|
||||||
disabled={preferences.facingMode !== 'environment'}
|
|
||||||
title={t('upload.controls.toggleFlash')}
|
|
||||||
aria-pressed={preferences.flashPreferred}
|
|
||||||
>
|
|
||||||
{preferences.flashPreferred ? <Zap className="h-4 w-4 text-yellow-300" /> : <ZapOff className="h-4 w-4" />}
|
|
||||||
<span className="sr-only">{t('upload.controls.toggleFlash')}</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className={cn(
|
|
||||||
controlIconButtonBase,
|
|
||||||
immersiveMode && 'border-white bg-white text-black'
|
|
||||||
)}
|
|
||||||
onClick={handleToggleImmersive}
|
|
||||||
title={
|
|
||||||
immersiveMode
|
|
||||||
? t('upload.controls.exitFullscreen', 'Menü einblenden')
|
|
||||||
: t('upload.controls.enterFullscreen', 'Vollbild')
|
|
||||||
}
|
|
||||||
aria-pressed={immersiveMode}
|
|
||||||
>
|
|
||||||
<Menu className="h-4 w-4" />
|
|
||||||
<span className="sr-only">
|
|
||||||
{immersiveMode
|
|
||||||
? t('upload.controls.exitFullscreen', 'Menü einblenden')
|
|
||||||
: t('upload.controls.enterFullscreen', 'Vollbild')}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1495,26 +1462,16 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-8">
|
<div className="flex flex-col gap-4">
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="flex h-14 w-14 items-center justify-center rounded-full border border-white/25 bg-white/10 text-white shadow-sm backdrop-blur hover:border-white/40 hover:bg-white/15"
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
disabled={demoReadOnly}
|
|
||||||
>
|
|
||||||
<ImagePlus className="h-6 w-6" />
|
|
||||||
<span className="sr-only">{t('upload.galleryButton')}</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{mode === 'review' && reviewPhoto ? (
|
{mode === 'review' && reviewPhoto ? (
|
||||||
<div className="flex w-full max-w-md flex-col gap-3">
|
<div className="flex w-full max-w-lg flex-col gap-3 self-center">
|
||||||
{uploadsRequireApproval ? (
|
{uploadsRequireApproval ? (
|
||||||
<div className="rounded-xl border border-amber-300/70 bg-amber-50/80 p-3 text-amber-900 shadow-sm backdrop-blur dark:border-amber-400/40 dark:bg-amber-500/10 dark:text-amber-50">
|
<div className="rounded-2xl border border-amber-300/70 bg-amber-50/80 p-4 text-amber-900 shadow-lg backdrop-blur dark:border-amber-400/40 dark:bg-amber-500/10 dark:text-amber-50">
|
||||||
<p className="text-sm font-semibold">{t('upload.review.noticeTitle', 'Uploads werden geprüft')}</p>
|
<p className="text-sm font-semibold">{t('upload.review.noticeTitle', 'Uploads werden geprüft')}</p>
|
||||||
<p className="text-xs">{t('upload.review.noticeBody', 'Dein Foto erscheint, sobald es freigegeben wurde.')}</p>
|
<p className="text-xs">{t('upload.review.noticeBody', 'Dein Foto erscheint, sobald es freigegeben wurde.')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="rounded-xl border border-white/20 bg-white/10 p-3 text-white shadow-sm backdrop-blur">
|
<div className="rounded-2xl border border-white/20 bg-white/10 p-4 text-white shadow-lg backdrop-blur">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold">{t('upload.liveShow.title', 'Live-Show')}</p>
|
<p className="text-sm font-semibold">{t('upload.liveShow.title', 'Live-Show')}</p>
|
||||||
@@ -1534,65 +1491,136 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
|||||||
: t('upload.liveShow.reviewed', 'Wird nach Freigabe für die Live-Show angezeigt.')}
|
: t('upload.liveShow.reviewed', 'Wird nach Freigabe für die Live-Show angezeigt.')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 sm:flex-row">
|
<div className="grid grid-cols-[1fr_auto] items-stretch gap-3">
|
||||||
<Button variant="secondary" className="flex-1" onClick={handleRetake}>
|
|
||||||
{t('upload.review.retake')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
className="flex-1 animate-pulse bg-pink-500 text-white shadow-lg hover:bg-pink-600 focus-visible:ring-pink-300"
|
className="flex h-16 items-center justify-center gap-2 rounded-2xl border border-emerald-200/60 bg-emerald-500/90 text-base font-semibold text-white shadow-lg shadow-emerald-900/20 transition hover:bg-emerald-500"
|
||||||
onClick={handleUsePhoto}
|
onClick={handleUsePhoto}
|
||||||
>
|
>
|
||||||
{t('upload.review.keep')}
|
<Check className="h-5 w-5" />
|
||||||
|
<span>{t('upload.review.keep')}</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="flex h-16 w-16 items-center justify-center rounded-2xl border border-rose-400/60 bg-rose-500/10 text-rose-100 shadow-lg shadow-rose-900/30 backdrop-blur transition hover:bg-rose-500/20"
|
||||||
|
onClick={handleRetake}
|
||||||
|
title={t('upload.review.retake')}
|
||||||
|
>
|
||||||
|
<X className="h-6 w-6" />
|
||||||
|
<span className="sr-only">{t('upload.review.retake')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative flex h-24 w-24 items-center justify-center">
|
<div className={actionDockClass}>
|
||||||
{!isCountdownActive && mode !== 'uploading' && (
|
<div className="grid grid-cols-[1fr_auto_1fr] items-end gap-4">
|
||||||
<span className="pointer-events-none absolute inset-0 rounded-full border border-white/30 opacity-60 animate-ping" />
|
<div className="flex items-center justify-start">
|
||||||
)}
|
<Button
|
||||||
{isCountdownActive && (
|
size="icon"
|
||||||
<div
|
variant="ghost"
|
||||||
className="absolute inset-1 rounded-full"
|
className={cn(
|
||||||
style={{
|
actionIconButtonBase,
|
||||||
background: `conic-gradient(#fff ${countdownDegrees}deg, rgba(255,255,255,0.12) ${countdownDegrees}deg)`,
|
preferences.flashPreferred && 'border-yellow-300/70 bg-yellow-400/20 text-yellow-100'
|
||||||
}}
|
)}
|
||||||
/>
|
onClick={handleToggleFlashPreference}
|
||||||
)}
|
disabled={demoReadOnly || preferences.facingMode !== 'environment'}
|
||||||
<div className="absolute inset-2 rounded-full bg-black/70 shadow-[0_10px_40px_rgba(0,0,0,0.45)] backdrop-blur" />
|
title={t('upload.controls.toggleFlash')}
|
||||||
<Button
|
aria-pressed={preferences.flashPreferred}
|
||||||
size="lg"
|
>
|
||||||
className="relative z-10 flex h-20 w-20 items-center justify-center rounded-full border-4 border-white/40 text-white shadow-2xl"
|
{preferences.flashPreferred ? (
|
||||||
onClick={handlePrimaryAction}
|
<Zap className="h-6 w-6 text-yellow-200" />
|
||||||
disabled={demoReadOnly || mode === 'uploading' || isCountdownActive}
|
) : (
|
||||||
style={{
|
<ZapOff className="h-6 w-6" />
|
||||||
background: `radial-gradient(circle at 20% 20%, ${branding.secondaryColor}, ${branding.primaryColor})`,
|
)}
|
||||||
boxShadow: `0 18px 36px ${branding.primaryColor}55`,
|
<span className="sr-only">{t('upload.controls.toggleFlash')}</span>
|
||||||
}}
|
</Button>
|
||||||
>
|
</div>
|
||||||
{isCountdownActive ? (
|
|
||||||
<span className="text-3xl font-bold leading-none">{countdownValue}</span>
|
<div className="relative flex h-28 w-28 items-center justify-center">
|
||||||
) : mode === 'uploading' ? (
|
{!isCountdownActive && mode !== 'uploading' && (
|
||||||
<Loader2 className="h-9 w-9 animate-spin" />
|
<span className="pointer-events-none absolute inset-0 rounded-full border border-white/30 opacity-60 animate-ping" />
|
||||||
) : (
|
)}
|
||||||
<Camera className="h-8 w-8 sm:h-10 sm:w-10" style={{ height: '32px', width: '32px' }} />
|
{isCountdownActive && (
|
||||||
)}
|
<div
|
||||||
<span className="sr-only">
|
className="absolute inset-1 rounded-full"
|
||||||
{isCameraActive ? t('upload.captureButton') : t('upload.buttons.startCamera')}
|
style={{
|
||||||
</span>
|
background: `conic-gradient(#fff ${countdownDegrees}deg, rgba(255,255,255,0.12) ${countdownDegrees}deg)`,
|
||||||
</Button>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="absolute inset-2 rounded-full bg-black/70 shadow-[0_16px_44px_rgba(0,0,0,0.5)] backdrop-blur" />
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="relative z-10 flex h-24 w-24 items-center justify-center rounded-full border-4 border-white/40 text-white shadow-2xl"
|
||||||
|
onClick={handlePrimaryAction}
|
||||||
|
disabled={demoReadOnly || mode === 'uploading' || isCountdownActive}
|
||||||
|
style={{
|
||||||
|
background: `radial-gradient(circle at 20% 20%, ${branding.secondaryColor}, ${branding.primaryColor})`,
|
||||||
|
boxShadow: `0 22px 44px ${branding.primaryColor}66`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isCountdownActive ? (
|
||||||
|
<span className="text-4xl font-bold leading-none">{countdownValue}</span>
|
||||||
|
) : mode === 'uploading' ? (
|
||||||
|
<Loader2 className="h-10 w-10 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Camera className="h-12 w-12" />
|
||||||
|
)}
|
||||||
|
<span className="sr-only">
|
||||||
|
{isCameraActive ? t('upload.captureButton') : t('upload.buttons.startCamera')}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={actionIconButtonBase}
|
||||||
|
onClick={handleSwitchCamera}
|
||||||
|
disabled={demoReadOnly}
|
||||||
|
title={t('upload.switchCamera')}
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-6 w-6" />
|
||||||
|
<span className="sr-only">{t('upload.switchCamera')}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex items-center justify-between gap-3">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="flex h-12 items-center gap-2 rounded-2xl border border-white/20 bg-white/10 px-3 text-xs font-semibold text-white shadow-lg shadow-black/20 backdrop-blur transition hover:bg-white/20"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={demoReadOnly}
|
||||||
|
>
|
||||||
|
<ImagePlus className="h-4 w-4" />
|
||||||
|
<span className="sr-only sm:not-sr-only">{t('upload.galleryButton')}</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
actionIconButtonBase,
|
||||||
|
immersiveMode && 'border-white bg-white text-black'
|
||||||
|
)}
|
||||||
|
onClick={handleToggleImmersive}
|
||||||
|
title={
|
||||||
|
immersiveMode
|
||||||
|
? t('upload.controls.exitFullscreen', 'Menü einblenden')
|
||||||
|
: t('upload.controls.enterFullscreen', 'Vollbild')
|
||||||
|
}
|
||||||
|
aria-pressed={immersiveMode}
|
||||||
|
>
|
||||||
|
<Menu className="h-5 w-5" />
|
||||||
|
<span className="sr-only">
|
||||||
|
{immersiveMode
|
||||||
|
? t('upload.controls.exitFullscreen', 'Menü einblenden')
|
||||||
|
: t('upload.controls.enterFullscreen', 'Vollbild')}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="flex h-14 w-14 items-center justify-center rounded-full border border-white/25 bg-white/10 text-white shadow-sm backdrop-blur hover:border-white/40 hover:bg-white/15"
|
|
||||||
onClick={handleSwitchCamera}
|
|
||||||
disabled={demoReadOnly}
|
|
||||||
>
|
|
||||||
<RotateCcw className="h-6 w-6" />
|
|
||||||
<span className="sr-only">{t('upload.switchCamera')}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user