370 lines
14 KiB
TypeScript
370 lines
14 KiB
TypeScript
"use client";
|
||
|
||
import { zodResolver } from "@hookform/resolvers/zod";
|
||
import { AlertCircle, LoaderIcon, Paperclip, X } from "lucide-react";
|
||
import { useRef, useState } from "react";
|
||
import { Controller, useForm } from "react-hook-form";
|
||
import { z } from "zod";
|
||
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
Field,
|
||
FieldError,
|
||
FieldGroup,
|
||
FieldLabel,
|
||
} from "@/components/ui/field";
|
||
import { Input } from "@/components/ui/input";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
const ticketSchema = z.object({
|
||
email: z
|
||
.string()
|
||
.min(1, "ایمیل الزامی است")
|
||
.email("لطفاً یک ایمیل معتبر وارد کنید"),
|
||
category: z.string().min(1, "لطفاً یک دسته انتخاب کنید"),
|
||
priority: z.string().min(1, "لطفاً یک اولویت انتخاب کنید"),
|
||
subject: z.string().min(1, "موضوع الزامی است"),
|
||
description: z.string().min(10, "توضیحات باید حداقل ۱۰ کاراکتر باشد"),
|
||
});
|
||
|
||
type TicketFormData = z.infer<typeof ticketSchema>;
|
||
|
||
interface Category {
|
||
value: string;
|
||
label: string;
|
||
}
|
||
|
||
interface Help4Props {
|
||
title?: string;
|
||
description?: string;
|
||
categories?: Category[];
|
||
className?: string;
|
||
onSubmit?: (data: TicketFormData & { files: File[] }) => Promise<void>;
|
||
}
|
||
|
||
const Help4 = ({
|
||
title = "ثبت تیکت پشتیبانی",
|
||
description = "مشکل خود را شرح دهید تا در اسرع وقت پاسخگوییم.",
|
||
categories = [
|
||
{ value: "technical", label: "مشکل فنی" },
|
||
{ value: "billing", label: "صورتحساب و پرداخت" },
|
||
{ value: "account", label: "دسترسی به حساب" },
|
||
{ value: "feature", label: "درخواست ویژگی" },
|
||
{ value: "bug", label: "گزارش باگ" },
|
||
{ value: "other", label: "سایر" },
|
||
],
|
||
className,
|
||
onSubmit,
|
||
}: Help4Props) => {
|
||
const [files, setFiles] = useState<File[]>([]);
|
||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||
const [showSuccess, setShowSuccess] = useState(false);
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
|
||
const form = useForm<TicketFormData>({
|
||
resolver: zodResolver(ticketSchema),
|
||
mode: "onSubmit",
|
||
reValidateMode: "onSubmit",
|
||
defaultValues: {
|
||
email: "",
|
||
category: "",
|
||
priority: "",
|
||
subject: "",
|
||
description: "",
|
||
},
|
||
});
|
||
|
||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const selectedFiles = Array.from(e.target.files || []);
|
||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||
const validFiles = selectedFiles.filter((file) => file.size <= maxSize);
|
||
setFiles((prev) => [...prev, ...validFiles].slice(0, 5)); // Max 5 files
|
||
};
|
||
|
||
const removeFile = (index: number) => {
|
||
setFiles((prev) => prev.filter((_, i) => i !== index));
|
||
};
|
||
|
||
const handleFormSubmit = async (data: TicketFormData) => {
|
||
try {
|
||
if (onSubmit) {
|
||
await onSubmit({ ...data, files });
|
||
} else {
|
||
console.log("Ticket submitted:", { ...data, files });
|
||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||
}
|
||
setIsSubmitted(true);
|
||
setShowSuccess(true);
|
||
form.reset();
|
||
setFiles([]);
|
||
setTimeout(() => setShowSuccess(false), 4500);
|
||
setTimeout(() => setIsSubmitted(false), 5000);
|
||
} catch {
|
||
form.setError("root", {
|
||
message: "مشکلی پیش آمد. لطفاً دوباره تلاش کنید.",
|
||
});
|
||
}
|
||
};
|
||
|
||
const formatFileSize = (bytes: number) => {
|
||
if (bytes < 1024) return `${bytes.toLocaleString("fa-IR")} بایت`;
|
||
if (bytes < 1024 * 1024)
|
||
return `${(bytes / 1024).toLocaleString("fa-IR", { maximumFractionDigits: 1 })} کیلوبایت`;
|
||
return `${(bytes / (1024 * 1024)).toLocaleString("fa-IR", { maximumFractionDigits: 1 })} مگابایت`;
|
||
};
|
||
|
||
return (
|
||
<section className={cn("py-32", className)}>
|
||
<div className="container">
|
||
<div className="mx-auto max-w-2xl">
|
||
<div className="mb-10 text-center">
|
||
<h1 className="mb-4 text-xl font-medium tracking-tight md:text-2xll">
|
||
{title}
|
||
</h1>
|
||
<p className="text-muted-foreground">{description}</p>
|
||
</div>
|
||
|
||
<div className="rounded-xl border bg-background p-8">
|
||
{isSubmitted && (
|
||
<div
|
||
className={cn(
|
||
"mb-6 rounded-lg border border-green-500/20 bg-green-500/10 p-4 text-center transition-opacity duration-500",
|
||
showSuccess ? "opacity-100" : "opacity-0",
|
||
)}
|
||
>
|
||
<p className="text-sm font-medium text-green-600 dark:text-green-400">
|
||
تیکت شما ثبت شد. ایمیل تأیید برایتان ارسال میشود.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<form onSubmit={form.handleSubmit(handleFormSubmit)}>
|
||
<FieldGroup>
|
||
<Controller
|
||
control={form.control}
|
||
name="email"
|
||
render={({ field, fieldState }) => (
|
||
<Field data-invalid={fieldState.invalid}>
|
||
<FieldLabel htmlFor={field.name}>
|
||
آدرس ایمیل{" "}
|
||
<span className="text-destructive">*</span>
|
||
</FieldLabel>
|
||
<Input
|
||
{...field}
|
||
id={field.name}
|
||
type="email"
|
||
aria-invalid={fieldState.invalid}
|
||
placeholder="you@example.com"
|
||
/>
|
||
{fieldState.invalid && (
|
||
<FieldError errors={[fieldState.error]} />
|
||
)}
|
||
</Field>
|
||
)}
|
||
/>
|
||
|
||
<div className="grid gap-4 sm:grid-cols-2">
|
||
<Controller
|
||
control={form.control}
|
||
name="category"
|
||
render={({ field, fieldState }) => (
|
||
<Field data-invalid={fieldState.invalid}>
|
||
<FieldLabel htmlFor={field.name}>
|
||
دستهبندی{" "}
|
||
<span className="text-destructive">*</span>
|
||
</FieldLabel>
|
||
<Select
|
||
onValueChange={field.onChange}
|
||
defaultValue={field.value}
|
||
>
|
||
<SelectTrigger id={field.name}>
|
||
<SelectValue placeholder="انتخاب دسته" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{categories.map((cat) => (
|
||
<SelectItem key={cat.value} value={cat.value}>
|
||
{cat.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
{fieldState.invalid && (
|
||
<FieldError errors={[fieldState.error]} />
|
||
)}
|
||
</Field>
|
||
)}
|
||
/>
|
||
|
||
<Controller
|
||
control={form.control}
|
||
name="priority"
|
||
render={({ field, fieldState }) => (
|
||
<Field data-invalid={fieldState.invalid}>
|
||
<FieldLabel htmlFor={field.name}>
|
||
اولویت <span className="text-destructive">*</span>
|
||
</FieldLabel>
|
||
<Select
|
||
onValueChange={field.onChange}
|
||
defaultValue={field.value}
|
||
>
|
||
<SelectTrigger id={field.name}>
|
||
<SelectValue placeholder="انتخاب اولویت" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="low">کم</SelectItem>
|
||
<SelectItem value="medium">متوسط</SelectItem>
|
||
<SelectItem value="high">بالا</SelectItem>
|
||
<SelectItem value="urgent">
|
||
<span className="flex items-center gap-2">
|
||
<AlertCircle className="size-4 text-destructive" />
|
||
فوری
|
||
</span>
|
||
</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
{fieldState.invalid && (
|
||
<FieldError errors={[fieldState.error]} />
|
||
)}
|
||
</Field>
|
||
)}
|
||
/>
|
||
</div>
|
||
|
||
<Controller
|
||
control={form.control}
|
||
name="subject"
|
||
render={({ field, fieldState }) => (
|
||
<Field data-invalid={fieldState.invalid}>
|
||
<FieldLabel htmlFor={field.name}>
|
||
موضوع <span className="text-destructive">*</span>
|
||
</FieldLabel>
|
||
<Input
|
||
{...field}
|
||
id={field.name}
|
||
aria-invalid={fieldState.invalid}
|
||
placeholder="خلاصهای از مشکل خود"
|
||
/>
|
||
{fieldState.invalid && (
|
||
<FieldError errors={[fieldState.error]} />
|
||
)}
|
||
</Field>
|
||
)}
|
||
/>
|
||
|
||
<Controller
|
||
control={form.control}
|
||
name="description"
|
||
render={({ field, fieldState }) => (
|
||
<Field data-invalid={fieldState.invalid}>
|
||
<FieldLabel htmlFor={field.name}>
|
||
توضیحات <span className="text-destructive">*</span>
|
||
</FieldLabel>
|
||
<Textarea
|
||
{...field}
|
||
id={field.name}
|
||
aria-invalid={fieldState.invalid}
|
||
placeholder="لطفاً تا حد امکان جزئیات را بنویسید..."
|
||
rows={6}
|
||
/>
|
||
{fieldState.invalid && (
|
||
<FieldError errors={[fieldState.error]} />
|
||
)}
|
||
</Field>
|
||
)}
|
||
/>
|
||
|
||
<div>
|
||
<FieldLabel>پیوستها</FieldLabel>
|
||
<p className="mb-3 text-xs text-muted-foreground">
|
||
حداکثر ۵ فایل (هر کدام تا ۱۰ مگابایت). فرمتهای مجاز:
|
||
تصویر، پیدیاف و فایل متنی.
|
||
</p>
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
multiple
|
||
onChange={handleFileChange}
|
||
accept="image/*,.pdf,.txt,.doc,.docx"
|
||
className="hidden"
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => fileInputRef.current?.click()}
|
||
disabled={files.length >= 5}
|
||
>
|
||
<Paperclip className="me-2 size-4" />
|
||
پیوست فایل
|
||
</Button>
|
||
|
||
{files.length > 0 && (
|
||
<div className="mt-4 space-y-2">
|
||
{files.map((file, index) => (
|
||
<div
|
||
key={index}
|
||
className="flex items-center justify-between rounded-lg border bg-muted/50 px-3 py-2"
|
||
>
|
||
<div className="flex items-center gap-2 overflow-hidden">
|
||
<Paperclip className="size-4 shrink-0 text-muted-foreground" />
|
||
<span className="truncate text-sm">
|
||
{file.name}
|
||
</span>
|
||
<span className="shrink-0 text-xs text-muted-foreground">
|
||
({formatFileSize(file.size)})
|
||
</span>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => removeFile(index)}
|
||
className="size-8 p-0"
|
||
>
|
||
<X className="size-4" />
|
||
<span className="sr-only">حذف فایل</span>
|
||
</Button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{form.formState.errors.root && (
|
||
<p className="text-sm text-destructive">
|
||
{form.formState.errors.root.message}
|
||
</p>
|
||
)}
|
||
|
||
<Button
|
||
size="lg"
|
||
className="w-full"
|
||
disabled={form.formState.isSubmitting}
|
||
>
|
||
{form.formState.isSubmitting ? (
|
||
<>
|
||
<LoaderIcon className="me-2 size-4 animate-spin" />
|
||
در حال ارسال...
|
||
</>
|
||
) : (
|
||
"ثبت تیکت"
|
||
)}
|
||
</Button>
|
||
</FieldGroup>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
};
|
||
|
||
export { Help4 };
|