feat: refine session workflow with global state, quick tasting, and statistics

This commit is contained in:
2025-12-18 17:19:38 +01:00
parent 7f600698e4
commit ca1621e765
14 changed files with 399 additions and 116 deletions

View File

@@ -0,0 +1,42 @@
'use client';
import React from 'react';
import { useSession } from '@/context/SessionContext';
import { GlassWater, X, ArrowRight, Sparkles } from 'lucide-react';
import Link from 'next/link';
import { useI18n } from '@/i18n/I18nContext';
export default function ActiveSessionBanner() {
const { activeSession, setActiveSession } = useSession();
const { t } = useI18n();
if (!activeSession) return null;
return (
<div className="fixed top-0 left-0 right-0 z-[100] animate-in slide-in-from-top duration-500">
<div className="bg-amber-600 text-white px-4 py-2 flex items-center justify-between shadow-lg">
<Link
href={`/sessions/${activeSession.id}`}
className="flex items-center gap-3 flex-1 min-w-0"
>
<div className="bg-white/20 p-1.5 rounded-lg shrink-0">
<Sparkles size={16} className="text-white animate-pulse" />
</div>
<div className="min-w-0">
<p className="text-[10px] font-black uppercase tracking-wider opacity-90 leading-none mb-1">{t('session.activeSession')}</p>
<p className="text-sm font-bold truncate leading-none">{activeSession.name}</p>
</div>
<ArrowRight size={14} className="opacity-50 ml-2" />
</Link>
<button
onClick={() => setActiveSession(null)}
className="ml-4 p-2 hover:bg-white/10 rounded-full transition-colors"
title="End Session"
>
<X size={20} />
</button>
</div>
</div>
);
}

View File

@@ -7,6 +7,7 @@ import { getStorageUrl } from '@/lib/supabase';
import { useSearchParams } from 'next/navigation';
import { validateSession } from '@/services/validate-session';
import { useI18n } from '@/i18n/I18nContext';
import { useSession } from '@/context/SessionContext';
import { shortenCategory } from '@/lib/format';
interface Bottle {
@@ -121,21 +122,24 @@ interface BottleGridProps {
export default function BottleGrid({ bottles }: BottleGridProps) {
const { t } = useI18n();
const { activeSession } = useSession();
const searchParams = useSearchParams();
const sessionId = searchParams.get('session_id');
const sessionIdFromUrl = searchParams.get('session_id');
const effectiveSessionId = activeSession?.id || sessionIdFromUrl;
const [validatedSessionId, setValidatedSessionId] = useState<string | null>(null);
React.useEffect(() => {
const checkSession = async () => {
if (sessionId) {
const isValid = await validateSession(sessionId);
setValidatedSessionId(isValid ? sessionId : null);
if (effectiveSessionId) {
const isValid = await validateSession(effectiveSessionId);
setValidatedSessionId(isValid ? effectiveSessionId : null);
} else {
setValidatedSessionId(null);
}
};
checkSession();
}, [sessionId]);
}, [effectiveSessionId]);
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);

View File

@@ -1,7 +1,7 @@
'use client';
import React, { useRef, useState } from 'react';
import { Camera, Upload, CheckCircle2, AlertCircle, Sparkles, ExternalLink, ChevronRight, Search, Loader2 } from 'lucide-react';
import { Camera, Upload, CheckCircle2, AlertCircle, X, Search, ExternalLink, ArrowRight, Loader2, Wand2, Plus, Sparkles, Droplets, ChevronRight } from 'lucide-react';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { useRouter, useSearchParams } from 'next/navigation';
import { analyzeBottle } from '@/services/analyze-bottle';
@@ -15,6 +15,7 @@ import { discoverWhiskybaseId } from '@/services/discover-whiskybase';
import { updateBottle } from '@/services/update-bottle';
import Link from 'next/link';
import { useI18n } from '@/i18n/I18nContext';
import { useSession } from '@/context/SessionContext';
import { shortenCategory } from '@/lib/format';
interface CameraCaptureProps {
@@ -28,20 +29,26 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
const supabase = createClientComponentClient();
const router = useRouter();
const searchParams = useSearchParams();
const sessionId = searchParams.get('session_id');
const { activeSession } = useSession();
// Maintain sessionId from query param for backwards compatibility,
// but prefer global activeSession
const sessionIdFromUrl = searchParams.get('session_id');
const effectiveSessionId = activeSession?.id || sessionIdFromUrl;
const [validatedSessionId, setValidatedSessionId] = React.useState<string | null>(null);
React.useEffect(() => {
const checkSession = async () => {
if (sessionId) {
const isValid = await validateSession(sessionId);
setValidatedSessionId(isValid ? sessionId : null);
if (effectiveSessionId) {
const isValid = await validateSession(effectiveSessionId);
setValidatedSessionId(isValid ? effectiveSessionId : null);
} else {
setValidatedSessionId(null);
}
};
checkSession();
}, [sessionId]);
}, [effectiveSessionId]);
const fileInputRef = useRef<HTMLInputElement>(null);
const galleryInputRef = useRef<HTMLInputElement>(null);
@@ -132,6 +139,34 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
}
};
const handleQuickSave = async () => {
if (!analysisResult || !previewUrl) return;
setIsSaving(true);
setError(null);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
throw new Error(t('camera.authRequired'));
}
const response = await saveBottle(analysisResult, previewUrl, user.id);
if (response.success && response.data) {
const url = `/bottles/${response.data.id}${validatedSessionId ? `?session_id=${validatedSessionId}` : ''}`;
router.push(url);
} else {
setError(response.error || t('common.error'));
}
} catch (err) {
console.error('Quick save failed:', err);
setError(err instanceof Error ? err.message : t('common.error'));
} finally {
setIsSaving(false);
}
};
const handleSave = async () => {
if (!analysisResult || !previewUrl) return;
@@ -348,23 +383,43 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
</button>
</div>
) : matchingBottle ? (
<Link
href={`/bottles/${matchingBottle.id}${validatedSessionId ? `?session_id=${validatedSessionId}` : ''}`}
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"
>
<ExternalLink size={20} />
{t('camera.toVault')}
</Link>
<div className="flex flex-col gap-3 w-full animate-in zoom-in-95 duration-300">
<Link
href={`/bottles/${matchingBottle.id}${validatedSessionId ? `?session_id=${validatedSessionId}` : ''}`}
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"
>
<ExternalLink size={20} />
{t('camera.toVault')}
</Link>
<button
onClick={() => setMatchingBottle(null)}
className="w-full py-3 px-6 text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200 font-bold transition-colors"
>
{t('camera.saveAnyway')}
</button>
</div>
) : (
<div className="flex flex-col gap-3 w-full">
<button
onClick={isQueued ? () => setPreviewUrl(null) : (previewUrl && analysisResult ? handleSave : triggerUpload)}
onClick={() => {
if (isQueued) {
setPreviewUrl(null);
} else if (previewUrl && analysisResult) {
if (validatedSessionId) {
handleQuickSave();
} else {
handleSave();
}
} else {
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"
className={`w-full py-4 px-6 rounded-xl font-semibold flex items-center justify-center gap-2 transition-all active:scale-[0.98] shadow-lg disabled:opacity-50 ${validatedSessionId && previewUrl && analysisResult ? 'bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 shadow-black/10' : 'bg-amber-600 hover:bg-amber-700 text-white shadow-amber-600/20'}`}
>
{isSaving ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-current"></div>
{t('camera.saving')}
</>
) : isQueued ? (
@@ -373,10 +428,17 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
{t('camera.nextBottle')}
</>
) : previewUrl && analysisResult ? (
<>
<CheckCircle2 size={20} />
{t('camera.inVault')}
</>
validatedSessionId ? (
<>
<Droplets size={20} className="text-amber-500" />
{t('camera.quickTasting')}
</>
) : (
<>
<CheckCircle2 size={20} />
{t('camera.inVault')}
</>
)
) : previewUrl ? (
<>
<Upload size={20} />
@@ -402,6 +464,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
</div>
)}
{/* Status Messages */}
{error && (
<div className="flex items-center gap-2 text-red-500 text-sm bg-red-50 dark:bg-red-900/10 p-3 rounded-lg w-full">
<AlertCircle size={16} />
@@ -416,7 +479,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
</div>
)}
{matchingBottle && (
{matchingBottle && !lastSavedId && (
<div className="flex flex-col gap-2 p-4 bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-900/30 rounded-xl w-full">
<div className="flex items-center gap-2 text-blue-600 dark:text-blue-400 font-bold text-sm">
<AlertCircle size={16} />
@@ -425,66 +488,65 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
<p className="text-xs text-blue-500/80">
{t('camera.alreadyInVaultDesc')}
</p>
<button
onClick={() => setMatchingBottle(null)}
className="text-[10px] text-zinc-400 font-black uppercase text-left hover:text-zinc-600"
>
{t('camera.saveAnyway')}
</button>
</div>
)}
{previewUrl && !isProcessing && !error && !isQueued && !matchingBottle && (
{/* Analysis Results Display */}
{previewUrl && !isProcessing && !error && !isQueued && !matchingBottle && analysisResult && (
<div className="flex flex-col gap-3 w-full animate-in fade-in slide-in-from-top-4 duration-500">
<div className="flex items-center gap-2 text-green-500 text-sm bg-green-50 dark:bg-green-900/10 p-3 rounded-lg w-full">
<CheckCircle2 size={16} />
{t('camera.analysisSuccess')}
</div>
{analysisResult && (
<div className="p-3 md:p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-2xl border border-zinc-200 dark:border-zinc-700">
<div className="flex items-center gap-2 mb-2 md:mb-3 text-amber-600 dark:text-amber-500">
<Sparkles size={18} />
<span className="font-bold text-[10px] md:text-sm uppercase tracking-wider">{t('camera.results')}</span>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-zinc-500">{t('bottle.nameLabel')}:</span>
<span className="font-semibold">{analysisResult.name || '-'}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-zinc-500">{t('bottle.distilleryLabel')}:</span>
<span className="font-semibold">{analysisResult.distillery || '-'}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-zinc-500">{t('bottle.categoryLabel')}:</span>
<span className="font-semibold">{shortenCategory(analysisResult.category || '-')}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-zinc-500">{t('bottle.abvLabel')}:</span>
<span className="font-semibold">{analysisResult.abv ? `${analysisResult.abv}%` : '-'}</span>
</div>
{analysisResult.distilled_at && (
<div className="flex justify-between text-sm">
<span className="text-zinc-500">{t('bottle.distilledLabel')}:</span>
<span className="font-semibold">{analysisResult.distilled_at}</span>
</div>
)}
{analysisResult.bottled_at && (
<div className="flex justify-between text-sm">
<span className="text-zinc-500">{t('bottle.bottledLabel')}:</span>
<span className="font-semibold">{analysisResult.bottled_at}</span>
</div>
)}
{analysisResult.batch_info && (
<div className="flex justify-between text-sm">
<span className="text-zinc-500">{t('bottle.batchLabel')}:</span>
<span className="font-semibold">{analysisResult.batch_info}</span>
</div>
)}
</div>
<div className="p-3 md:p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-2xl border border-zinc-200 dark:border-zinc-700">
<div className="flex items-center gap-2 mb-2 md:mb-3 text-amber-600 dark:text-amber-500">
<Sparkles size={18} />
<span className="font-bold text-[10px] md:text-sm uppercase tracking-wider">{t('camera.results')}</span>
</div>
)}
<div className="space-y-2">
<div className="flex justify-between items-center text-sm">
<span className="text-zinc-500">{t('bottle.nameLabel')}:</span>
<span className="font-semibold text-right">{analysisResult.name || '-'}</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-zinc-500">{t('bottle.distilleryLabel')}:</span>
<span className="font-semibold text-right">{analysisResult.distillery || '-'}</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-zinc-500">{t('bottle.categoryLabel')}:</span>
<span className="font-semibold text-right">{shortenCategory(analysisResult.category || '-')}</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-zinc-500">{t('bottle.abvLabel')}:</span>
<span className="font-semibold text-right">{analysisResult.abv ? `${analysisResult.abv}%` : '-'}</span>
</div>
{analysisResult.age && (
<div className="flex justify-between items-center text-sm">
<span className="text-zinc-500">{t('bottle.ageLabel')}:</span>
<span className="font-semibold text-right">{analysisResult.age} {t('bottle.years')}</span>
</div>
)}
{analysisResult.distilled_at && (
<div className="flex justify-between text-sm">
<span className="text-zinc-500">{t('bottle.distilledLabel')}:</span>
<span className="font-semibold">{analysisResult.distilled_at}</span>
</div>
)}
{analysisResult.bottled_at && (
<div className="flex justify-between text-sm">
<span className="text-zinc-500">{t('bottle.bottledLabel')}:</span>
<span className="font-semibold">{analysisResult.bottled_at}</span>
</div>
)}
{analysisResult.batch_info && (
<div className="flex justify-between text-sm">
<span className="text-zinc-500">{t('bottle.batchLabel')}:</span>
<span className="font-semibold">{analysisResult.batch_info}</span>
</div>
)}
</div>
</div>
</div>
)}
</div>

View File

@@ -0,0 +1,14 @@
'use client';
import React from 'react';
import { useSession } from '@/context/SessionContext';
export default function MainContentWrapper({ children }: { children: React.ReactNode }) {
const { activeSession } = useSession();
return (
<div className={`transition-all duration-500 ${activeSession ? 'pt-[52px]' : 'pt-0'}`}>
{children}
</div>
);
}

View File

@@ -2,15 +2,17 @@
import React, { useState, useEffect } from 'react';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { Calendar, Plus, GlassWater, Loader2, ChevronRight, Users } from 'lucide-react';
import { Calendar, Plus, GlassWater, Loader2, ChevronRight, Users, Check } from 'lucide-react';
import Link from 'next/link';
import { useI18n } from '@/i18n/I18nContext';
import { useSession } from '@/context/SessionContext';
interface Session {
id: string;
name: string;
scheduled_at: string;
participant_count?: number;
whisky_count?: number;
}
export default function SessionList() {
@@ -20,6 +22,7 @@ export default function SessionList() {
const [isLoading, setIsLoading] = useState(true);
const [isCreating, setIsCreating] = useState(false);
const [newName, setNewName] = useState('');
const { activeSession, setActiveSession } = useSession();
useEffect(() => {
fetchSessions();
@@ -30,7 +33,8 @@ export default function SessionList() {
.from('tasting_sessions')
.select(`
*,
session_participants (count)
session_participants (count),
tastings (count)
`)
.order('scheduled_at', { ascending: false });
@@ -39,7 +43,8 @@ export default function SessionList() {
} else {
setSessions(data.map(s => ({
...s,
participant_count: s.session_participants[0]?.count || 0
participant_count: s.session_participants[0]?.count || 0,
whisky_count: s.tastings[0]?.count || 0
})) || []);
}
setIsLoading(false);
@@ -64,6 +69,7 @@ export default function SessionList() {
} else {
setSessions(prev => [data, ...prev]);
setNewName('');
setActiveSession({ id: data.id, name: data.name });
}
setIsCreating(false);
};
@@ -103,14 +109,18 @@ export default function SessionList() {
) : (
<div className="space-y-3">
{sessions.map((session) => (
<Link
<div
key={session.id}
href={`/sessions/${session.id}`}
className="flex items-center justify-between p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-2xl border border-zinc-100 dark:border-zinc-800 group hover:border-amber-500/30 transition-all"
className={`flex items-center justify-between p-4 rounded-2xl border group transition-all ${activeSession?.id === session.id
? 'bg-amber-600 border-amber-600 shadow-lg shadow-amber-600/20'
: 'bg-zinc-50 dark:bg-zinc-800/50 border-zinc-100 dark:border-zinc-800 hover:border-amber-500/30'
}`}
>
<div className="space-y-1">
<div className="font-bold text-zinc-800 dark:text-zinc-100">{session.name}</div>
<div className="flex items-center gap-4 text-[10px] font-black uppercase tracking-widest text-zinc-400">
<Link href={`/sessions/${session.id}`} className="flex-1 space-y-1 min-w-0">
<div className={`font-bold truncate ${activeSession?.id === session.id ? 'text-white' : 'text-zinc-800 dark:text-zinc-100'}`}>
{session.name}
</div>
<div className={`flex items-center gap-4 text-[10px] font-black uppercase tracking-widest ${activeSession?.id === session.id ? 'text-white/80' : 'text-zinc-400'}`}>
<span className="flex items-center gap-1">
<Calendar size={12} />
{new Date(session.scheduled_at).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')}
@@ -121,13 +131,35 @@ export default function SessionList() {
{session.participant_count} {t('tasting.participants')}
</span>
)}
{session.whisky_count! > 0 && (
<span className="flex items-center gap-1">
<GlassWater size={12} />
{session.whisky_count} Whiskys
</span>
)}
</div>
</Link>
<div className="flex items-center gap-2">
{activeSession?.id !== session.id ? (
<button
onClick={() => setActiveSession({ id: session.id, name: session.name })}
className="p-2 bg-white dark:bg-zinc-700 text-amber-600 rounded-xl shadow-sm border border-zinc-200 dark:border-zinc-600 hover:scale-110 transition-transform"
title="Start Session"
>
<GlassWater size={18} />
</button>
) : (
<div className="p-2 bg-white/20 text-white rounded-xl">
<Check size={18} />
</div>
)}
<ChevronRight size={20} className={activeSession?.id === session.id ? 'text-white/50' : 'text-zinc-300'} />
</div>
<ChevronRight size={20} className="text-zinc-300 group-hover:text-amber-500 transition-colors" />
</Link>
))}
</div>
</div>
))
}
</div >
)}
</div>
</div >
);
}

View File

@@ -1,7 +1,8 @@
'use client';
import React, { useState, useMemo } from 'react';
import { Calendar, Star, ArrowUpDown, Clock, Trash2, Loader2, Users } from 'lucide-react';
import { Calendar, Star, ArrowUpDown, Clock, Trash2, Loader2, Users, GlassWater } from 'lucide-react';
import Link from 'next/link';
import { deleteTasting } from '@/services/delete-tasting';
interface Tasting {
@@ -19,6 +20,10 @@ interface Tasting {
name: string;
}
}[];
tasting_sessions?: {
id: string;
name: string;
};
}
interface TastingListProps {
@@ -108,9 +113,17 @@ export default function TastingList({ initialTastings }: TastingListProps) {
{note.is_sample ? 'Sample' : 'Bottle'}
</span>
<div className="text-[10px] text-zinc-500 font-bold bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded-lg flex items-center gap-1">
<Clock size={10} />
{new Date(note.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
</div>
{note.tasting_sessions && (
<Link
href={`/sessions/${note.tasting_sessions.id}`}
className="text-[10px] text-zinc-500 font-bold bg-amber-50 dark:bg-amber-900/20 px-2 py-1 rounded-lg flex items-center gap-1 border border-amber-200/50 dark:border-amber-800/50 transition-all hover:bg-amber-100 dark:hover:bg-amber-900/40"
>
<GlassWater size={10} className="text-amber-600" />
{note.tasting_sessions.name}
</Link>
)}
</div>
<div className="flex items-center gap-4">
<div className="text-[10px] text-zinc-400 font-black tracking-widest uppercase flex items-center gap-1">

View File

@@ -2,9 +2,10 @@
import React, { useState, useEffect } from 'react';
import { saveTasting } from '@/services/save-tasting';
import { Loader2, Send, Star, Users, Check } from 'lucide-react';
import { Loader2, Send, Star, Users, Check, Sparkles, Droplets } from 'lucide-react';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { useI18n } from '@/i18n/I18nContext';
import { useSession } from '@/context/SessionContext';
interface Buddy {
id: string;
@@ -28,6 +29,9 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm
const [error, setError] = useState<string | null>(null);
const [buddies, setBuddies] = useState<Buddy[]>([]);
const [selectedBuddyIds, setSelectedBuddyIds] = useState<string[]>([]);
const { activeSession } = useSession();
const effectiveSessionId = sessionId || activeSession?.id;
useEffect(() => {
const fetchData = async () => {
@@ -36,19 +40,21 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm
setBuddies(buddiesData || []);
// If Session ID, fetch session participants and pre-select them
if (sessionId) {
if (effectiveSessionId) {
const { data: participants } = await supabase
.from('session_participants')
.select('buddy_id')
.eq('session_id', sessionId);
.eq('session_id', effectiveSessionId);
if (participants) {
setSelectedBuddyIds(participants.map(p => p.buddy_id));
}
} else {
setSelectedBuddyIds([]);
}
};
fetchData();
}, [sessionId]);
}, [effectiveSessionId]);
const toggleBuddy = (id: string) => {
setSelectedBuddyIds(prev =>
@@ -64,7 +70,7 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm
try {
const result = await saveTasting({
bottle_id: bottleId,
session_id: sessionId,
session_id: effectiveSessionId,
rating,
nose_notes: nose,
palate_notes: palate,
@@ -91,6 +97,18 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm
return (
<form onSubmit={handleSubmit} className="space-y-6">
{activeSession && (
<div className="p-3 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-900/30 rounded-2xl flex items-center gap-3 animate-in fade-in slide-in-from-top-2">
<div className="bg-amber-600 text-white p-2 rounded-xl">
<Sparkles size={16} />
</div>
<div className="min-w-0">
<p className="text-[10px] font-black uppercase tracking-wider text-amber-700 dark:text-amber-400">Recording for Session</p>
<p className="text-xs font-bold text-amber-900 dark:text-amber-200 truncate">{activeSession.name}</p>
</div>
</div>
)}
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className="text-[11px] font-black text-zinc-400 uppercase tracking-widest flex items-center gap-2">