main
Amir Hossein Moghiseh 2025-02-22 19:14:53 +03:30
parent e3c332123c
commit 72cf471c13
43 changed files with 1590 additions and 795 deletions

View File

@ -16,10 +16,12 @@
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"next": "15.1.6", "next": "15.1.6",
"next-intl": "^3.26.4", "next-intl": "^3.26.4",
"rc-pagination": "^5.1.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-countup": "^6.5.3", "react-countup": "^6.5.3",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-intersection-observer": "^9.15.1", "react-intersection-observer": "^9.15.1",
"react-toastify": "^11.0.3",
"swiper": "^11.2.2" "swiper": "^11.2.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -23,6 +23,9 @@ dependencies:
next-intl: next-intl:
specifier: ^3.26.4 specifier: ^3.26.4
version: 3.26.4(next@15.1.6)(react@19.0.0) version: 3.26.4(next@15.1.6)(react@19.0.0)
rc-pagination:
specifier: ^5.1.0
version: 5.1.0(react-dom@19.0.0)(react@19.0.0)
react: react:
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.0.0 version: 19.0.0
@ -35,6 +38,9 @@ dependencies:
react-intersection-observer: react-intersection-observer:
specifier: ^9.15.1 specifier: ^9.15.1
version: 9.15.1(react-dom@19.0.0)(react@19.0.0) version: 9.15.1(react-dom@19.0.0)(react@19.0.0)
react-toastify:
specifier: ^11.0.3
version: 11.0.3(react-dom@19.0.0)(react@19.0.0)
swiper: swiper:
specifier: ^11.2.2 specifier: ^11.2.2
version: 11.2.4 version: 11.2.4
@ -63,6 +69,13 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/@babel/runtime@7.26.9:
resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.1
dev: false
/@emnapi/runtime@1.3.1: /@emnapi/runtime@1.3.1:
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
requiresBuild: true requiresBuild: true
@ -982,10 +995,19 @@ packages:
fsevents: 2.3.3 fsevents: 2.3.3
dev: true dev: true
/classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
dev: false
/client-only@0.0.1: /client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
dev: false dev: false
/clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
dev: false
/color-convert@2.0.1: /color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@ -2681,6 +2703,31 @@ packages:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true dev: true
/rc-pagination@5.1.0(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==}
peerDependencies:
react: '>=16.9.0'
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.26.9
classnames: 2.5.1
rc-util: 5.44.4(react-dom@19.0.0)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
dev: false
/rc-util@5.44.4(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==}
peerDependencies:
react: '>=16.9.0'
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.26.9
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
react-is: 18.3.1
dev: false
/react-countup@6.5.3(react@19.0.0): /react-countup@6.5.3(react@19.0.0):
resolution: {integrity: sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==} resolution: {integrity: sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==}
peerDependencies: peerDependencies:
@ -2716,6 +2763,21 @@ packages:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true dev: true
/react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
dev: false
/react-toastify@11.0.3(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-cbPtHJPfc0sGqVwozBwaTrTu1ogB9+BLLjd4dDXd863qYLj7DGrQ2sg5RAChjFUB4yc3w8iXOtWcJqPK/6xqRQ==}
peerDependencies:
react: ^18 || ^19
react-dom: ^18 || ^19
dependencies:
clsx: 2.1.1
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
dev: false
/react@19.0.0: /react@19.0.0:
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2748,6 +2810,10 @@ packages:
which-builtin-type: 1.2.1 which-builtin-type: 1.2.1
dev: true dev: true
/regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
dev: false
/regexp.prototype.flags@1.5.4: /regexp.prototype.flags@1.5.4:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -1,210 +0,0 @@
import React from "react";
import CategoriesData from "src/view/Categories";
const page = async ({ params, searchParams }) => {
const data = [
{
id: 1,
persianName: "Hydrating Cream",
englishName: "Hydrating Cream",
description:
"A deeply moisturizing cream that keeps your skin hydrated all day long.",
cost: 250000,
costWithDiscount: 200000,
hasDiscount: true,
discountPercent: 20,
stock: 5,
mainImage: "4",
},
{
id: 2,
persianName: "Hair Strengthening Shampoo",
englishName: "Hair Strengthening Shampoo",
description:
"A nourishing shampoo that strengthens hair roots and prevents hair fall.",
cost: 180000,
costWithDiscount: 150000,
hasDiscount: true,
discountPercent: 17,
stock: 2,
mainImage: "3",
},
{
id: 3,
persianName: "Vitamin C Serum",
englishName: "Vitamin C Serum",
description:
"An antioxidant-rich serum that brightens skin and reduces signs of aging.",
cost: 300000,
costWithDiscount: 270000,
hasDiscount: true,
discountPercent: 10,
stock: 3,
mainImage: "1",
},
{
id: 4,
persianName: "Charcoal Face Mask",
englishName: "Charcoal Face Mask",
description:
"A detoxifying mask that removes impurities and unclogs pores for a fresh look.",
cost: 220000,
costWithDiscount: 220000,
hasDiscount: false,
discountPercent: 0,
stock: 8,
mainImage: "4",
},
{
id: 5,
persianName: "Body Lotion",
englishName: "Body Lotion",
description:
"A lightweight body lotion that nourishes and hydrates dry skin.",
cost: 210000,
costWithDiscount: 185000,
hasDiscount: true,
discountPercent: 12,
stock: 6,
mainImage: "2",
},
{
id: 6,
persianName: "Aloe Vera Gel",
englishName: "Aloe Vera Gel",
description:
"A soothing gel enriched with aloe vera to calm irritated skin.",
cost: 160000,
costWithDiscount: 160000,
hasDiscount: false,
discountPercent: 0,
stock: 10,
mainImage: "3",
},
{
id: 7,
persianName: "Sunscreen SPF 50",
englishName: "Sunscreen SPF 50",
description:
"A broad-spectrum sunscreen that protects against UV rays and prevents sunburn.",
cost: 280000,
costWithDiscount: 230000,
hasDiscount: true,
discountPercent: 18,
stock: 4,
mainImage: "1",
},
{
id: 8,
persianName: "Face Cleanser",
englishName: "Face Cleanser",
description:
"A gentle face cleanser that removes dirt and oil without stripping moisture.",
cost: 190000,
costWithDiscount: 170000,
hasDiscount: true,
discountPercent: 10,
stock: 7,
mainImage: "2",
},
{
id: 9,
persianName: "Moisturizing Cream",
englishName: "Moisturizing Cream",
description:
"A rich cream that provides deep hydration for soft and smooth skin.",
cost: 260000,
costWithDiscount: 260000,
hasDiscount: false,
discountPercent: 0,
stock: 9,
mainImage: "4",
},
{
id: 10,
persianName: "Eye Serum",
englishName: "Eye Serum",
description:
"A lightweight eye serum that reduces puffiness and dark circles.",
cost: 350000,
costWithDiscount: 310000,
hasDiscount: true,
discountPercent: 12,
stock: 5,
mainImage: "3",
},
{
id: 11,
persianName: "Lip Balm",
englishName: "Lip Balm",
description:
"A moisturizing lip balm that prevents chapped lips and adds a subtle shine.",
cost: 90000,
costWithDiscount: 80000,
hasDiscount: true,
discountPercent: 11,
stock: 12,
mainImage: "1",
},
{
id: 12,
persianName: "Hand Cream",
englishName: "Hand Cream",
description:
"A fast-absorbing hand cream that keeps hands soft and hydrated.",
cost: 170000,
costWithDiscount: 150000,
hasDiscount: true,
discountPercent: 12,
stock: 6,
mainImage: "2",
},
{
id: 13,
persianName: "Night Repair Serum",
englishName: "Night Repair Serum",
description:
"A serum that works overnight to repair and rejuvenate your skin.",
cost: 390000,
costWithDiscount: 350000,
hasDiscount: true,
discountPercent: 10,
stock: 4,
mainImage: "3",
},
{
id: 14,
persianName: "Shaving Cream",
englishName: "Shaving Cream",
description:
"A rich shaving cream that provides a smooth and irritation-free shave.",
cost: 200000,
costWithDiscount: 200000,
hasDiscount: false,
discountPercent: 0,
stock: 9,
mainImage: "4",
},
{
id: 15,
persianName: "Shaving ",
englishName: "Shaving ",
description:
"A fast-absorbing hand cream that keeps hands soft and hydrated.",
cost: 200000,
costWithDiscount: 200000,
hasDiscount: false,
discountPercent: 0,
stock: 9,
mainImage: "1",
},
];
return (
<div>
<CategoriesData params={params} products={data} />
</div>
);
};
export default page;

View File

@ -1,14 +0,0 @@
"use client";
import { useRouter } from "next/navigation";
import React, { useEffect } from "react";
const Page = (props) => {
const router = useRouter();
useEffect(() => {
router.push("/categories/0");
}, []);
return;
};
export default Page;

View File

@ -1,13 +1,74 @@
import { NextIntlClientProvider } from "next-intl"; import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server"; import { getLocale, getMessages } from "next-intl/server";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { routing } from "src/i18n/routing"; import { routing } from "src/i18n/routing";
import "../../styles/globals.css"; import "../../styles/globals.css";
import Navbar from "src/components/NavBar";
import Footer from "src/view/Landing/components/Footer";
import { ToastContainer } from 'react-toastify';
import graphql from "src/utils/graphql";
import { Rubik, Roboto } from "next/font/google";
const gql = `
query Navbars($locale: I18NLocaleCode) {
navbars(locale: $locale, sort: ["order:asc"]) {
documentId
title
link
children {
id
title
link
}
}
}
`
const rubik = Rubik({
subsets: ["arabic"],
weight: ["400", "700"],
variable: "--font-rubik", // This will create a CSS variable for easy use
});
const roboto = Roboto({
subsets: ["latin"],
weight: ["400", "700"],
variable: "--font-roboto", // Create a CSS variable for Roboto
});
const fetchNavbarItems = async () => {
const locale = await getLocale()
const { navbars } = await graphql(gql, {
locale: locale
})
return navbars
}
// app/[locale]/page.js
export const generateMetadata = async ({ params }) => {
const { locale } = params;
const t = await getMessages(locale)
// Define titles and descriptions for each locale
return {
title: t.HomePage.SEO.title,
description: t.HomePage.SEO.descriptions,
};
};
export default async function LocaleLayout({ export default async function LocaleLayout({
children, children,
params: { locale } params
}) { }) {
const { locale } = await params
const navbaritems = await fetchNavbarItems()
// Ensure that the incoming `locale` is valid // Ensure that the incoming `locale` is valid
if (!routing.locales.includes(locale)) { if (!routing.locales.includes(locale)) {
notFound(); notFound();
@ -16,13 +77,19 @@ export default async function LocaleLayout({
// Providing all messages to the client // Providing all messages to the client
// side is the easiest way to get started // side is the easiest way to get started
const messages = await getMessages(); const messages = await getMessages();
console.log("messager",messages)
return ( return (
<html lang={locale}> <html lang={locale} dir={locale === "en" ? "ltr" : "rtl"}>
<body> <head>
<meta name="apple-mobile-web-app-title" content="AdHorizonIntl" />
</head>
<body className={`${rubik.variable} ${roboto.variable} `} suppressHydrationWarning>
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider messages={messages}>
<Navbar items={navbaritems} />
{children} {children}
<Footer />
<ToastContainer />
</NextIntlClientProvider> </NextIntlClientProvider>
</body> </body>
</html> </html>

View File

@ -1,25 +0,0 @@
import Product from "src/view/Products";
const page = async ({ params, searchParams }) => {
const data = {
id: 1,
persianName: "Hydrating Cream",
englishName: "Hydrating Cream",
description:
"Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptates impedit officia eos quo aliquam soluta iste earum aliquid harum doloremque iure consectetur vel, cumque autem id, numquam quisquam mollitia velit. . Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptates impedit officia eos quo aliquam soluta iste earum aliquid harum doloremque iure consectetur vel, cumque autem id, numquam quisquam mollitia velit. . ",
cost: 250000,
costWithDiscount: 200000,
hasDiscount: true,
discountPercent: 20,
stock: 5,
mainImage: "4",
};
return (
<>
<Product params={params} data={data} />
</>
);
};
export default page;

View File

@ -0,0 +1,151 @@
import { Home } from "lucide-react";
import { ChevronRight } from "lucide-react";
import { ShoppingCart } from "lucide-react";
import { getLocale, getMessages } from "next-intl/server";
import Link from "next/link";
import BrandInfo from "src/components/Product/BrandInfo";
import ProductDescription from "src/components/Product/ProductDescription";
import ProductGallery from "src/components/Product/ProductGallery";
import ProductInfo from "src/components/Product/ProductInfo";
import ProductProperties from "src/components/Product/ProductProperties";
import ProductRelated from "src/components/Product/ProductRelated";
import graphql from "src/utils/graphql";
const gql = `
query Products($locale: I18NLocaleCode, $slug: String!) {
products(filters: { slug: { eqi: $slug } }, locale: $locale) {
documentId
title
price
summery
qt
images {
documentId
alternativeText
url
}
category {
documentId
title
slug
}
description
brand {
documentId
title
image {
alternativeText
documentId
url
}
slug
}
slug
discount
showPrice
seo {
id
metaTitle
metaDescription
keywords
metaRobots
metaViewport
canonicalURL
structuredData
metaImage {
documentId
alternativeText
url
}
openGraph {
id
ogTitle
ogDescription
ogUrl
ogType
ogImage {
documentId
alternativeText
url
}
}
}
properties {
id
key
value
}
}
}
`;
const getProduct = async (slug) => {
const { products } = await graphql(gql, {
locale: "en",
slug,
});
return products[0];
};
export default async function ProductPage({ params }) {
const product = await getProduct(params.slug);
if (!product) {
notFound();
}
const locale = await getLocale();
const t = await getMessages({ locale });
console.log(t)
return (
<div className="contain-none md:container mx-auto px-4 py-8">
<div className="flex flex-col lg:flex-row gap-8">
<ProductGallery images={product.images} />
<div className="flex flex-col gap-4 w-full ">
<div className={`flex items-center gap-2 mb-6 `}>
<Link href="/" className=" underline inline-flex gap-2">
<Home />
</Link>
<ChevronRight className={`size-4 ${locale !== "en" && "rotate-180"} `} />
<Link href={`/products/${product.category.slug}`} className=" underline">
{product.category.title}
</Link>
<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 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}
/>
<BrandInfo brand={product.brand} />
</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>
);
}

View File

@ -0,0 +1,10 @@
import CategoriesData from "src/view/Categories";
const CategoryPage = async ({ params }) => {
const _params = await params;
return (
<CategoriesData category={_params.category ?? undefined} />
);
};
export default CategoryPage;

View File

@ -0,0 +1,10 @@
import CategoriesData from "src/view/Categories";
const page = async ({ params }) => {
const _params = await params;
return (
<CategoriesData category={_params.category ?? undefined} />
);
};
export default page;

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/app/favicon.ico 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/app/icon.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

3
src/app/icon.svg 100644

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 428 KiB

View File

@ -0,0 +1,21 @@
{
"name": "MyWebSite",
"short_name": "MySite",
"icons": [
{
"src": "/favicons/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/favicons/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@ -0,0 +1,115 @@
"use client"
import Image from "next/image"
import { ShoppingCart, HardHat, ChevronRight } from "lucide-react"
import Link from "next/link"
import { useLocale } from "next-intl"
import { useTranslations } from "next-intl"
export default function ProductCard({ product }) {
const { title, brand, category, price, discount, images, slug } = product
const isValidPrice = typeof price === "number" && !isNaN(price)
const isValidDiscount = typeof discount === "number" && !isNaN(discount) && discount > 0 && discount <= 100
const displayPrice = isValidPrice ? price : 0
const discountedPrice =
isValidPrice && isValidDiscount ? displayPrice - (displayPrice * discount) / 100 : displayPrice
const CategoryIcon = category.slug === "fmcg" ? ShoppingCart : HardHat
const fallbackBrandLetter = brand?.title ? brand.title.charAt(0).toUpperCase() : "#"
const locale = useLocale()
const t = useTranslations("")
return (
<div className="bg-white rounded-lg shadow-md overflow-hidden" dir={locale === "en" ? "ltr": "rtl"} >
<div className="relative h-48 w-full overflow-hidden rounded-lg">
<Image src={images?.[0]?.url} alt={images?.[0]?.alternativeText ?? ""} layout="fill" className="object-contain border" />
{isValidDiscount && (
<div className="absolute top-2 right-2 bg-red-500 text-white text-xs font-semibold px-2 py-1 rounded-full">
{discount}% OFF
</div>
)}
</div>
<div className="p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2 w-2/3">
{brand?.title &&
<>
<div className="size-6 min-w-6 rounded-full overflow-hidden relative aspect-square">
{brand?.image?.url ? (
<Image src={brand?.image?.url} alt={brand?.image.alternativeText ?? ""} fill className=" object-cover aspect-square" />
) : (
<div className="h-full w-full flex items-center justify-center bg-gray-200 text-gray-700 font-semibold">
{fallbackBrandLetter}
</div>
)}
</div>
<span className={`text-sm font-medium w-full px-1 ${locale === "en" ? "text-left" : "text-right"}`}>{brand?.title || ""}</span>
</>
}
</div>
<div className={`flex items-center text-sm text-gray-500 w-1/3 ${locale === "en" ? "flex-row-reverse justify-start" : "flex-row justify-end"}`} >
<CategoryIcon className="size-4 min-h-4 min-w-4 mx-1 aspect-square" />
<span>{category.slug === "fmcg" ? t("HomePage.Sides.fmcg.title") : t("HomePage.Sides.construction.title")}</span>
</div>
</div>
<h3 className={`text-lg font-semibold mb-2 line-clamp-2 min-h-14 ${locale === "en" ? "text-left" : "text-right"}`} >{title}</h3>
<div className="flex items-center justify-between mb-4">
<div>
{isValidPrice ? (
<>
<span className="text-xl font-bold">${discountedPrice.toFixed(2)}</span>
{isValidDiscount && (
<span className="text-sm text-gray-500 line-through ml-2">${displayPrice.toFixed(2)}</span>
)}
</>
) : (
<span className="text-xl font-bold"></span>
)}
</div>
</div>
<Link href={`/products/${brand?.slug || category?.slug || 'horizon'}/${slug}`.toLowerCase()}
className={`w-full flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors ${locale === "en" ? "flex-row" : "flex-row-reverse"}`}>
{t("Utils.moreDetail")}
<ChevronRight className="h-4 w-4 ml-2" />
</Link>
</div>
</div>
)
}
export function ProductCardSkeleton() {
return (
<div className="bg-white rounded-lg shadow-md overflow-hidden">
{/* Image placeholder */}
<div className="relative h-48 w-full bg-gray-200 animate-pulse" />
<div className="p-4">
{/* Brand and category placeholder */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
<div className="h-6 w-6 rounded-full bg-gray-300 animate-pulse" />
<div className="h-4 w-24 bg-gray-300 rounded animate-pulse" />
</div>
<div className="h-4 w-20 bg-gray-300 rounded animate-pulse" />
</div>
{/* Product name placeholder */}
<div className="h-6 w-3/4 bg-gray-300 rounded animate-pulse mb-2" />
{/* Price placeholder */}
<div className="flex items-center justify-between mb-4">
<div className="h-8 w-24 bg-gray-300 rounded animate-pulse" />
</div>
{/* Button placeholder */}
<div className="h-10 w-full bg-gray-300 rounded animate-pulse" />
</div>
</div>
)
}

View File

@ -1,68 +0,0 @@
"use client";
import logo from "../../../assets/images/logo.png";
import Image from "next/image";
import Link from "next/link";
const CardNormal = ({ product, priority }) => {
return (
<div className="group">
{" "}
<>
<Link href={`/products/${product.slug}`} onClick={(e) => e.preventDefault()} >
<div
className={`tr03 py-2 overflow-hidden xs:h-[270px] lg:h-[270px] border border-gray-200 ${1 == 1 ? "bg-white rounded-xl rounded-tl-[40px]" : " opacity-70"
}`}
>
<div className="w-full h-full flex justify-center relative p-2 ">
{!!product?.images?.[0]?.url ? (
<Image
src={
product?.images?.[0]?.url
}
fill
className="object-contain"
priority={!!priority}
alt={`${product?.images?.[0]?.alternativeText}`}
/>
) : (
<div className="xs:!w-[85px] lg:!w-[85px] h-[90px] xs:mt-5 lg:mt-10 ">
<Image
src={logo}
className="xs:!w-[70px] lg:!w-[70px] mx-auto opacity-25 mt-5"
/>
</div>
)}
</div>
<div className="p-3 text-left">
</div>
</div>
</Link>
</>
<div className="relative">
<div className="absolute w-full bottom-0 ">
<div className="bg-gray-100 rounded-t-3xl ltr mt-2 border border-gray-100 p-2 flex flex-col gap-1">
<p className="mb-0 text-base text-left">
{product.title}
</p>
{/* <div className="flex w-full">
<p className="mb-0 text-base ">${data.cost.toLocaleString()}</p>
</div> */}
<div className="w-full text-right rounded-full pr-2">
<p className="mb-0 text-sm rounded-lg text-left">{product.category.title} - {product.brand.title}</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default CardNormal;

View File

@ -3,16 +3,19 @@
import useEmblaCarousel from "embla-carousel-react" import useEmblaCarousel from "embla-carousel-react"
import { ChevronLeft, ChevronRight } from "lucide-react" import { ChevronLeft, ChevronRight } from "lucide-react"
import { useCallback } from "react" import { useCallback } from "react"
import CardNormal from "../Cards/CardNormal/page" import CardNormal from "../Cards/CardNormal"
import Autoplay from "embla-carousel-autoplay" import Autoplay from "embla-carousel-autoplay"
import Link from "next/link"
import { useLocale, useTranslations } from "next-intl"
export default function ProductCarousel({ title, subtitle, products }) { export default function ProductCarousel({ title, subtitle, products, showMoreLink }) {
const t = useTranslations("Utils")
const locale = useLocale()
const [emblaRef, emblaApi] = useEmblaCarousel( const [emblaRef, emblaApi] = useEmblaCarousel(
{ loop: true, align: "start" }, { loop: true, align: "start", direction: locale === "en" ? "ltr" : "rtl" },
[ [
Autoplay({ delay: 3000, stopOnInteraction: true }), Autoplay({ delay: 3000, stopOnInteraction: true }),
] ]
@ -27,42 +30,52 @@ export default function ProductCarousel({ title, subtitle, products }) {
}, [emblaApi]) }, [emblaApi])
return ( return (
<section className="py-12 px-4 md:px-6 lg:px-8"> <section className="py-12 max-w-screen-xlmx-auto " >
<div className="max-w-7xl mx-auto">
<h2 className="text-3xl font-bold text-center mb-2">{title}</h2>
<p className="text-xl text-center text-gray-600 mb-8">{subtitle}</p>
<div className="relative"> <div className="flex sm:justify-between flex-col sm:flex-row pb-4 items-center">
<div className="overflow-hidden" ref={emblaRef}> <div className="flex flex-col gap-2">
<div className="flex"> <h2 className="text-3xl font-bold text-center mb-2">{title}</h2>
{products.map((product) => ( {
<div subtitle &&
key={product.id} <p className="text-xl text-center text-gray-600 mb-8">{subtitle}</p>
className="flex-[0_0_100%] min-w-0 sm:flex-[0_0_50%] md:flex-[0_0_33.33%] lg:flex-[0_0_25%] pl-4" }
>
<CardNormal product={product} priority={true} />
</div>
))}
</div>
</div>
<button
variant="outline"
size="icon"
className="absolute top-1/2 left-4 transform -translate-y-1/2 bg-gray-200 rounded-full size-4"
onClick={scrollPrev}
>
<ChevronLeft className="h-4 w-4" />
</button>
<button
variant="outline"
size="icon"
className="absolute top-1/2 right-4 transform -translate-y-1/2 bg-gray-200 rounded-full size-4"
onClick={scrollNext}
>
<ChevronRight className="h-4 w-4" />
</button>
</div> </div>
<Link href={showMoreLink} className="text-sm text-blue-600 underline">
{t("showMoreLink")}
</Link>
</div> </div>
<div className="relative">
<div className="overflow-hidden" ref={emblaRef} >
<div className="flex">
{products.map((product) => (
<div
key={product.id}
className="flex-[0_0_100%] min-w-0 sm:flex-[0_0_50%] md:flex-[0_0_33.33%] lg:flex-[0_0_25%] pl-4"
>
<CardNormal product={product} priority={true} />
</div>
))}
</div>
</div>
<button
variant="outline"
size="icon"
className="absolute top-1/2 left-4 transform -translate-y-1/2 bg-gray-200 rounded-full size-4"
onClick={scrollPrev}
>
<ChevronLeft className="h-4 w-4" />
</button>
<button
variant="outline"
size="icon"
className="absolute top-1/2 right-4 transform -translate-y-1/2 bg-gray-200 rounded-full size-4"
onClick={scrollNext}
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
</section> </section>
) )
} }

View File

@ -0,0 +1,130 @@
"use client"; // Ensure this is a Client Component
import { X } from "lucide-react";
import { useLocale, useTranslations } from "next-intl";
import { toast } from "react-toastify";
import graphql from "src/utils/graphql";
const gql = `
mutation CreateInbox($data: InboxInput!,$locale:I18NLocaleCode) {
createInbox(data: $data, locale: $locale) {
documentId
}
}
`
export default function ContactModal({ close, open }) {
const t = useTranslations("ContactModal");
const locale = useLocale()
const submitForm = async (e) => {
e.preventDefault();
const email = e.target.email.value;
const companyName = e.target.company.value;
const message = e.target.message.value;
if (!email || !companyName || !message) {
toast.error(t("error.fillAllFields"));
return;
}
if (!email.includes("@")) {
toast.error(t("error.invalidEmail"));
return;
}
const {createInbox} = await graphql(gql, {
data: {
email,
companyName,
message
},
locale
})
if (createInbox.documentId) {
toast.success(t("success"));
close();
}
}
return (
<>
{/* Modal Overlay */}
{open && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
{/* Modal Content */}
<div className="bg-white rounded-lg shadow-lg w-full max-w-md p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">{t("title")}</h2>
<button
onClick={close}
className="text-gray-500 hover:text-gray-700"
>
<X className="h-6 w-6" />
</button>
</div>
{/* Form */}
<form onSubmit={submitForm} className="space-y-4">
{/* Email Field */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
{t("email")}
</label>
<input
type="email"
id="email"
name="email"
placeholder={t("emailPlaceholder")}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary"
required
/>
</div>
{/* Company Field */}
<div>
<label htmlFor="company" className="block text-sm font-medium text-gray-700">
{t("company")}
</label>
<input
type="text"
id="company"
name="company"
placeholder={t("companyPlaceholder")}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary"
required
/>
</div>
{/* Message Field */}
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700">
{t("message")}
</label>
<textarea
id="message"
name="message"
rows={4}
placeholder={t("messagePlaceholder")}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary"
required
/>
</div>
{/* Submit Button */}
<div>
<button
type="submit"
className="w-full px-4 py-2 bg-primary text-white rounded-md hover:bg-primary-dark transition-colors"
>
{t("sendButton")}
</button>
</div>
</form>
</div>
</div>
)}
</>
);
}

View File

@ -7,8 +7,8 @@ import test3 from "../../assets/images/product/3.png";
import logo from "../../assets/images/logo.png"; import logo from "../../assets/images/logo.png";
import { useState } from "react"; import { useState } from "react";
const GalleryBox = () => { const GalleryBox = ({ images }) => {
const file = [{ image: test1 }, { image: test2 }, { image: test3 }];
const [currentImageIndex, setCurrentImageIndex] = useState(0); const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [isLightboxOpen, setIsLightboxOpen] = useState(false); const [isLightboxOpen, setIsLightboxOpen] = useState(false);
@ -37,14 +37,14 @@ const GalleryBox = () => {
return ( return (
<div className="w-full rounded-3xl"> <div className="w-full rounded-3xl">
<div className="flex justify-center xs:pb-[10px]"> <div className="flex justify-center xs:pb-[10px]">
<div className="w-full max-w-[90vw]"> <div className=" relative p-8">
{file?.length > 0 ? ( {images?.length > 0 ? (
<Image <Image
src={file[0]?.image} src={images?.[0]?.url}
width={1000} width={500}
height={1000} height={500}
className="mx-auto object-cover cursor-pointer" className="mx-auto object-contain cursor-pointer"
alt={"llc"} alt={images?.[0]?.alternativeText ?? ""}
onClick={() => handleImageClick(0)} onClick={() => handleImageClick(0)}
priority priority
loading="eager" loading="eager"
@ -62,16 +62,16 @@ const GalleryBox = () => {
</div> </div>
<div className="flex p-3 overflow-x-auto" id="swich-scrollbar"> <div className="flex p-3 overflow-x-auto" id="swich-scrollbar">
{file.slice(1)?.map((e, index) => ( {images.slice(1)?.map((image, index) => (
<div <div
className="p-3 ml-2 bg-white border shadow-sm rounded-3xl cursor-pointer" className="p-3 ml-2 bg-white border shadow-sm rounded-3xl cursor-pointer"
key={e.id} key={image.documentId}
onClick={() => handleImageClick(index)} onClick={() => handleImageClick(index)}
> >
<div className="xs:w-[50px] lg:w-[50px]"> <div className="xs:w-[50px] lg:w-[50px]">
<Image <Image
src={e.image} src={image.url}
alt={e.fileName} alt={image.alternativeText}
width={250} width={250}
height={250} height={250}
priority priority
@ -86,9 +86,9 @@ const GalleryBox = () => {
<> <>
<div className="fixed inset-0 bg-black bg-opacity-75 z-50"> <div className="fixed inset-0 bg-black bg-opacity-75 z-50">
<div className="bg-white bg-opacity-70 p-12 flex items-center justify-center w-3/4 relative h-3/4 inset-0 left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 rounded-xl"> <div className="bg-white bg-opacity-70 p-12 flex items-center justify-center w-3/4 relative h-3/4 inset-0 left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 rounded-xl">
<Image <Image
src={file[currentImageIndex]?.image} src={images[currentImageIndex]?.url}
fill fill
className="mx-auto object-contain" className="mx-auto object-contain"
alt={"llc"} alt={"llc"}

View File

@ -4,7 +4,7 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useContext, useEffect, useRef, useState } from "react"; import { Fragment, useContext, useEffect, useRef, useState } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
@ -13,41 +13,12 @@ import { usePathname, useRouter } from "next/navigation";
import logo from "../../assets/images/logo.png"; import logo from "../../assets/images/logo.png";
import { useLocale } from "next-intl"; import { useLocale } from "next-intl";
const Navbar = ({ theme }) => {
const theme = 1;
const Navbar = ({ items }) => {
const router = useRouter(); const router = useRouter();
const NavBarData = [
{
id: 1,
name: "Home",
children: [],
href: "/"
},
{
id: 2,
name: "FMCG - (Comming Soon!)",
href: "/",
children: [],
},
{
id: 3,
name: "Constructions - (Comming Soon!)",
href: "/",
children: [],
},
{
id: 4,
name: "Feed - (Comming Soon!)",
href: "/",
children: [],
},
{
id: 4,
name: "About Us - (Comming Soon!)",
href: "/",
children: [],
},
];
const [closeNavbar, setClosNavbar] = useState(false); const [closeNavbar, setClosNavbar] = useState(false);
const [activeStepNavbar, setActiveStepNavbar] = useState(null); const [activeStepNavbar, setActiveStepNavbar] = useState(null);
@ -92,18 +63,9 @@ const Navbar = ({ theme }) => {
const pathname = usePathname() const pathname = usePathname()
const changeLocale = () => { const changeLocale = () => {
console.log(locale) const newLocale = locale === "en" ? "ar-OM" : "en";
let link = '' router.push(`/${newLocale}${window.location.pathname.replace("/ar-OM", "").replace("/en", "")}`);
if (locale === "en") { };
link = 'ar-OM' + (pathname.includes("/en") ? pathname.replace("en", "") : pathname)
}
else {
link = pathname.replace("ar-OM", "")
}
router.push(link)
}
return ( return (
@ -118,35 +80,22 @@ const Navbar = ({ theme }) => {
}`} }`}
> >
<nav <nav
className={` rtl flex justify-between w-full pl-10 ${isScrolled ? " bg-gray-100 " : " rounded-2xl" className={` ${locale === "en" ? "rtl pl-1" : "ltr pr-1"} flex justify-between w-full items-center ${isScrolled ? " bg-gray-100 " : " rounded-2xl"
} }
${theme == 1 ? "bg-gray-100 " : " "}`} ${theme == 1 ? "bg-gray-100 " : " "}`}
> >
<div className="w-full rtl p-4 mr-2"> <div className={`w-full ${locale === "en" ? "rtl" : "ltr"} px-4 py-4`}>
<div className="flex"> <div className="flex">
{/* <div className="mr-2 w-fit px-3 p-2 text-sm bg-white flex rounded-xl ">
<p className="mb-0">&nbsp; </p>
</div> */}
{/* <div className="mr-2 w-fit px-3 p-2 text-sm bg-white flex rounded-xl ">
<p className="mb-0">Industrial </p>
</div>
*/}
<button onClick={changeLocale} className="mr-2 w-fit px-3 p-2 text-sm bg-white flex rounded-xl "> <button onClick={changeLocale} className="mr-2 w-fit px-3 p-2 text-sm bg-white flex rounded-xl ">
<p className="mb-0">{locale === "en" ? "Arabic" : "English"}</p> <p className="mb-0">{locale === "en" ? "عربی" : "English"}</p>
</button> </button>
</div> </div>
</div> </div>
<div className="w-full flex ltr "> <div className={`w-full flex p-2 ${locale === "en" ? "ltr" : "rtl"} `}>
<div className="size-[50px] relative bg-white rounded-lg flex flex-col items-center rounded-tl-2xl justify-center my-auto"> <div className="size-[75px] relative p-1 bg-white rounded-lg flex flex-col items-center rounded-tl-2xl justify-center my-auto">
<Image <Image
src={logo} src={logo}
fill fill
@ -156,50 +105,50 @@ const Navbar = ({ theme }) => {
</div> </div>
{NavBarData.map((e, index) => ( {items.map((item, index) => (
<>
<div <div
key={index} key={index}
className={` mx-2 px-2 w-fit text-sm p-7 ${1 className={` mx-2 px-2 w-fit text-sm p-7 ${1
? "bg-visa2-200 rounded-full !text-white text-shadow pb-1 relative" ? "bg-visa2-200 rounded-full !text-white text-shadow pb-1 relative"
: "" : ""
} `} } `}
onMouseEnter={() => { onMouseEnter={() => {
// setHoverItemNavbar(index); // setHoverItemNavbar(index);
setActiveStepNavbar(e.id); setActiveStepNavbar(item.documentId);
}} }}
onMouseLeave={() => { onMouseLeave={() => {
// setHoverItemNavbar(-1); // setHoverItemNavbar(-1);
setActiveStepNavbar(null); setActiveStepNavbar(null);
}} }}
// onClick={() => context.setOpenNavBarServices(false)} // onClick={() => context.setOpenNavBarServices(false)}
>
<Link
href={`${item.link}`}
className={` whitespace-nowrap ${isScrolled ? "text-black" : "text-white"} ${theme == 1 ? "text-sm !text-black" : " "
}`}
> >
<Link {item.title}
href={`${e.href}`} {item.children.length > 0 && ""}
className={` whitespace-nowrap ${isScrolled ? "text-black" : "text-white"} ${theme == 1 ? "text-sm !text-black" : " " </Link>
}`}
>
{e.name}
{e.children.length > 0 && ""}
</Link>
{e.children.length > 0 && e.id == activeStepNavbar && ( {item.children.length > 0 && item.documentId == activeStepNavbar && (
<ul className="absolute left-0 mt-2 w-48 bg-white text-black rounded-lg shadow-lg z-50 overflow-hidden"> <ul className="absolute left-0 mt-2 w-48 bg-white text-black rounded-lg shadow-lg z-50 overflow-hidden">
{ {
e.children.map((c, index) => ( item.children.map((c, index) => (
<li key={index} className="px-4 py-2 hover:bg-gray-100 cursor-pointer"> <li key={index} className="px-4 py-2 hover:bg-gray-100 cursor-pointer">
<Link href={c.href}> <Link href={c.link}>
{c.name} {c.title}
</Link> </Link>
</li> </li>
)) ))
} }
</ul>
)}
</div>
</ul>
)}
</div>
</>
))} ))}
</div> </div>
@ -219,25 +168,26 @@ const Navbar = ({ theme }) => {
}`} }`}
> >
<div <div
className={`bg-gray-100 flex justify-between rtl p-3 ${isScrolled className={`bg-gray-100 flex justify-between items-center rtl p-3 ${isScrolled
? " rounded-bl-xl rounded-br-[40px]" ? " rounded-bl-xl rounded-br-[40px]"
: "mx-2 rounded-xl rounded-br-[40px]" : "mx-2 rounded-xl rounded-br-[40px]"
}`} }`}
> >
{/* <Link href={"/"} className="w-full"> */} {/* <Link href={"/"} className="w-full"> */}
<div className=" w-full mx-1 flex "> <div className=" w-full mx-1 flex items-center ">
<div className="xs:w-[60px] md:w-[100px] xs:mt-[5px] mt-[12px] "> <button onClick={changeLocale} className="mr-2 w-fit px-3 p-2 h-fit text-sm bg-white flex rounded-xl ">
<Image <p className="mb-0">{locale === "en" ? "عربی" : "English"}</p>
src={logo} </button>
width={500} </div>
height={500} <div className=" ">
alt="llc" <Image
className="mx-auto" src={logo}
/> width={200}
</div> height={200}
<button onClick={changeLocale} className="mr-2 w-fit px-3 p-2 text-sm bg-white flex rounded-xl "> alt="llc"
<p className="mb-0">{locale === "en" ? "Arabic" : "English"}</p> className="mx-auto"
</button> />
</div> </div>
{/* </Link> */} {/* </Link> */}
<div <div
@ -255,7 +205,7 @@ const Navbar = ({ theme }) => {
<path <path
d="M160.75 124C152.328 124 145.5 117.172 145.5 108.75V108.75C145.5 100.327 152.328 93.4995 160.75 93.4995L219.75 93.4995C228.172 93.4995 235 100.327 235 108.75V108.75C235 117.172 228.172 124 219.75 124L160.75 124ZM88.4999 26.4998C81.3202 26.4998 75.5 20.6795 75.5 13.4999V13.4999C75.5 6.32025 81.3202 0.499997 88.4999 0.499997L222 0.499997C229.18 0.499997 235 6.32025 235 13.4999V13.4999C235 20.6795 229.18 26.4998 222 26.4998L88.4999 26.4998ZM13 72.4996C6.09643 72.4996 0.499998 66.9031 0.499997 59.9996V59.9996C0.499997 53.096 6.09643 47.4996 13 47.4996L222.5 47.4996C229.404 47.4996 235 53.096 235 59.9996V59.9996C235 66.9031 229.404 72.4996 222.5 72.4996L13 72.4996Z" d="M160.75 124C152.328 124 145.5 117.172 145.5 108.75V108.75C145.5 100.327 152.328 93.4995 160.75 93.4995L219.75 93.4995C228.172 93.4995 235 100.327 235 108.75V108.75C235 117.172 228.172 124 219.75 124L160.75 124ZM88.4999 26.4998C81.3202 26.4998 75.5 20.6795 75.5 13.4999V13.4999C75.5 6.32025 81.3202 0.499997 88.4999 0.499997L222 0.499997C229.18 0.499997 235 6.32025 235 13.4999V13.4999C235 20.6795 229.18 26.4998 222 26.4998L88.4999 26.4998ZM13 72.4996C6.09643 72.4996 0.499998 66.9031 0.499997 59.9996V59.9996C0.499997 53.096 6.09643 47.4996 13 47.4996L222.5 47.4996C229.404 47.4996 235 53.096 235 59.9996V59.9996C235 66.9031 229.404 72.4996 222.5 72.4996L13 72.4996Z"
fill="black" fill="black"
fill-opacity="0.73" fillOpacity="0.73"
/> />
</svg> </svg>
</div> </div>
@ -309,7 +259,7 @@ const Navbar = ({ theme }) => {
className="mx-auto" className="mx-auto"
/> />
</div> </div>
{/* </Link> */} {/* </Link> */}
@ -319,13 +269,13 @@ const Navbar = ({ theme }) => {
<div className="mx-3"> <div className="mx-3">
<div> <div>
{NavBarData.map((e, index) => ( {items.map((e, index) => (
<> <Fragment key={index}>
<motion.div <motion.div
custom={index} custom={index}
animate="visible" animate="visible"
variants={toLeft} variants={toLeft}
key={index}
> >
{/* <Link href={e.url}> */} {/* <Link href={e.url}> */}
<> <>
@ -335,12 +285,12 @@ const Navbar = ({ theme }) => {
if (e.children.length > 0) { if (e.children.length > 0) {
setResponsiveNavBarItemStep(index); setResponsiveNavBarItemStep(index);
setActiveStepNavbar(e.id); setActiveStepNavbar(e.documentId);
} }
}} }}
> >
<span> <span>
{e.name} {e.title}
<small className="absolute left-0 mx-7 "> <small className="absolute left-0 mx-7 ">
{e.children.length > 0 ? ( {e.children.length > 0 ? (
<div> <div>
@ -374,23 +324,23 @@ const Navbar = ({ theme }) => {
{responsiveNavBarItemStep === index && {responsiveNavBarItemStep === index &&
e.children.length > 0 && e.children.length > 0 &&
e.id == activeStepNavbar && ( e.documentId == activeStepNavbar && (
<div> <div>
<div className="my-3 "> <div className="my-3 ">
{e.children.map((s, index) => ( {e.children.map((s, index) => (
// <Link href={s.url} key={index}> <Link href={s.link} key={index}>
<div <div
className="bg-primary-50 rounded-lg my-1 p-2 " className="bg-primary-50 rounded-lg my-1 p-2 "
key={index} key={index}
> >
<p className="mb-0 text-sm">{s.name}</p> <p className="mb-0 text-sm">{s.name}</p>
</div> </div>
// </Link> </Link>
))} ))}
</div> </div>
</div> </div>
)} )}
</> </Fragment>
))} ))}
</div> </div>
</div> </div>

View File

@ -0,0 +1,17 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import RcPagination from "rc-pagination";
import "rc-pagination/assets/index.css";
const Paginate = ({ pageInfo }) => {
const router = useRouter()
const pathname = usePathname()
const onChange = (page) => {
router.push(pathname + `?page=${page}`);
}
return <RcPagination onChange={onChange} current={pageInfo.page} total={pageInfo.total} pageSize={pageInfo.pageSize} />;
};
export default Paginate;

View File

@ -0,0 +1,20 @@
import Image from "next/image"
import Link from "next/link"
export default function BrandInfo({ brand }) {
return (
<Link href={`/products/${brand.slug}`} className="
flex items-center flex-col gap-2 ">
<Image
src={brand.image.url || "/placeholder.svg"}
alt={brand.image.alternativeText}
width={100}
height={100}
className="object-contain rounded-full overflow-hidden border aspect-square"
/>
<span className="text-lg font-semibold">{brand.title}</span>
</Link>
)
}

View File

@ -0,0 +1,16 @@
export default function ProductDescription({ description }) {
return (
<div>
{
description &&
<>
<h2 className="text-2xl font-bold mb-4">Product Description</h2>
<div className="prose max-w-none">
<p className="content" dangerouslySetInnerHTML={{ __html: description }}></p>
</div>
</>
}
</div>
)
}

View File

@ -0,0 +1,97 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { ChevronLeft, ChevronRight, X } from "lucide-react";
export default function ProductGallery({ images }) {
const [selectedImage, setSelectedImage] = useState(images[0]);
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxIndex, setLightboxIndex] = useState(0);
const openLightbox = (index) => {
setLightboxIndex(index);
setLightboxOpen(true);
};
const closeLightbox = () => {
setLightboxOpen(false);
};
const nextImage = () => {
setLightboxIndex((prevIndex) => (prevIndex + 1) % images.length);
};
const prevImage = () => {
setLightboxIndex((prevIndex) => (prevIndex - 1 + images.length) % images.length);
};
return (
<div className="max-w-[500px] lg:min-w-[500px]">
<div className="aspect-w-1 aspect-h-1 w-full rounded-lg overflow-hidden">
<Image
src={selectedImage.url || "/placeholder.svg"}
alt={selectedImage.alternativeText ?? ""}
width={500}
height={500}
className="object-contain cursor-pointer aspect-square "
onClick={() => openLightbox(images.findIndex((img) => img.documentId === selectedImage.documentId))}
/>
</div>
<div className="mt-4 grid grid-cols-4 gap-1">
{images.slice(0,4).map((image, index) => (
<Image
key={image.documentId}
src={image.url || "/placeholder.svg"}
alt={image.alternativeText ?? ""}
width={100}
height={100}
className={`object-cover cursor-pointer rounded-lg overflow-hidden ${
selectedImage.documentId === image.documentId ? "ring-2 ring-primary" : ""
}`}
onClick={() => setSelectedImage(image)}
/>
))}
</div>
{lightboxOpen && (
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
<div className="relative max-w-4xl w-full">
{/* Close Button */}
<button
className="absolute top-4 right-4 p-2 rounded-full text-white hover:bg-gray-700 transition-colors"
onClick={closeLightbox}
>
<X className="h-6 w-6" />
</button>
{/* Lightbox Image */}
<Image
src={images[lightboxIndex].url || "/placeholder.svg"}
alt={images[lightboxIndex].alternativeText}
width={1200}
height={1200}
className="object-contain w-full h-full"
/>
{/* Previous Button */}
<button
className="absolute top-1/2 left-4 transform -translate-y-1/2 p-2 rounded-full text-white hover:bg-gray-700 transition-colors"
onClick={prevImage}
>
<ChevronLeft className="h-8 w-8" />
</button>
{/* Next Button */}
<button
className="absolute top-1/2 right-4 transform -translate-y-1/2 p-2 rounded-full text-white hover:bg-gray-700 transition-colors"
onClick={nextImage}
>
<ChevronRight className="h-8 w-8" />
</button>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,104 @@
"use client"
import { ShoppingCart } from "lucide-react";
import { HardHat } from "lucide-react";
import { Tag } from "lucide-react";
import { useLocale } from "next-intl";
import Link from "next/link";
import ContactModal from "../ContactUs";
import { useState } from "react";
import { useTranslations } from "next-intl";
export default function ProductInfo({ title, price, discount, showPrice, category,summery }) {
const locale = useLocale()
// Determine the currency symbol based on the locale
const currencySymbol = locale === "ar-OM" ? "ريال" : "$";
const CategoryIcon = category.slug === "fmcg" ? ShoppingCart : HardHat
const discountedPrice = discount ? price * (1 - discount / 100) : price;
const [open, setOpen] = useState(false);
const openModal = () => {
setOpen(true);
};
const closeModal = () => {
setOpen(false);
}
const t = useTranslations("ContactModal")
return (
<div className="">
<ContactModal close={closeModal} open={open} />
<h1 className="text-3xl font-bold">{title}</h1>
<Link href={`/products/${category.slug}`} className="mt-4 flex items-center">
<span className={`inline-flex items-center rounded-md bg-gray-100 px-2.5 py-0.5 text-sm font-medium text-gray-800 ltr`}>
<CategoryIcon className="h-4 w-4 mx-1" />
{category.title}
</span>
</Link>
<p>
{
summery
}
</p>
{showPrice && (
<div className="mt-4">
{discount ? (
<div className="flex items-center">
<span className="text-2xl font-bold text-primary">
{locale === "ar-OM" ? (
<>
{discountedPrice.toFixed(2)} {currencySymbol}
</>
) : (
<>
{currencySymbol}
{discountedPrice.toFixed(2)}
</>
)}
</span>
<span className="ml-2 text-lg text-gray-500 line-through">
{locale === "ar-OM" ? (
<>
{price.toFixed(2)} {currencySymbol}
</>
) : (
<>
{currencySymbol}
{price.toFixed(2)}
</>
)}
</span>
<span className="ml-2 inline-flex items-center rounded-md bg-red-100 px-2.5 py-0.5 text-sm font-medium text-red-800">
{discount}% OFF
</span>
</div>
) : (
<span className="text-2xl font-bold text-primary">
{locale === "ar-OM" ? (
<>
{price.toFixed(2)} {currencySymbol}
</>
) : (
<>
{currencySymbol}
{price.toFixed(2)}
</>
)}
</span>
)}
</div>
)}
<div className="mt-10">
{/* Replaced Button with Tailwind CSS */}
<button onClick={openModal} className="w-full lg:w-fit px-8 ltr flex items-center justify-center py-2 bg-primary text-white rounded-md hover:bg-primary-dark transition-colors">
<ShoppingCart className="mr-2 h-4 w-4" /> {t("cta")}
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,18 @@
export default function ProductProperties({ properties }) {
return (
<div>
<h2 className="text-2xl font-bold mb-4">Product Specifications</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>
</div>
))}
</dl>
</div>
)
}

View File

@ -0,0 +1,86 @@
"use client"
import React, { useEffect } from 'react'
import graphql from 'src/utils/graphql'
import ProductCarousel from '../Carousel/ProductCarousel'
import { useTranslations } from 'next-intl'
const gql = `
query Products_connection(
$categoryId: ID
$brandId: ID
$locale: I18NLocaleCode
) {
products_connection(
filters: {
or: [
{ category: { documentId: { eqi: $categoryId } } }
{ brand: { documentId: { eqi: $brandId } } }
]
}
sort: ["createdAt:desc"]
locale: $locale
) {
nodes {
documentId
title
price
slug
discount
showPrice
images {
documentId
alternativeText
url
}
brand {
title
documentId
image {
documentId
alternativeText
url
}
slug
}
category {
documentId
title
slug
}
}
}
}
`
const ProductRelated = ({ category, brand }) => {
const [products, setProducts] = React.useState(null)
const fetchProducts = async () => {
const { products_connection: { nodes } } = await graphql(gql, {
categoryId: "",
brandId: brand.documentId,
locale: "en"
})
setProducts(nodes)
}
useEffect(() => {
fetchProducts()
}, [])
const t = useTranslations("PDP")
return (
<div>
{
products && products.length > 0 && (
<ProductCarousel products={products} title={t("productRelated")} showMoreLink={`/products/${brand.slug}`} />
)
}
</div>
)
}
export default ProductRelated

View File

@ -1,17 +1,23 @@
{ {
"HomePage": { "HomePage": {
"SEO":{
"title":"خدمات الأفق المتقدمة ش.م.م",
"description":"خدمات الأفق المتقدمة ش.م.م تقدم حلول سلاسل التوريد بالجملة للمنظفات والمواد الغذائية بجودة عالية وتركز على التميز والموثوقية"
},
"AboutUs": { "AboutUs": {
"brandName": "منتجات خدمات الأفق المتقدمة ش.م.م", "brandName": " خدمات الأفق المتقدمة ش.م.م",
"description": [ "description": [
"شريكك الموثوق لحلول سلاسل التوريد بالجملة", "شريككم الموثوق لحلول سلاسل التوريد بالجملة",
"خدمات الأفق المتقدمة هي شركة تجارية رائدة متخصصة في حلول سلاسل التوريد بالجملة لمجموعة متنوعة من المنتجات، بما في ذلك المنظفات عالية الجودة والمواد الغذائية. مع سنوات من الخبرة والعلاقات الواسعة في الصناعة، نفخر بالتزامنا بالتميز والموثوقية ورضا العملاء.", "خدمات الأفق المتقدمة هي شركة تجارية رائدة متخصصة في حلول سلاسل التوريد بالجملة لمجموعة متنوعة من المنتجات، بما في ذلك المنظفات عالية الجودة والمواد الغذائية. مع سنوات من الخبرة والعلاقات الواسعة في الصناعة، نفخر بالتزامنا بالتميز والموثوقية ورضا العملاء.",
"في AHS، نفهم تعقيدات سلسلة التوريد ونسعى لتبسيط العملية لشركائنا. شبكتنا الواسعة من الموردين والمصنعين تمكننا من الحصول على منتجات عالية الجودة بأسعار تنافسية، مما يضمن حصولك على أفضل قيمة لاستثمارك." "في AHS، نفهم تعقيدات سلسلة التوريد ونسعى لتبسيط العملية لشركائنا. شبكتنا الواسعة من الموردين والمصنعين تمكننا من الحصول على منتجات عالية الجودة بأسعار تنافسية، مما يضمن حصولكم على أفضل قيمة لاستثماركم."
] ]
}, },
"Sides": { "Sides": {
"title": "اكتشف خبراتنا", "title": "اكتشف خبراتنا",
"fmcg": { "fmcg": {
"title": "السلع الاستهلاكية سريعة الحركة", "title": "FMCG",
"subtitle": "السلع الاستهلاكية سريعة الحركة", "subtitle": "السلع الاستهلاكية سريعة الحركة",
"description": "قسم السلع الاستهلاكية سريعة الحركة لدينا متخصص في توزيع وتسويق المنتجات الاستهلاكية اليومية. نضمن سرعة دوران المنتجات، وسلاسل توريد فعالة، واستراتيجيات تسويقية مبتكرة لتلبية الطلبات المتغيرة باستمرار للمستهلكين." "description": "قسم السلع الاستهلاكية سريعة الحركة لدينا متخصص في توزيع وتسويق المنتجات الاستهلاكية اليومية. نضمن سرعة دوران المنتجات، وسلاسل توريد فعالة، واستراتيجيات تسويقية مبتكرة لتلبية الطلبات المتغيرة باستمرار للمستهلكين."
}, },
@ -20,9 +26,53 @@
"subtitle": "نبني المستقبل", "subtitle": "نبني المستقبل",
"description": "ذراع الإنشاءات لدينا مكرس لخلق بنية تحتية دائمة وحلول بناء مبتكرة. من المشاريع السكنية إلى المجمعات التجارية، نقدم الخبرة والجودة والاستدامة في كل مشروع بناء." "description": "ذراع الإنشاءات لدينا مكرس لخلق بنية تحتية دائمة وحلول بناء مبتكرة. من المشاريع السكنية إلى المجمعات التجارية، نقدم الخبرة والجودة والاستدامة في كل مشروع بناء."
} }
},
"products": {
"title": [
"منتجات Active",
"الإنشاءات"
]
} }
}, },
"Footer": { "Footer": {
"address": "مسقط، السيب، موالح الشمالية، شارع الموج، رقم المجمع ٣٥٨، رقم المبنى ١/٢٠٣، مكتب رقم ٥٣، صندوق البريد: ٥٧ء" "address": "مسقط، السيب، موالح الشمالية، شارع الموج، رقم المجمع ٣٥٨، رقم المبنى ١/٢٠٣، مكتب رقم ٥٣، صندوق البريد: ٥٧ء"
},
"PLP": {
"title": "المنتجات",
"subtitle": "استكشاف مجموعتنا من المنتجات",
"filter": {
"title": "المرشحات",
"categories": "الفئات",
"brands": "العلامات التجارية"
}
},
"PDP": {
"contactUs": "اتصل بنا",
"home": "الصفحة الرئيسية",
"productDetails": "تفاصيل المنتج",
"productDescription": "وصف المنتج",
"productSpecification": "مواصفات المنتج",
"productRelated": "المنتجات ذات الصلة"
},
"ContactModal": {
"title": "اتصل بنا",
"email": "البريد الإلكتروني",
"emailPlaceholder": "أدخل بريدك الإلكتروني",
"company": "الشركة",
"companyPlaceholder": "أدخل اسم شركتك",
"message": "الرسالة",
"messagePlaceholder": "أدخل رسالتك",
"sendButton": "إرسال الرسالة",
"closeButton": "إغلاق",
"error": {
"fillAllFields": "يرجى ملء جميع الحقول",
"invalidEmail": "يرجى إدخال عنوان بريد إلكتروني صالح"
},
"success": "تم إرسال الرسالة بنجاح",
"cta": "اتصل بنا"
},
"Utils": {
"showMoreLink": "عرض المزيد",
"moreDetail": "المزيد من التفاصيل"
} }
} }

View File

@ -1,5 +1,9 @@
{ {
"HomePage": { "HomePage": {
"SEO":{
"title":"Advanced Horizon Services LLC",
"description":"Advanced Horizon Services LLC offers reliable wholesale supply chain solutions for high-quality detergents and food products with a focus on excellence."
},
"AboutUs": { "AboutUs": {
"brandName": "ADVANCED HORIZON SERVICES LLC Products", "brandName": "ADVANCED HORIZON SERVICES LLC Products",
"description": [ "description": [
@ -8,6 +12,7 @@
" At AHS, we understand the complexities of the supply chain and strive to simplify the process for our partners. Our extensive network of suppliers and manufacturers allows us to source top-notch products at competitive prices, ensuring that you receive the best value for your investment." " At AHS, we understand the complexities of the supply chain and strive to simplify the process for our partners. Our extensive network of suppliers and manufacturers allows us to source top-notch products at competitive prices, ensuring that you receive the best value for your investment."
] ]
}, },
"Sides": { "Sides": {
"title": "Discover Our Expertise", "title": "Discover Our Expertise",
"fmcg": { "fmcg": {
@ -20,9 +25,53 @@
"subtitle": "Building the Future", "subtitle": "Building the Future",
"description": "Our Construction arm is dedicated to creating lasting infrastructure and innovative building solutions.From residential projects to commercial complexes, we bring expertise, quality, and sustainability to every construction endeavor." "description": "Our Construction arm is dedicated to creating lasting infrastructure and innovative building solutions.From residential projects to commercial complexes, we bring expertise, quality, and sustainability to every construction endeavor."
} }
},
"products":{
"title":[
"Active Products",
"Constructions"
]
} }
}, },
"Footer": { "Footer": {
"address": " Unit No. 53, Building No 203, Complex No.308 , Mawaleh North ,Road No. 108 Al Mouj Street , Muscat Uman.F.o.0ox.our.F.C.ll Muscat Airport" "address": " Unit No. 53, Building No 203, Complex No.308 , Mawaleh North ,Road No. 108 Al Mouj Street , Muscat Oman.F.o.0ox.our.F.C.ll Muscat Airport"
},
"PLP":{
"title":"Products",
"subtitle":"Explore Our Range of Products",
"filter":{
"title":"Filters",
"categories":"Categories",
"brands":"Brands"
}
},
"PDP":{
"contactUs":"Contact Us",
"home":"Home",
"productDetails":"Product Details",
"productDescription":"Product Description",
"productSpecification":"Product Specification",
"productRelated":"Frequently Bought Together"
},
"ContactModal": {
"title": "Contact Us",
"email": "Email",
"emailPlaceholder": "Enter your email",
"company": "Company",
"companyPlaceholder": "Enter your company name",
"message": "Message",
"messagePlaceholder": "Enter your message",
"sendButton": "Send Message",
"closeButton": "Close",
"error":{
"fillAllFields":"Please fill all fields",
"invalidEmail":"Please enter a valid email address"
},
"success":"Message sent successfully",
"cta":"Contact Us"
},
"Utils":{
"showMoreLink":"Show more",
"moreDetail":"More detail"
} }
} }

View File

@ -2,6 +2,16 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html[lang="en"] {
}
/* Arabic content (rtl) uses Tajawal */
html[lang="ar-OM"] {
}
@layer components { @layer components {
.btn { .btn {
@apply py-2 px-4 rounded-xl transition-all shadow-lg border-2; @apply py-2 px-4 rounded-xl transition-all shadow-lg border-2;

View File

@ -1,27 +1,164 @@
import React from "react"; "use client"
import CardNormal from "src/components/Cards/CardNormal/page"; import React, { useEffect } from "react";
import CardNormal, { ProductCardSkeleton } from "src/components/Cards/CardNormal";
import Paginate from "src/components/Paginate";
import graphql from "src/utils/graphql";
import Filters from "./Filters";
import { useParams, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
const brandAndCategoriesGQL = `
query Categories($locale: I18NLocaleCode) {
categories(locale: $locale,pagination: { start: 0, limit: 50 }) {
documentId
slug
title
}
brands(locale: $locale, pagination: { start: 0, limit: 50 }) {
title
documentId
slug
}
}
const Content = ({ products }) => { `
const productsGQL = `
query Products_connection(
$locale: I18NLocaleCode
$page: Int
$pageSize: Int
$category: String
$brand: String
) {
products_connection(
pagination: { page: $page, pageSize: $pageSize }
filters: {
or: [
{ category: { slug: { eqi: $category } } }
{ brand: { slug: { eqi: $brand } } }
]
}
locale: $locale
) {
nodes {
documentId
title
images {
documentId
alternativeText
url
}
category {
title
documentId
slug
}
brand {
documentId
title
slug
image {
documentId
alternativeText
url
}
}
slug
}
pageInfo {
total
page
pageSize
pageCount
}
}
}
`
const Content = () => {
const params = useParams()
const searchParams = useSearchParams()
const { category } = params
const [loading, setLoading] = React.useState(true)
const [pageInfo, setPageInfo] = React.useState({ page: 1, pageSize: 12, total: 0, pageCount: 0 })
const [products, setProducts] = React.useState([])
const [brands, setBrands] = React.useState([])
const [categories, setCategories] = React.useState([])
const getBrandsAndCategories = async () => {
const { brands, categories } = await graphql(brandAndCategoriesGQL, { locale: "en" });
setBrands(brands)
setCategories(categories)
}
const getProducts = async () => {
setLoading(true)
const page = parseInt(searchParams.get("page")) || 1
const { products_connection: { nodes, pageInfo } } = await graphql(productsGQL, {
locale: "en",
page: page,
pageSize: 12,
brand: category,
category: category
})
setProducts(nodes)
setPageInfo(pageInfo)
setLoading(false)
}
useEffect(() => {
getBrandsAndCategories()
}, [])
useEffect(() => {
getProducts(category)
}, [searchParams])
console.log("products", products)
const t = useTranslations("PLP")
return ( return (
<div className="px-2 pb-20"> <>
<div className="flex"> {
<div> brands && brands.length > 0 && categories && categories.length > 0 &&
<h1 className="text-3xl font-bold text-left "> <Filters brands={brands} categories={categories} />
Product Listing Page }
</h1> <div className="flex flex-col w-full items-center justify-center ">
<p className="mb-0 text-sm text-gray-400 xs:text-center lg:text-left"> <div className="px-2 pb-20 w-full">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Corporis,{" "} <div className="flex">
</p> <div>
</div> <h1 className="text-3xl font-bold ">
</div> {t("title")}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 mt-10"> </h1>
{products.map((product, index) => ( <p className="mb-0 text-sm text-gray-400 xs:text-center ">
<div key={index} className="relative"> {t("subtitle")}
<CardNormal data={product} priority /> </p>
</div>
</div> </div>
))} <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-10 w-full">
{!loading && products.map((product, index) => (
<div key={index} className="relative">
<CardNormal product={product} priority />
</div>
))}
{
loading && Array.from({ length: 12 }).map((_, index) => (
<div key={index} className="relative">
<ProductCardSkeleton />
</div>
))
}
</div>
</div>
<Paginate pageInfo={pageInfo} />
</div> </div>
</div> </>
); );
}; };

View File

@ -1,46 +1,72 @@
import React from "react"; "use client"
import { useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
const Filters = () => { const CheckboxGroup = ({ title, items, selectedItem, onItemSelect }) => {
return (
<div className="mb-6">
<h3 className="text-lg font-semibold mb-2">{title}</h3>
{items.map((item) => (
<div key={item.documentId} className="flex items-center mb-2">
<input
type="checkbox"
id={item.documentId}
checked={selectedItem === item.documentId}
onChange={() => onItemSelect(item.documentId)}
className="mx-2"
/>
<label htmlFor={item.documentId} className="text-sm">
{item.title}
</label>
</div>
))}
</div>
)
}
const Filters = ({ brands, categories }) => {
const [selectedCategory, setSelectedCategory] = useState(null)
const [selectedBrand, setSelectedBrand] = useState(null)
const params = useParams()
const router = useRouter()
const handleCategorySelect = (documentId) => {
setSelectedCategory((prevSelected) => (prevSelected === documentId ? null : documentId))
router.push("/products/"+(documentId ? `${categories.find(c=>c.documentId === documentId)?.slug}` : ""))
}
const handleBrandSelect = (documentId) => {
setSelectedBrand((prevSelected) => (prevSelected === documentId ? null : documentId))
router.push("/products/"+(documentId ? `${brands.find(b=>b.documentId === documentId)?.slug}` : ""))
}
useEffect(()=>{
const categoryOrBrand = params.category;
console.log("paramssssssssssssss", params.category,brands.find(b=>b.slug === categoryOrBrand),brands)
setSelectedBrand(brands.find(b=>b.slug === categoryOrBrand)?.documentId)
setSelectedCategory(categories.find(c=>c.slug === categoryOrBrand)?.documentId)
},[params])
const t = useTranslations("PLP.filter")
return ( return (
<aside className="w-full md:w-2/5 p-6 md:mr-6 mb-6 md:mb-0"> <aside className="w-full md:w-2/5 p-6 md:mr-6 mb-6 md:mb-0">
<h2 className="text-2xl font-bold mb-6 text-gray-800">Filters</h2> <h2 className="text-2xl font-bold mb-6 text-gray-800">{t("title")}</h2>
<CheckboxGroup
title="Categories"
items={categories}
selectedItem={selectedCategory}
onItemSelect={handleCategorySelect}
/>
<CheckboxGroup title="Brands" items={brands} selectedItem={selectedBrand} onItemSelect={handleBrandSelect} />
<div className="mb-6">
<label className="block text-sm font-semibold mb-2 text-gray-700">
Price Range
</label>
<input type="range" min="0" max="500000" className="w-full" />
<div className="flex justify-between text-gray-600 text-sm mt-1">
<span>0 $</span>
<span>500,000 $ </span>
</div>
</div>
<div className="mb-6">
<label className="block text-sm font-semibold mb-2 text-gray-700">
Category
</label>
<select className="w-full border p-3 rounded-sm bg-gray-100 text-sm">
<option>All</option>
<option>Skincare</option>
<option>Haircare</option>
<option>Bodycare</option>
</select>
</div>
<div className="mb-6">
<label className="block text-sm font-semibold mb-2 text-gray-700">
Availability
</label>
<div className="flex items-center">
<input type="checkbox" className="mr-2" />
<span className="text-gray-600 !text-sm">In Stock Only</span>
</div>
</div>
<button className="w-full bg-blue-600 text-white py-3 rounded-lg text-sm font-semibold hover:bg-blue-800 transition-all">
Apply Filters
</button>
</aside> </aside>
); );
}; };

View File

@ -1,19 +1,52 @@
import React from "react"; import React from "react";
import CardNormal from "src/components/Cards/CardNormal/page"; import CardNormal from "src/components/Cards/CardNormal";
import Navbar from "src/components/NavBar"; import Navbar from "src/components/NavBar";
import Content from "./components/Content"; import Content from "./components/Content";
import Filters from "./components/Filters"; import Filters from "./components/Filters";
import Footer from "../Landing/components/Footer"; import Footer from "../Landing/components/Footer";
import graphql from "src/utils/graphql";
import { getLocale } from "next-intl/server";
// query Products($locale:I18NLocaleCode,$page:Int,$pageSize:Int,$title:String,$categoryTitle:String,$brandTitle:String) {
// products(
// pagination: { page: $page, pageSize: $pageSize }
// filters: {
// category: { title: { contains: $categoryTitle } }
// brand: { title: { eq: $brandTitle } }
// title: { eq: $title }
// }
// locale: $locale
// ) {
// title
// documentId
// price
// brand {
// title
// documentId
// slug
// }
// category {
// documentId
// title
// slug
// }
// }
// }
const CategoriesData = async ({ category }) => {
const locale = await getLocale()
const CategoriesData = ({ products }) => {
return ( return (
<> <>
<Navbar theme={1} />
<div className="xs:container-none md:container mx-auto p-4 flex flex-col md:flex-row mt-10"> <div className={`container-none md:container mx-auto p-4 flex flex-col md:flex-row mt-10 ${locale === "en" ? "ltr" : "rtl"}`}>
<Filters />
<Content products={products} /> <Content category={category} />
</div> </div>
<Footer />
</> </>
); );
}; };

View File

@ -7,37 +7,40 @@ const AboutUs = () => {
const t = useTranslations("HomePage.AboutUs") const t = useTranslations("HomePage.AboutUs")
const locale = useLocale() const locale = useLocale()
return ( return (
<div className=" min-h-[calc(100vh-108px)] flex flex-col items-center justify-center relative "> <div className=" min-h-[calc(100vh-108px)] flex flex-col items-center justify-center relative ">
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" className="absolute left-0 top-0 bottom-0 -z-10 right-0"> <div className="max-w-screen-xl mx-auto">
<pattern id="diagonalLines" width="10" height="10" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="0" y2="10" stroke="#e2e8f0" stroke-width="1" />
</pattern>
<rect width="100%" height="100%" fill="url(#diagonalLines)" />
</svg>
<div className="xs:px-3 md:px-10 md:container md:mx-auto ">
<div className="grid xs:grid-cols-1 lg:grid-cols-3 gap-5 items-center " dir={locale == "en" ? "rtl" : "ltr"} >
<div className="mx-auto">
<Image src={`/images/logo-${locale}.png?v=1`} width={350} height={350} className="xs:w-[200px] lg:w-auto" />
</div>
<div className="text-left relative mb-10 col-span-2">
<h2 className={
`text-4xl font-bold relative z-10 xs:text-center lg:text-left ${locale === "en" ? "lg:text-left" : "lg:text-right"}`
}>
{t("brandName")}
</h2>
<p className={`mb-0 text-base mt-5 text-center ${locale === "en" ? "lg:text-left" : "lg:text-right"} leading-7`}> <svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" className="absolute left-0 top-0 bottom-0 -z-10 right-0">
<span className="text-primary-800 font-semibold text-lg"> <pattern id="diagonalLines" width="10" height="10" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
{t("description.0")} <line x1="0" y1="0" x2="0" y2="10" stroke="#e2e8f0" stroke-width="1" />
</span> </pattern>
<br /> <rect width="100%" height="100%" fill="url(#diagonalLines)" />
<br /> </svg>
{t("description.1")} <br /> <div className="xs:px-3 md:px-10 md:container md:mx-auto ">
<br /> <div className="grid xs:grid-cols-1 lg:grid-cols-3 gap-5 items-center " dir={locale == "en" ? "rtl" : "ltr"} >
{t("description.2")} <div className="mx-auto">
</p> <Image src={`/images/logo-${locale}.png?v=1`} width={350} height={350} className="xs:w-[200px] lg:w-auto" />
{/* </div>
<div className="text-left relative mb-10 col-span-2">
<h2 className={
`text-4xl font-bold relative z-10 xs:text-center lg:text-left ${locale === "en" ? "lg:text-left" : "lg:text-right"}`
}>
{t("brandName")}
</h2>
<p className={`mb-0 text-base mt-5 text-center ${locale === "en" ? "lg:text-left" : "lg:text-right"} leading-7`}>
<span className="text-primary-800 font-semibold text-lg">
{t("description.0")}
</span>
<br />
<br />
{t("description.1")} <br />
<br />
{t("description.2")}
</p>
{/*
<div className="flex gap-2 mx-auto mt-5 w-full justify-center lg:justify-end"> <div className="flex gap-2 mx-auto mt-5 w-full justify-center lg:justify-end">
<button className="btn btn-primary text-base py-3 px-10 "> <button className="btn btn-primary text-base py-3 px-10 ">
{" "} {" "}
@ -48,6 +51,7 @@ const AboutUs = () => {
Industrial Industrial
</button> </button>
</div> */} </div> */}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -24,11 +24,12 @@ const CounterDetail = ({ stats }) => {
<h3 className="mb-0 text-4xl text-gray-300 font-bold"> <h3 className="mb-0 text-4xl text-gray-300 font-bold">
{s.title} {s.title}
</h3> </h3>
<p className="mb-0 text-6xl font-bold text-gray-500 mt-5 "><CountUp <p className="mb-0 text-6xl font-bold text-gray-500 mt-5">
start={startCounting ? 0 : undefined} <CountUp
end={s.stat} start={startCounting ? 0 : undefined}
duration={2.75} end={s.stat}
/></p> duration={2.75}
/></p>
</div> </div>
)) ))
} }

View File

@ -10,8 +10,8 @@ import { useTranslations } from "next-intl";
const Footer = () => { const Footer = () => {
const t = useTranslations("Footer") const t = useTranslations("Footer")
return ( return (
<div className="bg-secondary-700 "> <div className="bg-secondary-700 py-20 px-4 lg:px-0">
<div className="lg:w-7/12 xs:w-full mx-auto py-20"> {/* <div className="lg:w-7/12 xs:w-full mx-auto py-20">
<div className="flex justify-center"> <div className="flex justify-center">
<div className={`text-center w-ful`}> <div className={`text-center w-ful`}>
<> <>
@ -27,7 +27,7 @@ const Footer = () => {
</> </>
</div> </div>
</div> </div>
</div> </div> */}
<div className="flex flex-col gap-4 items-center text-sm text-gray-200 justify-center pb-20"> <div className="flex flex-col gap-4 items-center text-sm text-gray-200 justify-center pb-20">
<div className="flex gap-2"> <div className="flex gap-2">
<MapPinHouse size={20} /> <MapPinHouse size={20} />
@ -61,7 +61,7 @@ const Footer = () => {
</div> </div>
<p className="mb-0 text-sm text-gray-200 w-full text-center opacity-30 "> <p className="mb-0 text-sm text-gray-200 w-full text-center opacity-30 ">
© 2025. All rights reserved. <br /> © 2025. All rights reserved. <br />
am - Om Advanced Horizon Services LLC
</p> </p>
{/* <div className="flex justify-center w-full my-5 "> {/* <div className="flex justify-center w-full my-5 ">
<Link href={`/products`}> <Link href={`/products`}>

View File

@ -1,33 +1,82 @@
import { getLocale, getMessages } from "next-intl/server";
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React from "react";
import CardNormal from "src/components/Cards/CardNormal/page"; import CardNormal from "src/components/Cards/CardNormal";
import ProductCarousel from "src/components/Carousel/ProductCarousel"; import ProductCarousel from "src/components/Carousel/ProductCarousel";
import graphql from "src/utils/graphql";
const Products = ({ products }) => { const gql = `
query Products_connection($locale: I18NLocaleCode, $page: Int, $pageSize: Int,$category:String,$brand:String) {
products_connection(
pagination: { page: $page, pageSize: $pageSize }
locale: $locale
sort: ["createdAt:desc"]
filters: {
or:[
{brand: { slug: { eqi: $brand } }}
{category:{ slug: { eqi: $category } }}
]
}
) {
nodes {
title
documentId
images {
alternativeText
documentId
url
}
category {
documentId
title
slug
}
brand {
title
documentId
slug
image {
url
alternativeText
documentId
}
}
slug
}
}
}
`
const getProducts = async (brand = "", category = "") => {
const locale = await getLocale()
const products = await graphql(gql, {
page: 1,
pageSize: 20,
locale: "en",
brand: brand,
category: category
})
console.log(products) return products.products_connection.nodes;
}
const Products = async () => {
const products1 = await getProducts("active")
const products2 = await getProducts("", "construction")
const t = await getMessages()
return ( return (
<div className="my-[120px]"> <div className="my-20 max-w-screen-xl mx-auto">
<div className="xs:px-3 md:px-10 md:container md:mx-auto mb-10">
<div className="text-left relative mb-10">
<h2 className="text-[30px] font-bold relative z-10 xs:text-center lg:text-left">
</h2>
<p className="mb-0 text-sm text-gray-400 xs:text-center lg:text-left">
</p>
</div>
<div> <div>
<ProductCarousel products={products.filter(p => p.brand.title === "active")} subtitle={""} title={"Active Products"} /> <ProductCarousel products={products1} subtitle={""} title={t.HomePage.products.title[0]} showMoreLink={"/products/active"} />
</div> </div>
<div> <div>
<ProductCarousel products={products.filter(p => p.brand.title === "savin")} subtitle={""} title={"Savin Products"} /> <ProductCarousel products={products2} subtitle={""} title={t.HomePage.products.title[1]} showMoreLink={"/products/constructions"} />
</div> </div>
{/* <div className="grid grid-cols-1 lg:grid-cols-3 gap-5"> {/* <div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
{products?.map((product, index) => ( {products?.map((product, index) => (
<div key={index} className="relative"> <div key={index} className="relative">
<CardNormal product={product} priority /> <CardNormal product={product} priority />
@ -35,7 +84,7 @@ const Products = ({ products }) => {
))} ))}
</div> */} </div> */}
{/* <div className="flex justify-center"> {/* <div className="flex justify-center">
<Link href={"categories/Product-20Listing-Page"}> <Link href={"categories/Product-20Listing-Page"}>
<button className="btn btn-primary px-10 text-sm "> <button className="btn btn-primary px-10 text-sm ">
{" "} {" "}
@ -43,8 +92,8 @@ const Products = ({ products }) => {
</button> </button>
</Link> </Link>
</div> */} </div> */}
</div>
</div> </div>
); );
}; };

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { ShoppingCart, Building2 } from 'lucide-react'; // Assuming you're using lucide-react for icons import { ShoppingCart, Building2 } from 'lucide-react'; // Assuming you're using lucide-react for icons
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import Link from 'next/link';
const Sides = () => { const Sides = () => {
@ -13,7 +14,7 @@ const Sides = () => {
</h2> </h2>
<div className="grid gap-6 lg:grid-cols-2"> <div className="grid gap-6 lg:grid-cols-2">
{/* First Card */} {/* First Card */}
<div className="flex flex-col items-center text-center bg-white shadow-lg rounded-lg p-6"> <Link href={"/products/fmcg"} className="flex flex-col items-center text-center bg-white shadow-lg rounded-lg p-6 hover:scale-105 transition-all">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<ShoppingCart className="w-12 h-12 mb-4 text-primary" /> <ShoppingCart className="w-12 h-12 mb-4 text-primary" />
<h3 className="text-2xl font-bold">{t("fmcg.title")}</h3> <h3 className="text-2xl font-bold">{t("fmcg.title")}</h3>
@ -24,10 +25,10 @@ const Sides = () => {
{t("fmcg.description")} {t("fmcg.description")}
</p> </p>
</div> </div>
</div> </Link>
{/* Second Card */} {/* Second Card */}
<div className="flex flex-col items-center text-center bg-white shadow-lg rounded-lg p-6"> <Link href={"/products/construction"} className="flex flex-col items-center text-center bg-white shadow-lg rounded-lg p-6 hover:scale-105 transition-all">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<Building2 className="w-12 h-12 mb-4 text-primary" /> <Building2 className="w-12 h-12 mb-4 text-primary" />
<h3 className="text-2xl font-bold">{t("construction.title")}</h3> <h3 className="text-2xl font-bold">{t("construction.title")}</h3>
@ -38,7 +39,7 @@ const Sides = () => {
{t("construction.description")} {t("construction.description")}
</p> </p>
</div> </div>
</div> </Link>
</div> </div>
</div> </div>
</section> </section>

View File

@ -7,37 +7,6 @@ import graphql from "src/utils/graphql";
import CounterDetail from "./components/CounterDetail"; import CounterDetail from "./components/CounterDetail";
import { getLocale } from "next-intl/server"; import { getLocale } from "next-intl/server";
const gql = `
query Products_connection($locale: I18NLocaleCode, $page: Int, $pageSize: Int) {
products_connection(
pagination: { page: $page, pageSize: $pageSize }
locale: $locale,
sort: ["createdAt:asc"]
) {
nodes {
title
documentId
images {
alternativeText
documentId
url
}
category {
documentId
title
slug
}
brand {
title
documentId
slug
}
slug
}
}
}
`
const gql_stats = ` const gql_stats = `
query Stats($locale:I18NLocaleCode) { query Stats($locale:I18NLocaleCode) {
@ -52,8 +21,8 @@ query Stats($locale:I18NLocaleCode) {
const getStats = async () => { const getStats = async () => {
const locale =await getLocale() const locale = await getLocale()
console.log("locale",locale) console.log("locale", locale)
const stats = await graphql(gql_stats, { const stats = await graphql(gql_stats, {
locale: locale locale: locale
}) })
@ -61,33 +30,23 @@ const getStats = async () => {
return stats.stats return stats.stats
} }
const getProducts = async () => {
const locale =await getLocale()
const products = await graphql(gql, {
page: 1,
pageSize: 20,
locale:locale
})
return products.products_connection.nodes;
}
const Landing = async () => { const Landing = async () => {
const products = await getProducts()
const stats = await getStats() const stats = await getStats()
return ( return (
<div className=" text-center text-6xl"> <div className=" text-center text-6xl">
{" "} {" "}
<Navbar theme={1} />
{/* <HeroSection /> */} {/* <HeroSection /> */}
<AboutUs /> <AboutUs />
<Sides /> <Sides />
<CounterDetail stats={stats} /> <CounterDetail stats={stats} />
<Products products={products} /> <Products />
{/* <WhyHorizon/> */} {/* <WhyHorizon/> */}
<Footer />
</div> </div>
); );
}; };

View File

@ -1,107 +0,0 @@
import React from "react";
import test from "../../assets/images/product/1.png";
import Navbar from "src/components/NavBar";
import Image from "next/image";
import GalleryBox from "src/components/GalleryBox";
import Footer from "../Landing/components/Footer";
const Product = ({ data }) => {
const testDataCk =
"<h1>Hydrating Cream</h1><p><strong>A deeply moisturizing cream</strong> that keeps your skin hydrated all day long. Formulated with natural ingredients, this cream penetrates deep into the skin, providing long-lasting moisture and nourishment.</p><h2>Product Features</h2><ul> <li><strong>Deep Hydration:</strong> Keeps skin hydrated for 24 hours</li> <li><strong>Lightweight Formula:</strong> Absorbs quickly without greasy residue</li> <li><strong>Rich in Vitamins:</strong> Contains Vitamin E and Aloe Vera</li> <li><strong>Dermatologically Tested:</strong> Safe for all skin types</li> <li><strong>Paraben-Free:</strong> No harmful chemicals</li></ul><h2>Why Choose Hydrating Cream?</h2><p>Our Hydrating Cream is enriched with the best natural ingredients to provide long-lasting hydration. <strong>Perfect for dry, sensitive, and combination skin.</strong></p><blockquote> <p>“This is the best moisturizer Ive ever used! My skin feels soft and hydrated all day.” - <em>Customer Review</em></p></blockquote><h2>Directions for Use</h2><p>Apply a small amount to clean skin and massage gently until fully absorbed. Use in the morning and evening for best results.</p><h2>Customer Reviews</h2><p><strong>★★★★★ - Amazing!</strong> I love how light yet moisturizing this cream is.</p><p><strong>★★★★★ - Highly Recommended</strong> My skin has never felt better!</p><p><strong>★★★★☆ - Great Product</strong> Works well but I wish it came in a larger size.</p><h2>FAQs</h2><ul> <li><strong>Is this suitable for sensitive skin?</strong> Yes! It is dermatologically tested and safe for all skin types.</li> <li><strong>Can I use this under makeup?</strong> Absolutely! It absorbs quickly and works well as a base.</li> <li><strong>Does this have SPF?</strong> No, but it can be layered with sunscreen.</li></ul><h2>Order Now</h2><p>Get your Hydrating Cream today and experience the difference! Available exclusively on our official store.</p>";
return (
<>
<Navbar theme={1} />
<div className="flex flex-col md:flex-row lg:px-20 ">
<div className="lg:col-span-3 ">
<GalleryBox />
</div>
<div className="lg:col-span-3 xs:px-5 lg:px-0 ">
<div className="text-left mt-7">
<h1 className="text-lg font-medium ">{data.englishName} </h1>
<p className="mb-0 text-sm text-gray-400">{data.englishName} </p>
</div>
<div className="flex my-4">
<div className="px-3 py-1 ml-0 rounded-full bg-primary-400">
<p className="mb-0 text-sm text-white ">orginal </p>
</div>
<div className="px-3 py-1 ml-2 rounded-full bg-danger-100">
<p className="mb-0 text-sm text-white ">off today </p>
</div>
<div className="px-3 py-1 ml-2 rounded-full bg-secondary-500">
<p className="mb-0 text-sm text-white ">no stock</p>
</div>
</div>
<div>
<div className="flex justify-between mt-3 text-left">
<h2 className="mb-0 text-sm text-gray-400">
{data?.description}
</h2>
</div>
</div>
<p className="font-bold text-2xl py-8">
${data.cost.toLocaleString()}
</p>
<button className="btn btn-primary px-20 text-base ">
{" "}
Contact and consultation
</button>
</div>
</div>
<div className="xs:px-5 lg:px-20 py-16">
<table className="w-full text-left border-collapse lg:px-20">
<thead>
<tr className="bg-gray-200">
<th className="p-2 border-b">Property</th>
<th className="p-2 border-b">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-2 border-b">Brand</td>
<td className="p-2 border-b">Nature's Essence</td>
</tr>
<tr>
<td className="p-2 border-b">Volume</td>
<td className="p-2 border-b">250ml</td>
</tr>
<tr>
<td className="p-2 border-b">Ingredients</td>
<td className="p-2 border-b">
<ul className="list-disc pl-4">
<li>Aloe Vera</li>
<li>Chamomile</li>
<li>Tea Tree Oil</li>
<li>Rosemary Extract</li>
</ul>
</td>
</tr>
<tr>
<td className="p-2 border-b">Category</td>
<td className="p-2 border-b">Hair Care</td>
</tr>
<tr>
<td className="p-2 border-b">For Hair Type</td>
<td className="p-2 border-b">All Hair Types</td>
</tr>
</tbody>
</table>
</div>
<div className="xs:px-5 lg:px-20 pb-20 ">
<div className="content" dangerouslySetInnerHTML={{ __html: testDataCk }} />
</div>
<Footer />
</>
);
};
export default Product;

View File

@ -8,6 +8,10 @@ export default {
], ],
theme: { theme: {
extend: { extend: {
fontFamily:{
tajawal: ['var(--font-almarai)', 'sans-serif'],
roboto: ['var(--font-rubik)', 'sans-serif'],
},
colors: { colors: {
backgroundPrimary: { backgroundPrimary: {
100: "#FFFBE6", 100: "#FFFBE6",