399 lines
12 KiB
TypeScript
399 lines
12 KiB
TypeScript
"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 bo‘limlari")}</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("Bo‘lim")} #{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("Bo‘lim qo‘shish")}
|
||
</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;
|