feat: add gallery upload and fix mobile header issues

- Added 'Upload from Gallery' button to CameraCapture component
- Implemented secondary file input without 'capture' attribute to allow gallery selection on mobile
- Fixed overlapping header elements on mobile by making the header responsive
- Compacted 'Dram of the Day' button on small screens
- Added translations for the new gallery upload feature
This commit is contained in:
2025-12-18 15:48:11 +01:00
parent 960fa89fc1
commit 0f56c8b0f4
6 changed files with 62 additions and 34 deletions

View File

@@ -43,6 +43,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
}, [sessionId]);
const fileInputRef = useRef<HTMLInputElement>(null);
const galleryInputRef = useRef<HTMLInputElement>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
@@ -227,6 +228,10 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
fileInputRef.current?.click();
};
const triggerGallery = () => {
galleryInputRef.current?.click();
};
return (
<div className="flex flex-col items-center gap-4 md:gap-6 w-full max-w-md mx-auto p-4 md:p-6 bg-white dark:bg-zinc-900 rounded-3xl shadow-2xl border border-zinc-200 dark:border-zinc-800 transition-all hover:shadow-whisky-amber/20">
<h2 className="text-xl md:text-2xl font-bold text-zinc-800 dark:text-zinc-100 italic">{t('camera.magicShot')}</h2>
@@ -260,6 +265,14 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
className="hidden"
/>
<input
type="file"
accept="image/*"
ref={galleryInputRef}
onChange={handleCapture}
className="hidden"
/>
{lastSavedId ? (
<div className="flex flex-col gap-3 w-full animate-in zoom-in-95 duration-300">
<div className="flex items-center gap-2 text-green-600 font-bold justify-center p-2">
@@ -342,38 +355,50 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
{t('camera.toVault')}
</Link>
) : (
<button
onClick={isQueued ? () => setPreviewUrl(null) : (previewUrl && analysisResult ? handleSave : triggerUpload)}
disabled={isProcessing || isSaving}
className="w-full py-4 px-6 bg-amber-600 hover:bg-amber-700 text-white rounded-xl font-semibold flex items-center justify-center gap-2 transition-all active:scale-[0.98] shadow-lg shadow-amber-600/20 disabled:opacity-50"
>
{isSaving ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
{t('camera.saving')}
</>
) : isQueued ? (
<>
<CheckCircle2 size={20} />
{t('camera.nextBottle')}
</>
) : previewUrl && analysisResult ? (
<>
<CheckCircle2 size={20} />
{t('camera.inVault')}
</>
) : previewUrl ? (
<>
<Upload size={20} />
{t('camera.newPhoto')}
</>
) : (
<>
<Camera size={20} />
{t('camera.openingCamera')}
</>
<div className="flex flex-col gap-3 w-full">
<button
onClick={isQueued ? () => setPreviewUrl(null) : (previewUrl && analysisResult ? handleSave : triggerUpload)}
disabled={isProcessing || isSaving}
className="w-full py-4 px-6 bg-amber-600 hover:bg-amber-700 text-white rounded-xl font-semibold flex items-center justify-center gap-2 transition-all active:scale-[0.98] shadow-lg shadow-amber-600/20 disabled:opacity-50"
>
{isSaving ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
{t('camera.saving')}
</>
) : isQueued ? (
<>
<CheckCircle2 size={20} />
{t('camera.nextBottle')}
</>
) : previewUrl && analysisResult ? (
<>
<CheckCircle2 size={20} />
{t('camera.inVault')}
</>
) : previewUrl ? (
<>
<Upload size={20} />
{t('camera.newPhoto')}
</>
) : (
<>
<Camera size={20} />
{t('camera.openingCamera')}
</>
)}
</button>
{!previewUrl && !isProcessing && (
<button
onClick={triggerGallery}
className="w-full py-3 px-6 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300 rounded-xl font-bold flex items-center justify-center gap-2 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-200 transition-all text-sm"
>
<Upload size={18} />
{t('camera.uploadGallery')}
</button>
)}
</button>
</div>
)}
{error && (

View File

@@ -44,7 +44,7 @@ export default function DramOfTheDay({ bottles }: DramOfTheDayProps) {
<button
onClick={suggestDram}
disabled={isRolling}
className="flex items-center gap-2 px-6 py-3 bg-amber-600 hover:bg-amber-700 text-white rounded-2xl font-black uppercase tracking-widest text-xs transition-all shadow-lg shadow-amber-600/20 active:scale-95 disabled:opacity-50"
className="flex items-center gap-2 px-4 sm:px-6 py-2 sm:py-3 bg-amber-600 hover:bg-amber-700 text-white rounded-2xl font-black uppercase tracking-widest text-[10px] sm:text-xs transition-all shadow-lg shadow-amber-600/20 active:scale-95 disabled:opacity-50"
>
{isRolling ? (
<Dices size={18} className="animate-spin" />