Files
CrazeAnalytix/components/ExperimentForm.tsx
T
Christian Vidal WolfandQwen-Coder f93d92da0b Add complete Experiments tracking system
New Features:
- Experiments tab with full CRUD operations
- Create experiments by ASIN or product line groups
- Track pricing, advertising, content, and promotion experiments
- Performance analytics with baseline vs experiment comparison
- Visual experiment badges in Weekly Sales grid
- Experiment detail view with metrics and learnings

Technical Changes:
- Add Supabase experiments table migration
- New services/experiments.ts for CRUD + calculations
- New components: ExperimentsView, ExperimentDetail, ExperimentForm, ExperimentBadge
- Integrate experiment indicators in WeeklyGrid
- Add navigation tab (desktop + mobile)
- Type definitions in types.ts

Usage:
1. Run SQL migration in Supabase
2. Navigate to Experiments tab
3. Create experiments with ASINs, dates, hypothesis
4. View performance lift after completion
5. See active experiments marked in Weekly Sales

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-20 19:31:44 +01:00

339 lines
13 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import { ExperimentCreateInput, ExperimentType, ExperimentMetric } from '../types';
import { createExperiment } from '../services/experiments';
interface ExperimentFormProps {
onClose: () => void;
onSuccess: () => void;
initialAsins?: string[];
initialMarketplace?: string;
}
const ExperimentForm: React.FC<ExperimentFormProps> = ({
onClose,
onSuccess,
initialAsins = [],
initialMarketplace = 'DE'
}) => {
const [formData, setFormData] = useState<ExperimentCreateInput>({
name: '',
description: '',
type: 'pricing',
asins: initialAsins,
marketplace: initialMarketplace,
start_date: new Date().toISOString().split('T')[0],
end_date: '',
hypothesis: '',
primary_metric: 'units',
target_lift_percent: 10,
owner: '',
});
const [loading, setLoading] = useState(false);
const [asinInput, setAsinInput] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name || !formData.asins.length) {
alert('Please fill in required fields');
return;
}
setLoading(true);
try {
await createExperiment(formData);
onSuccess();
onClose();
} catch (error: any) {
alert(`Error creating experiment: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleAddAsin = () => {
const newAsins = asinInput
.split(',')
.map(a => a.trim().toUpperCase())
.filter(a => a.length > 0);
const uniqueAsins = Array.from(new Set([...formData.asins, ...newAsins]));
setFormData({ ...formData, asins: uniqueAsins });
setAsinInput('');
};
const handleRemoveAsin = (asinToRemove: string) => {
setFormData({
...formData,
asins: formData.asins.filter(a => a !== asinToRemove),
});
};
const typeOptions: { value: ExperimentType; label: string; icon: string }[] = [
{ value: 'pricing', label: 'Pricing', icon: '💰' },
{ value: 'advertising', label: 'Advertising', icon: '📢' },
{ value: 'content', label: 'Content', icon: '📝' },
{ value: 'promotion', label: 'Promotion', icon: '🏷️' },
];
const metricOptions: { value: ExperimentMetric; label: string }[] = [
{ value: 'units', label: 'Units Sold' },
{ value: 'revenue', label: 'Revenue' },
{ value: 'acos', label: 'ACOS' },
{ value: 'ctr', label: 'CTR' },
{ value: 'cvr', label: 'Conversion Rate' },
{ value: 'bsr', label: 'BSR' },
];
const marketplaceOptions = ['DE', 'UK', 'FR', 'IT', 'ES'];
return (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-slate-900 border border-slate-700 rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-slate-700">
<h2 className="text-xl font-bold text-white">🧪 Create New Experiment</h2>
<button
onClick={onClose}
className="p-2 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-6 space-y-6">
{/* Name */}
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Experiment Name <span className="text-red-400">*</span>
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="e.g., Q1 Price Reduction Test"
required
/>
</div>
{/* Type */}
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Experiment Type <span className="text-red-400">*</span>
</label>
<div className="grid grid-cols-2 gap-2">
{typeOptions.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setFormData({ ...formData, type: opt.value })}
className={`p-3 rounded-lg border text-sm font-medium transition-colors flex items-center gap-2 ${
formData.type === opt.value
? 'bg-indigo-600/20 border-indigo-500 text-indigo-400'
: 'bg-slate-800 border-slate-600 text-slate-400 hover:border-slate-500'
}`}
>
<span className="text-lg">{opt.icon}</span>
{opt.label}
</button>
))}
</div>
</div>
{/* Marketplace */}
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Marketplace <span className="text-red-400">*</span>
</label>
<div className="flex gap-2">
{marketplaceOptions.map((mp) => (
<button
key={mp}
type="button"
onClick={() => setFormData({ ...formData, marketplace: mp })}
className={`px-4 py-2 rounded-lg border text-sm font-medium transition-colors ${
formData.marketplace === mp
? 'bg-indigo-600/20 border-indigo-500 text-indigo-400'
: 'bg-slate-800 border-slate-600 text-slate-400 hover:border-slate-500'
}`}
>
{mp}
</button>
))}
</div>
</div>
{/* ASINs */}
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Target ASINs <span className="text-red-400">*</span>
</label>
<div className="flex gap-2 mb-2">
<input
type="text"
value={asinInput}
onChange={(e) => setAsinInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), handleAddAsin())}
className="flex-1 bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="Enter ASINs (comma-separated)"
/>
<button
type="button"
onClick={handleAddAsin}
className="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg transition-colors"
>
Add
</button>
</div>
{formData.asins.length > 0 && (
<div className="flex flex-wrap gap-2 p-3 bg-slate-800/50 border border-slate-700 rounded-lg">
{formData.asins.map((asin) => (
<span
key={asin}
className="inline-flex items-center gap-1 px-2.5 py-1 bg-slate-700 border border-slate-600 rounded-lg text-sm text-slate-300 font-mono"
>
{asin}
<button
type="button"
onClick={() => handleRemoveAsin(asin)}
className="text-slate-400 hover:text-red-400 transition-colors"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</span>
))}
</div>
)}
</div>
{/* Timeline */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Start Date <span className="text-red-400">*</span>
</label>
<input
type="date"
value={formData.start_date}
onChange={(e) => setFormData({ ...formData, start_date: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
End Date
</label>
<input
type="date"
value={formData.end_date}
onChange={(e) => setFormData({ ...formData, end_date: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
</div>
{/* Hypothesis */}
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Hypothesis
</label>
<textarea
value={formData.hypothesis}
onChange={(e) => setFormData({ ...formData, hypothesis: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
rows={3}
placeholder="What do you expect to happen? e.g., 'Reducing price by 10% will increase units sold by 25%'"
/>
</div>
{/* Primary Metric & Target Lift */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Primary Metric
</label>
<select
value={formData.primary_metric}
onChange={(e) => setFormData({ ...formData, primary_metric: e.target.value as ExperimentMetric })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
{metricOptions.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Target Lift (%)
</label>
<input
type="number"
value={formData.target_lift_percent}
onChange={(e) => setFormData({ ...formData, target_lift_percent: parseFloat(e.target.value) || 0 })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
min="0"
max="1000"
/>
</div>
</div>
{/* Owner */}
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Owner
</label>
<input
type="text"
value={formData.owner}
onChange={(e) => setFormData({ ...formData, owner: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="Your name"
/>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Description
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
rows={3}
placeholder="Additional details about this experiment..."
/>
</div>
{/* Actions */}
<div className="flex gap-3 pt-4 border-t border-slate-700">
<button
type="button"
onClick={onClose}
className="flex-1 px-4 py-2.5 bg-slate-800 hover:bg-slate-700 text-white rounded-lg font-medium transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="flex-1 px-4 py-2.5 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-600/50 text-white rounded-lg font-medium transition-colors"
>
{loading ? 'Creating...' : 'Create Experiment'}
</button>
</div>
</form>
</div>
</div>
);
};
export default ExperimentForm;