update
parent
e3c332123c
commit
72cf471c13
|
@ -16,10 +16,12 @@
|
|||
"lucide-react": "^0.475.0",
|
||||
"next": "15.1.6",
|
||||
"next-intl": "^3.26.4",
|
||||
"rc-pagination": "^5.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-countup": "^6.5.3",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-intersection-observer": "^9.15.1",
|
||||
"react-toastify": "^11.0.3",
|
||||
"swiper": "^11.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -23,6 +23,9 @@ dependencies:
|
|||
next-intl:
|
||||
specifier: ^3.26.4
|
||||
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:
|
||||
specifier: ^19.0.0
|
||||
version: 19.0.0
|
||||
|
@ -35,6 +38,9 @@ dependencies:
|
|||
react-intersection-observer:
|
||||
specifier: ^9.15.1
|
||||
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:
|
||||
specifier: ^11.2.2
|
||||
version: 11.2.4
|
||||
|
@ -63,6 +69,13 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
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:
|
||||
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
|
||||
requiresBuild: true
|
||||
|
@ -982,10 +995,19 @@ packages:
|
|||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/classnames@2.5.1:
|
||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||
dev: false
|
||||
|
||||
/client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
dev: false
|
||||
|
||||
/clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
@ -2681,6 +2703,31 @@ packages:
|
|||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
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):
|
||||
resolution: {integrity: sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==}
|
||||
peerDependencies:
|
||||
|
@ -2716,6 +2763,21 @@ packages:
|
|||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -2748,6 +2810,10 @@ packages:
|
|||
which-builtin-type: 1.2.1
|
||||
dev: true
|
||||
|
||||
/regenerator-runtime@0.14.1:
|
||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||
dev: false
|
||||
|
||||
/regexp.prototype.flags@1.5.4:
|
||||
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
|
@ -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;
|
|
@ -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;
|
|
@ -1,13 +1,74 @@
|
|||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import { getLocale, getMessages } from "next-intl/server";
|
||||
import { notFound } from "next/navigation";
|
||||
import { routing } from "src/i18n/routing";
|
||||
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({
|
||||
children,
|
||||
params: { locale }
|
||||
params
|
||||
}) {
|
||||
|
||||
const { locale } = await params
|
||||
const navbaritems = await fetchNavbarItems()
|
||||
// Ensure that the incoming `locale` is valid
|
||||
if (!routing.locales.includes(locale)) {
|
||||
notFound();
|
||||
|
@ -16,13 +77,19 @@ export default async function LocaleLayout({
|
|||
// Providing all messages to the client
|
||||
// side is the easiest way to get started
|
||||
const messages = await getMessages();
|
||||
console.log("messager",messages)
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<body>
|
||||
<html lang={locale} dir={locale === "en" ? "ltr" : "rtl"}>
|
||||
<head>
|
||||
|
||||
<meta name="apple-mobile-web-app-title" content="AdHorizonIntl" />
|
||||
</head>
|
||||
<body className={`${rubik.variable} ${roboto.variable} `} suppressHydrationWarning>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Navbar items={navbaritems} />
|
||||
{children}
|
||||
<Footer />
|
||||
<ToastContainer />
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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 |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 428 KiB |
|
@ -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"
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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;
|
|
@ -3,16 +3,19 @@
|
|||
import useEmblaCarousel from "embla-carousel-react"
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||
import { useCallback } from "react"
|
||||
import CardNormal from "../Cards/CardNormal/page"
|
||||
import CardNormal from "../Cards/CardNormal"
|
||||
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(
|
||||
{ loop: true, align: "start" },
|
||||
{ loop: true, align: "start", direction: locale === "en" ? "ltr" : "rtl" },
|
||||
[
|
||||
Autoplay({ delay: 3000, stopOnInteraction: true }),
|
||||
]
|
||||
|
@ -27,42 +30,52 @@ export default function ProductCarousel({ title, subtitle, products }) {
|
|||
}, [emblaApi])
|
||||
|
||||
return (
|
||||
<section className="py-12 px-4 md:px-6 lg:px-8">
|
||||
<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>
|
||||
<section className="py-12 max-w-screen-xlmx-auto " >
|
||||
|
||||
<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 className="flex sm:justify-between flex-col sm:flex-row pb-4 items-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-3xl font-bold text-center mb-2">{title}</h2>
|
||||
{
|
||||
subtitle &&
|
||||
<p className="text-xl text-center text-gray-600 mb-8">{subtitle}</p>
|
||||
}
|
||||
</div>
|
||||
<Link href={showMoreLink} className="text-sm text-blue-600 underline">
|
||||
{t("showMoreLink")}
|
||||
</Link>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -7,8 +7,8 @@ import test3 from "../../assets/images/product/3.png";
|
|||
import logo from "../../assets/images/logo.png";
|
||||
import { useState } from "react";
|
||||
|
||||
const GalleryBox = () => {
|
||||
const file = [{ image: test1 }, { image: test2 }, { image: test3 }];
|
||||
const GalleryBox = ({ images }) => {
|
||||
|
||||
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||
const [isLightboxOpen, setIsLightboxOpen] = useState(false);
|
||||
|
@ -37,14 +37,14 @@ const GalleryBox = () => {
|
|||
return (
|
||||
<div className="w-full rounded-3xl">
|
||||
<div className="flex justify-center xs:pb-[10px]">
|
||||
<div className="w-full max-w-[90vw]">
|
||||
{file?.length > 0 ? (
|
||||
<div className=" relative p-8">
|
||||
{images?.length > 0 ? (
|
||||
<Image
|
||||
src={file[0]?.image}
|
||||
width={1000}
|
||||
height={1000}
|
||||
className="mx-auto object-cover cursor-pointer"
|
||||
alt={"llc"}
|
||||
src={images?.[0]?.url}
|
||||
width={500}
|
||||
height={500}
|
||||
className="mx-auto object-contain cursor-pointer"
|
||||
alt={images?.[0]?.alternativeText ?? ""}
|
||||
onClick={() => handleImageClick(0)}
|
||||
priority
|
||||
loading="eager"
|
||||
|
@ -62,16 +62,16 @@ const GalleryBox = () => {
|
|||
</div>
|
||||
|
||||
<div className="flex p-3 overflow-x-auto" id="swich-scrollbar">
|
||||
{file.slice(1)?.map((e, index) => (
|
||||
{images.slice(1)?.map((image, index) => (
|
||||
<div
|
||||
className="p-3 ml-2 bg-white border shadow-sm rounded-3xl cursor-pointer"
|
||||
key={e.id}
|
||||
key={image.documentId}
|
||||
onClick={() => handleImageClick(index)}
|
||||
>
|
||||
<div className="xs:w-[50px] lg:w-[50px]">
|
||||
<Image
|
||||
src={e.image}
|
||||
alt={e.fileName}
|
||||
src={image.url}
|
||||
alt={image.alternativeText}
|
||||
width={250}
|
||||
height={250}
|
||||
priority
|
||||
|
@ -88,7 +88,7 @@ const GalleryBox = () => {
|
|||
<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
|
||||
src={file[currentImageIndex]?.image}
|
||||
src={images[currentImageIndex]?.url}
|
||||
fill
|
||||
className="mx-auto object-contain"
|
||||
alt={"llc"}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import Image from "next/image";
|
||||
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";
|
||||
|
||||
|
@ -13,41 +13,12 @@ import { usePathname, useRouter } from "next/navigation";
|
|||
import logo from "../../assets/images/logo.png";
|
||||
import { useLocale } from "next-intl";
|
||||
|
||||
const Navbar = ({ theme }) => {
|
||||
|
||||
const theme = 1;
|
||||
const Navbar = ({ items }) => {
|
||||
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 [activeStepNavbar, setActiveStepNavbar] = useState(null);
|
||||
|
@ -92,18 +63,9 @@ const Navbar = ({ theme }) => {
|
|||
const pathname = usePathname()
|
||||
|
||||
const changeLocale = () => {
|
||||
console.log(locale)
|
||||
let link = ''
|
||||
if (locale === "en") {
|
||||
|
||||
link = 'ar-OM' + (pathname.includes("/en") ? pathname.replace("en", "") : pathname)
|
||||
}
|
||||
else {
|
||||
link = pathname.replace("ar-OM", "")
|
||||
}
|
||||
|
||||
router.push(link)
|
||||
}
|
||||
const newLocale = locale === "en" ? "ar-OM" : "en";
|
||||
router.push(`/${newLocale}${window.location.pathname.replace("/ar-OM", "").replace("/en", "")}`);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
|
@ -118,35 +80,22 @@ const Navbar = ({ theme }) => {
|
|||
}`}
|
||||
>
|
||||
<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 " : " "}`}
|
||||
>
|
||||
<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="mr-2 w-fit px-3 p-2 text-sm bg-white flex rounded-xl ">
|
||||
<p className="mb-0"> </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 ">
|
||||
<p className="mb-0">{locale === "en" ? "Arabic" : "English"}</p>
|
||||
<p className="mb-0">{locale === "en" ? "عربی" : "English"}</p>
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
</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
|
||||
src={logo}
|
||||
fill
|
||||
|
@ -156,50 +105,50 @@ const Navbar = ({ theme }) => {
|
|||
|
||||
</div>
|
||||
|
||||
{NavBarData.map((e, index) => (
|
||||
<>
|
||||
<div
|
||||
key={index}
|
||||
className={` mx-2 px-2 w-fit text-sm p-7 ${1
|
||||
? "bg-visa2-200 rounded-full !text-white text-shadow pb-1 relative"
|
||||
: ""
|
||||
} `}
|
||||
onMouseEnter={() => {
|
||||
// setHoverItemNavbar(index);
|
||||
setActiveStepNavbar(e.id);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
// setHoverItemNavbar(-1);
|
||||
setActiveStepNavbar(null);
|
||||
}}
|
||||
// onClick={() => context.setOpenNavBarServices(false)}
|
||||
{items.map((item, index) => (
|
||||
|
||||
<div
|
||||
key={index}
|
||||
className={` mx-2 px-2 w-fit text-sm p-7 ${1
|
||||
? "bg-visa2-200 rounded-full !text-white text-shadow pb-1 relative"
|
||||
: ""
|
||||
} `}
|
||||
onMouseEnter={() => {
|
||||
// setHoverItemNavbar(index);
|
||||
setActiveStepNavbar(item.documentId);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
// setHoverItemNavbar(-1);
|
||||
setActiveStepNavbar(null);
|
||||
}}
|
||||
// onClick={() => context.setOpenNavBarServices(false)}
|
||||
>
|
||||
<Link
|
||||
href={`${item.link}`}
|
||||
className={` whitespace-nowrap ${isScrolled ? "text-black" : "text-white"} ${theme == 1 ? "text-sm !text-black" : " "
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
href={`${e.href}`}
|
||||
className={` whitespace-nowrap ${isScrolled ? "text-black" : "text-white"} ${theme == 1 ? "text-sm !text-black" : " "
|
||||
}`}
|
||||
>
|
||||
{e.name}
|
||||
{e.children.length > 0 && ""}
|
||||
</Link>
|
||||
{item.title}
|
||||
{item.children.length > 0 && ""}
|
||||
</Link>
|
||||
|
||||
{e.children.length > 0 && e.id == activeStepNavbar && (
|
||||
<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.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">
|
||||
{
|
||||
item.children.map((c, index) => (
|
||||
|
||||
<li key={index} className="px-4 py-2 hover:bg-gray-100 cursor-pointer">
|
||||
<Link href={c.href}>
|
||||
{c.name}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
<li key={index} className="px-4 py-2 hover:bg-gray-100 cursor-pointer">
|
||||
<Link href={c.link}>
|
||||
{c.title}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
@ -219,25 +168,26 @@ const Navbar = ({ theme }) => {
|
|||
}`}
|
||||
>
|
||||
<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]"
|
||||
: "mx-2 rounded-xl rounded-br-[40px]"
|
||||
}`}
|
||||
>
|
||||
|
||||
{/* <Link href={"/"} className="w-full"> */}
|
||||
<div className=" w-full mx-1 flex ">
|
||||
<div className="xs:w-[60px] md:w-[100px] xs:mt-[5px] mt-[12px] ">
|
||||
<Image
|
||||
src={logo}
|
||||
width={500}
|
||||
height={500}
|
||||
alt="llc"
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
</button>
|
||||
<div className=" w-full mx-1 flex items-center ">
|
||||
<button onClick={changeLocale} className="mr-2 w-fit px-3 p-2 h-fit text-sm bg-white flex rounded-xl ">
|
||||
<p className="mb-0">{locale === "en" ? "عربی" : "English"}</p>
|
||||
</button>
|
||||
</div>
|
||||
<div className=" ">
|
||||
<Image
|
||||
src={logo}
|
||||
width={200}
|
||||
height={200}
|
||||
alt="llc"
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
{/* </Link> */}
|
||||
<div
|
||||
|
@ -255,7 +205,7 @@ const Navbar = ({ theme }) => {
|
|||
<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"
|
||||
fill="black"
|
||||
fill-opacity="0.73"
|
||||
fillOpacity="0.73"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -319,13 +269,13 @@ const Navbar = ({ theme }) => {
|
|||
|
||||
<div className="mx-3">
|
||||
<div>
|
||||
{NavBarData.map((e, index) => (
|
||||
<>
|
||||
{items.map((e, index) => (
|
||||
<Fragment key={index}>
|
||||
<motion.div
|
||||
custom={index}
|
||||
animate="visible"
|
||||
variants={toLeft}
|
||||
key={index}
|
||||
|
||||
>
|
||||
{/* <Link href={e.url}> */}
|
||||
<>
|
||||
|
@ -335,12 +285,12 @@ const Navbar = ({ theme }) => {
|
|||
if (e.children.length > 0) {
|
||||
setResponsiveNavBarItemStep(index);
|
||||
|
||||
setActiveStepNavbar(e.id);
|
||||
setActiveStepNavbar(e.documentId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{e.name}
|
||||
{e.title}
|
||||
<small className="absolute left-0 mx-7 ">
|
||||
{e.children.length > 0 ? (
|
||||
<div>
|
||||
|
@ -374,23 +324,23 @@ const Navbar = ({ theme }) => {
|
|||
|
||||
{responsiveNavBarItemStep === index &&
|
||||
e.children.length > 0 &&
|
||||
e.id == activeStepNavbar && (
|
||||
e.documentId == activeStepNavbar && (
|
||||
<div>
|
||||
<div className="my-3 ">
|
||||
{e.children.map((s, index) => (
|
||||
// <Link href={s.url} key={index}>
|
||||
<div
|
||||
className="bg-primary-50 rounded-lg my-1 p-2 "
|
||||
key={index}
|
||||
>
|
||||
<p className="mb-0 text-sm">{s.name}</p>
|
||||
</div>
|
||||
// </Link>
|
||||
<Link href={s.link} key={index}>
|
||||
<div
|
||||
className="bg-primary-50 rounded-lg my-1 p-2 "
|
||||
key={index}
|
||||
>
|
||||
<p className="mb-0 text-sm">{s.name}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
|
@ -1,17 +1,23 @@
|
|||
{
|
||||
|
||||
"HomePage": {
|
||||
"SEO":{
|
||||
"title":"خدمات الأفق المتقدمة ش.م.م",
|
||||
"description":"خدمات الأفق المتقدمة ش.م.م تقدم حلول سلاسل التوريد بالجملة للمنظفات والمواد الغذائية بجودة عالية وتركز على التميز والموثوقية"
|
||||
|
||||
},
|
||||
"AboutUs": {
|
||||
"brandName": "منتجات خدمات الأفق المتقدمة ش.م.م",
|
||||
"brandName": " خدمات الأفق المتقدمة ش.م.م",
|
||||
"description": [
|
||||
"شريكك الموثوق لحلول سلاسل التوريد بالجملة",
|
||||
"شريككم الموثوق لحلول سلاسل التوريد بالجملة",
|
||||
"خدمات الأفق المتقدمة هي شركة تجارية رائدة متخصصة في حلول سلاسل التوريد بالجملة لمجموعة متنوعة من المنتجات، بما في ذلك المنظفات عالية الجودة والمواد الغذائية. مع سنوات من الخبرة والعلاقات الواسعة في الصناعة، نفخر بالتزامنا بالتميز والموثوقية ورضا العملاء.",
|
||||
"في AHS، نفهم تعقيدات سلسلة التوريد ونسعى لتبسيط العملية لشركائنا. شبكتنا الواسعة من الموردين والمصنعين تمكننا من الحصول على منتجات عالية الجودة بأسعار تنافسية، مما يضمن حصولك على أفضل قيمة لاستثمارك."
|
||||
"في AHS، نفهم تعقيدات سلسلة التوريد ونسعى لتبسيط العملية لشركائنا. شبكتنا الواسعة من الموردين والمصنعين تمكننا من الحصول على منتجات عالية الجودة بأسعار تنافسية، مما يضمن حصولكم على أفضل قيمة لاستثماركم."
|
||||
]
|
||||
},
|
||||
"Sides": {
|
||||
"title": "اكتشف خبراتنا",
|
||||
"fmcg": {
|
||||
"title": "السلع الاستهلاكية سريعة الحركة",
|
||||
"title": "FMCG",
|
||||
"subtitle": "السلع الاستهلاكية سريعة الحركة",
|
||||
"description": "قسم السلع الاستهلاكية سريعة الحركة لدينا متخصص في توزيع وتسويق المنتجات الاستهلاكية اليومية. نضمن سرعة دوران المنتجات، وسلاسل توريد فعالة، واستراتيجيات تسويقية مبتكرة لتلبية الطلبات المتغيرة باستمرار للمستهلكين."
|
||||
},
|
||||
|
@ -20,9 +26,53 @@
|
|||
"subtitle": "نبني المستقبل",
|
||||
"description": "ذراع الإنشاءات لدينا مكرس لخلق بنية تحتية دائمة وحلول بناء مبتكرة. من المشاريع السكنية إلى المجمعات التجارية، نقدم الخبرة والجودة والاستدامة في كل مشروع بناء."
|
||||
}
|
||||
},
|
||||
"products": {
|
||||
"title": [
|
||||
"منتجات Active",
|
||||
"الإنشاءات"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Footer": {
|
||||
"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": "المزيد من التفاصيل"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
"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": {
|
||||
"brandName": "ADVANCED HORIZON SERVICES LLC Products",
|
||||
"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."
|
||||
]
|
||||
},
|
||||
|
||||
"Sides": {
|
||||
"title": "Discover Our Expertise",
|
||||
"fmcg": {
|
||||
|
@ -20,9 +25,53 @@
|
|||
"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."
|
||||
}
|
||||
},
|
||||
"products":{
|
||||
"title":[
|
||||
"Active Products",
|
||||
"Constructions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,16 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html[lang="en"] {
|
||||
|
||||
}
|
||||
|
||||
/* Arabic content (rtl) uses Tajawal */
|
||||
html[lang="ar-OM"] {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply py-2 px-4 rounded-xl transition-all shadow-lg border-2;
|
||||
|
|
|
@ -1,27 +1,164 @@
|
|||
import React from "react";
|
||||
import CardNormal from "src/components/Cards/CardNormal/page";
|
||||
"use client"
|
||||
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 (
|
||||
<div className="px-2 pb-20">
|
||||
<div className="flex">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-left ">
|
||||
Product Listing Page
|
||||
</h1>
|
||||
<p className="mb-0 text-sm text-gray-400 xs:text-center lg:text-left">
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Corporis,{" "}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 mt-10">
|
||||
{products.map((product, index) => (
|
||||
<div key={index} className="relative">
|
||||
<CardNormal data={product} priority />
|
||||
<>
|
||||
{
|
||||
brands && brands.length > 0 && categories && categories.length > 0 &&
|
||||
<Filters brands={brands} categories={categories} />
|
||||
}
|
||||
<div className="flex flex-col w-full items-center justify-center ">
|
||||
<div className="px-2 pb-20 w-full">
|
||||
<div className="flex">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold ">
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="mb-0 text-sm text-gray-400 xs:text-center ">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,19 +1,52 @@
|
|||
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 Content from "./components/Content";
|
||||
import Filters from "./components/Filters";
|
||||
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 (
|
||||
<>
|
||||
<Navbar theme={1} />
|
||||
<div className="xs:container-none md:container mx-auto p-4 flex flex-col md:flex-row mt-10">
|
||||
<Filters />
|
||||
<Content products={products} />
|
||||
|
||||
<div className={`container-none md:container mx-auto p-4 flex flex-col md:flex-row mt-10 ${locale === "en" ? "ltr" : "rtl"}`}>
|
||||
|
||||
<Content category={category} />
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,37 +7,40 @@ const AboutUs = () => {
|
|||
const t = useTranslations("HomePage.AboutUs")
|
||||
const locale = useLocale()
|
||||
return (
|
||||
<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">
|
||||
<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")}
|
||||
<div className=" min-h-[calc(100vh-108px)] flex flex-col items-center justify-center relative ">
|
||||
<div className="max-w-screen-xl mx-auto">
|
||||
|
||||
</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>
|
||||
{/*
|
||||
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" className="absolute left-0 top-0 bottom-0 -z-10 right-0">
|
||||
<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`}>
|
||||
<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">
|
||||
<button className="btn btn-primary text-base py-3 px-10 ">
|
||||
{" "}
|
||||
|
@ -48,6 +51,7 @@ const AboutUs = () => {
|
|||
Industrial
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -24,11 +24,12 @@ const CounterDetail = ({ stats }) => {
|
|||
<h3 className="mb-0 text-4xl text-gray-300 font-bold">
|
||||
{s.title}
|
||||
</h3>
|
||||
<p className="mb-0 text-6xl font-bold text-gray-500 mt-5 "><CountUp
|
||||
start={startCounting ? 0 : undefined}
|
||||
end={s.stat}
|
||||
duration={2.75}
|
||||
/></p>
|
||||
<p className="mb-0 text-6xl font-bold text-gray-500 mt-5">
|
||||
<CountUp
|
||||
start={startCounting ? 0 : undefined}
|
||||
end={s.stat}
|
||||
duration={2.75}
|
||||
/></p>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ import { useTranslations } from "next-intl";
|
|||
const Footer = () => {
|
||||
const t = useTranslations("Footer")
|
||||
return (
|
||||
<div className="bg-secondary-700 ">
|
||||
<div className="lg:w-7/12 xs:w-full mx-auto py-20">
|
||||
<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="flex justify-center">
|
||||
<div className={`text-center w-ful`}>
|
||||
<>
|
||||
|
@ -27,7 +27,7 @@ const Footer = () => {
|
|||
</>
|
||||
</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 gap-2">
|
||||
<MapPinHouse size={20} />
|
||||
|
@ -61,7 +61,7 @@ const Footer = () => {
|
|||
</div>
|
||||
<p className="mb-0 text-sm text-gray-200 w-full text-center opacity-30 ">
|
||||
© 2025. All rights reserved. <br />
|
||||
am - Om
|
||||
Advanced Horizon Services LLC
|
||||
</p>
|
||||
{/* <div className="flex justify-center w-full my-5 ">
|
||||
<Link href={`/products`}>
|
||||
|
|
|
@ -1,33 +1,82 @@
|
|||
import { getLocale, getMessages } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
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 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 (
|
||||
<div className="my-[120px]">
|
||||
<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 className="my-20 max-w-screen-xl mx-auto">
|
||||
|
||||
|
||||
<div>
|
||||
<ProductCarousel products={products.filter(p => p.brand.title === "active")} subtitle={""} title={"Active Products"} />
|
||||
</div>
|
||||
<div>
|
||||
<ProductCarousel products={products1} subtitle={""} title={t.HomePage.products.title[0]} showMoreLink={"/products/active"} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ProductCarousel products={products.filter(p => p.brand.title === "savin")} subtitle={""} title={"Savin Products"} />
|
||||
</div>
|
||||
<div>
|
||||
<ProductCarousel products={products2} subtitle={""} title={t.HomePage.products.title[1]} showMoreLink={"/products/constructions"} />
|
||||
</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) => (
|
||||
<div key={index} className="relative">
|
||||
<CardNormal product={product} priority />
|
||||
|
@ -35,7 +84,7 @@ const Products = ({ products }) => {
|
|||
))}
|
||||
</div> */}
|
||||
|
||||
{/* <div className="flex justify-center">
|
||||
{/* <div className="flex justify-center">
|
||||
<Link href={"categories/Product-20Listing-Page"}>
|
||||
<button className="btn btn-primary px-10 text-sm ">
|
||||
{" "}
|
||||
|
@ -43,8 +92,8 @@ const Products = ({ products }) => {
|
|||
</button>
|
||||
</Link>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { ShoppingCart, Building2 } from 'lucide-react'; // Assuming you're using lucide-react for icons
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
|
||||
const Sides = () => {
|
||||
|
||||
|
@ -13,7 +14,7 @@ const Sides = () => {
|
|||
</h2>
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* 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">
|
||||
<ShoppingCart className="w-12 h-12 mb-4 text-primary" />
|
||||
<h3 className="text-2xl font-bold">{t("fmcg.title")}</h3>
|
||||
|
@ -24,10 +25,10 @@ const Sides = () => {
|
|||
{t("fmcg.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* 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">
|
||||
<Building2 className="w-12 h-12 mb-4 text-primary" />
|
||||
<h3 className="text-2xl font-bold">{t("construction.title")}</h3>
|
||||
|
@ -38,7 +39,7 @@ const Sides = () => {
|
|||
{t("construction.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -7,37 +7,6 @@ import graphql from "src/utils/graphql";
|
|||
import CounterDetail from "./components/CounterDetail";
|
||||
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 = `
|
||||
query Stats($locale:I18NLocaleCode) {
|
||||
|
@ -52,8 +21,8 @@ query Stats($locale:I18NLocaleCode) {
|
|||
|
||||
|
||||
const getStats = async () => {
|
||||
const locale =await getLocale()
|
||||
console.log("locale",locale)
|
||||
const locale = await getLocale()
|
||||
console.log("locale", locale)
|
||||
const stats = await graphql(gql_stats, {
|
||||
locale: locale
|
||||
})
|
||||
|
@ -61,33 +30,23 @@ const getStats = async () => {
|
|||
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 products = await getProducts()
|
||||
|
||||
const stats = await getStats()
|
||||
|
||||
return (
|
||||
<div className=" text-center text-6xl">
|
||||
{" "}
|
||||
<Navbar theme={1} />
|
||||
|
||||
{/* <HeroSection /> */}
|
||||
<AboutUs />
|
||||
<Sides />
|
||||
<CounterDetail stats={stats} />
|
||||
<Products products={products} />
|
||||
<Products />
|
||||
{/* <WhyHorizon/> */}
|
||||
<Footer />
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 I’ve 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;
|
|
@ -8,6 +8,10 @@ export default {
|
|||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily:{
|
||||
tajawal: ['var(--font-almarai)', 'sans-serif'],
|
||||
roboto: ['var(--font-rubik)', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
backgroundPrimary: {
|
||||
100: "#FFFBE6",
|
||||
|
|
Loading…
Reference in New Issue