post bug fix
This commit is contained in:
2
.env
2
.env
@@ -1 +1 @@
|
|||||||
VITE_API_URL=https://simple-travel.felixits.uz/api/v1/
|
VITE_API_URL=https://api.simpletravel.uz/api/v1/
|
||||||
@@ -6,7 +6,7 @@ interface NewsData {
|
|||||||
desc: string;
|
desc: string;
|
||||||
title_ru: string;
|
title_ru: string;
|
||||||
desc_ru: string;
|
desc_ru: string;
|
||||||
category: string;
|
category: number | null;
|
||||||
banner: File | undefined | string;
|
banner: File | undefined | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export const useNewsStore = create<NewsStore>((set) => ({
|
|||||||
stepOneData: {
|
stepOneData: {
|
||||||
title: "",
|
title: "",
|
||||||
desc: "",
|
desc: "",
|
||||||
category: "",
|
category: null,
|
||||||
banner: undefined,
|
banner: undefined,
|
||||||
desc_ru: "",
|
desc_ru: "",
|
||||||
title_ru: "",
|
title_ru: "",
|
||||||
@@ -31,7 +31,7 @@ export const useNewsStore = create<NewsStore>((set) => ({
|
|||||||
stepOneData: {
|
stepOneData: {
|
||||||
title: "",
|
title: "",
|
||||||
desc: "",
|
desc: "",
|
||||||
category: "",
|
category: null,
|
||||||
banner: undefined,
|
banner: undefined,
|
||||||
desc_ru: "",
|
desc_ru: "",
|
||||||
title_ru: "",
|
title_ru: "",
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ export const newsForm = z.object({
|
|||||||
desc_ru: z.string().min(2, {
|
desc_ru: z.string().min(2, {
|
||||||
message: "Kamida 2 ta belgidan iborat bo‘lishi kerak.",
|
message: "Kamida 2 ta belgidan iborat bo‘lishi kerak.",
|
||||||
}),
|
}),
|
||||||
category: z.string().min(1, {
|
category: z.number().min(1, {
|
||||||
message: "Majburiy maydon",
|
message: "Majburiy maydon",
|
||||||
}),
|
}),
|
||||||
banner: fileSchema,
|
banner: fileSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// zod schema ni yangilaymiz
|
||||||
export const newsPostForm = z.object({
|
export const newsPostForm = z.object({
|
||||||
desc: z
|
desc: z
|
||||||
.string()
|
.string()
|
||||||
@@ -36,17 +37,22 @@ export const newsPostForm = z.object({
|
|||||||
sections: z
|
sections: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
image: fileSchema,
|
image: z.union([z.instanceof(File), z.string()]).optional(),
|
||||||
text: z.string().min(1, { message: "Matn bo'sh bo'lmasligi kerak." }),
|
text: z.string().optional(),
|
||||||
text_ru: z
|
text_ru: z.string().optional(),
|
||||||
.string()
|
|
||||||
.min(1, { message: "Ruscha matn bo'sh bo'lmasligi kerak." }),
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.min(1, { message: "Kamida bitta bo‘lim qo‘shing." }),
|
.optional(),
|
||||||
|
|
||||||
post_tags: z
|
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." }),
|
.min(1, { message: "Kamida bitta teg kiriting." }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -98,12 +98,8 @@ export interface NewsDetail {
|
|||||||
text_ru: string;
|
text_ru: string;
|
||||||
text_uz: string;
|
text_uz: string;
|
||||||
is_public: boolean;
|
is_public: boolean;
|
||||||
category: {
|
category: { id: number; name: string; name_ru: string; name_uz: string };
|
||||||
name: string;
|
post_tags: [
|
||||||
name_ru: string;
|
|
||||||
name_uz: string;
|
|
||||||
};
|
|
||||||
tag: [
|
|
||||||
{
|
{
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import StepOne from "@/pages/news/ui/StepOne";
|
|||||||
import StepTwo from "@/pages/news/ui/StepTwo";
|
import StepTwo from "@/pages/news/ui/StepTwo";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ const AddNews = () => {
|
|||||||
const isEditMode = useMemo(() => !!id, [id]);
|
const isEditMode = useMemo(() => !!id, [id]);
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data } = useQuery({
|
const { data, refetch } = useQuery({
|
||||||
queryKey: ["news_detail", id],
|
queryKey: ["news_detail", id],
|
||||||
queryFn: () => getDetailNews(Number(id)),
|
queryFn: () => getDetailNews(Number(id)),
|
||||||
select(data) {
|
select(data) {
|
||||||
@@ -22,6 +22,12 @@ const AddNews = () => {
|
|||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 w-full mx-auto bg-gray-900 text-white rounded-2xl shadow-lg">
|
<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">
|
<h1 className="text-3xl font-bold mb-6">
|
||||||
@@ -41,9 +47,16 @@ const AddNews = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{step === 1 && (
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"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 { Badge } from "@/shared/ui/badge";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import { Card } from "@/shared/ui/card";
|
import { Card } from "@/shared/ui/card";
|
||||||
@@ -10,12 +10,15 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/shared/ui/dialog";
|
} from "@/shared/ui/dialog";
|
||||||
|
import { Switch } from "@/shared/ui/switch";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Edit,
|
Edit,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Loader2,
|
Loader2,
|
||||||
PlusCircle,
|
PlusCircle,
|
||||||
@@ -41,11 +44,33 @@ const News = () => {
|
|||||||
queryFn: () => getAllNews({ page: currentPage, page_size: 10 }),
|
queryFn: () => getAllNews({ page: currentPage, page_size: 10 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate, isPending } = useMutation({
|
const { mutate: deleteMutate, isPending } = useMutation({
|
||||||
mutationFn: (id: number) => deleteNews(id),
|
mutationFn: (id: number) => deleteNews(id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setDeleteId(null);
|
setDeleteId(null);
|
||||||
queryClient.refetchQueries({ queryKey: ["all_news"] });
|
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: () => {
|
onError: () => {
|
||||||
toast.error(t("Xatolik yuz berdi"), {
|
toast.error(t("Xatolik yuz berdi"), {
|
||||||
@@ -57,10 +82,21 @@ const News = () => {
|
|||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = () => {
|
||||||
if (deleteId) {
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 w-full text-white flex justify-center items-center">
|
<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) => (
|
allNews?.data.data.results.map((item) => (
|
||||||
<Card
|
<Card
|
||||||
key={item.id}
|
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 */}
|
{/* Image */}
|
||||||
<div className="relative h-48 overflow-hidden">
|
<div className="relative h-64 overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src={item.image}
|
src={item.image}
|
||||||
alt={item.title}
|
alt={item.title}
|
||||||
@@ -158,7 +194,7 @@ const News = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-4 space-y-3">
|
<div className="p-4 space-y-3 flex-1 flex flex-col">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h2 className="text-xl font-bold line-clamp-2 group-hover:text-blue-400 transition-colors">
|
<h2 className="text-xl font-bold line-clamp-2 group-hover:text-blue-400 transition-colors">
|
||||||
{item.title}
|
{item.title}
|
||||||
@@ -169,21 +205,47 @@ const News = () => {
|
|||||||
{item.text}
|
{item.text}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Date */}
|
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
||||||
<span>{item.is_public}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Slug */}
|
{/* Slug */}
|
||||||
<div className="pt-2 border-t border-neutral-800">
|
{item.tag && item.tag.length > 0 && (
|
||||||
{item.tag?.map((e) => (
|
<div className="pt-2 border-t border-neutral-800 flex flex-wrap gap-2">
|
||||||
<code className="text-xs text-gray-500 bg-neutral-800 px-2 py-1 rounded">
|
{item.tag.map((e, idx) => (
|
||||||
/{e.name}
|
<code
|
||||||
</code>
|
key={idx}
|
||||||
))}
|
className="text-xs text-gray-500 bg-neutral-800 px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
/{e.name}
|
||||||
|
</code>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions - at the very bottom */}
|
||||||
<div className="flex justify-end gap-2 pt-3">
|
<div className="flex justify-end gap-2 pt-3">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate(`/news/edit/${item.id}`)}
|
onClick={() => navigate(`/news/edit/${item.id}`)}
|
||||||
@@ -240,7 +302,8 @@ const News = () => {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
{/* Pagination */}
|
||||||
|
<div className="flex justify-end gap-2 w-[90%] mx-auto mt-8">
|
||||||
<button
|
<button
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
||||||
|
|||||||
@@ -34,11 +34,12 @@ interface Data {
|
|||||||
text_uz: string;
|
text_uz: string;
|
||||||
is_public: boolean;
|
is_public: boolean;
|
||||||
category: {
|
category: {
|
||||||
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
name_ru: string;
|
name_ru: string;
|
||||||
name_uz: string;
|
name_uz: string;
|
||||||
};
|
};
|
||||||
tag: [
|
post_tags: [
|
||||||
{
|
{
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -59,66 +60,88 @@ interface Data {
|
|||||||
const StepOne = ({
|
const StepOne = ({
|
||||||
setStep,
|
setStep,
|
||||||
data: detail,
|
data: detail,
|
||||||
|
id,
|
||||||
}: {
|
}: {
|
||||||
setStep: Dispatch<SetStateAction<number>>;
|
setStep: Dispatch<SetStateAction<number>>;
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
data: Data;
|
data: Data;
|
||||||
|
id: number;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setStepOneData, stepOneData } = useNewsStore();
|
const { setStepOneData, stepOneData } = useNewsStore();
|
||||||
const hasReset = useRef(false); // 👈 infinite loopni oldini olish uchun
|
const hasReset = useRef(false);
|
||||||
|
|
||||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
// News Category fetch - barcha kategoriyalarni bir martada
|
||||||
useInfiniteQuery({
|
const {
|
||||||
queryKey: ["news_category", detail],
|
data: categoryData,
|
||||||
queryFn: ({ pageParam = 1 }) =>
|
fetchNextPage,
|
||||||
getAllNewsCategory({ page: pageParam, page_size: 10 }),
|
hasNextPage,
|
||||||
getNextPageParam: (lastPage) => {
|
isFetchingNextPage,
|
||||||
const currentPage = lastPage.data.data.current_page;
|
isLoading: isCategoriesLoading,
|
||||||
const totalPages = lastPage.data.data.total_pages;
|
} = useInfiniteQuery({
|
||||||
return currentPage < totalPages ? currentPage + 1 : undefined;
|
queryKey: ["news_category"],
|
||||||
},
|
queryFn: ({ pageParam = 1 }) =>
|
||||||
initialPageParam: 1,
|
getAllNewsCategory({ page: pageParam, page_size: 100 }),
|
||||||
});
|
getNextPageParam: (lastPage) => {
|
||||||
|
const currentPage = lastPage.data.data.current_page;
|
||||||
|
const totalPages = lastPage.data.data.total_pages;
|
||||||
|
return currentPage < totalPages ? currentPage + 1 : undefined;
|
||||||
|
},
|
||||||
|
initialPageParam: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Avtomatik barcha sahifalarni yuklash
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
|
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||||
|
|
||||||
const allCategories =
|
const allCategories =
|
||||||
data?.pages.flatMap((page) => page.data.data.results) ?? [];
|
categoryData?.pages.flatMap((page) => page.data.data.results) ?? [];
|
||||||
|
|
||||||
|
// Form setup
|
||||||
const form = useForm<z.infer<typeof newsForm>>({
|
const form = useForm<z.infer<typeof newsForm>>({
|
||||||
resolver: zodResolver(newsForm),
|
resolver: zodResolver(newsForm),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: stepOneData.title,
|
title: stepOneData.title || "",
|
||||||
category: stepOneData.category,
|
category: stepOneData.category || undefined,
|
||||||
banner: stepOneData.banner,
|
banner: stepOneData.banner,
|
||||||
desc: stepOneData.desc,
|
desc: stepOneData.desc || "",
|
||||||
desc_ru: stepOneData.desc_ru,
|
desc_ru: stepOneData.desc_ru || "",
|
||||||
title_ru: stepOneData.title_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(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
detail &&
|
!detail ||
|
||||||
allCategories.length > 0 &&
|
allCategories.length === 0 ||
|
||||||
!hasReset.current // faqat bir marta
|
hasReset.current ||
|
||||||
|
isCategoriesLoading
|
||||||
) {
|
) {
|
||||||
const foundCategory = allCategories.find(
|
return;
|
||||||
(cat) => cat.name === detail.category.name,
|
|
||||||
);
|
|
||||||
|
|
||||||
form.reset({
|
|
||||||
banner: detail.image as any,
|
|
||||||
category: foundCategory ? String(foundCategory.id) : "",
|
|
||||||
title: detail.title_uz,
|
|
||||||
title_ru: detail.title_ru,
|
|
||||||
desc: detail.text_uz,
|
|
||||||
desc_ru: detail.text_ru,
|
|
||||||
});
|
|
||||||
|
|
||||||
hasReset.current = true; // ✅ qayta reset bo‘lmasin
|
|
||||||
}
|
}
|
||||||
}, [detail, allCategories, form]);
|
|
||||||
|
const categoryId = detail.category.id;
|
||||||
|
|
||||||
|
form.reset({
|
||||||
|
banner: detail.image as any,
|
||||||
|
category: categoryId,
|
||||||
|
title: detail.title_uz,
|
||||||
|
title_ru: detail.title_ru,
|
||||||
|
desc: detail.text_uz,
|
||||||
|
desc_ru: detail.text_ru,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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>) {
|
function onSubmit(values: z.infer<typeof newsForm>) {
|
||||||
setStepOneData(values);
|
setStepOneData(values);
|
||||||
@@ -135,7 +158,7 @@ const StepOne = ({
|
|||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="space-y-6 bg-gray-900"
|
className="space-y-6 bg-gray-900"
|
||||||
>
|
>
|
||||||
{/* title */}
|
{/* Title */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="title"
|
name="title"
|
||||||
@@ -154,7 +177,7 @@ const StepOne = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* title_ru */}
|
{/* Title RU */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="title_ru"
|
name="title_ru"
|
||||||
@@ -173,7 +196,7 @@ const StepOne = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* desc */}
|
{/* Description */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="desc"
|
name="desc"
|
||||||
@@ -192,7 +215,7 @@ const StepOne = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* desc_ru */}
|
{/* Description RU */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="desc_ru"
|
name="desc_ru"
|
||||||
@@ -211,36 +234,44 @@ const StepOne = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* category */}
|
{/* Category */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="category"
|
name="category"
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<FormItem>
|
console.log("Category field value:", field.value);
|
||||||
<Label className="text-md">{t("Kategoriya")}</Label>
|
console.log("All categories:", allCategories);
|
||||||
<FormControl>
|
|
||||||
<InfiniteScrollSelect
|
return (
|
||||||
value={field.value}
|
<FormItem>
|
||||||
onValueChange={field.onChange}
|
<Label className="text-md">{t("Kategoriya")}</Label>
|
||||||
placeholder={t("Kategoriya tanlang")}
|
<FormControl>
|
||||||
label={t("Kategoriyalar")}
|
<InfiniteScrollSelect
|
||||||
data={allCategories}
|
value={field.value ? String(field.value) : ""}
|
||||||
hasNextPage={hasNextPage}
|
onValueChange={(value) => {
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
const numValue = Number(value);
|
||||||
fetchNextPage={fetchNextPage}
|
field.onChange(numValue);
|
||||||
renderOption={(cat) => ({
|
}}
|
||||||
key: cat.id,
|
placeholder={t("Kategoriya tanlang")}
|
||||||
value: String(cat.id),
|
label={t("Kategoriyalar")}
|
||||||
label: cat.name,
|
data={allCategories}
|
||||||
})}
|
hasNextPage={hasNextPage}
|
||||||
/>
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
</FormControl>
|
fetchNextPage={fetchNextPage}
|
||||||
<FormMessage />
|
renderOption={(cat) => ({
|
||||||
</FormItem>
|
key: cat.id,
|
||||||
)}
|
value: String(cat.id),
|
||||||
|
label: cat.name,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* banner */}
|
{/* Banner */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="banner"
|
name="banner"
|
||||||
@@ -301,6 +332,7 @@ const StepOne = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Submit */}
|
||||||
<div className="w-full flex justify-end">
|
<div className="w-full flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -38,22 +38,18 @@ interface Data {
|
|||||||
name_ru: string;
|
name_ru: string;
|
||||||
name_uz: string;
|
name_uz: string;
|
||||||
};
|
};
|
||||||
tag: [
|
post_tags: Array<{
|
||||||
{
|
id: number;
|
||||||
id: number;
|
name: string;
|
||||||
name: string;
|
name_ru: string;
|
||||||
name_ru: string;
|
name_uz: string;
|
||||||
name_uz: string;
|
}>;
|
||||||
},
|
post_images: Array<{
|
||||||
];
|
image: string;
|
||||||
post_images: [
|
text: string;
|
||||||
{
|
text_ru: string;
|
||||||
image: string;
|
text_uz: string;
|
||||||
text: string;
|
}>;
|
||||||
text_ru: string;
|
|
||||||
text_uz: string;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepTwo = ({
|
const StepTwo = ({
|
||||||
@@ -77,13 +73,12 @@ const StepTwo = ({
|
|||||||
desc_ru: "",
|
desc_ru: "",
|
||||||
is_public: "yes",
|
is_public: "yes",
|
||||||
sections: [{ image: undefined as any, text: "", text_ru: "" }],
|
sections: [{ image: undefined as any, text: "", text_ru: "" }],
|
||||||
post_tags: [""],
|
post_tags: [{ name: "", name_ru: "" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (detail && !hasReset.current) {
|
if (detail && !hasReset.current) {
|
||||||
// 🧠 xavfsiz map qilish
|
|
||||||
const mappedSections =
|
const mappedSections =
|
||||||
detail.post_images?.map((img) => ({
|
detail.post_images?.map((img) => ({
|
||||||
image: img.image,
|
image: img.image,
|
||||||
@@ -91,13 +86,18 @@ const StepTwo = ({
|
|||||||
text_ru: img.text_ru,
|
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({
|
form.reset({
|
||||||
desc: detail.text_uz || "",
|
desc: detail.text_uz || "",
|
||||||
desc_ru: detail.text_ru || "",
|
desc_ru: detail.text_ru || "",
|
||||||
is_public: detail.is_public ? "yes" : "no",
|
is_public: detail.is_public ? "yes" : "no",
|
||||||
post_tags: mappedTags.length > 0 ? mappedTags : [""],
|
post_tags:
|
||||||
|
mappedTags.length > 0 ? mappedTags : [{ name: "", name_ru: "" }],
|
||||||
sections:
|
sections:
|
||||||
mappedSections.length > 0
|
mappedSections.length > 0
|
||||||
? mappedSections
|
? mappedSections
|
||||||
@@ -108,18 +108,31 @@ const StepTwo = ({
|
|||||||
}
|
}
|
||||||
}, [detail, form]);
|
}, [detail, form]);
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const {
|
||||||
|
fields: sectionFields,
|
||||||
|
append: appendSection,
|
||||||
|
remove: removeSection,
|
||||||
|
} = useFieldArray({
|
||||||
control: form.control,
|
control: form.control,
|
||||||
name: "sections",
|
name: "sections",
|
||||||
});
|
});
|
||||||
const { watch, setValue } = form;
|
|
||||||
const postTags = watch("post_tags");
|
const {
|
||||||
const addTag = () => setValue("post_tags", [...postTags, ""]);
|
fields: tagFields,
|
||||||
const removeTag = (i: number) =>
|
append: appendTag,
|
||||||
setValue(
|
remove: removeTag,
|
||||||
"post_tags",
|
} = useFieldArray({
|
||||||
postTags.filter((_, idx) => idx !== i),
|
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({
|
const { mutate: added } = useMutation({
|
||||||
mutationFn: (body: FormData) => addNews(body),
|
mutationFn: (body: FormData) => addNews(body),
|
||||||
@@ -139,7 +152,7 @@ const StepTwo = ({
|
|||||||
|
|
||||||
const { mutate: update } = useMutation({
|
const { mutate: update } = useMutation({
|
||||||
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
|
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
|
||||||
updateNews({ id: id, body }),
|
updateNews({ id, body }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.refetchQueries({ queryKey: ["news_detail"] });
|
queryClient.refetchQueries({ queryKey: ["news_detail"] });
|
||||||
queryClient.refetchQueries({ queryKey: ["all_news"] });
|
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 onSubmit = (values: NewsPostFormType) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
@@ -176,28 +181,27 @@ const StepTwo = ({
|
|||||||
formData.append("image", stepOneData.banner);
|
formData.append("image", stepOneData.banner);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 sections
|
// Sections
|
||||||
values.sections.forEach((section, i) => {
|
values.sections?.forEach((section, i) => {
|
||||||
if (section.image instanceof File) {
|
if (section.image instanceof File)
|
||||||
formData.append(`post_images[${i}]`, section.image);
|
formData.append(`post_images[${i}]`, section.image);
|
||||||
}
|
if (section.text) formData.append(`post_text[${i}]`, section.text);
|
||||||
formData.append(`post_text[${i}]`, section.text);
|
if (section.text_ru)
|
||||||
formData.append(`post_text_ru[${i}]`, section.text_ru);
|
formData.append(`post_text_ru[${i}]`, section.text_ru);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Post Tags
|
||||||
values.post_tags.forEach((tag, i) => {
|
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({
|
if (id) update({ body: formData, id: Number(id) });
|
||||||
body: formData,
|
else added(formData);
|
||||||
id: Number(id),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
added(formData);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(form.formState.errors);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -239,45 +243,58 @@ const StepTwo = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Post Tags */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Label>{t("Teglar")}</Label>
|
<Label>{t("Teglar")}</Label>
|
||||||
{postTags.map((__, i) => (
|
{tagFields.map((field, i) => (
|
||||||
<FormField
|
<div key={field.id} className="flex gap-2 items-start">
|
||||||
key={i}
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`post_tags.${i}`}
|
name={`post_tags.${i}.name`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<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>
|
<FormControl>
|
||||||
<Input {...field} placeholder={t("Masalan: sport")} />
|
<Input {...field} placeholder={t("Teg (UZ)")} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</div>
|
</FormItem>
|
||||||
</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 mt-1"
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={addTag}
|
onClick={() => appendTag({ name: "", name_ru: "" })}
|
||||||
className="bg-gray-600 hover:bg-gray-700"
|
className="bg-gray-600 hover:bg-gray-700 mt-2"
|
||||||
>
|
>
|
||||||
<PlusCircle className="size-5 mr-2" />
|
<PlusCircle className="size-5 mr-2" />
|
||||||
{t("Teg qo'shish")}
|
{t("Teg qo'shish")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{fields.map((field, index) => (
|
{/* Sections */}
|
||||||
|
{sectionFields.map((field, index) => (
|
||||||
<div
|
<div
|
||||||
key={field.id}
|
key={field.id}
|
||||||
className="border border-gray-700 rounded-lg p-4 space-y-4"
|
className="border border-gray-700 rounded-lg p-4 space-y-4"
|
||||||
@@ -288,7 +305,7 @@ const StepTwo = ({
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => remove(index)}
|
onClick={() => removeSection(index)}
|
||||||
className="text-red-400 hover:text-red-500"
|
className="text-red-400 hover:text-red-500"
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
@@ -341,7 +358,7 @@ const StepTwo = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Text (UZ) */}
|
{/* Text UZ */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`sections.${index}.text`}
|
name={`sections.${index}.text`}
|
||||||
@@ -355,7 +372,7 @@ const StepTwo = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Text (RU) */}
|
{/* Text RU */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`sections.${index}.text_ru`}
|
name={`sections.${index}.text_ru`}
|
||||||
@@ -374,7 +391,7 @@ const StepTwo = ({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
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"
|
className="bg-gray-700 hover:bg-gray-600"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -530,5 +530,8 @@
|
|||||||
"Yuborish": "Отправить",
|
"Yuborish": "Отправить",
|
||||||
"Pulni o'tkazish": "Перевод средств",
|
"Pulni o'tkazish": "Перевод средств",
|
||||||
"To‘lov muvaffaqiyatli amalga oshirildi!": "Оплата успешно выполнена!",
|
"To‘lov muvaffaqiyatli amalga oshirildi!": "Оплата успешно выполнена!",
|
||||||
"Sizga tizimga kirishga ruxsat berilmagan": "Вам не разрешен доступ к системе."
|
"Sizga tizimga kirishga ruxsat berilmagan": "Вам не разрешен доступ к системе.",
|
||||||
|
"Оmmaviy": "Публичный",
|
||||||
|
"Shaxsiy": "Личный",
|
||||||
|
"Status o'zgartirildi": "Статус изменён"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -531,5 +531,8 @@
|
|||||||
"Yuborish": "Yuborish",
|
"Yuborish": "Yuborish",
|
||||||
"Pulni o'tkazish": "Pulni o'tkazish",
|
"Pulni o'tkazish": "Pulni o'tkazish",
|
||||||
"To‘lov muvaffaqiyatli amalga oshirildi!": "To‘lov muvaffaqiyatli amalga oshirildi!",
|
"To‘lov muvaffaqiyatli amalga oshirildi!": "To‘lov 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"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user