looood / src /components /onboarding.tsx
looda3131's picture
Clean push without any binary history
cc276cc
"use client";
import React, { useState } from 'react';
import { doc, setDoc, getDoc, writeBatch } from "firebase/firestore";
import { useAuth } from '@/contexts/auth-context';
import { useFirebase } from '@/contexts/firebase-context';
import { useSettings } from '@/contexts/settings-context';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Logo } from './logo';
import { Sparkles, Languages, HelpCircle, ArrowRight, ArrowLeft } from 'lucide-react';
import words from 'mnemonic-words';
import { cn } from '@/lib/utils';
import type { Language } from '@/lib/types';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '@/components/ui/dialog';
import { Separator } from './ui/separator';
import { Progress } from './ui/progress';
const InfoBubble = ({ text, emoji }: { text: string, emoji: string }) => (
<div className="info-bubble pointer-events-none">
<span className="mr-1">{emoji}</span>
{text}
</div>
);
const TOTAL_STEPS = 3;
const StepIndicator = ({ current, total, t }: { current: number, total: number, t: (key: string) => string }) => (
<div className="w-full px-2 pb-2">
<Progress value={(current / total) * 100} className="h-2" />
<p className="text-xs text-muted-foreground text-center mt-2">
{t('step')} {current} / {total}
</p>
</div>
);
export const Onboarding = () => {
const { setAuthStatus, auth, handleAuthenticatedUser } = useAuth();
const { db } = useFirebase();
const { addToast, t, language, setLanguage, playSound } = useSettings();
const [displayName, setDisplayName] = useState('');
const [publicId, setPublicId] = useState('');
const [recoveryId, setRecoveryId] = useState('');
const [step, setStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const handleNextStep = () => {
playSound('touch');
if (step === 1 && !displayName.trim()) {
addToast(t('nameCannotBeEmpty'));
return;
}
if (step === 2 && publicId.trim().length < 4) {
addToast(t('publicIdMinLength'));
return;
}
setStep(prev => prev + 1);
}
const handlePrevStep = () => {
playSound('touch');
setStep(prev => prev - 1);
}
const handleCreateProfile = async () => {
setIsLoading(true);
playSound('touch');
const trimmedDisplayName = displayName.trim();
const trimmedPublicId = publicId.trim().toLowerCase();
const trimmedRecoveryId = recoveryId.trim();
if (!trimmedRecoveryId || trimmedRecoveryId.length < 8) {
addToast("Recovery ID must be at least 8 characters.");
setIsLoading(false);
return;
}
try {
const publicIdRef = doc(db, 'publicIds', trimmedPublicId);
const recoveryIdRef = doc(db, 'recovery', trimmedRecoveryId);
const [publicIdSnap, recoveryIdSnap] = await Promise.all([
getDoc(publicIdRef),
getDoc(recoveryIdRef)
]);
if (publicIdSnap.exists()) {
addToast("This Public ID is already taken. Please choose another.");
setStep(2); // Go back to public ID step
setIsLoading(false);
return;
}
if (recoveryIdSnap.exists()) {
addToast("This Recovery ID is already in use. Please choose a more unique one.");
setIsLoading(false);
return;
}
const user = auth.currentUser;
if (!user) throw new Error("User not signed in.");
const androidId = localStorage.getItem('android_id');
const profile = {
uid: user.uid,
displayName: trimmedDisplayName,
publicId: trimmedPublicId,
photoURL: `https://api.dicebear.com/8.x/initials/svg?seed=${trimmedDisplayName}`,
...(androidId && { androidId }),
};
const batch = writeBatch(db);
batch.set(doc(db, 'users', user.uid), profile);
batch.set(doc(db, 'publicIds', trimmedPublicId), { uid: user.uid });
batch.set(recoveryIdRef, { uid: user.uid });
await batch.commit();
setStep(TOTAL_STEPS + 1); // Go to confirmation step
} catch (error: any) {
console.error("Profile creation error:", error);
addToast(`Error: ${error.message}`);
} finally {
setIsLoading(false);
}
};
const handleFinishOnboarding = () => {
playSound('touch');
if (auth.currentUser) {
handleAuthenticatedUser(auth.currentUser);
} else {
setAuthStatus('loading');
}
}
const generateRandomRecoveryId = () => {
playSound('touch');
const randomWords = [];
for (let i = 0; i < 4; i++) {
const randomIndex = Math.floor(Math.random() * words.length);
randomWords.push(words[randomIndex]);
}
setRecoveryId(randomWords.join('-'));
};
const handleLanguageChange = (lang: Language) => {
playSound('touch');
setLanguage(lang);
};
if (step > TOTAL_STEPS) {
return (
<div className="min-h-svh flex items-center justify-center bg-background p-4">
<Card className="w-full max-w-md">
<CardHeader>
<Logo />
<CardTitle className="text-center">{t('recoveryTitle')}</CardTitle>
<CardDescription className="text-center">{t('recoveryDescription')}</CardDescription>
</CardHeader>
<CardContent>
<Alert variant="default">
<AlertTitle>{t('yourRecoveryId')}</AlertTitle>
<AlertDescription>
{t('youChose')}: <strong className="font-mono">{recoveryId}</strong>. {t('keepItSafe')}!
</AlertDescription>
</Alert>
</CardContent>
<CardFooter>
<Button onClick={handleFinishOnboarding} className="w-full btn-gradient">{t('continueToApp')}</Button>
</CardFooter>
</Card>
</div>
);
}
const renderStepContent = () => {
switch (step) {
case 1:
return (
<div className="space-y-1">
<Label htmlFor="displayName">{t('displayNameLabel')}</Label>
<Input id="displayName" type="text" value={displayName} onChange={e => setDisplayName(e.target.value)} placeholder={t('displayNameLabel')} autoFocus/>
<p className="text-xs text-muted-foreground px-2">{t('displayNameHint')}</p>
</div>
);
case 2:
return (
<div className="space-y-1">
<Label htmlFor="publicId">{t('publicIdLabel')}</Label>
<Input id="publicId" type="text" value={publicId} onChange={e => setPublicId(e.target.value.replace(/[^a-z0-9-]/gi, '').toLowerCase())} placeholder={t('publicIdLabel')} autoFocus/>
<p className="text-xs text-muted-foreground px-2">{t('publicIdHint')}</p>
</div>
);
case 3:
return (
<div className="space-y-1">
<Label htmlFor="recoveryId">{t('recoveryIdPlaceholder')}</Label>
<div className="flex items-center gap-2">
<Input id="recoveryId" type="text" value={recoveryId} onChange={e => setRecoveryId(e.target.value)} placeholder={t('recoveryIdPlaceholder')} className="flex-1" autoFocus/>
<Button variant="ghost" size="icon" onClick={generateRandomRecoveryId} title="Generate random ID">
<Sparkles className="h-5 w-5" />
</Button>
</div>
<p className="text-xs text-muted-foreground px-2">{t('recoveryIdHint')}</p>
</div>
);
default:
return null;
}
}
return (
<div className="min-h-svh flex justify-center bg-background p-4 sm:p-6 md:p-8" dir={language === 'ar' ? 'rtl' : 'ltr'}>
<div className="absolute top-0 left-0 right-0 h-screen w-screen overflow-hidden pointer-events-none">
<InfoBubble emoji="💡" text={t('bubble1')} />
<InfoBubble emoji="🔒" text={t('bubble2')} />
<InfoBubble emoji="👤" text={t('bubble3')} />
<InfoBubble emoji="✨" text={t('bubble4')} />
</div>
<Card className="w-full max-w-sm z-10 bg-background/80 backdrop-blur-sm self-center">
<CardHeader>
<div className="relative w-full flex justify-center mb-4">
<Logo />
</div>
<div className="flex items-center justify-center gap-2 pt-2">
<CardTitle className="text-center">{t('createProfileTitle')}</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6">
<HelpCircle className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('howLoginWorksTitle')}</DialogTitle>
</DialogHeader>
<div className="space-y-4 text-sm text-muted-foreground">
<p>{t('howLoginWorksP1')}</p>
<ul className="space-y-2 list-disc pl-5">
<li><strong className="text-foreground">{t('howLoginWorksL1Title')}</strong> {t('howLoginWorksL1Text')}</li>
<li><strong className="text-foreground">{t('howLoginWorksL2Title')}</strong> {t('howLoginWorksL2Text')}</li>
</ul>
<p>{t('howLoginWorksP2')}</p>
</div>
</DialogContent>
</Dialog>
</div>
<CardDescription className="text-center pt-2">
{t('onboardingDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<StepIndicator current={step} total={TOTAL_STEPS} t={t} />
{renderStepContent()}
</CardContent>
<CardFooter className="flex-col gap-4">
<div className="w-full flex justify-between items-center">
<Button variant="outline" onClick={handlePrevStep} disabled={step === 1}>
<ArrowLeft className="h-4 w-4" />
</Button>
{step < TOTAL_STEPS ? (
<Button onClick={handleNextStep}>
{t('nextStep')} <ArrowRight className="h-4 w-4" />
</Button>
) : (
<Button onClick={handleCreateProfile} disabled={isLoading} className="btn-gradient flex-1">
{isLoading ? t('creating') : t('createProfileButton')}
</Button>
)}
</div>
<Separator />
<div className="w-full flex justify-center items-center gap-2">
<Button variant={language === 'en' ? 'secondary' : 'ghost'} size="sm" onClick={() => handleLanguageChange('en')}>English</Button>
<Languages className="w-4 h-4 text-muted-foreground"/>
<Button variant={language === 'ar' ? 'secondary' : 'ghost'} size="sm" onClick={() => handleLanguageChange('ar')}>العربية</Button>
</div>
<p className="text-center text-sm text-muted-foreground">
{t('haveAnAccount')}{' '}
<Button variant="link" className="p-0" onClick={() => { playSound('touch'); setAuthStatus('recovery'); }}>
{t('recoverAccount')}
</Button>
</p>
</CardFooter>
</Card>
</div>
);
};