api ulandi

This commit is contained in:
Samandar Turgunboyev
2025-10-25 18:42:01 +05:00
parent 1a08775451
commit 05b752daf2
84 changed files with 11179 additions and 3724 deletions

View File

@@ -1,4 +1,8 @@
"use client";
import { addNews } 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,
@@ -11,180 +15,275 @@ 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 { PlusCircle, Trash2, XIcon } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { ImagePlus, PlusCircle, Trash2 } from "lucide-react";
import { useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import z from "zod";
import { toast } from "sonner";
const newsItemSchema = z.object({
desc: z.string().min(2, {
message: "Yangilik matni kamida 2 belgidan iborat bolishi kerak",
}),
banner: z.string().min(1, { message: "Banner rasmi majburiy" }),
});
const StepTwo = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { stepOneData } = useNewsStore();
const queryClient = useQueryClient();
const newsListSchema = z.object({
items: z
.array(newsItemSchema)
.min(1, { message: "Kamida 1 ta yangilik kerak" }),
});
type NewsFormType = z.infer<typeof newsListSchema>;
const StepTwo = ({
setStep,
isEditMode,
}: {
setStep: Dispatch<SetStateAction<number>>;
isEditMode: boolean;
}) => {
const form = useForm<NewsFormType>({
resolver: zodResolver(newsListSchema),
const form = useForm<NewsPostFormType>({
resolver: zodResolver(newsPostForm),
defaultValues: {
items: [{ desc: "", banner: "" }],
desc: "",
desc_ru: "",
is_public: "yes",
sections: [{ image: undefined as any, text: "", text_ru: "" }],
post_tags: [""],
},
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "items",
name: "sections",
});
const navigator = useNavigate();
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),
);
function onSubmit() {
navigator("/news");
}
const { mutate: added } = useMutation({
mutationFn: (body: FormData) => addNews(body),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["all_news"] });
navigate("/news");
},
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("image", stepOneData.banner ?? "");
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);
values.sections.forEach((section, i) => {
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);
});
added(formData);
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 bg-gray-900 p-6 rounded-2xl text-white"
className="space-y-6 bg-gray-900 p-6 rounded-2xl text-white"
>
<h2 className="text-2xl font-semibold">Yangiliklar royxati</h2>
<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="relative border border-gray-700 bg-gray-800 rounded-xl p-4 space-y-4"
className="border border-gray-700 rounded-lg p-4 space-y-4"
>
{/* O'chirish tugmasi */}
{fields.length > 1 && (
<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="absolute top-3 right-3 text-red-400 hover:text-red-500"
className="text-red-400 hover:text-red-500"
>
<Trash2 className="size-5" />
<Trash2 className="size-4" />
</button>
)}
</div>
{/* DESC FIELD */}
{/* Image */}
<div>
<Label>{t("Rasm")}</Label>
{form.watch(`sections.${index}.image`) ? (
<div className="relative mt-2 w-48">
<img
src={URL.createObjectURL(
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={`items.${index}.desc`}
name={`sections.${index}.text`}
render={({ field }) => (
<FormItem>
<Label className="text-md">Yangilik haqida</Label>
<Label>{t("Matn (UZ)")}</Label>
<FormControl>
<Textarea
placeholder="Yangilik haqida"
{...field}
className="min-h-48 max-h-56 !text-md bg-gray-800 border-gray-700 text-white"
/>
<Textarea {...field} placeholder={t("Matn kiriting")} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* BANNER FIELD */}
{/* Text (RU) */}
<FormField
control={form.control}
name={`items.${index}.banner`}
render={() => (
name={`sections.${index}.text_ru`}
render={({ field }) => (
<FormItem>
<Label className="text-md">Banner rasmi</Label>
<Label>{t("Matn (RU)")}</Label>
<FormControl>
<div className="flex flex-col gap-3 w-full">
<Input
type="file"
id={`file-${index}`}
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
form.setValue(`items.${index}.banner`, url);
}
}}
className="hidden"
/>
<label
htmlFor={`file-${index}`}
className="w-full border-2 border-dashed h-40 border-gray-600 hover:border-gray-500 transition-all flex flex-col items-center gap-2 justify-center py-4 rounded-2xl cursor-pointer"
>
<p className="font-semibold text-xl text-white">
Drag or select files
</p>
<p className="text-gray-300 text-sm">
Drop files here or click to browse
</p>
</label>
{form.watch(`items.${index}.banner`) && (
<div className="relative size-24 rounded-md overflow-hidden border border-gray-700">
<img
src={form.watch(`items.${index}.banner`)}
alt="Banner preview"
className="object-cover w-full h-full"
/>
<button
type="button"
onClick={() =>
form.setValue(`items.${index}.banner`, "")
}
className="absolute top-1 right-1 bg-white/80 rounded-full p-1 shadow hover:bg-white"
>
<XIcon className="size-4 text-destructive" />
</button>
</div>
)}
</div>
<Textarea {...field} placeholder={t("Matn (rus tilida)")} />
</FormControl>
<FormMessage />
</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="button"
onClick={() => append({ desc: "", banner: "" })}
className="flex items-center px-6 py-5 text-lg gap-2 bg-gray-600 hover:bg-gray-700 text-white cursor-pointer"
>
<PlusCircle className="size-5" />
Qoshish
</Button>
</div>
{/* Navigatsiya tugmalari */}
<div className="w-full flex justify-between pt-4">
<Button
type="button"
onClick={() => setStep(1)}
className="bg-gray-600 hover:bg-gray-700 text-white"
>
Orqaga
</Button>
<Button
type="submit"
className="bg-blue-600 hover:bg-blue-700 text-white"
className="mt-6 px-8 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 cursor-pointer"
>
{isEditMode ? "Yangiliklarni saqlash" : "Saqlash"}
{t("Saqlash")}
</Button>
</div>
</form>