119 lines
6.5 KiB
TypeScript
119 lines
6.5 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import { CheckCircle2, AlertTriangle, Clock, Droplets, Info } from 'lucide-react';
|
|
import Link from 'next/link';
|
|
|
|
interface TimelineTasting {
|
|
id: string;
|
|
bottle_id: string;
|
|
bottle_name: string;
|
|
tasted_at: string;
|
|
rating: number;
|
|
tags: string[];
|
|
category?: string;
|
|
}
|
|
|
|
interface SessionTimelineProps {
|
|
tastings: TimelineTasting[];
|
|
sessionStart?: string;
|
|
}
|
|
|
|
// Keywords that indicate a "Peat Bomb"
|
|
const SMOKY_KEYWORDS = ['rauch', 'torf', 'smoke', 'peat', 'islay', 'ash', 'lagerfeuer', 'campfire', 'asphalte'];
|
|
|
|
export default function SessionTimeline({ tastings, sessionStart }: SessionTimelineProps) {
|
|
if (!tastings || tastings.length === 0) {
|
|
return (
|
|
<div className="p-8 text-center bg-zinc-50 dark:bg-zinc-900/50 rounded-3xl border border-dashed border-zinc-200 dark:border-zinc-800">
|
|
<Clock size={32} className="mx-auto text-zinc-300 mb-3" />
|
|
<p className="text-zinc-500 text-sm font-medium italic">Noch keine Dram-Historie vorhanden.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Sort by tasted_at
|
|
const sortedTastings = [...tastings].sort((a, b) =>
|
|
new Date(a.tasted_at).getTime() - new Date(b.tasted_at).getTime()
|
|
);
|
|
|
|
const firstTastingTime = sessionStart ? new Date(sessionStart).getTime() : new Date(sortedTastings[0].tasted_at).getTime();
|
|
|
|
const checkIsSmoky = (tasting: TimelineTasting) => {
|
|
const textToSearch = (tasting.tags.join(' ') + ' ' + (tasting.category || '')).toLowerCase();
|
|
return SMOKY_KEYWORDS.some(keyword => textToSearch.includes(keyword));
|
|
};
|
|
|
|
return (
|
|
<div className="relative pl-8 space-y-8 before:absolute before:inset-0 before:left-[11px] before:w-[2px] before:bg-zinc-200 dark:before:bg-zinc-800 before:h-full">
|
|
{sortedTastings.map((tasting, index) => {
|
|
const currentTime = new Date(tasting.tasted_at).getTime();
|
|
const diffMinutes = Math.round((currentTime - firstTastingTime) / (1000 * 60));
|
|
const isSmoky = checkIsSmoky(tasting);
|
|
|
|
// Palette warning logic: if this dram is peaty, warn about the NEXT one (metaphorically)
|
|
// Or if the PREVIOUS was peaty, show a warning on this one.
|
|
const wasPreviousSmoky = index > 0 && checkIsSmoky(sortedTastings[index - 1]);
|
|
const timeSinceLastDram = index > 0
|
|
? Math.round((currentTime - new Date(sortedTastings[index - 1].tasted_at).getTime()) / (1000 * 60))
|
|
: 0;
|
|
|
|
return (
|
|
<div key={tasting.id} className="relative group">
|
|
{/* Dot */}
|
|
<div className={`absolute -left-[30px] w-6 h-6 rounded-full border-4 border-white dark:border-zinc-900 shadow-sm z-10 flex items-center justify-center ${isSmoky ? 'bg-amber-600' : 'bg-zinc-400'}`}>
|
|
{isSmoky && <Droplets size={10} className="text-white fill-white" />}
|
|
</div>
|
|
|
|
{/* Relative Time */}
|
|
<div className="absolute -left-16 -top-1 w-12 text-right">
|
|
<span className="text-[10px] font-black text-zinc-400 dark:text-zinc-600 block leading-none">
|
|
{index === 0 ? 'START' : `+${diffMinutes}'`}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-zinc-900 p-4 rounded-2xl border border-zinc-200 dark:border-zinc-800 shadow-sm hover:shadow-md transition-shadow group-hover:border-amber-500/30">
|
|
<div className="flex justify-between items-start gap-3">
|
|
<div className="min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="text-[10px] font-black text-amber-600 uppercase tracking-widest">Dram #{index + 1}</span>
|
|
{isSmoky && (
|
|
<span className="bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 text-[8px] font-black px-1.5 py-0.5 rounded-md uppercase tracking-tighter">Peat Bomb</span>
|
|
)}
|
|
</div>
|
|
<Link
|
|
href={`/bottles/${tasting.bottle_id}`}
|
|
className="text-sm font-bold text-zinc-800 dark:text-zinc-100 hover:text-amber-600 truncate block"
|
|
>
|
|
{tasting.bottle_name}
|
|
</Link>
|
|
<div className="mt-2 flex flex-wrap gap-1">
|
|
{tasting.tags.slice(0, 3).map(tag => (
|
|
<span key={tag} className="text-[9px] text-zinc-500 dark:text-zinc-500 bg-zinc-100 dark:bg-zinc-800/50 px-2 py-0.5 rounded-full">
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="shrink-0 flex flex-col items-end">
|
|
<div className="text-lg font-black text-zinc-900 dark:text-white">{tasting.rating}</div>
|
|
<div className="text-[9px] font-bold text-zinc-400 uppercase tracking-tighter">Punkte</div>
|
|
</div>
|
|
</div>
|
|
|
|
{wasPreviousSmoky && timeSinceLastDram < 20 && (
|
|
<div className="mt-4 p-2 bg-amber-50 dark:bg-amber-900/10 border border-amber-200/50 dark:border-amber-900/30 rounded-xl flex items-center gap-2 animate-in slide-in-from-top-1">
|
|
<AlertTriangle size={12} className="text-amber-600 shrink-0" />
|
|
<p className="text-[9px] text-amber-800 dark:text-amber-400 font-bold leading-tight">
|
|
Achtung: Gaumen war noch torf-belegt (nur {timeSinceLastDram}m Abstand).
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|