Initial import of 1407 block sources under blocks/
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
"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 };
|
||||
Reference in New Issue
Block a user