185 lines
8.2 KiB
TypeScript
185 lines
8.2 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import { useRoutineStore } from '@/lib/routine-store';
|
|
import { RoutineEditor } from '@/components/RoutineEditor';
|
|
import { RoutineRunner } from '@/components/RoutineRunner';
|
|
import type { TransitionType } from '@/lib/routines';
|
|
import { ArrowLeft, Plus, Play, Trash2, ListChecks, Clock, Copy } from 'lucide-react';
|
|
|
|
export default function RoutinesPage() {
|
|
const { routines, templates, addRoutine, addTemplate, removeRoutine, removeTemplate, start, startFromTemplate } = useRoutineStore();
|
|
const [mounted, setMounted] = useState(false);
|
|
const [showEditor, setShowEditor] = useState(false);
|
|
|
|
useEffect(() => { setMounted(true); }, []);
|
|
if (!mounted) return null;
|
|
|
|
const activeRoutine = routines.find((r) => r.status === 'active' || r.status === 'paused');
|
|
const readyRoutines = routines.filter((r) => r.status === 'ready');
|
|
const pastRoutines = routines.filter((r) => r.status === 'completed' || r.status === 'cancelled').slice(-10).reverse();
|
|
|
|
const handleSave = (name: string, description: string, steps: { label: string; durationMinutes: number; transition: TransitionType; customTransitionMinutes: number; notes: string }[], asTemplate: boolean) => {
|
|
const params = {
|
|
name,
|
|
description: description || undefined,
|
|
steps: steps.map((s) => ({
|
|
label: s.label,
|
|
durationMinutes: s.durationMinutes,
|
|
transition: s.transition as TransitionType,
|
|
customTransitionMinutes: s.transition === 'custom' ? s.customTransitionMinutes : undefined,
|
|
notes: s.notes || undefined,
|
|
})),
|
|
isTemplate: asTemplate,
|
|
};
|
|
if (asTemplate) {
|
|
addTemplate(params);
|
|
} else {
|
|
addRoutine(params);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen" style={{ backgroundColor: 'var(--cm-bg-canvas)' }}>
|
|
<div className="max-w-3xl mx-auto px-4 py-8">
|
|
<Link href="/" className="flex items-center gap-2 text-sm mb-6" style={{ color: 'var(--cm-accent)' }}>
|
|
<ArrowLeft size={16} /> Back to Dashboard
|
|
</Link>
|
|
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-2xl font-bold" style={{ color: 'var(--cm-text-primary)' }}>Routines</h1>
|
|
<button
|
|
onClick={() => setShowEditor(true)}
|
|
className="flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium cursor-pointer"
|
|
style={{ backgroundColor: 'var(--cm-accent)', color: 'var(--cm-white)' }}
|
|
>
|
|
<Plus size={16} /> New Routine
|
|
</button>
|
|
</div>
|
|
|
|
{/* Active routine runner */}
|
|
{activeRoutine && (
|
|
<div className="mb-8">
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider mb-3 flex items-center gap-2" style={{ color: 'var(--cm-text-tertiary)' }}>
|
|
<Play size={14} /> Running
|
|
</h2>
|
|
<RoutineRunner routine={activeRoutine} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Templates */}
|
|
<div className="mb-8">
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider mb-3 flex items-center gap-2" style={{ color: 'var(--cm-text-tertiary)' }}>
|
|
<Copy size={14} /> Templates ({templates.length})
|
|
</h2>
|
|
{templates.length > 0 ? (
|
|
<div className="grid gap-3">
|
|
{templates.map((tmpl) => (
|
|
<div
|
|
key={tmpl.id}
|
|
className="rounded-xl border p-4 flex items-center gap-4"
|
|
style={{ backgroundColor: 'var(--cm-surface-card)', borderColor: 'var(--cm-border)' }}
|
|
>
|
|
<div className="flex-1">
|
|
<div className="font-medium text-sm" style={{ color: 'var(--cm-text-primary)' }}>{tmpl.name}</div>
|
|
{tmpl.description && (
|
|
<div className="text-xs mt-0.5" style={{ color: 'var(--cm-text-tertiary)' }}>{tmpl.description}</div>
|
|
)}
|
|
<div className="flex items-center gap-3 mt-1.5 text-xs" style={{ color: 'var(--cm-text-tertiary)' }}>
|
|
<span className="flex items-center gap-1"><ListChecks size={12} /> {tmpl.steps.length} steps</span>
|
|
<span className="flex items-center gap-1"><Clock size={12} /> {tmpl.totalDurationMinutes}m</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => startFromTemplate(tmpl.id)}
|
|
disabled={!!activeRoutine}
|
|
className="flex items-center gap-1.5 px-3 py-2 rounded-lg text-xs font-medium cursor-pointer disabled:opacity-30"
|
|
style={{ backgroundColor: 'var(--cm-success-15)', color: 'var(--cm-success)' }}
|
|
>
|
|
<Play size={14} /> Start
|
|
</button>
|
|
{!tmpl.isTemplate || tmpl.createdAt > Date.now() - 1000 ? (
|
|
<button
|
|
onClick={() => removeTemplate(tmpl.id)}
|
|
className="p-1.5 rounded cursor-pointer"
|
|
style={{ color: 'var(--cm-text-tertiary)' }}
|
|
>
|
|
<Trash2 size={14} />
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm" style={{ color: 'var(--cm-text-tertiary)' }}>No templates yet. Create one above.</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Ready routines */}
|
|
{readyRoutines.length > 0 && (
|
|
<div className="mb-8">
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider mb-3 flex items-center gap-2" style={{ color: 'var(--cm-text-tertiary)' }}>
|
|
<ListChecks size={14} /> Ready ({readyRoutines.length})
|
|
</h2>
|
|
<div className="grid gap-3">
|
|
{readyRoutines.map((r) => (
|
|
<div
|
|
key={r.id}
|
|
className="rounded-xl border p-4 flex items-center gap-4"
|
|
style={{ backgroundColor: 'var(--cm-surface-card)', borderColor: 'var(--cm-border)' }}
|
|
>
|
|
<div className="flex-1">
|
|
<div className="font-medium text-sm" style={{ color: 'var(--cm-text-primary)' }}>{r.name}</div>
|
|
<div className="flex items-center gap-3 mt-1 text-xs" style={{ color: 'var(--cm-text-tertiary)' }}>
|
|
<span>{r.steps.length} steps</span>
|
|
<span>{r.totalDurationMinutes}m</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => start(r.id)}
|
|
disabled={!!activeRoutine}
|
|
className="flex items-center gap-1.5 px-3 py-2 rounded-lg text-xs font-medium cursor-pointer disabled:opacity-30"
|
|
style={{ backgroundColor: 'var(--cm-accent)', color: 'var(--cm-white)' }}
|
|
>
|
|
<Play size={14} /> Start
|
|
</button>
|
|
<button
|
|
onClick={() => removeRoutine(r.id)}
|
|
className="p-1.5 rounded cursor-pointer"
|
|
style={{ color: 'var(--cm-text-tertiary)' }}
|
|
>
|
|
<Trash2 size={14} />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Past routines */}
|
|
{pastRoutines.length > 0 && (
|
|
<div>
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider mb-3" style={{ color: 'var(--cm-text-tertiary)' }}>
|
|
Recent ({pastRoutines.length})
|
|
</h2>
|
|
<div className="grid gap-2">
|
|
{pastRoutines.map((r) => (
|
|
<div
|
|
key={r.id}
|
|
className="rounded-xl border p-3 flex items-center gap-3"
|
|
style={{ backgroundColor: 'var(--cm-surface-card)', borderColor: 'var(--cm-border)' }}
|
|
>
|
|
<RoutineRunner routine={r} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<RoutineEditor isOpen={showEditor} onClose={() => setShowEditor(false)} onSave={handleSave} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|