From a2a983345a4252c1e5b0713f362c864eef9b9f4d Mon Sep 17 00:00:00 2001 From: Samandar Turgunboyev Date: Thu, 5 Feb 2026 19:02:53 +0500 Subject: [PATCH] tuman added --- src/features/district/ui/district.tsx | 301 ++++++++++++++++---------- src/shared/hooks/useDebounce.ts | 19 ++ vite.config.ts | 2 +- 3 files changed, 204 insertions(+), 118 deletions(-) create mode 100644 src/shared/hooks/useDebounce.ts diff --git a/src/features/district/ui/district.tsx b/src/features/district/ui/district.tsx index 6f9104e..470e0d9 100644 --- a/src/features/district/ui/district.tsx +++ b/src/features/district/ui/district.tsx @@ -1,6 +1,7 @@ "use client"; import type { MyDiscrictData } from "@/features/district/lib/data"; +import { useDebounce } from "@/shared/hooks/useDebounce"; import AddedButton from "@/shared/ui/added-button"; import { Button } from "@/shared/ui/button"; import { @@ -24,9 +25,9 @@ import { Skeleton } from "@/shared/ui/skeleton"; import { DashboardLayout } from "@/widgets/dashboard-layout/ui/DashboardLayout"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { AxiosError } from "axios"; +import axios, { AxiosError } from "axios"; import { Loader2 } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import z from "zod"; @@ -35,9 +36,13 @@ import { columnsDistrict } from "../lib/column"; import { DataTableDistruct } from "../lib/data-table"; import { districtForm } from "../lib/form"; -export default function District() { - const queryClinent = useQueryClient(); +interface NominatimResult { + place_id: number; + display_name: string; +} +export default function District() { + const queryClient = useQueryClient(); const form = useForm>({ resolver: zodResolver(districtForm), defaultValues: { name: "" }, @@ -49,62 +54,126 @@ export default function District() { const [selectedDistrict, setSelectedDistrict] = useState(null); - const handleEdit = (district: MyDiscrictData) => { - form.reset({ name: district.name }); - setIsDialogOpen(true); - setEditingDistrict(district.id); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + + // Debounced query using react-hook-form's value + const debouncedQuery = useDebounce(form.watch("name"), 500); + + /** 🔍 Nominatim API fetch */ + useEffect(() => { + if (!debouncedQuery || debouncedQuery.length < 2) { + setSearchResults([]); + return; + } + + let active = true; + + const fetchDistricts = async () => { + setIsSearching(true); + try { + const res = await axios.get( + `https://nominatim.openstreetmap.org/search`, + { + params: { + q: debouncedQuery, + format: "json", + polygon_geojson: 1, + countrycodes: "uz", + limit: 20, + addressdetails: 1, + }, + headers: { + "User-Agent": "DistrictAppUZ/1.0 (your.real.email@domain.uz)", + Referer: window.location.origin || "https://your-app.uz", + }, + }, + ); + + if (!active) return; + + // Filter ni yumshatdik: ko'proq natija chiqishi uchun + const filtered = res.data.filter( + (item: { class: string; type: string; display_name: string }) => + item.class === "place" || + item.class === "boundary" || + item.type === "administrative" || + item.type === "district" || + item.type === "county" || + item.display_name.toLowerCase().includes("tumani") || + item.display_name.toLowerCase().includes("tuman") || + item.display_name.toLowerCase().includes("district"), + ); + + setSearchResults(filtered); + } catch (err) { + console.error("Nominatim xatosi:", err); + toast.error("Joylashuvni qidirishda xato"); + } finally { + if (active) setIsSearching(false); + } + }; + + fetchDistricts(); + + return () => { + active = false; + }; + }, [debouncedQuery]); + + /** 🔹 Select suggestion */ + const handleSelect = (item: NominatimResult) => { + const nameToSave = item.display_name + .split(",") + .slice(0, 2) + .join(",") + .trim(); + form.setValue("name", nameToSave); + setSearchResults([]); }; + /** 🔹 Edit */ + const handleEdit = (district: MyDiscrictData) => { + form.reset({ name: district.name }); + setEditingDistrict(district.id); + setIsDialogOpen(true); + }; + + /** 🔹 Delete */ const handleDelete = () => { if (!selectedDistrict) return; deleteDis(selectedDistrict.id); }; + /** 🔹 Columns for DataTable */ const columns = columnsDistrict({ handleEdit, - onDeleteClick: (MyDiscrictData) => { - setSelectedDistrict(MyDiscrictData); + onDeleteClick: (d) => { + setSelectedDistrict(d); setDeleteDialog(true); }, }); - const { data, isError, isLoading } = useQuery({ + /** 🔹 Queries */ + const { data, isLoading, isError } = useQuery({ queryKey: ["my_disctrict"], queryFn: () => district_api.getDiscrict(), }); + /** 🔹 Mutations */ const { mutate: added, isPending: addedPending } = useMutation({ mutationFn: (body: { name: string }) => district_api.added(body), onSuccess: () => { toast.success("Yangi tuman qo'shildi"); - queryClinent.refetchQueries({ queryKey: ["my_disctrict"] }); + queryClient.refetchQueries({ queryKey: ["my_disctrict"] }); setIsDialogOpen(false); + form.reset({ name: "" }); }, onError: (error: AxiosError) => { - const data = error.response?.data as { message?: string }; - const errorData = error.response?.data as { - messages?: { - token_class: string; - token_type: string; - message: string; - }[]; - }; - const errorName = error.response?.data as { - data?: { - name: string[]; - }; - }; - - const message = - Array.isArray(errorName.data?.name) && errorName.data.name.length - ? errorName.data.name[0] - : data?.message || - (Array.isArray(errorData?.messages) && errorData.messages.length - ? errorData.messages[0].message - : undefined) || - "Xatolik yuz berdi"; - - toast.error(message); + toast.error( + (error.response?.data as { message: string })?.message || + "Xatolik yuz berdi", + ); }, }); @@ -113,35 +182,16 @@ export default function District() { district_api.edit({ body, id }), onSuccess: () => { toast.success("Tuman yangilandi"); - queryClinent.refetchQueries({ queryKey: ["my_disctrict"] }); + queryClient.refetchQueries({ queryKey: ["my_disctrict"] }); setIsDialogOpen(false); setEditingDistrict(null); + form.reset({ name: "" }); }, onError: (error: AxiosError) => { - const data = error.response?.data as { message?: string }; - const errorData = error.response?.data as { - messages?: { - token_class: string; - token_type: string; - message: string; - }[]; - }; - const errorName = error.response?.data as { - data?: { - name: string[]; - }; - }; - - const message = - Array.isArray(errorName.data?.name) && errorName.data.name.length - ? errorName.data.name[0] - : data?.message || - (Array.isArray(errorData?.messages) && errorData.messages.length - ? errorData.messages[0].message - : undefined) || - "Xatolik yuz berdi"; - - toast.error(message); + toast.error( + (error.response?.data as { message: string })?.message || + "Xatolik yuz berdi", + ); }, }); @@ -149,49 +199,31 @@ export default function District() { mutationFn: (id: number) => district_api.deleteDistrict(id), onSuccess: () => { toast.success("Tuman o'chirildi"); - queryClinent.refetchQueries({ queryKey: ["my_disctrict"] }); + queryClient.refetchQueries({ queryKey: ["my_disctrict"] }); setDeleteDialog(false); setSelectedDistrict(null); }, onError: (error: AxiosError) => { - const data = error.response?.data as { message?: string }; - const errorData = error.response?.data as { - messages?: { - token_class: string; - token_type: string; - message: string; - }[]; - }; - const errorName = error.response?.data as { - data?: { - name: string[]; - }; - }; - - const message = - Array.isArray(errorName.data?.name) && errorName.data.name.length - ? errorName.data.name[0] - : data?.message || - (Array.isArray(errorData?.messages) && errorData.messages.length - ? errorData.messages[0].message - : undefined) || - "Xatolik yuz berdi"; - - toast.error(message); + toast.error( + (error.response?.data as { message: string })?.message || + "Xatolik yuz berdi", + ); }, }); - async function onSubmit(data: z.infer) { + /** 🔹 Submit */ + const onSubmit = (values: z.infer) => { if (editingDistrict) { - edit({ body: { name: data.name }, id: editingDistrict }); + edit({ id: editingDistrict, body: { name: values.name } }); } else { - added({ name: data.name }); + added({ name: values.name }); } - } + }; return ( <> + {/* ADD/EDIT DIALOG */}
@@ -214,20 +246,52 @@ export default function District() { control={form.control} name="name" render={({ field }) => ( - + + + {/* SEARCH RESULTS */} + {searchResults.length > 0 && ( +
    + {isSearching ? ( +
  • + Qidirilmoqda... +
  • + ) : ( + searchResults.map((item) => ( +
  • handleSelect(item)} + > + {item.display_name} +
  • + )) + )} +
+ )} + + {!isSearching && + searchResults.length === 0 && + debouncedQuery.length >= 2 && ( +

+ Hech narsa topilmadi. Boshqa nom sinab ko‘ring + (masalan: Chilonzor tumani) +

+ )} +
)} /> -
+ +
-
+ {/* DISTRICT LIST */} +

Tumanlar ro‘yxati

- - {/* Loading state */} {isLoading ? (
{[1, 2, 3].map((i) => ( @@ -270,7 +333,7 @@ export default function District() {

Tumanlar yuklanmadi. Qayta urinib ko‘ring.

- ) : data ? ( + ) : data && data.data.data.length > 0 ? (
@@ -278,27 +341,31 @@ export default function District() {

Tumanlar mavjud emas

)}
- - - - - O‘chirish - -

- Siz haqiqatdan ham {selectedDistrict?.name} tumanni - o‘chirmoqchimisiz? -

- - - - -
-
+ {/* DELETE DIALOG */} + + + + O‘chirish + +

+ Siz haqiqatdan ham {selectedDistrict?.name}{" "} + tumanni o‘chirmoqchimisiz? +

+ + + + +
+
+ ); } diff --git a/src/shared/hooks/useDebounce.ts b/src/shared/hooks/useDebounce.ts new file mode 100644 index 0000000..0aedd21 --- /dev/null +++ b/src/shared/hooks/useDebounce.ts @@ -0,0 +1,19 @@ +// hooks/useDebounce.ts +import { useEffect, useState } from "react"; + +export function useDebounce(value: T, delay: number = 500): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + // har yangi value kelganda oldingi timeoutni tozalaymiz + return () => { + clearTimeout(handler); + }; + }, [value, delay]); // value yoki delay o'zgarsa → qayta ishlaydi + + return debouncedValue; +} diff --git a/vite.config.ts b/vite.config.ts index e46aacf..03eac41 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ port: 5174, host: true, // barcha hostlarga ruxsat allowedHosts: [ - "spin-ronald-officers-reasonably.trycloudflare.com", // ngrok host qo'shildi + "removed-treasure-discovery-loads.trycloudflare.com", // ngrok host qo'shildi ], }, });