From e7b838e3fed08be933b1a608b919ec4a7f4a2205 Mon Sep 17 00:00:00 2001 From: "nabijonovdavronbek619@gmail.com" Date: Wed, 4 Feb 2026 10:01:25 +0500 Subject: [PATCH] connected to backend --- app/[lang]/[carType]/[carDeatil]/page.tsx | 145 ++++++----- app/[lang]/[carType]/page.tsx | 268 +++++++-------------- app/[lang]/page.tsx | 3 +- components/carPageParts/carType_head.tsx | 20 +- components/cards/innerProductcard.tsx | 177 +++++++++----- components/cards/productCard.tsx | 21 +- components/emptyState.tsx | 30 +++ components/lib_components/time.tsx | 2 +- components/lib_components/upScroll.tsx | 2 +- components/loadingProduct.tsx | 19 ++ components/pageParts/hero.tsx | 4 +- components/pageParts/map.tsx | 2 +- components/pageParts/products.tsx | 63 ----- components/pageParts/products/products.tsx | 95 ++++++++ data/url.ts | 1 + next.config.ts | 24 +- package-lock.json | 32 ++- package.json | 3 +- public/locales/ru/common.json | 9 +- public/locales/uz/common.json | 19 +- store/carType.ts | 27 +++ 21 files changed, 571 insertions(+), 395 deletions(-) create mode 100644 components/emptyState.tsx create mode 100644 components/loadingProduct.tsx delete mode 100644 components/pageParts/products.tsx create mode 100644 components/pageParts/products/products.tsx create mode 100644 data/url.ts create mode 100644 store/carType.ts diff --git a/app/[lang]/[carType]/[carDeatil]/page.tsx b/app/[lang]/[carType]/[carDeatil]/page.tsx index 7e46321..9f7861b 100644 --- a/app/[lang]/[carType]/[carDeatil]/page.tsx +++ b/app/[lang]/[carType]/[carDeatil]/page.tsx @@ -3,45 +3,85 @@ import { useCarDetail } from "@/components/lib_components/carDetailProvider"; import Text from "@/components/lib_components/text"; import Image from "next/image"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import CarRentalModal from "@/components/lib_components/carRentalModal"; import { useTranslation } from "react-i18next"; +import { useCarType } from "@/store/carType"; +import { usePathname } from "next/navigation"; +import { logoImg } from "@/assets"; + +const baseUrl = "https://api.spes-texnika.uz/api/v1/products/"; export default function CarDetailPage() { - const { detail } = useCarDetail(); const [modalOpen, setModalOpen] = useState(false); + const { t } = useTranslation(); - if (!detail) { - return ( -
- -
- ); + // tools of request + const initialCar = useCarType((state) => state.initialCar); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const pathname = usePathname(); + const lang = pathname.split("/")[1]; + const [cars, setCars] = useState([]); + + console.log("car type id: ", initialCar.id); + console.log("request URL: ", `${baseUrl}${initialCar.id}/`); + + useEffect(() => { + // Agar ID bo'lmasa, fetchni ishga tushirma + if (!initialCar.id) { + setLoading(false); + return; + } + + const fetchProducts = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch(`${baseUrl}${initialCar.id}/`, { + headers: { + "Accept-Language": lang, + }, + }); + + if (!response.ok) { + throw new Error("Server xatosi"); + } + + const result = await response.json(); + console.log("backend full response: ", result); + console.log("backend Data: ", result?.data); + + // Data array ekanligini tekshirish + if (result?.data) { + if (Array.isArray(result.data)) { + setCars(result.data); + } else { + // Agar object bo'lsa, uni array ichiga o'rab qo'yamiz + setCars([result.data]); + } + } else { + setCars([]); + } + } catch (error) { + console.log("Xatolik: ", error); + setError(error instanceof Error ? error.message : "Noma'lum xatolik"); + setCars([]); + } finally { + setLoading(false); + } + }; + + fetchProducts(); + }, [lang]); // initialCar.id ham dependency ga qo'shildi + + const firstData = cars ? cars[0] : undefined; + + if (!firstData) { + return <>Maluot topilmadi; } - const {t} = useTranslation(); - - const techSpecs: Record = { - [t("weight_kg")]: detail.weight_kg && `${detail.weight_kg.toLocaleString("uz-UZ")} kg`, - [t("maxLength_m")]: detail.maxLength_m && `${detail.maxLength_m} m`, - [t("maxHeight_m")]: detail.maxHeight_m && `${detail.maxHeight_m} m`, - [t("capacity_tons")]: detail.capacity_tons && `${detail.capacity_tons} t`, - [t("capacity_kg")]: detail.capacity_kg && `${detail.capacity_kg} kg`, - [t("fuelType")]: detail.fuelType, - [t("tankVolume_m3")]: detail.tankVolume_m3 && `${detail.tankVolume_m3} m³`, - [t("maxSpeed_kmh")]: detail.maxSpeed_kmh && `${detail.maxSpeed_kmh} km/soat`, - [t("intercooler")]: detail.intercooler, - [t("enginePower_hp")]: detail.enginePower_hp, - [t("transmission")]: detail.transmission, - [t("bom")]: detail.bom && `${detail.bom} m`, - [t("qazish")]: detail.qazish && `${detail.qazish} m`, - [t("pichoq")]: detail.pichoq && `${detail.pichoq} m`, - [t("zichlash")]: detail.zichlash && `${detail.zichlash} m`, - [t("siqish")]: detail.siqish && `${detail.siqish} bar`, - [t("havo")]: detail.havo && `${detail.havo} l`, - [t("kompressor_sig")]: detail.kompressor_sig && `${detail.kompressor_sig} l` - }; - return (
{/* 1️⃣ Mashina nomi */}
- +
{/* 2️⃣ Rasmi + asosiy narx ma’lumotlari */} @@ -57,8 +97,8 @@ export default function CarDetailPage() { {/* Mashina rasmi */}
{detail.name} - {detail.price?.toLocaleString("uz-UZ")} + {firstData.price?.toLocaleString("uz-UZ")}
- {detail.min_order_time} + {firstData.minimal_order}
-
- - - {detail.price && (detail.price * 8).toLocaleString("uz-UZ")} - - -
{/* Izoh */}
@@ -110,25 +143,25 @@ export default function CarDetailPage() { {/* 3️⃣ Texnik xususiyatlar (faqat mavjudlari) */}
-

Texnik xususiyatlari

+

+ Texnik xususiyatlari +

- {Object.entries(techSpecs) - .filter(([_, value]) => value !== undefined) - .map(([key, value]) => ( -
-

{key}:

-

{value}

-
- ))} + {firstData?.features.map((item: any) => ( +
+

{item?.name}:

+

{item?.value}

+
+ ))}
{/* 4️⃣ Ijara modal */} setModalOpen(false)} /> diff --git a/app/[lang]/[carType]/page.tsx b/app/[lang]/[carType]/page.tsx index 1559cc2..f158c63 100644 --- a/app/[lang]/[carType]/page.tsx +++ b/app/[lang]/[carType]/page.tsx @@ -1,194 +1,100 @@ "use client"; - import InnerProductcard from "@/components/cards/innerProductcard"; +import { EmptyState } from "@/components/emptyState"; +import Text from "@/components/lib_components/text"; import Title from "@/components/lib_components/title"; -import { - asfalt, - assenizator, - avtogreyderlar, - avtokran, - avtolift, - avtovishka, - betonNasoslar, - buldozerlar, - cement_trucks, - dumb_trucks, - eks_yuklagichlar, - evakuatorDemo, - forkliftlar, - gildirakli_eks, - katkalar, - manipulyator, - mini_eks, - mini_loaders, - minora, - old_yuklagichlar, - paletli_eks, - shalandaTraller, - teleskop_yuklagichlar, - trailers, - vodovoz, -} from "@/data"; -import { useParams } from "next/navigation"; +import { LoadingSkeleton } from "@/components/loadingProduct"; +import { useCarType } from "@/store/carType"; +import { usePathname } from "next/navigation"; +import { useEffect, useState } from "react"; + +const baseUrl = "https://api.spes-texnika.uz/api/v1/subcategory/"; export default function CartType() { - const router = useParams(); - const carType = router.carType; + const initialCar = useCarType((state) => state.initialCar); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const pathname = usePathname(); + const lang = pathname.split("/")[1]; + const [cars, setCars] = useState([]); + + console.log("car type id: ", initialCar.id); + console.log("request URL: ", `${baseUrl}${initialCar.id}/`); + + useEffect(() => { + // Agar ID bo'lmasa, fetchni ishga tushirma + if (!initialCar.id) { + setLoading(false); + return; + } + + const fetchProducts = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch(`${baseUrl}${initialCar.id}/`, { + headers: { + "Accept-Language": lang, + }, + }); + + if (!response.ok) { + throw new Error("Server xatosi"); + } + + const result = await response.json(); + console.log("backend full response: ", result); + console.log("backend Data: ", result?.data); + + // Data array ekanligini tekshirish + if (result?.data) { + if (Array.isArray(result.data)) { + setCars(result.data); + } else { + // Agar object bo'lsa, uni array ichiga o'rab qo'yamiz + setCars([result.data]); + } + } else { + setCars([]); + } + } catch (error) { + console.log("Xatolik: ", error); + setError(error instanceof Error ? error.message : "Noma'lum xatolik"); + setCars([]); + } finally { + setLoading(false); + } + }; + + fetchProducts(); + }, [lang]); // initialCar.id ham dependency ga qo'shildi + return (
+ {/* Error message */} + {error && ( + <div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg"> + <p className="text-red-600 text-center"> + <Text txt="downloadError" />: {error} + </p> + </div> + )} + {/* car type groups */} - <div className="mt-10 grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 max-w-[1200px] w-full mx-auto"> - {/* Avtosementavoz */} - {carType === "cement-truck" && - cement_trucks.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Samasvallar */} - {carType === "dumb-truck" && - dumb_trucks.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Traylerlar */} - {carType === "trailers" && - trailers.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Avtoliftlar */} - {carType === "Avtolift" && - avtolift.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Avtovishka */} - {carType === "Avtovishka" && - avtovishka.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Avtokran */} - {carType === "avtocranes" && - avtokran.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Minora kranlar */} - {carType === "tower-cranes" && - minora.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Minora kranlar */} - {carType === "mini-loaders" && - mini_loaders.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* old yuklagichlar */} - {carType === "front-loaders" && - old_yuklagichlar.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Teleskopik yuklagichlar */} - {carType === "tele-loaders" && - teleskop_yuklagichlar.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* forkliftlar */} - {carType === "forklift-trucks" && - forkliftlar.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Paletli ekskavatorlar */} - {carType === "crawler-excavators" && - paletli_eks.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* G'ildirakli ekskavatorlar */} - {carType === "wheel-excavators" && - gildirakli_eks.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Mini-ekskavatorlar */} - {carType === "mini-excavators" && - mini_eks.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* ekskavator yuklagichlar */} - {carType === "excavator-loaders" && - eks_yuklagichlar.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* avtogreyderlar */} - {carType === "avtograders" && - avtogreyderlar.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Buldozerlar */} - {carType === "buldozers" && - buldozerlar.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Katkalar */} - {carType === "katkas" && - katkalar.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* Evakuatorlar */} - {carType === "evakuator" && - evakuatorDemo.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* shalanda */} - {carType === "shalanda_traller" && - shalandaTraller.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* asfalt yotqizuvchi */} - {carType === "asfalt_frez" && - asfalt.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* asfalt ko'chiruvchi */} - {carType === "beton_nasos" && - betonNasoslar.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* manipulyator */} - {carType === "manipulyator" && - manipulyator.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* vodovoz */} - {carType === "vodovoz" && - vodovoz.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} - - {/* assertizator */} - {carType === "assenizator" && - assenizator.map((item) => { - return <InnerProductcard data={item} key={item.id} />; - })} + <div className="mt-10 grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 max-w-[1200px] w-full mx-auto px-4"> + {loading ? ( + <LoadingSkeleton /> + ) : cars.length > 0 ? ( + // MUHIM: Array ustidan map qilish kerak! + cars.map((car, index) => ( + <InnerProductcard key={car.id || index} data={car} /> + )) + ) : ( + <EmptyState /> + )} </div> </div> ); diff --git a/app/[lang]/page.tsx b/app/[lang]/page.tsx index 9a8b11c..4ba0728 100644 --- a/app/[lang]/page.tsx +++ b/app/[lang]/page.tsx @@ -1,4 +1,4 @@ -import Products from "@/components/pageParts/products"; +import Products from "@/components/pageParts/products/products"; import Texnika from "@/components/pageParts/texnika"; import Offer from "@/components/pageParts/offer"; import Faq from "@/components/pageParts/faq"; @@ -27,4 +27,3 @@ export default function Home() { </div> ); } - diff --git a/components/carPageParts/carType_head.tsx b/components/carPageParts/carType_head.tsx index 448261b..d50c14b 100644 --- a/components/carPageParts/carType_head.tsx +++ b/components/carPageParts/carType_head.tsx @@ -5,9 +5,11 @@ import Image from "next/image"; import { useParams } from "next/navigation"; import Text from "../lib_components/text"; import Link from "next/link"; +import { useCarType } from "@/store/carType"; export default function CarType_Header() { const params = useParams(); + const initialCar = useCarType((state) => state.initialCar); console.log(params); return ( <div @@ -47,12 +49,20 @@ export default function CarType_Header() { <Text txt="home" /> </Link> / - <Link href={`/${params.lang}/${params.carType}`} className={params.carDeatil ? "hover:text-secondary hover:cursor-pointer":"text-secondary"}> - <Text txt={`${params.carType}`} /> + <Link + href={`/${params.lang}/${params.carType}`} + className={ + params.carDeatil + ? "hover:text-secondary hover:cursor-pointer" + : "text-secondary" + } + > + {initialCar.name} </Link> - {params.carDeatil && '/'} - {params.carDeatil && <div className="text-secondary"> - <Text txt={`${params.carType}`} /></div>} + {params.carDeatil && "/"} + {params.carDeatil && ( + <div className="text-secondary">{initialCar.name}</div> + )} </div> </div> </div> diff --git a/components/cards/innerProductcard.tsx b/components/cards/innerProductcard.tsx index 641ead0..52ab41e 100644 --- a/components/cards/innerProductcard.tsx +++ b/components/cards/innerProductcard.tsx @@ -1,5 +1,4 @@ "use client"; - import { innerCardTypes } from "@/types"; import Link from "next/link"; import { useParams } from "next/navigation"; @@ -8,74 +7,132 @@ import Image from "next/image"; import Text from "../lib_components/text"; import { useCarDetail } from "../lib_components/carDetailProvider"; import { motion } from "framer-motion"; +import { useCarType } from "@/store/carType"; -export default function InnerProductcard({ data }: { data: innerCardTypes }) { +export default function InnerProductcard({ data }: { data: any }) { const route = useParams(); const { setDetail } = useCarDetail(); + const setInitialCar = useCarType((state) => state.setInitialCar); + + const carInfo = { + id: data?.id, + name: data?.name, + }; return ( - <motion.div - initial={{ opacity: 0, y: 40 }} - whileInView={{ opacity: 1, y: 0 }} - viewport={{ once: true, amount: 0.2 }} - transition={{ duration: 0.5, ease: "easeOut" }} - whileHover={{ - scale: 1.05, - boxShadow: "0px 0px 15px rgba(0,0,0,0.1)", + <Link + href={`/${route.lang}/${route.carType}/${data.name}`} + onClick={() => { + setDetail(data); + setInitialCar(carInfo); }} - whileTap={{ scale: 0.97 }} - className="h-[420px] rounded-lg overflow-hidden bg-white transition-all" + className="block h-full" > - <Link - href={`/${route.lang}/${route.carType}/${data.name}`} - onClick={() => setDetail(data)} - className="h-full flex flex-col items-center justify-between rounded-lg hover:cursor-pointer" + <motion.div + initial={{ opacity: 0, y: 40 }} + whileInView={{ opacity: 1, y: 0 }} + viewport={{ once: true, amount: 0.2 }} + transition={{ duration: 0.5, ease: "easeOut" }} + className="h-full group" > - {/* Rasm qismi */} - <div className="rounded-t-lg bg-white"> - <Image - src={data.image} - alt={data.name} - width={0} - height={200} - className="object-fill w-full max-h-[200px] h-full rounded-t-sm" - /> - </div> - - {/* Pastki qism */} - <div className="bg-[#fafafa] w-full p-2 px-4 rounded-b-lg flex flex-col items-start justify-start gap-2"> - <div className="text-xl font-semibold"> - <Text txt={data.name} /> - </div> - - <div className="flex gap-2"> - <Text txt="hour-price" /> - {data.price?.toLocaleString("uz-UZ")} - <Text txt="wallet" /> - </div> - - <div className="flex gap-2"> - <Text txt="min-time" /> - {data.min_order_time} - <Text txt="time" /> - </div> - - {/* Tugma animatsiyasi */} - <motion.button - whileHover={{ - scale: 1.05, - backgroundColor: "#ffffff", - color: "#dc2626", // hoverda text-secondary (agar sizda secondary = red) - borderColor: "#dc2626", - }} - whileTap={{ scale: 0.95 }} - transition={{ duration: 0.3 }} - className="w-full p-3 bg-secondary rounded-lg text-white border-2 border-secondary" + <div + className="h-full bg-white rounded-2xl overflow-hidden border-2 border-gray-100 + hover:border-[#f2a01c] transition-all duration-300 + shadow-sm hover:shadow-xl flex flex-col" + > + {/* Rasm container - katta va responsive */} + <div + className="relative w-full aspect-[4/3] bg-gradient-to-br from-gray-50 via-white to-gray-50 + overflow-hidden group-hover:bg-gradient-to-br group-hover:from-orange-50 + group-hover:via-white group-hover:to-orange-50 transition-all duration-500" > - <Text txt="more" /> - </motion.button> + {/* Rasm */} + <div className="absolute inset-0 p-6 flex items-center justify-center"> + <motion.div + whileHover={{ scale: 1.05 }} + transition={{ duration: 0.4 }} + className="relative w-full h-full" + > + <Image + src={data.image} + alt={data.name} + fill + sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" + className="object-contain" + priority + /> + </motion.div> + </div> + + {/* Dekorativ element */} + <div + className="absolute top-4 right-4 w-12 h-12 bg-[#f2a01c]/10 rounded-full + flex items-center justify-center opacity-0 group-hover:opacity-100 + transition-opacity duration-300" + > + <svg + className="w-6 h-6 text-[#f2a01c]" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M9 5l7 7-7 7" + /> + </svg> + </div> + </div> + + {/* Content qismi */} + <div className="flex-1 p-6 flex flex-col justify-between bg-white"> + {/* Nomi va tavsif */} + <div className="mb-4"> + <h3 + className="text-xl font-bold text-[#0c1239] mb-2 line-clamp-2 + group-hover:text-[#f2a01c] transition-colors duration-300" + > + <Text txt={data.name} /> + </h3> + + {/* Agar qo'shimcha ma'lumot bo'lsa */} + {data.description && ( + <p className="text-sm text-gray-600 line-clamp-2"> + <Text txt={data.description} /> + </p> + )} + </div> + + {/* Tugma - past qismda */} + <div className="pt-4 border-t border-gray-100"> + <motion.div + whileHover={{ x: 4 }} + transition={{ duration: 0.2 }} + className="flex items-center justify-between text-[#f2a01c] font-semibold" + > + <span> + <Text txt="more" /> + </span> + <svg + className="w-5 h-5" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M9 5l7 7-7 7" + /> + </svg> + </motion.div> + </div> + </div> </div> - </Link> - </motion.div> + </motion.div> + </Link> ); } diff --git a/components/cards/productCard.tsx b/components/cards/productCard.tsx index 1181735..8732bd1 100644 --- a/components/cards/productCard.tsx +++ b/components/cards/productCard.tsx @@ -1,15 +1,23 @@ "use client"; -import { ProductTypes } from "@/types"; import Image from "next/image"; import Link from "next/link"; import Text from "../lib_components/text"; import { useParams } from "next/navigation"; import { motion } from "framer-motion"; +import { useCarType } from "@/store/carType"; -export default function ProductCard({ data }: { data: ProductTypes }) { +export default function ProductCard({ data }: { data: any }) { const { lang } = useParams(); + const setInitialCar = useCarType((state) => state.setInitialCar); + const carData = { + name: data?.name, + id: data?.id, + }; + + console.log("data: ", data); + return ( <motion.div initial={{ opacity: 0, y: 50 }} @@ -23,14 +31,15 @@ export default function ProductCard({ data }: { data: ProductTypes }) { className="rounded-xl border-2 border-primary h-[430px]" > <Link - href={`/${lang}/${data.path}`} + href={`/${lang}/${data.name}`} + onClick={() => setInitialCar(carData)} className="h-full flex flex-col items-center justify-between rounded-lg bg-white transition-transform" > {/* Yuqori qism - rasm */} <div className="rounded-t-lg bg-white py-10 px-2 flex justify-center items-center"> <Image src={data.image} - alt={data.truck_name} + alt={data.name} width={260} height={200} className="object-contain max-h-[200px] h-full rounded-xl" @@ -40,10 +49,10 @@ export default function ProductCard({ data }: { data: ProductTypes }) { {/* Pastki qism - matn */} <div className="bg-[#fafafa] w-full py-5 rounded-b-lg flex flex-col items-center justify-center space-y-1"> <div className="font-medium text-primary text-xl text-center"> - <Text txt={data.truck_name} /> + <Text txt={data.name} /> </div> <div className="text-secondary text-md font-extrabold text-center"> - <Text txt={data.desc} /> + <Text txt="agreement" /> </div> <div className="text-center text-secondary bg-primary max-w-[200px] w-full rounded-xl text-lg py-2 hover:cursor-pointer"> <Text txt="more" /> diff --git a/components/emptyState.tsx b/components/emptyState.tsx new file mode 100644 index 0000000..a1fba2f --- /dev/null +++ b/components/emptyState.tsx @@ -0,0 +1,30 @@ +import Text from "./lib_components/text"; + +// Empty State Component +export const EmptyState = () => { + return ( + <div className="col-span-full flex flex-col items-center justify-center py-20 px-4"> + <div className="w-24 h-24 mb-6 rounded-full bg-[#f2a01c]/10 flex items-center justify-center"> + <svg + className="w-12 h-12 text-[#f2a01c]" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" + /> + </svg> + </div> + <h3 className="text-2xl font-bold text-[#0c1239] mb-2"> + <Text txt="noData" /> + </h3> + <p className="text-gray-600 text-center max-w-md"> + <Text txt="noDataDesc" /> + </p> + </div> + ); +}; diff --git a/components/lib_components/time.tsx b/components/lib_components/time.tsx index 803bf22..f64d6ce 100644 --- a/components/lib_components/time.tsx +++ b/components/lib_components/time.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' export default function Time() { const {t} = useTranslation(); return ( - <div dir='ltr' className='icon_animation2 fixed bottom-10 right-20 z-10 rounded-[50%] bg-primary p-5 text-lg text-secondary flex flex-col items-center justify-center'> + <div dir='ltr' className='icon_animation2 fixed bottom-10 right-20 z-40 rounded-[50%] bg-primary p-5 text-lg text-secondary flex flex-col items-center justify-center'> <p>{t('work_day_title')}</p> <p>24/7</p> </div> diff --git a/components/lib_components/upScroll.tsx b/components/lib_components/upScroll.tsx index 93d94f8..fc7f07d 100644 --- a/components/lib_components/upScroll.tsx +++ b/components/lib_components/upScroll.tsx @@ -37,7 +37,7 @@ export default function UpScrollIcon() { {showButton && ( <span onClick={scrollToTop} - className="fixed bottom-6 right-6 bg-secondary hover:bg-primary text-white p-3 rounded-full cursor-pointer shadow-lg transition" + className="fixed z-40 bottom-6 right-6 bg-secondary hover:bg-primary text-white p-3 rounded-full cursor-pointer shadow-lg transition" > <FaArrowUp size={20} /> </span> diff --git a/components/loadingProduct.tsx b/components/loadingProduct.tsx new file mode 100644 index 0000000..171d996 --- /dev/null +++ b/components/loadingProduct.tsx @@ -0,0 +1,19 @@ +// Loading Skeleton Component +export const LoadingSkeleton = () => { + return ( + <> + {[1, 2, 3, 4].map((item) => ( + <div + key={item} + className="bg-white rounded-lg shadow-md overflow-hidden animate-pulse" + > + <div className="w-full h-48 bg-gray-200"></div> + <div className="p-4"> + <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div> + <div className="h-4 bg-gray-200 rounded w-1/2"></div> + </div> + </div> + ))} + </> + ); +}; diff --git a/components/pageParts/hero.tsx b/components/pageParts/hero.tsx index d894c25..3cbedb0 100644 --- a/components/pageParts/hero.tsx +++ b/components/pageParts/hero.tsx @@ -48,7 +48,7 @@ export default function HeroSection() { className={`${navigationPrevEl.replace( ".", "" - )} w-10 h-10 absolute z-10 left-[10%] top-50 p-0 bg-primary text-[30px] text-center text-white flex items-center justify-center hover:bg-secondary hover:cursor-pointer transition`} + )} w-10 h-10 absolute z-10 left-[10%] top-50 p-0 bg-primary text-[30px] text-center text-white lg:flex hidden items-center justify-center hover:bg-secondary hover:cursor-pointer transition`} > <ArrowLeft /> </button> @@ -56,7 +56,7 @@ export default function HeroSection() { className={`${navigationNextEl.replace( ".", "" - )} w-10 h-10 absolute z-10 right-[10%] top-50 bg-primary text-[30px] text-center text-white flex items-center justify-center hover:bg-secondary hover:cursor-pointer transition `} + )} w-10 h-10 absolute z-10 right-[10%] top-50 bg-primary text-[30px] text-center text-white lg:flex hidden items-center justify-center hover:bg-secondary hover:cursor-pointer transition `} > <ArrowRight /> </button> diff --git a/components/pageParts/map.tsx b/components/pageParts/map.tsx index be29129..ffff603 100644 --- a/components/pageParts/map.tsx +++ b/components/pageParts/map.tsx @@ -14,7 +14,7 @@ export default function Map() { </div> {/* contact information */} - <div className="absolute flex flex-col gap-3 sm:top-20 top-5 sm:right-20 z-50 bg-white rounded-[15px] p-5 px-10 max-w-[400px] w-full text-left "> + <div className="absolute flex flex-col gap-3 sm:top-20 top-5 sm:right-20 z-30 bg-white rounded-[15px] p-5 px-10 max-w-[400px] w-full text-left "> <div className="text-left flex w-full justify-start"> <Title text="contacts" /> </div> diff --git a/components/pageParts/products.tsx b/components/pageParts/products.tsx deleted file mode 100644 index 78e61a4..0000000 --- a/components/pageParts/products.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import React, { useEffect, useState } from "react"; -import Title from "../lib_components/title"; -import Text from "../lib_components/text"; -import { Asphalt, Ekskavator, Forklift, Kran, Truck } from "@/assets"; -import type { productFilterTypes, ProductTypes } from "@/types"; -import { - allProducts, -} from "@/data"; -import ProductCard from "../cards/productCard"; - -export default function Products() { - - const [cars, setCars] = useState(allProducts); - - return ( - <div dir="ltr" className="max-w-[1200px] w-full mx-auto"> - {/* title part */} - <div className="flex flex-col mb-10"> - <div className="flex items-center justify-center w-full "> - <div className="text-secondary px-2 text-[18px] font-semibold "> - <Text txt="katalog" /> - </div> - </div> - <Title text="pricing-h2" /> - </div> - - {/* product filters */} - {/* <div className="flex flex-wrap gap-1 gap-y-4 items-center justify-center mb-10 "> - {productFilterTypesMainPage.map((item, index) => ( - <button - key={index} - onClick={() => setProductFilter(item.name)} - className={`${ - productFilter === item.name ? "bg-secondary" : "" - } flex items-center gap-2 h-[58px] hover:bg-secondary border-gray-300 hover:border-secondary border-[1px] px-7 text-2xl rounded-tr-full rounded-bl-full `} - > - <Text txt={item.name} /> - {item.image && ( - <Image - src={item.image} - alt="Truck images" - width={50} - height={50} - className="object-cover" - /> - )} - </button> - ))} - </div> */} - - {/* products */} - <div className="px-4 grid gap-5 grid-cols-1 place-content-center min-[500px]:grid-cols-2 min-lg:grid-cols-4 min-[1210px]:grid-cols-4"> - {cars.map((item: ProductTypes, index: number) => ( - <div key={index}> - <ProductCard data={item} /> - </div> - ))} - </div> - </div> - ); -} diff --git a/components/pageParts/products/products.tsx b/components/pageParts/products/products.tsx new file mode 100644 index 0000000..1f20ccb --- /dev/null +++ b/components/pageParts/products/products.tsx @@ -0,0 +1,95 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import Title from "../../lib_components/title"; +import Text from "../../lib_components/text"; +import type { ProductTypes } from "@/types"; +import { allProducts } from "@/data"; +import ProductCard from "../../cards/productCard"; +import { LoadingSkeleton } from "@/components/loadingProduct"; +import { EmptyState } from "@/components/emptyState"; +import { usePathname } from "next/navigation"; +import { baseUrl } from "@/data/url"; + +export default function Products() { + const [cars, setCars] = useState<ProductTypes[]>([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + const pathname = usePathname(); + const lang = pathname.split("/")[1]; + + useEffect(() => { + const fetchProducts = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch(baseUrl, { + headers: { + "Accept-Language": lang, + }, + }); + + if (!response.ok) { + throw new Error("Server xatosi"); + } + + const data = await response.json(); + console.log("backend Data: ", data?.data); + + if (data?.data && data.data.length > 0) { + setCars(data.data); + } else { + setCars([]); + } + } catch (error) { + console.log("Xatolik: ", error); + setError(error instanceof Error ? error.message : "Noma'lum xatolik"); + // Xatolik bo'lsa ham local data'ni ko'rsatish + setCars(allProducts); + } finally { + setLoading(false); + } + }; + + fetchProducts(); + }, [lang]); // Bo'sh array - faqat bitta marta ishlaydi + + return ( + <div dir="ltr" className="max-w-[1200px] w-full mx-auto"> + {/* title part */} + <div className="flex flex-col mb-10"> + <div className="flex items-center justify-center w-full"> + <div className="text-[#f2a01c] px-2 text-[18px] font-semibold"> + <Text txt="katalog" /> + </div> + </div> + <Title text="pricing-h2" /> + </div> + + {/* Error message */} + {error && ( + <div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg"> + <p className="text-red-600 text-center"> + <Text txt="downloadError" /> : {error} + </p> + </div> + )} + + {/* products */} + <div className="px-4 grid gap-5 grid-cols-1 place-content-center min-[500px]:grid-cols-2 min-lg:grid-cols-4 min-[1210px]:grid-cols-4"> + {loading ? ( + <LoadingSkeleton /> + ) : cars.length > 0 ? ( + cars.map((item: ProductTypes, index: number) => ( + <div key={item.id || index}> + <ProductCard data={item} /> + </div> + )) + ) : ( + <EmptyState /> + )} + </div> + </div> + ); +} diff --git a/data/url.ts b/data/url.ts new file mode 100644 index 0000000..beeba83 --- /dev/null +++ b/data/url.ts @@ -0,0 +1 @@ +export const baseUrl = "https://api.spes-texnika.uz/api/v1/category/"; \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index 0b9b31c..333fe35 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,22 @@ -// next.config.ts -const nextConfig = { - reactStrictMode: true, +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + images: { + remotePatterns: [ + { + protocol: 'http', + hostname: 'api.spes-texnika.uz', + port: '', + pathname: '/resources/media/**', + }, + { + protocol: 'https', + hostname: 'api.spes-texnika.uz', + port: '', + pathname: '/resources/media/**', + }, + ], + }, }; -export default nextConfig; +export default nextConfig; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cb5b110..4f7ba2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,8 @@ "react-icons": "^5.5.0", "react-leaflet": "^5.0.0", "react-scroll": "^1.9.3", - "swiper": "^12.0.3" + "swiper": "^12.0.3", + "zustand": "^5.0.11" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.14", @@ -3583,6 +3584,35 @@ "engines": { "node": ">=18" } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 5d03fcd..d36caf8 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "react-icons": "^5.5.0", "react-leaflet": "^5.0.0", "react-scroll": "^1.9.3", - "swiper": "^12.0.3" + "swiper": "^12.0.3", + "zustand": "^5.0.11" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.14", diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index aa783bd..890a26f 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -142,7 +142,7 @@ "bom-length": "Длина стрелы", "header-location": "Узбекистан, Ташкент", "phone": "Номер телефона", - "news-title1": "Лучший прокат оборудования для твой следующий проект", + "news-title1": "Лучший прокат оборудования для твой следующий проект", "news-title2": "Новый погрузчик - это именно то, что вам нужно!", "news-title3": "Новый комплект компрессоров специально для вас", "news-title4": "С нашими мощными кранами ваша работа станет проще", @@ -181,6 +181,9 @@ "vodovoz": "Водовоз", "assenizator": "Ассенизатор", "manipulyator": "Манипулятор", - "Avtovishka":"Автовышка", - "Avtolift":"Автолифт" + "Avtovishka": "Автовышка", + "Avtolift": "Автолифт", + "downloadError": "Ошибка при загрузке данных", + "noData": "Специальная техника не найдена", + "noDataDesc": "На данный момент товары отсутствуют. Пожалуйста, попробуйте позже." } diff --git a/public/locales/uz/common.json b/public/locales/uz/common.json index 97550f0..2c95df2 100644 --- a/public/locales/uz/common.json +++ b/public/locales/uz/common.json @@ -47,7 +47,7 @@ "buldozer": "Buldozer", "katkas": "Katkalar", "katka": "Katka", - "mini-loaders":"Kichik yuklagichlar", + "mini-loaders": "Kichik yuklagichlar", "compressors": "Kompressorlar", "testimonials": "Sharhlar", "clients'": "Mijozlar", @@ -99,7 +99,7 @@ "book": "Buyurtma berish", "ask": "Telegramdan so'rang", "description": "Tavsif", - "avtotower": "Avtominora", + "avtotower": "Avtominora", "avtocrane": "Avtokran", "rent-cement-trucks": "Sementovozlar ijarasi", "details": "Tafsilotlar", @@ -142,7 +142,7 @@ "bom-length": "Bomning uzunligi", "header-location": "O'zbekiston, Toshkent", "phone": "Telefon nomer", -"news-title1": "Keyingi loyihangiz uchun eng yaxshi texnikalar ijarasi", + "news-title1": "Keyingi loyihangiz uchun eng yaxshi texnikalar ijarasi", "news-title2": "Yangi yuklagich siz uchun eng yaxshi texnika!", "news-title3": "Yangi kompressorlar to'plami aynan siz uchun", "news-title4": "Bizning kuchli kranlarimiz bilan ishingiz yanada osonlashadi.", @@ -178,9 +178,12 @@ "asfalt_yotqizuvchi": "Asfalt yotqizuvchi mashina", "asfalt_kochiruvchi": "Asfalt ko‘chiruvchi mashina", "beton_nasos": "Beton nasos mashinalari", - "vodovoz":"Vodovoz", - "assenizator":"Assenizator", - "manipulyator":"Manipulyator", - "Avtovishka":"Avtovishka", - "Avtolift":"Avtolift" + "vodovoz": "Vodovoz", + "assenizator": "Assenizator", + "manipulyator": "Manipulyator", + "Avtovishka": "Avtovishka", + "Avtolift": "Avtolift", + "downloadError": "Ma'lumotlarni yuklashda xatolik", + "noData": "Mahsus texnikalar topilmadi", + "noDataDesc": "Hozircha mahsulotlar mavjud emas. Iltimos, keyinroq qayta urinib ko'ring." } diff --git a/store/carType.ts b/store/carType.ts new file mode 100644 index 0000000..53dd270 --- /dev/null +++ b/store/carType.ts @@ -0,0 +1,27 @@ +import { create } from "zustand"; + +// Type definition +interface CarType { + name: string; + id: number; +} + +interface CarStore { + initialCar: CarType; + setInitialCar: (data: CarType) => void; + clearCar: () => void; // Tozalash uchun +} + +export const useCarType = create<CarStore>((set) => ({ + initialCar: { + name: "", + id: 0, + }, + + setInitialCar: (data) => set({ initialCar: data }), + + clearCar: () => + set({ + initialCar: { name: "", id: 0 }, + }), +}));