mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 16:25:23 +02:00
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>
339 lines
13 KiB
TypeScript
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;
|