Initial commit: BRB Store Next.js storefront with Meshkee image slots.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user