Initial commit: BRB Store Next.js storefront with Meshkee image slots.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-21 12:13:41 +03:30
co-authored by Cursor
commit cfa21cbee9
79 changed files with 9482 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Container } from "@/components/Container";
import { PageHero } from "@/components/layout/PageHero";
import { Placeholder } from "@/components/Placeholder";
import { about, features, site, stats } from "@/lib/data";
export const metadata: Metadata = {
title: "درباره ما",
description: about.paragraphs[0],
};
export default function AboutPage() {
return (
<>
<PageHero kicker="About us" title="درباره ما" image="/images/slides/slide-1.jpg" />
<section className="py-20 sm:py-28">
<Container className="grid items-start gap-14 lg:grid-cols-12">
<div className="lg:col-span-6">
<p className="section-kicker">Burgheii Brothers</p>
<h2 className="mt-4 text-2xl font-extrabold leading-snug sm:text-[32px]">{about.title}</h2>
<div className="mt-8 space-y-5 text-[16px] leading-9 text-muted">
{about.paragraphs.map((paragraph) => (
<p key={paragraph}>{paragraph}</p>
))}
</div>
<Link href="/contact" className="mt-8 inline-flex rounded-xl bg-brand px-6 py-3 text-[14px] font-medium text-white hover:bg-brand-dark">
شروع همکاری / تماس
</Link>
</div>
<div className="relative lg:col-span-6">
<Placeholder src={about.image} alt={site.name} className="w-full rounded-2xl object-cover" />
<div className="mt-5 grid grid-cols-2 gap-3">
{stats.map((stat) => (
<div key={stat.label} className="rounded-2xl bg-paper px-5 py-5">
<p className="text-2xl font-extrabold text-brand">{stat.value}</p>
<p className="mt-1 text-[12px] leading-6 text-muted">{stat.label}</p>
</div>
))}
</div>
</div>
</Container>
</section>
<section className="border-t border-line py-16">
<Container className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{features.map((feature, index) => (
<div key={feature.title} className="border-r border-line pr-5">
<p className="font-latin text-[11px] tracking-[0.28em] text-brand">0{index + 1}</p>
<h3 className="mt-2 text-[16px] font-bold">{feature.title}</h3>
<p className="mt-2 text-[13px] leading-7 text-muted">{feature.text}</p>
</div>
))}
</Container>
</section>
</>
);
}
+51
View File
@@ -0,0 +1,51 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Container } from "@/components/Container";
import { PageHero } from "@/components/layout/PageHero";
import { blogPosts, getBlogPost } from "@/lib/data";
export function generateStaticParams() {
return blogPosts.map((post) => ({ slug: post.slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = getBlogPost(slug);
if (!post) return { title: "مقاله" };
return { title: post.title, description: post.excerpt };
}
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = getBlogPost(slug);
if (!post) notFound();
return (
<>
<PageHero kicker="Blog" title={post.title} image={post.image} />
<section className="py-16 sm:py-24">
<Container className="max-w-3xl">
<p className="font-latin text-[12px] tracking-[0.18em] text-muted">{post.date}</p>
<div className="mt-8 space-y-5 text-[16px] leading-9 text-muted">
{post.body.map((paragraph) => (
<p key={paragraph}>{paragraph}</p>
))}
</div>
<Link href="/blog" className="mt-10 inline-flex items-center gap-3 text-[14px] font-bold text-brand">
بازگشت به مقالات
<span className="h-px w-10 bg-brand" />
</Link>
</Container>
</section>
</>
);
}
+39
View File
@@ -0,0 +1,39 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Container } from "@/components/Container";
import { PageHero } from "@/components/layout/PageHero";
import { Placeholder } from "@/components/Placeholder";
import { blogPosts } from "@/lib/data";
export const metadata: Metadata = {
title: "مقالات",
description: "نکته‌هایی از تعمیرگاه و بورس قطعات لوازم خانگی برادران برقعی.",
};
export default function BlogPage() {
return (
<>
<PageHero kicker="Blog" title="مقالات" image="/images/slides/slide-2.jpg" />
<section className="py-16 sm:py-24">
<Container>
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
{blogPosts.map((post) => (
<Link key={post.slug} href={`/blog/${post.slug}`} className="group block">
<div className="overflow-hidden rounded-2xl">
<Placeholder
src={post.image}
alt={post.title}
className="aspect-[16/10] w-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
</div>
<p className="mt-4 font-latin text-[11px] tracking-[0.18em] text-muted">{post.date}</p>
<h2 className="mt-2 text-[18px] font-bold leading-7 group-hover:text-brand">{post.title}</h2>
<p className="mt-2 line-clamp-2 text-[14px] leading-7 text-muted">{post.excerpt}</p>
</Link>
))}
</div>
</Container>
</section>
</>
);
}
+93
View File
@@ -0,0 +1,93 @@
import type { Metadata } from "next";
import { Mail, MapPin, Send } from "lucide-react";
import { Container } from "@/components/Container";
import { ContactForm } from "@/components/contact/ContactForm";
import { PageHero } from "@/components/layout/PageHero";
import { Placeholder } from "@/components/Placeholder";
import { branches, phones, site } from "@/lib/data";
export const metadata: Metadata = {
title: "ارتباط با ما",
description: "آدرس شعب، تلفن و فرم تماس فروشگاه و تعمیرگاه برادران برقعی در قم.",
};
export default function ContactPage() {
return (
<>
<PageHero kicker="Contact us" title="ارتباط با ما" image="/images/lifestyle/theme-photos-Klby0nxseY8-unsplash.jpg" />
<section className="py-16 sm:py-20">
<Container>
<p className="max-w-2xl text-[16px] leading-8 text-muted">
مجموعه ی فروشگاه و تعمیرگاه برادران برقعی دارای فعالیت در زمینه های فروش لوازم خانگی، فروش قطعات لوازم خانگی
و تعمیرات آنها می باشد.
</p>
<div className="mt-10 grid gap-5 md:grid-cols-2">
{branches.map((branch) => (
<article key={branch.id} className="overflow-hidden rounded-2xl border border-line">
<div className="relative aspect-[16/7]">
<Placeholder src={branch.image} alt={branch.title} className="h-full w-full object-cover" />
</div>
<div className="flex gap-3 p-5">
<MapPin className="mt-1 size-5 shrink-0 text-brand" />
<div>
<h2 className="text-[16px] font-bold">{branch.title}</h2>
<p className="mt-1 text-[14px] leading-7 text-muted">{branch.address}</p>
</div>
</div>
</article>
))}
</div>
</Container>
</section>
<section className="bg-paper py-16 sm:py-20">
<Container className="grid gap-12 lg:grid-cols-12">
<div className="lg:col-span-5">
<h2 className="text-[22px] font-bold">تلفن و تلگرام</h2>
<ul className="mt-6 divide-y divide-line overflow-hidden rounded-2xl border border-line bg-white">
{phones.map((phone) => (
<li key={phone.value} className="flex items-center justify-between gap-3 px-4 py-3">
<span className="text-[12px] text-muted">{phone.label}</span>
<a href={phone.href} className="font-latin text-[16px] font-medium hover:text-brand" dir="ltr">
{phone.value}
</a>
</li>
))}
</ul>
<div className="mt-6 space-y-3 text-[14px]">
<a href={`mailto:${site.email}`} className="flex items-center gap-2 hover:text-brand">
<Mail className="size-4 text-brand" />
{site.emailLabel}
</a>
<a
href={site.telegram}
target="_blank"
rel="noreferrer"
className="flex items-center gap-2 hover:text-brand"
>
<Send className="size-4 text-brand" />
کانال تلگرام borgheeibrothers
</a>
</div>
</div>
<div className="rounded-2xl bg-white p-6 sm:p-8 lg:col-span-7">
<h2 className="mb-2 text-[22px] font-bold">پیام ارسال کنید</h2>
<p className="mb-6 text-[13px] text-muted">نام، شماره و پیام — پاسخ از شعبه قم.</p>
<ContactForm />
<div className="mt-8 overflow-hidden rounded-2xl">
<iframe
title="موقعیت فروشگاه برادران برقعی"
src={site.map.embed}
className="h-[280px] w-full"
loading="lazy"
/>
</div>
</div>
</Container>
</section>
</>
);
}
+79
View File
@@ -0,0 +1,79 @@
@import "tailwindcss";
:root {
--brand: #e7432b;
--brand-dark: #c93621;
--ink: #161616;
--muted: #6c6363;
--line: #e4ddd8;
--paper: #f4f0ec;
--white: #ffffff;
}
@theme inline {
--color-brand: var(--brand);
--color-brand-dark: var(--brand-dark);
--color-ink: var(--ink);
--color-muted: var(--muted);
--color-line: var(--line);
--color-paper: var(--paper);
--font-sans: var(--font-vazir), Tahoma, sans-serif;
--font-latin: var(--font-montserrat), var(--font-vazir), sans-serif;
}
html {
scroll-behavior: smooth;
}
body {
background: var(--white);
color: var(--ink);
font-family: var(--font-sans);
}
::selection {
background: var(--brand);
color: #fff;
}
.font-latin {
font-family: var(--font-latin);
}
.section-kicker {
font-family: var(--font-latin);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--brand);
}
.glass-box,
.caption-card {
background: rgb(255 255 255 / 0.18);
border: 1px solid rgb(255 255 255 / 0.72);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.9),
0 12px 32px rgb(0 0 0 / 0.1);
border-radius: 1rem;
}
.image-mask {
background: rgb(22 22 22 / 0.55);
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
input,
textarea,
button {
font-family: inherit;
}
+44
View File
@@ -0,0 +1,44 @@
import type { Metadata } from "next";
import { Montserrat, Vazirmatn } from "next/font/google";
import "./globals.css";
import { Footer } from "@/components/layout/Footer";
import { Header } from "@/components/layout/Header";
import { TopBar } from "@/components/layout/TopBar";
import { site } from "@/lib/data";
const vazir = Vazirmatn({
subsets: ["arabic", "latin"],
variable: "--font-vazir",
display: "swap",
weight: ["300", "400", "500", "600", "700", "800"],
});
const montserrat = Montserrat({
subsets: ["latin"],
variable: "--font-montserrat",
display: "swap",
weight: ["400", "500", "600", "700"],
});
export const metadata: Metadata = {
title: {
default: `${site.name} | ${site.tagline}`,
template: `%s | ${site.name}`,
},
description: site.description,
keywords: site.keywords,
icons: { icon: "/favicon.ico" },
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="fa" dir="rtl" className={`${vazir.variable} ${montserrat.variable}`}>
<body className={`${vazir.className} min-h-screen antialiased`}>
<TopBar />
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
+16
View File
@@ -0,0 +1,16 @@
import { STATIC_IMAGE_SLOTS } from "@/lib/static-images";
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
"Cache-Control": "public, max-age=60",
};
export async function OPTIONS() {
return new Response(null, { status: 204, headers });
}
export async function GET() {
return Response.json({ slots: STATIC_IMAGE_SLOTS }, { headers });
}
+136
View File
@@ -0,0 +1,136 @@
import { AboutPreview } from "@/components/home/AboutPreview";
import { BannerRow } from "@/components/home/BannerRow";
import { BlogCarousel } from "@/components/home/BlogCarousel";
import { Branches } from "@/components/home/Branches";
import { CategoryShowcase } from "@/components/home/CategoryShowcase";
import { Hero } from "@/components/home/Hero";
import { Services } from "@/components/home/Services";
import { Stats } from "@/components/home/Stats";
import { WideBanner } from "@/components/home/WideBanner";
import {
about,
bannersBelowSlider,
branches,
services,
showcaseCategories,
slides,
threeBannerRow,
twoBannerRow,
} from "@/lib/data";
import { fetchStaticImageSlots, overlaySlotImages, pickSlot } from "@/lib/static-images";
export const revalidate = 60;
export default async function HomePage() {
const slots = await fetchStaticImageSlots("home");
const slider = overlaySlotImages(
pickSlot(slots, "slider"),
slides.map((slide) => ({
id: slide.id,
image: slide.image,
href: slide.href,
title: slide.title,
kicker: slide.kicker,
})),
"slider",
);
const icons = overlaySlotImages(
pickSlot(slots, "three-icon-image"),
services.map((service) => ({
id: service.id,
image: "",
href: service.href,
title: service.title,
text: service.text,
})),
"three-icon-image",
);
const categories = overlaySlotImages(
pickSlot(slots, "categories"),
showcaseCategories.map((item) => ({
id: item.id,
image: item.image,
href: item.href,
title: item.title,
text: item.text,
})),
"categories",
);
const aboutImage = overlaySlotImages(
pickSlot(slots, "about-us-image"),
[{ id: "about", image: about.image }],
"about-us-image",
);
const fiveBanners = overlaySlotImages(
pickSlot(slots, "five-banner-section"),
[...twoBannerRow, ...threeBannerRow].map((banner) => ({
id: banner.id,
image: banner.image,
href: banner.href,
title: banner.title,
kicker: banner.kicker,
text: banner.text,
})),
"five-banner-section",
);
const twoBanners = overlaySlotImages(
pickSlot(slots, "two-banner-row"),
bannersBelowSlider.map((banner) => ({
id: banner.id,
image: banner.image,
href: banner.href,
title: banner.title,
kicker: banner.kicker,
text: banner.text,
})),
"two-banner-row",
);
const fullRow = overlaySlotImages(
pickSlot(slots, "full-row-image"),
[
{
id: "wide",
image: "/images/banners/wide.jpg",
href: "/contact",
title: "اگر به دنبال قطعه ی خاصی هستید، با ما تماس بگیرید.",
kicker: "Contact us",
},
],
"full-row-image",
);
const branchImages = overlaySlotImages(
pickSlot(slots, "two-branch-image"),
branches.map((branch) => ({
id: branch.id,
image: branch.image,
href: "/contact",
title: branch.title,
text: branch.address,
})),
"two-branch-image",
);
return (
<>
<Hero slides={slider.items} aspectRatio={slider.aspectRatio} />
<Services items={icons.items} />
<CategoryShowcase items={categories.items} aspectRatio={categories.aspectRatio} />
<AboutPreview image={aboutImage.items[0]} aspectRatio={aboutImage.aspectRatio} />
<BannerRow banners={fiveBanners.items.slice(0, 2)} columns={2} aspectRatio={fiveBanners.aspectRatio} />
<BannerRow banners={fiveBanners.items.slice(2, 5)} columns={3} aspectRatio={fiveBanners.aspectRatio} />
<BlogCarousel />
<BannerRow banners={twoBanners.items} columns={2} aspectRatio={twoBanners.aspectRatio} />
<Stats />
<WideBanner image={fullRow.items[0]} aspectRatio={fullRow.aspectRatio} />
<Branches images={branchImages.items} aspectRatio={branchImages.aspectRatio} />
</>
);
}