Files
reui-blocks/blocks/help4.tsx
T

370 lines
14 KiB
TypeScript
Raw Normal View History

"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 };