Add Next.js RTL clone of sohansalehnia.com.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-03 09:10:25 +03:30
co-authored by Cursor
commit 4ca234b20f
53 changed files with 8387 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+9
View File
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+51
View File
@@ -0,0 +1,51 @@
# Sohan Salehnia Website Context
## Project
- Persian RTL storefront clone of `sohansalehnia.com` (سوهان صالح نیا, traditional Qom sohan sweets).
- Next.js 16 App Router, React 19, TypeScript, Tailwind 4, `src/app`.
- Language: `fa`, direction: `rtl`.
- Local images live in `public/images`. Fonts (Iran Yekan, Montserrat ExtraLight, Fontello/WMi icons) live in `public/fonts`.
## Scope (this clone)
Implemented pages:
- `/` homepage
- `/aboutus` about
- `/contactus` contact (form is client-side only; no backend submit yet)
- `/products` and `/articles` are placeholders. Products, workshops, and blog will be read from Meshkee later.
## Brand
| Token | Value | Use |
|---|---|---|
| Primary | `#706C01` | links, buttons, product titles, branch details |
| Gold | `#868215` | section accents, counters, English labels |
| Page | `#F9F9F9` | page background, header, footer |
| Heading | `#4B4B4B` | titles |
| Muted | `#5E5E5E` | labels, footer copy |
| Icon gray | `#939393` | social icons |
Logo: `public/images/logo.png` (from the live Meshkee theme). Hero, banners, product icons, pattern, and sohan photos are downloaded from the original theme CDN.
## Content source
Copy, addresses, phones, map embed, and section structure are taken from the live Meshkee site. Do not invent replacement marketing copy unless asked.
Branches:
- شعبه یکم: ميدان جهاد،باجك اول،نبش كوي ٤٤ — `02537728181`
- شعبه دوم: قم،بلوار الغدیر،روبروی دانشگاه قم — `02537728181`
Login still points to `https://app.sohansalehnia.com/auth/login`.
## Footer credit
Bottom row follows the SafeTeb Meshkee credit pattern (`Designed And Developed By / Meshkee E-Commerce Team` with animated frame + white Meshkee logo), recolored to the olive/gold theme. Component: `src/components/MeshkeeCredit.tsx`.
## Next work
- Wire `/products` and `/articles` (and any workshops) to Meshkee public API.
- Replace contact form local success state with the real endpoint.
- Keep `CONTEXT.md` updated when routes, brand tokens, or data sources change.
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
]);
export default eslintConfig;
+9
View File
@@ -0,0 +1,9 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
formats: ["image/avif", "image/webp"],
},
};
export default nextConfig;
+6782
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "sohan-salehnia",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --port 3001",
"build": "next build",
"start": "next start --port 3001",
"lint": "eslint"
},
"dependencies": {
"next": "16.3.0",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.3.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

+26
View File
@@ -0,0 +1,26 @@
import type { Metadata } from "next";
import AboutPreview from "@/components/AboutPreview";
import PageHeader from "@/components/PageHeader";
import PromoBanner from "@/components/PromoBanner";
import SohanFeature from "@/components/SohanFeature";
import Stats from "@/components/Stats";
import WhyUs from "@/components/WhyUs";
import { ABOUT_PAGE, SITE } from "@/lib/site";
export const metadata: Metadata = {
title: "درباره ما",
description: SITE.description,
};
export default function AboutPage() {
return (
<>
<PageHeader {...ABOUT_PAGE} />
<SohanFeature />
<Stats />
<AboutPreview />
<WhyUs />
<PromoBanner />
</>
);
}
+22
View File
@@ -0,0 +1,22 @@
import type { Metadata } from "next";
import Container from "@/components/Container";
import PageHeader from "@/components/PageHeader";
export const metadata: Metadata = {
title: "مقالات",
};
export default function ArticlesPage() {
return (
<>
<PageHeader kicker="مقالات" title="سوهان صالح نیا" english="Articles" />
<section className="bg-page py-20">
<Container className="text-center">
<p className="text-lg text-muted">
مقالات به‌زودی از طریق بک‌اند مشکی بارگذاری می‌شود.
</p>
</Container>
</section>
</>
);
}
+63
View File
@@ -0,0 +1,63 @@
import type { Metadata } from "next";
import ContactForm from "@/components/ContactForm";
import Container from "@/components/Container";
import PageHeader from "@/components/PageHeader";
import { CONTACT_PAGE, CONTACT_SIDEBAR, MAP_EMBED, SITE } from "@/lib/site";
export const metadata: Metadata = {
title: "تماس با ما",
description: SITE.description,
};
export default function ContactPage() {
return (
<>
<PageHeader {...CONTACT_PAGE} />
<section className="bg-white py-16">
<Container>
<div className="flex flex-col items-start gap-10 lg:flex-row lg:items-center">
<div className="w-full lg:w-2/3">
<div className="mb-8">
<h3 className="text-2xl text-heading">{CONTACT_PAGE.kicker}</h3>
<p className="text-2xl text-gold">{CONTACT_PAGE.title}</p>
<p className="mt-2 text-muted">{CONTACT_PAGE.intro}</p>
</div>
<ContactForm />
</div>
<div className="w-full lg:w-1/3">
{CONTACT_SIDEBAR.map((item, index) => (
<div key={`${item.title}-${index}`} className="mb-5 flex items-center gap-2">
<i
className={`${item.icon} text-[44px]`}
style={{ color: "rgb(185, 185, 185)" }}
/>
<div>
<h5>{item.title}</h5>
<a href={item.href} dir="ltr" className="text-primary">
{item.value}
</a>
</div>
</div>
))}
</div>
</div>
</Container>
</section>
<section className="bg-page py-16">
<Container>
<iframe
className="min-h-[500px] w-full rounded-[20px]"
src={MAP_EMBED}
width="100%"
height="500"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="موقعیت سوهان صالح نیا"
/>
</Container>
</section>
</>
);
}
+497
View File
@@ -0,0 +1,497 @@
@import "tailwindcss";
@font-face {
font-family: "iran-yekan";
src: url("/fonts/iran-yekan-light.ttf") format("truetype");
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "iran-yekan";
src: url("/fonts/iran-yekan-regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "iran-yekan";
src: url("/fonts/iran-yekan-medium.ttf") format("truetype");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "iran-yekan";
src: url("/fonts/iran-yekan-bold.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "montserrat-extralight";
src: url("/fonts/montserrat-extralight.ttf") format("truetype");
font-weight: 200;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "fontello";
src: url("/fonts/fontello.woff2") format("woff2"),
url("/fonts/fontello.woff") format("woff"),
url("/fonts/fontello.ttf") format("truetype");
font-weight: normal;
font-style: normal;
font-display: swap;
}
:root {
--wb-primary-color: #706c01;
--wb-gold: #868215;
--wb-muted: #5e5e5e;
--wb-heading: #4b4b4b;
--wb-page: #f9f9f9;
--wb-icon: #939393;
--background: #f9f9f9;
--foreground: #000000;
}
@theme inline {
--color-primary: var(--wb-primary-color);
--color-gold: var(--wb-gold);
--color-muted: var(--wb-muted);
--color-heading: var(--wb-heading);
--color-page: var(--wb-page);
--font-sans: "iran-yekan", Tahoma, sans-serif;
}
* {
box-sizing: border-box;
}
html,
body {
min-height: 100%;
}
body {
background: var(--wb-page);
color: var(--foreground);
font-family: "iran-yekan", Tahoma, sans-serif;
font-size: 0.9rem;
line-height: 1.8;
}
a {
color: inherit;
text-decoration: none;
}
button,
input,
textarea {
font-family: inherit;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
[class^="WMi-"]:before,
[class*=" WMi-"]:before {
font-family: "fontello";
font-style: normal;
font-weight: normal;
speak: never;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin: 0 !important;
text-align: center;
font-variant: normal;
text-transform: none;
line-height: 1em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.WMi-login:before {
content: "\e877";
}
.WMi-instagram:before {
content: "\f16d";
}
.WMi-telegram-1:before {
content: "\f1d9";
}
.WMi-whatsapp:before {
content: "\f613";
}
.WMi-cancel-linear:before {
content: "\f61d";
}
.WMi-left-chevron:before {
content: "\f626";
}
.WMi-menu-5:before {
content: "\f693";
}
.button-light {
background-color: color-mix(in srgb, var(--wb-primary-color), white 90%);
padding: 10px 17px;
color: var(--wb-primary-color);
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
border: 0;
transition: 0.4s;
}
.button-light:hover {
background-color: var(--wb-primary-color);
color: #fff;
}
.button-style-2 {
color: var(--wb-primary-color);
position: relative;
background-color: transparent;
border: 0;
display: inline-flex;
align-items: center;
gap: 6px;
}
.button-style-2:after {
content: "";
position: absolute;
bottom: -7px;
right: 0;
width: 50px;
height: 1px;
background-color: var(--wb-primary-color);
transition: 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.button-style-2:hover:after {
width: 100%;
}
.soft-shadow {
box-shadow: 0 25px 35px -4px rgba(0, 0, 0, 0.05);
}
.header-nav {
padding-bottom: 10px;
z-index: 1000;
background: #f9f9f9;
}
@media (min-width: 320px) {
.header-nav {
padding-top: 20px;
}
}
@media (min-width: 1200px) {
.header-nav {
padding-top: 40px;
}
}
.header-nav.fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #fff;
border-radius: 20px;
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.04);
animation: slideDown 0.3s ease;
}
@media (min-width: 320px) {
.header-nav.fixed {
margin: 10px;
width: calc(100% - 20px);
}
}
@media (min-width: 1200px) {
.header-nav.fixed {
padding: 15px 30px;
margin: 30px 70px;
width: calc(100% - 140px);
}
}
@keyframes slideDown {
from {
transform: translateY(-20px);
opacity: 0.6;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@media (max-width: 1024px) {
.nav-items {
height: 100%;
background-color: #fff;
position: fixed;
top: 0;
right: 0;
width: 70%;
padding: 60px 25px;
box-shadow: -7px 0 20px rgba(0, 0, 0, 0.15);
z-index: 20;
transition: 0.4s ease-in-out;
transform: translateX(100%);
}
.nav-items.open-menu {
transform: translateX(0);
}
.nav-items li:not(:last-child) {
border-bottom: 1px solid #ededed;
padding-bottom: 5px;
margin-bottom: 5px;
}
}
.overlay {
background-color: rgba(0, 0, 0, 0.5);
display: none;
height: 100vh;
left: 0;
position: fixed;
top: 0;
width: 100%;
z-index: 10;
backdrop-filter: blur(5px);
}
.overlay.show {
display: block;
}
.hero-banner img {
object-fit: cover;
width: 100%;
}
@media (min-width: 320px) {
.hero-banner img {
height: 300px;
}
}
@media (min-width: 1200px) {
.hero-banner img {
height: auto;
max-height: none;
}
}
.english-label {
font-family: "montserrat-extralight", Montserrat, sans-serif;
}
.form-1 input,
.form-1 textarea {
border: 1px solid #e6e6e6 !important;
padding: 12px 20px;
width: 100%;
background: #fff;
border-radius: 0;
}
.form-1 input:focus,
.form-1 textarea:focus {
outline: none;
box-shadow: none;
}
.form-1 label {
position: absolute;
top: -12px;
right: 30px;
background: #fff;
padding-inline: 10px;
color: #ccc;
}
.footer-bottom {
position: relative;
z-index: 1;
padding: 20px 0 19px;
background-color: #2a290f;
}
.footer-bottom-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 4px 0;
}
.footer-bottom-site {
color: #c4bf8a;
font-size: 12px;
}
.meshkee-credit {
position: relative;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 6px 10px;
color: #b8b48a;
text-decoration: none;
transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.meshkee-credit:hover {
color: #efe9b8;
text-decoration: none;
}
.meshkee-credit__frame {
position: absolute;
inset: 0;
pointer-events: none;
}
.meshkee-credit__line {
position: absolute;
background: #868215;
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
.meshkee-credit__line--l {
top: 0;
left: 0;
width: 1px;
height: 100%;
transform: scaleY(0);
transform-origin: top;
}
.meshkee-credit__line--b {
bottom: 0;
left: 0;
width: 100%;
height: 1px;
transform: scaleX(0);
transform-origin: left;
transition-delay: 0.14s;
}
.meshkee-credit__line--r {
top: 0;
right: 0;
width: 1px;
height: 100%;
transform: scaleY(0);
transform-origin: bottom;
transition-delay: 0.28s;
}
.meshkee-credit__line--t {
top: 0;
left: 0;
width: 100%;
height: 1px;
transform: scaleX(0);
transform-origin: right;
transition-delay: 0.42s;
}
.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 {
position: relative;
flex-shrink: 0;
width: auto;
height: 42px;
aspect-ratio: 1 / 1;
object-fit: contain;
opacity: 0.7;
transition: opacity 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.meshkee-credit:hover .meshkee-credit__logo {
opacity: 1;
}
.meshkee-credit__text {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 42px;
line-height: 1.25;
}
.meshkee-credit__label {
font-size: 11px;
font-family: "montserrat-extralight", Montserrat, sans-serif;
}
.meshkee-credit__name {
font-size: 13px;
font-weight: 600;
color: #ddd6a4;
transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.meshkee-credit:hover .meshkee-credit__name {
color: #fff8c8;
}
@media only screen and (max-width: 575px) {
.footer-bottom-bar {
flex-direction: column;
justify-content: center;
text-align: center;
}
}
+33
View File
@@ -0,0 +1,33 @@
import type { Metadata } from "next";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { SITE } from "@/lib/site";
import "./globals.css";
export const metadata: Metadata = {
metadataBase: new URL(SITE.url),
title: {
default: SITE.name,
template: `%s | ${SITE.name}`,
},
description: SITE.description,
icons: {
icon: "/images/logo.png",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="fa" dir="rtl">
<body className="flex min-h-screen flex-col antialiased">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</body>
</html>
);
}
+59
View File
@@ -0,0 +1,59 @@
import Image from "next/image";
import Link from "next/link";
import AboutPreview from "@/components/AboutPreview";
import Container from "@/components/Container";
import PromoBanner from "@/components/PromoBanner";
import SohanFeature from "@/components/SohanFeature";
import { PRODUCT_TYPES, SITE } from "@/lib/site";
export default function HomePage() {
return (
<>
<section className="hero-banner relative">
<Link href="/">
<Image
src="/images/hero.jpg"
alt={SITE.name}
width={1603}
height={811}
priority
className="w-full"
/>
</Link>
</section>
<section className="bg-page py-16">
<Container>
<div className="soft-shadow flex flex-col flex-wrap items-center rounded-[30px] px-4 py-8 lg:px-8">
<div className="mb-6 flex items-center gap-1">
<span className="text-[20px] text-muted">انواع محصولات</span>
<span className="text-[24px] text-primary">{SITE.name}</span>
</div>
<div className="flex w-full flex-col items-center justify-between gap-8 lg:flex-row">
{PRODUCT_TYPES.map((item, index) => (
<Link
key={`${item.title}-${index}`}
href={item.href}
className="flex items-center gap-2"
>
<Image
src="/images/product-habe.png"
alt={item.title}
width={149}
height={161}
className="h-auto w-[90px]"
/>
<p className="text-[23px] text-primary">{item.title}</p>
</Link>
))}
</div>
</div>
</Container>
</section>
<AboutPreview />
<SohanFeature />
<PromoBanner />
</>
);
}
+22
View File
@@ -0,0 +1,22 @@
import type { Metadata } from "next";
import Container from "@/components/Container";
import PageHeader from "@/components/PageHeader";
export const metadata: Metadata = {
title: "محصولات",
};
export default function ProductsPage() {
return (
<>
<PageHeader kicker="محصولات" title="سوهان صالح نیا" english="Products" />
<section className="bg-page py-20">
<Container className="text-center">
<p className="text-lg text-muted">
فهرست محصولات به‌زودی از طریق بک‌اند مشکی بارگذاری می‌شود.
</p>
</Container>
</section>
</>
);
}
+39
View File
@@ -0,0 +1,39 @@
import Image from "next/image";
import Link from "next/link";
import Container from "./Container";
import { ABOUT } from "@/lib/site";
export default function AboutPreview() {
return (
<section className="bg-page py-16">
<Container>
<div className="flex flex-col items-center gap-10 lg:flex-row">
<div className="w-full lg:w-1/3">
<Image
src="/images/about.png"
alt="درباره سوهان صالح نیا"
width={584}
height={372}
className="h-auto w-full"
/>
</div>
<div className="w-full lg:w-2/3">
<Image
src="/images/logo.png"
alt=""
width={125}
height={40}
/>
<p className="my-6 leading-8 text-black">{ABOUT.body}</p>
<div className="text-start">
<Link href="/contactus" className="button-style-2 pb-1">
<span>{ABOUT.cta}</span>
<i className="WMi-left-chevron" />
</Link>
</div>
</div>
</div>
</Container>
</section>
);
}
+49
View File
@@ -0,0 +1,49 @@
"use client";
import { FormEvent, useState } from "react";
export default function ContactForm() {
const [sent, setSent] = useState(false);
function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setSent(true);
event.currentTarget.reset();
}
return (
<form onSubmit={onSubmit} className="form-1 mb-3">
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
<div className="relative pt-2">
<label htmlFor="name">نام</label>
<input type="text" name="name" id="name" required />
</div>
<div className="relative pt-2">
<label htmlFor="email">ایمیل</label>
<input type="email" name="email" id="email" required />
</div>
<div className="relative pt-2">
<label htmlFor="cell_number">شماره همراه</label>
<input type="tel" name="cell_number" id="cell_number" required />
</div>
<div className="relative pt-2">
<label htmlFor="subject">موضوع</label>
<input type="text" name="title" id="subject" required />
</div>
<div className="relative pt-2 lg:col-span-2">
<label htmlFor="message">متن پیام</label>
<textarea name="message" id="message" rows={4} required />
</div>
<div className="text-start lg:col-span-2">
<button type="submit" className="button-style-2 p-0 pb-1">
ارسال پیام
<i className="WMi-left-chevron" />
</button>
{sent && (
<p className="mt-4 text-sm text-primary">پیام شما با موفقیت ثبت شد.</p>
)}
</div>
</div>
</form>
);
}
+13
View File
@@ -0,0 +1,13 @@
export default function Container({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div className={`mx-auto w-full max-w-[1140px] px-4 ${className}`.trim()}>
{children}
</div>
);
}
+64
View File
@@ -0,0 +1,64 @@
import Image from "next/image";
import Container from "./Container";
import MeshkeeCredit from "./MeshkeeCredit";
import { FOOTER, SITE } from "@/lib/site";
export default function Footer() {
return (
<footer className="bg-page">
<Container className="py-16">
<div className="mx-auto max-w-[760px]">
<div className="mb-12 text-center">
<Image
src="/images/logo.png"
alt={SITE.name}
width={125}
height={40}
className="mx-auto"
/>
<p className="mt-3 leading-8 text-muted">{FOOTER.about}</p>
</div>
<div className="grid gap-8 sm:grid-cols-2">
{FOOTER.branches.map((branch) => (
<div key={branch.title}>
<h5 className="font-black text-muted">{branch.title}</h5>
<div className="mt-3 flex flex-wrap gap-6">
<div>
<p className="mb-2 text-muted">آدرس ما</p>
<p className="mb-0 text-primary">{branch.address}</p>
</div>
<div>
<p className="mb-2 text-muted">شماره تماس</p>
<a href={`tel:${branch.tel}`} className="text-primary" dir="ltr">
{branch.phone}
</a>
</div>
</div>
</div>
))}
</div>
<ul className="mt-12 flex items-center justify-center gap-3">
<li>
<a href="/" aria-label="اینستاگرام" className="text-[#939393]">
<i className="WMi-instagram text-[32px]" />
</a>
</li>
<li>
<a href="/" aria-label="تلگرام" className="text-[#939393]">
<i className="WMi-telegram-1 text-[28px]" />
</a>
</li>
</ul>
</div>
</Container>
<div className="footer-bottom">
<Container>
<div className="footer-bottom-bar">
<div className="footer-bottom-site">{SITE.name}</div>
<MeshkeeCredit />
</div>
</Container>
</div>
</footer>
);
}
+137
View File
@@ -0,0 +1,137 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { NAV_ITEMS, SITE } from "@/lib/site";
export default function Header() {
const pathname = usePathname();
const [open, setOpen] = useState(false);
const [fixed, setFixed] = useState(false);
useEffect(() => {
const onScroll = () => setFixed(window.scrollY > 100);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => {
setOpen(false);
}, [pathname]);
useEffect(() => {
document.body.style.overflow = open ? "hidden" : "";
return () => {
document.body.style.overflow = "";
};
}, [open]);
return (
<header>
{fixed ? <div className="h-[72px] lg:h-[118px]" aria-hidden /> : null}
<nav className={`header-nav ${fixed ? "fixed" : ""}`}>
<div className="mx-auto w-full max-w-[1140px] px-4">
<div className="flex w-full items-center">
<div className="flex w-full items-center justify-between lg:hidden">
<button
type="button"
className="hamburger border-0 bg-transparent p-0"
aria-label="باز کردن منو"
onClick={() => setOpen(true)}
>
<i className="WMi-menu-5 text-[1.8rem] text-heading" />
</button>
<Link href="/">
<Image
src="/images/logo.png"
alt={SITE.name}
width={125}
height={40}
priority
/>
</Link>
</div>
<div className="hidden w-full items-center lg:flex">
<div className="w-[41.66%]">
<ul className="flex items-center gap-8">
{NAV_ITEMS.map((item) => (
<li key={item.href}>
<Link
href={item.href}
className={`py-2 text-[15px] transition-colors ${
pathname === item.href
? "text-primary"
: "text-black hover:text-primary"
}`}
>
{item.label}
</Link>
</li>
))}
</ul>
</div>
<div className="w-[16.66%] text-center">
<Link href="/" className="inline-block">
<Image
src="/images/logo.png"
alt={SITE.name}
width={125}
height={40}
priority
/>
</Link>
</div>
<div className="flex w-[41.66%] justify-end">
<a
href={SITE.loginUrl}
className="button-light px-3 py-1 text-[1.2rem] leading-none"
>
ورود
<i className="WMi-login" />
</a>
</div>
</div>
</div>
{!fixed && (
<hr className="mt-4 hidden w-full border-neutral-200 lg:block" />
)}
</div>
<ul className={`nav-items flex flex-col lg:hidden ${open ? "open-menu" : ""}`}>
<button
type="button"
className="absolute top-[3%] left-[5%] border-0 bg-transparent"
aria-label="بستن منو"
onClick={() => setOpen(false)}
>
<i className="WMi-cancel-linear text-xl" />
</button>
{NAV_ITEMS.map((item) => (
<li key={item.href}>
<Link href={item.href} className="flex py-2">
{item.label}
</Link>
</li>
))}
<li>
<a
href={SITE.loginUrl}
className="button-light mt-2 px-3 py-1 text-[1.2rem] leading-none"
>
ورود
<i className="WMi-login" />
</a>
</li>
</ul>
</nav>
<div
className={`overlay ${open ? "show" : ""}`}
onClick={() => setOpen(false)}
/>
</header>
);
}
+31
View File
@@ -0,0 +1,31 @@
import Image from "next/image";
export default function MeshkeeCredit() {
return (
<a
href="https://meshkee.com"
target="_blank"
rel="noopener noreferrer"
dir="ltr"
className="meshkee-credit"
>
<span className="meshkee-credit__frame" aria-hidden="true">
<span className="meshkee-credit__line meshkee-credit__line--l" />
<span className="meshkee-credit__line meshkee-credit__line--t" />
<span className="meshkee-credit__line meshkee-credit__line--r" />
<span className="meshkee-credit__line meshkee-credit__line--b" />
</span>
<Image
src="/images/MeshkeeLogo-White.png"
alt="Meshkee"
width={42}
height={42}
className="meshkee-credit__logo"
/>
<span className="meshkee-credit__text">
<span className="meshkee-credit__label">Designed And Developed By</span>
<span className="meshkee-credit__name">Meshkee E-Commerce Team</span>
</span>
</a>
);
}
+40
View File
@@ -0,0 +1,40 @@
import Image from "next/image";
export default function PageHeader({
kicker,
title,
english,
}: {
kicker: string;
title: string;
english: string;
}) {
return (
<section className="bg-page overflow-hidden py-12">
<div className="flex justify-end">
<div className="relative w-full lg:w-10/12">
<div
className="absolute inset-y-0 left-0 z-0 flex items-center"
dir="ltr"
>
<Image
src="/images/pattern.png"
alt=""
width={1064}
height={89}
className="h-[89px] w-[200px] shrink-0 object-cover object-left sm:w-[260px] lg:w-[320px]"
/>
<span className="english-label hidden shrink-0 ps-3 text-[52px] font-thin leading-none text-gold opacity-25 sm:inline">
{english}
</span>
</div>
<div className="relative z-10 w-fit bg-page ps-3">
<h3 className="text-[25px] font-normal text-heading">{kicker}</h3>
<p className="text-[30px] leading-tight text-gold">{title}</p>
</div>
</div>
</div>
<h1 className="sr-only">{kicker}</h1>
</section>
);
}
+30
View File
@@ -0,0 +1,30 @@
import Image from "next/image";
import Link from "next/link";
import Container from "./Container";
export default function PromoBanner() {
return (
<section className="bg-page">
<Container>
<Link href="/products" className="hidden p-12 lg:block">
<Image
src="/images/promo-banner.png"
alt="سوهان صالح نیا"
width={904}
height={234}
className="my-3 w-full rounded-[20px]"
/>
</Link>
<Link href="/products" className="lg:hidden">
<Image
src="/images/promo-banner.png"
alt="سوهان صالح نیا"
width={904}
height={234}
className="my-3 w-full rounded-[10px]"
/>
</Link>
</Container>
</section>
);
}
+34
View File
@@ -0,0 +1,34 @@
import Image from "next/image";
import Container from "./Container";
import { SOHAN_FEATURE } from "@/lib/site";
export default function SohanFeature() {
return (
<section
className="bg-cover bg-page py-16"
style={{ backgroundImage: "url(/images/sohan-bg.png)" }}
>
<Container>
<div className="flex justify-end">
<div className="w-full lg:w-9/12">
<div className="mb-10 flex items-end gap-3">
<Image
src="/images/sohan.png"
alt="سوهان"
width={338}
height={335}
className="w-[180px] rounded-[20px] sm:w-[250px] lg:w-[350px]"
style={{ filter: "drop-shadow(17px 31px 20px rgb(0, 0, 0, .5))" }}
/>
<div>
<h3 className="text-[27px] text-heading">{SOHAN_FEATURE.title}</h3>
<p className="text-[30px] text-gold">{SOHAN_FEATURE.subtitle}</p>
</div>
</div>
<p className="leading-8">{SOHAN_FEATURE.body}</p>
</div>
</div>
</Container>
</section>
);
}
+85
View File
@@ -0,0 +1,85 @@
"use client";
import { useEffect, useRef, useState } from "react";
import Container from "./Container";
import { STATS } from "@/lib/site";
function useCountUp(target: number, active: boolean) {
const [value, setValue] = useState(0);
useEffect(() => {
if (!active) return;
const duration = 1400;
const start = performance.now();
let frame = 0;
const tick = (now: number) => {
const progress = Math.min((now - start) / duration, 1);
setValue(Math.round(target * progress));
if (progress < 1) frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frame);
}, [active, target]);
return value;
}
function StatItem({
value,
label,
active,
}: {
value: number;
label: string;
active: boolean;
}) {
const count = useCountUp(value, active);
return (
<div className="text-center">
<span className="mb-1 block text-[36px] leading-none text-gold lg:text-[49px]">
{count}
</span>
<p className="text-[16px] text-heading lg:text-[20px]">{label}</p>
</div>
);
}
export default function Stats() {
const ref = useRef<HTMLDivElement>(null);
const [active, setActive] = useState(false);
useEffect(() => {
const node = ref.current;
if (!node) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setActive(true);
observer.disconnect();
}
},
{ threshold: 0.4 },
);
observer.observe(node);
return () => observer.disconnect();
}, []);
return (
<section className="bg-page py-16" ref={ref}>
<Container>
<div className="flex flex-wrap items-center justify-around gap-8">
{STATS.map((stat) => (
<StatItem
key={stat.label}
value={stat.value}
label={stat.label}
active={active}
/>
))}
</div>
</Container>
</section>
);
}
+62
View File
@@ -0,0 +1,62 @@
import Image from "next/image";
import Container from "./Container";
import { WHY_US } from "@/lib/site";
function WhyItem({
title,
subtitle,
bordered,
}: {
title: string;
subtitle: string;
bordered?: boolean;
}) {
return (
<div
className={`flex items-center gap-2 ${
bordered ? "mb-4 border-b border-neutral-200 pb-4" : ""
}`}
>
<Image src="/images/icon-1.png" alt="" width={52} height={57} />
<div>
<p className="text-[22px] text-heading">{title}</p>
<p className="text-[22px] text-gold">{subtitle}</p>
</div>
</div>
);
}
export default function WhyUs() {
const [leftTop, leftBottom, rightTop, rightBottom] = WHY_US.items;
return (
<section className="bg-page py-16">
<Container>
<div className="mb-8 flex flex-col items-center p-1">
<h3 className="text-[25px] text-heading">{WHY_US.kicker}</h3>
<span className="inline-block text-[25px] text-gold">{WHY_US.title}</span>
</div>
<div className="flex flex-col items-center justify-around lg:flex-row">
<div className="w-full lg:w-1/4">
<WhyItem {...leftTop} bordered />
<WhyItem {...leftBottom} />
</div>
<div className="col-10 mx-auto my-8 w-10/12 lg:w-1/4">
<Image
src="/images/sohan.png"
alt="سوهان"
width={338}
height={335}
className="h-auto w-full"
style={{ filter: "drop-shadow(17px 31px 20px rgb(0, 0, 0, .5))" }}
/>
</div>
<div className="w-full lg:w-1/4">
<WhyItem {...rightTop} bordered />
<WhyItem {...rightBottom} />
</div>
</div>
</Container>
</section>
);
}
+112
View File
@@ -0,0 +1,112 @@
export const SITE = {
name: "سوهان صالح نیا",
nameEn: "Sohan Salehnia",
url: "https://www.sohansalehnia.com",
loginUrl:
"https://app.sohansalehnia.com/auth/login?redirect=http://www.sohansalehnia.com",
description:
"سوهان صالح نیا، تولید و عرضه انواع سوهان سنتی قم با طعم تازه و اصیل.",
primary: "#706C01",
gold: "#868215",
};
export const NAV_ITEMS = [
{ href: "/", label: "صفحه اصلی" },
{ href: "/products", label: "محصولات" },
{ href: "/articles", label: "مقالات" },
{ href: "/contactus", label: "تماس با ما" },
{ href: "/aboutus", label: "درباره ما" },
] as const;
export const PRODUCT_TYPES = [
{ title: "حبه سوهان", href: "/products" },
{ title: "حبه سوهان", href: "/products" },
{ title: "حبه سوهان", href: "/products" },
] as const;
export const ABOUT = {
heading: "درباره ما",
subheading: "از ما بیشتر بدانید ...",
body: "لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان کـــــــه لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه است.",
cta: "همین حالا با ما تماس بگیرید",
};
export const SOHAN_FEATURE = {
title: "سوهان",
subtitle: "لذیذ و تازه بلوط",
body: "لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبــود ابزارهای کاربردی می باشد.",
};
export const ABOUT_PAGE = {
kicker: "درباره سوهان",
title: "صالح نیا بیشتــــر بدانیــــــد...",
english: "About us",
};
export const CONTACT_PAGE = {
kicker: "با ما",
title: "تماس بگیرید",
english: "Contact us",
intro: "همیشه در دسترس هستیم، با ما در ارتباط باشید",
};
export const STATS = [
{ value: 230, label: "ارسال های هفتگی" },
{ value: 5, label: "پشتیبانی" },
{ value: 1000, label: "کاربران خوشحال" },
{ value: 430, label: "تنوع محصولات" },
] as const;
export const WHY_US = {
kicker: "چرا باید",
title: "ما را انتخاب کنید",
items: [
{ title: "ارسال", subtitle: "رایگان در شهر قم" },
{ title: "ارسال", subtitle: "رایگان در شهر قم" },
{ title: "ارسال", subtitle: "رایگان در شهر قم" },
{ title: "ارسال", subtitle: "رایگان در شهر قم" },
],
} as const;
export const FOOTER = {
about:
"لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است.ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک",
branches: [
{
title: "اطلاعات تماس شعبه یکم",
address: "ميدان جهاد،باجك اول،نبش كوي ٤٤",
phone: "37728181 - (025)",
tel: "02537728181",
},
{
title: "اطلاعات تماس شعبه دوم",
address: "قم،بلوار الغدیر،روبروی دانشگاه قم",
phone: "37728181 - (025)",
tel: "02537728181",
},
],
};
export const CONTACT_SIDEBAR = [
{
icon: "WMi-instagram",
title: "شماره تماس (شعبه یک)",
value: "(025) 37728181",
href: "tel:02537728181",
},
{
icon: "WMi-whatsapp",
title: "شماره تماس (شعبه یک)",
value: "(025) 37728181",
href: "tel:02537728181",
},
{
icon: "WMi-whatsapp",
title: "شماره تماس (شعبه یک)",
value: "(025) 37728181",
href: "tel:02537728181",
},
] as const;
export const MAP_EMBED =
"https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3282.0930113137806!2d50.8950383!3d34.65235390000001!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3f93bb9b1d8a57f5%3A0x27365b97ef84d3db!2z2LTbjNix24zZhtuMINio2YTZiNi3!5e0!3m2!1sen!2s!4v1767243250550!5m2!1sen!2s";
+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}