diff --git a/app/aboutus/page.tsx b/app/aboutus/page.tsx new file mode 100644 index 0000000..91fdf9f --- /dev/null +++ b/app/aboutus/page.tsx @@ -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 ( + <> + +
+
+ +
+
+

{SITE.nameFa}

+

{SITE.about}

+
+
+ +
+
+

SALAR STORE

+

سالار استور

+

{SITE.about}

+
+
+ {SITE.nameFa} +
+
+
+ + ); +} diff --git a/app/api/comments/route.ts b/app/api/comments/route.ts new file mode 100644 index 0000000..a7ad676 --- /dev/null +++ b/app/api/comments/route.ts @@ -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 }); +} diff --git a/app/api/search/route.ts b/app/api/search/route.ts new file mode 100644 index 0000000..96a28b0 --- /dev/null +++ b/app/api/search/route.ts @@ -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 }); +} diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..62b0b73 --- /dev/null +++ b/app/blog/[slug]/page.tsx @@ -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 { + 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 ( + <> + +
+ +
+ {blog.publishedAt || blog.createdAt ? : null} + {blog.categoryName ? {blog.categoryName} : null} + {author ? {author} : null} +
+ {blog.abstract ?

{blog.abstract}

: null} +
+ +
+
+ + ); +} diff --git a/app/blog/page.tsx b/app/blog/page.tsx new file mode 100644 index 0000000..319b53f --- /dev/null +++ b/app/blog/page.tsx @@ -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> = { 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 ( + <> + +
+ {posts.length ? ( +
+ {posts.map((post) => ( + + +
+ +

{post.title}

+ {post.abstract ? ( +

{post.abstract}

+ ) : null} +
+ + ))} +
+ ) : ( +

مقاله‌ای یافت نشد.

+ )} + (nextPage > 1 ? `/blog?page=${nextPage}` : "/blog")} + /> +
+ + ); +} diff --git a/app/contactus/page.tsx b/app/contactus/page.tsx new file mode 100644 index 0000000..779de81 --- /dev/null +++ b/app/contactus/page.tsx @@ -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 ( + <> + +
+
+
+ {contacts.map((item) => { + const Icon = item.icon; + const content = ( + <> + + + + + {item.label} + + + ); + + if (!item.href) { + return ( +
+ {content} +
+ ); + } + + return ( + + {content} + + ); + })} +
+ +
+
+ + ); +} diff --git a/app/globals.css b/app/globals.css index a2dc41e..910a65a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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%; } diff --git a/app/layout.tsx b/app/layout.tsx index 9852c15..b3d04bc 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( - - {children} + + + +
+
{children}
+