1126 lines
32 KiB
TypeScript
1126 lines
32 KiB
TypeScript
"use client";
|
|
|
|
import { Check, ChevronRight, Plus } from "lucide-react";
|
|
import { motion } from "motion/react";
|
|
import React, { useRef, useState } from "react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} 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 { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const toFaDigits = (value: number) => value.toLocaleString("fa-IR");
|
|
|
|
interface Hyperlink {
|
|
label: string;
|
|
href: string;
|
|
}
|
|
|
|
interface StepComponentProps {
|
|
onSubmit: () => void;
|
|
goBack?: () => void;
|
|
currentStep: number;
|
|
totalSteps: number;
|
|
}
|
|
|
|
interface OnboardingStep {
|
|
component: React.ComponentType<StepComponentProps>;
|
|
}
|
|
|
|
interface OnboardingStepHeaderProps {
|
|
title: string;
|
|
stepIndex: number;
|
|
totalSteps: number;
|
|
goBack?: () => void;
|
|
}
|
|
|
|
const OnboardingStepHeader = ({
|
|
title,
|
|
stepIndex,
|
|
totalSteps,
|
|
goBack,
|
|
}: OnboardingStepHeaderProps) => {
|
|
return (
|
|
<div className="relative">
|
|
{goBack && stepIndex > 0 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={goBack}
|
|
className="absolute top-1/2 inset-s-full -translate-y-1/2 translate-x-1/2"
|
|
aria-label="بازگشت به مرحله قبل"
|
|
>
|
|
<ChevronRight className="size-4" />
|
|
</Button>
|
|
)}
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground">
|
|
{toFaDigits(stepIndex + 1)}/{toFaDigits(totalSteps)}
|
|
</p>
|
|
<h3 className="text-xl font-semibold tracking-tight md:whitespace-nowrap">
|
|
{title}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface OnboardingStepProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
const OnboardingStep = ({ children }: OnboardingStepProps) => {
|
|
return (
|
|
<div className="flex w-full flex-col-reverse gap-10 sm:rounded-2xl sm:border md:min-h-[85dvh] md:flex-row lg:rounded-3xl">
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface OnboardingStepLeftWrapperProps {
|
|
title: string;
|
|
currentStep: number;
|
|
totalSteps: number;
|
|
children: React.ReactNode;
|
|
goBack?: () => void;
|
|
}
|
|
|
|
const OnboardingStepLeftWrapper = ({
|
|
title,
|
|
currentStep,
|
|
totalSteps,
|
|
goBack,
|
|
children,
|
|
}: OnboardingStepLeftWrapperProps) => {
|
|
return (
|
|
<div className="flex flex-1/2 justify-center pe-10 sm:py-10 sm:pb-10 sm:ps-10 md:py-20 lg:justify-start lg:pe-0 lg:ps-24">
|
|
<div className="flex h-full w-full max-w-sm shrink-0 flex-col gap-6 md:max-w-md">
|
|
<OnboardingStepHeader
|
|
title={title}
|
|
stepIndex={currentStep}
|
|
totalSteps={totalSteps}
|
|
goBack={goBack}
|
|
/>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface OnboardingStepRightWrapperProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
const OnboardingStepRightWrapper = ({
|
|
children,
|
|
className,
|
|
}: OnboardingStepRightWrapperProps) => {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"hidden flex-1/2 overflow-hidden sm:pt-10 md:pt-20 lg:block",
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface FileInputProps {
|
|
label: string;
|
|
name: string;
|
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
onRemove: () => void;
|
|
value: string | null;
|
|
description?: string;
|
|
}
|
|
|
|
const FileInput = ({
|
|
label,
|
|
name,
|
|
onChange,
|
|
onRemove,
|
|
value,
|
|
description,
|
|
}: FileInputProps) => {
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const handleUploadFile = () => {
|
|
fileInputRef.current?.click();
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center gap-4">
|
|
<div
|
|
onClick={handleUploadFile}
|
|
className="flex size-18 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-full border bg-muted"
|
|
>
|
|
{value ? (
|
|
<img
|
|
src={value}
|
|
alt="تصویر پروفایل"
|
|
className="size-full object-cover"
|
|
/>
|
|
) : (
|
|
<Plus />
|
|
)}
|
|
</div>
|
|
<div className="space-y-3">
|
|
<Label>{label}</Label>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="file"
|
|
name={name}
|
|
ref={fileInputRef}
|
|
className="hidden"
|
|
onChange={onChange}
|
|
/>
|
|
<Button
|
|
variant="outline"
|
|
type="button"
|
|
size="sm"
|
|
onClick={handleUploadFile}
|
|
>
|
|
بارگذاری تصویر
|
|
</Button>
|
|
{value && (
|
|
<Button
|
|
variant="outline"
|
|
type="button"
|
|
size="sm"
|
|
onClick={onRemove}
|
|
>
|
|
حذف
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">{description}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface DashboardIllustrationProps {
|
|
image?: string | null;
|
|
variant?: "zoomed-in" | "zoomed-out";
|
|
title?: string;
|
|
transformOrigin?: string;
|
|
className?: string;
|
|
}
|
|
|
|
const DashboardIllustration = ({
|
|
image,
|
|
variant = "zoomed-out",
|
|
title = "عنوان فضای کار",
|
|
transformOrigin = "-20% -10%",
|
|
className,
|
|
}: DashboardIllustrationProps) => {
|
|
return (
|
|
<motion.div
|
|
style={{
|
|
transformOrigin,
|
|
}}
|
|
animate={{
|
|
scale: variant === "zoomed-in" ? 1.5 : 1,
|
|
}}
|
|
transition={{
|
|
type: "spring",
|
|
stiffness: 300,
|
|
damping: 40,
|
|
}}
|
|
className={cn(
|
|
"flex h-full w-5xl overflow-hidden rounded-xl border",
|
|
className,
|
|
)}
|
|
>
|
|
<div className="h-full flex-2/7 shrink-0 overflow-hidden bg-muted">
|
|
<div className="flex items-center justify-between gap-2 border-b p-4">
|
|
<div className="flex items-center gap-2 overflow-hidden">
|
|
<div
|
|
className={cn(
|
|
"size-8 shrink-0 overflow-hidden rounded-md",
|
|
!image && "bg-blue-500",
|
|
)}
|
|
>
|
|
{image && (
|
|
<img
|
|
src={image}
|
|
alt="تصویر فضای کار"
|
|
className="size-full object-cover"
|
|
/>
|
|
)}
|
|
</div>
|
|
<p className="truncate overflow-hidden font-semibold">{title}</p>
|
|
</div>
|
|
<ChevronRight className="size-4 rtl:rotate-180" />
|
|
</div>
|
|
<ul className="space-y-2 p-4">
|
|
{Array.from({ length: 10 }).map((_, index) => (
|
|
<li
|
|
key={`sidebar-tab-${index}`}
|
|
className="h-9 rounded-lg border bg-background/50 hover:shadow-md"
|
|
/>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
<div className="flex flex-5/7 shrink-0 flex-col justify-between p-4">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className="size-9 rounded-lg border bg-muted/50" />
|
|
<div className="h-9 w-64 rounded-lg border bg-muted/50" />
|
|
<div className="flex items-center gap-2">
|
|
{Array.from({ length: 2 }).map((_, index) => (
|
|
<div
|
|
key={`icon-btn-${index}`}
|
|
className="size-9 rounded-lg border"
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<Button variant="outline">
|
|
<span className="block h-5 w-20 rounded-md bg-muted/50" />
|
|
<Plus />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="overflow-hidden rounded-lg border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-muted/50">
|
|
{[10, 40, 30, 60].map((width, index) => (
|
|
<TableHead
|
|
key={`th-${index}`}
|
|
style={{ width }}
|
|
className="h-9 border-e last:border-e-0"
|
|
/>
|
|
))}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{Array.from({ length: 8 }).map((_, rowIndex) => (
|
|
<TableRow
|
|
key={`row-${rowIndex}`}
|
|
className="even:bg-muted/20"
|
|
>
|
|
{Array.from({ length: 4 }).map((_, colIndex) => (
|
|
<TableCell
|
|
key={`cell-${rowIndex}-${colIndex}`}
|
|
className="h-9 border-e last:border-e-0"
|
|
/>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{Array.from({ length: 5 }).map((_, index) => (
|
|
<div
|
|
key={`icon-btn-${index}`}
|
|
className="size-9 rounded-lg border bg-muted/50"
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
};
|
|
|
|
interface FormFieldProps {
|
|
label: string;
|
|
placeholder: string;
|
|
name: string;
|
|
type?: string;
|
|
input?: string;
|
|
onChange?: React.ChangeEventHandler;
|
|
options?: { label: string; value: string }[];
|
|
}
|
|
|
|
const FormField = ({
|
|
label,
|
|
placeholder,
|
|
name,
|
|
type,
|
|
input = "input",
|
|
onChange,
|
|
options,
|
|
}: FormFieldProps) => {
|
|
return (
|
|
<div className="space-y-2">
|
|
<Label htmlFor={name}>{label}</Label>
|
|
{input === "textarea" && (
|
|
<Textarea
|
|
id={name}
|
|
placeholder={placeholder}
|
|
name={name}
|
|
className="w-full"
|
|
style={{
|
|
resize: "none",
|
|
}}
|
|
onChange={onChange}
|
|
/>
|
|
)}
|
|
{input === "input" && (
|
|
<Input
|
|
type={type}
|
|
id={name}
|
|
placeholder={placeholder}
|
|
name={name}
|
|
dir={
|
|
type === "email" || name === "workspaceHandle" ? "ltr" : undefined
|
|
}
|
|
className={cn(
|
|
"w-full",
|
|
(type === "email" || name === "workspaceHandle") && "text-end",
|
|
)}
|
|
onChange={onChange}
|
|
/>
|
|
)}
|
|
{input === "select" && (
|
|
<Select>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder={placeholder} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{options?.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface SkipOnboardingButtonProps {
|
|
onSubmit: () => void;
|
|
label: string;
|
|
description: string;
|
|
}
|
|
|
|
const SkipOnboardingButton = ({
|
|
onSubmit,
|
|
label,
|
|
description,
|
|
}: SkipOnboardingButtonProps) => {
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
|
|
return (
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button className="w-full" variant="ghost">
|
|
{label}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>مطمئن هستید؟</DialogTitle>
|
|
<DialogDescription>{description}</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button
|
|
onClick={() => {
|
|
setIsDialogOpen(false);
|
|
onSubmit();
|
|
}}
|
|
>
|
|
تأیید
|
|
</Button>
|
|
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
|
|
انصراف
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
const StepOneComponent = ({
|
|
onSubmit,
|
|
currentStep,
|
|
totalSteps,
|
|
}: StepComponentProps) => {
|
|
const [profilePicture, setProfilePicture] = useState<string | null>(null);
|
|
const [productUpdates, setProductUpdates] = useState(true);
|
|
|
|
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (e.target.files && e.target.files[0]) {
|
|
setProfilePicture(URL.createObjectURL(e.target.files[0]));
|
|
}
|
|
};
|
|
|
|
const handleRemoveImage = () => {
|
|
setProfilePicture(null);
|
|
};
|
|
|
|
const TEXT_FIELDS = [
|
|
{
|
|
label: "نام و نام خانوادگی",
|
|
placeholder: "مریم احمدیزاده",
|
|
name: "name",
|
|
type: "text",
|
|
},
|
|
{
|
|
label: "بیوگرافی",
|
|
placeholder: "چند جمله درباره خودتان...",
|
|
name: "about",
|
|
type: "text",
|
|
input: "textarea",
|
|
},
|
|
{
|
|
label: "ایمیل کاری",
|
|
placeholder: "maryam@example.ir",
|
|
name: "email",
|
|
type: "email",
|
|
},
|
|
];
|
|
|
|
return (
|
|
<OnboardingStep>
|
|
<OnboardingStepLeftWrapper
|
|
title="تنظیم پروفایل"
|
|
currentStep={currentStep}
|
|
totalSteps={totalSteps}
|
|
>
|
|
<form
|
|
onSubmit={(e) => e.preventDefault()}
|
|
className="space-y-6 py-4 text-start"
|
|
>
|
|
<FileInput
|
|
label="تصویر پروفایل"
|
|
name="profilePicture"
|
|
onChange={handleFileUpload}
|
|
onRemove={handleRemoveImage}
|
|
value={profilePicture}
|
|
description="فایلهای تصویر و jpeg تا ۱۰ مگابایت"
|
|
/>
|
|
|
|
<div className="space-y-6 border-b pt-4 pb-6">
|
|
{TEXT_FIELDS.map((field, i) => {
|
|
return (
|
|
<FormField
|
|
key={`text-field-${i}-${field.name}`}
|
|
label={field.label}
|
|
placeholder={field.placeholder}
|
|
name={field.name}
|
|
type={field.type}
|
|
input={field.input}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="inline-flex w-full items-center gap-3 text-xs">
|
|
<span dir="rtl" className="inline-flex shrink-0 items-center">
|
|
<Switch
|
|
checked={productUpdates}
|
|
onCheckedChange={setProductUpdates}
|
|
aria-label="دریافت بهروزرسانیهای محصول"
|
|
/>
|
|
</span>
|
|
<div className="space-y-1">
|
|
<p
|
|
className={cn(
|
|
"font-medium transition-colors",
|
|
productUpdates
|
|
? "text-foreground"
|
|
: "text-muted-foreground",
|
|
)}
|
|
>
|
|
بهروزرسانیهای محصول را برایم بفرستید
|
|
</p>
|
|
<p className="text-muted-foreground">
|
|
از ویژگیها و بهبودهای جدید مطلع بمانید.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Button type="submit" onClick={onSubmit} className="mt-4 w-full">
|
|
ادامه
|
|
</Button>
|
|
</form>
|
|
</OnboardingStepLeftWrapper>
|
|
<OnboardingStepRightWrapper className="sm:pb-10 md:pb-20">
|
|
<DashboardIllustration />
|
|
</OnboardingStepRightWrapper>
|
|
</OnboardingStep>
|
|
);
|
|
};
|
|
|
|
const StepTwoComponent = ({
|
|
currentStep,
|
|
totalSteps,
|
|
onSubmit,
|
|
goBack,
|
|
}: StepComponentProps) => {
|
|
const [profilePicture, setProfilePicture] = useState<string | null>(null);
|
|
const [formValue, setFormValue] = useState<Record<string, string>>({});
|
|
|
|
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (e.target.files && e.target.files[0]) {
|
|
setProfilePicture(URL.createObjectURL(e.target.files[0]));
|
|
}
|
|
};
|
|
|
|
const handleRemoveImage = () => {
|
|
setProfilePicture(null);
|
|
};
|
|
|
|
const handleTextAreaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
setFormValue({ ...formValue, [e.target.name]: e.target.value });
|
|
};
|
|
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setFormValue({ ...formValue, [e.target.name]: e.target.value });
|
|
};
|
|
|
|
const TEXT_FIELDS = [
|
|
{
|
|
label: "نام سازمان",
|
|
placeholder: "آکمه",
|
|
name: "companyName",
|
|
type: "text",
|
|
onChange: handleInputChange,
|
|
},
|
|
{
|
|
label: "آدرس فضای کار",
|
|
placeholder: "akme-workspace",
|
|
name: "workspaceHandle",
|
|
type: "text",
|
|
onChange: handleInputChange,
|
|
},
|
|
{
|
|
label: "کشور",
|
|
placeholder: "کشور خود را انتخاب کنید",
|
|
name: "billingCountry",
|
|
input: "select",
|
|
options: [
|
|
{ label: "ایران", value: "ir" },
|
|
{ label: "آمریکا", value: "us" },
|
|
{ label: "کانادا", value: "ca" },
|
|
{ label: "بریتانیا", value: "uk" },
|
|
{ label: "استرالیا", value: "au" },
|
|
],
|
|
},
|
|
{
|
|
label: "چطور با ما آشنا شدید؟",
|
|
placeholder: "موتور جستجو، معرفی دوست، شبکه اجتماعی...",
|
|
name: "referralSource",
|
|
type: "text",
|
|
input: "textarea",
|
|
onChange: handleTextAreaChange,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<OnboardingStep>
|
|
<OnboardingStepLeftWrapper
|
|
title="تنظیم فضای کار"
|
|
currentStep={currentStep}
|
|
totalSteps={totalSteps}
|
|
goBack={goBack}
|
|
>
|
|
<form
|
|
onSubmit={(e) => e.preventDefault()}
|
|
className="space-y-6 py-4 text-start"
|
|
>
|
|
<FileInput
|
|
label="لوگوی فضای کار"
|
|
name="workspaceLogo"
|
|
onChange={handleFileUpload}
|
|
onRemove={handleRemoveImage}
|
|
value={profilePicture}
|
|
description="فایلهای تصویر و jpeg تا ۱۰ مگابایت"
|
|
/>
|
|
|
|
<div className="space-y-6 pt-4">
|
|
{TEXT_FIELDS.map((field, i) => {
|
|
return (
|
|
<FormField
|
|
key={`text-field-${i}-${field.name}`}
|
|
label={field.label}
|
|
placeholder={field.placeholder}
|
|
name={field.name}
|
|
type={field.type}
|
|
input={field.input}
|
|
onChange={field.onChange}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<Button type="submit" onClick={onSubmit} className="mt-4 w-full">
|
|
ادامه
|
|
</Button>
|
|
</form>
|
|
</OnboardingStepLeftWrapper>
|
|
<OnboardingStepRightWrapper className="bg-gradient-to-b from-background to-muted">
|
|
<DashboardIllustration
|
|
variant="zoomed-in"
|
|
image={profilePicture}
|
|
title={formValue?.companyName}
|
|
/>
|
|
</OnboardingStepRightWrapper>
|
|
</OnboardingStep>
|
|
);
|
|
};
|
|
|
|
const StepThreeComponent = ({
|
|
currentStep,
|
|
totalSteps,
|
|
onSubmit,
|
|
goBack,
|
|
}: StepComponentProps) => {
|
|
const [mode, setMode] = useState<string>("pro");
|
|
|
|
const modes = [
|
|
{
|
|
title: "همگامسازی کامل",
|
|
value: "pro",
|
|
description:
|
|
"همه مخاطبین، رویدادها و پیامها همگام میشوند. تصویر کامل ارتباطات شما.",
|
|
image:
|
|
"/block-media/shadcnblocks/block/block-1.svg",
|
|
},
|
|
{
|
|
title: "فقط مخاطبین",
|
|
value: "lite",
|
|
description:
|
|
"فقط اطلاعات تماس همگام میشود. مناسب کاربران حساس به حریم خصوصی.",
|
|
image:
|
|
"/block-media/shadcnblocks/block/block-2.svg",
|
|
},
|
|
];
|
|
|
|
const features = [
|
|
"وارد کردن خودکار مخاطبین موجود",
|
|
"همگامسازی رویدادهای تقویم بین دستگاهها",
|
|
"یادآورهای هوشمند بر اساس برنامه شما",
|
|
];
|
|
|
|
return (
|
|
<OnboardingStep>
|
|
<OnboardingStepLeftWrapper
|
|
title="اتصال حسابها"
|
|
currentStep={currentStep}
|
|
totalSteps={totalSteps}
|
|
goBack={goBack}
|
|
>
|
|
<div className="space-y-8 text-start">
|
|
<div className="space-y-3 text-sm">
|
|
<p>
|
|
با اتصال حسابها پتانسیل کامل فضای کار را آزاد کنید. منظم
|
|
بمانید و هیچ موقعیت مهمی را از دست ندهید.
|
|
</p>
|
|
<ul className="space-y-1">
|
|
{features.map((feature, i) => {
|
|
return (
|
|
<li key={`feature-${i}`} className="flex items-center gap-2">
|
|
<Check className="size-4" />
|
|
<p>{feature}</p>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{modes.map((m, i) => {
|
|
const isActive = m.value === mode;
|
|
return (
|
|
<div
|
|
role="button"
|
|
key={`mode-${i}`}
|
|
tabIndex={0}
|
|
onClick={() => setMode(m.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault();
|
|
setMode(m.value);
|
|
}
|
|
}}
|
|
className={cn(
|
|
"flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-4 text-start text-xs transition-colors",
|
|
isActive
|
|
? "border-primary bg-primary/5"
|
|
: "hover:bg-muted/50",
|
|
)}
|
|
>
|
|
<img
|
|
src={m.image}
|
|
alt={m.title}
|
|
className="size-10 shrink-0 dark:invert"
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<p
|
|
className={cn(
|
|
"font-medium transition-colors",
|
|
isActive
|
|
? "text-foreground"
|
|
: "text-muted-foreground",
|
|
)}
|
|
>
|
|
{m.title}
|
|
</p>
|
|
<p className="text-muted-foreground">{m.description}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="w-full space-y-3">
|
|
<Button className="w-full gap-2" onClick={onSubmit}>
|
|
<img
|
|
src="/block-media/shadcnblocks/block/logos/google-icon.svg"
|
|
alt="گوگل"
|
|
className="size-4"
|
|
/>
|
|
اتصال با گوگل
|
|
</Button>
|
|
<Button className="w-full gap-2" onClick={onSubmit}>
|
|
<img
|
|
src="/block-media/shadcnblocks/block/logos/facebook-icon.svg"
|
|
alt="فیسبوک"
|
|
className="size-4"
|
|
/>
|
|
اتصال با فیسبوک
|
|
</Button>
|
|
<SkipOnboardingButton
|
|
onSubmit={onSubmit}
|
|
label="رد کردن این مرحله"
|
|
description="میتوانید بعداً از تنظیمات حسابها را متصل کنید."
|
|
/>
|
|
</div>
|
|
|
|
<p className="text-center text-xs text-muted-foreground">
|
|
دادههای شما امن هستند و هرگز با اشخاص ثالث به اشتراک گذاشته نمیشوند.
|
|
</p>
|
|
</div>
|
|
</OnboardingStepLeftWrapper>
|
|
<OnboardingStepRightWrapper className="sm:pb-10 md:pb-20">
|
|
<DashboardIllustration />
|
|
</OnboardingStepRightWrapper>
|
|
</OnboardingStep>
|
|
);
|
|
};
|
|
|
|
const StepFourComponent = ({
|
|
currentStep,
|
|
totalSteps,
|
|
onSubmit,
|
|
goBack,
|
|
}: StepComponentProps) => {
|
|
const [usecase, setUsecase] = useState<string | null>(null);
|
|
|
|
const toggleUsecase = (i: string) => {
|
|
setUsecase((prev) => (prev === i ? null : i));
|
|
};
|
|
|
|
const descriptions = [
|
|
"میخواهیم تجربه را مطابق شیوه کار شما شخصیسازی کنیم.",
|
|
"کاربرد اصلی خود را انتخاب کنید تا قالبها، نماها و میانبرها را برای شما بهینه کنیم.",
|
|
];
|
|
|
|
const usecases = [
|
|
"فروش",
|
|
"استخدام",
|
|
"بازاریابی",
|
|
"موفقیت مشتری",
|
|
"پشتیبانی",
|
|
"مهندسی",
|
|
"محصول",
|
|
"طراحی",
|
|
"منابع انسانی",
|
|
"حقوقی",
|
|
"مالی",
|
|
"عملیات",
|
|
"سایر",
|
|
];
|
|
|
|
return (
|
|
<OnboardingStep>
|
|
<OnboardingStepLeftWrapper
|
|
title="نقش خود را بگویید"
|
|
currentStep={currentStep}
|
|
totalSteps={totalSteps}
|
|
goBack={goBack}
|
|
>
|
|
<div className="flex h-full flex-col justify-between text-start">
|
|
<div className="space-y-6">
|
|
<div className="space-y-2">
|
|
{descriptions.map((description, i) => {
|
|
return (
|
|
<p key={`description-${i}`} className="text-sm">
|
|
{description}
|
|
</p>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<p className="text-sm">کدام مورد بیشترین شباهت را به نقش شما دارد؟</p>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{usecases.map((u, i) => {
|
|
const isActive = usecase === u;
|
|
return (
|
|
<div
|
|
key={`usecase-${i}`}
|
|
role="button"
|
|
tabIndex={0}
|
|
className={cn(
|
|
"cursor-pointer rounded-lg border px-2 py-1 text-xs transition-colors",
|
|
isActive
|
|
? "border-primary bg-primary/5 font-medium text-foreground"
|
|
: "text-muted-foreground hover:bg-muted/50",
|
|
)}
|
|
onClick={() => toggleUsecase(u)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault();
|
|
toggleUsecase(u);
|
|
}
|
|
}}
|
|
>
|
|
{u}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{usecase === "سایر" && (
|
|
<div className="space-y-2">
|
|
<Label>نقش خود را مشخص کنید</Label>
|
|
<Input
|
|
type="text"
|
|
placeholder="مثلاً پژوهش، اداری..."
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Button onClick={onSubmit} className="w-full">
|
|
ادامه
|
|
</Button>
|
|
</div>
|
|
</OnboardingStepLeftWrapper>
|
|
<OnboardingStepRightWrapper className="sm:pb-10 md:pb-20">
|
|
<DashboardIllustration />
|
|
</OnboardingStepRightWrapper>
|
|
</OnboardingStep>
|
|
);
|
|
};
|
|
|
|
const StepFiveComponent = ({
|
|
currentStep,
|
|
totalSteps,
|
|
onSubmit,
|
|
goBack,
|
|
}: StepComponentProps) => {
|
|
const descriptions = [
|
|
"وقتی تیمها با هم کار میکنند، اتفاقات بزرگی رخ میدهد.",
|
|
"همکاران را به فضای کار دعوت کنید. پروژهها را به اشتراک بگذارید، همزمان همکاری کنید و همه را همسو نگه دارید.",
|
|
];
|
|
|
|
return (
|
|
<OnboardingStep>
|
|
<OnboardingStepLeftWrapper
|
|
title="دعوت تیم"
|
|
currentStep={currentStep}
|
|
totalSteps={totalSteps}
|
|
goBack={goBack}
|
|
>
|
|
<div className="flex h-full flex-col justify-between text-start">
|
|
<div className="space-y-8">
|
|
<div className="space-y-2">
|
|
{descriptions.map((description, i) => {
|
|
return (
|
|
<p key={`description-${i}`} className="text-sm">
|
|
{description}
|
|
</p>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<form onSubmit={(e) => e.preventDefault()} className="space-y-8">
|
|
<div className="space-y-4">
|
|
<Label>آدرس ایمیل را وارد کنید</Label>
|
|
<div className="space-y-2">
|
|
{Array.from({ length: 2 }).map((_, i) => (
|
|
<div
|
|
key={`input-${i}`}
|
|
className="flex flex-col gap-2 sm:flex-row sm:items-center"
|
|
>
|
|
<Input
|
|
type="email"
|
|
dir="rtl"
|
|
placeholder="ali.rezaei@example.ir"
|
|
className="flex-1 text-end"
|
|
/>
|
|
<Select defaultValue="viewer">
|
|
<SelectTrigger className="w-full sm:w-36">
|
|
<SelectValue placeholder="انتخاب نقش" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="admin">مدیر</SelectItem>
|
|
<SelectItem value="editor">ویرایشگر</SelectItem>
|
|
<SelectItem value="viewer">بیننده</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Button type="submit" className="w-full">
|
|
ارسال دعوتنامه
|
|
</Button>
|
|
<SkipOnboardingButton
|
|
onSubmit={onSubmit}
|
|
label="فعلاً رد شوید"
|
|
description="هر زمان میتوانید از تنظیمات فضای کار اعضا را دعوت کنید."
|
|
/>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
با ادامه، با شرایط استفاده و حریم خصوصی ما موافقت میکنید. اعضای
|
|
دعوتشده ایمیلی با دستورالعمل پیوستن به فضای کار دریافت میکنند.
|
|
</p>
|
|
</div>
|
|
</OnboardingStepLeftWrapper>
|
|
<OnboardingStepRightWrapper className="sm:pb-10 md:pb-20">
|
|
<DashboardIllustration
|
|
transformOrigin="-20% -10%"
|
|
variant="zoomed-in"
|
|
className="translate-x-1/4 xl:translate-x-1/5 2xl:translate-x-0"
|
|
/>
|
|
</OnboardingStepRightWrapper>
|
|
</OnboardingStep>
|
|
);
|
|
};
|
|
|
|
interface Onboarding1Props {
|
|
className?: string;
|
|
companyLogo?: string;
|
|
copyright?: string;
|
|
footerLinks?: Hyperlink[];
|
|
steps?: OnboardingStep[];
|
|
}
|
|
|
|
const Onboarding1 = ({
|
|
className,
|
|
companyLogo = "/block-media/shadcnblocks/block/logos/ShadcnBlocks-logo-word.svg",
|
|
copyright = `© ${new Date().getFullYear().toLocaleString("fa-IR")} شادسیان بلاکس`,
|
|
footerLinks = [
|
|
{ label: "حریم خصوصی", href: "https://ShadcnBlocks" },
|
|
{
|
|
label: "پشتیبانی",
|
|
href: "https://ShadcnBlocks",
|
|
},
|
|
{
|
|
label: "خروج",
|
|
href: "https://ShadcnBlocks",
|
|
},
|
|
],
|
|
steps = [
|
|
{
|
|
component: StepOneComponent,
|
|
},
|
|
{
|
|
component: StepTwoComponent,
|
|
},
|
|
{
|
|
component: StepThreeComponent,
|
|
},
|
|
{
|
|
component: StepFourComponent,
|
|
},
|
|
{
|
|
component: StepFiveComponent,
|
|
},
|
|
],
|
|
}: Onboarding1Props) => {
|
|
const [currentStep, setCurrentStep] = useState(0);
|
|
|
|
const step = steps[currentStep];
|
|
|
|
const handleSubmit = () => {
|
|
if (currentStep !== steps.length - 1) setCurrentStep(currentStep + 1);
|
|
};
|
|
|
|
const handleGoBack = () => {
|
|
if (currentStep > 0) setCurrentStep(currentStep - 1);
|
|
};
|
|
|
|
return (
|
|
<section dir="rtl" lang="fa" className="py-20">
|
|
<div
|
|
className={cn(
|
|
"container flex flex-col items-center gap-20 text-start",
|
|
className,
|
|
)}
|
|
>
|
|
<img
|
|
src={companyLogo}
|
|
alt="لوگوی شرکت"
|
|
className="w-54 dark:invert"
|
|
/>
|
|
|
|
<step.component
|
|
onSubmit={handleSubmit}
|
|
currentStep={currentStep}
|
|
totalSteps={steps.length}
|
|
goBack={handleGoBack}
|
|
/>
|
|
|
|
<div className="flex flex-wrap items-center justify-center gap-4 text-xs text-muted-foreground">
|
|
<p>{copyright}</p>
|
|
{footerLinks.map((link) => (
|
|
<a key={link.label} href={link.href} className="underline">
|
|
{link.label}
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export { Onboarding1 };
|