referenzen auf "credits" entfernt. Kamera-Seite schicker gemacht
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import Header from '../components/Header';
|
||||
import BottomNav from '../components/BottomNav';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
@@ -19,6 +17,7 @@ import { cn } from '@/lib/utils';
|
||||
import {
|
||||
AlertTriangle,
|
||||
Camera,
|
||||
ChevronDown,
|
||||
Grid3X3,
|
||||
ImagePlus,
|
||||
Info,
|
||||
@@ -109,7 +108,7 @@ export default function UploadPage() {
|
||||
const eventKey = token ?? '';
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { markCompleted, completedCount } = useGuestTaskProgress(token);
|
||||
const { markCompleted } = useGuestTaskProgress(token);
|
||||
const { t, locale } = useTranslation();
|
||||
const stats = useEventStats();
|
||||
|
||||
@@ -137,7 +136,8 @@ const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
const [uploadWarning, setUploadWarning] = useState<string | null>(null);
|
||||
|
||||
const [errorDialog, setErrorDialog] = useState<UploadErrorDialog | null>(null);
|
||||
const [errorDialog, setErrorDialog] = useState<UploadErrorDialog | null>(null);
|
||||
const [taskDetailsExpanded, setTaskDetailsExpanded] = useState(false);
|
||||
|
||||
const [eventPackage, setEventPackage] = useState<EventPackage | null>(null);
|
||||
const [canUpload, setCanUpload] = useState(true);
|
||||
@@ -616,10 +616,13 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
reader.readAsDataURL(file);
|
||||
}, [canUpload, t]);
|
||||
|
||||
const handleOpenInspiration = useCallback(() => {
|
||||
if (!eventKey) return;
|
||||
navigate(`/e/${encodeURIComponent(eventKey)}/gallery`);
|
||||
}, [eventKey, navigate]);
|
||||
const emotionLabel = useMemo(() => {
|
||||
if (task?.emotion?.name) return task.emotion.name;
|
||||
if (emotionSlug) {
|
||||
return emotionSlug.replace('-', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
return t('upload.hud.moodFallback');
|
||||
}, [emotionSlug, t, task?.emotion?.name]);
|
||||
|
||||
const difficultyBadgeClass = useMemo(() => {
|
||||
if (!task) return 'text-white';
|
||||
@@ -641,6 +644,27 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
[stats.latestPhotoAt, t],
|
||||
);
|
||||
|
||||
const socialChips = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'online',
|
||||
label: t('upload.hud.cards.online'),
|
||||
value: stats.onlineGuests > 0 ? `${stats.onlineGuests}` : '0',
|
||||
},
|
||||
{
|
||||
id: 'emotion',
|
||||
label: t('upload.taskInfo.emotion').replace('{value}', emotionLabel),
|
||||
value: t('upload.hud.moodLabel').replace('{mood}', emotionLabel),
|
||||
},
|
||||
{
|
||||
id: 'last-upload',
|
||||
label: t('upload.hud.cards.lastUpload'),
|
||||
value: relativeLastUpload,
|
||||
},
|
||||
],
|
||||
[emotionLabel, relativeLastUpload, stats.onlineGuests, t],
|
||||
);
|
||||
|
||||
useEffect(() => () => {
|
||||
resetCountdownTimer();
|
||||
if (uploadProgressTimerRef.current) {
|
||||
@@ -648,7 +672,64 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
}
|
||||
}, [resetCountdownTimer]);
|
||||
|
||||
const heroEmotion = task?.emotion?.name ?? t('upload.hud.moodFallback');
|
||||
useEffect(() => {
|
||||
setTaskDetailsExpanded(false);
|
||||
}, [task?.id]);
|
||||
|
||||
const handlePrimaryAction = useCallback(() => {
|
||||
if (!isCameraActive) {
|
||||
startCamera();
|
||||
return;
|
||||
}
|
||||
beginCapture();
|
||||
}, [beginCapture, isCameraActive, startCamera]);
|
||||
|
||||
const taskFloatingCard = showTaskOverlay && task ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTaskDetailsExpanded((prev) => !prev)}
|
||||
className="absolute left-6 right-6 top-0 z-30 -translate-y-1/2 rounded-3xl border border-white/40 bg-black/70 p-4 text-left text-white shadow-2xl backdrop-blur transition hover:bg-black/80 focus:outline-none focus:ring-2 focus:ring-white/60"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="secondary" className="flex items-center gap-2 rounded-full text-[11px]">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{t('upload.taskInfo.badge').replace('{id}', `${task.id}`)}
|
||||
</Badge>
|
||||
<span className={cn('text-xs font-semibold uppercase tracking-wide', difficultyBadgeClass)}>
|
||||
{t(`upload.taskInfo.difficulty.${task.difficulty ?? 'medium'}`)}
|
||||
</span>
|
||||
<span className="ml-auto flex items-center text-xs uppercase tracking-wide text-white/70">
|
||||
{emotionLabel}
|
||||
<ChevronDown
|
||||
className={cn('ml-1 h-3.5 w-3.5 transition', taskDetailsExpanded ? 'rotate-180' : 'rotate-0')}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm font-semibold leading-snug">{task.title}</p>
|
||||
{taskDetailsExpanded ? (
|
||||
<div className="mt-2 space-y-2 text-xs text-white/80">
|
||||
<p>{task.description}</p>
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px]">
|
||||
{task.instructions && (
|
||||
<span>
|
||||
{t('upload.taskInfo.instructionsPrefix')}: {task.instructions}
|
||||
</span>
|
||||
)}
|
||||
{preferences.countdownEnabled && (
|
||||
<span className="rounded-full border border-white/20 px-2 py-0.5">
|
||||
{t('upload.countdownLabel').replace('{seconds}', `${preferences.countdownSeconds}`)}
|
||||
</span>
|
||||
)}
|
||||
{emotionLabel && (
|
||||
<span className="rounded-full border border-white/20 px-2 py-0.5">
|
||||
{t('upload.taskInfo.emotion').replace('{value}', emotionLabel)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
const limitStatusSection = limitCards.length > 0 ? (
|
||||
<section className="space-y-4 rounded-[28px] border border-white/20 bg-white/80 p-5 shadow-lg backdrop-blur dark:border-white/10 dark:bg-slate-900/60">
|
||||
@@ -735,28 +816,11 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
|
||||
const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[120px]') => (
|
||||
<>
|
||||
<div className="pb-16">
|
||||
<Header eventToken={eventKey} title={t('upload.cameraTitle')} />
|
||||
<main className="px-4">
|
||||
<div className={wrapperClassName}>{content}</div>
|
||||
</main>
|
||||
<BottomNav />
|
||||
</div>
|
||||
<div className={wrapperClassName}>{content}</div>
|
||||
{errorDialogNode}
|
||||
</>
|
||||
);
|
||||
|
||||
if (!supportsCamera && !task) {
|
||||
return renderWithDialog(
|
||||
<>
|
||||
{limitStatusSection}
|
||||
<Alert>
|
||||
<AlertDescription>{t('upload.cameraUnsupported.message')}</AlertDescription>
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (loadingTask) {
|
||||
return renderWithDialog(
|
||||
<div className="flex flex-col items-center justify-center gap-4 text-center">
|
||||
@@ -804,35 +868,59 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
|
||||
const renderPermissionNotice = () => {
|
||||
if (permissionState === 'granted') return null;
|
||||
if (permissionState === 'unsupported') {
|
||||
return (
|
||||
<Alert className="rounded-[24px] border border-amber-200 bg-amber-50/70 text-amber-900 dark:border-amber-400/40 dark:bg-amber-500/10 dark:text-amber-50">
|
||||
<AlertDescription>{t('upload.cameraUnsupported.message')}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
if (permissionState === 'denied' || permissionState === 'error') {
|
||||
return (
|
||||
<Alert variant="destructive" className="rounded-[24px] border border-rose-200 bg-rose-50/80 text-rose-900 dark:border-rose-500/40 dark:bg-rose-500/15 dark:text-rose-50">
|
||||
<AlertDescription className="space-y-3">
|
||||
<div>{permissionMessage}</div>
|
||||
<Button size="sm" variant="outline" onClick={startCamera}>
|
||||
{t('upload.buttons.tryAgain')}
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const titles: Record<PermissionState, string> = {
|
||||
idle: t('upload.cameraDenied.title'),
|
||||
prompt: t('upload.cameraDenied.title'),
|
||||
granted: '',
|
||||
denied: t('upload.cameraDenied.title'),
|
||||
error: t('upload.cameraError.title'),
|
||||
unsupported: t('upload.cameraUnsupported.title'),
|
||||
};
|
||||
|
||||
const fallbackMessages: Record<PermissionState, string> = {
|
||||
idle: t('upload.cameraDenied.prompt'),
|
||||
prompt: t('upload.cameraDenied.prompt'),
|
||||
granted: '',
|
||||
denied: t('upload.cameraDenied.explanation'),
|
||||
error: t('upload.cameraError.explanation'),
|
||||
unsupported: t('upload.cameraUnsupported.message'),
|
||||
};
|
||||
|
||||
const title = titles[permissionState];
|
||||
const description = permissionMessage ?? fallbackMessages[permissionState];
|
||||
const canRetryCamera = permissionState !== 'unsupported';
|
||||
|
||||
return (
|
||||
<Alert className="rounded-[24px] border border-slate-200 bg-white/80 text-slate-800 dark:border-white/10 dark:bg-white/5 dark:text-white">
|
||||
<AlertDescription>{t('upload.cameraDenied.prompt')}</AlertDescription>
|
||||
</Alert>
|
||||
<div className="rounded-[32px] border border-white/15 bg-white/85 p-5 text-slate-900 shadow-lg dark:border-white/10 dark:bg-white/5 dark:text-white">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-slate-900/10 dark:bg-white/10">
|
||||
<Camera className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold">{title}</p>
|
||||
<p className="text-xs text-slate-600 dark:text-white/70">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
{canRetryCamera && (
|
||||
<Button onClick={startCamera} size="sm">
|
||||
{t('upload.buttons.startCamera')}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="secondary" size="sm" onClick={() => fileInputRef.current?.click()}>
|
||||
{t('upload.galleryButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return renderWithDialog(
|
||||
<>
|
||||
<section className="relative overflow-hidden rounded-[32px] border border-white/10 bg-black text-white shadow-2xl">
|
||||
<div className="relative pt-8">
|
||||
{taskFloatingCard}
|
||||
<section className="relative overflow-hidden rounded-[32px] border border-white/10 bg-black text-white shadow-2xl">
|
||||
<div className="relative aspect-[3/4] sm:aspect-video">
|
||||
<video
|
||||
ref={videoRef}
|
||||
@@ -857,57 +945,13 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
)}
|
||||
|
||||
{!isCameraActive && (
|
||||
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-black/70 text-center text-sm">
|
||||
<Camera className="mb-3 h-8 w-8 text-pink-400" />
|
||||
<p className="max-w-xs text-white/90">
|
||||
{t('upload.cameraInactive').replace(
|
||||
'{hint}',
|
||||
(permissionMessage ?? t('upload.cameraInactiveHint').replace('{label}', t('upload.buttons.startCamera')))
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Button size="sm" onClick={startCamera}>
|
||||
{t('upload.buttons.startCamera')}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => fileInputRef.current?.click()}>
|
||||
{t('upload.galleryButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showTaskOverlay && task && (
|
||||
<div className="absolute left-3 right-3 top-3 z-30 flex flex-col gap-2 rounded-xl border border-white/15 bg-black/40 p-3 backdrop-blur-sm">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Badge variant="secondary" className="flex items-center gap-2 text-xs">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{t('upload.taskInfo.badge').replace('{id}', `${task.id}`)}
|
||||
</Badge>
|
||||
<span className={cn('text-xs font-medium uppercase tracking-wide', difficultyBadgeClass)}>
|
||||
{t(`upload.taskInfo.difficulty.${task.difficulty ?? 'medium'}`)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold leading-tight">{task.title}</h1>
|
||||
<p className="mt-1 text-xs leading-relaxed text-white/80">{task.description}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] text-white/70">
|
||||
{task.instructions && (
|
||||
<span>
|
||||
{t('upload.taskInfo.instructionsPrefix')}: {task.instructions}
|
||||
</span>
|
||||
)}
|
||||
{emotionSlug && (
|
||||
<span className="rounded-full border border-white/20 px-2 py-0.5">
|
||||
{t('upload.taskInfo.emotion').replace('{value}', `${task.emotion?.name || emotionSlug}`)}
|
||||
</span>
|
||||
)}
|
||||
{preferences.countdownEnabled && (
|
||||
<span className="rounded-full border border-white/20 px-2 py-0.5">
|
||||
{t('upload.countdownLabel').replace('{seconds}', `${preferences.countdownSeconds}`)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute left-4 top-4 z-20 flex items-center gap-2 rounded-full bg-black/70 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white">
|
||||
<Camera className="h-4 w-4 text-pink-400" />
|
||||
<span>
|
||||
{permissionState === 'unsupported'
|
||||
? t('upload.cameraUnsupported.title')
|
||||
: t('upload.cameraDenied.title')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -940,7 +984,7 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative z-30 flex flex-col gap-3 bg-gradient-to-t from-black via-black/80 to-transparent p-4">
|
||||
<div className="relative z-30 flex flex-col gap-4 bg-gradient-to-t from-black via-black/80 to-transparent p-4">
|
||||
{uploadWarning && (
|
||||
<Alert className="border-yellow-400/20 bg-yellow-500/10 text-white">
|
||||
<AlertDescription className="text-xs">
|
||||
@@ -957,72 +1001,67 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap justify-between gap-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
variant={preferences.gridEnabled ? 'default' : 'secondary'}
|
||||
className="h-10 w-10 rounded-full bg-white/15 text-white"
|
||||
onClick={handleToggleGrid}
|
||||
>
|
||||
<Grid3X3 className="h-5 w-5" />
|
||||
<span className="sr-only">{t('upload.controls.toggleGrid')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant={preferences.countdownEnabled ? 'default' : 'secondary'}
|
||||
className="h-10 w-10 rounded-full bg-white/15 text-white"
|
||||
onClick={handleToggleCountdown}
|
||||
>
|
||||
<span className="text-sm font-semibold">{preferences.countdownSeconds}s</span>
|
||||
<span className="sr-only">{t('upload.controls.toggleCountdown')}</span>
|
||||
</Button>
|
||||
{preferences.facingMode === 'user' && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant={preferences.mirrorFrontPreview ? 'default' : 'secondary'}
|
||||
className="h-10 w-10 rounded-full bg-white/15 text-white"
|
||||
onClick={handleToggleMirror}
|
||||
>
|
||||
<span className="text-sm font-semibold">?</span>
|
||||
<span className="sr-only">{t('upload.controls.toggleMirror')}</span>
|
||||
</Button>
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 text-xs font-medium uppercase tracking-wide text-white/80">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={preferences.gridEnabled ? 'default' : 'secondary'}
|
||||
className={cn(
|
||||
'rounded-full border-white/30 bg-white/10 px-4 py-1 text-xs',
|
||||
preferences.gridEnabled && 'bg-white text-black'
|
||||
)}
|
||||
onClick={handleToggleGrid}
|
||||
>
|
||||
<Grid3X3 className="mr-1 h-3.5 w-3.5" />
|
||||
{t('upload.controls.toggleGrid')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={preferences.countdownEnabled ? 'default' : 'secondary'}
|
||||
className={cn(
|
||||
'rounded-full border-white/30 bg-white/10 px-4 py-1 text-xs',
|
||||
preferences.countdownEnabled && 'bg-white text-black'
|
||||
)}
|
||||
onClick={handleToggleCountdown}
|
||||
>
|
||||
{t('upload.countdownLabel').replace('{seconds}', `${preferences.countdownSeconds}`)}
|
||||
</Button>
|
||||
{preferences.facingMode === 'user' && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant={preferences.flashPreferred ? 'default' : 'secondary'}
|
||||
className="h-10 w-10 rounded-full bg-white/15 text-white"
|
||||
onClick={handleToggleFlashPreference}
|
||||
disabled={preferences.facingMode !== 'environment'}
|
||||
>
|
||||
{preferences.flashPreferred ? <Zap className="h-5 w-5 text-yellow-300" /> : <ZapOff className="h-5 w-5" />}
|
||||
<span className="sr-only">{t('upload.controls.toggleFlash')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="rounded-full border-white/30 bg-white/10 text-white"
|
||||
onClick={handleSwitchCamera}
|
||||
variant={preferences.mirrorFrontPreview ? 'default' : 'secondary'}
|
||||
className={cn(
|
||||
'rounded-full border-white/30 bg-white/10 px-4 py-1 text-xs',
|
||||
preferences.mirrorFrontPreview && 'bg-white text-black'
|
||||
)}
|
||||
onClick={handleToggleMirror}
|
||||
>
|
||||
<RotateCcw className="mr-1 h-4 w-4" />
|
||||
{t('upload.switchCamera')}
|
||||
{t('upload.controls.toggleMirror')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="rounded-full border-white/30 bg-white/10 text-white"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<ImagePlus className="mr-1 h-4 w-4" />
|
||||
{t('upload.galleryButton')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant={preferences.flashPreferred ? 'default' : 'secondary'}
|
||||
className={cn(
|
||||
'rounded-full border-white/30 bg-white/10 px-4 py-1 text-xs',
|
||||
preferences.flashPreferred && 'bg-white text-black'
|
||||
)}
|
||||
onClick={handleToggleFlashPreference}
|
||||
disabled={preferences.facingMode !== 'environment'}
|
||||
>
|
||||
{preferences.flashPreferred ? <Zap className="mr-1 h-3.5 w-3.5 text-yellow-300" /> : <ZapOff className="mr-1 h-3.5 w-3.5" />}
|
||||
{t('upload.controls.toggleFlash')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex h-14 w-14 items-center justify-center rounded-full border border-white/30 bg-white/10 text-white"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<ImagePlus className="h-6 w-6" />
|
||||
<span className="sr-only">{t('upload.galleryButton')}</span>
|
||||
</Button>
|
||||
{mode === 'review' && reviewPhoto ? (
|
||||
<div className="flex w-full max-w-md flex-col gap-3 sm:flex-row">
|
||||
<Button variant="secondary" className="flex-1" onClick={handleRetake}>
|
||||
@@ -1035,17 +1074,42 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
) : (
|
||||
<Button
|
||||
size="lg"
|
||||
className="h-16 w-16 rounded-full border-4 border-white/40 bg-white/90 text-black shadow-xl"
|
||||
onClick={beginCapture}
|
||||
disabled={!isCameraActive || mode === 'countdown'}
|
||||
className="flex h-20 w-20 items-center justify-center rounded-full border-4 border-white/40 bg-white text-black shadow-2xl"
|
||||
onClick={handlePrimaryAction}
|
||||
disabled={mode === 'countdown' || mode === 'uploading'}
|
||||
>
|
||||
<Camera className="h-7 w-7" />
|
||||
<span className="sr-only">{t('upload.captureButton')}</span>
|
||||
<Camera className="h-8 w-8" />
|
||||
<span className="sr-only">
|
||||
{isCameraActive ? t('upload.captureButton') : t('upload.buttons.startCamera')}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex h-14 w-14 items-center justify-center rounded-full border border-white/30 bg-white/10 text-white"
|
||||
onClick={handleSwitchCamera}
|
||||
>
|
||||
<RotateCcw className="h-6 w-6" />
|
||||
<span className="sr-only">{t('upload.switchCamera')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{permissionState !== 'granted' && renderPermissionNotice()}
|
||||
{limitStatusSection}
|
||||
|
||||
Reference in New Issue
Block a user