Files
simple-admin/src/pages/news/ui/StepTwo.tsx
2025-11-18 15:06:04 +05:00

446 lines
13 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 { addNews, updateNews } from "@/pages/news/lib/api";
import { useNewsStore } from "@/pages/news/lib/data";
import { newsPostForm, type NewsPostFormType } from "@/pages/news/lib/form";
import { Button } from "@/shared/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import { Textarea } from "@/shared/ui/textarea";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { ImagePlus, Loader2, PlusCircle, Trash2 } from "lucide-react";
import { useEffect, useRef } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
interface Data {
id: number;
title: string;
title_ru: string;
title_uz: string;
image: string;
text: string;
text_ru: string;
text_uz: string;
is_public: boolean;
category: {
name: string;
name_ru: string;
name_uz: string;
};
post_tags: Array<{
id: number;
name: string;
name_ru: string;
name_uz: string;
}>;
post_images: Array<{
id: number;
image: string;
text: string;
text_ru: string;
text_uz: string;
}>;
}
const StepTwo = ({
data: detail,
id,
}: {
isEditMode: boolean;
id: string;
data: Data;
}) => {
const { t } = useTranslation();
const hasReset = useRef(false);
const navigate = useNavigate();
const { stepOneData, resetStepOneData } = useNewsStore();
const queryClient = useQueryClient();
const deletedSections = useRef<number[]>([]);
const form = useForm<NewsPostFormType>({
resolver: zodResolver(newsPostForm),
defaultValues: {
desc: "",
desc_ru: "",
is_public: "yes",
sections: [{ image: undefined as any, text: "", text_ru: "" }],
post_tags: [{ name: "", name_ru: "" }],
},
});
useEffect(() => {
if (detail && !hasReset.current) {
const mappedTags =
detail.post_tags?.map((t) => ({
name: t.name_uz,
name_ru: t.name_ru,
})) ?? [];
form.reset({
desc: detail.text_uz || "",
desc_ru: detail.text_ru || "",
is_public: detail.is_public ? "yes" : "no",
post_tags:
mappedTags.length > 0 ? mappedTags : [{ name: "", name_ru: "" }],
sections:
detail.post_images?.map((img) => ({
id: img.id,
image: img.image,
text: img.text_uz,
text_ru: img.text_ru,
})) ?? [],
});
hasReset.current = true;
}
}, [detail, form]);
const handleRemoveSection = (index: number) => {
const section = form.getValues(`sections.${index}`);
if (section?.id) {
deletedSections.current.push(section.id);
}
// Formdan o'chiramiz
removeSection(index);
};
const {
fields: sectionFields,
append: appendSection,
remove: removeSection,
} = useFieldArray({
control: form.control,
name: "sections",
});
const {
fields: tagFields,
append: appendTag,
remove: removeTag,
} = useFieldArray({
control: form.control,
name: "post_tags",
});
const handleImageChange = (
e: React.ChangeEvent<HTMLInputElement>,
index: number,
) => {
const file = e.target.files?.[0];
if (file) form.setValue(`sections.${index}.image`, file);
};
const { mutate: added, isPending } = useMutation({
mutationFn: (body: FormData) => addNews(body),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["all_news"] });
queryClient.refetchQueries({ queryKey: ["news_detail"] });
navigate("/news");
resetStepOneData();
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
richColors: true,
position: "top-center",
});
},
});
const { mutate: update, isPending: updatePending } = useMutation({
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
updateNews({ id, body }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["news_detail"] });
queryClient.refetchQueries({ queryKey: ["all_news"] });
navigate("/news");
resetStepOneData();
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
richColors: true,
position: "top-center",
});
},
});
const onSubmit = (values: NewsPostFormType) => {
const formData = new FormData();
formData.append("title", stepOneData.title);
formData.append("title_ru", stepOneData.title_ru);
formData.append("text", values.desc);
formData.append("text_ru", values.desc_ru);
formData.append("is_public", values.is_public === "no" ? "false" : "true");
formData.append("category", String(stepOneData.category));
if (stepOneData.banner instanceof File) {
formData.append("image", stepOneData.banner);
}
values.sections?.forEach((section, i) => {
if (section.id) {
formData.append(`updates[${i}]id`, String(section.id));
if (section.text) formData.append(`updates[${i}]text`, section.text);
if (section.text_ru)
formData.append(`updates[${i}]text_ru`, section.text_ru);
if (section.image instanceof File) {
formData.append(`updates[${i}]image`, section.image);
}
} else {
if (section.image instanceof File)
formData.append(`post_images`, section.image);
if (section.text) formData.append(`post_text`, section.text);
if (section.text_ru) formData.append(`post_text_ru`, section.text_ru);
}
});
deletedSections.current.forEach((id) => {
formData.append(`delete_list`, String(id));
});
values.post_tags.forEach((tag, i) => {
formData.append(`post_tags[${i}]name`, tag.name);
formData.append(`post_tags[${i}]name_ru`, tag.name_ru);
});
if (id) {
update({ body: formData, id: Number(id) });
} else {
added(formData);
}
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 bg-gray-900 p-6 rounded-2xl text-white"
>
<Label className="text-lg">{t("Yangilik bolimlari")}</Label>
{/* DESC (UZ) */}
<FormField
control={form.control}
name="desc"
render={({ field }) => (
<FormItem>
<Label>{t("Qisqacha ta'rif")}</Label>
<FormControl>
<Textarea {...field} placeholder={t("Qisqacha ta'rif")} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* DESC (RU) */}
<FormField
control={form.control}
name="desc_ru"
render={({ field }) => (
<FormItem>
<Label>{t("Qisqacha ta'rif")} (ru)</Label>
<FormControl>
<Textarea
{...field}
placeholder={t("Qisqacha ta'rif") + " (ru)"}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Post Tags */}
<div className="space-y-4">
<Label>{t("Teglar")}</Label>
{tagFields.map((field, i) => (
<div key={field.id} className="flex gap-2 items-start">
<FormField
control={form.control}
name={`post_tags.${i}.name`}
render={({ field }) => (
<FormItem>
<FormControl>
<Input {...field} placeholder={t("Teg")} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`post_tags.${i}.name_ru`}
render={({ field }) => (
<FormItem>
<FormControl>
<Input {...field} placeholder={t("Teg") + " (ru)"} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{tagFields.length > 1 && (
<button
type="button"
onClick={() => removeTag(i)}
className="text-red-400 hover:text-red-500"
>
<Trash2 className="size-4" />
</button>
)}
</div>
))}
<Button
type="button"
onClick={() => appendTag({ name: "", name_ru: "" })}
className="bg-gray-600 hover:bg-gray-700 mt-2"
>
<PlusCircle className="size-5 mr-2" />
{t("Teg qo'shish")}
</Button>
</div>
{/* Sections */}
{sectionFields.map((field, index) => (
<div
key={field.id}
className="border border-gray-700 rounded-lg p-4 space-y-4"
>
<div className="flex justify-between items-center">
<p className="text-sm text-gray-300">
{t(" ")} #{index + 1}
</p>
<button
type="button"
onClick={() => handleRemoveSection(index)}
className="text-red-400 hover:text-red-500"
>
<Trash2 className="size-4" />
</button>
</div>
{/* Image */}
<div>
<Label>{t("Rasm")}</Label>
{form.watch(`sections.${index}.image`) ? (
<div className="relative mt-2 w-48">
<img
src={
form.watch(`sections.${index}.image`) instanceof File
? URL.createObjectURL(
form.watch(`sections.${index}.image`) as File,
)
: String(form.watch(`sections.${index}.image`) || "")
}
alt="preview"
className="rounded-lg border border-gray-700 object-cover h-40 w-full"
/>
<button
type="button"
onClick={() =>
form.setValue(`sections.${index}.image`, undefined as any)
}
className="absolute top-1 right-1 bg-black/70 rounded-full p-1 hover:bg-black/90"
>
<Trash2 className="size-4 text-red-400" />
</button>
</div>
) : (
<>
<input
id={`section-img-${index}`}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => handleImageChange(e, index)}
/>
<label
htmlFor={`section-img-${index}`}
className="inline-flex items-center cursor-pointer bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg mt-2"
>
<ImagePlus className="size-5 mr-2" />
{t("Rasm tanlash")}
</label>
</>
)}
</div>
{/* Text UZ */}
<FormField
control={form.control}
name={`sections.${index}.text`}
render={({ field }) => (
<FormItem>
<Label>{t("Matn")}</Label>
<FormControl>
<Textarea {...field} placeholder={t("Matn kiriting")} />
</FormControl>
</FormItem>
)}
/>
{/* Text RU */}
<FormField
control={form.control}
name={`sections.${index}.text_ru`}
render={({ field }) => (
<FormItem>
<Label>{t("Matn") + " (ru)"}</Label>
<FormControl>
<Textarea {...field} placeholder={t("Matn") + " (ru)"} />
</FormControl>
</FormItem>
)}
/>
</div>
))}
<Button
type="button"
onClick={() =>
appendSection({ image: undefined as any, text: "", text_ru: "" })
}
className="bg-gray-700 hover:bg-gray-600"
>
<PlusCircle className="size-5 mr-2" />
{t("Bolim qoshish")}
</Button>
<div className="flex justify-end">
<Button
type="submit"
className="mt-6 px-8 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 cursor-pointer"
>
{isPending || updatePending ? (
<Loader2 className="animate-spin" />
) : (
t("Saqlash")
)}
</Button>
</div>
</form>
</Form>
);
};
export default StepTwo;