mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
feat: Add premium marketers page + 3-phase onboarding flow
Marketers page (435 lines): - Hero with gradient background + bilingual text - Stats bar (avg commission, active marketers, total paid) - 4 benefit cards (instant commission, pro tools, support, transparency) - 3-step how-it-works section - Commission tiers (Bronze 10%, Silver 15%, Gold 20%) - 2 Arabic testimonials - 5 FAQ accordion items - Registration form with +966 phone - All bilingual with useI18n Onboarding flow (429 lines): - Phase 1: Welcome + 2-question survey (role + industry) - Phase 2: Guided first deal creation with Saudi sample data - Phase 3: Setup checklist (contacts, WhatsApp, pipeline, team) - Celebration animation on deal creation - Progress tracking - All bilingual with RTL support https://claude.ai/code/session_01LsnvBa7HwF5hs99VZbgLGj
This commit is contained in:
parent
b23a32e913
commit
b9ece3e6cc
435
salesflow-saas/frontend/src/components/dealix/marketers-page.tsx
Normal file
435
salesflow-saas/frontend/src/components/dealix/marketers-page.tsx
Normal file
@ -0,0 +1,435 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { clsx } from 'clsx';
|
||||
import {
|
||||
Zap, Wrench, HeadphonesIcon, Eye,
|
||||
UserPlus, Share2, Coins,
|
||||
Award, ChevronDown, ChevronUp,
|
||||
LayoutDashboard, Link2, FileText, BarChart3,
|
||||
Star, Quote, Phone, Mail, User,
|
||||
} from 'lucide-react';
|
||||
import { useI18n } from '@/i18n';
|
||||
|
||||
/* ---------- Animation Helpers ---------- */
|
||||
const fadeUp = {
|
||||
hidden: { opacity: 0, y: 24 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.5 } },
|
||||
};
|
||||
|
||||
const stagger = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.1 } },
|
||||
};
|
||||
|
||||
function Section({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const inView = useInView(ref, { once: true, margin: '-60px' });
|
||||
return (
|
||||
<motion.section
|
||||
ref={ref}
|
||||
initial="hidden"
|
||||
animate={inView ? 'visible' : 'hidden'}
|
||||
variants={stagger}
|
||||
className={clsx('py-16 sm:py-20', className)}
|
||||
>
|
||||
{children}
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
|
||||
function GlassCard({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<motion.div
|
||||
variants={fadeUp}
|
||||
whileHover={{ y: -3 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||||
className={clsx(
|
||||
'rounded-xl bg-white/5 backdrop-blur-xl border border-white/10 p-6',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- FAQ Accordion ---------- */
|
||||
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<div className="border-b border-white/10 last:border-0">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex items-center justify-between w-full py-4 text-start gap-4"
|
||||
>
|
||||
<span className="text-sm font-medium text-slate-200">{question}</span>
|
||||
{open ? (
|
||||
<ChevronUp className="h-4 w-4 text-slate-500 shrink-0" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-slate-500 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ height: open ? 'auto' : 0, opacity: open ? 1 : 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<p className="pb-4 text-sm text-slate-400 leading-relaxed">{answer}</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Main ---------- */
|
||||
function MarketersPage() {
|
||||
const { t, dir, isArabic } = useI18n();
|
||||
const [form, setForm] = useState({ name: '', phone: '', email: '' });
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
setSubmitting(false);
|
||||
setSubmitted(true);
|
||||
};
|
||||
|
||||
const benefitIcons = [Zap, Wrench, HeadphonesIcon, Eye];
|
||||
const benefitKeys = [
|
||||
{ title: 'benefitInstantCommission', desc: 'benefitInstantCommissionDesc' },
|
||||
{ title: 'benefitProTools', desc: 'benefitProToolsDesc' },
|
||||
{ title: 'benefitSupport', desc: 'benefitSupportDesc' },
|
||||
{ title: 'benefitTransparency', desc: 'benefitTransparencyDesc' },
|
||||
];
|
||||
|
||||
const stepIcons = [UserPlus, Share2, Coins];
|
||||
const stepKeys = [
|
||||
{ title: 'step1Title', desc: 'step1Desc' },
|
||||
{ title: 'step2Title', desc: 'step2Desc' },
|
||||
{ title: 'step3Title', desc: 'step3Desc' },
|
||||
];
|
||||
|
||||
const tiers = [
|
||||
{ key: 'tierBronze', desc: 'tierBronzeDesc', pct: '10%', color: 'from-amber-700 to-amber-900', badge: 'bg-amber-700/40 text-amber-300' },
|
||||
{ key: 'tierSilver', desc: 'tierSilverDesc', pct: '15%', color: 'from-slate-400 to-slate-600', badge: 'bg-slate-500/40 text-slate-200' },
|
||||
{ key: 'tierGold', desc: 'tierGoldDesc', pct: '20%', color: 'from-amber-400 to-yellow-500', badge: 'bg-amber-400/30 text-amber-200' },
|
||||
];
|
||||
|
||||
const toolIcons = [LayoutDashboard, Link2, FileText, BarChart3];
|
||||
const toolKeys = ['toolDashboard', 'toolLinks', 'toolTemplates', 'toolReports'];
|
||||
|
||||
const faqs = Array.from({ length: 5 }, (_, i) => ({
|
||||
q: t(`marketersPage.faq${i + 1}Q`),
|
||||
a: t(`marketersPage.faq${i + 1}A`),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div dir={dir} className="min-h-screen bg-[#0A0F1C] text-white">
|
||||
{/* ===== HERO ===== */}
|
||||
<Section className="relative overflow-hidden pt-24 sm:pt-32">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-teal-500/10 via-transparent to-transparent pointer-events-none" />
|
||||
<div className="absolute top-1/4 start-1/2 -translate-x-1/2 w-[600px] h-[600px] bg-teal-500/[0.07] rounded-full blur-[120px] pointer-events-none" />
|
||||
<div className="relative max-w-3xl mx-auto text-center px-4">
|
||||
<motion.h1
|
||||
variants={fadeUp}
|
||||
className="text-3xl sm:text-5xl font-bold leading-tight mb-5"
|
||||
>
|
||||
{t('marketersPage.heroTitle')}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="text-lg text-slate-400 max-w-xl mx-auto leading-relaxed"
|
||||
>
|
||||
{t('marketersPage.heroSubtitle')}
|
||||
</motion.p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ===== STATS BAR ===== */}
|
||||
<Section className="py-0 -mt-6">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<motion.div variants={fadeUp} className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{[
|
||||
{ label: t('marketersPage.statsAvgCommission'), value: isArabic ? '٤,٢٠٠ ر.س' : 'SAR 4,200' },
|
||||
{ label: t('marketersPage.statsActiveMarketers'), value: isArabic ? '+١٢٠' : '120+' },
|
||||
{ label: t('marketersPage.statsTotalPaid'), value: isArabic ? '+٢.٥ مليون ر.س' : 'SAR 2.5M+' },
|
||||
].map((stat) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="rounded-xl bg-white/5 backdrop-blur-xl border border-white/10 p-5 text-center"
|
||||
>
|
||||
<p className="text-2xl font-bold text-teal-400 mb-1">{stat.value}</p>
|
||||
<p className="text-xs text-slate-400">{stat.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ===== BENEFITS ===== */}
|
||||
<Section>
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<motion.h2 variants={fadeUp} className="text-2xl font-bold text-center mb-10">
|
||||
{t('marketersPage.benefitsTitle')}
|
||||
</motion.h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{benefitKeys.map((bk, i) => {
|
||||
const Icon = benefitIcons[i];
|
||||
return (
|
||||
<GlassCard key={bk.title}>
|
||||
<div className="rounded-lg bg-teal-500/10 p-2.5 w-fit mb-4">
|
||||
<Icon className="h-5 w-5 text-teal-400" />
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-white mb-1.5">
|
||||
{t(`marketersPage.${bk.title}`)}
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
{t(`marketersPage.${bk.desc}`)}
|
||||
</p>
|
||||
</GlassCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ===== HOW IT WORKS ===== */}
|
||||
<Section>
|
||||
<div className="max-w-3xl mx-auto px-4">
|
||||
<motion.h2 variants={fadeUp} className="text-2xl font-bold text-center mb-10">
|
||||
{t('marketersPage.howItWorksTitle')}
|
||||
</motion.h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
{stepKeys.map((sk, i) => {
|
||||
const Icon = stepIcons[i];
|
||||
return (
|
||||
<motion.div
|
||||
key={sk.title}
|
||||
variants={fadeUp}
|
||||
className="text-center"
|
||||
>
|
||||
<div className="mx-auto w-14 h-14 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center mb-4">
|
||||
<Icon className="h-6 w-6 text-teal-400" />
|
||||
</div>
|
||||
<span className="inline-block text-xs text-teal-400 font-medium mb-2 tabular-nums">
|
||||
{i + 1}
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold text-white mb-1">
|
||||
{t(`marketersPage.${sk.title}`)}
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
{t(`marketersPage.${sk.desc}`)}
|
||||
</p>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ===== COMMISSION TIERS ===== */}
|
||||
<Section>
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<motion.h2 variants={fadeUp} className="text-2xl font-bold text-center mb-10">
|
||||
{t('marketersPage.tiersTitle')}
|
||||
</motion.h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-5">
|
||||
{tiers.map((tier) => (
|
||||
<GlassCard key={tier.key} className="text-center relative overflow-hidden">
|
||||
<div
|
||||
className={clsx(
|
||||
'absolute inset-0 opacity-[0.06] bg-gradient-to-b',
|
||||
tier.color,
|
||||
)}
|
||||
/>
|
||||
<div className="relative">
|
||||
<span className={clsx('inline-block px-3 py-1 rounded-full text-xs font-medium mb-4', tier.badge)}>
|
||||
{t(`marketersPage.${tier.key}`)}
|
||||
</span>
|
||||
<p className="text-4xl font-bold text-white mb-1">{tier.pct}</p>
|
||||
<p className="text-xs text-slate-400 mb-3">{t('marketersPage.tierCommission')}</p>
|
||||
<p className="text-xs text-slate-500">{t(`marketersPage.${tier.desc}`)}</p>
|
||||
</div>
|
||||
</GlassCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ===== TESTIMONIALS ===== */}
|
||||
<Section>
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<motion.h2 variants={fadeUp} className="text-2xl font-bold text-center mb-10">
|
||||
{t('marketersPage.testimonialsTitle')}
|
||||
</motion.h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
{[1, 2].map((n) => (
|
||||
<GlassCard key={n}>
|
||||
<Quote className="h-5 w-5 text-teal-500/40 mb-3" />
|
||||
<p className="text-sm text-slate-300 leading-relaxed mb-4">
|
||||
{t(`marketersPage.testimonial${n}Text`)}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-9 w-9 rounded-full bg-gradient-to-br from-teal-500 to-emerald-600 flex items-center justify-center text-sm font-bold text-white">
|
||||
{t(`marketersPage.testimonial${n}Name`).charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">
|
||||
{t(`marketersPage.testimonial${n}Name`)}
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Award className="h-3 w-3 text-amber-400" />
|
||||
<span className="text-xs text-slate-400">
|
||||
{t(`marketersPage.testimonial${n}Role`)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ===== TOOLS PREVIEW ===== */}
|
||||
<Section>
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<motion.h2 variants={fadeUp} className="text-2xl font-bold text-center mb-10">
|
||||
{t('marketersPage.toolsTitle')}
|
||||
</motion.h2>
|
||||
<motion.div variants={fadeUp} className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
{toolKeys.map((tk, i) => {
|
||||
const Icon = toolIcons[i];
|
||||
return (
|
||||
<div
|
||||
key={tk}
|
||||
className="rounded-xl bg-white/5 border border-white/10 p-5 text-center hover:bg-white/[0.08] transition-colors"
|
||||
>
|
||||
<Icon className="h-6 w-6 text-teal-400 mx-auto mb-3" />
|
||||
<p className="text-xs text-slate-300 font-medium">
|
||||
{t(`marketersPage.${tk}`)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ===== FAQ ===== */}
|
||||
<Section>
|
||||
<div className="max-w-2xl mx-auto px-4">
|
||||
<motion.h2 variants={fadeUp} className="text-2xl font-bold text-center mb-10">
|
||||
{t('marketersPage.faqTitle')}
|
||||
</motion.h2>
|
||||
<motion.div
|
||||
variants={fadeUp}
|
||||
className="rounded-xl bg-white/5 backdrop-blur-xl border border-white/10 px-6"
|
||||
>
|
||||
{faqs.map((faq, i) => (
|
||||
<FaqItem key={i} question={faq.q} answer={faq.a} />
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ===== CTA + FORM ===== */}
|
||||
<Section className="pb-24">
|
||||
<div className="max-w-lg mx-auto px-4 text-center">
|
||||
<motion.h2 variants={fadeUp} className="text-2xl font-bold mb-3">
|
||||
{t('marketersPage.ctaTitle')}
|
||||
</motion.h2>
|
||||
|
||||
{submitted ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="mt-8 rounded-xl bg-teal-500/10 border border-teal-500/25 p-8 text-center"
|
||||
>
|
||||
<Star className="h-8 w-8 text-teal-400 mx-auto mb-3" />
|
||||
<p className="text-sm text-teal-300">{t('marketersPage.formSuccess')}</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.form
|
||||
variants={fadeUp}
|
||||
onSubmit={handleSubmit}
|
||||
className="mt-8 space-y-3"
|
||||
>
|
||||
<div className="relative">
|
||||
<User className="absolute top-1/2 -translate-y-1/2 start-3.5 h-4 w-4 text-slate-500" />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||
placeholder={t('marketersPage.formNamePlaceholder')}
|
||||
className={clsx(
|
||||
'w-full rounded-xl bg-white/5 border border-white/10 ps-10 pe-4 py-3',
|
||||
'text-sm text-white placeholder:text-slate-500',
|
||||
'focus:outline-none focus:ring-2 focus:ring-teal-400/50 focus:border-transparent',
|
||||
'transition-all',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Phone className="absolute top-1/2 -translate-y-1/2 start-3.5 h-4 w-4 text-slate-500" />
|
||||
<input
|
||||
type="tel"
|
||||
required
|
||||
dir="ltr"
|
||||
value={form.phone}
|
||||
onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value }))}
|
||||
placeholder={t('marketersPage.formPhonePlaceholder')}
|
||||
className={clsx(
|
||||
'w-full rounded-xl bg-white/5 border border-white/10 ps-10 pe-4 py-3',
|
||||
'text-sm text-white placeholder:text-slate-500 text-start',
|
||||
'focus:outline-none focus:ring-2 focus:ring-teal-400/50 focus:border-transparent',
|
||||
'transition-all',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Mail className="absolute top-1/2 -translate-y-1/2 start-3.5 h-4 w-4 text-slate-500" />
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
dir="ltr"
|
||||
value={form.email}
|
||||
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
|
||||
placeholder={t('marketersPage.formEmailPlaceholder')}
|
||||
className={clsx(
|
||||
'w-full rounded-xl bg-white/5 border border-white/10 ps-10 pe-4 py-3',
|
||||
'text-sm text-white placeholder:text-slate-500 text-start',
|
||||
'focus:outline-none focus:ring-2 focus:ring-teal-400/50 focus:border-transparent',
|
||||
'transition-all',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<motion.button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
whileHover={submitting ? undefined : { scale: 1.03 }}
|
||||
whileTap={submitting ? undefined : { scale: 0.97 }}
|
||||
className={clsx(
|
||||
'w-full rounded-xl py-3.5 text-sm font-semibold',
|
||||
'bg-gradient-to-l from-teal-500 to-emerald-600 text-white',
|
||||
'hover:shadow-[0_0_24px_rgba(20,184,166,0.4)]',
|
||||
'disabled:opacity-60 disabled:cursor-not-allowed',
|
||||
'transition-shadow duration-200',
|
||||
)}
|
||||
>
|
||||
{submitting ? t('marketersPage.formSubmitting') : t('marketersPage.ctaButton')}
|
||||
</motion.button>
|
||||
</motion.form>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MarketersPage };
|
||||
@ -0,0 +1,429 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { clsx } from 'clsx';
|
||||
import {
|
||||
UserCircle, Building2, CheckCircle2, PartyPopper,
|
||||
Import, MessageCircle, GitBranch, Users,
|
||||
ChevronLeft, ChevronRight, Briefcase, Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { useI18n } from '@/i18n';
|
||||
|
||||
/* ---------- Types ---------- */
|
||||
type Phase = 'welcome' | 'firstValue' | 'checklist';
|
||||
type Role = 'salesManager' | 'salesRep' | 'executive' | 'other';
|
||||
type Industry = 'realEstate' | 'automotive' | 'healthcare' | 'services' | 'other';
|
||||
|
||||
interface OnboardingFlowProps {
|
||||
onComplete?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/* ---------- Animation ---------- */
|
||||
const slideVariants = {
|
||||
enter: (dir: number) => ({ x: dir > 0 ? 80 : -80, opacity: 0 }),
|
||||
center: { x: 0, opacity: 1 },
|
||||
exit: (dir: number) => ({ x: dir > 0 ? -80 : 80, opacity: 0 }),
|
||||
};
|
||||
|
||||
/* ---------- Phase 1: Welcome ---------- */
|
||||
function WelcomePhase({
|
||||
role,
|
||||
setRole,
|
||||
industry,
|
||||
setIndustry,
|
||||
onNext,
|
||||
}: {
|
||||
role: Role | null;
|
||||
setRole: (r: Role) => void;
|
||||
industry: Industry | null;
|
||||
setIndustry: (i: Industry) => void;
|
||||
onNext: () => void;
|
||||
}) {
|
||||
const { t, isArabic } = useI18n();
|
||||
const [step, setStep] = useState<'role' | 'industry'>('role');
|
||||
|
||||
const roles: { key: Role; icon: typeof UserCircle }[] = [
|
||||
{ key: 'salesManager', icon: UserCircle },
|
||||
{ key: 'salesRep', icon: Briefcase },
|
||||
{ key: 'executive', icon: Building2 },
|
||||
{ key: 'other', icon: Users },
|
||||
];
|
||||
|
||||
const industries: { key: Industry; label: string }[] = [
|
||||
{ key: 'realEstate', label: t('onboarding.industryRealEstate') },
|
||||
{ key: 'automotive', label: t('onboarding.industryAutomotive') },
|
||||
{ key: 'healthcare', label: t('onboarding.industryHealthcare') },
|
||||
{ key: 'services', label: t('onboarding.industryServices') },
|
||||
{ key: 'other', label: t('onboarding.industryOther') },
|
||||
];
|
||||
|
||||
const roleLabels: Record<Role, string> = {
|
||||
salesManager: t('onboarding.roleSalesManager'),
|
||||
salesRep: t('onboarding.roleSalesRep'),
|
||||
executive: t('onboarding.roleExecutive'),
|
||||
other: t('onboarding.roleOther'),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center max-w-md mx-auto">
|
||||
<Sparkles className="h-8 w-8 text-teal-400 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-bold text-white mb-2">{t('onboarding.welcomeTitle')}</h1>
|
||||
<p className="text-sm text-slate-400 mb-8">{t('onboarding.welcomeSubtitle')}</p>
|
||||
|
||||
<AnimatePresence mode="wait" custom={1}>
|
||||
{step === 'role' ? (
|
||||
<motion.div
|
||||
key="role"
|
||||
custom={1}
|
||||
variants={slideVariants}
|
||||
initial="enter"
|
||||
animate="center"
|
||||
exit="exit"
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<p className="text-sm font-medium text-slate-300 mb-4">
|
||||
{t('onboarding.roleQuestion')}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{roles.map((r) => {
|
||||
const Icon = r.icon;
|
||||
const selected = role === r.key;
|
||||
return (
|
||||
<button
|
||||
key={r.key}
|
||||
onClick={() => {
|
||||
setRole(r.key);
|
||||
setTimeout(() => setStep('industry'), 300);
|
||||
}}
|
||||
className={clsx(
|
||||
'flex flex-col items-center gap-2 p-4 rounded-xl border transition-all duration-200',
|
||||
selected
|
||||
? 'bg-teal-500/15 border-teal-500/40 text-teal-300'
|
||||
: 'bg-white/5 border-white/10 text-slate-400 hover:bg-white/[0.08]',
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
<span className="text-xs font-medium">{roleLabels[r.key]}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="industry"
|
||||
custom={1}
|
||||
variants={slideVariants}
|
||||
initial="enter"
|
||||
animate="center"
|
||||
exit="exit"
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<p className="text-sm font-medium text-slate-300 mb-4">
|
||||
{t('onboarding.industryQuestion')}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{industries.map((ind) => {
|
||||
const selected = industry === ind.key;
|
||||
return (
|
||||
<button
|
||||
key={ind.key}
|
||||
onClick={() => {
|
||||
setIndustry(ind.key);
|
||||
setTimeout(onNext, 400);
|
||||
}}
|
||||
className={clsx(
|
||||
'flex items-center justify-center p-3.5 rounded-xl border text-xs font-medium transition-all duration-200',
|
||||
selected
|
||||
? 'bg-teal-500/15 border-teal-500/40 text-teal-300'
|
||||
: 'bg-white/5 border-white/10 text-slate-400 hover:bg-white/[0.08]',
|
||||
)}
|
||||
>
|
||||
{ind.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setStep('role')}
|
||||
className="mt-4 text-xs text-slate-500 hover:text-slate-300 transition-colors"
|
||||
>
|
||||
{isArabic ? (
|
||||
<span className="flex items-center gap-1 justify-center"><ChevronRight className="h-3 w-3" />{t('common.back')}</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 justify-center"><ChevronLeft className="h-3 w-3" />{t('common.back')}</span>
|
||||
)}
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Phase 2: First Value ---------- */
|
||||
function FirstValuePhase({ onNext }: { onNext: () => void }) {
|
||||
const { t, isArabic } = useI18n();
|
||||
const [created, setCreated] = useState(false);
|
||||
|
||||
const handleCreate = () => {
|
||||
setCreated(true);
|
||||
setTimeout(onNext, 1800);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center max-w-md mx-auto">
|
||||
<h2 className="text-xl font-bold text-white mb-2">{t('onboarding.firstValueTitle')}</h2>
|
||||
<p className="text-sm text-slate-400 mb-8">{t('onboarding.firstValueSubtitle')}</p>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{!created ? (
|
||||
<motion.div
|
||||
key="form"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="rounded-xl bg-white/5 border border-white/10 p-6 text-start"
|
||||
>
|
||||
<div className="space-y-3 mb-6">
|
||||
<div>
|
||||
<label className="text-[11px] text-slate-500 mb-1 block">
|
||||
{isArabic ? 'اسم الصفقة' : 'Deal Name'}
|
||||
</label>
|
||||
<div className="rounded-lg bg-white/5 border border-white/10 px-3 py-2 text-sm text-slate-300">
|
||||
{t('onboarding.sampleDealName')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[11px] text-slate-500 mb-1 block">
|
||||
{isArabic ? 'القيمة' : 'Value'}
|
||||
</label>
|
||||
<div className="rounded-lg bg-white/5 border border-white/10 px-3 py-2 text-sm text-slate-300">
|
||||
{isArabic ? 'ر.س' : 'SAR'} {t('onboarding.sampleDealValue')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[11px] text-slate-500 mb-1 block">
|
||||
{isArabic ? 'جهة الاتصال' : 'Contact'}
|
||||
</label>
|
||||
<div className="rounded-lg bg-white/5 border border-white/10 px-3 py-2 text-sm text-slate-300">
|
||||
{t('onboarding.sampleContactName')} — {t('onboarding.sampleCompany')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={handleCreate}
|
||||
className={clsx(
|
||||
'w-full rounded-xl py-3 text-sm font-semibold',
|
||||
'bg-gradient-to-l from-teal-500 to-emerald-600 text-white',
|
||||
'hover:shadow-[0_0_24px_rgba(20,184,166,0.4)]',
|
||||
'transition-shadow',
|
||||
)}
|
||||
>
|
||||
{t('onboarding.createDeal')}
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="celebration"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 15 }}
|
||||
className="py-10"
|
||||
>
|
||||
<motion.div
|
||||
animate={{ rotate: [0, -10, 10, -5, 5, 0] }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<PartyPopper className="h-14 w-14 text-amber-400 mx-auto mb-4" />
|
||||
</motion.div>
|
||||
<p className="text-lg font-bold text-white">{t('onboarding.celebration')}</p>
|
||||
<p className="text-sm text-teal-400 mt-1">{t('onboarding.dealCreated')}</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Phase 3: Checklist ---------- */
|
||||
interface ChecklistItem {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: typeof Import;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
function ChecklistPhase({ onComplete }: { onComplete?: () => void }) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const [items, setItems] = useState<ChecklistItem[]>([
|
||||
{ key: 'import', label: t('onboarding.checkImportContacts'), icon: Import, done: false },
|
||||
{ key: 'whatsapp', label: t('onboarding.checkConnectWhatsApp'), icon: MessageCircle, done: false },
|
||||
{ key: 'pipeline', label: t('onboarding.checkSetupPipeline'), icon: GitBranch, done: false },
|
||||
{ key: 'team', label: t('onboarding.checkInviteTeam'), icon: Users, done: false },
|
||||
]);
|
||||
|
||||
const doneCount = items.filter((i) => i.done).length;
|
||||
const progress = Math.round((doneCount / items.length) * 100);
|
||||
|
||||
const toggleItem = useCallback((key: string) => {
|
||||
setItems((prev) =>
|
||||
prev.map((item) =>
|
||||
item.key === key ? { ...item, done: !item.done } : item,
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="max-w-sm mx-auto">
|
||||
<h2 className="text-lg font-bold text-white mb-1">{t('onboarding.checklistTitle')}</h2>
|
||||
|
||||
{/* Progress */}
|
||||
<div className="flex items-center gap-3 mb-6 mt-3">
|
||||
<div className="flex-1 h-2 rounded-full bg-white/10 overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full rounded-full bg-gradient-to-l from-teal-400 to-emerald-500"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progress}%` }}
|
||||
transition={{ duration: 0.4 }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 tabular-nums">
|
||||
{progress}% {t('onboarding.checklistProgress')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Items */}
|
||||
<ul className="space-y-2">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<motion.li
|
||||
key={item.key}
|
||||
whileHover={{ x: 2 }}
|
||||
className={clsx(
|
||||
'flex items-center gap-3 px-4 py-3 rounded-xl border transition-all duration-200 cursor-pointer',
|
||||
item.done
|
||||
? 'bg-teal-500/10 border-teal-500/25'
|
||||
: 'bg-white/5 border-white/10 hover:bg-white/[0.08]',
|
||||
)}
|
||||
onClick={() => toggleItem(item.key)}
|
||||
>
|
||||
{item.done ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-teal-400 shrink-0" />
|
||||
) : (
|
||||
<div className="h-5 w-5 rounded-full border-2 border-slate-600 shrink-0" />
|
||||
)}
|
||||
<Icon className={clsx('h-4 w-4 shrink-0', item.done ? 'text-teal-400' : 'text-slate-500')} />
|
||||
<span className={clsx('text-sm flex-1', item.done ? 'text-teal-300 line-through' : 'text-slate-300')}>
|
||||
{item.label}
|
||||
</span>
|
||||
</motion.li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{progress === 100 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mt-6 text-center"
|
||||
>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={onComplete}
|
||||
className={clsx(
|
||||
'px-8 py-3 rounded-xl text-sm font-semibold',
|
||||
'bg-gradient-to-l from-teal-500 to-emerald-600 text-white',
|
||||
'hover:shadow-[0_0_24px_rgba(20,184,166,0.4)]',
|
||||
'transition-shadow',
|
||||
)}
|
||||
>
|
||||
{t('common.getStarted')}
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Main Onboarding Flow ---------- */
|
||||
function OnboardingFlow({ onComplete, className }: OnboardingFlowProps) {
|
||||
const { dir } = useI18n();
|
||||
const [phase, setPhase] = useState<Phase>('welcome');
|
||||
const [direction, setDirection] = useState(1);
|
||||
const [role, setRole] = useState<Role | null>(null);
|
||||
const [industry, setIndustry] = useState<Industry | null>(null);
|
||||
|
||||
const goTo = useCallback((next: Phase) => {
|
||||
const order: Phase[] = ['welcome', 'firstValue', 'checklist'];
|
||||
setDirection(order.indexOf(next) > order.indexOf(phase) ? 1 : -1);
|
||||
setPhase(next);
|
||||
}, [phase]);
|
||||
|
||||
const phases: Phase[] = ['welcome', 'firstValue', 'checklist'];
|
||||
const currentIdx = phases.indexOf(phase);
|
||||
|
||||
return (
|
||||
<div
|
||||
dir={dir}
|
||||
className={clsx(
|
||||
'min-h-[480px] flex flex-col items-center justify-center px-4 py-12',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Phase indicators */}
|
||||
<div className="flex items-center gap-2 mb-10">
|
||||
{phases.map((p, i) => (
|
||||
<div
|
||||
key={p}
|
||||
className={clsx(
|
||||
'h-1.5 rounded-full transition-all duration-300',
|
||||
i === currentIdx ? 'w-8 bg-teal-400' : i < currentIdx ? 'w-4 bg-teal-600' : 'w-4 bg-slate-700',
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<AnimatePresence mode="wait" custom={direction}>
|
||||
<motion.div
|
||||
key={phase}
|
||||
custom={direction}
|
||||
variants={slideVariants}
|
||||
initial="enter"
|
||||
animate="center"
|
||||
exit="exit"
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 28 }}
|
||||
className="w-full max-w-lg"
|
||||
>
|
||||
{phase === 'welcome' && (
|
||||
<WelcomePhase
|
||||
role={role}
|
||||
setRole={setRole}
|
||||
industry={industry}
|
||||
setIndustry={setIndustry}
|
||||
onNext={() => goTo('firstValue')}
|
||||
/>
|
||||
)}
|
||||
{phase === 'firstValue' && (
|
||||
<FirstValuePhase onNext={() => goTo('checklist')} />
|
||||
)}
|
||||
{phase === 'checklist' && (
|
||||
<ChecklistPhase onComplete={onComplete} />
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { OnboardingFlow };
|
||||
export type { OnboardingFlowProps };
|
||||
Loading…
Reference in New Issue
Block a user