Files
CrazeAnalytix/components/ExperimentTracker.tsx
T
Christian Vidal WolfandClaude Opus 4.6 812425a1ef feat: add marketplace differentiation to Experiment Tracker
Each experiment now tracks which marketplace (Amazon DE, UK, etc.) it
applies to. The form defaults to the currently active marketplace filter,
and the delta % calculation filters by both product line and marketplace.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:07:34 +01:00

639 lines
35 KiB
TypeScript

import React, { useState, useMemo, useCallback } from 'react';
import {
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
ReferenceDot
} from 'recharts';
import { WeeklyPivotRow } from '../services/dataProcessor';
// --- Types ---
export interface Experiment {
id: string;
week: string; // "2026-08" matching YYYY-WW format used in pivot data
product_line: string;
marketplace: string; // e.g. "Amazon DE", "Amazon UK"
action_type: 'SEO' | 'Price' | 'Advertising' | 'Images' | 'A+ Content' | 'Variants' | 'Stock' | 'Other';
description: string;
expected_impact: 'More Sales' | 'Better Visibility' | 'Lower Costs';
status: 'in_progress' | 'success' | 'failed';
created_at: string;
}
type ActionType = Experiment['action_type'];
type ExpectedImpact = Experiment['expected_impact'];
type ExperimentStatus = Experiment['status'];
const ACTION_TYPES: ActionType[] = ['SEO', 'Price', 'Advertising', 'Images', 'A+ Content', 'Variants', 'Stock', 'Other'];
const EXPECTED_IMPACTS: ExpectedImpact[] = ['More Sales', 'Better Visibility', 'Lower Costs'];
const STATUSES: ExperimentStatus[] = ['in_progress', 'success', 'failed'];
const ACTION_COLORS: Record<ActionType, string> = {
'SEO': '#3b82f6',
'Price': '#f59e0b',
'Advertising': '#f43f5e',
'Images': '#a855f7',
'A+ Content': '#10b981',
'Variants': '#14b8a6',
'Stock': '#f97316',
'Other': '#64748b',
};
const ACTION_BG_CLASSES: Record<ActionType, string> = {
'SEO': 'bg-blue-500/20 text-blue-400 border-blue-500/30',
'Price': 'bg-amber-500/20 text-amber-400 border-amber-500/30',
'Advertising': 'bg-rose-500/20 text-rose-400 border-rose-500/30',
'Images': 'bg-purple-500/20 text-purple-400 border-purple-500/30',
'A+ Content': 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30',
'Variants': 'bg-teal-500/20 text-teal-400 border-teal-500/30',
'Stock': 'bg-orange-500/20 text-orange-400 border-orange-500/30',
'Other': 'bg-slate-500/20 text-slate-400 border-slate-500/30',
};
const STATUS_CLASSES: Record<ExperimentStatus, string> = {
'in_progress': 'bg-blue-500/20 text-blue-400 border-blue-500/30',
'success': 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30',
'failed': 'bg-red-500/20 text-red-400 border-red-500/30',
};
const STATUS_LABELS: Record<ExperimentStatus, string> = {
'in_progress': 'In Progress',
'success': 'Success',
'failed': 'Failed',
};
const STORAGE_KEY = 'craze_experiments';
// --- Storage helpers ---
const loadExperiments = (): Experiment[] => {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : [];
} catch {
return [];
}
};
const saveExperiments = (experiments: Experiment[]) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(experiments));
};
// --- Current ISO week helper ---
const getCurrentWeek = (): string => {
const now = new Date();
const jan1 = new Date(now.getFullYear(), 0, 1);
const dayOfYear = Math.floor((now.getTime() - jan1.getTime()) / 86400000) + 1;
const weekNum = Math.ceil((dayOfYear + jan1.getDay()) / 7);
return `${now.getFullYear()}-${String(weekNum).padStart(2, '0')}`;
};
// --- Props ---
interface ExperimentTrackerProps {
rows: WeeklyPivotRow[];
weeks: string[]; // sorted descending (most recent first)
primaryMetric: 'units' | 'revenue';
customerFilters: string[]; // active marketplace filters from parent
}
// --- Custom dot for experiment markers ---
const ExperimentDot: React.FC<any> = (props) => {
const { cx, cy, experiments } = props;
if (!experiments || experiments.length === 0 || !cx || !cy) return null;
const color = ACTION_COLORS[experiments[0].action_type as ActionType] || '#64748b';
return (
<g>
<circle cx={cx} cy={cy} r={10} fill={color} fillOpacity={0.25} stroke={color} strokeWidth={1.5} />
<circle cx={cx} cy={cy} r={4} fill={color} />
{experiments.length > 1 && (
<text x={cx + 12} y={cy - 4} fill={color} fontSize={9} fontWeight="bold">
+{experiments.length - 1}
</text>
)}
</g>
);
};
// --- Custom chart tooltip ---
const ChartTooltipContent: React.FC<any> = ({ active, payload, label, experiments }) => {
if (!active || !payload || !payload.length) return null;
const weekExperiments = experiments?.filter((e: Experiment) => e.week === label) || [];
return (
<div className="bg-slate-950 border border-white/20 rounded-xl shadow-2xl p-3 max-w-xs">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 pb-2 border-b border-white/10">
Week {label?.split('-')[1]}/{label?.split('-')[0]?.slice(-2)}
</div>
{payload.map((entry: any, i: number) => (
<div key={i} className="flex justify-between items-center gap-4 mb-1">
<span className="text-[10px] text-slate-500 font-bold">{entry.name}</span>
<span className="text-xs font-black" style={{ color: entry.color }}>
{entry.name === 'Revenue' ? `€${entry.value?.toLocaleString('de-DE')}` : entry.value?.toLocaleString('de-DE')}
</span>
</div>
))}
{weekExperiments.length > 0 && (
<div className="mt-2 pt-2 border-t border-white/10 space-y-1.5">
<div className="text-[9px] font-black text-fuchsia-400 uppercase tracking-widest">Experiments</div>
{weekExperiments.map((exp: Experiment) => (
<div key={exp.id} className="flex items-start gap-2">
<span
className="w-2 h-2 rounded-full mt-1 flex-shrink-0"
style={{ backgroundColor: ACTION_COLORS[exp.action_type] }}
/>
<div>
<span className={`text-[9px] font-bold px-1.5 py-0.5 rounded border ${ACTION_BG_CLASSES[exp.action_type]}`}>
{exp.action_type}
</span>
<span className="text-[10px] text-white/70 ml-1.5">{exp.product_line}</span>
{exp.marketplace && <span className="text-[9px] text-slate-500 ml-1">({exp.marketplace})</span>}
<p className="text-[10px] text-slate-400 mt-0.5 leading-tight">{exp.description}</p>
</div>
</div>
))}
</div>
)}
</div>
);
};
// --- Main Component ---
const ExperimentTracker: React.FC<ExperimentTrackerProps> = ({ rows, weeks, primaryMetric, customerFilters }) => {
const [experiments, setExperiments] = useState<Experiment[]>(loadExperiments);
const [showForm, setShowForm] = useState(false);
const [showPanel, setShowPanel] = useState(true);
const [editingStatus, setEditingStatus] = useState<string | null>(null);
// Form state
const [formWeek, setFormWeek] = useState(getCurrentWeek);
const [formLine, setFormLine] = useState('');
const [formMarketplace, setFormMarketplace] = useState('');
const [formAction, setFormAction] = useState<ActionType>('SEO');
const [formDesc, setFormDesc] = useState('');
const [formImpact, setFormImpact] = useState<ExpectedImpact>('More Sales');
// Available marketplaces from data
const marketplaces = useMemo(() => {
const mks = new Set(rows.map(r => r.customer).filter(Boolean));
return Array.from(mks).sort();
}, [rows]);
// Default marketplace to active filter when opening form
const defaultMarketplace = useMemo(() => {
if (customerFilters.length === 1) return customerFilters[0];
if (customerFilters.length > 0) return customerFilters[0];
return marketplaces[0] || '';
}, [customerFilters, marketplaces]);
// Available product lines from data
const productLines = useMemo(() => {
const lines = new Set(rows.map(r => r.line).filter(Boolean));
return Array.from(lines).sort();
}, [rows]);
// Chart data: aggregate totals by week (ascending order for chart)
const chartData = useMemo(() => {
const reversedWeeks = [...weeks].reverse(); // ascending for chart
return reversedWeeks.map(week => {
let units = 0;
let revenue = 0;
let spend = 0;
for (const row of rows) {
units += row.unitsByWeek[week] || 0;
revenue += row.revenueByWeek[week] || 0;
spend += row.spendByWeek[week] || 0;
}
return { week, units, revenue, spend };
});
}, [rows, weeks]);
// Weeks with experiments (for ReferenceDots)
const weekExperimentMap = useMemo(() => {
const map = new Map<string, Experiment[]>();
for (const exp of experiments) {
const existing = map.get(exp.week) || [];
existing.push(exp);
map.set(exp.week, existing);
}
return map;
}, [experiments]);
// Compute delta % for an experiment
const computeDelta = useCallback((exp: Experiment): { baseline: number | null; post: number | null; delta: number | null } => {
const ascWeeks = [...weeks].reverse();
const weekIdx = ascWeeks.indexOf(exp.week);
if (weekIdx === -1) return { baseline: null, post: null, delta: null };
const getWeekTotal = (w: string) => {
let total = 0;
for (const row of rows) {
if (row.line !== exp.product_line) continue;
if (exp.marketplace && row.customer !== exp.marketplace) continue;
total += primaryMetric === 'units'
? (row.unitsByWeek[w] || 0)
: (row.revenueByWeek[w] || 0);
}
return total;
};
// 3 weeks before
const beforeWeeks = ascWeeks.slice(Math.max(0, weekIdx - 3), weekIdx);
// 3 weeks after
const afterWeeks = ascWeeks.slice(weekIdx + 1, weekIdx + 4);
if (beforeWeeks.length === 0) return { baseline: null, post: null, delta: null };
const baselineSum = beforeWeeks.reduce((sum, w) => sum + getWeekTotal(w), 0);
const baselineAvg = baselineSum / beforeWeeks.length;
if (afterWeeks.length === 0) return { baseline: baselineAvg, post: null, delta: null };
const postSum = afterWeeks.reduce((sum, w) => sum + getWeekTotal(w), 0);
const postAvg = postSum / afterWeeks.length;
const delta = baselineAvg > 0 ? ((postAvg - baselineAvg) / baselineAvg) * 100 : null;
return { baseline: baselineAvg, post: postAvg, delta };
}, [rows, weeks, primaryMetric]);
// Persist experiments
const updateExperiments = useCallback((updated: Experiment[]) => {
setExperiments(updated);
saveExperiments(updated);
}, []);
const handleSubmit = useCallback(() => {
if (!formLine || !formMarketplace || !formDesc.trim()) return;
const newExp: Experiment = {
id: `exp_${Date.now()}`,
week: formWeek,
product_line: formLine,
marketplace: formMarketplace,
action_type: formAction,
description: formDesc.trim(),
expected_impact: formImpact,
status: 'in_progress',
created_at: new Date().toISOString(),
};
updateExperiments([...experiments, newExp]);
setShowForm(false);
setFormDesc('');
setFormWeek(getCurrentWeek());
}, [formWeek, formLine, formMarketplace, formAction, formDesc, formImpact, experiments, updateExperiments]);
const handleStatusChange = useCallback((id: string, newStatus: ExperimentStatus) => {
updateExperiments(experiments.map(e => e.id === id ? { ...e, status: newStatus } : e));
setEditingStatus(null);
}, [experiments, updateExperiments]);
const handleDelete = useCallback((id: string) => {
updateExperiments(experiments.filter(e => e.id !== id));
}, [experiments, updateExperiments]);
// Available weeks for the form dropdown
const availableWeeks = useMemo(() => [...weeks].slice(0, 20), [weeks]);
return (
<div className="flex flex-col gap-3">
{/* Header + Log Action Button */}
<div className="flex items-center justify-between bg-slate-900 border border-white/10 p-3 px-4 rounded-xl">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-fuchsia-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-fuchsia-500/20">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
</div>
<div>
<h3 className="text-sm font-black text-white uppercase tracking-wider">Experiment Tracker</h3>
<p className="text-[10px] text-slate-500 font-bold">{experiments.length} experiments logged</p>
</div>
</div>
<button
onClick={() => { setFormMarketplace(defaultMarketplace); setShowForm(true); }}
className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 rounded-lg text-xs font-bold transition-colors border border-indigo-500/50 shadow-lg shadow-indigo-500/20"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Log Action
</button>
</div>
{/* Chart */}
{chartData.length > 0 && (
<div className="bg-slate-900 border border-white/10 rounded-xl p-4 shadow-lg">
<div className="text-[10px] font-black text-slate-500 uppercase tracking-widest mb-3">
Total {primaryMetric === 'units' ? 'Units' : 'Revenue'} by Week
{experiments.length > 0 && (
<span className="ml-2 text-fuchsia-400">
{experiments.length} experiment{experiments.length !== 1 ? 's' : ''} marked
</span>
)}
</div>
<ResponsiveContainer width="100%" height={260}>
<LineChart data={chartData} margin={{ top: 10, right: 20, left: 10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis
dataKey="week"
stroke="#64748b"
tick={{ fontSize: 9, fontWeight: 'bold' }}
tickFormatter={(w: string) => `${w.split('-')[1]}/${w.split('-')[0].slice(-2)}`}
/>
<YAxis
stroke="#64748b"
tick={{ fontSize: 9 }}
tickFormatter={(v: number) => primaryMetric === 'revenue' ? `€${(v / 1000).toFixed(0)}k` : v.toLocaleString('de-DE')}
/>
<Tooltip
content={<ChartTooltipContent experiments={experiments} />}
cursor={{ stroke: '#6366f1', strokeWidth: 1, strokeDasharray: '4 4' }}
/>
<Line
type="monotone"
dataKey={primaryMetric}
stroke="#6366f1"
strokeWidth={2.5}
dot={{ fill: '#6366f1', r: 3, strokeWidth: 0 }}
activeDot={{ r: 5, fill: '#818cf8', strokeWidth: 2, stroke: '#6366f1' }}
name={primaryMetric === 'units' ? 'Units' : 'Revenue'}
/>
<Line
type="monotone"
dataKey="spend"
stroke="#f43f5e"
strokeWidth={1.5}
strokeDasharray="4 3"
dot={false}
name="Ads Spend"
/>
{/* Experiment markers */}
{chartData.map((point) => {
const exps = weekExperimentMap.get(point.week);
if (!exps || exps.length === 0) return null;
return (
<ReferenceDot
key={point.week}
x={point.week}
y={point[primaryMetric]}
shape={<ExperimentDot experiments={exps} />}
/>
);
})}
</LineChart>
</ResponsiveContainer>
</div>
)}
{/* Log Action Modal */}
{showForm && (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={() => setShowForm(false)}>
<div className="bg-slate-900 border border-white/15 rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between mb-5">
<h3 className="text-base font-black text-white uppercase tracking-wider">Log Experiment</h3>
<button onClick={() => setShowForm(false)} className="text-slate-500 hover:text-white transition-colors">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-4">
{/* Week */}
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1.5">Week</label>
<select
value={formWeek}
onChange={e => setFormWeek(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
{availableWeeks.map(w => (
<option key={w} value={w}>Week {w.split('-')[1]}/{w.split('-')[0]} {w === getCurrentWeek() ? '(Current)' : ''}</option>
))}
</select>
</div>
{/* Product Line */}
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1.5">Product Line</label>
<select
value={formLine}
onChange={e => setFormLine(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<option value="">Select product line...</option>
{productLines.map(line => (
<option key={line} value={line}>{line}</option>
))}
</select>
</div>
{/* Marketplace */}
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1.5">Marketplace</label>
<select
value={formMarketplace}
onChange={e => setFormMarketplace(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<option value="">Select marketplace...</option>
{marketplaces.map(mk => (
<option key={mk} value={mk}>{mk}</option>
))}
</select>
</div>
{/* Action Type */}
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1.5">Action Type</label>
<div className="flex flex-wrap gap-1.5">
{ACTION_TYPES.map(type => (
<button
key={type}
onClick={() => setFormAction(type)}
className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-all ${formAction === type
? ACTION_BG_CLASSES[type] + ' ring-1 ring-white/20'
: 'bg-slate-800 text-slate-500 border-white/5 hover:border-white/15'
}`}
>
{type}
</button>
))}
</div>
</div>
{/* Description */}
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1.5">Description</label>
<textarea
value={formDesc}
onChange={e => setFormDesc(e.target.value)}
placeholder="e.g., Added keyword 'caja musical' to title..."
rows={3}
className="w-full bg-slate-950 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 resize-none"
/>
</div>
{/* Expected Impact */}
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1.5">Expected Impact</label>
<div className="flex gap-2">
{EXPECTED_IMPACTS.map(impact => (
<button
key={impact}
onClick={() => setFormImpact(impact)}
className={`flex-1 px-3 py-2 rounded-lg text-xs font-bold border transition-all ${formImpact === impact
? 'bg-indigo-600/20 text-indigo-400 border-indigo-500/30 ring-1 ring-indigo-500/20'
: 'bg-slate-800 text-slate-500 border-white/5 hover:border-white/15'
}`}
>
{impact}
</button>
))}
</div>
</div>
</div>
{/* Submit */}
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-white/10">
<button
onClick={() => setShowForm(false)}
className="px-4 py-2 rounded-lg text-xs font-bold text-slate-400 bg-slate-800 hover:bg-slate-700 border border-white/5 transition-colors"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={!formLine || !formMarketplace || !formDesc.trim()}
className="px-5 py-2 rounded-lg text-xs font-bold text-white bg-indigo-600 hover:bg-indigo-500 border border-indigo-500/50 shadow-lg shadow-indigo-500/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>
Save Experiment
</button>
</div>
</div>
</div>
)}
{/* Experiments Panel */}
{experiments.length > 0 && (
<div className="bg-slate-900 border border-white/10 rounded-xl shadow-lg overflow-hidden">
<button
onClick={() => setShowPanel(!showPanel)}
className="w-full flex items-center justify-between p-3 px-4 hover:bg-white/[0.02] transition-colors"
>
<div className="flex items-center gap-2">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
Experiments Log ({experiments.length})
</span>
</div>
<svg className={`w-4 h-4 text-slate-500 transition-transform ${showPanel ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showPanel && (
<div className="overflow-x-auto border-t border-white/5">
<table className="w-full text-left border-collapse min-w-[900px]">
<thead>
<tr className="border-b border-white/10">
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Week</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Marketplace</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Product Line</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Action</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Description</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Expected</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Status</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest text-right">Delta %</th>
<th className="p-2.5 px-4 text-[10px] font-black text-slate-500 uppercase tracking-widest w-10"></th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{[...experiments].sort((a, b) => b.week.localeCompare(a.week)).map(exp => {
const { delta } = computeDelta(exp);
return (
<tr key={exp.id} className="hover:bg-white/[0.02] transition-colors">
<td className="p-2.5 px-4 text-xs font-bold text-white whitespace-nowrap">
W{exp.week.split('-')[1]}/{exp.week.split('-')[0].slice(-2)}
</td>
<td className="p-2.5 px-4">
<span className="text-[10px] text-sky-400 font-bold">{exp.marketplace || '—'}</span>
</td>
<td className="p-2.5 px-4">
<span className="text-[10px] text-fuchsia-400 font-bold uppercase tracking-widest">{exp.product_line}</span>
</td>
<td className="p-2.5 px-4">
<span className={`text-[10px] font-bold px-2 py-1 rounded border ${ACTION_BG_CLASSES[exp.action_type]}`}>
{exp.action_type}
</span>
</td>
<td className="p-2.5 px-4 text-xs text-white/70 max-w-[250px] truncate" title={exp.description}>
{exp.description}
</td>
<td className="p-2.5 px-4 text-[10px] text-slate-400 font-bold whitespace-nowrap">
{exp.expected_impact}
</td>
<td className="p-2.5 px-4">
{editingStatus === exp.id ? (
<select
value={exp.status}
onChange={e => handleStatusChange(exp.id, e.target.value as ExperimentStatus)}
onBlur={() => setEditingStatus(null)}
autoFocus
className="bg-slate-950 border border-white/10 rounded px-2 py-1 text-[10px] text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
{STATUSES.map(s => (
<option key={s} value={s}>{STATUS_LABELS[s]}</option>
))}
</select>
) : (
<button
onClick={() => setEditingStatus(exp.id)}
className={`text-[10px] font-bold px-2 py-1 rounded border cursor-pointer hover:ring-1 hover:ring-white/20 transition-all ${STATUS_CLASSES[exp.status]}`}
>
{STATUS_LABELS[exp.status]}
</button>
)}
</td>
<td className="p-2.5 px-4 text-right">
{delta !== null ? (
<span className={`text-xs font-black ${delta >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{delta >= 0 ? '+' : ''}{delta.toFixed(1)}%
</span>
) : (
<span className="text-[10px] text-slate-600 italic">Pending</span>
)}
</td>
<td className="p-2.5 px-4">
<button
onClick={() => handleDelete(exp.id)}
className="text-slate-600 hover:text-red-400 transition-colors"
title="Delete experiment"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
)}
</div>
);
};
export default ExperimentTracker;