446 lines
13 KiB
TypeScript
446 lines
13 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, 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 bo‘limlari")}</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("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"
|
||
>
|
||
{isPending || updatePending ? (
|
||
<Loader2 className="animate-spin" />
|
||
) : (
|
||
t("Saqlash")
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Form>
|
||
);
|
||
};
|
||
|
||
export default StepTwo;
|