pdp change

main
Amir Hossein Moghiseh 2025-03-05 18:26:11 +03:30
parent a594730535
commit 27a0733667
11 changed files with 556 additions and 108 deletions

View File

@ -25,7 +25,8 @@
"react-intersection-observer": "^9.15.1",
"react-toastify": "^11.0.3",
"swiper": "^11.2.2",
"tailwind-merge": "^3.0.2"
"tailwind-merge": "^3.0.2",
"yet-another-react-lightbox": "^3.21.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3",

View File

@ -53,6 +53,9 @@ dependencies:
tailwind-merge:
specifier: ^3.0.2
version: 3.0.2
yet-another-react-lightbox:
specifier: ^3.21.7
version: 3.21.7(react-dom@19.0.0)(react@19.0.0)
devDependencies:
'@eslint/eslintrc':
@ -3500,6 +3503,17 @@ packages:
hasBin: true
dev: true
/yet-another-react-lightbox@3.21.7(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-dcdokNuCIl92f0Vl+uzeKULnQhztIGpoZFUMvtVNUPmtwsQWpqWufeieDPeg9JtFyVCcbj4vYw3V00DS0QNoWA==}
engines: {node: '>=14'}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
dev: false
/yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}

View File

@ -1,11 +1,11 @@
import { ChevronRight, Home } from "lucide-react";
import { ArrowRight, Award, Boxes, CheckCircle2, ChevronRight, Link as LinkIcon, MessageSquare, ShieldCheck, Ship } from "lucide-react";
import { getLocale, getMessages } from "next-intl/server";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import ColorAvg from "src/components/ColorAvg";
import ProductDescription from "src/components/Product/ProductDescription";
import ProductGallery from "src/components/Product/ProductGallery";
import ProductInfo from "src/components/Product/ProductInfo";
import BackgroundColor from "src/components/BackgroundColor";
import { ProductGallery } from "src/components/Gallery";
import ProductProperties from "src/components/Product/ProductProperties";
import ProductRelated from "src/components/Product/ProductRelated";
import graphql from "src/utils/graphql";
@ -47,6 +47,16 @@ query Products($locale: I18NLocaleCode, $slug: String!) {
slug
discount
showPrice
properties {
id
key
value
}
subcategories {
title
documentId
slug
}
seo {
id
metaTitle
@ -74,16 +84,7 @@ query Products($locale: I18NLocaleCode, $slug: String!) {
}
}
}
properties {
id
key
value
}
subcategories {
title
documentId
slug
}
}
}
@ -131,6 +132,8 @@ query Products($locale:I18NLocaleCode,$slug:String!) {
}
}
`
export async function generateMetadata({ params }) {
const { locale, slug } = await params;
@ -203,6 +206,23 @@ const getProduct = async (slug) => {
});
return products[0];
};
const features = [
{
icon: Award,
title: "Certified Product",
description: "Meets international quality standards"
},
{
icon: Ship,
title: "Bulk Shipping",
description: "Efficient worldwide delivery solutions"
},
{
icon: ShieldCheck,
title: "Quality Assured",
description: "Rigorous testing at every stage"
}
];
export default async function ProductPage({ params }) {
const { slug } = await params
@ -215,65 +235,170 @@ export default async function ProductPage({ params }) {
const t = await getMessages({ locale });
return (
<div className="max-w-screen-xl mx-auto px-4 py-8 ">
{/* <ColorAvg image={product.brand.image.url}/> */}
<div className="flex flex-col lg:flex-row gap-8 items-center lg:items-start">
<ProductGallery images={product.images} />
<div className="flex flex-col gap-4 w-full ">
<div className="flex justify-between w-full">
<div className={`flex items-center gap-2 mb-6 `}>
<Link href="/" className=" underline inline-flex gap-2">
<Home />
</Link>
{
product?.category?.slug &&
<>
<ChevronRight className={`size-4 ${locale !== "en" && "rotate-180"} `} />
<Link href={`/products/${product.category.slug}`} className=" underline">
// <div className="max-w-screen-xl mx-auto px-4 py-8 ">
// {/* <ColorAvg image={product.brand.image.url}/> */}
// <div className="flex flex-col lg:flex-row gap-8 items-center lg:items-start">
// <ProductGallery images={product.images} />
// <div className="flex flex-col gap-4 w-full ">
// <div className="flex justify-between w-full">
// <div className={`flex items-center gap-2 mb-6 `}>
// <Link href="/" className=" underline inline-flex gap-2">
// <Home />
// </Link>
// {
// product?.category?.slug &&
// <>
// <ChevronRight className={`size-4 ${locale !== "en" && "rotate-180"} `} />
// <Link href={`/products/${product.category.slug}`} className=" underline">
// {product.category.title}
// </Link>
// </>
// }
// {
// product?.brand?.slug &&
// <>
// <ChevronRight className={`size-4 ${locale !== "en" && "rotate-180"} `} />
// <Link href={`/products/${product.brand.slug}`} className=" underline">
// {product.brand.title}
// </Link>
// </>
// }
// <ChevronRight className={`size-4 ${locale !== "en" && "rotate-180"} `} />
// <span>{product.title}</span>
// </div>
// </div>
// <div className="h-full flex justify-between flex-row w-full items-start">
// <ProductInfo
// title={product.title}
// price={product.price}
// discount={product.discount}
// showPrice={product.showPrice}
// category={product.category}
// summery={product.summery}
// brand={product.brand}
// subcategories={product.subcategories}
// />
// </div>
// </div>
// </div>
// <div className="mt-12">
// <ProductProperties properties={product.properties} />
// </div>
// <div className="mt-12">
// <ProductDescription description={product.description} />
// </div>
// <div className="mt-12">
// <ProductRelated brand={product.brand} category={product.category} />
// </div>
// </div>
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-white">
{/* Hero Section */}
<div className="text-white w-full h-full relative" >
<BackgroundColor image={product?.brand?.image?.url} />
<div className="max-w-7xl mx-auto px-4 py-28 relative z-10">
<div className="flex flex-col md:flex-row gap-8 items-center">
<div className="flex-1 space-y-6">
<h1 className="text-4xl md:text-5xl font-bold leading-tight">{product.title}</h1>
<div className="text-base text-gray-50/50 font-semibold">
Price upon request
</div>
<div className="flex flex-wrap items-center gap-3">
{product?.brand && (
<Link href={`/products/${product?.brand?.slug}`} className="inline-flex items-center gap-2 px-3 py-1 bg-white/20 rounded-full text-sm hover:bg-white/30 transition-colors">
<div className="relative size-4">
<Image
src={product.brand?.image?.url}
alt={product.brand?.title}
className="aspect-square object-contain"
fill
/>
</div>
{product?.brand?.title}
</Link>
)}
<Link href={`/products/${product.category.slug}`} className="inline-flex items-center gap-2 px-3 py-1 bg-white/20 rounded-full text-sm hover:bg-white/30 transition-colors">
<Boxes className="w-4 h-4" />
{product.category.title}
</Link>
</>
}
{
product?.brand?.slug &&
<>
<ChevronRight className={`size-4 ${locale !== "en" && "rotate-180"} `} />
<Link href={`/products/${product.brand.slug}`} className=" underline">
{product.brand.title}
{product.brand?.subcategories.map(subcat => (
<Link
key={subcat.documentId}
href={`/products/${subcat.slug}`}
className="inline-flex items-center gap-2 px-3 py-1 bg-white/20 rounded-full text-sm hover:bg-white/30 transition-colors"
>
<LinkIcon className="w-4 h-4" />
{subcat.title}
</Link>
</>
}
<ChevronRight className={`size-4 ${locale !== "en" && "rotate-180"} `} />
<span>{product.title}</span>
))}
</div>
</div>
<div className="h-full flex justify-between flex-row w-full items-start">
<div className="flex-1 w-full md:w-auto">
<div className="rounded-2xl overflow-hidden shadow-2xl bg-white/10 backdrop-blur-sm">
<ProductGallery images={product.images} />
</div>
</div>
</div>
</div>
</div>
<ProductInfo
title={product.title}
price={product.price}
discount={product.discount}
showPrice={product.showPrice}
category={product.category}
summery={product.summery}
brand={product.brand}
subcategories={product.subcategories}
{/* Product Description */}
<div className="py-16 bg-gray-50">
<div className="max-w-7xl mx-auto px-4">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
<div className="lg:col-span-2">
<h2 className="text-3xl font-bold mb-8">Product Description</h2>
<div
className="prose max-w-none text-gray-600 text-lg leading-relaxed content text-justify"
dangerouslySetInnerHTML={{ __html: product.description }}
/>
</div>
<ProductProperties product={product} />
</div>
</div>
{/* Features */}
<div className="py-16 ">
<hr />
<div className="max-w-7xl mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{features.map((feature, index) => (
<div key={index} className="flex items-start gap-4 p-6 rounded-xl bg-gray-50">
<div className="w-12 h-12 rounded-lg bg-gray-900 text-white flex items-center justify-center">
<feature.icon className="w-6 h-6" />
</div>
<div className="mt-12">
<ProductProperties properties={product.properties} />
<div>
<h3 className="text-xl font-semibold mb-2">{feature.title}</h3>
<p className="text-gray-600">{feature.description}</p>
</div>
<div className="mt-12">
<ProductDescription description={product.description} />
</div>
<div className="mt-12">
<ProductRelated brand={product.brand} category={product.category} />
))}
</div>
</div>
<hr />
</div>
</div>
{/* Related Products */}
<div className="py-16 bg-gray-50">
<div className="max-w-7xl mx-auto px-4">
<ProductRelated category={product?.category} brand={product?.brand} />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,14 @@
"use client"
import React from 'react'
import useAvgColor from '../ColorAvg'
const BackgroundColor = ({ image }) => {
const {color} = useAvgColor(image)
return (
<div className='w-full h-full absolute bottom-0 top-0 left-0 right-0 bg-gradient-to-b from-[#A4756E] to-black' />
)
}
export default BackgroundColor

View File

@ -1,24 +1,32 @@
"use client"
import React, { useEffect } from 'react'
"use client";
import { useEffect, useState } from 'react';
import { FastAverageColor } from 'fast-average-color';
const ColorAvg = ({image}) => {
const useAvgColor = (image) => {
const [color, setColor] = useState("");
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const fac = new FastAverageColor();
console.log("image",image)
if (image) {
fac.getColorAsync(image)
.then(color => {
console.log("color",color)
// document.body.style.background = color.rgba
// document.body.style.color = color.isDark ? '#fff' : '#000';
// container.style.backgroundColor = color.rgba;
// container.style.color = color.isDark ? '#fff' : '#000';
.then((colorResult) => {
setColor(colorResult.hex ?? "#e18c8c");
setIsDark(colorResult.isDark)
})
.catch(e => {
.catch((e) => {
console.log(e);
});
}, [])
return (
<></>
)
}else{
setColor("#e18c8c")
}
export default ColorAvg
return () => fac.destroy(); // Clean up on component unmount
}, [image]);
return {color,isDark};
};
export default useAvgColor;

View File

@ -36,7 +36,8 @@ export default function ContactModal({ close, open }) {
data: {
email,
companyName,
message
message,
},
locale
})

View File

@ -0,0 +1,131 @@
import React from 'react';
import { Send, X, User, Building, Mail, Phone, Package, MessageSquare } from 'lucide-react';
export const EnquiryForm = ({ isOpen, onClose }) => {
const handleSubmit = (e) => {
e.preventDefault();
// Handle form submission
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg relative max-h-[90vh] overflow-y-auto">
<div className="bg-gradient-to-r from-blue-600 to-blue-700 p-6 rounded-t-2xl">
<h3 className="text-2xl font-bold text-white">Request Quote</h3>
<p className="text-blue-100 mt-1">Fill out the form below for pricing and more information</p>
<button
onClick={onClose}
className="absolute right-4 top-4 text-white/80 hover:text-white transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div className="space-y-4">
<div className="relative">
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Full Name
</label>
<div className="relative">
<User className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
id="name"
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
</div>
<div className="relative">
<label htmlFor="company" className="block text-sm font-medium text-gray-700 mb-1">
Company Name
</label>
<div className="relative">
<Building className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
id="company"
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
</div>
<div className="relative">
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<div className="relative">
<Mail className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="email"
id="email"
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
</div>
<div className="relative">
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-1">
Phone Number
</label>
<div className="relative">
<Phone className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="tel"
id="phone"
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
</div>
<div className="relative">
<label htmlFor="quantity" className="block text-sm font-medium text-gray-700 mb-1">
Desired Quantity (MOQ: 1000 units)
</label>
<div className="relative">
<Package className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="number"
id="quantity"
min="1000"
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
</div>
<div className="relative">
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1">
Additional Requirements
</label>
<div className="relative">
<MessageSquare className="w-5 h-5 text-gray-400 absolute left-3 top-3" />
<textarea
id="message"
rows={4}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
></textarea>
</div>
</div>
</div>
<button
type="submit"
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 text-white py-3 px-4 rounded-lg hover:from-blue-700 hover:to-blue-800 transition-all flex items-center justify-center gap-2 text-lg font-semibold"
>
<Send className="w-5 h-5" />
Submit Enquiry
</button>
</form>
</div>
</div>
);
};

View File

@ -0,0 +1,84 @@
"use client"
import React, { useState } from 'react';
import useEmblaCarousel from 'embla-carousel-react';
import Autoplay from 'embla-carousel-autoplay';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import Lightbox from "yet-another-react-lightbox";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
import "yet-another-react-lightbox/styles.css";
export const ProductGallery = ({ images }) => {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }, [Autoplay()]);
const [lightboxOpen, setLightboxOpen] = useState(false);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const scrollPrev = React.useCallback(() => {
if (emblaApi) emblaApi.scrollPrev();
}, [emblaApi]);
const scrollNext = React.useCallback(() => {
if (emblaApi) emblaApi.scrollNext();
}, [emblaApi]);
const openLightbox = (index) => {
setCurrentImageIndex(index);
setLightboxOpen(true);
};
const slides = images.map(image => ({
src: image.url,
alt: image.alternativeText || 'Product image'
}));
return (
<div className="relative group">
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex">
{images.map((image, index) => (
<div key={image.documentId} className="flex-[0_0_100%] min-w-0">
<img
src={image.url}
alt={image.alternativeText || ''}
className="w-full h-[400px] object-contain cursor-pointer"
onClick={() => openLightbox(index)}
/>
</div>
))}
</div>
</div>
<button
onClick={scrollPrev}
className="absolute left-4 top-1/2 -translate-y-1/2 bg-white/90 p-3 rounded-full shadow-lg hover:bg-white transition-colors opacity-0 group-hover:opacity-100"
>
<ChevronLeft className="w-6 h-6 text-gray-900" />
</button>
<button
onClick={scrollNext}
className="absolute right-4 top-1/2 -translate-y-1/2 bg-white/90 p-3 rounded-full shadow-lg hover:bg-white transition-colors opacity-0 group-hover:opacity-100"
>
<ChevronRight className="w-6 h-6 text-gray-900" />
</button>
<Lightbox
open={lightboxOpen}
close={() => setLightboxOpen(false)}
index={currentImageIndex}
slides={slides}
plugins={[Zoom]}
zoom={{
maxZoomPixelRatio: 5,
zoomInMultiplier: 2,
doubleTapDelay: 300,
doubleClickDelay: 300,
doubleClickMaxStops: 2,
keyboardMoveDistance: 50,
wheelZoomDistanceFactor: 100,
pinchZoomDistanceFactor: 100,
scrollToZoom: true
}}
/>
</div>
);
};

View File

@ -86,7 +86,7 @@ const Navbar = ({ items }) => {
<p className="mb-0 text-white"> salam</p>
</div> */}
{isScrolled && (
<div className="w-full lg:h-[76px] h-2">
<div className="w-full lg:h-[66px] h-2">
</div>
)}
@ -185,14 +185,14 @@ const Navbar = ({ items }) => {
{
isScrolled &&
(
<div className="relative rounded-lg flex flex-col items-center rounded-tl-2xl justify-center my-auto">
<Link href={"/"} className="relative rounded-lg flex flex-col items-center rounded-tl-2xl justify-center my-auto">
<Image
src={"/images/1.png"}
width={75} height={75}
alt="llc "
className="mx-auto object-contain"
/>
</div>
</Link>
)
}
{/* <div className={`w-full ${locale === "en" ? "rtl" : "ltr"} px-4 py-4`}>

View File

@ -1,19 +1,89 @@
import { getMessages } from "next-intl/server"
"use client"
import { CheckCircle2 } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import ContactModal from "../ContactUs";
import Image from "next/image";
import Link from "next/link";
import { LinkIcon } from "lucide-react";
import { ArrowRight } from "lucide-react";
export default async function ProductProperties({ properties }) {
const t = await getMessages()
export default function ProductProperties({ product }) {
const t = useTranslations("PDP")
const [open, setOpen] = useState(false);
const openModal = () => {
setOpen(true);
};
const closeModal = () => {
setOpen(false);
}
return (
<div className="space-y-8">
<div className="bg-white rounded-xl p-6 shadow-sm">
<h3 className="text-xl font-semibold mb-4">{t("productSpecification")}</h3>
<div className="space-y-4">
{product.properties.map(prop => (
<div key={prop.id} className="flex items-center gap-4">
<CheckCircle2 className="w-5 h-5 text-gray-900 flex-shrink-0" />
<div>
<h2 className="text-2xl font-bold mb-4">{t.PDP.productSpecifications}</h2>
<dl className="grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2">
{properties.map((prop) => (
<div key={prop.id} className="border-t border-gray-200 pt-6">
<dt className="font-medium text-gray-900 capitalize">{prop.key}</dt>
<dd className="mt-2 text-gray-500">{prop.value}</dd>
<span className="font-medium">{prop.key}:</span>
<span className="text-gray-600 ml-2">{prop.value}</span>
</div>
</div>
))}
</dl>
</div>
</div>
{product?.brand &&
<div className="bg-white rounded-xl p-6 shadow-sm">
<h3 className="text-xl font-semibold mb-4">Brand </h3>
<div className="flex items-center gap-4">
<div className="relative size-20">
<Image
src={product?.brand?.image?.url}
alt={product?.brand?.image?.alternativeText}
fill
className="object-contain"
/>
</div>
<div className="flex flex-col justify-around">
<Link href={`/products/${product?.brand?.slug}`} className="font-bold text-lg">
{product?.brand?.title}
</Link>
<Link href={`/products/${product?.brand?.slug}`} className="text-sm text-black/70">
View All
</Link>
</div>
</div>
</div>
}
{product.brand?.subcategories.lenght &&
<div className="bg-white rounded-xl p-6 shadow-sm">
<h3 className="text-xl font-semibold mb-4">Use Cases </h3>
<div className="flex items-center gap-4">
{product.brand?.subcategories.map(subcat => (
<Link
key={subcat.documentId}
href={`/products/${subcat.slug}`}
className="inline-flex items-center gap-2 px-3 py-1 bg-black/10 rounded-full text-sm hover:bg-black/30 transition-colors"
>
<LinkIcon className="w-4 h-4" />
{subcat.title}
</Link>
))}
</div>
</div>
}
<button
onClick={() => setOpen(true)}
// onClick={() => setIsModalOpen(true)}
className="w-full bg-gray-900 text-white py-4 rounded-lg hover:bg-gray-800 transition-colors flex items-center justify-center gap-2 font-semibold"
>
Get Quote
<ArrowRight className="w-5 h-5" />
</button>
<ContactModal close={closeModal} open={open} />
</div>
)
}

View File

@ -73,7 +73,7 @@ const Content = ({ content }) => {
</div>
</div>
</div>
<ContactModal close={closeModal} open={open} />
</section >
)
}