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:
@@ -129,11 +129,11 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center gap-6 md:gap-12 p-4 md:p-24 bg-zinc-50 dark:bg-black">
|
<main className="flex min-h-screen flex-col items-center gap-6 md:gap-12 p-4 md:p-24 bg-zinc-50 dark:bg-black">
|
||||||
<div className="z-10 max-w-5xl w-full flex flex-col items-center gap-8">
|
<div className="z-10 max-w-5xl w-full flex flex-col items-center gap-8">
|
||||||
<header className="w-full flex justify-between items-center">
|
<header className="w-full flex flex-col sm:flex-row justify-between items-center gap-4 sm:gap-0">
|
||||||
<h1 className="text-4xl font-black text-zinc-900 dark:text-white tracking-tighter">
|
<h1 className="text-4xl font-black text-zinc-900 dark:text-white tracking-tighter">
|
||||||
WHISKY<span className="text-amber-600">VAULT</span>
|
WHISKY<span className="text-amber-600">VAULT</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex flex-wrap items-center justify-center sm:justify-end gap-3 md:gap-4">
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
<DramOfTheDay bottles={bottles} />
|
<DramOfTheDay bottles={bottles} />
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
|||||||
}, [sessionId]);
|
}, [sessionId]);
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const galleryInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
@@ -227,6 +228,10 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
|||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const triggerGallery = () => {
|
||||||
|
galleryInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<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>
|
<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"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
ref={galleryInputRef}
|
||||||
|
onChange={handleCapture}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
{lastSavedId ? (
|
{lastSavedId ? (
|
||||||
<div className="flex flex-col gap-3 w-full animate-in zoom-in-95 duration-300">
|
<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">
|
<div className="flex items-center gap-2 text-green-600 font-bold justify-center p-2">
|
||||||
@@ -342,6 +355,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
|||||||
{t('camera.toVault')}
|
{t('camera.toVault')}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<button
|
<button
|
||||||
onClick={isQueued ? () => setPreviewUrl(null) : (previewUrl && analysisResult ? handleSave : triggerUpload)}
|
onClick={isQueued ? () => setPreviewUrl(null) : (previewUrl && analysisResult ? handleSave : triggerUpload)}
|
||||||
disabled={isProcessing || isSaving}
|
disabled={isProcessing || isSaving}
|
||||||
@@ -374,6 +388,17 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function DramOfTheDay({ bottles }: DramOfTheDayProps) {
|
|||||||
<button
|
<button
|
||||||
onClick={suggestDram}
|
onClick={suggestDram}
|
||||||
disabled={isRolling}
|
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 ? (
|
{isRolling ? (
|
||||||
<Dices size={18} className="animate-spin" />
|
<Dices size={18} className="animate-spin" />
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ export const de: TranslationKeys = {
|
|||||||
toVault: 'Zum Whisky im Vault',
|
toVault: 'Zum Whisky im Vault',
|
||||||
authRequired: 'Bitte melde dich an, um Flaschen zu speichern.',
|
authRequired: 'Bitte melde dich an, um Flaschen zu speichern.',
|
||||||
processingError: 'Verarbeitung fehlgeschlagen. Bitte erneut versuchen.',
|
processingError: 'Verarbeitung fehlgeschlagen. Bitte erneut versuchen.',
|
||||||
|
uploadGallery: 'Aus Galerie hochladen',
|
||||||
},
|
},
|
||||||
tasting: {
|
tasting: {
|
||||||
addNote: 'Neue Note hinzufügen',
|
addNote: 'Neue Note hinzufügen',
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ export const en: TranslationKeys = {
|
|||||||
toVault: 'Go to bottle in Vault',
|
toVault: 'Go to bottle in Vault',
|
||||||
authRequired: 'Please sign in to save bottles.',
|
authRequired: 'Please sign in to save bottles.',
|
||||||
processingError: 'Processing failed. Please try again.',
|
processingError: 'Processing failed. Please try again.',
|
||||||
|
uploadGallery: 'Upload from Gallery',
|
||||||
},
|
},
|
||||||
tasting: {
|
tasting: {
|
||||||
addNote: 'Add Tasting Note',
|
addNote: 'Add Tasting Note',
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export type TranslationKeys = {
|
|||||||
toVault: string;
|
toVault: string;
|
||||||
authRequired: string;
|
authRequired: string;
|
||||||
processingError: string;
|
processingError: string;
|
||||||
|
uploadGallery: string;
|
||||||
};
|
};
|
||||||
tasting: {
|
tasting: {
|
||||||
addNote: string;
|
addNote: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user