Add Saleh Kargar factory website with FA/AR i18n.
Clone of salehkargar.ir as a Next.js site with localized pages, SEO, and product gallery. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
ab5425663f
commit
eccbb5d79d
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
|
||||
const tabIds = ["work", "goal", "services"] as const;
|
||||
|
||||
export default function AboutTabs() {
|
||||
const t = useTranslations("AboutTabs");
|
||||
const [active, setActive] = useState(0);
|
||||
const activeId = tabIds[active];
|
||||
|
||||
return (
|
||||
<section className="section about-tabs">
|
||||
<div className="container">
|
||||
<div className="about-layout">
|
||||
<div className="about-media">
|
||||
<Link href="/" className="zoom-box">
|
||||
<Image
|
||||
src="/images/featurebox/2.jpg"
|
||||
alt={t("sampleWork")}
|
||||
width={400}
|
||||
height={500}
|
||||
/>
|
||||
<span className="zoom-label">{t("sampleWork")}</span>
|
||||
</Link>
|
||||
<div className="about-side-photo">
|
||||
<Image
|
||||
src="/images/featurebox/1.jpg"
|
||||
alt={t("factoryView")}
|
||||
width={400}
|
||||
height={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="about-panel">
|
||||
<div className="tabs-nav" role="tablist">
|
||||
{tabIds.map((id, index) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active === index}
|
||||
className={active === index ? "is-active" : ""}
|
||||
onClick={() => setActive(index)}
|
||||
>
|
||||
{t(`tabs.${id}.label`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="tabs-panel" role="tabpanel">
|
||||
<h2>{t(`tabs.${activeId}.title`)}</h2>
|
||||
<h3 className="tabs-subtitle">
|
||||
{t(`tabs.${activeId}.subtitle`)
|
||||
.split("\n")
|
||||
.map((line) => (
|
||||
<span key={line}>
|
||||
{line}
|
||||
<br />
|
||||
</span>
|
||||
))}
|
||||
</h3>
|
||||
<p className="tabs-body">
|
||||
{t(`tabs.${activeId}.body`)
|
||||
.split("\n")
|
||||
.map((line) => (
|
||||
<span key={line}>
|
||||
{line}
|
||||
<br />
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
<Link
|
||||
href={activeId === "goal" ? "/aboutlime" : "/aboutus"}
|
||||
className="btn-theme"
|
||||
>
|
||||
{t(`tabs.${activeId}.cta`)}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
const phones = [
|
||||
{ key: "accounting", href: "tel:+982538700105", text: "8700105-0253" },
|
||||
{ key: "sales", href: "tel:+982538700061", text: "4-8700061-0253" },
|
||||
{ key: "loading", href: "tel:+982538700074", text: "8700074-8700683-0253" },
|
||||
] as const;
|
||||
|
||||
export default async function Footer() {
|
||||
const t = await getTranslations("Footer");
|
||||
|
||||
return (
|
||||
<footer className="site-footer">
|
||||
<div className="footer-action">
|
||||
<div className="container">
|
||||
<h3>{t("brand")}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="footer-widgets">
|
||||
<div className="container footer-grid">
|
||||
<aside>
|
||||
<h4>{t("aboutTitle")}</h4>
|
||||
<p>{t("aboutText")}</p>
|
||||
</aside>
|
||||
|
||||
<aside>
|
||||
<h4>{t("addressTitle")}</h4>
|
||||
<address className="footer-address">{t("address")}</address>
|
||||
<ul>
|
||||
<li>
|
||||
<span className="bullet" aria-hidden />
|
||||
{t("noteFactory")}
|
||||
</li>
|
||||
<li>
|
||||
<span className="bullet" aria-hidden />
|
||||
{t("noteSlogan")}
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<aside>
|
||||
<h4>{t("contactTitle")}</h4>
|
||||
<ul>
|
||||
{phones.map((item) => (
|
||||
<li key={item.key}>
|
||||
<span className="bullet" aria-hidden />
|
||||
<span>
|
||||
{t(item.key)} :{" "}
|
||||
<a href={item.href} className="footer-phone">
|
||||
{item.text}
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="footer-copy">
|
||||
<div className="container">
|
||||
<p>{t("copyright")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Link, usePathname, useRouter } from "@/i18n/navigation";
|
||||
import { routing, type AppLocale } from "@/i18n/routing";
|
||||
|
||||
const navLinks = [
|
||||
{ key: "home", href: "/" },
|
||||
{ key: "products", href: "/products" },
|
||||
{ key: "aboutChalk", href: "/aboutsuperchalk" },
|
||||
{ key: "aboutLime", href: "/aboutlime" },
|
||||
{ key: "gallery", href: "/gallery" },
|
||||
{ key: "aboutUs", href: "/aboutus" },
|
||||
] as const;
|
||||
|
||||
export default function Header() {
|
||||
const t = useTranslations("Nav");
|
||||
const tLang = useTranslations("Language");
|
||||
const locale = useLocale();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const langRef = useRef<HTMLLIElement>(null);
|
||||
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [langOpen, setLangOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 10);
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
setLangOpen(false);
|
||||
}, [pathname, locale]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.toggle("nav-open", open);
|
||||
return () => document.body.classList.remove("nav-open");
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!langOpen) return;
|
||||
|
||||
const onPointerDown = (event: MouseEvent) => {
|
||||
if (!langRef.current?.contains(event.target as Node)) {
|
||||
setLangOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") setLangOpen(false);
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", onPointerDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", onPointerDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [langOpen]);
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === "/") return pathname === "/";
|
||||
return pathname === href || pathname.startsWith(`${href}/`);
|
||||
};
|
||||
|
||||
const switchLocale = (nextLocale: AppLocale) => {
|
||||
router.replace(pathname, { locale: nextLocale });
|
||||
setLangOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className={`site-header${scrolled ? " is-sticky" : ""}`}>
|
||||
<div className="header-bar">
|
||||
<div className="header-brand-row">
|
||||
<div className="header-main">
|
||||
<Link href="/" className="header-wordmark">
|
||||
<Image
|
||||
src="/images/header/logo.jpg"
|
||||
alt={t("logoAlt")}
|
||||
width={260}
|
||||
height={44}
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<div className="header-divider" />
|
||||
|
||||
<div className="header-nav-row">
|
||||
<button
|
||||
type="button"
|
||||
className={`menu-toggle${open ? " is-open" : ""}`}
|
||||
aria-label={open ? t("closeMenu") : t("openMenu")}
|
||||
aria-expanded={open}
|
||||
aria-controls="main-navigation"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</button>
|
||||
|
||||
<nav
|
||||
id="main-navigation"
|
||||
className={`main-nav${open ? " is-open" : ""}`}
|
||||
aria-label={t("mainMenu")}
|
||||
>
|
||||
<ul>
|
||||
<li className="nav-lang" ref={langRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`nav-icon-btn lang-trigger${langOpen ? " is-open" : ""}`}
|
||||
aria-label={tLang("label")}
|
||||
aria-expanded={langOpen}
|
||||
aria-haspopup="listbox"
|
||||
onClick={() => setLangOpen((v) => !v)}
|
||||
>
|
||||
<GlobeIcon />
|
||||
<span className="lang-current">
|
||||
{locale === "ar" ? "AR" : "FA"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{langOpen && (
|
||||
<ul className="lang-menu" role="listbox" aria-label={tLang("label")}>
|
||||
{routing.locales.map((item) => (
|
||||
<li key={item} role="option" aria-selected={item === locale}>
|
||||
<button
|
||||
type="button"
|
||||
className={`lang-option${item === locale ? " is-active" : ""}`}
|
||||
onClick={() => switchLocale(item)}
|
||||
>
|
||||
<Image
|
||||
src={`/images/languages/${item}.png`}
|
||||
alt=""
|
||||
width={20}
|
||||
height={14}
|
||||
/>
|
||||
<span>{tLang(item)}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
|
||||
{navLinks.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
aria-current={isActive(item.href) ? "page" : undefined}
|
||||
className={isActive(item.href) ? "is-active" : undefined}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{t(item.key)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li>
|
||||
<button type="button" className="nav-icon-btn" aria-label={t("login")}>
|
||||
<UserIcon />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link href="/" className="header-crest" aria-label={t("logoAlt")}>
|
||||
<Image
|
||||
src="/images/header/logo1.png"
|
||||
alt={t("logoAlt")}
|
||||
width={92}
|
||||
height={92}
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function GlobeIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" aria-hidden>
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="1.6" />
|
||||
<path
|
||||
d="M3 12h18M12 3c2.8 2.6 4.2 5.7 4.2 9s-1.4 6.4-4.2 9c-2.8-2.6-4.2-5.7-4.2-9S9.2 5.6 12 3z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function UserIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" aria-hidden>
|
||||
<circle cx="12" cy="8" r="3.2" stroke="currentColor" strokeWidth="1.6" />
|
||||
<path
|
||||
d="M5 19.5c1.8-3.2 4-4.7 7-4.7s5.2 1.5 7 4.7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import Image from "next/image";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export default async function Hero() {
|
||||
const t = await getTranslations("Hero");
|
||||
|
||||
const services = [
|
||||
{ title: t("transport"), image: "/images/slider/1.png" },
|
||||
{ title: t("demolition"), image: "/images/slider/2.png" },
|
||||
{ title: t("limeProduction"), image: "/images/slider/3.png" },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="hero">
|
||||
<div className="hero-bg">
|
||||
<Image
|
||||
src="/images/slider/slider.jpg"
|
||||
alt={t("imageAlt")}
|
||||
fill
|
||||
priority
|
||||
className="hero-bg-img"
|
||||
sizes="100vw"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="hero-overlay">
|
||||
<div className="hero-center animate-fade-up">
|
||||
<h1>{t("title")}</h1>
|
||||
<hr className="hero-hr" />
|
||||
<div className="hero-en">{t("subtitleEn")}</div>
|
||||
</div>
|
||||
|
||||
<ul className="hero-services">
|
||||
{services.map((item, i) => (
|
||||
<li
|
||||
key={item.title}
|
||||
className="animate-fade-up"
|
||||
style={{ animationDelay: `${0.2 + i * 0.12}s` }}
|
||||
>
|
||||
<Image src={item.image} alt={item.title} width={64} height={60} />
|
||||
<span>{item.title}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { siteConfig } from "@/data/seo";
|
||||
|
||||
type JsonLdProps = {
|
||||
data: Record<string, unknown> | Record<string, unknown>[];
|
||||
};
|
||||
|
||||
export default function JsonLd({ data }: JsonLdProps) {
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function organizationJsonLd() {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
name: siteConfig.name,
|
||||
alternateName: siteConfig.nameEn,
|
||||
url: siteConfig.url,
|
||||
logo: `${siteConfig.url}/images/header/logo1.png`,
|
||||
image: `${siteConfig.url}${siteConfig.ogImage}`,
|
||||
description: siteConfig.description,
|
||||
slogan: siteConfig.slogan,
|
||||
telephone: siteConfig.phone,
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
addressLocality: siteConfig.address.locality,
|
||||
streetAddress: siteConfig.address.street,
|
||||
addressCountry: siteConfig.address.country,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function localBusinessJsonLd() {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "LocalBusiness",
|
||||
"@id": `${siteConfig.url}/#business`,
|
||||
name: siteConfig.name,
|
||||
alternateName: siteConfig.nameEn,
|
||||
url: siteConfig.url,
|
||||
image: `${siteConfig.url}${siteConfig.ogImage}`,
|
||||
description: siteConfig.description,
|
||||
telephone: siteConfig.phone[0],
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
addressLocality: siteConfig.address.locality,
|
||||
streetAddress: siteConfig.address.street,
|
||||
addressCountry: siteConfig.address.country,
|
||||
},
|
||||
areaServed: {
|
||||
"@type": "Country",
|
||||
name: "Iran",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function productJsonLd(product: {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
price: number;
|
||||
slug: string;
|
||||
}) {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
name: product.title,
|
||||
description: product.description,
|
||||
image: `${siteConfig.url}${product.image}`,
|
||||
brand: {
|
||||
"@type": "Brand",
|
||||
name: siteConfig.shortName,
|
||||
},
|
||||
offers: {
|
||||
"@type": "Offer",
|
||||
url: `${siteConfig.url}/products/${product.slug}`,
|
||||
priceCurrency: "IRR",
|
||||
price: product.price,
|
||||
availability: "https://schema.org/InStock",
|
||||
seller: {
|
||||
"@type": "Organization",
|
||||
name: siteConfig.name,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function breadcrumbJsonLd(
|
||||
items: { name: string; path: string }[],
|
||||
) {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: items.map((item, index) => ({
|
||||
"@type": "ListItem",
|
||||
position: index + 1,
|
||||
name: item.name,
|
||||
item: `${siteConfig.url}${item.path}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import Image from "next/image";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export default async function Location() {
|
||||
const t = await getTranslations("Location");
|
||||
|
||||
return (
|
||||
<section className="section location">
|
||||
<div className="container">
|
||||
<div className="section-head section-head-center">
|
||||
<h2>{t("title")}</h2>
|
||||
<Image
|
||||
src="/images/infoitems/line.png"
|
||||
alt=""
|
||||
width={151}
|
||||
height={5}
|
||||
className="section-line"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="location-grid">
|
||||
<div className="location-copy">
|
||||
<Image
|
||||
src="/images/info1/2.png"
|
||||
alt=""
|
||||
width={80}
|
||||
height={80}
|
||||
className="location-icon"
|
||||
/>
|
||||
<h3>{t("heading")}</h3>
|
||||
<p>
|
||||
{t("subtitle")}
|
||||
<br />
|
||||
<br />
|
||||
{t("coverage")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="location-map">
|
||||
<Image
|
||||
src="/images/info1/1.png"
|
||||
alt={t("mapAlt")}
|
||||
width={700}
|
||||
height={420}
|
||||
className="location-map-img"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
type Props = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export default function PageHero({ title }: Props) {
|
||||
return (
|
||||
<section className="page-hero">
|
||||
<div className="container">
|
||||
<h1>{title}</h1>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHero from "@/components/PageHero";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function PageShell({ title, children }: Props) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main>
|
||||
<PageHero title={title} />
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import Image from "next/image";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
const benefitKeys = [
|
||||
"quality",
|
||||
"lessCement",
|
||||
"whiteness",
|
||||
"easyWork",
|
||||
"finish",
|
||||
] as const;
|
||||
|
||||
const useKeys = ["whitework", "grout", "mosaic", "slurry"] as const;
|
||||
|
||||
export default async function PowderFeatures() {
|
||||
const t = await getTranslations("Powder");
|
||||
|
||||
return (
|
||||
<section className="section powder-features">
|
||||
<div className="container">
|
||||
<div className="section-head section-head-center">
|
||||
<h2>{t("title")}</h2>
|
||||
<Image
|
||||
src="/images/infoitems/line.png"
|
||||
alt=""
|
||||
width={151}
|
||||
height={5}
|
||||
className="section-line"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="powder-grid">
|
||||
<div className="powder-col">
|
||||
<div className="powder-photo shadow-box">
|
||||
<Image
|
||||
src="/images/info/1.jpg"
|
||||
alt={t("benefitsImageAlt")}
|
||||
width={700}
|
||||
height={480}
|
||||
/>
|
||||
</div>
|
||||
<div className="powder-text">
|
||||
<h3>{t("benefitsTitle")}</h3>
|
||||
<p className="dots">. . . . . . . . . . . . . . . . . . . . . . . .</p>
|
||||
<ul>
|
||||
{benefitKeys.map((key) => (
|
||||
<li key={key}>{t(`benefits.${key}`)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="powder-col powder-col-reverse">
|
||||
<div className="powder-text">
|
||||
<h3>{t("usesTitle")}</h3>
|
||||
<p className="dots">. . . . . . . . . . . . . . . . . . . . . . . . .</p>
|
||||
<ul>
|
||||
{useKeys.map((key) => (
|
||||
<li key={key}>{t(`uses.${key}`)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="powder-photo shadow-box">
|
||||
<Image
|
||||
src="/images/info/2.jpg"
|
||||
alt={t("usesImageAlt")}
|
||||
width={700}
|
||||
height={480}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import Image from "next/image";
|
||||
import { getTranslations, getLocale } from "next-intl/server";
|
||||
import type { Product } from "@/data/products";
|
||||
import { formatPrice } from "@/data/products";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
|
||||
type Props = {
|
||||
product: Product;
|
||||
};
|
||||
|
||||
export default async function ProductDetail({ product }: Props) {
|
||||
const t = await getTranslations("Products");
|
||||
const locale = await getLocale();
|
||||
const title = t(`items.${product.slug}.title`);
|
||||
const description = t(`items.${product.slug}.description`);
|
||||
|
||||
return (
|
||||
<section className="section product-detail">
|
||||
<div className="container product-detail-grid">
|
||||
<div className="product-detail-media shadow-box">
|
||||
<Image
|
||||
src={product.image}
|
||||
alt={title}
|
||||
width={800}
|
||||
height={800}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="product-detail-info">
|
||||
<h2>{title}</h2>
|
||||
<hr />
|
||||
<p className="product-detail-desc">{description}</p>
|
||||
<p className="product-detail-price">
|
||||
{formatPrice(product.price, locale, t("currency"))}
|
||||
</p>
|
||||
<Link href="/products" className="btn-theme">
|
||||
{t("backToProducts")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import Image from "next/image";
|
||||
import { getTranslations, getLocale } from "next-intl/server";
|
||||
import type { Product } from "@/data/products";
|
||||
import { formatPrice } from "@/data/products";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
|
||||
type Props = {
|
||||
products: Product[];
|
||||
};
|
||||
|
||||
export default async function ProductGrid({ products }: Props) {
|
||||
const t = await getTranslations("Products");
|
||||
const locale = await getLocale();
|
||||
|
||||
return (
|
||||
<div className="product-grid">
|
||||
{products.map((product) => {
|
||||
const title = t(`items.${product.slug}.title`);
|
||||
return (
|
||||
<article key={product.id} className="product-tile">
|
||||
<Link href={`/products/${product.slug}`} className="product-tile-link">
|
||||
<div className="product-tile-img">
|
||||
<Image
|
||||
src={product.image}
|
||||
alt={title}
|
||||
width={500}
|
||||
height={500}
|
||||
/>
|
||||
</div>
|
||||
<h2>{title}</h2>
|
||||
<p className="product-tile-price">
|
||||
{formatPrice(product.price, locale, t("currency"))}
|
||||
</p>
|
||||
</Link>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import Image from "next/image";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
const productionKeys = [
|
||||
"clay",
|
||||
"mineralPowder",
|
||||
"joshaghan",
|
||||
"superChalk",
|
||||
"buildingLime",
|
||||
"limestone",
|
||||
"superPowder",
|
||||
"sand",
|
||||
] as const;
|
||||
|
||||
export default async function Productions() {
|
||||
const t = await getTranslations("Productions");
|
||||
|
||||
return (
|
||||
<section className="section productions">
|
||||
<div className="container">
|
||||
<div className="productions-grid">
|
||||
<div className="productions-list">
|
||||
<div className="section-head">
|
||||
<h2>{t("title")}</h2>
|
||||
<Image
|
||||
src="/images/infoitems/line.png"
|
||||
alt=""
|
||||
width={151}
|
||||
height={5}
|
||||
className="section-line"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="production-items">
|
||||
{productionKeys.map((key) => (
|
||||
<article key={key} className="production-item">
|
||||
<h3>{t(`items.${key}.title`)}</h3>
|
||||
<p>{t(`items.${key}.description`)}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="productions-visual">
|
||||
<Image
|
||||
src="/images/infoitems/1.png"
|
||||
alt={t("imageAlt")}
|
||||
width={640}
|
||||
height={720}
|
||||
className="productions-img"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRef } from "react";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
|
||||
const carouselItems = [
|
||||
{ key: "aryanClay", image: "/images/infoitems1/1.jpg" },
|
||||
{ key: "ore", image: "/images/infoitems1/2.jpg" },
|
||||
{ key: "superPowder", image: "/images/files/Bd4HF0Mm3G.jpg" },
|
||||
{ key: "mineralPowder", image: "/images/infoitems1/4.jpg" },
|
||||
{ key: "ore", image: "/images/infoitems1/2.jpg", id: "ore-2" },
|
||||
{ key: "joshaghan", image: "/images/infoitems1/3.jpg" },
|
||||
{ key: "superChalk", image: "/images/files/eADGpxlT4k.jpg" },
|
||||
] as const;
|
||||
|
||||
export default function ProductsCarousel() {
|
||||
const t = useTranslations("Carousel");
|
||||
const trackRef = useRef<HTMLUListElement>(null);
|
||||
|
||||
const scrollBy = (dir: 1 | -1) => {
|
||||
const el = trackRef.current;
|
||||
if (!el) return;
|
||||
const amount = el.clientWidth * 0.7;
|
||||
el.scrollBy({ left: dir * -amount, behavior: "smooth" });
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="section products-carousel">
|
||||
<div className="container">
|
||||
<div className="section-head section-head-center">
|
||||
<h2>{t("title")}</h2>
|
||||
<Image
|
||||
src="/images/infoitems/line.png"
|
||||
alt=""
|
||||
width={151}
|
||||
height={5}
|
||||
className="section-line"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="carousel-wrap">
|
||||
<button
|
||||
type="button"
|
||||
className="carousel-btn prev"
|
||||
aria-label={t("prev")}
|
||||
onClick={() => scrollBy(-1)}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
|
||||
<ul className="carousel-track" ref={trackRef}>
|
||||
{carouselItems.map((item, index) => {
|
||||
const title = t(`items.${item.key}`);
|
||||
return (
|
||||
<li key={`${"id" in item ? item.id : item.key}-${index}`}>
|
||||
<Link href="/products" className="product-card-link">
|
||||
<article className="product-card">
|
||||
<div className="product-card-img">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={title}
|
||||
width={700}
|
||||
height={400}
|
||||
/>
|
||||
</div>
|
||||
<p>{title}</p>
|
||||
</article>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="carousel-btn next"
|
||||
aria-label={t("next")}
|
||||
onClick={() => scrollBy(1)}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user