Compare commits
5 Commits
afe402a58a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80c2dd8930 | ||
|
|
b60c0af553 | ||
|
|
6456e3eced | ||
|
|
ebc08eff8f | ||
|
|
83bfb6296d |
@@ -1,98 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import { useCarDetail } from "@/components/lib_components/carDetailProvider";
|
||||
import Text from "@/components/lib_components/text";
|
||||
import Image from "next/image";
|
||||
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 { usePathname, useParams } from "next/navigation";
|
||||
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";
|
||||
// for gitea
|
||||
|
||||
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() {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// tools of request
|
||||
const initialCar = useCarType((state) => state.initialCar);
|
||||
const initialSubCategory = useSubCategory(
|
||||
(state) => state.initialSubCategory,
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const pathname = usePathname();
|
||||
const params = useParams();
|
||||
const lang = pathname.split("/")[1];
|
||||
const carId = params.carDeatil as string;
|
||||
const [cars, setCars] = useState<any[]>([]);
|
||||
|
||||
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) {
|
||||
const fetchProducts = async () => {
|
||||
if (!carId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch(`${baseUrl}${initialCar.id}/`, {
|
||||
headers: {
|
||||
"Accept-Language": lang,
|
||||
},
|
||||
});
|
||||
const response = await fetch(`${baseUrl}${carId}/by-subcategory/`, {
|
||||
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);
|
||||
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);
|
||||
|
||||
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();
|
||||
}, [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) {
|
||||
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 (
|
||||
<div
|
||||
dir="ltr"
|
||||
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">
|
||||
<Text txt={""} />
|
||||
</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">
|
||||
{/* Mashina rasmi */}
|
||||
<div className="max-w-[600px] w-full h-auto">
|
||||
@@ -105,22 +142,24 @@ export default function CarDetailPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Asosiy ma’lumotlar */}
|
||||
{/* Asosiy ma'lumotlar */}
|
||||
<div className="lg:space-y-6 space-y-3 w-full">
|
||||
<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">
|
||||
{firstData.price?.toLocaleString("uz-UZ")}
|
||||
<Text txt="wallet" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-lg font-semibold flex gap-2">
|
||||
<Text txt="min-time" />
|
||||
<span className="font-medium flex gap-2 text-gray-500">
|
||||
{firstData.minimal_order}
|
||||
<Text txt="time" />
|
||||
</span>
|
||||
</div>
|
||||
{!isMinimum && (
|
||||
<div className="text-lg font-semibold flex gap-2">
|
||||
<Text txt="min-time" />
|
||||
<span className="font-medium flex gap-2 text-gray-500">
|
||||
{firstData.minimal_order}
|
||||
<Text txt="time" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Izoh */}
|
||||
<div className="space-y-2 text-gray-500 text-lg">
|
||||
@@ -141,7 +180,7 @@ export default function CarDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3️⃣ Texnik xususiyatlar (faqat mavjudlari) */}
|
||||
{/* Texnik xususiyatlar */}
|
||||
<div className="w-full border-t border-gray-300 pt-6">
|
||||
<h2 className="text-xl font-semibold mb-4 text-secondary">
|
||||
Texnik xususiyatlari
|
||||
@@ -159,7 +198,7 @@ export default function CarDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4️⃣ Ijara modal */}
|
||||
{/* Ijara modal */}
|
||||
<CarRentalModal
|
||||
car={firstData}
|
||||
isOpen={modalOpen}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// app/layout.tsx
|
||||
// app/layout.tsx for
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function InnerProductcard({ data }: { data: any }) {
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/${route.lang}/${route.carType}/${data.name}`}
|
||||
href={`/${route.lang}/${route.carType}/${data.id}`}
|
||||
onClick={() => {
|
||||
setDetail(data);
|
||||
setInitialCar(carInfo);
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function SliderCard({ data }: { data: ProductTypes }) {
|
||||
alt="slider image"
|
||||
width={600}
|
||||
height={600}
|
||||
className="object-cover max-w-[600px] w-full h-[300px]"
|
||||
className="object-contain max-w-[600px] w-full h-[300px]"
|
||||
/>
|
||||
|
||||
{/* Pastki kontent */}
|
||||
|
||||
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": "Манипулятор",
|
||||
"Avtovishka": "Автовышка",
|
||||
"Avtolift": "Автолифт",
|
||||
"errorTitle": "Произошла ошибка",
|
||||
"retry": "Повторить",
|
||||
"downloadError": "Ошибка при загрузке данных",
|
||||
"noData": "Специальная техника не найдена",
|
||||
"noDataDesc": "На данный момент товары отсутствуют. Пожалуйста, попробуйте позже."
|
||||
|
||||
@@ -184,6 +184,8 @@
|
||||
"manipulyator": "Manipulyator",
|
||||
"Avtovishka": "Avtovishka",
|
||||
"Avtolift": "Avtolift",
|
||||
"errorTitle": "Xatolik yuz berdi",
|
||||
"retry": "Qayta urinish",
|
||||
"downloadError": "Ma'lumotlarni yuklashda xatolik",
|
||||
"noData": "Mahsus texnikalar topilmadi",
|
||||
"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