post bug fix

This commit is contained in:
Samandar Turgunboyev
2025-11-04 18:10:37 +05:00
parent c6c01a4607
commit 2d96eab3d7
10 changed files with 325 additions and 192 deletions

2
.env
View File

@@ -1 +1 @@
VITE_API_URL=https://simple-travel.felixits.uz/api/v1/
VITE_API_URL=https://api.simpletravel.uz/api/v1/

View File

@@ -6,7 +6,7 @@ interface NewsData {
desc: string;
title_ru: string;
desc_ru: string;
category: string;
category: number | null;
banner: File | undefined | string;
}
@@ -20,7 +20,7 @@ export const useNewsStore = create<NewsStore>((set) => ({
stepOneData: {
title: "",
desc: "",
category: "",
category: null,
banner: undefined,
desc_ru: "",
title_ru: "",
@@ -31,7 +31,7 @@ export const useNewsStore = create<NewsStore>((set) => ({
stepOneData: {
title: "",
desc: "",
category: "",
category: null,
banner: undefined,
desc_ru: "",
title_ru: "",

View File

@@ -18,12 +18,13 @@ export const newsForm = z.object({
desc_ru: z.string().min(2, {
message: "Kamida 2 ta belgidan iborat bolishi kerak.",
}),
category: z.string().min(1, {
category: z.number().min(1, {
message: "Majburiy maydon",
}),
banner: fileSchema,
});
// zod schema ni yangilaymiz
export const newsPostForm = z.object({
desc: z
.string()
@@ -36,17 +37,22 @@ export const newsPostForm = z.object({
sections: z
.array(
z.object({
image: fileSchema,
text: z.string().min(1, { message: "Matn bo'sh bo'lmasligi kerak." }),
text_ru: z
.string()
.min(1, { message: "Ruscha matn bo'sh bo'lmasligi kerak." }),
image: z.union([z.instanceof(File), z.string()]).optional(),
text: z.string().optional(),
text_ru: z.string().optional(),
}),
)
.min(1, { message: "Kamida bitta bolim qoshing." }),
.optional(),
post_tags: z
.array(z.string().min(1, { message: "Teg bo'sh bo'lmasligi kerak." }))
.array(
z.object({
name: z.string().min(1, { message: "Teg bo'sh bo'lmasligi kerak." }),
name_ru: z
.string()
.min(1, { message: "Teg (RU) bo'sh bo'lmasligi kerak." }),
}),
)
.min(1, { message: "Kamida bitta teg kiriting." }),
});

View File

@@ -98,12 +98,8 @@ export interface NewsDetail {
text_ru: string;
text_uz: string;
is_public: boolean;
category: {
name: string;
name_ru: string;
name_uz: string;
};
tag: [
category: { id: number; name: string; name_ru: string; name_uz: string };
post_tags: [
{
id: number;
name: string;

View File

@@ -4,7 +4,7 @@ import StepOne from "@/pages/news/ui/StepOne";
import StepTwo from "@/pages/news/ui/StepTwo";
import { useQuery } from "@tanstack/react-query";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
@@ -13,7 +13,7 @@ const AddNews = () => {
const isEditMode = useMemo(() => !!id, [id]);
const [step, setStep] = useState(1);
const { t } = useTranslation();
const { data } = useQuery({
const { data, refetch } = useQuery({
queryKey: ["news_detail", id],
queryFn: () => getDetailNews(Number(id)),
select(data) {
@@ -22,6 +22,12 @@ const AddNews = () => {
enabled: !!id,
});
useEffect(() => {
if (id) {
refetch();
}
}, [id]);
return (
<div className="p-8 w-full mx-auto bg-gray-900 text-white rounded-2xl shadow-lg">
<h1 className="text-3xl font-bold mb-6">
@@ -41,9 +47,16 @@ const AddNews = () => {
</div>
</div>
{step === 1 && (
<StepOne isEditMode={isEditMode} setStep={setStep} data={data!} />
<StepOne
isEditMode={isEditMode}
setStep={setStep}
data={data!}
id={Number(id)}
/>
)}
{step === 2 && (
<StepTwo key={id} data={data!} isEditMode={isEditMode} id={id!} />
)}
{step === 2 && <StepTwo data={data!} isEditMode={isEditMode} id={id!} />}
</div>
);
};

View File

@@ -1,5 +1,5 @@
"use client";
import { deleteNews, getAllNews } from "@/pages/news/lib/api";
import { deleteNews, getAllNews, updateNews } from "@/pages/news/lib/api";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Card } from "@/shared/ui/card";
@@ -10,12 +10,15 @@ import {
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { Switch } from "@/shared/ui/switch";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import clsx from "clsx";
import {
ChevronLeft,
ChevronRight,
Edit,
Eye,
EyeOff,
FolderOpen,
Loader2,
PlusCircle,
@@ -41,11 +44,33 @@ const News = () => {
queryFn: () => getAllNews({ page: currentPage, page_size: 10 }),
});
const { mutate, isPending } = useMutation({
const { mutate: deleteMutate, isPending } = useMutation({
mutationFn: (id: number) => deleteNews(id),
onSuccess: () => {
setDeleteId(null);
queryClient.refetchQueries({ queryKey: ["all_news"] });
toast.success(t("Yangilik o'chirildi"), {
richColors: true,
position: "top-center",
});
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
richColors: true,
position: "top-center",
});
},
});
const { mutate: togglePublicMutate } = useMutation({
mutationFn: ({ id, body }: { id: number; body: FormData }) =>
updateNews({ body, id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["all_news"] });
toast.success(t("Status o'zgartirildi"), {
richColors: true,
position: "top-center",
});
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
@@ -57,10 +82,21 @@ const News = () => {
const confirmDelete = () => {
if (deleteId) {
mutate(deleteId);
deleteMutate(deleteId);
}
};
const handleTogglePublic = (id: number, currentStatus: boolean) => {
const formData = new FormData();
console.log(currentStatus);
formData.append("is_public", String(currentStatus));
togglePublicMutate({
id,
body: formData,
});
};
if (isLoading) {
return (
<div className="min-h-screen bg-gray-900 w-full text-white flex justify-center items-center">
@@ -135,10 +171,10 @@ const News = () => {
allNews?.data.data.results.map((item) => (
<Card
key={item.id}
className="overflow-hidden bg-neutral-900 hover:bg-neutral-800 transition-all duration-300 border border-neutral-800 hover:border-neutral-700 group"
className="overflow-hidden p-0 bg-neutral-900 hover:bg-neutral-800 transition-all duration-300 border border-neutral-800 hover:border-neutral-700 group flex flex-col"
>
{/* Image */}
<div className="relative h-48 overflow-hidden">
<div className="relative h-64 overflow-hidden">
<img
src={item.image}
alt={item.title}
@@ -158,7 +194,7 @@ const News = () => {
</div>
{/* Content */}
<div className="p-4 space-y-3">
<div className="p-4 space-y-3 flex-1 flex flex-col">
{/* Title */}
<h2 className="text-xl font-bold line-clamp-2 group-hover:text-blue-400 transition-colors">
{item.title}
@@ -169,21 +205,47 @@ const News = () => {
{item.text}
</p>
{/* Date */}
<div className="flex items-center gap-2 text-xs text-gray-500">
<span>{item.is_public}</span>
</div>
{/* Slug */}
<div className="pt-2 border-t border-neutral-800">
{item.tag?.map((e) => (
<code className="text-xs text-gray-500 bg-neutral-800 px-2 py-1 rounded">
{item.tag && item.tag.length > 0 && (
<div className="pt-2 border-t border-neutral-800 flex flex-wrap gap-2">
{item.tag.map((e, idx) => (
<code
key={idx}
className="text-xs text-gray-500 bg-neutral-800 px-2 py-1 rounded"
>
/{e.name}
</code>
))}
</div>
)}
{/* Actions */}
{/* Spacer to push content to bottom */}
<div className="flex-1"></div>
{/* Public/Private Toggle */}
<div className="pt-3 border-t border-neutral-800">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{item.is_public ? (
<Eye size={16} className="text-green-500" />
) : (
<EyeOff size={16} className="text-gray-500" />
)}
<span className="text-sm text-gray-400">
{t("Оmmaviy")}
</span>
</div>
<Switch
checked={item.is_public}
onCheckedChange={() =>
handleTogglePublic(item.id, !item.is_public)
}
className="data-[state=checked]:bg-green-600"
/>
</div>
</div>
{/* Actions - at the very bottom */}
<div className="flex justify-end gap-2 pt-3">
<Button
onClick={() => navigate(`/news/edit/${item.id}`)}
@@ -240,7 +302,8 @@ const News = () => {
</DialogContent>
</Dialog>
<div className="flex justify-end gap-2">
{/* Pagination */}
<div className="flex justify-end gap-2 w-[90%] mx-auto mt-8">
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}

View File

@@ -34,11 +34,12 @@ interface Data {
text_uz: string;
is_public: boolean;
category: {
id: number;
name: string;
name_ru: string;
name_uz: string;
};
tag: [
post_tags: [
{
id: number;
name: string;
@@ -59,20 +60,28 @@ interface Data {
const StepOne = ({
setStep,
data: detail,
id,
}: {
setStep: Dispatch<SetStateAction<number>>;
isEditMode: boolean;
data: Data;
id: number;
}) => {
const { t } = useTranslation();
const { setStepOneData, stepOneData } = useNewsStore();
const hasReset = useRef(false); // 👈 infinite loopni oldini olish uchun
const hasReset = useRef(false);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
queryKey: ["news_category", detail],
// News Category fetch - barcha kategoriyalarni bir martada
const {
data: categoryData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading: isCategoriesLoading,
} = useInfiniteQuery({
queryKey: ["news_category"],
queryFn: ({ pageParam = 1 }) =>
getAllNewsCategory({ page: pageParam, page_size: 10 }),
getAllNewsCategory({ page: pageParam, page_size: 100 }),
getNextPageParam: (lastPage) => {
const currentPage = lastPage.data.data.current_page;
const totalPages = lastPage.data.data.total_pages;
@@ -81,44 +90,58 @@ const StepOne = ({
initialPageParam: 1,
});
const allCategories =
data?.pages.flatMap((page) => page.data.data.results) ?? [];
// Avtomatik barcha sahifalarni yuklash
useEffect(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const allCategories =
categoryData?.pages.flatMap((page) => page.data.data.results) ?? [];
// Form setup
const form = useForm<z.infer<typeof newsForm>>({
resolver: zodResolver(newsForm),
defaultValues: {
title: stepOneData.title,
category: stepOneData.category,
title: stepOneData.title || "",
category: stepOneData.category || undefined,
banner: stepOneData.banner,
desc: stepOneData.desc,
desc_ru: stepOneData.desc_ru,
title_ru: stepOneData.title_ru,
desc: stepOneData.desc || "",
desc_ru: stepOneData.desc_ru || "",
title_ru: stepOneData.title_ru || "",
},
});
// ✅ reset faqat bir marta, ma'lumot tayyor bo'lganda ishlaydi
// Reset form when detail & categories are ready (EDIT MODE)
useEffect(() => {
if (
detail &&
allCategories.length > 0 &&
!hasReset.current // faqat bir marta
!detail ||
allCategories.length === 0 ||
hasReset.current ||
isCategoriesLoading
) {
const foundCategory = allCategories.find(
(cat) => cat.name === detail.category.name,
);
return;
}
const categoryId = detail.category.id;
form.reset({
banner: detail.image as any,
category: foundCategory ? String(foundCategory.id) : "",
category: categoryId,
title: detail.title_uz,
title_ru: detail.title_ru,
desc: detail.text_uz,
desc_ru: detail.text_ru,
});
hasReset.current = true; // ✅ qayta reset bolmasin
}
}, [detail, allCategories, form]);
// Kategoriyani alohida set qilish
setTimeout(() => {
form.setValue("category", categoryId, { shouldValidate: false });
}, 0);
hasReset.current = true;
}, [detail, allCategories, form, isCategoriesLoading]);
function onSubmit(values: z.infer<typeof newsForm>) {
setStepOneData(values);
@@ -135,7 +158,7 @@ const StepOne = ({
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 bg-gray-900"
>
{/* title */}
{/* Title */}
<FormField
control={form.control}
name="title"
@@ -154,7 +177,7 @@ const StepOne = ({
)}
/>
{/* title_ru */}
{/* Title RU */}
<FormField
control={form.control}
name="title_ru"
@@ -173,7 +196,7 @@ const StepOne = ({
)}
/>
{/* desc */}
{/* Description */}
<FormField
control={form.control}
name="desc"
@@ -192,7 +215,7 @@ const StepOne = ({
)}
/>
{/* desc_ru */}
{/* Description RU */}
<FormField
control={form.control}
name="desc_ru"
@@ -211,17 +234,24 @@ const StepOne = ({
)}
/>
{/* category */}
{/* Category */}
<FormField
control={form.control}
name="category"
render={({ field }) => (
render={({ field }) => {
console.log("Category field value:", field.value);
console.log("All categories:", allCategories);
return (
<FormItem>
<Label className="text-md">{t("Kategoriya")}</Label>
<FormControl>
<InfiniteScrollSelect
value={field.value}
onValueChange={field.onChange}
value={field.value ? String(field.value) : ""}
onValueChange={(value) => {
const numValue = Number(value);
field.onChange(numValue);
}}
placeholder={t("Kategoriya tanlang")}
label={t("Kategoriyalar")}
data={allCategories}
@@ -237,10 +267,11 @@ const StepOne = ({
</FormControl>
<FormMessage />
</FormItem>
)}
);
}}
/>
{/* banner */}
{/* Banner */}
<FormField
control={form.control}
name="banner"
@@ -301,6 +332,7 @@ const StepOne = ({
)}
/>
{/* Submit */}
<div className="w-full flex justify-end">
<Button
type="submit"

View File

@@ -38,22 +38,18 @@ interface Data {
name_ru: string;
name_uz: string;
};
tag: [
{
post_tags: Array<{
id: number;
name: string;
name_ru: string;
name_uz: string;
},
];
post_images: [
{
}>;
post_images: Array<{
image: string;
text: string;
text_ru: string;
text_uz: string;
},
];
}>;
}
const StepTwo = ({
@@ -77,13 +73,12 @@ const StepTwo = ({
desc_ru: "",
is_public: "yes",
sections: [{ image: undefined as any, text: "", text_ru: "" }],
post_tags: [""],
post_tags: [{ name: "", name_ru: "" }],
},
});
useEffect(() => {
if (detail && !hasReset.current) {
// 🧠 xavfsiz map qilish
const mappedSections =
detail.post_images?.map((img) => ({
image: img.image,
@@ -91,13 +86,18 @@ const StepTwo = ({
text_ru: img.text_ru,
})) ?? [];
const mappedTags = detail.tag?.map((t) => t.name_uz) ?? [];
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 : [""],
post_tags:
mappedTags.length > 0 ? mappedTags : [{ name: "", name_ru: "" }],
sections:
mappedSections.length > 0
? mappedSections
@@ -108,18 +108,31 @@ const StepTwo = ({
}
}, [detail, form]);
const { fields, append, remove } = useFieldArray({
const {
fields: sectionFields,
append: appendSection,
remove: removeSection,
} = 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 {
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 } = useMutation({
mutationFn: (body: FormData) => addNews(body),
@@ -139,7 +152,7 @@ const StepTwo = ({
const { mutate: update } = useMutation({
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
updateNews({ id: id, body }),
updateNews({ id, body }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["news_detail"] });
queryClient.refetchQueries({ queryKey: ["all_news"] });
@@ -154,14 +167,6 @@ const StepTwo = ({
},
});
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();
@@ -176,28 +181,27 @@ const StepTwo = ({
formData.append("image", stepOneData.banner);
}
// 🔥 sections
values.sections.forEach((section, i) => {
if (section.image instanceof File) {
// 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);
if (section.text) formData.append(`post_text[${i}]`, section.text);
if (section.text_ru)
formData.append(`post_text_ru[${i}]`, section.text_ru);
});
// Post Tags
values.post_tags.forEach((tag, i) => {
formData.append(`post_tags[${i}]`, tag);
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);
}
if (id) update({ body: formData, id: Number(id) });
else added(formData);
};
console.log(form.formState.errors);
return (
<Form {...form}>
<form
@@ -239,45 +243,58 @@ const StepTwo = ({
)}
/>
{/* Post Tags */}
<div className="space-y-4">
<Label>{t("Teglar")}</Label>
{postTags.map((__, i) => (
{tagFields.map((field, i) => (
<div key={field.id} className="flex gap-2 items-start">
<FormField
key={i}
control={form.control}
name={`post_tags.${i}`}
name={`post_tags.${i}.name`}
render={({ field }) => (
<FormItem>
<div className="relative">
{postTags.length > 1 && (
<FormControl>
<Input {...field} placeholder={t("Teg (UZ)")} />
</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="absolute top-1 right-1 text-red-400 hover:text-red-500"
className="text-red-400 hover:text-red-500 mt-1"
>
<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"
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>
{fields.map((field, index) => (
{/* Sections */}
{sectionFields.map((field, index) => (
<div
key={field.id}
className="border border-gray-700 rounded-lg p-4 space-y-4"
@@ -288,7 +305,7 @@ const StepTwo = ({
</p>
<button
type="button"
onClick={() => remove(index)}
onClick={() => removeSection(index)}
className="text-red-400 hover:text-red-500"
>
<Trash2 className="size-4" />
@@ -341,7 +358,7 @@ const StepTwo = ({
)}
</div>
{/* Text (UZ) */}
{/* Text UZ */}
<FormField
control={form.control}
name={`sections.${index}.text`}
@@ -355,7 +372,7 @@ const StepTwo = ({
)}
/>
{/* Text (RU) */}
{/* Text RU */}
<FormField
control={form.control}
name={`sections.${index}.text_ru`}
@@ -374,7 +391,7 @@ const StepTwo = ({
<Button
type="button"
onClick={() =>
append({ image: undefined as any, text: "", text_ru: "" })
appendSection({ image: undefined as any, text: "", text_ru: "" })
}
className="bg-gray-700 hover:bg-gray-600"
>

View File

@@ -530,5 +530,8 @@
"Yuborish": "Отправить",
"Pulni o'tkazish": "Перевод средств",
"Tolov muvaffaqiyatli amalga oshirildi!": "Оплата успешно выполнена!",
"Sizga tizimga kirishga ruxsat berilmagan": "Вам не разрешен доступ к системе."
"Sizga tizimga kirishga ruxsat berilmagan": "Вам не разрешен доступ к системе.",
"Оmmaviy": "Публичный",
"Shaxsiy": "Личный",
"Status o'zgartirildi": "Статус изменён"
}

View File

@@ -531,5 +531,8 @@
"Yuborish": "Yuborish",
"Pulni o'tkazish": "Pulni o'tkazish",
"Tolov muvaffaqiyatli amalga oshirildi!": "Tolov muvaffaqiyatli amalga oshirildi!",
"Sizga tizimga kirishga ruxsat berilmagan": "Sizga tizimga kirishga ruxsat berilmagan"
"Sizga tizimga kirishga ruxsat berilmagan": "Sizga tizimga kirishga ruxsat berilmagan",
"Оmmaviy": "Ommaviy",
"Shaxsiy": "Shaxsiy",
"Status o'zgartirildi": "Status o'zgartirildi"
}