Files
reui-blocks/blocks/help4.tsx
T

370 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 };