- OnboardingOverlay: 3-step walkthrough for first-time users (localStorage flag) - ambient-sounds.ts: Web Audio API noise generators (rain, white noise, brown noise, coffee shop) - FocusView: ambient sound picker with volume slider, auto-stops on session end
168 lines
5.0 KiB
TypeScript
168 lines
5.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Clock, Timer, Bell, ChevronRight, X } from 'lucide-react';
|
|
|
|
const ONBOARDING_KEY = 'cm-onboarding-complete';
|
|
|
|
interface OnboardingStep {
|
|
title: string;
|
|
description: string;
|
|
icon: React.ReactNode;
|
|
hint: string;
|
|
}
|
|
|
|
const STEPS: OnboardingStep[] = [
|
|
{
|
|
title: 'Create a Timer',
|
|
description: 'Tap "New Timer" or press N to create your first timer. Try natural language like "meeting in 30 min".',
|
|
icon: <Timer size={32} />,
|
|
hint: 'Timers can be alarms, countdowns, Pomodoro sessions, or event countdowns.',
|
|
},
|
|
{
|
|
title: 'Set Pre-Warning Cascades',
|
|
description: 'Choose how early you want reminders — Aggressive gives you many warnings, Light just a few gentle nudges.',
|
|
icon: <Bell size={32} />,
|
|
hint: 'Cascades are what make ChronoMind unique. Never be caught off-guard again.',
|
|
},
|
|
{
|
|
title: 'Watch Your Timeline',
|
|
description: 'Active timers appear here sorted by fire time. Each card shows countdown, cascade progress, and urgency level.',
|
|
icon: <Clock size={32} />,
|
|
hint: 'Try Focus mode (eye icon) for distraction-free sessions, or Routines for multi-step workflows.',
|
|
},
|
|
];
|
|
|
|
export function OnboardingOverlay() {
|
|
const [step, setStep] = useState(0);
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
const done = localStorage.getItem(ONBOARDING_KEY);
|
|
if (!done) setVisible(true);
|
|
}, []);
|
|
|
|
const dismiss = () => {
|
|
setVisible(false);
|
|
localStorage.setItem(ONBOARDING_KEY, 'true');
|
|
};
|
|
|
|
const next = () => {
|
|
if (step < STEPS.length - 1) {
|
|
setStep(step + 1);
|
|
} else {
|
|
dismiss();
|
|
}
|
|
};
|
|
|
|
if (!visible) return null;
|
|
|
|
const current = STEPS[step];
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
|
{/* Backdrop */}
|
|
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" />
|
|
|
|
{/* Card */}
|
|
<div
|
|
className="relative w-full max-w-sm mx-4 rounded-2xl border shadow-2xl overflow-hidden"
|
|
style={{
|
|
backgroundColor: 'var(--cm-bg-elevated)',
|
|
borderColor: 'var(--cm-border)',
|
|
}}
|
|
>
|
|
{/* Close */}
|
|
<button
|
|
onClick={dismiss}
|
|
className="absolute top-3 right-3 p-1 rounded-lg transition-colors cursor-pointer z-10"
|
|
style={{ color: 'var(--cm-text-tertiary)' }}
|
|
aria-label="Skip onboarding"
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
|
|
{/* Step indicator */}
|
|
<div className="flex gap-1.5 px-6 pt-5">
|
|
{STEPS.map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="flex-1 h-1 rounded-full transition-colors"
|
|
style={{
|
|
backgroundColor: i <= step ? 'var(--cm-accent)' : 'var(--cm-surface-muted)',
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="px-6 pt-6 pb-4 text-center">
|
|
<div
|
|
className="w-16 h-16 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
|
style={{ backgroundColor: 'rgba(90,140,255,0.12)', color: 'var(--cm-accent)' }}
|
|
>
|
|
{current.icon}
|
|
</div>
|
|
<h3
|
|
className="text-lg font-semibold mb-2"
|
|
style={{ color: 'var(--cm-text-primary)' }}
|
|
>
|
|
{current.title}
|
|
</h3>
|
|
<p
|
|
className="text-sm leading-relaxed mb-3"
|
|
style={{ color: 'var(--cm-text-secondary)' }}
|
|
>
|
|
{current.description}
|
|
</p>
|
|
<p
|
|
className="text-xs italic"
|
|
style={{ color: 'var(--cm-text-tertiary)' }}
|
|
>
|
|
{current.hint}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="px-6 pb-6 flex gap-3">
|
|
{step > 0 && (
|
|
<button
|
|
onClick={() => setStep(step - 1)}
|
|
className="flex-1 py-2.5 rounded-xl text-sm font-medium transition-colors cursor-pointer"
|
|
style={{
|
|
backgroundColor: 'var(--cm-surface-muted)',
|
|
color: 'var(--cm-text-secondary)',
|
|
}}
|
|
>
|
|
Back
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={next}
|
|
className="flex-1 py-2.5 rounded-xl text-sm font-semibold transition-colors cursor-pointer flex items-center justify-center gap-1"
|
|
style={{
|
|
backgroundColor: 'var(--cm-accent)',
|
|
color: '#fff',
|
|
}}
|
|
>
|
|
{step === STEPS.length - 1 ? 'Get Started' : 'Next'}
|
|
{step < STEPS.length - 1 && <ChevronRight size={16} />}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Skip link */}
|
|
<div className="pb-4 text-center">
|
|
<button
|
|
onClick={dismiss}
|
|
className="text-xs cursor-pointer underline"
|
|
style={{ color: 'var(--cm-text-tertiary)' }}
|
|
>
|
|
Skip walkthrough
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|