diff --git a/app/[locale]/products/[slug]/page.tsx b/app/[locale]/products/[slug]/page.tsx index d0335b4..22d52b6 100644 --- a/app/[locale]/products/[slug]/page.tsx +++ b/app/[locale]/products/[slug]/page.tsx @@ -1,40 +1,87 @@ "use client"; -import { DATA } from "@/lib/demoData"; import { Features, RightSide, SliderComp } from "@/components/pages/products"; +import { useProductPageInfo } from "@/store/useProduct"; +import { useQuery } from "@tanstack/react-query"; +import httpClient from "@/request/api"; +import { endPoints } from "@/request/links"; +import { AlertCircle } from "lucide-react"; +import { LoadingSkeleton } from "@/components/pages/products/slug/loading"; +import { EmptyState } from "@/components/pages/products/slug/empty"; +import { useEffect } from "react"; + +// Types +interface ProductImage { + id: number; + product: number; + image: string; + is_main: boolean; + order: number; +} + +interface ProductDetail { + id: number; + name: string; + articular: string; + status: string; + description: string; + size: number; + price: string; + features: string[]; + images: ProductImage[]; +} export default function SlugPage() { - const statusColor = - DATA[0].status === "full" - ? "text-green-500" - : DATA[0].status === "empty" - ? "text-red-600" - : "text-yellow-800"; + const productZustand = useProductPageInfo((state) => state.product); - const statusText = - DATA[0].status === "full" - ? "Sotuvda mavjud" - : DATA[0].status === "empty" - ? "Sotuvda qolmagan" - : "Buyurtma asosida"; + const { data: product, isLoading } = useQuery({ + queryKey: ["product", productZustand.id], + queryFn: () => httpClient(endPoints.product.detail(productZustand.id)), + select: (data) => data?.data?.data as ProductDetail, + enabled: !!productZustand.id, + }); + + useEffect(()=>console.log("product detail: ",product)) + + // Loading State + if (isLoading) { + return ; + } + + // Empty State + if (!product) { + return ; + } + + // Extract images + const productImages = product.images?.map((img) => img.image) || []; + const mainImage = product.images?.find((img) => img.is_main)?.image || productImages[0] || ""; + const features = product.features.map((item:any)=>item.name) return ( -
-
-
- +
+
+ {/* Main Product Section */} +
+ {/* Left - Image Slider */} + + {/* Right - Product Info */}
- + + {/* Features Section */} + {product.features && product.features.length > 0 && ( + + )}
); diff --git a/components/pages/home/blog/catalog.tsx b/components/pages/home/blog/catalog.tsx index dfc3901..7f01503 100644 --- a/components/pages/home/blog/catalog.tsx +++ b/components/pages/home/blog/catalog.tsx @@ -16,9 +16,6 @@ export default function Catalog() { queryFn: () => httpClient(endPoints.category.all), select: (data): CategoryType[] => data?.data?.results, }); - useEffect(() => { - console.log("product catalog data: ", data); - }, [data]); if (isLoading) { return ( diff --git a/components/pages/products/filter/filter.tsx b/components/pages/products/filter/filter.tsx index 45a1d17..0cae6ce 100644 --- a/components/pages/products/filter/filter.tsx +++ b/components/pages/products/filter/filter.tsx @@ -55,7 +55,6 @@ export default function Filter() { useEffect(() => { catalog && setCatalogData(catalog); size && setSizeData(size); - console.log("catalog: ", catalog, "size: ", size); }, [size, catalog]); // Bo'lim uchun ko'rsatiladigan itemlar @@ -67,7 +66,6 @@ export default function Filter() { const visibleSectionNumber = numberExpanded ? sizeData : sizeData.slice(0, 10); - console.log("filter: ", filter); return (
diff --git a/components/pages/products/product/mianProduct.tsx b/components/pages/products/product/mianProduct.tsx index c3b2f9e..ffd7e5b 100644 --- a/components/pages/products/product/mianProduct.tsx +++ b/components/pages/products/product/mianProduct.tsx @@ -5,12 +5,14 @@ import { useQuery } from "@tanstack/react-query"; import ProductCard from "./productCard"; import { useCategory } from "@/store/useCategory"; import { useFilter } from "@/lib/filter-zustand"; -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; +import { useProductPageInfo } from "@/store/useProduct"; export default function MainProduct() { const category = useCategory((state) => state.category); const filter = useFilter((state) => state.filter); - const getFiltersByType = useFilter((state)=>state.getFiltersByType) + const getFiltersByType = useFilter((state) => state.getFiltersByType); + const setProduct = useProductPageInfo((state) => state.setProducts); // Query params yaratish const queryParams = useMemo(() => { @@ -38,11 +40,19 @@ export default function MainProduct() { }, [category.id, category.have_sub_category, queryParams]); const { data, isLoading, error } = useQuery({ - queryKey: ["products", category.id , queryParams], + queryKey: ["products", category.id, queryParams], queryFn: () => httpClient(requestLink), - select: (data) => data?.data?.data?.results, + select: (data) => { + const product = data?.data?.data?.results; + return product.map((item: any) => ({ + id: item.id, + name: item.name, + image: item.images[0].image, + })); + }, }); + if (isLoading) { return (
@@ -74,6 +84,7 @@ export default function MainProduct() { {data.map((item: any) => ( setProduct(item)} title={item.name} image={item.image} slug={item.slug} diff --git a/components/pages/products/product/productCard.tsx b/components/pages/products/product/productCard.tsx index 57171f7..cccb60c 100644 --- a/components/pages/products/product/productCard.tsx +++ b/components/pages/products/product/productCard.tsx @@ -1,4 +1,3 @@ - import { useLocale } from "next-intl"; import Image from "next/image"; import Link from "next/link"; @@ -7,17 +6,19 @@ interface ProductCardProps { title: string; image: string; slug: string; + getProduct: () => void; } export default function ProductCard({ title, image, slug, + getProduct, }: ProductCardProps) { const locale = useLocale(); return ( - +
{/* Image Container */}
diff --git a/components/pages/products/slug/empty.tsx b/components/pages/products/slug/empty.tsx new file mode 100644 index 0000000..95b5a32 --- /dev/null +++ b/components/pages/products/slug/empty.tsx @@ -0,0 +1,36 @@ +// Empty State Component +export function EmptyState() { + return ( +
+
+
+ + + +
+

+ Mahsulot topilmadi +

+

+ Siz qidirayotgan mahsulot mavjud emas yoki o'chirilgan +

+ + Mahsulotlarga qaytish + +
+
+ ); +} diff --git a/components/pages/products/slug/features.tsx b/components/pages/products/slug/features.tsx index de8c8ea..9e1d14f 100644 --- a/components/pages/products/slug/features.tsx +++ b/components/pages/products/slug/features.tsx @@ -1,27 +1,41 @@ export function Features({ features }: { features: string[] }) { + if (!features || features.length === 0) { + return null; + } + return ( - - - - - - - - {features.map((feature, index) => ( - - - - ))} - -
- Feature -
- {feature} -
+
+

+ Xususiyatlar +

+
+ + + + + + + + {features.map((feature, index) => ( + + + + ))} + +
+ Xususiyat +
+
+ + {feature} +
+
+
+
); -} +} \ No newline at end of file diff --git a/components/pages/products/slug/loading.tsx b/components/pages/products/slug/loading.tsx new file mode 100644 index 0000000..9397fc9 --- /dev/null +++ b/components/pages/products/slug/loading.tsx @@ -0,0 +1,61 @@ +// Loading Skeleton Component +export function LoadingSkeleton() { + return ( +
+
+
+ {/* Image Skeleton */} +
+ + {/* Info Skeleton */} +
+ {/* Title */} +
+ + {/* Articular */} +
+ + {/* Status */} +
+ + {/* Description */} +
+
+
+
+
+ + {/* Price */} +
+ + {/* Buttons */} +
+
+
+ + {/* Social */} +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( +
+ ))} +
+
+
+ + {/* Features Skeleton */} +
+
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+
+
+ ); +} diff --git a/components/pages/products/slug/rightSide.tsx b/components/pages/products/slug/rightSide.tsx index 7039a68..ffcac6c 100644 --- a/components/pages/products/slug/rightSide.tsx +++ b/components/pages/products/slug/rightSide.tsx @@ -1,116 +1,106 @@ import { usePriceModalStore } from "@/store/useProceModalStore"; -import { Facebook } from "lucide-react"; +import { Facebook, Share2 } from "lucide-react"; const socialLinks = [ { name: "telegram", icon: "✈️", color: "#0088cc" }, - { name: "facebook", icon: , color: "#1877F2" }, - { name: "odnoklassniki", icon: "ok", color: "#ED7100" }, - { name: "vkontakte", icon: "VK", color: "#0077FF" }, + { name: "facebook", icon: , color: "#1877F2" }, + { name: "whatsapp", icon: "💬", color: "#25D366" }, { name: "twitter", icon: "𝕏", color: "#1DA1F2" }, - { name: "whatsapp", icon: "W", color: "#25D366" }, ]; interface RightSideProps { id: number; title: string; - name: string; + articular: string; + status: string; description: string; - statusText: string; - statusColor: string; + price: string; image: string; } export function RightSide({ title, - name, + articular, + status, description, - statusColor, - statusText, + price, id, image, }: RightSideProps) { const openModal = usePriceModalStore((state) => state.openModal); + const handleGetPrice = () => { openModal({ - id: id, + id, name: title, - image: image, - inStock: true, + image, + inStock: status === "Sotuvda mavjud", }); }; + + // Status color logic + const isInStock = status === "Sotuvda mavjud"; + const statusColor = isInStock + ? "bg-green-600/20 text-green-400 border border-green-600/30" + : "bg-red-600/20 text-red-400 border border-red-600/30"; + return ( -
+
{/* Title */} -

+

{title}

{/* Article ID */} -
-

- Artikul: - {name} -

+
+ Artikul: + {articular}
{/* Status Badge */} -
- - {statusText} +
+ + {status}
- {/* description */} -
-

+ {/* Description */} +

+

{description}

{/* Price Section */} -
-

- 17.00$ -

- - {/* Action Buttons */} -
- {/* */} - - {/* */} +
+ {/* Price */} +
+

Narx:

+

+ ${price} +

- {/* Social Share Icons */} -
-
+ {/* Action Button */} + + + {/* Social Share */} +
+
+ + Ulashish: +
+
); -} +} \ No newline at end of file diff --git a/components/pages/products/slug/slider.tsx b/components/pages/products/slug/slider.tsx index 4cfb19b..349621c 100644 --- a/components/pages/products/slug/slider.tsx +++ b/components/pages/products/slug/slider.tsx @@ -1,60 +1,133 @@ "use client"; import { Swiper, SwiperSlide } from "swiper/react"; -import { Navigation } from "swiper/modules"; +import { Navigation, Pagination, Thumbs } from "swiper/modules"; +import { useState } from "react"; +import type { Swiper as SwiperType } from "swiper"; import "swiper/css"; import "swiper/css/navigation"; -import { DATA } from "@/lib/demoData"; +import "swiper/css/pagination"; +import "swiper/css/thumbs"; +import Image from "next/image"; -// The custom CSS selectors for navigation const navigationPrevEl = ".custom-swiper-prev"; const navigationNextEl = ".custom-swiper-next"; export function SliderComp({ imgs }: { imgs: string[] }) { + const [thumbsSwiper, setThumbsSwiper] = useState(null); + + // Agar rasm bo'lmasa + if (!imgs || imgs.length === 0) { + return ( +
+
+ + + +

Rasm mavjud emas

+
+
+ ); + } + return ( -
-
+
+ {/* Main Slider */} +
1} + className="w-[90%] h-96 md:h-96 rounded-lg overflow-hidden shadow-xl" > {imgs.map((image, index) => ( - {`${DATA[0].title} +
+ {`Product +
))}
- {/* Custom buttons */} - - + + {/* Navigation Buttons */} + {imgs.length > 1 && ( + <> + + + + )}
+ + {/* Thumbnail Slider */} + {imgs.length > 1 && ( + + {imgs.map((image, index) => ( + +
+ {`Thumbnail +
+
+ ))} +
+ )}
); -} +} \ No newline at end of file diff --git a/components/priceContact.tsx b/components/priceContact.tsx index 56906b9..cbd2f76 100644 --- a/components/priceContact.tsx +++ b/components/priceContact.tsx @@ -1,10 +1,19 @@ "use client"; - import { useTranslations } from "next-intl"; import Image from "next/image"; import { useState, useEffect } from "react"; import { X } from "lucide-react"; import { usePriceModalStore } from "@/store/useProceModalStore"; +import { useMutation } from "@tanstack/react-query"; +import httpClient from "@/request/api"; +import { endPoints } from "@/request/links"; +import { toast } from "react-toastify"; + +interface FormType { + name: string; + product: number; + phone: number; // ✅ String bo'lishi kerak +} export function PriceModal() { const t = useTranslations("priceModal"); @@ -13,31 +22,41 @@ export function PriceModal() { const [formData, setFormData] = useState({ name: "", phone: "+998 ", - captcha: "", }); const [errors, setErrors] = useState({ name: "", phone: "", - captcha: "", }); - const [captchaCode, setCaptchaCode] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); - - // Generate random captcha - useEffect(() => { - if (isOpen) { - const code = Math.random().toString(36).substring(2, 8).toUpperCase(); - setCaptchaCode(code); - } - }, [isOpen]); + const formRequest = useMutation({ + mutationFn: (data: FormType) => + httpClient.post(endPoints.post.productContact, data), + onSuccess: () => { + setFormData({ + name: "", + phone: "+998 ", + }); + toast.success(t("success") || "Muvaffaqiyatli yuborildi!"); + closeModal(); + }, + onError: (error) => { + console.error("Error:", error); + toast.error(t("error") || "Xatolik yuz berdi"); + }, + }); // Reset form when modal closes useEffect(() => { if (!isOpen) { - setFormData({ name: "", phone: "+998 ", captcha: "" }); - setErrors({ name: "", phone: "", captcha: "" }); + setFormData({ + name: "", + phone: "+998 ", + }); + setErrors({ + name: "", + phone: "", + }); } }, [isOpen]); @@ -58,15 +77,14 @@ export function PriceModal() { if (!numbers.startsWith("998")) { return "+998 "; } - + let formatted = "+998 "; const rest = numbers.slice(3); - if (rest.length > 0) formatted += rest.slice(0, 2); if (rest.length > 2) formatted += " " + rest.slice(2, 5); if (rest.length > 5) formatted += " " + rest.slice(5, 7); if (rest.length > 7) formatted += " " + rest.slice(7, 9); - + return formatted; }; @@ -90,51 +108,45 @@ export function PriceModal() { const newErrors = { name: "", phone: "", - captcha: "", }; + // Name validation if (!formData.name.trim()) { - newErrors.name = t("validation.nameRequired"); + newErrors.name = t("validation.nameRequired") || "Ism kiritilishi shart"; } + // Phone validation const phoneNumbers = formData.phone.replace(/\D/g, ""); if (phoneNumbers.length !== 12) { - newErrors.phone = t("validation.phoneRequired"); + newErrors.phone = + t("validation.phoneRequired") || "To'liq telefon raqam kiriting"; } else if (!phoneNumbers.startsWith("998")) { - newErrors.phone = t("validation.phoneInvalid"); - } - - if (!formData.captcha.trim()) { - newErrors.captcha = t("validation.captchaRequired"); - } else if (formData.captcha.toUpperCase() !== captchaCode) { - newErrors.captcha = t("validation.captchaRequired"); + newErrors.phone = + t("validation.phoneInvalid") || "Noto'g'ri telefon raqam"; } setErrors(newErrors); - return !newErrors.name && !newErrors.phone && !newErrors.captcha; + return !newErrors.name && !newErrors.phone; }; - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validateForm()) { return; } - setIsSubmitting(true); + // Telefon raqamni tozalash (faqat raqamlar) + const cleanPhone = formData.phone.replace(/\D/g, ""); - try { - // API call logikangiz - await new Promise((resolve) => setTimeout(resolve, 1500)); + const sendedData: FormType = { + name: formData.name, + phone: Number(cleanPhone), // ✅ String sifatida yuborish + product: product?.id || 0, + }; - // Success - alert(t("success")); - closeModal(); - } catch (error) { - alert(t("error")); - } finally { - setIsSubmitting(false); - } + console.log("Sended data:", sendedData); + formRequest.mutate(sendedData); }; if (!isOpen || !product) return null; @@ -143,49 +155,58 @@ export function PriceModal() {
{/* Backdrop */}
{/* Modal */} -
+
{/* Close button */} {/* Content */} -
-

{t("title")}

+
+

+ {t("title") || "Narx so'rash"} +

{/* Product Info */} -
-
+
+
{product.name}
-
-

{product.name}

- - {t("product.inStock")} +
+

+ {product.name} +

+ + {product.inStock + ? t("product.inStock") || "Sotuvda mavjud" + : t("product.outOfStock") || "Sotuvda yo'q"}
{/* Form */} -
+ {/* Name */}
-
{/* Phone */}
-
- {/* Captcha */} - {/*
- -
-
- - {captchaCode} - -
- - - - -
-
- -
- {errors.captcha && ( -

{errors.captcha}

- )} -
*/} - {/* Submit Button */}