652 lines
19 KiB
TypeScript
652 lines
19 KiB
TypeScript
"use client";
|
||
|
||
import type { LucideIcon } from "lucide-react";
|
||
import {
|
||
ChevronLeftIcon,
|
||
Chrome,
|
||
LayoutGrid,
|
||
Leaf,
|
||
Lock,
|
||
Puzzle,
|
||
Shield,
|
||
Sparkles,
|
||
Star,
|
||
User,
|
||
Workflow,
|
||
} from "lucide-react";
|
||
import { AnimatePresence, motion } from "motion/react";
|
||
import { useState } from "react";
|
||
|
||
import { Button } from "@/components/ui/button";
|
||
import { Checkbox } from "@/components/ui/checkbox";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
interface IconListItem {
|
||
icon: LucideIcon;
|
||
title: string;
|
||
}
|
||
|
||
interface StepDef {
|
||
title: string;
|
||
description: string | IconListItem[];
|
||
className?: string;
|
||
isBlocking?: boolean;
|
||
component: React.ComponentType<StepComponentProps>;
|
||
secondaryCta?: React.ComponentType;
|
||
cta?: React.ComponentType<StepCtaProps>;
|
||
}
|
||
|
||
const Background = () => {
|
||
return (
|
||
<div className="flex h-[100dvh] w-full items-start">
|
||
<div className="h-full w-64 shrink-0 border-r">
|
||
<div className="flex h-14 w-full items-center border-b p-4">
|
||
<img
|
||
src="/block-media/shadcnblocks/block/logos/ShadcnBlocks-logo-word.png"
|
||
className="w-40 dark:invert"
|
||
alt="logo"
|
||
/>
|
||
</div>
|
||
<ul className="space-y-2 p-4">
|
||
{Array.from({ length: 5 }).map((_, index) => {
|
||
return (
|
||
<li
|
||
key={`dummy-link-${index}`}
|
||
className="block h-8 w-50 rounded-md bg-muted"
|
||
/>
|
||
);
|
||
})}
|
||
</ul>
|
||
</div>
|
||
<div className="flex h-14 w-full items-center border-b p-4">
|
||
<span className="block h-8 w-full max-w-50 rounded-md bg-muted" />
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
interface StepIndicatorProps {
|
||
totalSteps: number;
|
||
currentStep: number;
|
||
}
|
||
|
||
const StepIndicator = ({ totalSteps, currentStep }: StepIndicatorProps) => {
|
||
return (
|
||
<div className="flex items-center justify-center gap-2">
|
||
{Array.from({ length: totalSteps }).map((_, i) => {
|
||
return (
|
||
<span
|
||
key={`step-indicator-${i}`}
|
||
className={cn(
|
||
"block size-2 rounded-full transition-colors",
|
||
currentStep >= i ? "bg-primary" : "border bg-muted",
|
||
)}
|
||
/>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
interface FadeContainerProps {
|
||
children: React.ReactNode;
|
||
condition: boolean;
|
||
className?: string;
|
||
}
|
||
|
||
const FadeContainer = ({
|
||
children,
|
||
condition,
|
||
className,
|
||
}: FadeContainerProps) => {
|
||
const fadeVariants = {
|
||
initial: {
|
||
opacity: 0,
|
||
filter: "blur(4px)",
|
||
},
|
||
animate: {
|
||
opacity: 1,
|
||
filter: "blur(0px)",
|
||
},
|
||
exit: {
|
||
opacity: 0,
|
||
filter: "blur(4px)",
|
||
},
|
||
};
|
||
|
||
return (
|
||
<AnimatePresence mode="popLayout">
|
||
{condition && (
|
||
<motion.div
|
||
variants={fadeVariants}
|
||
initial="initial"
|
||
animate="animate"
|
||
exit="exit"
|
||
className={className}
|
||
>
|
||
{children}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
);
|
||
};
|
||
|
||
interface StepDescriptionCardProps {
|
||
title: string;
|
||
description: string | IconListItem[];
|
||
}
|
||
|
||
const StepDescriptionCard = ({
|
||
title,
|
||
description,
|
||
}: StepDescriptionCardProps) => {
|
||
return (
|
||
<div className="space-y-2 md:space-y-4">
|
||
<h3 className="text-xl font-semibold md:text-xl">{title}</h3>
|
||
<div className="text-sm leading-tight text-muted-foreground md:text-base">
|
||
{typeof description === "string" ? (
|
||
<p>{description}</p>
|
||
) : (
|
||
<ul className="space-y-4">
|
||
{description?.map((item, i) => {
|
||
return (
|
||
<li key={`step-${i}`} className="flex items-center gap-2">
|
||
<item.icon className="size-4 shrink-0" />
|
||
<p>{item.title}</p>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
interface StepComponentProps {
|
||
stepIndex: number;
|
||
onValidationChange: (stepIndex: number, isValid: boolean) => void;
|
||
}
|
||
|
||
interface StepCtaProps {
|
||
onClick: () => void;
|
||
disabled?: boolean;
|
||
}
|
||
|
||
const StepOneComponent = () => {
|
||
return (
|
||
<form
|
||
onSubmit={(e) => e.preventDefault()}
|
||
className="grid grid-cols-2 gap-2 md:gap-4"
|
||
>
|
||
<div className="space-y-2">
|
||
<Label>نام</Label>
|
||
<Input type="text" placeholder="نام" className="bg-background" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>نام خانوادگی</Label>
|
||
<Input type="text" placeholder="نام خانوادگی" className="bg-background" />
|
||
</div>
|
||
<div className="col-span-2 space-y-2">
|
||
<Label>کجا کار میکنید؟</Label>
|
||
<Input type="text" placeholder="رویش مارکت" className="bg-background" />
|
||
</div>
|
||
<div className="col-span-2 space-y-2">
|
||
<Label>در چه حوزه ای فعالیت میکنید؟</Label>
|
||
<Select>
|
||
<SelectTrigger className="w-full bg-background">
|
||
<SelectValue placeholder="حوزه را انتخاب کنید" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="technology">تکنولوژی</SelectItem>
|
||
<SelectItem value="finance">مالی</SelectItem>
|
||
<SelectItem value="healthcare">بهداشت و درمان</SelectItem>
|
||
<SelectItem value="retail">خرده فروشی</SelectItem>
|
||
<SelectItem value="other">سایر</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="col-span-2 space-y-2">
|
||
<Label>چند نفر در تیم شما هستن؟</Label>
|
||
<Select>
|
||
<SelectTrigger className="w-full bg-background">
|
||
<SelectValue placeholder="سایز تیم شما" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="solo">فقط خودم</SelectItem>
|
||
<SelectItem value="small">2-10 نفر</SelectItem>
|
||
<SelectItem value="medium">11-50 نفر</SelectItem>
|
||
<SelectItem value="large">51-200 نفر</SelectItem>
|
||
<SelectItem value="enterprise">200+ نفر</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="col-span-2 space-y-2">
|
||
<Label>چه چیزی نقش شما رو به بهترین نحو توصیف میکند؟</Label>
|
||
<Select>
|
||
<SelectTrigger className="w-full bg-background">
|
||
<SelectValue placeholder="نقش را انتخاب کنید" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="founder">موسس</SelectItem>
|
||
<SelectItem value="sales">فروش</SelectItem>
|
||
<SelectItem value="marketing">بازاریابی</SelectItem>
|
||
<SelectItem value="operations">عملیات</SelectItem>
|
||
<SelectItem value="other">سایر</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</form>
|
||
);
|
||
};
|
||
|
||
const StepTwoComponent = () => {
|
||
const goalOptions = [
|
||
{ value: "close-deals", label: "بستن معاملات بیشتر" },
|
||
{ value: "grow-network", label: "شبکه ام را رشد بده" },
|
||
{ value: "track-pipeline", label: "خط لوله من را دنبال کنید" },
|
||
{ value: "automate-outreach", label: "دسترسی خودکار" },
|
||
{ value: "team-collaboration", label: "همکاری با تیم" },
|
||
{ value: "find-prospects", label: "چشم انداز جدید پیدا کنید" },
|
||
{ value: "retain-customers", label: "حفظ مشتریان" },
|
||
{ value: "analyze-performance", label: "تجزیه و تحلیل عملکرد" },
|
||
{ value: "streamline-workflow", label: "گردش کار را ساده کنید" },
|
||
{ value: "other", label: "یک چیز دیگر" },
|
||
];
|
||
|
||
const [selectedGoals, setSelectedGoals] = useState<string[]>([]);
|
||
|
||
const toggleGoal = (goalValue: string, checked: boolean) => {
|
||
setSelectedGoals((prev) =>
|
||
checked
|
||
? [...prev, goalValue]
|
||
: prev.filter((value) => value !== goalValue),
|
||
);
|
||
};
|
||
|
||
return (
|
||
<form
|
||
onSubmit={(e) => e.preventDefault()}
|
||
className="grid grid-cols-2 gap-6 pb-2 lg:gap-8"
|
||
>
|
||
{goalOptions.map((goal) => {
|
||
const isSelected = selectedGoals.includes(goal.value);
|
||
return (
|
||
<Label htmlFor={goal.value} key={goal.value}>
|
||
<div
|
||
role="button"
|
||
className={cn(
|
||
"flex w-full cursor-pointer items-center gap-2 rounded-full md:px-6 md:py-4",
|
||
selectedGoals.includes(goal.value)
|
||
? "md:bg-muted"
|
||
: "md:bg-muted/50",
|
||
)}
|
||
>
|
||
<Checkbox
|
||
id={goal.value}
|
||
checked={isSelected}
|
||
onCheckedChange={(checked) =>
|
||
toggleGoal(goal.value, checked === true)
|
||
}
|
||
className="bg-background"
|
||
/>
|
||
{goal.label}
|
||
</div>
|
||
</Label>
|
||
);
|
||
})}
|
||
</form>
|
||
);
|
||
};
|
||
|
||
const StepThreeComponent = ({
|
||
stepIndex,
|
||
onValidationChange,
|
||
}: StepComponentProps) => {
|
||
const [isConnected, setIsConnected] = useState<string | boolean>(false);
|
||
|
||
const handleConnectButtonClick = (provider: string) => {
|
||
setIsConnected(provider);
|
||
onValidationChange(stepIndex, true);
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col items-center justify-center gap-4 md:min-h-[40.5dvh]">
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => handleConnectButtonClick("gmail")}
|
||
disabled={!!isConnected}
|
||
>
|
||
<img
|
||
src="/block-media/shadcnblocks/block/logos/google-icon.svg"
|
||
className="size-4"
|
||
alt="Google"
|
||
/>
|
||
{isConnected === "gmail" ? "Connected" : "ادامه با گوگل"}
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => handleConnectButtonClick("facebook")}
|
||
disabled={!!isConnected}
|
||
>
|
||
<img
|
||
src="/block-media/shadcnblocks/block/logos/facebook-icon.svg"
|
||
className="size-4"
|
||
alt="Facebook"
|
||
/>
|
||
{isConnected === "facebook" ? "Connected" : "Continue with Facebook"}
|
||
</Button>
|
||
<Button
|
||
variant="link"
|
||
onClick={() => handleConnectButtonClick("manual")}
|
||
disabled={!!isConnected}
|
||
>
|
||
{isConnected === "manual" ? "Skipped" : "فعلاً رد شوید"}
|
||
</Button>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const StepFourComponent = () => {
|
||
return (
|
||
<div className="flex h-full flex-col items-center justify-between md:min-h-[40.5dvh]">
|
||
<img
|
||
src="/block-media/shadcnblocks/block/dashboard-placeholder.png"
|
||
className="w-full"
|
||
style={{
|
||
maskImage: "linear-gradient(to bottom, black 0%, transparent 100%)",
|
||
}}
|
||
alt="Dashboard placeholder"
|
||
/>
|
||
<div className="flex w-full items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Leaf className="size-4 fill-primary stroke-green-200" />
|
||
<p>50k+ active users</p>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<p>امتیاز ۴٫۹ از ۵</p>
|
||
<div className="flex items-center gap-1">
|
||
{Array.from({ length: 5 }).map((_, i) => {
|
||
return <Star key={i} className="size-4 fill-primary" />;
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const StepFourSecondaryCta = () => {
|
||
return (
|
||
<Button className="w-full" size="lg">
|
||
Get the Extension
|
||
</Button>
|
||
);
|
||
};
|
||
|
||
const StepFourCta = ({ onClick, disabled }: StepCtaProps) => {
|
||
return (
|
||
<Button
|
||
className="w-full"
|
||
size="lg"
|
||
variant="ghost"
|
||
onClick={onClick}
|
||
disabled={disabled}
|
||
>
|
||
شاید بعداً
|
||
</Button>
|
||
);
|
||
};
|
||
|
||
const StepFiveComponent = () => {
|
||
const premiumFeatures = [
|
||
{
|
||
icon: LayoutGrid,
|
||
title: "Custom Views",
|
||
description: "Organize your data exactly how you want it",
|
||
},
|
||
{
|
||
icon: Workflow,
|
||
title: "Automation",
|
||
description: "Set up triggers and actions to save time",
|
||
},
|
||
{
|
||
icon: Chrome,
|
||
title: "Web Clipper",
|
||
description: "Save contacts from any website instantly",
|
||
},
|
||
{
|
||
icon: Sparkles,
|
||
title: "AI Insights",
|
||
description: "Get smart suggestions and auto-complete data",
|
||
},
|
||
{
|
||
icon: Puzzle,
|
||
title: "App Connections",
|
||
description: "Sync with 2000+ tools you already use",
|
||
},
|
||
{
|
||
icon: Star,
|
||
title: "پشتیبانی اولویتدار",
|
||
description: "Dedicated help and راهاندازی اولیه assistance",
|
||
},
|
||
];
|
||
|
||
return (
|
||
<ul className="space-y-4 md:space-y-6">
|
||
{premiumFeatures.map((feature) => (
|
||
<li key={feature.title} className="flex items-start gap-2">
|
||
<feature.icon className="mt-0.5 size-5 shrink-0" />
|
||
<div className="space-y-0.5">
|
||
<p className="text-sm leading-tight font-medium md:text-base">
|
||
{feature.title}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground md:text-sm">
|
||
{feature.description}
|
||
</p>
|
||
</div>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
);
|
||
};
|
||
|
||
const StepFiveCta = ({ onClick, disabled }: StepCtaProps) => {
|
||
return (
|
||
<Button className="w-full" size="lg" onClick={onClick} disabled={disabled}>
|
||
Activate Trial
|
||
</Button>
|
||
);
|
||
};
|
||
|
||
interface Onboarding2Props {
|
||
steps: StepDef[];
|
||
className?: string;
|
||
}
|
||
|
||
const Onboarding2 = ({
|
||
steps = [
|
||
{
|
||
title: " درمورد خودتون به ما بگویید",
|
||
description: "چند سوال سریع در مورد تجربه ی شخصیتون",
|
||
className: "bg-orange-200",
|
||
component: StepOneComponent,
|
||
},
|
||
{
|
||
title: "چی میخوای به دست بیاری؟",
|
||
description:
|
||
"انتخاب همه که اعمال میشوند. این به ما کمک میکند داشبورد و پیشنهادهای شما را متناسب سازی کنیم",
|
||
className: "bg-blue-200",
|
||
component: StepTwoComponent,
|
||
},
|
||
{
|
||
title: "لینک اکانت شما",
|
||
description: [
|
||
{
|
||
icon: User,
|
||
title:
|
||
"مخاطبین و جلسات و گفتگوهای خود رو به صورت خودکار همگامسازی کنید",
|
||
},
|
||
{
|
||
icon: Shield,
|
||
title: "دیتا شما ایمن و خصوصی میماند",
|
||
},
|
||
{
|
||
icon: Lock,
|
||
title: "رمزنگاری با استاندارد سازمانی",
|
||
},
|
||
],
|
||
className: "bg-pink-200",
|
||
isBlocking: true,
|
||
component: StepThreeComponent,
|
||
},
|
||
{
|
||
title: "به وبگردی خود سرعت و قدرت ببخشید",
|
||
description:
|
||
"افزونه ما به شما این امکان را میدهد که با یک کلیک، از هر وبسایتی که هستید مخاطبین را ذخیره کنید و یادداشت بردارید",
|
||
className: "bg-green-200",
|
||
component: StepFourComponent,
|
||
secondaryCta: StepFourSecondaryCta,
|
||
cta: StepFourCta,
|
||
},
|
||
{
|
||
title: "تجربه کامل رو باز کنید ( دسترسی به تمام امکانات )",
|
||
description:
|
||
"دوره آزمایشی ۱۴ روزه خود را آغاز کرده و تمامی ویژگیها را تجربه کنید نیاز به کارت اعتباری نیست.",
|
||
className: "bg-purple-200",
|
||
component: StepFiveComponent,
|
||
cta: StepFiveCta,
|
||
},
|
||
],
|
||
className,
|
||
}: Onboarding2Props) => {
|
||
const [currentStep, setCurrentStep] = useState(0);
|
||
const [stepValidation, setStepValidation] = useState<Record<number, boolean>>(
|
||
{},
|
||
);
|
||
|
||
const handleStepValidChange = (stepIndex: number, isValid: boolean) => {
|
||
setStepValidation((prev) => ({ ...prev, [stepIndex]: isValid }));
|
||
};
|
||
|
||
const canProceed = () => {
|
||
const step = steps[currentStep];
|
||
|
||
if (!step.component || !step.isBlocking) return true;
|
||
|
||
return stepValidation[currentStep] === true;
|
||
};
|
||
|
||
const handleNext = () => {
|
||
if (currentStep < steps.length - 1) {
|
||
setCurrentStep(currentStep + 1);
|
||
}
|
||
};
|
||
|
||
const handlePrevious = () => {
|
||
if (currentStep > 0) {
|
||
setCurrentStep(currentStep - 1);
|
||
}
|
||
};
|
||
|
||
const step = steps[currentStep];
|
||
|
||
return (
|
||
<section>
|
||
<Background />
|
||
|
||
<Dialog open={true}>
|
||
<DialogContent
|
||
showCloseButton={false}
|
||
className={cn(
|
||
"flex items-center justify-center overflow-hidden rounded-none p-0 md:max-w-3xl md:items-stretch lg:max-w-5xl",
|
||
className,
|
||
)}
|
||
>
|
||
<DialogHeader className="sr-only">
|
||
<DialogTitle>راهاندازی اولیه</DialogTitle>
|
||
<DialogDescription>راهنمای شروع کار</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="flex min-h-[40dvh] w-full flex-col-reverse items-start tracking-tighter md:flex-row">
|
||
<div className="flex h-full w-full flex-col justify-between gap-4 p-6 pb-6 md:flex-[45%] md:gap-6 lg:p-12">
|
||
<div className="space-y-2">
|
||
<FadeContainer condition={currentStep > 0}>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={handlePrevious}
|
||
disabled={currentStep === 0}
|
||
>
|
||
<ChevronLeftIcon />
|
||
بازگشت
|
||
</Button>
|
||
</FadeContainer>
|
||
<StepDescriptionCard
|
||
title={step.title}
|
||
description={step.description}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-4 md:space-y-6">
|
||
<FadeContainer condition={canProceed()} className="space-y-2">
|
||
{step.cta ? (
|
||
<step.cta onClick={handleNext} disabled={!canProceed()} />
|
||
) : (
|
||
<Button
|
||
size="lg"
|
||
className="w-full"
|
||
onClick={handleNext}
|
||
disabled={
|
||
currentStep === steps.length - 1 || !canProceed()
|
||
}
|
||
>
|
||
ادامه
|
||
</Button>
|
||
)}
|
||
{step.secondaryCta && <step.secondaryCta />}
|
||
</FadeContainer>
|
||
|
||
<StepIndicator
|
||
totalSteps={steps.length}
|
||
currentStep={currentStep}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
className={cn(
|
||
"flex h-full w-full flex-[55%] shrink-0 items-center justify-center p-6 transition-colors lg:p-12",
|
||
step.className || "bg-foreground/30",
|
||
)}
|
||
>
|
||
<step.component
|
||
stepIndex={currentStep}
|
||
onValidationChange={handleStepValidChange}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</section>
|
||
);
|
||
};
|
||
|
||
export { Onboarding2 };
|