Files
CrazeAnalytix/components/ExperimentBadge.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

116 lines
3.6 KiB
TypeScript

import React from 'react';
import { ActiveExperiment, ExperimentStatus } from '../types';
import { getExperimentStatusColor, getExperimentTypeColor, getExperimentIcon } from '../services/experiments';
interface ExperimentBadgeProps {
experiments: ActiveExperiment[];
onClick?: (experimentId: string) => void;
}
export const ExperimentBadge: React.FC<ExperimentBadgeProps> = ({ experiments, onClick }) => {
if (!experiments || experiments.length === 0) return null;
const activeExp = experiments.find(e => e.status === 'active');
const plannedExp = experiments.find(e => e.status === 'planned');
const hasPast = experiments.some(e => e.status === 'completed' || e.status === 'paused');
// Priority: active > planned > past
const displayExp = activeExp || plannedExp || experiments[0];
const getStatusBadge = (status: ExperimentStatus) => {
switch (status) {
case 'active': return '🟢';
case 'planned': return '🟡';
case 'completed': return '⚪';
case 'paused': return '⏸️';
}
};
const daysText = displayExp.days_remaining !== undefined
? displayExp.days_remaining > 0
? `${displayExp.days_remaining}d left`
: 'Ending soon'
: '';
return (
<div className="inline-flex items-center gap-1">
{experiments.length > 1 && (
<span className="text-[9px] text-slate-500 mr-1">+{experiments.length}</span>
)}
<button
onClick={(e) => {
e.stopPropagation();
onClick?.(displayExp.experiment_id);
}}
className={`
inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-medium
border ${getExperimentStatusColor(displayExp.status)}
hover:opacity-80 transition-opacity cursor-pointer
`}
title={`${displayExp.experiment_name}
Type: ${displayExp.type}
Start: ${displayExp.start_date}
End: ${displayExp.end_date || 'Ongoing'}
${daysText}`}
>
<span>{getExperimentIcon(displayExp.type)}</span>
<span className="hidden xl:inline truncate max-w-[80px]">{displayExp.experiment_name}</span>
{daysText && <span className="opacity-60">{daysText}</span>}
</button>
</div>
);
};
interface ExperimentStatusBadgeProps {
status: ExperimentStatus;
size?: 'sm' | 'md' | 'lg';
}
export const ExperimentStatusBadge: React.FC<ExperimentStatusBadgeProps> = ({ status, size = 'md' }) => {
const sizeClasses = {
sm: 'px-1.5 py-0.5 text-[9px]',
md: 'px-2 py-1 text-xs',
lg: 'px-3 py-1.5 text-sm',
};
const statusLabels = {
active: 'Active',
planned: 'Planned',
completed: 'Completed',
paused: 'Paused',
};
return (
<span className={`inline-flex items-center gap-1 rounded-full font-medium border ${getExperimentStatusColor(status)} ${sizeClasses[size]}`}>
{getStatusBadge(status)} {statusLabels[status]}
</span>
);
};
interface ExperimentTypeBadgeProps {
type: string;
size?: 'sm' | 'md' | 'lg';
}
export const ExperimentTypeBadge: React.FC<ExperimentTypeBadgeProps> = ({ type, size = 'md' }) => {
const sizeClasses = {
sm: 'px-1.5 py-0.5 text-[9px]',
md: 'px-2 py-1 text-xs',
lg: 'px-3 py-1.5 text-sm',
};
const typeLabels = {
pricing: 'Pricing',
advertising: 'Advertising',
content: 'Content',
promotion: 'Promotion',
};
return (
<span className={`inline-flex items-center gap-1 rounded-full font-medium border ${getExperimentTypeColor(type as any)} ${sizeClasses[size]}`}>
<span>{getExperimentIcon(type as any)}</span>
{typeLabels[type as keyof typeof typeLabels] || type}
</span>
);
};