detail page updated , add subcategory zustand
This commit is contained in:
@@ -1,98 +1,134 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCarDetail } from "@/components/lib_components/carDetailProvider";
|
|
||||||
import Text from "@/components/lib_components/text";
|
import Text from "@/components/lib_components/text";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import CarRentalModal from "@/components/lib_components/carRentalModal";
|
import CarRentalModal from "@/components/lib_components/carRentalModal";
|
||||||
import { useTranslation } from "react-i18next";
|
import { usePathname, useParams } from "next/navigation";
|
||||||
import { useCarType } from "@/store/carType";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { logoImg } from "@/assets";
|
import { logoImg } from "@/assets";
|
||||||
|
import { useSubCategory } from "@/store/subCategory";
|
||||||
|
import { minimumValues } from "@/data/minimimValues";
|
||||||
|
import { LoadingSkeleton } from "@/components/loadingProduct";
|
||||||
|
import { EmptyState } from "@/components/emptyState";
|
||||||
|
import { ErrorState } from "@/components/errorState";
|
||||||
|
|
||||||
const baseUrl = "https://api.spes-texnika.uz/api/v1/products/";
|
const baseUrl = "https://api.spes-texnika.uz/api/v1/products/";
|
||||||
|
|
||||||
|
type Lang = "uz" | "ru" | "en";
|
||||||
|
const validLangs: Lang[] = ["uz", "ru", "en"];
|
||||||
|
|
||||||
|
function checkCategory(categoryName: string | undefined, lang: string) {
|
||||||
|
if (!categoryName || !validLangs.includes(lang as Lang)) {
|
||||||
|
return { isMinimum: false, text: "" };
|
||||||
|
}
|
||||||
|
const validLang = lang as Lang;
|
||||||
|
const matched = minimumValues[validLang].find(
|
||||||
|
(item: string) => item === categoryName,
|
||||||
|
);
|
||||||
|
if (matched)
|
||||||
|
return { isMinimum: true, text: minimumValues.values[validLang] };
|
||||||
|
return { isMinimum: false, text: "" };
|
||||||
|
}
|
||||||
|
|
||||||
export default function CarDetailPage() {
|
export default function CarDetailPage() {
|
||||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
// tools of request
|
const initialSubCategory = useSubCategory(
|
||||||
const initialCar = useCarType((state) => state.initialCar);
|
(state) => state.initialSubCategory,
|
||||||
|
);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const params = useParams();
|
||||||
const lang = pathname.split("/")[1];
|
const lang = pathname.split("/")[1];
|
||||||
|
const carId = params.carDeatil as string;
|
||||||
const [cars, setCars] = useState<any[]>([]);
|
const [cars, setCars] = useState<any[]>([]);
|
||||||
|
|
||||||
console.log("car type id: ", initialCar.id);
|
const fetchProducts = async () => {
|
||||||
console.log("request URL: ", `${baseUrl}${initialCar.id}/`);
|
if (!carId) {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Agar ID bo'lmasa, fetchni ishga tushirma
|
|
||||||
if (!initialCar.id) {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchProducts = async () => {
|
try {
|
||||||
try {
|
setLoading(true);
|
||||||
setLoading(true);
|
setError(null);
|
||||||
setError(null);
|
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}${initialCar.id}/`, {
|
const response = await fetch(`${baseUrl}${carId}/`, {
|
||||||
headers: {
|
headers: {
|
||||||
"Accept-Language": lang,
|
"Accept-Language": lang,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Server xatosi");
|
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);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log("backend full response: ", result);
|
||||||
|
console.log("backend Data: ", result?.data);
|
||||||
|
|
||||||
|
if (result?.data) {
|
||||||
|
if (Array.isArray(result.data)) {
|
||||||
|
setCars(result.data);
|
||||||
|
} else {
|
||||||
|
setCars([result.data]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setCars([]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Xatolik: ", err);
|
||||||
|
setError(err instanceof Error ? err.message : "Noma'lum xatolik");
|
||||||
|
setCars([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
fetchProducts();
|
fetchProducts();
|
||||||
}, [lang]); // initialCar.id ham dependency ga qo'shildi
|
}, [carId, lang]);
|
||||||
|
|
||||||
const firstData = cars ? cars[0] : undefined;
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="my-10 max-w-[1200px] w-full mx-auto grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 px-2">
|
||||||
|
<LoadingSkeleton />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="my-10 max-w-[1200px] w-full mx-auto px-2">
|
||||||
|
<ErrorState message={error} onRetry={fetchProducts} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstData = cars[0];
|
||||||
|
|
||||||
if (!firstData) {
|
if (!firstData) {
|
||||||
return <>Maluot topilmadi</>;
|
return (
|
||||||
|
<div className="my-10 max-w-[1200px] w-full mx-auto px-2">
|
||||||
|
<EmptyState />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { isMinimum, text } = checkCategory(initialSubCategory.name, lang);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
className="my-10 max-w-[1200px] w-full mx-auto space-y-8 flex flex-col items-start justify-center px-2"
|
className="my-10 max-w-[1200px] w-full mx-auto space-y-8 flex flex-col items-start justify-center px-2"
|
||||||
>
|
>
|
||||||
{/* 1️⃣ Mashina nomi */}
|
{/* Mashina nomi */}
|
||||||
<div className="text-2xl font-bold w-full text-center text-secondary mb-10">
|
<div className="text-2xl font-bold w-full text-center text-secondary mb-10">
|
||||||
<Text txt={""} />
|
<Text txt={""} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2️⃣ Rasmi + asosiy narx ma’lumotlari */}
|
{/* Rasmi + asosiy narx ma'lumotlari */}
|
||||||
<div className="flex flex-col md:flex-row md:items-start items-center gap-6 justify-center w-full">
|
<div className="flex flex-col md:flex-row md:items-start items-center gap-6 justify-center w-full">
|
||||||
{/* Mashina rasmi */}
|
{/* Mashina rasmi */}
|
||||||
<div className="max-w-[600px] w-full h-auto">
|
<div className="max-w-[600px] w-full h-auto">
|
||||||
@@ -105,22 +141,24 @@ export default function CarDetailPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Asosiy ma’lumotlar */}
|
{/* Asosiy ma'lumotlar */}
|
||||||
<div className="lg:space-y-6 space-y-3 w-full">
|
<div className="lg:space-y-6 space-y-3 w-full">
|
||||||
<div className="text-lg font-semibold flex gap-2">
|
<div className="text-lg font-semibold flex gap-2">
|
||||||
<Text txt="hour-price" />
|
{isMinimum ? <p>{text}</p> : <Text txt="hour-price" />} :
|
||||||
<span className="font-medium flex gap-2 text-gray-500">
|
<span className="font-medium flex gap-2 text-gray-500">
|
||||||
{firstData.price?.toLocaleString("uz-UZ")}
|
{firstData.price?.toLocaleString("uz-UZ")}
|
||||||
<Text txt="wallet" />
|
<Text txt="wallet" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-semibold flex gap-2">
|
{!isMinimum && (
|
||||||
<Text txt="min-time" />
|
<div className="text-lg font-semibold flex gap-2">
|
||||||
<span className="font-medium flex gap-2 text-gray-500">
|
<Text txt="min-time" />
|
||||||
{firstData.minimal_order}
|
<span className="font-medium flex gap-2 text-gray-500">
|
||||||
<Text txt="time" />
|
{firstData.minimal_order}
|
||||||
</span>
|
<Text txt="time" />
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Izoh */}
|
{/* Izoh */}
|
||||||
<div className="space-y-2 text-gray-500 text-lg">
|
<div className="space-y-2 text-gray-500 text-lg">
|
||||||
@@ -141,7 +179,7 @@ export default function CarDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3️⃣ Texnik xususiyatlar (faqat mavjudlari) */}
|
{/* Texnik xususiyatlar */}
|
||||||
<div className="w-full border-t border-gray-300 pt-6">
|
<div className="w-full border-t border-gray-300 pt-6">
|
||||||
<h2 className="text-xl font-semibold mb-4 text-secondary">
|
<h2 className="text-xl font-semibold mb-4 text-secondary">
|
||||||
Texnik xususiyatlari
|
Texnik xususiyatlari
|
||||||
@@ -159,7 +197,7 @@ export default function CarDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 4️⃣ Ijara modal */}
|
{/* Ijara modal */}
|
||||||
<CarRentalModal
|
<CarRentalModal
|
||||||
car={firstData}
|
car={firstData}
|
||||||
isOpen={modalOpen}
|
isOpen={modalOpen}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function InnerProductcard({ data }: { data: any }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/${route.lang}/${route.carType}/${data.name}`}
|
href={`/${route.lang}/${route.carType}/${data.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDetail(data);
|
setDetail(data);
|
||||||
setInitialCar(carInfo);
|
setInitialCar(carInfo);
|
||||||
|
|||||||
43
components/errorState.tsx
Normal file
43
components/errorState.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import Text from "./lib_components/text";
|
||||||
|
|
||||||
|
interface ErrorStateProps {
|
||||||
|
message?: string;
|
||||||
|
onRetry?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorState = ({ message, onRetry }: ErrorStateProps) => {
|
||||||
|
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-red-100 flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-12 h-12 text-red-500"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-bold text-[#0c1239] mb-2">
|
||||||
|
<Text txt="errorTitle" />
|
||||||
|
</h3>
|
||||||
|
{message && (
|
||||||
|
<p className="text-gray-500 text-center max-w-md mb-4">{message}</p>
|
||||||
|
)}
|
||||||
|
{onRetry && (
|
||||||
|
<button
|
||||||
|
onClick={onRetry}
|
||||||
|
className="bg-secondary p-3 px-6 rounded-lg border-2 border-secondary text-white
|
||||||
|
hover:cursor-pointer hover:bg-white hover:text-secondary transition-all"
|
||||||
|
>
|
||||||
|
<Text txt="retry" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
10
data/minimimValues.ts
Normal file
10
data/minimimValues.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export const minimumValues = {
|
||||||
|
uz: ["Shalanda va Traller", "Evakuator", "Samosval"],
|
||||||
|
ru: ["Шаланда и трейлер", "Эвакуатор", "Самосвал"],
|
||||||
|
en: ["Shalanda and Trailer", "Evakuator", "Samosval"],
|
||||||
|
values: {
|
||||||
|
uz: "1 ta reys uchun narx",
|
||||||
|
ru: "Цена за 1 рейс",
|
||||||
|
en: "Price per trip",
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -184,6 +184,8 @@
|
|||||||
"manipulyator": "Манипулятор",
|
"manipulyator": "Манипулятор",
|
||||||
"Avtovishka": "Автовышка",
|
"Avtovishka": "Автовышка",
|
||||||
"Avtolift": "Автолифт",
|
"Avtolift": "Автолифт",
|
||||||
|
"errorTitle": "Произошла ошибка",
|
||||||
|
"retry": "Повторить",
|
||||||
"downloadError": "Ошибка при загрузке данных",
|
"downloadError": "Ошибка при загрузке данных",
|
||||||
"noData": "Специальная техника не найдена",
|
"noData": "Специальная техника не найдена",
|
||||||
"noDataDesc": "На данный момент товары отсутствуют. Пожалуйста, попробуйте позже."
|
"noDataDesc": "На данный момент товары отсутствуют. Пожалуйста, попробуйте позже."
|
||||||
|
|||||||
@@ -184,6 +184,8 @@
|
|||||||
"manipulyator": "Manipulyator",
|
"manipulyator": "Manipulyator",
|
||||||
"Avtovishka": "Avtovishka",
|
"Avtovishka": "Avtovishka",
|
||||||
"Avtolift": "Avtolift",
|
"Avtolift": "Avtolift",
|
||||||
|
"errorTitle": "Xatolik yuz berdi",
|
||||||
|
"retry": "Qayta urinish",
|
||||||
"downloadError": "Ma'lumotlarni yuklashda xatolik",
|
"downloadError": "Ma'lumotlarni yuklashda xatolik",
|
||||||
"noData": "Mahsus texnikalar topilmadi",
|
"noData": "Mahsus texnikalar topilmadi",
|
||||||
"noDataDesc": "Hozircha mahsulotlar mavjud emas. Iltimos, keyinroq qayta urinib ko'ring."
|
"noDataDesc": "Hozircha mahsulotlar mavjud emas. Iltimos, keyinroq qayta urinib ko'ring."
|
||||||
|
|||||||
13
vercel.json
Normal file
13
vercel.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"framework": "nextjs",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"source": "/(.*)",
|
||||||
|
"headers": [
|
||||||
|
{ "key": "X-Content-Type-Options", "value": "nosniff" },
|
||||||
|
{ "key": "X-Frame-Options", "value": "DENY" },
|
||||||
|
{ "key": "X-XSS-Protection", "value": "1; mode=block" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user