diff --git a/index.html b/index.html index 6ea9d59..b86a998 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ - + - FIAS - React Js app + Simple Travel
diff --git a/package-lock.json b/package-lock.json index b7f3d0b..451f109 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.7", @@ -34,6 +35,7 @@ "cmdk": "^1.1.1", "date-fns": "^4.1.0", "dayjs": "^1.11.18", + "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.24", "i18next": "^25.5.2", "i18next-browser-languagedetector": "^8.2.0", @@ -2109,6 +2111,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tabs": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", @@ -3860,6 +3891,34 @@ "integrity": "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==", "dev": true }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, "node_modules/emoji-regex": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", diff --git a/package.json b/package.json index e36bd21..bc0a08a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.7", @@ -38,6 +39,7 @@ "cmdk": "^1.1.1", "date-fns": "^4.1.0", "dayjs": "^1.11.18", + "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.24", "i18next": "^25.5.2", "i18next-browser-languagedetector": "^8.2.0", diff --git a/public/Logo_blue.png b/public/Logo_blue.png new file mode 100644 index 0000000..6758c9d Binary files /dev/null and b/public/Logo_blue.png differ diff --git a/public/navLogo.png b/public/navLogo.png new file mode 100644 index 0000000..6758c9d Binary files /dev/null and b/public/navLogo.png differ diff --git a/src/App.tsx b/src/App.tsx index b5e754a..c3ee832 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ import AddNews from "@/pages/news/ui/AddNews"; import News from "@/pages/news/ui/News"; import NewsCategory from "@/pages/news/ui/NewsCategory"; import Seo from "@/pages/seo/ui/Seo"; +import SiteBannerAdmin from "@/pages/site-banner/ui/Banner"; import PolicyCrud from "@/pages/site-page/ui/PolicyCrud"; import SitePage from "@/pages/site-page/ui/SitePage"; import SupportAgency from "@/pages/support/ui/SupportAgency"; @@ -88,6 +89,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> } /> @@ -97,6 +99,7 @@ const App = () => { } /> } /> } /> + } /> diff --git a/src/pages/faq/lib/api.ts b/src/pages/faq/lib/api.ts index bb9d080..578da5c 100644 --- a/src/pages/faq/lib/api.ts +++ b/src/pages/faq/lib/api.ts @@ -1,4 +1,9 @@ -import type { Faq, FaqCategory, FaqCategoryDetail } from "@/pages/faq/lib/type"; +import type { + Faq, + FaqCategory, + FaqCategoryDetail, + FaqOne, +} from "@/pages/faq/lib/type"; import httpClient from "@/shared/config/api/httpClient"; import { FAQ, FAQ_CATEGORIES } from "@/shared/config/api/URLs"; import type { AxiosResponse } from "axios"; @@ -6,11 +11,50 @@ import type { AxiosResponse } from "axios"; const getAllFaq = async (params: { page: number; page_size: number; + category: number; }): Promise> => { const res = await httpClient.get(FAQ, { params }); return res; }; +const getOneFaq = async (id: number): Promise> => { + const res = await httpClient.get(`${FAQ}${id}/`); + return res; +}; + +const createFaq = async (body: { + title: string; + title_ru: string; + text: string; + text_ru: string; + category: number; +}) => { + const res = await httpClient.post(FAQ, body); + return res; +}; + +const updateFaq = async ({ + body, + id, +}: { + id: number; + body: { + title: string; + title_ru: string; + text: string; + text_ru: string; + category?: number; + }; +}) => { + const res = await httpClient.patch(`${FAQ}${id}/`, body); + return res; +}; + +const deleteFaq = async ({ id }: { id: number }) => { + const res = await httpClient.delete(`${FAQ}${id}/`); + return res; +}; + const getAllFaqCategory = async (params: { page: number; page_size: number; @@ -48,10 +92,14 @@ const deleteFaqCategory = async (id: number) => { }; export { + createFaq, createFaqCategory, + deleteFaq, deleteFaqCategory, getAllFaq, getAllFaqCategory, getDetailFaqCategory, + getOneFaq, + updateFaq, updateFaqCategory, }; diff --git a/src/pages/faq/lib/type.ts b/src/pages/faq/lib/type.ts index 1e7ed5f..06086d8 100644 --- a/src/pages/faq/lib/type.ts +++ b/src/pages/faq/lib/type.ts @@ -21,6 +21,7 @@ export interface FaqCategoryDetail { data: { id: number; name: string; + name_uz: string; name_ru: string; }; } @@ -48,3 +49,16 @@ export interface Faq { }[]; }; } + +export interface FaqOne { + status: boolean; + data: { + id: number; + title: string; + title_uz: string; + title_ru: string; + text: string; + text_ru: string; + text_uz: string; + }; +} diff --git a/src/pages/faq/ui/Faq.tsx b/src/pages/faq/ui/Faq.tsx index f32cf64..b1e4c70 100644 --- a/src/pages/faq/ui/Faq.tsx +++ b/src/pages/faq/ui/Faq.tsx @@ -1,6 +1,13 @@ "use client"; -import { getAllFaq, getAllFaqCategory } from "@/pages/faq/lib/api"; +import { + createFaq, + deleteFaq, + getAllFaq, + getAllFaqCategory, + getOneFaq, + updateFaq, +} from "@/pages/faq/lib/api"; import { Button } from "@/shared/ui/button"; import { Dialog, @@ -16,16 +23,9 @@ import { FormItem, FormMessage, } from "@/shared/ui/form"; +import { InfiniteScrollSelect } from "@/shared/ui/infiniteScrollSelect"; import { Input } from "@/shared/ui/input"; import { Label } from "@/shared/ui/label"; -import { - Select, - SelectContent, - SelectGroup, - SelectLabel, - SelectTrigger, - SelectValue, -} from "@/shared/ui/select"; import { Table, TableBody, @@ -37,70 +37,231 @@ import { import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs"; import { Textarea } from "@/shared/ui/textarea"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useQuery } from "@tanstack/react-query"; -import { Pencil, PlusCircle, Trash2 } from "lucide-react"; -import { useEffect, useState } from "react"; +import { + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { + ChevronRight, + Loader2, + Pencil, + PlusCircle, + Trash2, +} from "lucide-react"; +import { useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import z from "zod"; const faqForm = z.object({ categories: z.string().min(1, { message: "Majburiy maydon" }), title: z.string().min(1, { message: "Majburiy maydon" }), + title_ru: z.string().min(1, { message: "Majburiy maydon" }), answer: z.string().min(1, { message: "Majburiy maydon" }), + answer_ru: z.string().min(1, { message: "Majburiy maydon" }), }); const Faq = () => { - const { t } = useTranslation(); - const { data: category } = useQuery({ - queryKey: ["all_faqcategory"], - queryFn: () => { - return getAllFaqCategory({ page: 1, page_size: 99 }); - }, - select(data) { - return data.data.data.results; - }, - }); - const { data: faq } = useQuery({ - queryKey: ["all_faq"], - queryFn: () => { - return getAllFaq({ page: 1, page_size: 99 }); - }, - select(data) { - return data.data.data.results; - }, - }); const [activeTab, setActiveTab] = useState(""); + const { t } = useTranslation(); + const [openModal, setOpenModal] = useState(false); + const [editFaq, setEditFaq] = useState(null); + const queryClient = useQueryClient(); + const scrollRef = useRef(null); + const loaderRef = useRef(null); + + // Infinite scroll uchun useInfiniteQuery + const { + data: categoryData, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useInfiniteQuery({ + queryKey: ["all_faqcategory"], + queryFn: ({ pageParam = 1 }) => { + return getAllFaqCategory({ page: pageParam, page_size: 10 }); + }, + getNextPageParam: (lastPage) => { + const data = lastPage.data.data; + if (data.current_page < data.total_pages) { + return data.current_page + 1; + } + return undefined; + }, + initialPageParam: 1, + }); + + // Barcha kategoriyalarni birlashtirib olish + const category = + categoryData?.pages.flatMap((page) => page.data.data.results) ?? []; + + const { data: faq } = useQuery({ + queryKey: ["all_faq", activeTab], + queryFn: () => { + return getAllFaq({ page: 1, page_size: 10, category: Number(activeTab) }); + }, + select(data) { + return data.data.data.results; + }, + enabled: !!activeTab, + }); + + const { data: detailFaq } = useQuery({ + queryKey: ["detail_faq", editFaq], + queryFn: () => { + return getOneFaq(editFaq!); + }, + select(data) { + return data.data.data; + }, + enabled: !!editFaq, + }); + + const { mutate: create, isPending } = useMutation({ + mutationFn: (body: { + title: string; + title_ru: string; + text: string; + text_ru: string; + category: number; + }) => createFaq(body), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["all_faq"] }); + setOpenModal(false); + toast.success(t("Muvaffaqiyatli qo'shildi"), { position: "top-center" }); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + position: "top-center", + richColors: true, + }); + }, + }); + + const { mutate: edit, isPending: editPending } = useMutation({ + mutationFn: ({ + body, + id, + }: { + id: number; + body: { + title: string; + title_ru: string; + text: string; + text_ru: string; + category?: number; + }; + }) => updateFaq({ body, id }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["all_faq"] }); + setOpenModal(false); + toast.success(t("Tahrirlandi"), { position: "top-center" }); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + position: "top-center", + richColors: true, + }); + }, + }); + + const { mutate: deleteFaqs, isPending: deletePending } = useMutation({ + mutationFn: ({ id }: { id: number }) => deleteFaq({ id }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["all_faq"] }); + setDeleteId(null); + toast.success(t("O'chirildi"), { position: "top-center" }); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + position: "top-center", + richColors: true, + }); + }, + }); useEffect(() => { - if (category) { + if (category.length > 0 && !activeTab) { setActiveTab(String(category[0].id)); } - }, [category]); + }, [category, activeTab]); + + // Intersection Observer for lazy loading + useEffect(() => { + if (!scrollRef.current || !loaderRef.current) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, + { root: scrollRef.current, threshold: 0.1 }, + ); + + if (loaderRef.current) { + observer.observe(loaderRef.current); + } + + return () => observer.disconnect(); + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); const [deleteId, setDeleteId] = useState(null); - const [editFaq, setEditFaq] = useState(null); - const [openModal, setOpenModal] = useState(false); const form = useForm>({ resolver: zodResolver(faqForm), defaultValues: { answer: "", + answer_ru: "", categories: "", title: "", + title_ru: "", }, }); + useEffect(() => { + if (detailFaq) { + form.setValue("title", detailFaq.title_uz); + form.setValue("title_ru", detailFaq.title_ru); + form.setValue("answer", detailFaq.text_uz); + form.setValue("answer_ru", detailFaq.text_ru); + form.setValue("categories", activeTab); + } + }, [detailFaq, form]); + function onSubmit(value: z.infer) { - console.log(value); + if (editFaq === null) { + create({ + category: Number(value.categories), + text: value.answer, + text_ru: value.answer_ru, + title: value.title, + title_ru: value.title_ru, + }); + } else if (editFaq) { + edit({ + body: { + text: value.answer, + text_ru: value.answer_ru, + title: value.title, + title_ru: value.title_ru, + }, + id: editFaq, + }); + } } const handleEdit = (faq: number) => { + setOpenModal(true); setEditFaq(faq); }; const handleDelete = () => { if (deleteId) { + deleteFaqs({ id: deleteId }); } }; @@ -125,22 +286,42 @@ const Faq = () => { setOpenModal(true); }} > - {t("Yangi qo‘shish")} + {t("Yangi qo'shish")} {/* Tabs */} - - {category?.map((cat) => ( - - {cat.name} - - ))} - +
+ + {category.map((cat) => ( + + {cat.name} + + ))} + {hasNextPage && ( +
+ {isFetchingNextPage ? ( + + ) : ( + + + + )} +
+ )} +
+
{/* Tabs content */} - {category?.map((cat) => ( + {category.map((cat) => ( {faq && faq?.length > 0 ? (
@@ -192,7 +373,7 @@ const Faq = () => {
) : (

- {t("Bu bo‘limda savollar yo‘q.")} + {t("Bu bo'limda savollar yo'q")}

)}
@@ -200,10 +381,10 @@ const Faq = () => {
- + - {editFaq ? t("FAQni tahrirlash") : t("Yangi FAQ qo‘shish")} + {editFaq ? t("FAQni tahrirlash") : t("Yangi FAQ qo'shish")} @@ -216,24 +397,37 @@ const Faq = () => { - + onValueChange={field.onChange} + placeholder={t("Kategoriya tanlang")} + label={t("Kategoriyalar")} + data={category || []} + fetchNextPage={fetchNextPage} + renderOption={(cat) => ({ + key: cat.id, + value: String(cat.id), + label: cat.name, + })} + /> + + + + )} + /> + + ( + + + + @@ -241,10 +435,10 @@ const Faq = () => { /> ( - + { )} /> + ( + + + +