Build the Salar Store storefront and connect products, blogs, and home specials to the Meshkee API.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
2783305f07
commit
7d26eaa838
@@ -0,0 +1,57 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import { PageHero } from "@/components/PageHero";
|
||||
import { SITE } from "@/lib/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `درباره ما | ${SITE.nameFa}`,
|
||||
description: SITE.about,
|
||||
};
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHero title="درباره ما" english="About Us" />
|
||||
<section className="container-site py-10 md:py-14">
|
||||
<div className="relative overflow-hidden rounded-2xl min-h-[280px] md:min-h-[360px]">
|
||||
<Image
|
||||
src="/images/pages/about-bg.webp"
|
||||
alt=""
|
||||
fill
|
||||
priority
|
||||
sizes="1200px"
|
||||
className="object-cover object-center"
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to left, rgba(0,0,0,0.82) 0%, rgba(0,0,0,0.55) 48%, rgba(0,0,0,0.2) 100%)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative z-[1] max-w-xl p-6 md:p-10 text-white">
|
||||
<h2 className="text-2xl md:text-4xl font-extrabold mb-4">{SITE.nameFa}</h2>
|
||||
<p className="text-sm md:text-base leading-8 text-white/90 text-justify">{SITE.about}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 grid md:grid-cols-2 gap-8 items-center rounded-2xl bg-zinc-50 p-5 md:p-8">
|
||||
<div>
|
||||
<p className="font-carter text-brand tracking-[0.2em] text-sm mb-2">SALAR STORE</p>
|
||||
<h2 className="section-title mb-4">سالار استور</h2>
|
||||
<p className="text-zinc-600 leading-8 text-justify">{SITE.about}</p>
|
||||
</div>
|
||||
<div className="relative mx-auto w-full max-w-xs">
|
||||
<Image
|
||||
src="/images/pages/about-side.png"
|
||||
alt={SITE.nameFa}
|
||||
width={300}
|
||||
height={603}
|
||||
className="h-auto w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { API } from "@/lib/site";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
const response = await fetch(`${API.base}/tenants/${API.domain}/comments`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await response.json();
|
||||
return Response.json(data, { status: response.status });
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { listMappedProducts } from "@/lib/mappers";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const query = new URL(request.url).searchParams.get("q")?.trim() ?? "";
|
||||
if (query.length < 2) {
|
||||
return Response.json({ items: [] });
|
||||
}
|
||||
|
||||
const data = await listMappedProducts({ name: query, page: 1, pageSize: 8 });
|
||||
return Response.json({ items: data.items, total: data.total });
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Media } from "@/components/Media";
|
||||
import { PageHero } from "@/components/PageHero";
|
||||
import { RichContent } from "@/components/RichContent";
|
||||
import { getBlog } from "@/lib/api";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { PLACEHOLDER } from "@/lib/data";
|
||||
import { SITE } from "@/lib/site";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ slug: string }>;
|
||||
};
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
try {
|
||||
const blog = await getBlog(slug);
|
||||
if (!blog) return { title: SITE.nameFa };
|
||||
return {
|
||||
title: `${blog.title} | ${SITE.nameFa}`,
|
||||
description: blog.abstract || SITE.aboutShort,
|
||||
};
|
||||
} catch {
|
||||
return { title: SITE.nameFa };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function BlogDetailPage({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const blog = await getBlog(slug);
|
||||
if (!blog) notFound();
|
||||
|
||||
const author = [blog.author?.firstName, blog.author?.lastName].filter(Boolean).join(" ");
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
title={blog.title}
|
||||
english="Blog"
|
||||
crumbs={[{ name: "وبلاگ", href: "/blog" }, { name: blog.title }]}
|
||||
/>
|
||||
<article className="container-site py-10 md:py-14">
|
||||
<Media
|
||||
src={blog.titleImageUrl || PLACEHOLDER.blog}
|
||||
alt={blog.title}
|
||||
className="aspect-[16/7] rounded-2xl"
|
||||
sizes="1200px"
|
||||
priority
|
||||
/>
|
||||
<div className="mt-6 flex flex-wrap gap-3 text-sm text-zinc-400">
|
||||
{blog.publishedAt || blog.createdAt ? <time>{formatDate(blog.publishedAt || blog.createdAt || "")}</time> : null}
|
||||
{blog.categoryName ? <span>{blog.categoryName}</span> : null}
|
||||
{author ? <span>{author}</span> : null}
|
||||
</div>
|
||||
{blog.abstract ? <p className="mt-4 text-zinc-600 leading-8">{blog.abstract}</p> : null}
|
||||
<div className="mt-8">
|
||||
<RichContent html={blog.mainTextHtml} />
|
||||
</div>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { Media } from "@/components/Media";
|
||||
import { PageHero } from "@/components/PageHero";
|
||||
import { Pagination } from "@/components/Pagination";
|
||||
import { getBlogs } from "@/lib/api";
|
||||
import { mapBlog } from "@/lib/mappers";
|
||||
import { SITE } from "@/lib/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `وبلاگ | ${SITE.nameFa}`,
|
||||
description: "آخرین مقالات سالار استور",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
};
|
||||
|
||||
export default async function BlogPage({ searchParams }: Props) {
|
||||
const { page: pageParam } = await searchParams;
|
||||
const page = Math.max(1, Number(pageParam) || 1);
|
||||
let data: Awaited<ReturnType<typeof getBlogs>> = { items: [], total: 0, page, pageSize: 12 };
|
||||
try {
|
||||
data = await getBlogs({ page, pageSize: 12 });
|
||||
} catch {
|
||||
data = { items: [], total: 0, page, pageSize: 12 };
|
||||
}
|
||||
const posts = data.items.map(mapBlog);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHero title="وبلاگ سالار استور" english="Blog" />
|
||||
<section className="container-site py-10 md:py-14">
|
||||
{posts.length ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post) => (
|
||||
<Link
|
||||
key={post.slug}
|
||||
href={`/blog/${post.slug}`}
|
||||
className="overflow-hidden rounded-2xl border border-black/5 bg-white hover:border-brand/40"
|
||||
>
|
||||
<Media src={post.image} alt="" className="aspect-[16/10]" sizes="400px" />
|
||||
<div className="p-4">
|
||||
<time className="text-xs text-zinc-400">{post.date}</time>
|
||||
<h2 className="mt-2 text-base font-bold leading-8 line-clamp-2">{post.title}</h2>
|
||||
{post.abstract ? (
|
||||
<p className="mt-1 text-sm leading-7 text-zinc-500 line-clamp-2">{post.abstract}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="py-16 text-center text-sm text-zinc-500">مقالهای یافت نشد.</p>
|
||||
)}
|
||||
<Pagination
|
||||
page={data.page}
|
||||
pageSize={data.pageSize}
|
||||
total={data.total}
|
||||
makeHref={(nextPage) => (nextPage > 1 ? `/blog?page=${nextPage}` : "/blog")}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ContactForm } from "@/components/ContactForm";
|
||||
import { PageHero } from "@/components/PageHero";
|
||||
import { IconInstagram, IconPhone, IconPin, IconTelegram, IconWhatsapp } from "@/components/Icons";
|
||||
import { SITE } from "@/lib/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `تماس با ما | ${SITE.nameFa}`,
|
||||
description: SITE.address,
|
||||
};
|
||||
|
||||
const contacts = [
|
||||
{
|
||||
href: `tel:${SITE.phone}`,
|
||||
label: SITE.phoneDisplay,
|
||||
dir: "ltr" as const,
|
||||
numeric: true,
|
||||
icon: IconPhone,
|
||||
},
|
||||
{
|
||||
href: SITE.whatsapp,
|
||||
label: SITE.phoneDisplay,
|
||||
dir: "ltr" as const,
|
||||
numeric: true,
|
||||
icon: IconWhatsapp,
|
||||
},
|
||||
{
|
||||
href: SITE.instagram,
|
||||
label: SITE.instagramHandle,
|
||||
dir: "ltr" as const,
|
||||
numeric: false,
|
||||
icon: IconInstagram,
|
||||
},
|
||||
{
|
||||
href: SITE.telegram,
|
||||
label: SITE.telegramHandle,
|
||||
dir: "ltr" as const,
|
||||
numeric: false,
|
||||
icon: IconTelegram,
|
||||
},
|
||||
{
|
||||
href: undefined,
|
||||
label: SITE.address,
|
||||
dir: undefined,
|
||||
numeric: false,
|
||||
icon: IconPin,
|
||||
},
|
||||
];
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHero title="تماس با ما" english="Contact Us" />
|
||||
<section className="container-site py-10 md:py-14">
|
||||
<div className="grid gap-8 lg:grid-cols-[280px_1fr] items-start">
|
||||
<div className="rounded-2xl border border-black/5 divide-y divide-black/5">
|
||||
{contacts.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const content = (
|
||||
<>
|
||||
<span className="grid place-items-center size-10 rounded-full bg-zinc-50 text-brand shrink-0">
|
||||
<Icon className="w-5 h-5" />
|
||||
</span>
|
||||
<span dir={item.dir} className={item.numeric ? "font-oswald tracking-wide" : ""}>
|
||||
{item.label}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
if (!item.href) {
|
||||
return (
|
||||
<div key={item.label} className="flex items-center gap-3 px-4 py-4 text-sm">
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
key={item.href + item.label}
|
||||
href={item.href}
|
||||
className="flex items-center gap-3 px-4 py-4 text-sm hover:text-brand"
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ContactForm />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+251
-17
@@ -1,26 +1,260 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-background: #ffffff;
|
||||
--color-foreground: #292d32;
|
||||
--color-brand: #fc2424;
|
||||
--color-brand-soft: #ffecec;
|
||||
--color-ink: #292d32;
|
||||
--color-muted: #f5f6f9;
|
||||
--font-sans: var(--font-vazir), Tahoma, sans-serif;
|
||||
--font-oswald: var(--font-oswald), Impact, sans-serif;
|
||||
--font-carter: var(--font-carter-one), Impact, sans-serif;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background: #ffffff;
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.font-oswald {
|
||||
font-family: var(--font-oswald);
|
||||
}
|
||||
|
||||
.font-carter {
|
||||
font-family: var(--font-carter);
|
||||
}
|
||||
|
||||
.container-site {
|
||||
width: min(1200px, calc(100% - 2rem));
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.page-hero {
|
||||
background-color: #fbfbfc;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'%3E%3Ccircle cx='2' cy='2' r='1' fill='%23292d32' fill-opacity='0.06'/%3E%3C/svg%3E");
|
||||
background-size: 28px 28px;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #fc2424;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.footer-bottom-bar {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.footer-bottom-site {
|
||||
color: #9c9c9c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.meshkee-credit {
|
||||
align-items: center;
|
||||
color: #9c9c9c;
|
||||
display: inline-flex;
|
||||
gap: 10px;
|
||||
padding: 6px 10px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.meshkee-credit:hover {
|
||||
color: #e0e0e0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.meshkee-credit__frame {
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.meshkee-credit__line {
|
||||
background: #777;
|
||||
position: absolute;
|
||||
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.meshkee-credit__line--l {
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
transition-delay: 0s;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.meshkee-credit__line--b {
|
||||
bottom: 0;
|
||||
height: 1px;
|
||||
left: 0;
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition-delay: 0.14s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.meshkee-credit__line--r {
|
||||
height: 100%;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transform: scaleY(0);
|
||||
transform-origin: bottom;
|
||||
transition-delay: 0.28s;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.meshkee-credit__line--t {
|
||||
height: 1px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform: scaleX(0);
|
||||
transform-origin: right;
|
||||
transition-delay: 0.42s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.meshkee-credit:hover .meshkee-credit__line--l,
|
||||
.meshkee-credit:hover .meshkee-credit__line--r {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.meshkee-credit:hover .meshkee-credit__line--b,
|
||||
.meshkee-credit:hover .meshkee-credit__line--t {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.meshkee-credit:not(:hover) .meshkee-credit__line--l {
|
||||
transition-delay: 0.42s;
|
||||
}
|
||||
|
||||
.meshkee-credit:not(:hover) .meshkee-credit__line--b {
|
||||
transition-delay: 0.28s;
|
||||
}
|
||||
|
||||
.meshkee-credit:not(:hover) .meshkee-credit__line--r {
|
||||
transition-delay: 0.14s;
|
||||
}
|
||||
|
||||
.meshkee-credit:not(:hover) .meshkee-credit__line--t {
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
.meshkee-credit__logo {
|
||||
aspect-ratio: 1 / 1;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
height: 42px;
|
||||
mix-blend-mode: screen;
|
||||
object-fit: contain;
|
||||
opacity: 0.7;
|
||||
position: relative;
|
||||
width: auto;
|
||||
transition: opacity 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.meshkee-credit:hover .meshkee-credit__logo {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.meshkee-credit__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
line-height: 1.25;
|
||||
min-height: 42px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.meshkee-credit__label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.meshkee-credit__name {
|
||||
color: #cfcfcf;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.meshkee-credit:hover .meshkee-credit__name {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.footer-bottom-bar {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.draw-frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
border: 1.5px solid currentColor;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.draw-frame-host:hover .draw-frame {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.rich-content {
|
||||
color: #444;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.rich-content h2,
|
||||
.rich-content h3,
|
||||
.rich-content h4 {
|
||||
color: var(--color-ink);
|
||||
font-weight: 800;
|
||||
margin: 1.5rem 0 0.75rem;
|
||||
}
|
||||
|
||||
.rich-content p,
|
||||
.rich-content li {
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
|
||||
.rich-content ul,
|
||||
.rich-content ol {
|
||||
padding-inline-start: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.rich-content a {
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.rich-content img {
|
||||
border-radius: 12px;
|
||||
display: block;
|
||||
height: auto;
|
||||
margin: 1.25rem auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
+28
-13
@@ -1,29 +1,44 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Carter_One, Oswald, Vazirmatn } from "next/font/google";
|
||||
import { Footer } from "@/components/Footer";
|
||||
import { Header } from "@/components/Header";
|
||||
import { TopHeader } from "@/components/TopHeader";
|
||||
import { SITE } from "@/lib/site";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
const vazir = Vazirmatn({
|
||||
subsets: ["arabic", "latin"],
|
||||
variable: "--font-vazir",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
const oswald = Oswald({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-oswald",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const carter = Carter_One({
|
||||
weight: "400",
|
||||
subsets: ["latin"],
|
||||
variable: "--font-carter-one",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: `${SITE.nameFa} | ${SITE.tagline}`,
|
||||
description: SITE.aboutShort,
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
<html lang="fa" dir="rtl" className={`${vazir.variable} ${oswald.variable} ${carter.variable} h-full antialiased`}>
|
||||
<body className="min-h-full flex flex-col bg-white text-ink">
|
||||
<TopHeader />
|
||||
<Header />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<section className="container-site py-24 text-center">
|
||||
<h1 className="text-3xl font-extrabold">صفحه پیدا نشد</h1>
|
||||
<p className="mt-3 text-zinc-500">محتوای مورد نظر در دسترس نیست.</p>
|
||||
<Link href="/" className="mt-6 inline-block text-brand">
|
||||
بازگشت به خانه
|
||||
</Link>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+19
-64
@@ -1,69 +1,24 @@
|
||||
import Image from "next/image";
|
||||
import { AboutUs } from "@/components/AboutUs";
|
||||
import { BannerGrid4, BannerRow3 } from "@/components/Banners";
|
||||
import { BlogCarousel } from "@/components/BlogCarousel";
|
||||
import { Brands } from "@/components/Brands";
|
||||
import { Categories } from "@/components/Categories";
|
||||
import { HeroSlider } from "@/components/HeroSlider";
|
||||
import { SpecialSale } from "@/components/SpecialSale";
|
||||
import { TopSelling } from "@/components/TopSelling";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert h-5 w-[100px]"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the{" "}
|
||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
||||
page.tsx
|
||||
</code>{" "}
|
||||
file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert h-[14px] w-4"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={14}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<>
|
||||
<HeroSlider />
|
||||
<SpecialSale />
|
||||
<Categories />
|
||||
<BannerGrid4 />
|
||||
<TopSelling />
|
||||
<BannerRow3 />
|
||||
<Brands />
|
||||
<AboutUs />
|
||||
<BlogCarousel />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { PageHero } from "@/components/PageHero";
|
||||
import { Pagination } from "@/components/Pagination";
|
||||
import { ProductGrid } from "@/components/ProductGrid";
|
||||
import { ProductTabs } from "@/components/ProductTabs";
|
||||
import { ProductView } from "@/components/ProductView";
|
||||
import { getCategories, getCategoryBySlug, getComments, getProduct, getStoreItemByProduct } from "@/lib/api";
|
||||
import { listMappedProductsForCategory } from "@/lib/mappers";
|
||||
import { SITE } from "@/lib/site";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ slug: string }>;
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
};
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
try {
|
||||
const product = await getProduct(slug);
|
||||
if (product) {
|
||||
return {
|
||||
title: `${product.nameFa || product.title} | ${SITE.nameFa}`,
|
||||
description: product.summary || SITE.tagline,
|
||||
};
|
||||
}
|
||||
|
||||
const category = await getCategoryBySlug(slug);
|
||||
if (category) {
|
||||
return {
|
||||
title: `${category.nameFa || category.name} | ${SITE.nameFa}`,
|
||||
description: SITE.tagline,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
|
||||
return { title: SITE.nameFa };
|
||||
}
|
||||
|
||||
export default async function ProductOrCategoryPage({ params, searchParams }: Props) {
|
||||
const { slug } = await params;
|
||||
const product = await getProduct(slug);
|
||||
|
||||
if (product) {
|
||||
const [storeItem, comments] = await Promise.all([
|
||||
getStoreItemByProduct(product.id),
|
||||
getComments("product", product.id),
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
title={product.nameFa || product.title}
|
||||
english={product.title}
|
||||
crumbs={[{ name: "محصولات", href: "/products" }, { name: product.nameFa || product.title }]}
|
||||
/>
|
||||
<section className="container-site py-10 md:py-14">
|
||||
<ProductView product={product} variants={storeItem?.variants ?? []} />
|
||||
<ProductTabs productId={product.id} descriptionHtml={product.descriptionHtml} comments={comments} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const [category, categories] = await Promise.all([getCategoryBySlug(slug), getCategories()]);
|
||||
if (!category) notFound();
|
||||
|
||||
const { page: pageParam } = await searchParams;
|
||||
const page = Math.max(1, Number(pageParam) || 1);
|
||||
const children = categories.filter((item) => item.parentId === category.id);
|
||||
const data = await listMappedProductsForCategory(
|
||||
category.id,
|
||||
children.map((item) => item.id),
|
||||
page,
|
||||
16,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
title={category.nameFa || category.name}
|
||||
english={category.name}
|
||||
crumbs={[{ name: "محصولات", href: "/products" }, { name: category.nameFa || category.name }]}
|
||||
/>
|
||||
<section className="container-site py-10 md:py-14">
|
||||
{children.length > 0 && (
|
||||
<div className="mb-8 flex flex-wrap gap-2">
|
||||
{children.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={`/products/${item.slug}`}
|
||||
className="rounded-full border border-black/10 px-4 py-2 text-sm hover:border-brand hover:text-brand"
|
||||
>
|
||||
{item.nameFa || item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<ProductGrid products={data.items} />
|
||||
<Pagination
|
||||
page={data.page}
|
||||
pageSize={data.pageSize}
|
||||
total={data.total}
|
||||
makeHref={(nextPage) => (nextPage > 1 ? `/products/${slug}?page=${nextPage}` : `/products/${slug}`)}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Metadata } from "next";
|
||||
import { PageHero } from "@/components/PageHero";
|
||||
import { Pagination } from "@/components/Pagination";
|
||||
import { ProductGrid } from "@/components/ProductGrid";
|
||||
import { listMappedProducts } from "@/lib/mappers";
|
||||
import { SITE } from "@/lib/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `محصولات | ${SITE.nameFa}`,
|
||||
description: SITE.tagline,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
searchParams: Promise<{ q?: string; page?: string }>;
|
||||
};
|
||||
|
||||
export default async function ProductsPage({ searchParams }: Props) {
|
||||
const { q, page: pageParam } = await searchParams;
|
||||
const page = Math.max(1, Number(pageParam) || 1);
|
||||
const pageSize = 16;
|
||||
const data = await listMappedProducts({ page, pageSize, name: q });
|
||||
|
||||
const makeHref = (nextPage: number) => {
|
||||
const params = new URLSearchParams();
|
||||
if (q) params.set("q", q);
|
||||
if (nextPage > 1) params.set("page", String(nextPage));
|
||||
const query = params.toString();
|
||||
return query ? `/products?${query}` : "/products";
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHero title={q ? `جستجو: ${q}` : "محصولات"} english="Products" />
|
||||
<section className="container-site py-10 md:py-14">
|
||||
<ProductGrid products={data.items} />
|
||||
<Pagination page={data.page} pageSize={data.pageSize} total={data.total} makeHref={makeHref} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user