Add FA, EN, and AR static copy, with English LTR and catalog hidden outside Persian.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
51fb9f6a26
commit
6543c994c3
+7
-3
@@ -12,7 +12,10 @@
|
||||
## Stack
|
||||
- Next.js 15 (App Router) + React 19 + TypeScript
|
||||
- Tailwind CSS v4 (utility import) + custom CSS in `src/app/globals.css`
|
||||
- Language: `fa`, direction: `rtl`
|
||||
- Language: `fa` (default, RTL), `en` (LTR), `ar` (RTL)
|
||||
- Static copy lives in `src/i18n/fa.ts`, `src/i18n/en.ts`, `src/i18n/ar.ts`
|
||||
- Locale cookie: `kaman-locale` (also mirrored in localStorage)
|
||||
- Products and blog are **FA-only** for now (hidden in EN/AR nav, homepage, and redirected away)
|
||||
- Fonts:
|
||||
- `iran-yekan` from Meshkee CDN (Persian UI / body)
|
||||
- `Rajdhani` via `next/font/google` (English UI, class `.en`)
|
||||
@@ -37,12 +40,12 @@
|
||||
| `/about` | درباره ما |
|
||||
| `/contact` | تماس با ما |
|
||||
|
||||
Header (RTL): logo + menu on the right; search, language (FA/EN), and account on the left.
|
||||
Header (RTL for FA/AR, LTR for EN): logo + menu on the inline-start; search, language (FA/EN/AR), and account on the inline-end.
|
||||
- Signed out: icon links to `https://customer.kamancable.ir`
|
||||
- Signed in: `{firstName} عزیز` dropdown — پروفایل من, خروج, and مدیریت وبسایت if the user is a website admin
|
||||
- Session: shared cookies `meshkee_customer_access_token` / refresh, hydrated into localStorage; `/api/auth/me` proxies `GET /auth/me`
|
||||
|
||||
Homepage product cards are Meshkee `categories` slot teasers (duplicatable; empty API keeps `src/data/site.ts`). The full catalog is `/products`.
|
||||
Homepage product cards and blog carousel are FA-only. EN and AR hide products/blog (nav, homepage, and `/products` `/blog` redirect home). The full catalog remains `/products` in FA.
|
||||
Homepage blog carousel prefers Meshkee blogs, with `src/data/site.ts` as fallback.
|
||||
|
||||
## Meshkee API integration
|
||||
@@ -120,6 +123,7 @@ Rules:
|
||||
## Important paths
|
||||
- Layout / shell: `src/app/layout.tsx`, `src/components/Header.tsx`, `src/components/AccountMenu.tsx`, `src/components/Footer.tsx`
|
||||
- Home: `src/app/page.tsx`, `src/components/HeroSlider.tsx`, `src/components/ContactBar.tsx`
|
||||
- i18n: `src/i18n/config.ts`, `src/i18n/fa.ts`, `src/i18n/en.ts`, `src/i18n/ar.ts`, `src/i18n/locale-context.tsx`
|
||||
- Products: `src/app/products/page.tsx`, `src/app/products/[id]/[slug]/page.tsx`
|
||||
- Blog: `src/app/blog/page.tsx`, `src/app/blog/[id]/[slug]/page.tsx`
|
||||
- Config: `src/lib/config.ts`
|
||||
|
||||
+43
-45
@@ -1,104 +1,102 @@
|
||||
import SlotImage from "@/components/SlotImage";
|
||||
import {
|
||||
aboutGallery,
|
||||
aboutIntro,
|
||||
laboratoryText,
|
||||
machineryText,
|
||||
qualityPolicy,
|
||||
site,
|
||||
stats,
|
||||
} from "@/data/site";
|
||||
import { aboutGallery } from "@/data/site";
|
||||
import { getDictionary } from "@/i18n/get-locale";
|
||||
import { pageMetadata } from "@/lib/seo";
|
||||
import { fetchStaticImageSlots, overlaySlotImages, pickSlot } from "@/lib/static-images";
|
||||
|
||||
export const metadata = pageMetadata({
|
||||
title: "درباره ما",
|
||||
description:
|
||||
"آشنایی با شرکت صنایع کابل کمان؛ تولیدکننده سیم و کابل فشار ضعیف، مخابراتی و ابزار دقیق از سال ۱۳۷۰ در زنجان.",
|
||||
path: "/about",
|
||||
image: "/images/samples/cables-coils.jpg",
|
||||
});
|
||||
export async function generateMetadata() {
|
||||
const { locale, t } = await getDictionary();
|
||||
return pageMetadata({
|
||||
title: t.aboutPage.title,
|
||||
description: t.aboutPage.metaDescription,
|
||||
path: "/about",
|
||||
image: "/images/samples/cables-coils.jpg",
|
||||
locale,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function AboutPage() {
|
||||
const { locale, t } = await getDictionary();
|
||||
const slots = await fetchStaticImageSlots("about");
|
||||
const hero = overlaySlotImages(
|
||||
pickSlot(slots, "about-hero"),
|
||||
[{ id: "about-hero", image: "/images/samples/cables-coils.jpg", title: "درباره ما" }],
|
||||
[{ id: "about-hero", image: "/images/samples/cables-coils.jpg", title: t.aboutPage.title }],
|
||||
"about-hero",
|
||||
);
|
||||
const gallery = overlaySlotImages(
|
||||
pickSlot(slots, "about-gallery"),
|
||||
aboutGallery.map((item) => ({ id: item.id, image: item.image, title: item.title })),
|
||||
aboutGallery.map((item, index) => ({
|
||||
id: item.id,
|
||||
image: item.image,
|
||||
title: t.aboutPage.galleryItems[index] ?? item.title,
|
||||
})),
|
||||
"about-gallery",
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-hero">
|
||||
<SlotImage src={hero.items[0]?.image || "/images/samples/cables-coils.jpg"} alt={hero.items[0]?.title || "درباره صنایع کابل کمان"} fill />
|
||||
<SlotImage src={hero.items[0]?.image || "/images/samples/cables-coils.jpg"} alt={hero.items[0]?.title || t.aboutPage.heroAlt} fill />
|
||||
<div className="container">
|
||||
<h1>درباره ما</h1>
|
||||
<p className="en">About Us</p>
|
||||
<h1>{t.aboutPage.title}</h1>
|
||||
{locale !== "en" ? <p className="en">{t.aboutPage.kicker}</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section">
|
||||
<div className="container about-blocks">
|
||||
<div className="stats">
|
||||
{stats.map((stat) => (
|
||||
{t.aboutPage.stats.map((stat) => (
|
||||
<article key={stat.label} className="stat">
|
||||
<strong>{stat.value}</strong>
|
||||
<div>{stat.label}</div>
|
||||
<p className="en">{stat.labelEn}</p>
|
||||
{stat.kicker && stat.kicker !== stat.label ? <p className="en">{stat.kicker}</p> : null}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<article className="about-block">
|
||||
<h2>شرکت کابل کمان</h2>
|
||||
<p className="muted">{aboutIntro}</p>
|
||||
<h2>{t.aboutPage.companyTitle}</h2>
|
||||
<p className="muted">{t.aboutPage.intro}</p>
|
||||
</article>
|
||||
|
||||
<article className="about-block">
|
||||
<h2>اهداف</h2>
|
||||
<p className="en">Quality Policy</p>
|
||||
<p className="muted">صداقت بهترین روش است.</p>
|
||||
<p className="muted">
|
||||
شرکت صنایع کابل کمان بهعنوان یکی از تولیدکنندگان سیم و کابل، رضایت مشتریان را از طریق ارتقاء کیفیت و
|
||||
رعایت کلیه قوانین و الزامات حاکم بر محصول خود، بهعنوان برترین هدف برگزیده است.
|
||||
</p>
|
||||
<h2>{t.aboutPage.goalsTitle}</h2>
|
||||
{locale !== "en" ? <p className="en">{t.aboutPage.goalsKicker}</p> : null}
|
||||
<p className="muted">{t.aboutPage.honesty}</p>
|
||||
<p className="muted">{t.aboutPage.goalsBody}</p>
|
||||
<ol className="muted">
|
||||
{qualityPolicy.map((item) => (
|
||||
{t.aboutTabs.qualityPolicy.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ol>
|
||||
</article>
|
||||
|
||||
<article className="about-block">
|
||||
<h2>آزمایشگاه صنایع کابل کمان</h2>
|
||||
<p className="en">Laboratory</p>
|
||||
<p className="muted">{laboratoryText}</p>
|
||||
<h2>{t.aboutPage.labTitle}</h2>
|
||||
{locale !== "en" ? <p className="en">{t.aboutPage.labKicker}</p> : null}
|
||||
<p className="muted">{t.aboutPage.labText}</p>
|
||||
</article>
|
||||
|
||||
<article className="about-block">
|
||||
<h2>تجهیزات و ماشینآلات</h2>
|
||||
<p className="en">Equipment & Machinery</p>
|
||||
<p className="muted">{machineryText}</p>
|
||||
<h2>{t.aboutPage.machineryTitle}</h2>
|
||||
{locale !== "en" ? <p className="en">{t.aboutPage.machineryKicker}</p> : null}
|
||||
<p className="muted">{t.aboutPage.machineryText}</p>
|
||||
</article>
|
||||
|
||||
<article className="about-block">
|
||||
<h2>ضمانتنامه شرکت</h2>
|
||||
<p className="en">Guarantee</p>
|
||||
<p className="muted">{machineryText}</p>
|
||||
<h2>{t.aboutPage.guaranteeTitle}</h2>
|
||||
{locale !== "en" ? <p className="en">{t.aboutPage.guaranteeKicker}</p> : null}
|
||||
<p className="muted">{t.aboutPage.machineryText}</p>
|
||||
</article>
|
||||
|
||||
<article className="about-block">
|
||||
<h2>گالری</h2>
|
||||
<p className="en">Gallery</p>
|
||||
<h2>{t.aboutPage.galleryTitle}</h2>
|
||||
{locale !== "en" ? <p className="en">{t.aboutPage.galleryKicker}</p> : null}
|
||||
<div className="gallery">
|
||||
{gallery.items.map((item) => (
|
||||
<div key={item.id} className="gallery-item">
|
||||
<SlotImage src={item.image} alt={item.title || site.shortName} />
|
||||
<SlotImage src={item.image} alt={item.title || t.brand.shortName} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+25
-18
@@ -1,50 +1,57 @@
|
||||
import ContactForm from "@/components/ContactForm";
|
||||
import SlotImage from "@/components/SlotImage";
|
||||
import { site } from "@/data/site";
|
||||
import { getDictionary } from "@/i18n/get-locale";
|
||||
import { pageMetadata } from "@/lib/seo";
|
||||
import { fetchStaticImageSlots, overlaySlotImages, pickSlot } from "@/lib/static-images";
|
||||
|
||||
export const metadata = pageMetadata({
|
||||
title: "تماس با ما",
|
||||
description: `ارتباط با صنایع کابل کمان. دفتر تهران: ${site.address}. تلفن ${site.phoneFa}.`,
|
||||
path: "/contact",
|
||||
image: "/images/slider/bg3.jpg",
|
||||
});
|
||||
export async function generateMetadata() {
|
||||
const { locale, t } = await getDictionary();
|
||||
return pageMetadata({
|
||||
title: t.contactPage.title,
|
||||
description: `${t.contactPage.metaDescription} ${t.contactInfo.address}. ${t.contactInfo.phoneLabel} ${site.phone}.`,
|
||||
path: "/contact",
|
||||
image: "/images/slider/bg3.jpg",
|
||||
locale,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function ContactPage() {
|
||||
const { locale, t } = await getDictionary();
|
||||
const slots = await fetchStaticImageSlots("contact");
|
||||
const hero = overlaySlotImages(
|
||||
pickSlot(slots, "contact-hero"),
|
||||
[{ id: "contact-hero", image: "/images/slider/bg3.jpg", title: "تماس با ما" }],
|
||||
[{ id: "contact-hero", image: "/images/slider/bg3.jpg", title: t.contactPage.title }],
|
||||
"contact-hero",
|
||||
);
|
||||
const phone = locale === "fa" ? site.phoneFa : site.phone;
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-hero">
|
||||
<SlotImage src={hero.items[0]?.image || "/images/slider/bg3.jpg"} alt={hero.items[0]?.title || "تماس با صنایع کابل کمان"} fill />
|
||||
<SlotImage src={hero.items[0]?.image || "/images/slider/bg3.jpg"} alt={hero.items[0]?.title || t.contactPage.heroAlt} fill />
|
||||
<div className="container">
|
||||
<h1>با ما در ارتباط باشید</h1>
|
||||
<p className="en">Contact Us</p>
|
||||
<h1>{t.contactPage.heading}</h1>
|
||||
{locale !== "en" ? <p className="en">{t.contactPage.kicker}</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section">
|
||||
<div className="container contact-grid">
|
||||
<div className="info-card">
|
||||
<h2>اطلاعات تماس</h2>
|
||||
<h2>{t.contactPage.infoTitle}</h2>
|
||||
<div className="info-row">
|
||||
<span className="kicker en">Address</span>
|
||||
<p>{site.address}</p>
|
||||
<span className="kicker en">{t.contactInfo.addressEn}</span>
|
||||
<p>{t.contactInfo.address}</p>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span className="kicker en">Factory</span>
|
||||
<p>{site.factory}</p>
|
||||
<span className="kicker en">{t.contactInfo.factoryEn}</span>
|
||||
<p>{t.contactInfo.factory}</p>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span className="kicker en">Phone</span>
|
||||
<span className="kicker en">{t.contactInfo.phoneEn}</span>
|
||||
<p>
|
||||
<a href={`tel:${site.phone.replace(/-/g, "")}`}>{site.phoneFa}</a>
|
||||
<a href={`tel:${site.phone.replace(/-/g, "")}`}>{phone}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,7 +59,7 @@ export default async function ContactPage() {
|
||||
</div>
|
||||
<div className="container map-wrap">
|
||||
<iframe
|
||||
title="موقعیت دفتر صنایع کابل کمان"
|
||||
title={t.contactPage.mapTitle}
|
||||
src="https://www.openstreetmap.org/export/embed.html?bbox=51.412%2C35.705%2C51.425%2C35.713&layer=mapnik&marker=35.709%2C51.418"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
+6
-1
@@ -64,7 +64,12 @@ body {
|
||||
color: var(--white);
|
||||
font-family: var(--font-fa);
|
||||
font-weight: 300;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
html[lang="en"],
|
||||
html[lang="en"] body {
|
||||
font-family: var(--font-en), "Rajdhani", sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
+29
-19
@@ -4,7 +4,8 @@ import { Oswald, Rajdhani } from "next/font/google";
|
||||
import Footer from "@/components/Footer";
|
||||
import Header from "@/components/Header";
|
||||
import Providers from "@/components/Providers";
|
||||
import { site } from "@/data/site";
|
||||
import { getDictionary } from "@/i18n/get-locale";
|
||||
import { localeDir, localeHtml, localeOg } from "@/i18n/config";
|
||||
import { websiteDomain } from "@/lib/config";
|
||||
import { getBusinessId } from "@/lib/meshkee";
|
||||
|
||||
@@ -24,22 +25,25 @@ const oswald = Oswald({
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(`https://${websiteDomain}`),
|
||||
title: {
|
||||
default: `${site.shortName} | ${site.name}`,
|
||||
template: `%s | ${site.shortName}`,
|
||||
},
|
||||
description: site.description,
|
||||
icons: {
|
||||
icon: "/favicon.png",
|
||||
},
|
||||
openGraph: {
|
||||
locale: "fa_IR",
|
||||
siteName: site.shortName,
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const { locale, t } = await getDictionary();
|
||||
return {
|
||||
metadataBase: new URL(`https://${websiteDomain}`),
|
||||
title: {
|
||||
default: `${t.brand.shortName} | ${t.brand.name}`,
|
||||
template: `%s | ${t.brand.shortName}`,
|
||||
},
|
||||
description: t.brand.description,
|
||||
icons: {
|
||||
icon: "/favicon.png",
|
||||
},
|
||||
openGraph: {
|
||||
locale: localeOg[locale],
|
||||
siteName: t.brand.shortName,
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
@@ -47,11 +51,17 @@ export default async function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const businessId = await getBusinessId();
|
||||
const { locale } = await getDictionary();
|
||||
|
||||
return (
|
||||
<html lang="fa" dir="rtl" className={`${rajdhani.variable} ${oswald.variable}`}>
|
||||
<html
|
||||
lang={localeHtml[locale]}
|
||||
dir={localeDir[locale]}
|
||||
className={`${rajdhani.variable} ${oswald.variable}`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body>
|
||||
<Providers businessId={businessId || "24"}>
|
||||
<Providers businessId={businessId || "24"} locale={locale}>
|
||||
<div className="wrapper">
|
||||
<Header />
|
||||
<main>{children}</main>
|
||||
|
||||
+126
-101
@@ -10,84 +10,105 @@ import SlotImage from "@/components/SlotImage";
|
||||
import SlotLink from "@/components/SlotLink";
|
||||
import { blogHref, blogImage, fetchBlogs } from "@/lib/blog";
|
||||
import { blogPosts, clients, products, site, slides } from "@/data/site";
|
||||
import { showsCatalog } from "@/i18n/config";
|
||||
import { getDictionary } from "@/i18n/get-locale";
|
||||
import { pageMetadata } from "@/lib/seo";
|
||||
import { fetchStaticImageSlots, overlaySlotImages, pickSlot, slotRatioStyle } from "@/lib/static-images";
|
||||
|
||||
export const metadata = pageMetadata({
|
||||
title: `${site.shortName} | ${site.name}`,
|
||||
description: site.description,
|
||||
path: "/",
|
||||
image: "/images/slider/bg1.jpg",
|
||||
absolute: true,
|
||||
});
|
||||
export async function generateMetadata() {
|
||||
const { locale, t } = await getDictionary();
|
||||
return pageMetadata({
|
||||
title: `${t.brand.shortName} | ${t.brand.name}`,
|
||||
description: t.brand.description,
|
||||
path: "/",
|
||||
image: "/images/slider/bg1.jpg",
|
||||
absolute: true,
|
||||
locale,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
const { locale, t } = await getDictionary();
|
||||
const catalog = showsCatalog(locale);
|
||||
const slots = await fetchStaticImageSlots("home");
|
||||
const slider = overlaySlotImages(
|
||||
pickSlot(slots, "slider"),
|
||||
slides.map((slide) => ({
|
||||
slides.map((slide, index) => ({
|
||||
id: slide.id,
|
||||
image: slide.image,
|
||||
title: slide.title,
|
||||
kicker: slide.kicker,
|
||||
title: t.hero.slides[index]?.title ?? slide.title,
|
||||
kicker: t.hero.slides[index]?.kicker ?? slide.kicker,
|
||||
})),
|
||||
"slider",
|
||||
);
|
||||
const aboutImage = overlaySlotImages(
|
||||
pickSlot(slots, "about-us-image"),
|
||||
[{ id: "about", image: "/images/about/factory.jpg", title: "کارخانه صنایع کابل کمان" }],
|
||||
[{ id: "about", image: "/images/about/factory.jpg", title: t.home.aboutImageAlt }],
|
||||
"about-us-image",
|
||||
);
|
||||
const productItems = overlaySlotImages(
|
||||
pickSlot(slots, "categories"),
|
||||
products.map((product) => ({
|
||||
id: product.id,
|
||||
image: product.image,
|
||||
title: product.title,
|
||||
kicker: product.titleEn,
|
||||
text: product.text,
|
||||
})),
|
||||
products.map((product) => {
|
||||
const copy = t.categories.find((item) => item.id === product.id);
|
||||
return {
|
||||
id: product.id,
|
||||
image: product.image,
|
||||
title: copy?.title ?? product.title,
|
||||
kicker: copy?.kicker ?? product.titleEn,
|
||||
text: copy?.text ?? product.text,
|
||||
};
|
||||
}),
|
||||
"categories",
|
||||
);
|
||||
const clientItems = overlaySlotImages(
|
||||
pickSlot(slots, "partners"),
|
||||
clients.map((client) => ({
|
||||
clients.map((client, index) => ({
|
||||
id: client.id,
|
||||
image: client.image,
|
||||
title: client.title,
|
||||
title: t.partners[index] ?? client.title,
|
||||
})),
|
||||
"partners",
|
||||
);
|
||||
const apiBlogs = await fetchBlogs({ pageSize: 6 });
|
||||
const blogFallbacks = apiBlogs.items.length
|
||||
? apiBlogs.items.map((post) => ({
|
||||
id: post.id,
|
||||
image: blogImage(post),
|
||||
title: post.title,
|
||||
text: post.abstract || undefined,
|
||||
href: blogHref(post),
|
||||
}))
|
||||
: blogPosts.map((post) => ({
|
||||
id: post.id,
|
||||
image: post.image,
|
||||
title: post.title,
|
||||
kicker: post.date,
|
||||
text: post.text,
|
||||
href: blogHref(post),
|
||||
}));
|
||||
const blogItems = overlaySlotImages(pickSlot(slots, "home-blog"), blogFallbacks, "home-blog");
|
||||
|
||||
const sliderItems = slider.items.map((item, index) => ({
|
||||
...item,
|
||||
title: t.hero.slides[index]?.title ?? item.title,
|
||||
kicker: t.hero.slides[index]?.kicker ?? item.kicker,
|
||||
}));
|
||||
|
||||
let blogItems = overlaySlotImages(undefined, [], "home-blog");
|
||||
if (catalog) {
|
||||
const apiBlogs = await fetchBlogs({ pageSize: 6 });
|
||||
const blogFallbacks = apiBlogs.items.length
|
||||
? apiBlogs.items.map((post) => ({
|
||||
id: post.id,
|
||||
image: blogImage(post),
|
||||
title: post.title,
|
||||
text: post.abstract || undefined,
|
||||
href: blogHref(post),
|
||||
}))
|
||||
: blogPosts.map((post) => ({
|
||||
id: post.id,
|
||||
image: post.image,
|
||||
title: post.title,
|
||||
kicker: post.date,
|
||||
text: post.text,
|
||||
href: blogHref(post),
|
||||
}));
|
||||
blogItems = overlaySlotImages(pickSlot(slots, "home-blog"), blogFallbacks, "home-blog");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="sr-only">{site.name}</h1>
|
||||
<HeroSlider slides={slider.items} aspectRatio={slider.aspectRatio} />
|
||||
<h1 className="sr-only">{t.brand.name}</h1>
|
||||
<HeroSlider slides={sliderItems} aspectRatio={slider.aspectRatio} />
|
||||
|
||||
<section className="app-strip">
|
||||
<Reveal>
|
||||
<div className="container app-strip-inner">
|
||||
<h2>اپلیکیشن اختصاصی</h2>
|
||||
<h2>{t.home.appTitle}</h2>
|
||||
<a className="btn btn-gold" href={site.appUrl} target="_blank" rel="noopener noreferrer">
|
||||
دانلود اپلیکیشن
|
||||
{t.home.appCta}
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
@@ -100,7 +121,7 @@ export default async function HomePage() {
|
||||
<SlotLink href={aboutImage.items[0]?.href}>
|
||||
<SlotImage
|
||||
src={aboutImage.items[0]?.image || "/images/about/factory.jpg"}
|
||||
alt={aboutImage.items[0]?.title || "کارخانه صنایع کابل کمان"}
|
||||
alt={aboutImage.items[0]?.title || t.home.aboutImageAlt}
|
||||
fill
|
||||
/>
|
||||
</SlotLink>
|
||||
@@ -110,14 +131,14 @@ export default async function HomePage() {
|
||||
<div>
|
||||
<SectionTitle
|
||||
index="01"
|
||||
en="About"
|
||||
title="صنایع کابل کمان"
|
||||
text={site.tagline}
|
||||
en={t.home.aboutKicker}
|
||||
title={t.home.aboutTitle}
|
||||
text={t.brand.tagline}
|
||||
/>
|
||||
<AboutTabs />
|
||||
<p className="about-cta">
|
||||
<a className="about-contact-link" href="#contact">
|
||||
تماس با ما
|
||||
{t.home.contactCta}
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M7.2 3.8h2.6l1.2 3-1.6 1.1a12.5 12.5 0 0 0 5.7 5.7l1.1-1.6 3 1.2v2.6c0 .7-.6 1.4-1.3 1.5-2 .3-6.3-.2-10.1-4S5.4 6.9 5.7 4.9c.1-.7.8-1.1 1.5-1.1Z"
|
||||
@@ -137,71 +158,75 @@ export default async function HomePage() {
|
||||
<ContactBar />
|
||||
</Reveal>
|
||||
|
||||
<section className="section" id="products">
|
||||
<div className="container">
|
||||
<Reveal>
|
||||
<SectionTitle
|
||||
index="02"
|
||||
en="Products"
|
||||
title="محصولات کابل و سیم"
|
||||
text="تنوع کامل سیم و کابل فشار ضعیف، مخابراتی، شبکه و ابزار دقیق، تولیدشده مطابق با استانداردهای ملی و بینالمللی."
|
||||
/>
|
||||
</Reveal>
|
||||
<Reveal stagger className="product-grid" style={slotRatioStyle(productItems.aspectRatio)}>
|
||||
{productItems.items.map((item, index) => (
|
||||
<article
|
||||
key={item.id}
|
||||
className="product-card"
|
||||
id={`product-${item.id}`}
|
||||
style={{ "--d": index } as CSSProperties}
|
||||
>
|
||||
<div className="thumb">
|
||||
<SlotLink href={item.href}>
|
||||
<SlotImage src={item.image} alt={item.title || "محصول کابل کمان"} fill />
|
||||
</SlotLink>
|
||||
</div>
|
||||
<div className="body">
|
||||
<h3>{item.title}</h3>
|
||||
{item.kicker ? <p className="en">{item.kicker}</p> : null}
|
||||
{item.text ? <p className="product-excerpt">{item.text}</p> : null}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
{catalog ? (
|
||||
<section className="section" id="products">
|
||||
<div className="container">
|
||||
<Reveal>
|
||||
<SectionTitle
|
||||
index="02"
|
||||
en={t.home.productsKicker}
|
||||
title={t.home.productsTitle}
|
||||
text={t.home.productsText}
|
||||
/>
|
||||
</Reveal>
|
||||
<Reveal stagger className="product-grid" style={slotRatioStyle(productItems.aspectRatio)}>
|
||||
{productItems.items.map((item, index) => (
|
||||
<article
|
||||
key={item.id}
|
||||
className="product-card"
|
||||
id={`product-${item.id}`}
|
||||
style={{ "--d": index } as CSSProperties}
|
||||
>
|
||||
<div className="thumb">
|
||||
<SlotLink href={item.href}>
|
||||
<SlotImage src={item.image} alt={item.title || t.home.productImageAlt} fill />
|
||||
</SlotLink>
|
||||
</div>
|
||||
<div className="body">
|
||||
<h3>{item.title}</h3>
|
||||
{item.kicker ? <p className="en">{item.kicker}</p> : null}
|
||||
{item.text ? <p className="product-excerpt">{item.text}</p> : null}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="section" id="blog" style={{ paddingTop: 0 }}>
|
||||
<div className="container">
|
||||
<Reveal>
|
||||
<SectionTitle
|
||||
index="03"
|
||||
en="Blog"
|
||||
title="آخرین اخبار و مقالات"
|
||||
text="تازهترین دانستنیهای فنی و اخبار صنایع سیم و کابل کمان را در این بخش دنبال کنید."
|
||||
/>
|
||||
</Reveal>
|
||||
<Reveal delay={80} style={slotRatioStyle(blogItems.aspectRatio)}>
|
||||
<BlogCarousel posts={blogItems.items} />
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
{catalog ? (
|
||||
<section className="section" id="blog" style={{ paddingTop: 0 }}>
|
||||
<div className="container">
|
||||
<Reveal>
|
||||
<SectionTitle
|
||||
index="03"
|
||||
en={t.home.blogKicker}
|
||||
title={t.home.blogTitle}
|
||||
text={t.home.blogText}
|
||||
/>
|
||||
</Reveal>
|
||||
<Reveal delay={80} style={slotRatioStyle(blogItems.aspectRatio)}>
|
||||
<BlogCarousel posts={blogItems.items} />
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="section" style={{ paddingTop: 0 }}>
|
||||
<section className="section" style={{ paddingTop: catalog ? 0 : undefined }}>
|
||||
<div className="container">
|
||||
<Reveal>
|
||||
<SectionTitle
|
||||
index="04"
|
||||
en="Partners"
|
||||
title="همکاران و مشتریان"
|
||||
text="برندها و مجموعههایی که کیفیت تولید کابل کمان را برای پروژههای خود انتخاب کردهاند."
|
||||
index={catalog ? "04" : "02"}
|
||||
en={t.home.partnersKicker}
|
||||
title={t.home.partnersTitle}
|
||||
text={t.home.partnersText}
|
||||
/>
|
||||
</Reveal>
|
||||
<Reveal stagger className="clients" style={slotRatioStyle(clientItems.aspectRatio)}>
|
||||
{clientItems.items.map((item, index) => (
|
||||
<div key={item.id} className="client" style={{ "--d": index } as CSSProperties}>
|
||||
<SlotLink href={item.href}>
|
||||
<SlotImage src={item.image} alt={item.title || "همکار کابل کمان"} />
|
||||
<SlotImage src={item.image} alt={item.title || t.home.partnerImageAlt} />
|
||||
</SlotLink>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -2,33 +2,34 @@
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { goalsText, qualityPolicy, resumeText } from "@/data/site";
|
||||
import { useMessages } from "@/i18n/locale-context";
|
||||
|
||||
export default function AboutTabs() {
|
||||
const t = useMessages();
|
||||
const [tab, setTab] = useState<"resume" | "goals">("resume");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="tabs" role="tablist">
|
||||
<button type="button" className={tab === "resume" ? "active" : undefined} onClick={() => setTab("resume")}>
|
||||
رزومه
|
||||
{t.aboutTabs.resume}
|
||||
</button>
|
||||
<button type="button" className={tab === "goals" ? "active" : undefined} onClick={() => setTab("goals")}>
|
||||
اهداف
|
||||
{t.aboutTabs.goalsTab}
|
||||
</button>
|
||||
</div>
|
||||
{tab === "resume" ? (
|
||||
<p className="muted">{resumeText}</p>
|
||||
<p className="muted">{t.aboutTabs.resumeText}</p>
|
||||
) : (
|
||||
<div>
|
||||
{goalsText.map((paragraph) => (
|
||||
{t.aboutTabs.goalsParagraphs.map((paragraph) => (
|
||||
<p key={paragraph.slice(0, 24)} className="muted">
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
<p className="muted">خط مشی کیفیت عبارت است از:</p>
|
||||
<p className="muted">{t.aboutTabs.qualityIntro}</p>
|
||||
<ol className="muted">
|
||||
{qualityPolicy.map((item) => (
|
||||
{t.aboutTabs.qualityPolicy.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useEffect, useRef, useState, type MouseEvent } from "react";
|
||||
|
||||
import { interpolate } from "@/i18n/index";
|
||||
import { useMessages } from "@/i18n/locale-context";
|
||||
import { businessDashboardUrl, customerPortalUrl } from "@/lib/config";
|
||||
import { useSession, userDisplayName } from "@/lib/session";
|
||||
|
||||
@@ -35,6 +37,7 @@ function ChevronIcon({ open }: { open: boolean }) {
|
||||
}
|
||||
|
||||
export default function AccountMenu() {
|
||||
const t = useMessages();
|
||||
const { user, isAuthenticated, isWebsiteAdmin, logout, getAccessToken } = useSession();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [handoffBusy, setHandoffBusy] = useState(false);
|
||||
@@ -92,9 +95,9 @@ export default function AccountMenu() {
|
||||
|
||||
if (!isAuthenticated || !user) {
|
||||
return (
|
||||
<a href={customerPortalUrl} className="header-tool header-login" aria-label="ورود">
|
||||
<a href={customerPortalUrl} className="header-tool header-login" aria-label={t.account.login}>
|
||||
<UserIcon />
|
||||
<span>ورود</span>
|
||||
<span>{t.account.login}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -109,17 +112,17 @@ export default function AccountMenu() {
|
||||
aria-haspopup="menu"
|
||||
>
|
||||
<UserIcon />
|
||||
<span>{userDisplayName(user)} عزیز</span>
|
||||
<span>{interpolate(t.account.dear, { name: userDisplayName(user) })}</span>
|
||||
<ChevronIcon open={open} />
|
||||
</button>
|
||||
{open ? (
|
||||
<div role="menu" className="account-menu__dropdown">
|
||||
<a role="menuitem" href={`${customerPortalUrl}/profile`} onClick={() => setOpen(false)}>
|
||||
پروفایل من
|
||||
{t.account.profile}
|
||||
</a>
|
||||
{isWebsiteAdmin ? (
|
||||
<a role="menuitem" href={businessDashboardUrl} onClick={openBusinessDashboard}>
|
||||
{handoffBusy ? "در حال انتقال…" : "مدیریت وبسایت"}
|
||||
{handoffBusy ? t.account.transferring : t.account.manage}
|
||||
</a>
|
||||
) : null}
|
||||
<button
|
||||
@@ -130,7 +133,7 @@ export default function AccountMenu() {
|
||||
logout();
|
||||
}}
|
||||
>
|
||||
خروج
|
||||
{t.account.logout}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
|
||||
import { interpolate } from "@/i18n/index";
|
||||
import { useMessages } from "@/i18n/locale-context";
|
||||
|
||||
export default function CallbackForm() {
|
||||
const t = useMessages();
|
||||
const [status, setStatus] = useState<"idle" | "sending" | "ok" | "error">("idle");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
@@ -21,35 +25,35 @@ export default function CallbackForm() {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
title: "درخواست تماس",
|
||||
title: t.callback.title,
|
||||
cellNumber,
|
||||
text: `درخواست تماس از صفحه اصلی. شماره: ${cellNumber}`,
|
||||
text: interpolate(t.callback.body, { phone: cellNumber }),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
setStatus("error");
|
||||
setMessage("ثبت شماره انجام نشد. لطفاً دوباره تلاش کنید.");
|
||||
setMessage(t.callback.error);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("ok");
|
||||
setMessage("شماره شما ثبت شد. بهزودی تماس میگیریم.");
|
||||
setMessage(t.callback.ok);
|
||||
form.reset();
|
||||
} catch {
|
||||
setStatus("error");
|
||||
setMessage("ارتباط با سرور برقرار نشد.");
|
||||
setMessage(t.callback.network);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="callback-form" onSubmit={onSubmit}>
|
||||
<label className="sr-only" htmlFor="callback-name">
|
||||
نام
|
||||
{t.callback.name}
|
||||
</label>
|
||||
<input id="callback-name" name="name" type="text" required placeholder="نام" autoComplete="name" />
|
||||
<input id="callback-name" name="name" type="text" required placeholder={t.callback.name} autoComplete="name" />
|
||||
<label className="sr-only" htmlFor="callback-phone">
|
||||
شماره تماس
|
||||
{t.callback.phone}
|
||||
</label>
|
||||
<input
|
||||
id="callback-phone"
|
||||
@@ -57,11 +61,11 @@ export default function CallbackForm() {
|
||||
type="tel"
|
||||
required
|
||||
inputMode="tel"
|
||||
placeholder="شماره تماس"
|
||||
placeholder={t.callback.phone}
|
||||
autoComplete="tel"
|
||||
/>
|
||||
<button type="submit" className="btn btn-gold" disabled={status === "sending"}>
|
||||
{status === "sending" ? "در حال ارسال…" : "ثبت شماره"}
|
||||
{status === "sending" ? t.callback.sending : t.callback.submit}
|
||||
</button>
|
||||
{message ? <p className={status === "ok" ? "ok" : "err"}>{message}</p> : null}
|
||||
</form>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
|
||||
import { useMessages } from "@/i18n/locale-context";
|
||||
import { customerPortalUrl } from "@/lib/config";
|
||||
import { formatFaDate } from "@/lib/format";
|
||||
import type { CommentItem } from "@/lib/comments";
|
||||
@@ -16,6 +17,7 @@ export default function CommentBox({
|
||||
entityId: string;
|
||||
comments: CommentItem[];
|
||||
}) {
|
||||
const t = useMessages();
|
||||
const { user, isAuthenticated, ready } = useSession();
|
||||
const [status, setStatus] = useState<"idle" | "sending" | "ok" | "error">("idle");
|
||||
const [message, setMessage] = useState("");
|
||||
@@ -43,27 +45,27 @@ export default function CommentBox({
|
||||
});
|
||||
if (!response.ok) {
|
||||
setStatus("error");
|
||||
setMessage("ارسال نظر انجام نشد. لطفاً دوباره تلاش کنید.");
|
||||
setMessage(t.comments.sendError);
|
||||
return;
|
||||
}
|
||||
setStatus("ok");
|
||||
setMessage("نظر شما ثبت شد و پس از تأیید نمایش داده میشود.");
|
||||
setMessage(t.comments.ok);
|
||||
form.reset();
|
||||
} catch {
|
||||
setStatus("error");
|
||||
setMessage("ارتباط با سرور برقرار نشد.");
|
||||
setMessage(t.contactForm.network);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="comment-box">
|
||||
{comments.length === 0 ? (
|
||||
<p className="comment-empty">هنوز نظری ثبت نشده است.</p>
|
||||
<p className="comment-empty">{t.comments.empty}</p>
|
||||
) : (
|
||||
<div className="comment-list">
|
||||
{comments.map((comment, index) => (
|
||||
<article key={comment.id ?? index} className="comment-item">
|
||||
<strong>{comment.authorName || "کاربر"}</strong>
|
||||
<strong>{comment.authorName || t.comments.user}</strong>
|
||||
{comment.createdAt ? <time>{formatFaDate(comment.createdAt)}</time> : null}
|
||||
<p>{comment.text}</p>
|
||||
</article>
|
||||
@@ -73,9 +75,9 @@ export default function CommentBox({
|
||||
|
||||
{ready && !isAuthenticated ? (
|
||||
<div className="comment-login">
|
||||
<p>برای ثبت نظر وارد حساب کاربری شوید.</p>
|
||||
<p>{t.comments.loginBody}</p>
|
||||
<a className="btn btn-gold" href={customerPortalUrl}>
|
||||
ورود
|
||||
{t.account.login}
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -83,17 +85,17 @@ export default function CommentBox({
|
||||
{ready && isAuthenticated ? (
|
||||
<form className="comment-form" onSubmit={onSubmit}>
|
||||
<label className="sr-only" htmlFor={`comment-${entityType}-${entityId}`}>
|
||||
نظر شما
|
||||
{t.comments.yourComment}
|
||||
</label>
|
||||
<textarea
|
||||
id={`comment-${entityType}-${entityId}`}
|
||||
name="text"
|
||||
rows={4}
|
||||
required
|
||||
placeholder="نظر خود را بنویسید…"
|
||||
placeholder={t.comments.placeholder}
|
||||
/>
|
||||
<button type="submit" className="btn btn-gold" disabled={status === "sending"}>
|
||||
{status === "sending" ? "در حال ارسال…" : "ثبت نظر"}
|
||||
{status === "sending" ? t.comments.sending : t.comments.submit}
|
||||
</button>
|
||||
{message ? <p className={status === "ok" ? "ok" : "err"}>{message}</p> : null}
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import CallbackForm from "@/components/CallbackForm";
|
||||
import { site, socials } from "@/data/site";
|
||||
import { useMessages } from "@/i18n/locale-context";
|
||||
|
||||
function InstagramIcon() {
|
||||
return (
|
||||
@@ -72,12 +75,20 @@ const icons: Record<string, () => ReactNode> = {
|
||||
};
|
||||
|
||||
export default function ContactBar() {
|
||||
const t = useMessages();
|
||||
const socialLabels: Record<string, string> = {
|
||||
instagram: t.social.instagram,
|
||||
telegram: t.social.telegram,
|
||||
linkedin: t.social.linkedin,
|
||||
email: t.social.email,
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="contact-bar" id="contact" aria-label="تماس با ما">
|
||||
<section className="contact-bar" id="contact" aria-label={t.contactBar.label}>
|
||||
<div className="container contact-bar-frame">
|
||||
<div className="contact-bar-inner">
|
||||
<div className="contact-bar-phone">
|
||||
<span className="contact-bar-kicker">تماس با ما</span>
|
||||
<span className="contact-bar-kicker">{t.contactBar.label}</span>
|
||||
<a href={`tel:${site.phone.replace(/-/g, "")}`} className="contact-bar-number">
|
||||
<span dir="ltr">{site.phone}</span>
|
||||
<PhoneIcon />
|
||||
@@ -93,8 +104,8 @@ export default function ContactBar() {
|
||||
className="contact-social"
|
||||
target={item.href.startsWith("mailto:") ? undefined : "_blank"}
|
||||
rel="noopener noreferrer"
|
||||
aria-label={item.label}
|
||||
title={item.label}
|
||||
aria-label={socialLabels[item.id] || item.label}
|
||||
title={socialLabels[item.id] || item.label}
|
||||
>
|
||||
{Icon ? <Icon /> : item.labelEn}
|
||||
</a>
|
||||
@@ -103,7 +114,7 @@ export default function ContactBar() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="contact-bar-subscribe">
|
||||
<p className="contact-bar-subscribe-text">شماره تماس خود را بگذارید، ما با شما تماس میگیریم.</p>
|
||||
<p className="contact-bar-subscribe-text">{t.contactBar.subscribe}</p>
|
||||
<CallbackForm />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
|
||||
import { useLocale, useMessages } from "@/i18n/locale-context";
|
||||
|
||||
export default function ContactForm() {
|
||||
const locale = useLocale();
|
||||
const t = useMessages();
|
||||
const [status, setStatus] = useState<"idle" | "sending" | "ok" | "error">("idle");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
@@ -28,47 +32,49 @@ export default function ContactForm() {
|
||||
|
||||
if (!response.ok) {
|
||||
setStatus("error");
|
||||
setMessage("ارسال پیام انجام نشد. لطفاً دوباره تلاش کنید.");
|
||||
setMessage(t.contactForm.error);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("ok");
|
||||
setMessage("پیام شما ثبت شد. بهزودی با شما تماس میگیریم.");
|
||||
setMessage(t.contactForm.ok);
|
||||
form.reset();
|
||||
} catch {
|
||||
setStatus("error");
|
||||
setMessage("ارتباط با سرور برقرار نشد.");
|
||||
setMessage(t.contactForm.network);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="contact-form" onSubmit={onSubmit}>
|
||||
<h2>پیام خود را ارسال کنید</h2>
|
||||
<p className="en" style={{ margin: 0, color: "var(--gold)" }}>
|
||||
Send a message
|
||||
</p>
|
||||
<h2>{t.contactForm.heading}</h2>
|
||||
{locale !== "en" ? (
|
||||
<p className="en" style={{ margin: 0, color: "var(--gold)" }}>
|
||||
{t.contactForm.kicker}
|
||||
</p>
|
||||
) : null}
|
||||
<label>
|
||||
نام
|
||||
{t.contactForm.name}
|
||||
<input name="name" type="text" required />
|
||||
</label>
|
||||
<label>
|
||||
موضوع
|
||||
{t.contactForm.subject}
|
||||
<input name="title" type="text" required />
|
||||
</label>
|
||||
<label>
|
||||
ایمیل
|
||||
{t.contactForm.email}
|
||||
<input name="email" type="email" />
|
||||
</label>
|
||||
<label>
|
||||
شماره تماس
|
||||
{t.contactForm.phone}
|
||||
<input name="cellNumber" type="tel" />
|
||||
</label>
|
||||
<label>
|
||||
متن پیام
|
||||
{t.contactForm.message}
|
||||
<textarea name="text" required />
|
||||
</label>
|
||||
<button type="submit" className="btn btn-gold" disabled={status === "sending"}>
|
||||
{status === "sending" ? "در حال ارسال…" : "ارسال پیام"}
|
||||
{status === "sending" ? t.contactForm.sending : t.contactForm.submit}
|
||||
</button>
|
||||
{message ? <p className={status === "ok" ? "ok" : "err"}>{message}</p> : null}
|
||||
</form>
|
||||
|
||||
+33
-16
@@ -1,40 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import { navLinks, productsNav, site } from "@/data/site";
|
||||
import { site } from "@/data/site";
|
||||
import { showsCatalog } from "@/i18n/config";
|
||||
import { useLocale, useMessages } from "@/i18n/locale-context";
|
||||
|
||||
export default function Footer() {
|
||||
const locale = useLocale();
|
||||
const t = useMessages();
|
||||
const catalog = showsCatalog(locale);
|
||||
const phone = locale === "fa" ? site.phoneFa : site.phone;
|
||||
|
||||
return (
|
||||
<footer className="site-footer">
|
||||
<div className="container footer-grid">
|
||||
<div>
|
||||
<h3>{site.shortName}</h3>
|
||||
<p>{site.description}</p>
|
||||
<h3>{t.brand.shortName}</h3>
|
||||
<p>{t.brand.description}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>دسترسی سریع</h3>
|
||||
<h3>{t.footer.quickLinks}</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/">{navLinks[0].label}</Link>
|
||||
<Link href="/">{t.nav.home}</Link>
|
||||
</li>
|
||||
{catalog ? (
|
||||
<li>
|
||||
<Link href="/products">{t.nav.products}</Link>
|
||||
</li>
|
||||
) : null}
|
||||
{catalog ? (
|
||||
<li>
|
||||
<Link href="/blog">{t.nav.blog}</Link>
|
||||
</li>
|
||||
) : null}
|
||||
<li>
|
||||
<Link href="/about">{t.nav.about}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href={productsNav.href}>{productsNav.label}</Link>
|
||||
<Link href="/contact">{t.nav.contact}</Link>
|
||||
</li>
|
||||
{navLinks.slice(1).map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link href={link.href}>{link.label}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>تماس با ما</h3>
|
||||
<h3>{t.footer.contact}</h3>
|
||||
<ul>
|
||||
<li>{site.address}</li>
|
||||
<li>{t.contactInfo.address}</li>
|
||||
<li>
|
||||
<a href={`tel:${site.phone.replace(/-/g, "")}`}>{site.phoneFa}</a>
|
||||
<a href={`tel:${site.phone.replace(/-/g, "")}`}>{phone}</a>
|
||||
</li>
|
||||
<li>{site.factory}</li>
|
||||
<li>{t.contactInfo.factory}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +59,7 @@ export default function Footer() {
|
||||
<div className="footer-bottom">
|
||||
<div className="container">
|
||||
<div className="footer-bottom-bar">
|
||||
<div className="footer-bottom-site">{site.name}</div>
|
||||
<div className="footer-bottom-site">{t.brand.name}</div>
|
||||
<a
|
||||
href="https://meshkee.com"
|
||||
target="_blank"
|
||||
|
||||
+87
-97
@@ -5,9 +5,9 @@ import { usePathname, useRouter } from "next/navigation";
|
||||
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import AccountMenu from "@/components/AccountMenu";
|
||||
import { blogPosts, navLinks, products, productsNav, site } from "@/data/site";
|
||||
|
||||
type Locale = "fa" | "en";
|
||||
import { products, site } from "@/data/site";
|
||||
import { localeOptions, showsCatalog } from "@/i18n/config";
|
||||
import { useLocale, useMessages, useSetLocale } from "@/i18n/locale-context";
|
||||
|
||||
function SearchIcon() {
|
||||
return (
|
||||
@@ -29,8 +29,11 @@ function ChevronIcon() {
|
||||
export default function Header() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const locale = useLocale();
|
||||
const t = useMessages();
|
||||
const setLocale = useSetLocale();
|
||||
const catalog = showsCatalog(locale);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [locale, setLocale] = useState<Locale>("fa");
|
||||
const [query, setQuery] = useState("");
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [productsOpen, setProductsOpen] = useState(false);
|
||||
@@ -38,17 +41,6 @@ export default function Header() {
|
||||
const searchRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = window.localStorage.getItem("kaman-locale");
|
||||
if (saved === "en" || saved === "fa") setLocale(saved);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale;
|
||||
document.documentElement.dir = locale === "fa" ? "rtl" : "ltr";
|
||||
window.localStorage.setItem("kaman-locale", locale);
|
||||
}, [locale]);
|
||||
|
||||
useEffect(() => {
|
||||
function onPointer(event: MouseEvent) {
|
||||
if (!searchRef.current?.contains(event.target as Node)) setSearchOpen(false);
|
||||
@@ -81,39 +73,30 @@ export default function Header() {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (q.length < 2) return [];
|
||||
const pages = [
|
||||
...navLinks.map((link) => ({
|
||||
href: link.href,
|
||||
title: locale === "en" ? link.labelEn : link.label,
|
||||
})),
|
||||
{
|
||||
href: "/products",
|
||||
title: locale === "en" ? productsNav.labelEn : productsNav.label,
|
||||
},
|
||||
{
|
||||
href: "/blog",
|
||||
title: locale === "en" ? "Blog" : "وبلاگ",
|
||||
},
|
||||
{ href: "/", title: t.nav.home },
|
||||
...(catalog
|
||||
? [
|
||||
{ href: "/products", title: t.nav.products },
|
||||
{ href: "/blog", title: t.nav.blog },
|
||||
]
|
||||
: []),
|
||||
{ href: "/about", title: t.nav.about },
|
||||
{ href: "/contact", title: t.nav.contact },
|
||||
];
|
||||
const catalog = products.map((product) => ({
|
||||
href: `/products`,
|
||||
title: locale === "en" ? product.titleEn : product.title,
|
||||
}));
|
||||
const articles = blogPosts.map((post) => ({
|
||||
href: `/blog`,
|
||||
title: locale === "en" ? post.titleEn : post.title,
|
||||
}));
|
||||
return [...pages, ...catalog, ...articles].filter((item) => item.title.toLowerCase().includes(q)).slice(0, 8);
|
||||
}, [locale, query]);
|
||||
const catalogItems = catalog
|
||||
? t.categories.map((product) => ({
|
||||
href: `/#product-${product.id}`,
|
||||
title: product.title,
|
||||
}))
|
||||
: [];
|
||||
return [...pages, ...catalogItems].filter((item) => item.title.toLowerCase().includes(q)).slice(0, 8);
|
||||
}, [catalog, query, t]);
|
||||
|
||||
function onSearch(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
const first = results[0];
|
||||
if (!first) return;
|
||||
if (/^https?:\/\//i.test(first.href)) {
|
||||
window.open(first.href, "_blank", "noopener,noreferrer");
|
||||
} else {
|
||||
router.push(first.href);
|
||||
}
|
||||
router.push(first.href);
|
||||
setSearchOpen(false);
|
||||
setQuery("");
|
||||
}
|
||||
@@ -124,10 +107,10 @@ export default function Header() {
|
||||
<div className="header-start">
|
||||
<Link href="/" className="brand" onClick={() => setOpen(false)}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={site.logo} alt={site.shortName} width={32} height={32} />
|
||||
<img src={site.logo} alt={t.brand.shortName} width={32} height={32} />
|
||||
<span className="brand-text">
|
||||
<strong>{locale === "en" ? site.nameEnShort : site.shortName}</strong>
|
||||
<span className="en">{site.nameEnShort}</span>
|
||||
<strong>{t.brand.shortName}</strong>
|
||||
{locale !== "en" ? <span className="en">{t.brand.nameEnShort}</span> : null}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
@@ -135,58 +118,66 @@ export default function Header() {
|
||||
type="button"
|
||||
className="menu-toggle"
|
||||
aria-expanded={open}
|
||||
aria-label={locale === "en" ? "Menu" : "منو"}
|
||||
aria-label={t.nav.menu}
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
{locale === "en" ? "Menu" : "منو"}
|
||||
{t.nav.menu}
|
||||
</button>
|
||||
|
||||
<nav className={open ? "nav open" : "nav"}>
|
||||
{navLinks.slice(0, 1).map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={pathname === "/" && !hash.startsWith("#product") && hash !== "#products" ? "active" : undefined}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{locale === "en" ? link.labelEn : link.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div
|
||||
className={productsOpen ? "nav-dropdown open" : "nav-dropdown"}
|
||||
onMouseEnter={() => setProductsOpen(true)}
|
||||
onMouseLeave={() => setProductsOpen(false)}
|
||||
<Link
|
||||
href="/"
|
||||
className={pathname === "/" && !hash.startsWith("#product") && hash !== "#products" ? "active" : undefined}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<Link
|
||||
href={productsNav.href}
|
||||
className={pathname.startsWith("/products") || hash === "#products" || hash.startsWith("#product-") ? "active" : undefined}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{locale === "en" ? productsNav.labelEn : productsNav.label}
|
||||
<ChevronIcon />
|
||||
</Link>
|
||||
<ul className="nav-sub">
|
||||
{products.map((product) => (
|
||||
<li key={product.id}>
|
||||
<Link href={`/#product-${product.id}`} onClick={() => setOpen(false)}>
|
||||
{locale === "en" ? product.titleEn : product.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{t.nav.home}
|
||||
</Link>
|
||||
|
||||
{navLinks.slice(1).map((link) => (
|
||||
{catalog ? (
|
||||
<div
|
||||
className={productsOpen ? "nav-dropdown open" : "nav-dropdown"}
|
||||
onMouseEnter={() => setProductsOpen(true)}
|
||||
onMouseLeave={() => setProductsOpen(false)}
|
||||
>
|
||||
<Link
|
||||
href="/products"
|
||||
className={pathname.startsWith("/products") || hash === "#products" || hash.startsWith("#product-") ? "active" : undefined}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{t.nav.products}
|
||||
<ChevronIcon />
|
||||
</Link>
|
||||
<ul className="nav-sub">
|
||||
{products.map((product) => {
|
||||
const copy = t.categories.find((item) => item.id === product.id);
|
||||
return (
|
||||
<li key={product.id}>
|
||||
<Link href={`/#product-${product.id}`} onClick={() => setOpen(false)}>
|
||||
{copy?.title || product.title}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{catalog ? (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={pathname === link.href ? "active" : undefined}
|
||||
href="/blog"
|
||||
className={pathname.startsWith("/blog") ? "active" : undefined}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{locale === "en" ? link.labelEn : link.label}
|
||||
{t.nav.blog}
|
||||
</Link>
|
||||
))}
|
||||
) : null}
|
||||
|
||||
<Link href="/about" className={pathname === "/about" ? "active" : undefined} onClick={() => setOpen(false)}>
|
||||
{t.nav.about}
|
||||
</Link>
|
||||
<Link href="/contact" className={pathname === "/contact" ? "active" : undefined} onClick={() => setOpen(false)}>
|
||||
{t.nav.contact}
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -194,7 +185,7 @@ export default function Header() {
|
||||
<div className={searchOpen ? "header-search open" : "header-search"} ref={searchRef}>
|
||||
<form onSubmit={onSearch}>
|
||||
<label className="sr-only" htmlFor="site-search">
|
||||
{locale === "en" ? "Search" : "جستجو"}
|
||||
{t.nav.search}
|
||||
</label>
|
||||
<input
|
||||
id="site-search"
|
||||
@@ -206,13 +197,13 @@ export default function Header() {
|
||||
setSearchOpen(true);
|
||||
}}
|
||||
onFocus={() => setSearchOpen(true)}
|
||||
placeholder={locale === "en" ? "Search" : "جستجو"}
|
||||
placeholder={t.nav.search}
|
||||
/>
|
||||
<button
|
||||
type={searchOpen && query.trim() ? "submit" : "button"}
|
||||
className="header-tool"
|
||||
aria-expanded={searchOpen}
|
||||
aria-label={locale === "en" ? "Search" : "جستجو"}
|
||||
aria-label={t.nav.search}
|
||||
onClick={() => {
|
||||
if (!searchOpen) setSearchOpen(true);
|
||||
}}
|
||||
@@ -240,14 +231,13 @@ export default function Header() {
|
||||
</div>
|
||||
|
||||
<label className="header-lang">
|
||||
<span className="sr-only">{locale === "en" ? "Language" : "زبان"}</span>
|
||||
<select
|
||||
value={locale}
|
||||
onChange={(event) => setLocale(event.target.value as Locale)}
|
||||
aria-label={locale === "en" ? "Language" : "انتخاب زبان"}
|
||||
>
|
||||
<option value="fa">FA</option>
|
||||
<option value="en">EN</option>
|
||||
<span className="sr-only">{t.nav.language}</span>
|
||||
<select value={locale} onChange={(event) => setLocale(event.target.value)} aria-label={t.nav.languageSelect}>
|
||||
{localeOptions.map((option) => (
|
||||
<option key={option.id} value={option.id}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import SlotImage from "@/components/SlotImage";
|
||||
import SlotLink from "@/components/SlotLink";
|
||||
import { heroHighlights } from "@/data/site";
|
||||
import { useLocale, useMessages } from "@/i18n/locale-context";
|
||||
import { slotRatioStyle, type SlotMediaItem } from "@/lib/static-images";
|
||||
|
||||
function RevealLine({
|
||||
@@ -29,9 +29,11 @@ export default function HeroSlider({
|
||||
slides: SlotMediaItem[];
|
||||
aspectRatio?: string;
|
||||
}) {
|
||||
const t = useMessages();
|
||||
const locale = useLocale();
|
||||
const [index, setIndex] = useState(0);
|
||||
const current = slides[index] ?? slides[0];
|
||||
const tabs = heroHighlights.slice(0, Math.max(slides.length, heroHighlights.length));
|
||||
const tabs = t.hero.highlights.slice(0, Math.max(slides.length, t.hero.highlights.length));
|
||||
|
||||
useEffect(() => {
|
||||
if (slides.length < 2) return undefined;
|
||||
@@ -48,7 +50,7 @@ export default function HeroSlider({
|
||||
{slides.map((slide, slideIndex) => (
|
||||
<div key={slide.id} className={slideIndex === index ? "hero-slide active" : "hero-slide"}>
|
||||
<SlotLink href={slide.href}>
|
||||
<SlotImage src={slide.image} alt={slide.title || "اسلایدر صنایع کابل کمان"} fill priority={slideIndex === 0} />
|
||||
<SlotImage src={slide.image} alt={slide.title || t.home.sliderAlt} fill priority={slideIndex === 0} />
|
||||
</SlotLink>
|
||||
</div>
|
||||
))}
|
||||
@@ -64,7 +66,7 @@ export default function HeroSlider({
|
||||
</h2>
|
||||
</div>
|
||||
<div className="container">
|
||||
<div className="highlights" role="tablist" aria-label="اسلایدر">
|
||||
<div className="highlights" role="tablist" aria-label={t.home.sliderLabel}>
|
||||
{tabs.map((item, tabIndex) => {
|
||||
const active = tabIndex === index;
|
||||
return (
|
||||
@@ -78,7 +80,7 @@ export default function HeroSlider({
|
||||
>
|
||||
<div className="n">{item.n}</div>
|
||||
<h3>{item.title}</h3>
|
||||
<p className="en">{item.titleEn}</p>
|
||||
{locale !== "en" && item.kicker ? <p className="en">{item.kicker}</p> : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import CommentBox from "@/components/CommentBox";
|
||||
import { useMessages } from "@/i18n/locale-context";
|
||||
import type { CommentItem } from "@/lib/comments";
|
||||
import type { TechnicalRow } from "@/lib/technical";
|
||||
|
||||
@@ -21,19 +22,20 @@ export default function ProductTabs({
|
||||
comments: CommentItem[];
|
||||
entityId: string;
|
||||
}) {
|
||||
const t = useMessages();
|
||||
const [tab, setTab] = useState<TabId>("description");
|
||||
|
||||
return (
|
||||
<section className="detail-tabs">
|
||||
<div className="detail-tabs-nav" role="tablist" aria-label="جزئیات محصول">
|
||||
<div className="detail-tabs-nav" role="tablist" aria-label={t.comments.tabsLabel}>
|
||||
<button type="button" className={tab === "description" ? "active" : undefined} onClick={() => setTab("description")}>
|
||||
توضیحات
|
||||
{t.comments.description}
|
||||
</button>
|
||||
<button type="button" className={tab === "technical" ? "active" : undefined} onClick={() => setTab("technical")}>
|
||||
مشخصات فنی
|
||||
{t.comments.technical}
|
||||
</button>
|
||||
<button type="button" className={tab === "comments" ? "active" : undefined} onClick={() => setTab("comments")}>
|
||||
نظرات
|
||||
{t.comments.commentsTab}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +46,7 @@ export default function ProductTabs({
|
||||
) : descriptionText ? (
|
||||
<p>{descriptionText}</p>
|
||||
) : (
|
||||
<p className="comment-empty">توضیحاتی ثبت نشده است.</p>
|
||||
<p className="comment-empty">{t.comments.noDescription}</p>
|
||||
)
|
||||
) : null}
|
||||
|
||||
@@ -61,7 +63,7 @@ export default function ProductTabs({
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p className="comment-empty">مشخصات فنی برای این محصول ثبت نشده است.</p>
|
||||
<p className="comment-empty">{t.comments.noSpecs}</p>
|
||||
)
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { LocaleProvider } from "@/i18n/locale-context";
|
||||
import type { Locale } from "@/i18n/config";
|
||||
import { SessionProvider } from "@/lib/session";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export default function Providers({
|
||||
businessId,
|
||||
locale,
|
||||
children,
|
||||
}: {
|
||||
businessId: string;
|
||||
locale: Locale;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return <SessionProvider businessId={businessId}>{children}</SessionProvider>;
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<SessionProvider businessId={businessId}>{children}</SessionProvider>
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
import type { Messages } from "@/i18n/fa";
|
||||
|
||||
export const ar: Messages = {
|
||||
brand: {
|
||||
name: "صناعات أسلاك وكابلات كمان",
|
||||
shortName: "كابلات كمان",
|
||||
nameEn: "Kaman Cable & Wire Industries",
|
||||
nameEnShort: "Kaman Cable",
|
||||
tagline: "إنتاج كابلات الجهد المنخفض والاتصالات والتحكم والأجهزة الدقيقة",
|
||||
description:
|
||||
"تنتج شركة كابلات كمان منذ عام ۱۳۷۰ في المدينة الصناعية بزنجان أنواع كابلات الكهرباء والاتصالات للجهد المنخفض وكابلات التحكم والأجهزة الدقيقة وفقاً للمواصفات الوطنية والدولية.",
|
||||
},
|
||||
nav: {
|
||||
home: "الرئيسية",
|
||||
products: "المنتجات",
|
||||
blog: "المدونة",
|
||||
about: "من نحن",
|
||||
contact: "اتصل بنا",
|
||||
menu: "القائمة",
|
||||
search: "بحث",
|
||||
language: "اللغة",
|
||||
languageSelect: "اختر اللغة",
|
||||
},
|
||||
account: {
|
||||
login: "دخول",
|
||||
dear: "{name} العزيز",
|
||||
profile: "ملفي",
|
||||
logout: "خروج",
|
||||
manage: "إدارة الموقع",
|
||||
transferring: "جارٍ التحويل…",
|
||||
},
|
||||
footer: {
|
||||
quickLinks: "روابط سريعة",
|
||||
contact: "اتصل بنا",
|
||||
},
|
||||
contactInfo: {
|
||||
address: "طهران، شارع كريم خان زند، شارع سنائي، أعلى ميدان سنائي، رقم 82، الطابق الثالث، الوحدة 6",
|
||||
factory: "المدينة الصناعية رقم 1، محافظة زنجان",
|
||||
phoneLabel: "الهاتف",
|
||||
addressLabel: "العنوان",
|
||||
factoryLabel: "المصنع",
|
||||
addressEn: "Address",
|
||||
factoryEn: "Factory",
|
||||
phoneEn: "Phone",
|
||||
},
|
||||
social: {
|
||||
instagram: "إنستغرام",
|
||||
telegram: "تيليغرام",
|
||||
linkedin: "لينكدإن",
|
||||
email: "البريد الإلكتروني",
|
||||
},
|
||||
home: {
|
||||
appTitle: "التطبيق الخاص",
|
||||
appCta: "تحميل التطبيق",
|
||||
aboutKicker: "About",
|
||||
aboutTitle: "صناعات كابلات كمان",
|
||||
contactCta: "اتصل بنا",
|
||||
productsKicker: "Products",
|
||||
productsTitle: "منتجات الكابلات والأسلاك",
|
||||
productsText:
|
||||
"مجموعة كاملة من كابلات الجهد المنخفض والاتصالات والشبكات والأجهزة الدقيقة، مصنّعة وفق المواصفات الوطنية والدولية.",
|
||||
blogKicker: "Blog",
|
||||
blogTitle: "آخر الأخبار والمقالات",
|
||||
blogText: "تابع أحدث المعارف الفنية وأخبار صناعات أسلاك وكابلات كمان.",
|
||||
partnersKicker: "Partners",
|
||||
partnersTitle: "الشركاء والعملاء",
|
||||
partnersText: "علامات تجارية ومؤسسات اختارت جودة إنتاج كابلات كمان لمشاريعها.",
|
||||
aboutImageAlt: "مصنع صناعات كابلات كمان",
|
||||
productImageAlt: "منتج كابلات كمان",
|
||||
partnerImageAlt: "شريك كابلات كمان",
|
||||
sliderAlt: "شريط صور كابلات كمان",
|
||||
sliderLabel: "الشريط",
|
||||
},
|
||||
hero: {
|
||||
slides: [
|
||||
{
|
||||
kicker: "KAMAN CABLE & WIRE INDUSTRIES",
|
||||
title: "أسلاك وكابلات كمان",
|
||||
},
|
||||
{
|
||||
kicker: "ENDORSED BY THE MINISTRY OF ENERGY AND OIL COMPANY",
|
||||
title: "معتمد من وزارة الطاقة وشركات النفط",
|
||||
},
|
||||
{
|
||||
kicker: "HIGH QUALITY AND UP-TO-DATE PRODUCTS",
|
||||
title: "روّاد في تقديم منتجات عالية الجودة ومواكبة للعصر",
|
||||
},
|
||||
],
|
||||
highlights: [
|
||||
{ n: "١", title: "تنوع المنتجات", kicker: "Variety of products" },
|
||||
{ n: "٢", title: "خط إنتاج قوي", kicker: "Powerful Production Line" },
|
||||
{ n: "٣", title: "منتج نموذجي", kicker: "Top Producer" },
|
||||
],
|
||||
},
|
||||
aboutTabs: {
|
||||
resume: "النبذة",
|
||||
goalsTab: "الأهداف",
|
||||
resumeText:
|
||||
"بدأت شركة كابلات كمان نشاطها عام ۱۳۷۰ على أرض مساحتها ۴۰۰۰۰ متر مربع في المدينة الصناعية رقم 1 بمحافظة زنجان. وبعد تركيب الآلات وتوفير البنية التحتية والاستعانة بالمتخصصين ذوي الخبرة، حصلت في مطلع عام ۱۳۷۳ على رخصة التشغيل بطاقة ۱۵۰۰۰ طن وعلامة المواصفة الوطنية (ISIRI). ولأن الجودة ورضا العميل أساس كل نشاط، حصلت الشركة في سنواتها الأولى على شهادتي ISO 9001:2015 وCE، وتواصل توريد منتجاتها بجودة عالية للأسواق المحلية ودول الجوار بما يتوافق مع أهداف العملاء.",
|
||||
goalsParagraphs: [
|
||||
"الصدق أفضل طريقة.",
|
||||
"بصفتها أحد منتجي الأسلاك والكابلات، جعلت كابلات كمان رضا العملاء عبر رفع الجودة والالتزام بجميع المتطلبات هدفها الأسمى. وتعلن الإدارة العليا التزامها بتنفيذ نظام إدارة الجودة وفق ISO 9001:2015 والحفاظ عليه وتحسين فاعليته باستمرار.",
|
||||
],
|
||||
qualityIntro: "سياسة الجودة هي:",
|
||||
qualityPolicy: [
|
||||
"زيادة تنوع وحجم المنتجات",
|
||||
"رفع مستوى رضا العملاء",
|
||||
"تطوير المواصفات الوطنية والدولية في المؤسسة",
|
||||
],
|
||||
},
|
||||
aboutPage: {
|
||||
title: "من نحن",
|
||||
kicker: "About Us",
|
||||
metaDescription:
|
||||
"تعرّف على شركة كابلات كمان؛ منتج أسلاك وكابلات الجهد المنخفض والاتصالات والأجهزة الدقيقة منذ عام ۱۳۷۰ في زنجان.",
|
||||
heroAlt: "عن صناعات كابلات كمان",
|
||||
companyTitle: "شركة كابلات كمان",
|
||||
intro:
|
||||
"كابلات كمان شركة راسخة في إنتاج الكابلات والكهرباء. تستخدم أفضل الآلات الأوروبية لإنتاج أسلاك وكابلات الجهد المنخفض للاتصالات وكابلات التحكم والأجهزة الدقيقة. منتجاتها مطابقة للمواصفات الوطنية والدولية وتُورَّد بأعلى جودة للأسواق المحلية ودول الجوار بما يتوافق مع أهداف العملاء.",
|
||||
goalsTitle: "الأهداف",
|
||||
goalsKicker: "Quality Policy",
|
||||
honesty: "الصدق أفضل طريقة.",
|
||||
goalsBody:
|
||||
"بصفتها أحد منتجي الأسلاك والكابلات، جعلت كابلات كمان رضا العملاء عبر رفع الجودة والالتزام بجميع المتطلبات هدفها الأسمى.",
|
||||
labTitle: "مختبر صناعات كابلات كمان",
|
||||
labKicker: "Laboratory",
|
||||
labText:
|
||||
"حصل مختبر الشركة عام ۱۳۹۱ على شهادة مختبر متعاون من مؤسسة المواصفات في محافظة زنجان، ويحمل اليوم شهادة اعتماد المختبر من المركز الوطني للاعتماد في إيران (ISIRI-ISO/IEC 17025). كما يملك اعتمادات من توانير وشركات توزيع الكهرباء وشركات النفط. وبأجهزة متقدمة ومتخصصين ذوي خبرة، جاهز لإجراء الاختبارات وفق حاجة قطاع الكهرباء والعملاء.",
|
||||
machineryTitle: "التجهيزات والآلات",
|
||||
machineryKicker: "Equipment & Machinery",
|
||||
machineryText:
|
||||
"المصنع مجهّز بآلات من شركات أوروبية مرموقة لإنتاج أسلاك وكابلات الجهد المنخفض للاتصالات وكابلات التحكم والأجهزة الدقيقة وفق المواصفات الوطنية (ISIRI 607, 3569-1, 1926, …) والدولية (IEC, BS, VDE, …).",
|
||||
guaranteeTitle: "ضمان الشركة",
|
||||
guaranteeKicker: "Guarantee",
|
||||
galleryTitle: "المعرض",
|
||||
galleryKicker: "Gallery",
|
||||
galleryItems: [
|
||||
"مصنع كابلات كمان",
|
||||
"ملفات الكابلات",
|
||||
"تفاصيل الكابل",
|
||||
"ملفات النحاس",
|
||||
"خط الإنتاج",
|
||||
"آلات الإنتاج",
|
||||
],
|
||||
stats: [
|
||||
{ value: "۱۳۷۰", label: "سنة التأسيس", kicker: "Founded" },
|
||||
{ value: "۴۰٬۰۰۰", label: "متر مربع للمصنع", kicker: "Factory Area" },
|
||||
{ value: "۱۵٬۰۰۰", label: "طن طاقة سنوية", kicker: "Annual Capacity" },
|
||||
{ value: "ISO", label: "۹۰۰۱:۲۰۱۵ و ۱۷۰۲۵", kicker: "Certified" },
|
||||
],
|
||||
},
|
||||
contactPage: {
|
||||
title: "اتصل بنا",
|
||||
heading: "تواصل معنا",
|
||||
kicker: "Contact Us",
|
||||
metaDescription: "التواصل مع صناعات كابلات كمان. مكتب طهران ومصنع زنجان.",
|
||||
heroAlt: "الاتصال بصناعات كابلات كمان",
|
||||
infoTitle: "بيانات الاتصال",
|
||||
mapTitle: "موقع مكتب صناعات كابلات كمان",
|
||||
},
|
||||
contactBar: {
|
||||
label: "اتصل بنا",
|
||||
subscribe: "اترك رقمك وسنتصل بك.",
|
||||
},
|
||||
callback: {
|
||||
name: "الاسم",
|
||||
phone: "رقم الهاتف",
|
||||
submit: "إرسال الرقم",
|
||||
sending: "جارٍ الإرسال…",
|
||||
title: "طلب اتصال",
|
||||
body: "طلب اتصال من الصفحة الرئيسية. الرقم: {phone}",
|
||||
error: "تعذّر حفظ الرقم. يرجى المحاولة مرة أخرى.",
|
||||
ok: "تم حفظ رقمك. سنتصل بك قريباً.",
|
||||
network: "تعذّر الاتصال بالخادم.",
|
||||
},
|
||||
contactForm: {
|
||||
heading: "أرسل رسالتك",
|
||||
kicker: "Send a message",
|
||||
name: "الاسم",
|
||||
subject: "الموضوع",
|
||||
email: "البريد الإلكتروني",
|
||||
phone: "رقم الهاتف",
|
||||
message: "نص الرسالة",
|
||||
submit: "إرسال الرسالة",
|
||||
sending: "جارٍ الإرسال…",
|
||||
error: "تعذّر إرسال الرسالة. يرجى المحاولة مرة أخرى.",
|
||||
ok: "تم إرسال رسالتك. سنتواصل معك قريباً.",
|
||||
network: "تعذّر الاتصال بالخادم.",
|
||||
},
|
||||
categories: [
|
||||
{
|
||||
id: "power",
|
||||
title: "كابل القدرة",
|
||||
kicker: "Power Cable",
|
||||
text: "تنقل أسلاك الكهرباء التيار عبر الموصلات الداخلية المكوِّنة للكابل.",
|
||||
},
|
||||
{
|
||||
id: "flat",
|
||||
title: "الكابلات المسطحة",
|
||||
kicker: "Flat Cables",
|
||||
text: "تناسب هذه الكابلات المصاعد والوصلات المتحركة.",
|
||||
},
|
||||
{
|
||||
id: "phone",
|
||||
title: "كابلات الاتصالات",
|
||||
kicker: "Phone Cables",
|
||||
text: "يُملأ داخل كابلات الاتصالات بهلام بلاستيكي لزج لمنع الرطوبة من إحداث قصر أو أكسدة بين الأسلاك.",
|
||||
},
|
||||
{
|
||||
id: "network",
|
||||
title: "كابلات الشبكة",
|
||||
kicker: "Network Cable",
|
||||
text: "يلزم كابل شبكة لربط الحواسيب والخوادم والموجّهات وغيرها على الشبكة.",
|
||||
},
|
||||
{
|
||||
id: "instrument",
|
||||
title: "الأجهزة الدقيقة والتحكم",
|
||||
kicker: "Instrumentation & Control",
|
||||
text: "تشكّل الأجهزة الدقيقة بنية نظام التحكم والأتمتة، وتشمل الحساسات والمتحكمات وغيرها.",
|
||||
},
|
||||
{
|
||||
id: "coaxial",
|
||||
title: "محوري",
|
||||
kicker: "Coaxial",
|
||||
text: "الكابل المحوري نوع شائع من كابلات نقل البيانات المحمية، ويتكون من موصلين.",
|
||||
},
|
||||
{
|
||||
id: "fire",
|
||||
title: "إنذار الحريق",
|
||||
kicker: "Fire Alarm",
|
||||
text: "تتولى أنظمة إنذار الحريق الإبلاغ عند اندلاع حريق، ويمكن تصنيفها إلى مجموعتين.",
|
||||
},
|
||||
{
|
||||
id: "smart",
|
||||
title: "المنزل الذكي",
|
||||
kicker: "Smart House",
|
||||
text: "في المنزل الذكي تتصل جميع الأجهزة وتُدار من جهاز مركزي.",
|
||||
},
|
||||
],
|
||||
partners: ["شريك ١", "شريك ٢", "شريك ٣", "شريك ٤", "شريك ٥"],
|
||||
comments: {
|
||||
empty: "لا توجد تعليقات بعد.",
|
||||
submit: "نشر التعليق",
|
||||
sending: "جارٍ الإرسال…",
|
||||
ok: "تم إرسال تعليقك وسيظهر بعد الموافقة.",
|
||||
loginPrompt: "سجّل الدخول لنشر تعليق.",
|
||||
loginBody: "سجّل الدخول إلى حسابك لنشر تعليق.",
|
||||
user: "مستخدم",
|
||||
yourComment: "تعليقك",
|
||||
placeholder: "اكتب تعليقك…",
|
||||
sendError: "تعذّر إرسال التعليق. يرجى المحاولة مرة أخرى.",
|
||||
unavailable: "التعليق غير متاح لهذا المحتوى.",
|
||||
noDescription: "لا يوجد وصف مسجّل.",
|
||||
noSpecs: "لا توجد مواصفات فنية لهذا المنتج.",
|
||||
tabsLabel: "تفاصيل المنتج",
|
||||
description: "الوصف",
|
||||
technical: "المواصفات الفنية",
|
||||
commentsTab: "التعليقات",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
export const locales = ["fa", "en", "ar"] as const;
|
||||
|
||||
export type Locale = (typeof locales)[number];
|
||||
|
||||
export const defaultLocale: Locale = "fa";
|
||||
|
||||
export const localeCookie = "kaman-locale";
|
||||
|
||||
export const localeDir: Record<Locale, "rtl" | "ltr"> = {
|
||||
fa: "rtl",
|
||||
ar: "rtl",
|
||||
en: "ltr",
|
||||
};
|
||||
|
||||
export const localeHtml: Record<Locale, string> = {
|
||||
fa: "fa",
|
||||
ar: "ar",
|
||||
en: "en",
|
||||
};
|
||||
|
||||
export const localeOg: Record<Locale, string> = {
|
||||
fa: "fa_IR",
|
||||
ar: "ar_SA",
|
||||
en: "en_US",
|
||||
};
|
||||
|
||||
export const localeOptions: { id: Locale; label: string }[] = [
|
||||
{ id: "fa", label: "FA" },
|
||||
{ id: "en", label: "EN" },
|
||||
{ id: "ar", label: "AR" },
|
||||
];
|
||||
|
||||
/** Meshkee catalog and blog are FA-only until those modules are translated. */
|
||||
export function showsCatalog(locale: Locale) {
|
||||
return locale === "fa";
|
||||
}
|
||||
|
||||
export function isLocale(value: string | null | undefined): value is Locale {
|
||||
return value === "fa" || value === "en" || value === "ar";
|
||||
}
|
||||
|
||||
export function isCatalogPath(pathname: string) {
|
||||
return pathname === "/products" || pathname.startsWith("/products/") || pathname === "/blog" || pathname.startsWith("/blog/");
|
||||
}
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
import type { Messages } from "@/i18n/fa";
|
||||
|
||||
export const en: Messages = {
|
||||
brand: {
|
||||
name: "Kaman Cable & Wire Industries",
|
||||
shortName: "Kaman Cable",
|
||||
nameEn: "Kaman Cable & Wire Industries",
|
||||
nameEnShort: "Kaman Cable",
|
||||
tagline: "Low-voltage, telecom, control, and instrumentation cables",
|
||||
description:
|
||||
"Since 1991, Kaman Cable has manufactured low-voltage power and telecom cables, control and instrumentation cables in Zanjan Industrial Town, in line with national and international standards.",
|
||||
},
|
||||
nav: {
|
||||
home: "Home",
|
||||
products: "Products",
|
||||
blog: "Blog",
|
||||
about: "About",
|
||||
contact: "Contact",
|
||||
menu: "Menu",
|
||||
search: "Search",
|
||||
language: "Language",
|
||||
languageSelect: "Select language",
|
||||
},
|
||||
account: {
|
||||
login: "Login",
|
||||
dear: "Hi, {name}",
|
||||
profile: "My profile",
|
||||
logout: "Log out",
|
||||
manage: "Website admin",
|
||||
transferring: "Redirecting…",
|
||||
},
|
||||
footer: {
|
||||
quickLinks: "Quick links",
|
||||
contact: "Contact us",
|
||||
},
|
||||
contactInfo: {
|
||||
address: "Tehran, Karim Khan Zand St., Sanaee St., above Sanaee Sq., No. 82, 3rd floor, Unit 6",
|
||||
factory: "Industrial Town No. 1, Zanjan Province",
|
||||
phoneLabel: "Phone",
|
||||
addressLabel: "Address",
|
||||
factoryLabel: "Factory",
|
||||
addressEn: "Address",
|
||||
factoryEn: "Factory",
|
||||
phoneEn: "Phone",
|
||||
},
|
||||
social: {
|
||||
instagram: "Instagram",
|
||||
telegram: "Telegram",
|
||||
linkedin: "LinkedIn",
|
||||
email: "Email",
|
||||
},
|
||||
home: {
|
||||
appTitle: "Dedicated app",
|
||||
appCta: "Download the app",
|
||||
aboutKicker: "About",
|
||||
aboutTitle: "Kaman Cable",
|
||||
contactCta: "Contact us",
|
||||
productsKicker: "Products",
|
||||
productsTitle: "Cables and wires",
|
||||
productsText:
|
||||
"A full range of low-voltage, telecom, network, and instrumentation cables, manufactured to national and international standards.",
|
||||
blogKicker: "Blog",
|
||||
blogTitle: "News and articles",
|
||||
blogText: "Follow the latest technical notes and news from Kaman Cable.",
|
||||
partnersKicker: "Partners",
|
||||
partnersTitle: "Partners and clients",
|
||||
partnersText: "Brands and organizations that specify Kaman Cable for their projects.",
|
||||
aboutImageAlt: "Kaman Cable factory",
|
||||
productImageAlt: "Kaman Cable product",
|
||||
partnerImageAlt: "Kaman Cable partner",
|
||||
sliderAlt: "Kaman Cable homepage slider",
|
||||
sliderLabel: "Slider",
|
||||
},
|
||||
hero: {
|
||||
slides: [
|
||||
{
|
||||
kicker: "KAMAN CABLE & WIRE INDUSTRIES",
|
||||
title: "Kaman cable & wire",
|
||||
},
|
||||
{
|
||||
kicker: "ENDORSED BY THE MINISTRY OF ENERGY AND OIL COMPANY",
|
||||
title: "Approved by the Ministry of Energy and oil companies",
|
||||
},
|
||||
{
|
||||
kicker: "HIGH QUALITY AND UP-TO-DATE PRODUCTS",
|
||||
title: "Leading in quality, up-to-date products",
|
||||
},
|
||||
],
|
||||
highlights: [
|
||||
{ n: "1", title: "Product range", kicker: "Variety of products" },
|
||||
{ n: "2", title: "Production line", kicker: "Powerful Production Line" },
|
||||
{ n: "3", title: "Trusted maker", kicker: "Top Producer" },
|
||||
],
|
||||
},
|
||||
aboutTabs: {
|
||||
resume: "Profile",
|
||||
goalsTab: "Goals",
|
||||
resumeText:
|
||||
"Kaman Cable started operations in 1991 on a 40,000 m² site in Industrial Town No. 1, Zanjan Province. After installing machinery and infrastructure, and with experienced specialists, the plant obtained its operating license in 1994 for 15,000 tonnes of annual capacity and the national ISIRI standard mark. Quality and customer satisfaction sit at the centre of every activity. By focusing on customer needs, the company earned ISO 9001:2015 and CE in its early years, and continues to supply domestic and neighbouring markets with products that match customer goals.",
|
||||
goalsParagraphs: [
|
||||
"Honesty is the best policy.",
|
||||
"As a wire and cable manufacturer, Kaman Cable has made customer satisfaction — through quality and full compliance — its highest goal. Senior management is committed to implementing, maintaining, and continually improving the effectiveness of the quality management system to ISO 9001:2015.",
|
||||
],
|
||||
qualityIntro: "Our quality policy is:",
|
||||
qualityPolicy: [
|
||||
"Increase the range and volume of products",
|
||||
"Raise customer satisfaction",
|
||||
"Develop national and international standards in the organisation",
|
||||
],
|
||||
},
|
||||
aboutPage: {
|
||||
title: "About us",
|
||||
kicker: "About Us",
|
||||
metaDescription:
|
||||
"Kaman Cable manufactures low-voltage, telecom, and instrumentation cables. Founded in 1991 in Zanjan, Iran.",
|
||||
heroAlt: "About Kaman Cable",
|
||||
companyTitle: "Kaman Cable",
|
||||
intro:
|
||||
"Kaman Cable is an established manufacturer of electrical cables. The company uses European machinery to produce low-voltage power and telecom cables, control and instrumentation cables. Products follow national and international standards and are supplied at high quality to domestic and neighbouring markets, aligned with customer goals.",
|
||||
goalsTitle: "Goals",
|
||||
goalsKicker: "Quality Policy",
|
||||
honesty: "Honesty is the best policy.",
|
||||
goalsBody:
|
||||
"As a wire and cable manufacturer, Kaman Cable has made customer satisfaction — through quality and full compliance — its highest goal.",
|
||||
labTitle: "Kaman Cable laboratory",
|
||||
labKicker: "Laboratory",
|
||||
labText:
|
||||
"In 2012 the Kaman Cable laboratory received a partner-lab certificate from the Zanjan Standards Institute, and it now holds ISIRI-ISO/IEC 17025 accreditation from the National Accreditation Center of Iran. It also holds approvals from Tavanir, regional electricity distribution companies, and oil companies. With advanced equipment and experienced specialists, the lab is ready to run the tests required by the power industry and our customers.",
|
||||
machineryTitle: "Equipment and machinery",
|
||||
machineryKicker: "Equipment & Machinery",
|
||||
machineryText:
|
||||
"The plant is equipped with European machinery for low-voltage power and telecom cables, control and instrumentation cables, to national standards (ISIRI 607, 3569-1, 1926, …) and international standards (IEC, BS, VDE, …).",
|
||||
guaranteeTitle: "Company guarantee",
|
||||
guaranteeKicker: "Guarantee",
|
||||
galleryTitle: "Gallery",
|
||||
galleryKicker: "Gallery",
|
||||
galleryItems: [
|
||||
"Kaman Cable factory",
|
||||
"Cable coils",
|
||||
"Cable detail",
|
||||
"Copper coils",
|
||||
"Production line",
|
||||
"Production machinery",
|
||||
],
|
||||
stats: [
|
||||
{ value: "1991", label: "Founded", kicker: "Founded" },
|
||||
{ value: "40,000", label: "m² factory", kicker: "Factory Area" },
|
||||
{ value: "15,000", label: "tonnes per year", kicker: "Annual Capacity" },
|
||||
{ value: "ISO", label: "9001:2015 & 17025", kicker: "Certified" },
|
||||
],
|
||||
},
|
||||
contactPage: {
|
||||
title: "Contact us",
|
||||
heading: "Get in touch",
|
||||
kicker: "Contact Us",
|
||||
metaDescription: "Contact Kaman Cable. Tehran office and Zanjan factory.",
|
||||
heroAlt: "Contact Kaman Cable",
|
||||
infoTitle: "Contact details",
|
||||
mapTitle: "Kaman Cable office location",
|
||||
},
|
||||
contactBar: {
|
||||
label: "Contact us",
|
||||
subscribe: "Leave your number and we will call you back.",
|
||||
},
|
||||
callback: {
|
||||
name: "Name",
|
||||
phone: "Phone number",
|
||||
submit: "Send number",
|
||||
sending: "Sending…",
|
||||
title: "Callback request",
|
||||
body: "Callback request from the homepage. Number: {phone}",
|
||||
error: "Could not save your number. Please try again.",
|
||||
ok: "Your number was saved. We will call you soon.",
|
||||
network: "Could not reach the server.",
|
||||
},
|
||||
contactForm: {
|
||||
heading: "Send a message",
|
||||
kicker: "Send a message",
|
||||
name: "Name",
|
||||
subject: "Subject",
|
||||
email: "Email",
|
||||
phone: "Phone number",
|
||||
message: "Message",
|
||||
submit: "Send message",
|
||||
sending: "Sending…",
|
||||
error: "Could not send the message. Please try again.",
|
||||
ok: "Your message was sent. We will contact you soon.",
|
||||
network: "Could not reach the server.",
|
||||
},
|
||||
categories: [
|
||||
{
|
||||
id: "power",
|
||||
title: "Power cable",
|
||||
kicker: "Power Cable",
|
||||
text: "Power cables carry electrical current through their internal conductors.",
|
||||
},
|
||||
{
|
||||
id: "flat",
|
||||
title: "Flat cables",
|
||||
kicker: "Flat Cables",
|
||||
text: "These cables are suited to lifts and other moving connections.",
|
||||
},
|
||||
{
|
||||
id: "phone",
|
||||
title: "Telecom cables",
|
||||
kicker: "Phone Cables",
|
||||
text: "A jelly filling inside telecom cables helps keep moisture from causing shorts or oxidation between conductors.",
|
||||
},
|
||||
{
|
||||
id: "network",
|
||||
title: "Network cables",
|
||||
kicker: "Network Cable",
|
||||
text: "A LAN cable connects computers, servers, routers, and other devices on a network.",
|
||||
},
|
||||
{
|
||||
id: "instrument",
|
||||
title: "Instrumentation & control",
|
||||
kicker: "Instrumentation & Control",
|
||||
text: "Instrumentation forms the backbone of a control and automation system, including sensors, controllers, and more.",
|
||||
},
|
||||
{
|
||||
id: "coaxial",
|
||||
title: "Coaxial",
|
||||
kicker: "Coaxial",
|
||||
text: "Coaxial cable is a common shielded data cable built from two conductors.",
|
||||
},
|
||||
{
|
||||
id: "fire",
|
||||
title: "Fire alarm",
|
||||
kicker: "Fire Alarm",
|
||||
text: "Fire-alarm systems notify occupants in a fire and are grouped into two main types.",
|
||||
},
|
||||
{
|
||||
id: "smart",
|
||||
title: "Smart home",
|
||||
kicker: "Smart House",
|
||||
text: "In a smart home, appliances are connected and managed from a central controller.",
|
||||
},
|
||||
],
|
||||
partners: ["Partner 1", "Partner 2", "Partner 3", "Partner 4", "Partner 5"],
|
||||
comments: {
|
||||
empty: "No comments yet.",
|
||||
submit: "Post comment",
|
||||
sending: "Sending…",
|
||||
ok: "Your comment was submitted and will appear after approval.",
|
||||
loginPrompt: "Sign in to post a comment.",
|
||||
loginBody: "Sign in to your account to post a comment.",
|
||||
user: "User",
|
||||
yourComment: "Your comment",
|
||||
placeholder: "Write your comment…",
|
||||
sendError: "Could not send the comment. Please try again.",
|
||||
unavailable: "Comments are not available for this item.",
|
||||
noDescription: "No description has been added.",
|
||||
noSpecs: "No technical specifications are recorded for this product.",
|
||||
tabsLabel: "Product details",
|
||||
description: "Description",
|
||||
technical: "Technical specs",
|
||||
commentsTab: "Comments",
|
||||
},
|
||||
};
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
export const fa = {
|
||||
brand: {
|
||||
name: "صنایع سیم و کابل کمان",
|
||||
shortName: "کابل کمان",
|
||||
nameEn: "Kaman Cable & Wire Industries",
|
||||
nameEnShort: "Kaman Cable",
|
||||
tagline: "تولید سیم و کابل فشار ضعیف، مخابراتی، کنترل و ابزار دقیق",
|
||||
description:
|
||||
"شرکت صنایع کابل کمان از سال ۱۳۷۰ در شهر صنعتی زنجان به تولید انواع سیم و کابل فشار ضعیف برق و مخابراتی، کابلهای کنترل و ابزار دقیق مطابق با استانداردهای ملی و بینالمللی میپردازد.",
|
||||
},
|
||||
nav: {
|
||||
home: "صفحه اصلی",
|
||||
products: "محصولات",
|
||||
blog: "وبلاگ",
|
||||
about: "درباره ما",
|
||||
contact: "ارتباط با ما",
|
||||
menu: "منو",
|
||||
search: "جستجو",
|
||||
language: "زبان",
|
||||
languageSelect: "انتخاب زبان",
|
||||
},
|
||||
account: {
|
||||
login: "ورود",
|
||||
dear: "{name} عزیز",
|
||||
profile: "پروفایل من",
|
||||
logout: "خروج",
|
||||
manage: "مدیریت وبسایت",
|
||||
transferring: "در حال انتقال…",
|
||||
},
|
||||
footer: {
|
||||
quickLinks: "دسترسی سریع",
|
||||
contact: "تماس با ما",
|
||||
},
|
||||
contactInfo: {
|
||||
address: "تهران، خیابان کریمخان زند، خیابان سنایی، بالاتر از میدان سنایی، پلاک ۸۲، طبقه سوم، واحد ۶",
|
||||
factory: "شهر صنعتی شماره یک، استان زنجان",
|
||||
phoneLabel: "تلفن",
|
||||
addressLabel: "آدرس",
|
||||
factoryLabel: "کارخانه",
|
||||
addressEn: "Address",
|
||||
factoryEn: "Factory",
|
||||
phoneEn: "Phone",
|
||||
},
|
||||
social: {
|
||||
instagram: "اینستاگرام",
|
||||
telegram: "تلگرام",
|
||||
linkedin: "لینکدین",
|
||||
email: "ایمیل",
|
||||
},
|
||||
home: {
|
||||
appTitle: "اپلیکیشن اختصاصی",
|
||||
appCta: "دانلود اپلیکیشن",
|
||||
aboutKicker: "About",
|
||||
aboutTitle: "صنایع کابل کمان",
|
||||
contactCta: "تماس با ما",
|
||||
productsKicker: "Products",
|
||||
productsTitle: "محصولات کابل و سیم",
|
||||
productsText:
|
||||
"تنوع کامل سیم و کابل فشار ضعیف، مخابراتی، شبکه و ابزار دقیق، تولیدشده مطابق با استانداردهای ملی و بینالمللی.",
|
||||
blogKicker: "Blog",
|
||||
blogTitle: "آخرین اخبار و مقالات",
|
||||
blogText: "تازهترین دانستنیهای فنی و اخبار صنایع سیم و کابل کمان را در این بخش دنبال کنید.",
|
||||
partnersKicker: "Partners",
|
||||
partnersTitle: "همکاران و مشتریان",
|
||||
partnersText: "برندها و مجموعههایی که کیفیت تولید کابل کمان را برای پروژههای خود انتخاب کردهاند.",
|
||||
aboutImageAlt: "کارخانه صنایع کابل کمان",
|
||||
productImageAlt: "محصول کابل کمان",
|
||||
partnerImageAlt: "همکار کابل کمان",
|
||||
sliderAlt: "اسلایدر صنایع کابل کمان",
|
||||
sliderLabel: "اسلایدر",
|
||||
},
|
||||
hero: {
|
||||
slides: [
|
||||
{
|
||||
kicker: "KAMAN CABLE & WIRE INDUSTRIES",
|
||||
title: "kaman cable & wire",
|
||||
},
|
||||
{
|
||||
kicker: "ENDORSED BY THE MINISTRY OF ENERGY AND OIL COMPANY",
|
||||
title: "دارای تاییدیه از وزارت نیرو و شرکت نفت",
|
||||
},
|
||||
{
|
||||
kicker: "HIGH QUALITY AND UP-TO-DATE PRODUCTS",
|
||||
title: "پیشرو در ارائه محصولات با کیفیت و بروز",
|
||||
},
|
||||
],
|
||||
highlights: [
|
||||
{ n: "۱", title: "تنوع محصولات", kicker: "Variety of products" },
|
||||
{ n: "۲", title: "خط تولید قدرتمند", kicker: "Powerful Production Line" },
|
||||
{ n: "۳", title: "تولید کننده نمونه", kicker: "Top Producer" },
|
||||
],
|
||||
},
|
||||
aboutTabs: {
|
||||
resume: "رزومه",
|
||||
goalsTab: "اهداف",
|
||||
resumeText:
|
||||
"شرکت صنایع کابل کمان در سال ۱۳۷۰ در زمینی به وسعت ۴۰۰۰۰ متر مربع در شهر صنعتی شماره یک واقع در استان زنجان شروع به فعالیت نمود. این واحد تولیدی پس از گذراندن مراحل نصب ماشین آلات و تامین زیر ساختهای مناسب و با بهرهمندی از متخصصین و کارشناسان با تجربه در این رشته، در ابتدای سال ۱۳۷۳ موفق به اخذ پروانه بهرهبرداری با ظرفیت ۱۵۰۰۰ تن و دریافت گواهینامه استاندارد ملی (ISIRI) گردید. از آنجایی که کیفیت و رضایتمندی مشتری سرلوحه کلیه فعالیتها است، لذا این واحد تولیدی با تکیه بر اصل مشتریمداری و با تلاش متخصصین خود توانست در سالهای نخست گواهینامه ISO 9001:2015 و CE را اخذ نموده و در راستای نیاز روزافزون بازارهای داخلی و کشورهای همسایه به صورت گسترده محصولات خود را با کیفیت مرغوب و سازگار با اهداف مشتری عرضه نماید.",
|
||||
goalsParagraphs: [
|
||||
"صداقت بهترین روش است.",
|
||||
"شرکت صنایع کابل کمان بهعنوان یکی از تولیدکنندگان سیم و کابل، رضایت مشتریان را از طریق ارتقاء کیفیت و رعایت کلیه قوانین و الزامات حاکم بر محصول خود، بهعنوان برترین هدف برگزیده است. در این راستا مدیریت ارشد، اعتقاد و تعهد خود را به اجرا، حفظ و بهبود مستمر اثربخشی سیستم مدیریت کیفیت بر اساس استاندارد ISO 9001:2015 اعلام میدارد.",
|
||||
],
|
||||
qualityIntro: "خط مشی کیفیت عبارت است از:",
|
||||
qualityPolicy: [
|
||||
"افزایش تنوع و حجم محصولات تولیدی",
|
||||
"افزایش سطح رضایتمندی مشتریان",
|
||||
"توسعه استانداردهای ملی و بینالمللی در سازمان",
|
||||
],
|
||||
},
|
||||
aboutPage: {
|
||||
title: "درباره ما",
|
||||
kicker: "About Us",
|
||||
metaDescription:
|
||||
"آشنایی با شرکت صنایع کابل کمان؛ تولیدکننده سیم و کابل فشار ضعیف، مخابراتی و ابزار دقیق از سال ۱۳۷۰ در زنجان.",
|
||||
heroAlt: "درباره صنایع کابل کمان",
|
||||
companyTitle: "شرکت کابل کمان",
|
||||
intro:
|
||||
"شرکت کابل کمان یک شرکت موفق در زمینه تولید کابل و برق است که سابقه زیادی در این زمینه دارد. شرکت کابل کمان در حال حاضر از بهترین ماشینآلات شرکتهای سازنده معتبر اروپایی استفاده میکند و به تولید انواع سیم و کابل فشار ضعیف برق و مخابراتی، کابلهای کنترل و ابزار دقیق مشغول است. محصولات شرکت کابل کمان مطابق با استانداردهای ملی و بینالمللی است و محصولاتی با بهترین کیفیت را ارائه میدهد و به صورت گسترده نیاز بازارهای داخلی و کشورهای همسایه را با بالاترین کیفیت و سازگار با اهداف مشتری عرضه میکند.",
|
||||
goalsTitle: "اهداف",
|
||||
goalsKicker: "Quality Policy",
|
||||
honesty: "صداقت بهترین روش است.",
|
||||
goalsBody:
|
||||
"شرکت صنایع کابل کمان بهعنوان یکی از تولیدکنندگان سیم و کابل، رضایت مشتریان را از طریق ارتقاء کیفیت و رعایت کلیه قوانین و الزامات حاکم بر محصول خود، بهعنوان برترین هدف برگزیده است.",
|
||||
labTitle: "آزمایشگاه صنایع کابل کمان",
|
||||
labKicker: "Laboratory",
|
||||
labText:
|
||||
"طبق تاریخچه آزمایشگاه شرکت صنایع کابل کمان در سال ۱۳۹۱ موفق به دریافت گواهینامه آزمایشگاه همکار از موسسه استاندارد استان زنجان شد و بهعنوان همکار آن موسسه محترم اقدام به فعالیت نمود و هماکنون دارای گواهینامه تایید صلاحیت آزمایشگاه از طرف مرکز ملی تایید صلاحیت ایران (ISIRI-ISO/IEC 17025) میباشد. علاوه بر گواهینامههای فوق، تاییدیه از شرکت توانیر، شرکتهای توزیع نیروی برق استانهای مختلف و شرکتهای نفت را نیز دارا میباشد. آزمایشگاه شرکت با داشتن تجهیزات پیشرفته و متخصصین با تجربه و توانمند جهت انجام انواع تست، آمادگی خدمترسانی مطابق با نیاز واحدهای مختلف صنعت برق و مشتریان گرامی را دارد.",
|
||||
machineryTitle: "تجهیزات و ماشینآلات",
|
||||
machineryKicker: "Equipment & Machinery",
|
||||
machineryText:
|
||||
"این شرکت هماکنون مجهز به انواع ماشینآلات از شرکتهای سازنده معتبر اروپایی، در زمینه تولید انواع سیم و کابل فشار ضعیف برق و مخابراتی، کابلهای کنترل و ابزار دقیق مطابق با انواع استانداردهای ملی (ISIRI 607, 3569-1, 1926, …) و بینالمللی (IEC, BS, VDE, …) قادر به ارائه محصولات با کیفیت است.",
|
||||
guaranteeTitle: "ضمانتنامه شرکت",
|
||||
guaranteeKicker: "Guarantee",
|
||||
galleryTitle: "گالری",
|
||||
galleryKicker: "Gallery",
|
||||
galleryItems: [
|
||||
"کارخانه کابل کمان",
|
||||
"کلافهای کابل",
|
||||
"جزئیات کابل",
|
||||
"کلاف مس",
|
||||
"خط تولید",
|
||||
"ماشینآلات تولید",
|
||||
],
|
||||
stats: [
|
||||
{ value: "۱۳۷۰", label: "سال تاسیس", kicker: "Founded" },
|
||||
{ value: "۴۰٬۰۰۰", label: "متر مربع کارخانه", kicker: "Factory Area" },
|
||||
{ value: "۱۵٬۰۰۰", label: "تن ظرفیت سالانه", kicker: "Annual Capacity" },
|
||||
{ value: "ISO", label: "۹۰۰۱:۲۰۱۵ و ۱۷۰۲۵", kicker: "Certified" },
|
||||
],
|
||||
},
|
||||
contactPage: {
|
||||
title: "تماس با ما",
|
||||
heading: "با ما در ارتباط باشید",
|
||||
kicker: "Contact Us",
|
||||
metaDescription: "ارتباط با صنایع کابل کمان. دفتر تهران و کارخانه زنجان.",
|
||||
heroAlt: "تماس با صنایع کابل کمان",
|
||||
infoTitle: "اطلاعات تماس",
|
||||
mapTitle: "موقعیت دفتر صنایع کابل کمان",
|
||||
},
|
||||
contactBar: {
|
||||
label: "تماس با ما",
|
||||
subscribe: "شماره تماس خود را بگذارید، ما با شما تماس میگیریم.",
|
||||
},
|
||||
callback: {
|
||||
name: "نام",
|
||||
phone: "شماره تماس",
|
||||
submit: "ثبت شماره",
|
||||
sending: "در حال ارسال…",
|
||||
title: "درخواست تماس",
|
||||
body: "درخواست تماس از صفحه اصلی. شماره: {phone}",
|
||||
error: "ثبت شماره انجام نشد. لطفاً دوباره تلاش کنید.",
|
||||
ok: "شماره شما ثبت شد. بهزودی تماس میگیریم.",
|
||||
network: "ارتباط با سرور برقرار نشد.",
|
||||
},
|
||||
contactForm: {
|
||||
heading: "پیام خود را ارسال کنید",
|
||||
kicker: "Send a message",
|
||||
name: "نام",
|
||||
subject: "موضوع",
|
||||
email: "ایمیل",
|
||||
phone: "شماره تماس",
|
||||
message: "متن پیام",
|
||||
submit: "ارسال پیام",
|
||||
sending: "در حال ارسال…",
|
||||
error: "ارسال پیام انجام نشد. لطفاً دوباره تلاش کنید.",
|
||||
ok: "پیام شما ثبت شد. بهزودی با شما تماس میگیریم.",
|
||||
network: "ارتباط با سرور برقرار نشد.",
|
||||
},
|
||||
categories: [
|
||||
{
|
||||
id: "power",
|
||||
title: "کابل قدرت",
|
||||
kicker: "Power Cable",
|
||||
text: "سیمهای برق وظیفه انتقال جریان الکتریسیته را دارند و این انتقال توسط بخشهای داخلی تشکیلدهنده انجام میگیرد.",
|
||||
},
|
||||
{
|
||||
id: "flat",
|
||||
title: "کابلهای تخت",
|
||||
kicker: "Flat Cables",
|
||||
text: "این نوع کابلها جهت استفاده در بالابرها و نیز در اتصالات متحرک مناسب میباشند.",
|
||||
},
|
||||
{
|
||||
id: "phone",
|
||||
title: "کابلهای مخابراتی",
|
||||
kicker: "Phone Cables",
|
||||
text: "برای اینکه رطوبت حالت اتصال و اکسیدگی بین سیمها ایجاد نشود درون کابلهای مخابراتی را از نوعی ژله پلاستیکی چسبناک پر میکنند.",
|
||||
},
|
||||
{
|
||||
id: "network",
|
||||
title: "کابلهای شبکه",
|
||||
kicker: "Network Cable",
|
||||
text: "برای برقراری ارتباط در هر شبکه نیازمند کابل شبکه هستیم. کابل LAN ارتباط بین دو یا چند کامپیوتر، سرور، روتر و … را فراهم میکند.",
|
||||
},
|
||||
{
|
||||
id: "instrument",
|
||||
title: "ابزار دقیق و کنترل",
|
||||
kicker: "Instrumentation & Control",
|
||||
text: "ابزار دقیق در حقیقت زیرساخت یک سیستم کنترل و اتوماسیون را تشکیل میدهند و شامل انواع سنسور، انواع کنترلر و … میباشند.",
|
||||
},
|
||||
{
|
||||
id: "coaxial",
|
||||
title: "کواکسیال",
|
||||
kicker: "Coaxial",
|
||||
text: "کابل کواکسیال نوع خیلی رایج از کابل انتقال دادههای محافظتشده میباشد که از دو رسانا ساخته شده است.",
|
||||
},
|
||||
{
|
||||
id: "fire",
|
||||
title: "اعلام حریق",
|
||||
kicker: "Fire Alarm",
|
||||
text: "سیستمهای اعلام حریق وظیفه اطلاعرسانی در هنگام آتشسوزی را بر عهده دارند و میتوان آنها را به دو گروه تقسیم کرد.",
|
||||
},
|
||||
{
|
||||
id: "smart",
|
||||
title: "خانه هوشمند",
|
||||
kicker: "Smart House",
|
||||
text: "تفاوت خانه هوشمند با خانه معمولی این است که همه وسایل خانه هوشمند به یکدیگر متصل هستند و با دستگاه مرکزی کنترل میشوند.",
|
||||
},
|
||||
],
|
||||
partners: ["همکار ۱", "همکار ۲", "همکار ۳", "همکار ۴", "همکار ۵"],
|
||||
comments: {
|
||||
empty: "هنوز نظری ثبت نشده است.",
|
||||
submit: "ثبت نظر",
|
||||
sending: "در حال ارسال…",
|
||||
ok: "نظر شما ثبت شد و پس از تأیید نمایش داده میشود.",
|
||||
loginPrompt: "برای ثبت نظر وارد شوید.",
|
||||
loginBody: "برای ثبت نظر وارد حساب کاربری شوید.",
|
||||
user: "کاربر",
|
||||
yourComment: "نظر شما",
|
||||
placeholder: "نظر خود را بنویسید…",
|
||||
sendError: "ارسال نظر انجام نشد. لطفاً دوباره تلاش کنید.",
|
||||
unavailable: "ثبت نظر برای این مطلب در دسترس نیست.",
|
||||
noDescription: "توضیحاتی ثبت نشده است.",
|
||||
noSpecs: "مشخصات فنی برای این محصول ثبت نشده است.",
|
||||
tabsLabel: "جزئیات محصول",
|
||||
description: "توضیحات",
|
||||
technical: "مشخصات فنی",
|
||||
commentsTab: "نظرات",
|
||||
},
|
||||
};
|
||||
|
||||
export type Messages = typeof fa;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
import { defaultLocale, isLocale, localeCookie, type Locale } from "@/i18n/config";
|
||||
import { getMessages } from "@/i18n/index";
|
||||
|
||||
export async function getLocale(): Promise<Locale> {
|
||||
const value = (await cookies()).get(localeCookie)?.value;
|
||||
return isLocale(value) ? value : defaultLocale;
|
||||
}
|
||||
|
||||
export async function getDictionary() {
|
||||
const locale = await getLocale();
|
||||
return { locale, t: getMessages(locale) };
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ar } from "@/i18n/ar";
|
||||
import { defaultLocale, isLocale, type Locale } from "@/i18n/config";
|
||||
import { en } from "@/i18n/en";
|
||||
import { fa, type Messages } from "@/i18n/fa";
|
||||
|
||||
export const dictionaries: Record<Locale, Messages> = { fa, en, ar };
|
||||
|
||||
export function getMessages(locale: Locale | string | null | undefined): Messages {
|
||||
return dictionaries[isLocale(locale) ? locale : defaultLocale];
|
||||
}
|
||||
|
||||
export function interpolate(template: string, vars: Record<string, string>) {
|
||||
return Object.entries(vars).reduce((text, [key, value]) => text.split(`{${key}}`).join(value), template);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, type ReactNode } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
import { isCatalogPath, isLocale, localeCookie, localeDir, localeHtml, type Locale } from "@/i18n/config";
|
||||
import { getMessages } from "@/i18n/index";
|
||||
|
||||
const LocaleContext = createContext<Locale>("fa");
|
||||
|
||||
export function LocaleProvider({ locale, children }: { locale: Locale; children: ReactNode }) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const saved = window.localStorage.getItem(localeCookie);
|
||||
const hasCookie = document.cookie.split("; ").some((part) => part.startsWith(`${localeCookie}=`));
|
||||
if (!hasCookie && isLocale(saved) && saved !== locale) {
|
||||
persistLocale(saved);
|
||||
router.refresh();
|
||||
}
|
||||
}, [locale, router]);
|
||||
|
||||
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>;
|
||||
}
|
||||
|
||||
export function useLocale() {
|
||||
return useContext(LocaleContext);
|
||||
}
|
||||
|
||||
export function useMessages() {
|
||||
return getMessages(useLocale());
|
||||
}
|
||||
|
||||
export function persistLocale(locale: Locale) {
|
||||
document.cookie = `${localeCookie}=${locale}; Path=/; Max-Age=31536000; SameSite=Lax`;
|
||||
window.localStorage.setItem(localeCookie, locale);
|
||||
document.documentElement.lang = localeHtml[locale];
|
||||
document.documentElement.dir = localeDir[locale];
|
||||
}
|
||||
|
||||
export function useSetLocale() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
return useCallback(
|
||||
(next: string) => {
|
||||
if (!isLocale(next)) return;
|
||||
persistLocale(next);
|
||||
if (isCatalogPath(pathname) && next !== "fa") {
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
router.refresh();
|
||||
},
|
||||
[pathname, router],
|
||||
);
|
||||
}
|
||||
+7
-3
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import { site } from "@/data/site";
|
||||
import { defaultLocale, localeOg, type Locale } from "@/i18n/config";
|
||||
import { getMessages } from "@/i18n/index";
|
||||
import { websiteDomain } from "@/lib/config";
|
||||
|
||||
export const siteUrl = `https://${websiteDomain}`;
|
||||
@@ -11,15 +12,18 @@ export function pageMetadata({
|
||||
path = "/",
|
||||
image,
|
||||
absolute = false,
|
||||
locale = defaultLocale,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
path?: string;
|
||||
image?: string | null;
|
||||
absolute?: boolean;
|
||||
locale?: Locale;
|
||||
}): Metadata {
|
||||
const url = `${siteUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
const ogImage = image || `${siteUrl}/images/slider/bg1.jpg`;
|
||||
const t = getMessages(locale);
|
||||
|
||||
return {
|
||||
title: absolute ? { absolute: title } : title,
|
||||
@@ -27,9 +31,9 @@ export function pageMetadata({
|
||||
alternates: { canonical: url },
|
||||
openGraph: {
|
||||
type: path === "/" ? "website" : "article",
|
||||
locale: "fa_IR",
|
||||
locale: localeOg[locale],
|
||||
url,
|
||||
siteName: site.shortName,
|
||||
siteName: t.brand.shortName,
|
||||
title,
|
||||
description,
|
||||
images: [{ url: ogImage, alt: title }],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { meshkeeApiBase, websiteDomain } from "@/lib/config";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
export type SlotKind = "single" | "list";
|
||||
|
||||
@@ -158,9 +159,9 @@ export function parseAspectRatio(ratio?: string | null) {
|
||||
}
|
||||
|
||||
/** CSS custom property consumed by `.about-photo`, `.product-card .thumb`, `.client`. */
|
||||
export function slotRatioStyle(ratio?: string | null): { "--slot-ratio": string } | undefined {
|
||||
export function slotRatioStyle(ratio?: string | null): CSSProperties | undefined {
|
||||
const parsed = parseAspectRatio(ratio);
|
||||
return parsed ? { "--slot-ratio": parsed } : undefined;
|
||||
return parsed ? ({ ["--slot-ratio"]: parsed } as CSSProperties) : undefined;
|
||||
}
|
||||
|
||||
export function pickSlot(slots: StaticImageSlot[], key: string) {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { isCatalogPath, isLocale, localeCookie, showsCatalog } from "@/i18n/config";
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const raw = request.cookies.get(localeCookie)?.value;
|
||||
const locale = isLocale(raw) ? raw : "fa";
|
||||
if (!showsCatalog(locale) && isCatalogPath(request.nextUrl.pathname)) {
|
||||
return NextResponse.redirect(new URL("/", request.url));
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/products", "/products/:path*", "/blog", "/blog/:path*"],
|
||||
};
|
||||
Reference in New Issue
Block a user