Files
simple-admin/src/pages/news/ui/StepTwo.tsx
Samandar Turgunboyev 2d0285dafc api ulandi
2025-10-29 18:41:59 +05:00

399 lines
12 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, 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;
};
tag: [
{
id: number;
name: string;
name_ru: string;
name_uz: string;
},
];
post_images: [
{
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 form = useForm<NewsPostFormType>({
resolver: zodResolver(newsPostForm),
defaultValues: {
desc: "",
desc_ru: "",
is_public: "yes",
sections: [{ image: undefined as any, text: "", text_ru: "" }],
post_tags: [""],
},
});
useEffect(() => {
if (detail && !hasReset.current) {
// 🧠 xavfsiz map qilish
const mappedSections =
detail.post_images?.map((img) => ({
image: img.image,
text: img.text_uz,
text_ru: img.text_ru,
})) ?? [];
const mappedTags = detail.tag?.map((t) => t.name_uz) ?? [];
form.reset({
desc: detail.text_uz || "",
desc_ru: detail.text_ru || "",
is_public: detail.is_public ? "yes" : "no",
post_tags: mappedTags.length > 0 ? mappedTags : [""],
sections:
mappedSections.length > 0
? mappedSections
: [{ image: "", text: "", text_ru: "" }],
});
hasReset.current = true;
}
}, [detail, form]);
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "sections",
});
const { watch, setValue } = form;
const postTags = watch("post_tags");
const addTag = () => setValue("post_tags", [...postTags, ""]);
const removeTag = (i: number) =>
setValue(
"post_tags",
postTags.filter((_, idx) => idx !== i),
);
const { mutate: added } = 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 } = useMutation({
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
updateNews({ id: 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 handleImageChange = (
e: React.ChangeEvent<HTMLInputElement>,
index: number,
) => {
const file = e.target.files?.[0];
if (file) form.setValue(`sections.${index}.image`, file);
};
const onSubmit = (values: NewsPostFormType) => {
const formData = new FormData();
formData.append("title", stepOneData.title);
formData.append("title_ru", stepOneData.title_ru);
formData.append("text", stepOneData.desc);
formData.append("text_ru", stepOneData.desc_ru);
formData.append("is_public", values.is_public === "no" ? "false" : "true");
formData.append("category", stepOneData.category);
if (stepOneData.banner instanceof File) {
formData.append("image", stepOneData.banner);
}
// 🔥 sections
values.sections.forEach((section, i) => {
if (section.image instanceof File) {
formData.append(`post_images[${i}]`, section.image);
}
formData.append(`post_text[${i}]`, section.text);
formData.append(`post_text_ru[${i}]`, section.text_ru);
});
values.post_tags.forEach((tag, i) => {
formData.append(`post_tags[${i}]`, tag);
});
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 (UZ)")}</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 (rus tilida)")}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-4">
<Label>{t("Teglar")}</Label>
{postTags.map((__, i) => (
<FormField
key={i}
control={form.control}
name={`post_tags.${i}`}
render={({ field }) => (
<FormItem>
<div className="relative">
{postTags.length > 1 && (
<button
type="button"
onClick={() => removeTag(i)}
className="absolute top-1 right-1 text-red-400 hover:text-red-500"
>
<Trash2 className="size-4" />
</button>
)}
<FormControl>
<Input {...field} placeholder={t("Masalan: sport")} />
</FormControl>
<FormMessage />
</div>
</FormItem>
)}
/>
))}
<Button
type="button"
onClick={addTag}
className="bg-gray-600 hover:bg-gray-700"
>
<PlusCircle className="size-5 mr-2" />
{t("Teg qo'shish")}
</Button>
</div>
{fields.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("Bolim")} #{index + 1}
</p>
<button
type="button"
onClick={() => remove(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 (UZ)")}</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 (rus tilida)")} />
</FormControl>
</FormItem>
)}
/>
</div>
))}
<Button
type="button"
onClick={() =>
append({ 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"
>
{t("Saqlash")}
</Button>
</div>
</form>
</Form>
);
};
export default StepTwo;