Files
Dramlog-Prod/src/components/OnboardingTutorial.tsx
robin 096daffb3e feat: Upgrade to Tailwind CSS v4.1.18
- Migrate from tailwindcss v3.3 to v4.1.18
- Replace @tailwind directives with @import 'tailwindcss'
- Move custom colors to @theme block in globals.css
- Convert custom utilities to @utility syntax
- Update PostCSS config to use @tailwindcss/postcss
- Remove autoprefixer (now built-in)
2026-01-19 22:26:21 +01:00

181 lines
6.3 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import { Scan, GlassWater, Users, Settings, ArrowRight, X, Sparkles } from 'lucide-react';
import { useI18n } from '@/i18n/I18nContext';
import { useAuth } from '@/context/AuthContext';
const ONBOARDING_KEY = 'dramlog_onboarding_complete';
interface OnboardingStep {
id: string;
icon: React.ReactNode;
title: string;
description: string;
}
const getSteps = (t: (path: string) => string): OnboardingStep[] => [
{
id: 'welcome',
icon: <Sparkles size={32} className="text-orange-500" />,
title: t('tutorial.steps.welcome.title'),
description: t('tutorial.steps.welcome.desc'),
},
{
id: 'scan',
icon: <Scan size={32} className="text-orange-500" />,
title: t('tutorial.steps.scan.title'),
description: t('tutorial.steps.scan.desc'),
},
{
id: 'taste',
icon: <GlassWater size={32} className="text-orange-500" />,
title: t('tutorial.steps.taste.title'),
description: t('tutorial.steps.taste.desc'),
},
{
id: 'activity',
icon: <Users size={32} className="text-orange-500" />,
title: t('tutorial.steps.activity.title'),
description: t('tutorial.steps.activity.desc'),
},
{
id: 'ready',
icon: <Settings size={32} className="text-orange-500" />,
title: t('tutorial.steps.ready.title'),
description: t('tutorial.steps.ready.desc'),
},
];
export default function OnboardingTutorial() {
const { t } = useI18n();
const { user, isLoading } = useAuth();
const STEPS = getSteps(t);
const [isOpen, setIsOpen] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const pathname = usePathname();
useEffect(() => {
// Don't show if auth is still loading
if (isLoading) return;
// Don't show if no user is logged in
if (!user) {
setIsOpen(false);
return;
}
// Don't show on login/auth pages
if (pathname === '/login' || pathname === '/auth' || pathname === '/register') {
return;
}
// Check if onboarding was completed
const completed = localStorage.getItem(ONBOARDING_KEY);
if (!completed) {
// Small delay to not overwhelm on first load
const timer = setTimeout(() => setIsOpen(true), 1000);
return () => clearTimeout(timer);
}
}, [pathname]);
const handleNext = () => {
if (currentStep < STEPS.length - 1) {
setCurrentStep(prev => prev + 1);
} else {
handleComplete();
}
};
const handleComplete = () => {
localStorage.setItem(ONBOARDING_KEY, 'true');
setIsOpen(false);
};
const handleSkip = () => {
localStorage.setItem(ONBOARDING_KEY, 'true');
setIsOpen(false);
};
if (!isOpen) return null;
const step = STEPS[currentStep];
const isLastStep = currentStep === STEPS.length - 1;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-200 bg-black/90 backdrop-blur-xs flex items-center justify-center p-6"
>
{/* Close button */}
<button
onClick={handleSkip}
className="absolute top-6 right-6 p-2 text-zinc-500 hover:text-white transition-colors"
>
<X size={24} />
</button>
<motion.div
key={step.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="max-w-sm w-full"
>
{/* Icon */}
<div className="w-20 h-20 mx-auto rounded-3xl bg-zinc-900 border border-zinc-800 flex items-center justify-center mb-8">
{step.icon}
</div>
{/* Content */}
<h2 className="text-2xl font-bold text-white text-center mb-3">
{step.title}
</h2>
<p className="text-zinc-400 text-center text-sm leading-relaxed mb-8">
{step.description}
</p>
{/* Progress dots */}
<div className="flex justify-center gap-2 mb-8">
{STEPS.map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded-full transition-all ${index === currentStep
? 'w-6 bg-orange-500'
: index < currentStep
? 'bg-orange-500/50'
: 'bg-zinc-700'
}`}
/>
))}
</div>
{/* Buttons */}
<div className="flex gap-3">
{!isLastStep && (
<button
onClick={handleSkip}
className="flex-1 py-3 px-4 text-sm font-bold text-zinc-500 hover:text-white transition-colors"
>
{t('tutorial.skip')}
</button>
)}
<button
onClick={handleNext}
className="flex-1 py-3 px-4 bg-orange-600 hover:bg-orange-500 text-white font-bold text-sm rounded-xl flex items-center justify-center gap-2 transition-colors"
>
{isLastStep ? t('tutorial.finish') : t('tutorial.next')}
<ArrowRight size={16} />
</button>
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
}