first commit

This commit is contained in:
amirhosein.ashourloo
2026-09-15 20:37:49 +03:30
commit dee487ad73
91 changed files with 7365 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
node_modules/
.next/
out/
.env*
!.env.example
*.log
.npm-cache/
asset-preview.jpg
+1
View File
@@ -0,0 +1 @@
24
+13
View File
@@ -0,0 +1,13 @@
<!-- 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 -->
## Project-specific requirements
Read `CONTEXT.md` before making changes. It contains the authoritative user requirements, source findings, static route map, asset paths, API boundaries, and current verification status. Update it whenever a prompt changes meaningful requirements or implementation state. Preserve the real Next.js static-export architecture, Persian RTL, Swiper, user-supplied images, and local IRANYekan typography (Bold main headings; Light elsewhere). Never fabricate real portfolio records or successful form submissions.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+131
View File
@@ -0,0 +1,131 @@
# Sanaz Goodarzi — project context
Last updated: 2026-09-15. Read this file before making changes. Update it on each prompt when requirements, decisions, implementation, or verification status change. Never replace the original migration objective with a follow-up styling request.
## User requirements (authoritative)
- Rebuild the site running at `http://sanaz-goodarzi.local:8000/` in **real Next.js**, Persian (`lang="fa"`), RTL (`dir="rtl"`).
- Treat original site content as valid; correct obvious visual, navigation, spelling, and UX defects.
- Implement all public pages as static pages now. The user will supply API documentation later.
- Use **Swiper** for sliders/carousels. Do not substitute a custom slider.
- Keep the original charcoal, gold, and white palette. Improve mobile hamburger spacing.
- Fully responsive layouts for mobile, tablet, desktop; support narrow screens, touch targets, keyboard focus, and reduced motion.
- User supplied images in root `img/`, and permitted copying/moving them. Render the copy in `public/img/` with the original filenames/subdirectories. Keep the supplied originals intact.
- Dynamic areas use clearly marked thumbnails/previews until real API content is provided. Never present invented projects, project dates, locations, metrics, or successful form submissions as real.
- Footer bottom Meshkee credit must follow `https://safeteb.com/`, including the logo, exact English text, link, and animated border.
- Latest typography instruction: use the user's files in **`public/fonts/`** (the user called it “font”). Main headings use **Bold 700**; everything else uses **Light 300**. Do not return to Vazirmatn, Shabnam, or a remote font CDN.
- Keep development and output in this local workspace. No publication was requested.
- This architecture website has no product catalog or store. Ignore all product, store-item, cart, checkout, order, payment, marketplace, and shop-login instructions in this and future backend prompts unless the user explicitly reverses this requirement.
## Stack and running
- Next.js App Router, React, plain CSS, lucide-react icons, Swiper.
- `next.config.mjs`: `output: 'export'`, `trailingSlash: true`, unoptimized image output.
- `npm run dev`: development at `http://localhost:3000` (original site remains on port 8000).
- `npm run build`: exports production files into `out/`.
- `npm start`: serves `out/` locally; optional `PORT` environment variable.
- `npm run lint`: ESLint / Next recommended rules.
- Use Node 24 LTS (`.nvmrc`), minimum 20.9. This machine's default Laragon Node is 18.8. `scripts/runtime.mjs` uses the already-installed Codex Node runtime as a Windows fallback when needed. No runtime is downloaded.
- `package-lock.json` is authoritative for installed versions. Do not run a new scaffold or replace this project with Vinext/Sites templates.
## Public routes
| Route | Implementation |
|---|---|
| `/` | Full-viewport hero plus five source-matched category panels; vertical Swiper on desktop, normal stacked scrolling on mobile; no footer |
| `/ourservices/` | Five text cards in a two-column grid overlapping the 690px source subheader; one column on mobile |
| `/aboutus/` | Source bird-image hero with three colored icons, biography with circular photo, image/progress section, accessible FAQ |
| `/contactus/` | Three source-matched contact columns and centered single-column static form; four offices and real contacts |
| `/portfolios/` | Category filtering with URL query state; clearly labeled category thumbnails |
| `/portfolios/residential/` | Static residential preview |
| `/portfolios/beauty/` | Static beauty salon preview |
| `/portfolios/commercial/` | Static office/commercial preview |
| `/portfolios/landscape/` | Static landscape preview |
| `/portfolios/facade/` | Static facade preview |
| `/portfolios/[id]/[titleFaSlug]/` | Canonical API portfolio details generated at build time from `by-id`, with hover zoom, lightbox gallery, and related work |
| `/blog/` | API blog listing; a thumbnail empty state appears while no posts are published |
| `/blog/[id]/[titleSlug]/` | Canonical API blog details generated at build time from `by-id`, with CMS SEO and JSON-LD when supplied |
| unknown route | Persian 404 page |
No blog link/page was found in the original public navigation. Do not invent a blog or articles simply because the original brief mentioned blogs as an example of dynamic content.
## Source-matched visual structure (2026-09-06)
- The user rejected the earlier free redesign. Preserve the original layout, imagery, section order and proportions; fix only clear UX/content defects and apply explicit follow-ups.
- Home: full-height image 1 hero; five half-image/half-text panels, images on the left, text on the right, alternating white/black backgrounds and large gold V/B/O/L/F letters at the seam. No extra introduction, about strip or contact CTA.
- Use hamburger navigation at every viewport size, with comfortable top/right spacing and the supplied gold logo at the left. Keep the animated dark fullscreen menu.
- About progress values 68, 25, 36 and 82 were verified in the original DOM; they are source data, not invented metrics.
- Portfolio source eventually returned a backend TLS connection error to its upstream portfolio service. Do not copy its debug output or fabricate unavailable project records.
## Source findings
- Read original home, about, and services in browser, and contact page through HTTP. Original navigation exposes the five main routes above.
- Original `/portfolios` failed to finish loading in the browser and a bounded HTTP request timed out. Portfolio records therefore were **not verified**. Current images represent categories only, not named projects.
- Original home incorrectly labeled the facade service as office/commercial. Corrected to طراحی نما.
- Original services page repeated office copy under landscape. Replaced with the matching landscape copy from original home.
- Original footer/navigation/category/social links frequently pointed to `/` or `#`; replaced with meaningful routes and verified contact destinations.
- Original admin/editor controls are not public site functionality and are intentionally excluded.
- Original logo URL returned a Git LFS pointer, not an image. It was removed. The user subsequently supplied the actual `public/Logo.png` (case-sensitive path), now rendered by the shared Brand component in the header, mobile menu, and footer with the header wordmark hidden to match the source. Preserve its gold colors, transparency, and aspect ratio.
- Original Shabnam CDN did not resolve. User then supplied IRANYekan fonts; these supersede all prior font decisions.
## Files and boundaries
- `lib/content.js`: verified source text, navigation, five services, biography, FAQs, four offices, three phones, two emails, Instagram.
- `lib/portfolio.js`: explicit `isPlaceholder: true` records and `getPortfolioItems()` adapter boundary.
- `components/home-experience.js`: active home implementation. Vertical Swiper at min-width 900px and min-height 650px; otherwise stacked sections. Mode changes remount Swiper so mousewheel initialization remains correct. Six navigation dots, keyboard support, reduced motion, and inactive desktop slides marked inert. `components/hero.js` is a legacy unused component.
- `components/shell.js`: shared header, native dialog mobile menu, footer. Dialog provides modal keyboard behavior; closes on Escape, backdrop, close button, or navigation. Scroll is locked while open.
- `components/contact-form.js`: validates locally; **never sends or stores data**. Status explicitly states no message was sent. The send API is pending.
- `components/portfolio-grid.js`: category filter and URL query sync, including browser back/forward.
- `app/globals.css`: base tokens, accessibility, shared components and animation rules. `app/reference.css`, imported afterward, owns source-matching layout and responsive overrides at 1200, 899 and 650px. Shared subheaders use the original image without any overlay and dark titles.
- `app/layout.js`: `next/font/local` with `iranyekanweblightfanum.woff` at 300 and `iranyekanwebboldfanum.woff` at 700.
- `public/meshkee-logo.png`: genuine logo retrieved from safeteb.com. Credit links to `https://meshkee.com` and reads `Designed And Developed By` / `Meshkee E-Commerce Team`.
- `public/img/slider/1.jpg`: charcoal/gold hero; 2 residential; 3 beauty; 4 office/commercial; 5 landscape; 6 facade. Other images retain original paths. `img/frames/` holds supplied frame references, not project portfolio records.
## API integration next steps
Backend contract received 2026-09-15: base `https://api.meshkee.com/api/v1`, tenant `sanazgoodarzi.ir`, official documentation `https://api.meshkee.com/docs/website`.
Tenant SEO prompt `6d011d374a2bb32e88d778c8` supersedes the earlier generic slug-only detail URL guidance. Public CMS detail URLs use numeric/string ID plus a Persian title slug: `/blog/{id}/{titleSlug}` and `/portfolios/{id}/{titleFaSlug}`. The page resolves content through the matching `by-id` endpoint; the title segment is for readable canonical URLs.
1. Read the user's API documentation; do not invent URLs, tokens, schemas, or authentication.
2. Replace the portfolio adapter and remove preview badges only for actual records. Preserve category IDs or map API values explicitly.
3. Current detail slugs are exported by `generateStaticParams`. New API slugs require a rebuild, or an explicitly agreed change to client-side routing/server rendering.
4. Keep client secrets out of browser bundles. Decide static fetch-at-build vs client fetch based on the real API and freshness requirements.
5. Contact form is connected to `POST /tenants/sanazgoodarzi.ir/contact-submissions` with title/name/email/cellNumber/text. It resets and displays success text plus toast only after HTTP success; failures preserve entered values.
6. Mark preview detail pages `noindex` until populated. Review metadata when real content is connected.
## Verification status
- Dependencies installed; npm audit at install reported zero vulnerabilities.
- Development home compiled and HTTP returned 200; preview opened at localhost:3000.
- Initial lint found no errors and one config export warning; the warning has been corrected.
- Final `npm run build` passed using Next.js 16.3.4, including all 10 public content routes and the generated not-found route. All are static/SSG exports.
- Final `npm run lint` passed with zero errors and zero warnings.
- `scripts/verify-export.mjs` passed: 10 HTTP routes, 37 linked local assets/files, valid signatures for both IRANYekan WOFF files, Persian RTL markup, the Meshkee credit, and a real HTTP 404 with the Persian not-found page.
- Preview tab at `http://localhost:3000/` was marked as the deliverable; the source site on port 8000 was preserved. Use `npm run dev` for continued editing, or `npm start` for the exported static preview. Do not start both on the same port.
- Browser QA performed on desktop home hero/category slides (including mousewheel), source-matched about/services, and 390x844 mobile home, services, contact and fullscreen menu. Mobile services/contact showed no horizontal overflow. Home footer absence and inner-page footer presence checked. Build, lint and export verification passed again after the latest footer/subheader changes.
- SEO prompt verification on 2026-09-15: production build and lint passed. `scripts/verify-seo.mjs` passed for all 6 crawlable static pages (unique title/description, exactly one H1, canonical URL, meaningful image alt) and confirmed the generated config contains exactly 6 static routes. `scripts/verify-export.mjs` passed all 11 current exported/preview routes. API blogs and portfolios were empty, so placeholder static params are noindex and will be replaced by real ID/title params on a build after CMS publication.
## Change log
- SEO prompt `6d011d374a2bb32e88d778c8`: changed CMS detail links and static params to tenant-specific ID + Persian-title canonical URLs; added unique descriptions and canonical metadata to all crawlable static pages; added CMS-first detail metadata and meaningful OG image alt text; confirmed one H1 per implemented page; and added build-time route scanning that writes `public/meshkee/sitemap-config.json`. Never add local `public/sitemap.xml` or `public/robots.txt`; production nginx proxies them to Meshkee. Product/category sitemap instructions remain excluded by explicit user requirement.
- Backend prompt `579d72949e25b19a25256699`: connected blog and portfolio public APIs with empty-state thumbnails; added API detail routes, portfolio lightbox/hover zoom/related row, contact submission behavior, customer account detection from shared cookies plus `/auth/me`, and API branding with local `Logo.png` fallback. API state at implementation: branding URLs null, blogs 0, portfolios 0. Product/store features were explicitly excluded by the user.
- Latest steering during reference matching: remove Footer entirely from the home route only; retain it on all other routes. Remove the subheader overlay, keep the source photo unobscured, and use dark titles/English subtitles. This explicitly supersedes the earlier 80% black overlay instruction.
- Logo follow-up: integrated user-supplied `public/Logo.png` in the shared header/footer/mobile-menu brand. Responsive widths preserve the original 462×473 aspect ratio; intrinsic dimensions reserve layout space and alt text identifies the brand.
- Latest subheader instruction supersedes the light-overlay design: all shared page headings now use a uniform black `rgba(0,0,0,0.8)` photo overlay and white titles, descriptions, English eyebrows, breadcrumbs, and link states at every breakpoint. Font weights and responsive sizing are preserved.
- Scroll animation correction: replaced late WAAPI opacity resets with pre-armed CSS transitions on offscreen elements only. Already-visible content is never hidden; entrances begin 48px before viewport entry, move 16px, and run once. Mutation processing checks added subtrees instead of rescanning the full page on Swiper updates. Reduced-motion changes and cleanup reveal prepared content; focused content stays visible. No Animate.css dependency needed for these small opacity/transform transitions. This supersedes the previous viewport WAAPI implementation. Production build and lint passed after the scroll correction.
- Animation follow-up: added progressively enhanced, once-per-element viewport entrances (including filtered portfolio cards), hero/subheader text entrances, and subtle button/icon feedback. Mobile dialog slides in from the right in 440ms and closes in 260ms; backdrop fades and links enter sequentially. Native dialog stays modal and scrolling stays locked until exit completes; Escape is intercepted to animate before closing. Reduced-motion users get immediate menu changes and no reveal effects. `components/page-motion.js` owns observers and cancels animations on unmount/preference changes. Production build and lint both passed after this change.
- Subheader readability follow-up: added a consistent high-opacity warm-white photo overlay at every breakpoint, explicit charcoal heading/body colors, darker gold eyebrow, and readable 13px breadcrumb/eyebrow and 16px description sizes. Preserved IRANYekan Bold headings / Light remaining text. This applies to all shared `PageHeading` sections.
- Initial request: created the Next.js static migration, source content mapping, Swiper, shared navigation/footer, responsive public pages, and API placeholders.
- Image follow-up: copied supplied `img/` to `public/img/` without renaming source files.
- Responsive follow-up: mobile/tablet/desktop layouts, hamburger spacing, dialog navigation, touch targets, fluid grids/images.
- Font follow-up: switched to supplied IRANYekan local Light/Bold fonts and recorded the weight rule.
+30
View File
@@ -0,0 +1,30 @@
# گروه معماری ساناز گودرزی
نسخهٔ استاتیک فارسی و راست‌به‌چپ با Next.js و Swiper.
## اجرا
Node.js 24 LTS پیشنهاد می‌شود.
```sh
npm install
npm run dev
```
پیش‌نمایش: `http://localhost:3000`
```sh
npm run lint
npm run build
npm start
```
خروجی استاتیک در `out/` ایجاد می‌شود. برای انتشار روی هاست استاتیک، همین پوشه باید سرو شود. همهٔ مسیرها فایل `index.html` مستقل دارند و مسیر ناشناخته باید `404.html` را با وضعیت 404 دریافت کند.
## ادامهٔ توسعه
ابتدا [CONTEXT.md](./CONTEXT.md) را بخوانید و با تغییر نیازها به‌روزرسانی کنید. متن و اطلاعات تماس در `lib/content.js` و داده‌های نمایشی نمونه‌کار در `lib/portfolio.js` قرار دارند.
عکس‌ها با نام‌های اصلی در `public/img/` و فونت‌های IRANYekan در `public/fonts/` هستند. عنوان‌های اصلی Bold و باقی متن‌ها Light هستند.
نمونه‌کارها فعلاً پیش‌نمایش حوزه‌های طراحی‌اند. فرم تماس هنوز به API متصل نیست و هیچ پیامی ارسال یا ذخیره نمی‌کند. داکیومنت API برای اتصال واقعی لازم است.
+15
View File
@@ -0,0 +1,15 @@
import { aboutParagraphs, faqs } from '../../lib/content';
export const metadata = { title: 'دربارهٔ گروه معماری ساناز گودرزی', description:'با رویکرد، تجربه و حوزه‌های فعالیت گروه معماری و دکوراسیون ساناز گودرزی در طراحی، نظارت و اجرای پروژه‌های معماری آشنا شوید.', alternates:{canonical:'/aboutus/'} };
const indicators = [['رضایت مشتریان',68],['تنوع خدمات',25],['زیبایی بصری',36],['نمونه‌کارها',82]];
export default function About() {
return <>
<section className="reference-about-hero"><p className="reference-english" dir="ltr">About Us</p><h1>رویای شما<br/>تبدیل به واقعیت می‌شود</h1>
<div className="reference-about-icons" aria-label="طراحی، نظارت و اجرا">{[1,2,3].map((number)=><span key={number}><img src={`/img/info/${number}.png`} alt={['نظارت و اجرا','طراحی معماری','دکوراسیون داخلی'][number-1]} width="60" height="80"/></span>)}</div>
</section>
<section className="reference-about-intro"><div><h2>گروه معماری و دکوراسیون<br/>ساناز گودرزی</h2><p className="reference-english" dir="ltr">ARCHITECT & INTERIOR DESIGN</p><p>{aboutParagraphs.join(' ')}</p></div><img src="/img/info_1/bg.jpg" alt="طراحی داخلی فضای غذاخوری" loading="lazy"/></section>
<section className="reference-about-values"><img src="/img/info_2/bg.jpg" alt="طراحی فضای نشیمن" loading="lazy"/><div className="reference-about-values-copy"><h2>رویای شما تبدیل به واقعیت می‌شود؛ این تمام چیزی است که ما انجام می‌دهیم.</h2><p className="reference-english" dir="ltr">TURN YOUR DREAMS INTO REALITY</p><p>رویای شما تبدیل به واقعیت می‌شود؛ این تمام چیزی است که ما انجام می‌دهیم.</p>
{indicators.map(([label,value],index)=><div className="reference-progress" key={label}><label htmlFor={`indicator-${index}`}>{label}</label><progress id={`indicator-${index}`} value={value} max="100">{value}%</progress></div>)}
</div></section>
<section className="reference-faq"><div><h2>با ما بیشتر آشنا شوید</h2><p className="reference-english" dir="ltr">MORE ABOUT US</p><p>این مجموعه با اولویت قرار دادن خواسته‌های کارفرما و رسیدگی پیگیرانه به موضوعات پیش‌آمده، تا حصول نتیجهٔ رضایت‌بخش دست از تلاش و همکاری بر نخواهد داشت.</p><div className="faqs">{faqs.map(([question,answer])=><details key={question}><summary>{question}<span aria-hidden="true">+</span></summary><p>{answer}</p></details>)}</div></div></section>
</>;
}
+11
View File
@@ -0,0 +1,11 @@
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { faSlug, getBlogById, getBlogs, imageUrlOf, plainText, titleOf } from '../../../../lib/meshkee-api';
export const dynamicParams = false;
export async function generateStaticParams() { const params=(await getBlogs()).map((post)=>({slug:String(post.id),titleSlug:faSlug(titleOf(post))})); return params.length?params:[{slug:'__placeholder__',titleSlug:'بدون-عنوان'}]; }
export async function generateMetadata({params}) { const {slug}=await params; const post=await getBlogById(slug); if(!post)return{robots:{index:false,follow:false}}; const title=titleOf(post,'بلاگ'); const description=post.seoMetaDescription||plainText(post.abstract||post.summary||post.mainTextHtml||'').slice(0,160)||`مقالهٔ ${title} از گروه معماری ساناز گودرزی.`; const canonical=`/blog/${post.id}/${faSlug(title)}/`; return {title:post.seoMetaTitle||title,description,alternates:{canonical},openGraph:{title,description,url:canonical,type:'article',images:imageUrlOf(post)?[{url:imageUrlOf(post),alt:title}]:[]}}; }
export default async function BlogDetail({params}) { const {slug}=await params; const post=await getBlogById(slug); if(!post)notFound(); const title=titleOf(post); const image=imageUrlOf(post); return <article className="editorial-detail container">
{post.schema?.jsonLd&&<script type="application/ld+json" dangerouslySetInnerHTML={{__html:JSON.stringify(post.schema.jsonLd)}}/>}
<Link className="text-link" href="/blog/">بازگشت به بلاگ ←</Link><span className="eyebrow">بلاگ معماری</span><h1>{title}</h1>{post.abstract&&<p className="editorial-lead">{plainText(post.abstract)}</p>}{image&&<img className="editorial-cover" src={image} alt={`تصویر شاخص مقالهٔ ${title}`}/>}<div className="rich-content" dangerouslySetInnerHTML={{__html:post.mainTextHtml||post.contentHtml||''}}/>
</article>; }
+13
View File
@@ -0,0 +1,13 @@
import Link from 'next/link';
import { ArrowUpLeft } from 'lucide-react';
import { PageHeading } from '../../components/common';
import { faSlug, getBlogs, imageUrlOf, plainText, titleOf } from '../../lib/meshkee-api';
export const metadata = { title: 'بلاگ معماری و طراحی داخلی', description: 'مقاله‌ها و نوشته‌های گروه معماری ساناز گودرزی دربارهٔ معماری، طراحی داخلی، طراحی نما و فضای سبز.', alternates:{canonical:'/blog/'} };
export default async function BlogPage() {
const posts = await getBlogs();
return <><PageHeading title="بلاگ" en="BLOG"/><section className="container editorial-list">
{!posts.length && <div className="empty-content"><div className="empty-thumbnail" aria-hidden="true"/><h2>مقاله‌ها به‌زودی منتشر می‌شوند</h2><p>این بخش به API بلاگ متصل است و پس از انتشار نخستین مطلب، محتوای واقعی همین‌جا نمایش داده می‌شود.</p></div>}
<div className="editorial-grid">{posts.map((post) => { const title=titleOf(post,'مقالهٔ معماری'); const image=imageUrlOf(post)||'/img/subheader/bg.jpg'; return <article key={post.id}><Link href={`/blog/${post.id}/${faSlug(title)}/`}><div className="editorial-image"><img src={image} alt={title} loading="lazy"/></div><small>{post.categoryName||'بلاگ معماری'}</small><h2>{title}</h2>{(post.abstract||post.summary)&&<p>{plainText(post.abstract||post.summary)}</p>}<span>ادامه مطلب <ArrowUpLeft size={18}/></span></Link></article>})}</div>
</section></>;
}
+15
View File
@@ -0,0 +1,15 @@
import { Instagram, Mail } from 'lucide-react';
import { PageHeading } from '../../components/common';
import ContactForm from '../../components/contact-form';
import { offices, phones, emails, instagram } from '../../lib/content';
export const metadata = { title: 'تماس با گروه معماری ساناز گودرزی', description:'اطلاعات تماس، شماره تلفن، ایمیل و نشانی دفترهای گروه معماری ساناز گودرزی در تهران، کیش، قم و ازمیر.', alternates:{canonical:'/contactus/'} };
export default function Contact() {
return <><PageHeading title="تماس با ما" en="Contact Us"/>
<section className="reference-contact">
<div><h2>آدرس مجموعه</h2>{offices.map(office=><p key={office.city}>{office.city} — {office.address}</p>)}<p className="contact-address-latin" dir="ltr">{offices[3].latin}</p></div>
<div className="reference-contact-links"><h2>اطلاعات تماس با ما</h2>{phones.map(phone=><a key={phone.value} dir="ltr" href={`tel:${phone.value}`}>{phone.label}</a>)}{emails.map(email=><a key={email} href={`mailto:${email}`} dir="ltr">{email}</a>)}</div>
<div><h2>با ما در شبکه‌های اجتماعی</h2><div className="reference-contact-social"><a href={instagram} target="_blank" rel="noreferrer"><span dir="ltr">instagram</span><Instagram/></a><a href={`mailto:${emails[0]}`}><span dir="ltr">email</span><Mail/></a></div></div>
</section>
<section className="reference-message"><h2>منتظر پیام شما هستیم</h2><p className="reference-english" dir="ltr">SEND US MESSAGE</p><ContactForm/></section>
</>;
}
+88
View File
@@ -0,0 +1,88 @@
:root{--gold:#e4c385;--gold-dark:#89682f;--ink:#262827;--muted:#73746f;--paper:#faf9f6;--line:#e5e1d9;--dark:#222523;--font:var(--font-iranyekan),Tahoma,sans-serif}
*{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:110px}body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--font);font-size:16px;line-height:1.95}a{color:inherit;text-decoration:none}button,input,textarea{font:inherit}button,a,input,textarea,summary{-webkit-tap-highlight-color:transparent}button{cursor:pointer}button:disabled{cursor:default}img{max-width:100%;display:block}button{color:inherit}h1,h2,h3,p{margin:0}h1,h2,h3{line-height:1.6}h1{font-size:clamp(36px,4.5vw,64px);font-weight:300}h2{font-size:clamp(28px,3vw,42px);font-weight:300}h3{font-size:25px;font-weight:300}p{color:var(--muted)}:focus-visible{outline:2px solid var(--gold-dark);outline-offset:5px}.container{width:min(1240px,calc(100% - 112px));margin-inline:auto}.eyebrow{display:block;color:var(--gold-dark);font-size:12px;font-weight:300;letter-spacing:.06em;margin-bottom:20px}.eyebrow[dir=ltr]{text-align:right;letter-spacing:.18em}.button{display:inline-flex;align-items:center;justify-content:center;gap:34px;min-height:54px;padding:13px 25px;border:1px solid transparent;font-size:14px;transition:background .2s,color .2s}.button-gold{background:var(--gold);color:var(--ink)}.button-gold:hover{background:#f1d8a8}.button-dark{background:var(--dark);color:white}.button-dark:hover{background:#444b43}.text-link{display:inline-flex;align-items:center;gap:24px;border-bottom:1px solid #bbae92;padding:8px 0;font-size:14px;transition:gap .2s}.text-link:hover{gap:32px}.skip-link{position:fixed;top:8px;right:8px;transform:translateY(-160%);z-index:100;background:white;padding:10px 20px}.skip-link:focus{transform:none}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.site-header{position:relative;width:100%;z-index:20;background:var(--paper);border-bottom:1px solid var(--line)}.site-header.over-hero{position:absolute;top:0;background:linear-gradient(#171b1890,transparent);border-bottom:1px solid #ffffff22;color:#fff}.header-inner{width:min(1400px,calc(100% - 80px));height:112px;margin:auto;display:flex;align-items:center;justify-content:space-between;gap:30px}.brand{display:flex;flex-direction:column;gap:0;line-height:1.6}.brand-name{font-size:27px;font-weight:300;white-space:nowrap}.brand-dot{color:var(--gold);padding-right:3px}.brand-caption{font-family:var(--font);font-size:8px;letter-spacing:1.8px}.desktop-nav{display:flex;gap:29px;align-items:center;font-size:13px}.desktop-nav a{position:relative;padding:18px 0}.desktop-nav a:after{content:'';position:absolute;bottom:10px;right:0;width:0;height:1px;background:var(--gold);transition:width .2s}.desktop-nav a:hover:after,.desktop-nav a[aria-current]:after{width:100%}.header-contact{display:flex;align-items:center;gap:20px;font-size:12px;border:1px solid #9998;padding:9px 15px}.menu-button{border:0;background:none;display:none;padding:12px;min-width:48px;min-height:48px}.icon-button{display:grid;place-items:center;border:1px solid var(--line);background:transparent;width:48px;height:48px}.mobile-menu{border:0;margin:0 0 0 auto;padding:32px;max-height:100dvh;height:100dvh;width:min(430px,100%);max-width:100%;color:var(--ink);background:var(--paper)}.mobile-menu::backdrop{background:#111a;backdrop-filter:blur(5px)}.menu-top{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:55px}.mobile-menu nav{display:flex;flex-direction:column}.mobile-menu nav a{display:flex;align-items:center;gap:20px;border-bottom:1px solid var(--line);padding:18px 0;font-size:22px}.mobile-menu nav a svg{margin-right:auto;width:18px}.mobile-menu nav small{font-family:var(--font);color:var(--gold-dark);font-size:11px}.mobile-menu nav a[aria-current]{color:var(--gold-dark)}.menu-phone{display:block;margin-top:38px;text-align:right}
.hero{position:relative;height:min(880px,100svh);min-height:650px;background:#303532;color:#fff}.hero>.swiper{height:100%}.hero .swiper-slide{height:100%;overflow:hidden}.hero-image{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center}.hero-shade{position:absolute;inset:0;background:linear-gradient(270deg,#171c19c9 0%,#171c1970 52%,#171c1910),linear-gradient(0deg,#151c1890,transparent 30%)}.hero-content{position:relative;z-index:1;padding-top:clamp(195px,27vh,285px);padding-bottom:140px}.hero-content .eyebrow{color:var(--gold);font-size:13px;display:flex;align-items:center;gap:15px}.hero-content .eyebrow:before{content:'';width:32px;height:1px;background:var(--gold)}.hero h1{font-size:clamp(38px,4.8vw,68px);font-weight:300;line-height:1.7;max-width:770px;letter-spacing:-1px}.hero h1 em{color:var(--gold);font-style:normal}.hero-content p{color:#e1e2dd;font-size:15px;margin:19px 0 31px}.hero-bottom{position:absolute;z-index:5;bottom:28px;left:0;right:0;display:flex;align-items:center;justify-content:space-between;gap:25px;padding-top:24px;border-top:1px solid #ffffff30}.scroll-cue{display:flex;align-items:center;gap:12px;font-size:11px;color:#e1e2dd}.slider-controls{display:flex;align-items:center;gap:16px}.slider-controls button{display:grid;place-items:center;width:42px;height:42px;border:1px solid #ffffff40;background:transparent;color:#fff;transition:background .2s}.slider-controls button:hover{background:#ffffff20}.slide-count{display:flex;align-items:center;gap:16px;font-family:var(--font);font-size:12px;color:#c1c3bc}.slide-count b{font-size:20px;color:var(--gold);font-weight:300}.slide-count span{color:#ffffff50}.hero-signature{font-family:var(--font);font-size:9px;letter-spacing:1.5px;color:#d1d3cd}
.services-home{padding-top:110px;padding-bottom:100px}.section-heading{display:flex;justify-content:space-between;gap:80px;align-items:end;margin-bottom:75px}.section-heading-side{max-width:370px}.section-heading-side p{font-size:15px;margin-bottom:20px}.service-editorial{display:grid;gap:100px}.service-row{display:grid;grid-template-columns:1.18fr 1fr;gap:90px;align-items:center}.service-row:nth-child(even){direction:ltr}.service-row:nth-child(even)>*{direction:rtl}.service-photo{position:relative;overflow:hidden}.service-photo img{height:480px;width:100%;object-fit:cover;transition:transform .7s}.service-photo:hover img{transform:scale(1.025)}.image-index{position:absolute;left:0;bottom:0;background:var(--paper);font-family:var(--font);font-size:39px;line-height:1;padding:25px 30px 10px;color:#a39476}.service-copy h3{font-size:35px;margin-bottom:24px}.service-copy p{margin-bottom:25px;font-size:16px;line-height:2.2}.service-copy .eyebrow{margin-bottom:18px}.about-band{background:#eeece5;padding:80px 0}.about-band-grid{display:grid;grid-template-columns:1.15fr 1fr;gap:90px;align-items:center}.about-band img{width:100%;height:400px;object-fit:cover}.about-band p{margin:24px 0}.contact-cta{display:flex;justify-content:space-between;align-items:center;padding-block:95px;gap:30px}.contact-cta .eyebrow{margin-bottom:12px}.contact-cta h2{font-size:34px}
.page-heading{background:#eeede8 url('/img/subheader/bg.jpg') center/cover no-repeat;padding:46px 0 55px;min-height:285px;border-bottom:1px solid var(--line)}.page-heading h1{font-size:44px;margin-bottom:12px}.page-heading .eyebrow{margin:28px 0 6px;letter-spacing:.2em}.page-heading p{max-width:620px;color:#575d56}.breadcrumb{display:flex;gap:14px;font-size:11px;color:#7d7f76}.breadcrumb a:hover{color:var(--gold-dark)}.services-page{padding-top:45px}.category-tabs{display:flex;gap:12px;flex-wrap:wrap}.category-tabs button,.category-tabs a{font-size:13px;display:inline-flex;align-items:center;min-height:44px;white-space:nowrap;padding:9px 19px;border:1px solid var(--line);background:transparent;transition:background .2s}.category-tabs button[aria-pressed=true],.category-tabs a:hover,.category-tabs button:hover{background:var(--dark);color:#fff;border-color:var(--dark)}.service-detail{display:grid;grid-template-columns:.9fr 1fr;gap:75px;padding:80px 0;border-bottom:1px solid var(--line);align-items:start;scroll-margin-top:30px}.service-detail h2{font-size:34px;margin-bottom:25px}.service-detail p{margin-bottom:16px}.service-detail .text-link{margin-top:15px}.service-detail-image{position:relative}.service-detail-image img{width:100%;height:540px;object-fit:cover}.service-detail-image>span{position:absolute;bottom:15px;left:25px;color:#fff;font-family:var(--font);font-size:55px;text-shadow:0 1px 15px #222}.about-intro{display:grid;grid-template-columns:.8fr 1.2fr;gap:80px;align-items:start;padding-block:90px}.about-intro h2{font-size:31px;margin-bottom:24px}.about-intro p{margin-bottom:20px;font-size:15px}.about-portrait{position:relative;background:#efede6;padding:32px 20px}.about-portrait img{width:100%;aspect-ratio:1;object-fit:contain}.about-portrait>span{display:block;font-family:var(--font);font-size:12px;letter-spacing:3px;line-height:1.8;text-align:center;margin:35px 0 8px;color:#7b725e}.values-band{background:var(--dark);color:white;padding:65px 0;text-align:center}.values-band h2{font-size:30px;margin-bottom:40px}.values-grid{display:grid;grid-template-columns:repeat(3,1fr)}.values-grid>div+div{border-right:1px solid #ffffff20}.values-grid img{margin:auto auto 20px;object-fit:contain}.values-grid h3{font-size:18px}.faq-section{padding-block:95px;display:grid;grid-template-columns:1fr 1.2fr;gap:85px}.faq-section h2{font-size:32px;margin-bottom:22px}.faqs details{border-bottom:1px solid #dcd8ce}.faqs summary{cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:20px;list-style:none;padding:22px 0;font-weight:300}.faqs summary::-webkit-details-marker{display:none}.faqs summary span{color:var(--gold-dark);font-size:26px;transition:transform .2s}.faqs details[open] summary span{transform:rotate(45deg)}.faqs details p{padding-bottom:25px;font-size:15px}
.contact-info{padding-block:70px;display:grid;grid-template-columns:repeat(3,1fr);gap:40px}.contact-info>div{min-width:0}.contact-info>div+div{border-right:1px solid var(--line);padding-right:35px}.gold-icon{color:var(--gold-dark);width:26px;height:26px;margin-bottom:18px}.contact-info h2{font-size:22px;margin-bottom:20px}.contact-info a{display:block;text-align:right;font-size:16px;overflow-wrap:anywhere}.contact-info a:hover{color:var(--gold-dark)}.contact-info p{font-size:13px;margin-top:12px}.offices-section{padding:65px 0;background:#eeece6}.offices-section .eyebrow{margin-bottom:8px}.offices-section h2{font-size:32px}.offices-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:35px;margin-top:40px}.offices-grid article{border-top:1px solid #c7c0ae;padding-top:25px}.offices-grid svg{color:var(--gold-dark);margin-bottom:14px}.offices-grid h3{font-size:20px;margin-bottom:12px}.offices-grid p{font-size:14px}.offices-grid p[dir=ltr]{font-family:var(--font);font-size:12px;margin-top:8px;text-align:right}.message-section{padding-block:80px}.message-section .section-heading{margin-bottom:35px}.message-section h2{font-size:32px}.contact-form{max-width:1000px}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:25px;margin-bottom:25px}.contact-form label{display:block;font-size:13px;font-weight:300}.contact-form input,.contact-form textarea{display:block;width:100%;border:1px solid #d8d4ca;background:#ffffff66;border-radius:0;color:var(--ink);padding:14px 16px;margin-top:9px;font-size:15px;min-width:0}.contact-form textarea{resize:vertical}.contact-form input::placeholder,.contact-form textarea::placeholder{color:#92948d}.form-bottom{display:flex;justify-content:space-between;align-items:center;gap:30px;margin-top:28px}.form-note{font-size:12px;max-width:560px}.form-bottom button{flex-shrink:0}.form-status{font-size:14px;margin-top:20px;color:var(--gold-dark)}
.portfolio-section{padding-block:50px 80px}.portfolio-note{font-size:13px;margin:25px 0 40px}.portfolio-grid{display:grid;grid-template-columns:1fr 1fr;gap:55px 35px}.portfolio-image{position:relative;overflow:hidden}.portfolio-image img{height:390px;width:100%;object-fit:cover;transition:transform .6s}.portfolio-image:hover img{transform:scale(1.025)}.portfolio-image>span{position:absolute;top:18px;right:18px;background:#faf9f0ee;color:#4c4d46;padding:5px 13px;font-size:11px}.portfolio-card-title{display:flex;justify-content:space-between;align-items:center;padding-top:20px}.portfolio-card-title small{color:var(--muted);font-size:11px}.portfolio-card-title h2{font-size:24px;margin-top:5px}.portfolio-card-title>svg{color:var(--gold-dark)}.portfolio-detail{padding-block:70px;display:grid;grid-template-columns:1.2fr 1fr;gap:70px;align-items:center}.portfolio-detail>img{width:100%;height:570px;object-fit:cover}.portfolio-detail h2{margin-bottom:25px}.placeholder-notice{border-right:2px solid var(--gold);background:#eeece6;padding:20px;margin:25px 0}.placeholder-notice p{font-size:14px;margin-top:8px}.not-found{min-height:60vh;padding-block:100px}.not-found p{margin:20px 0 30px}
.site-footer{background:var(--dark);color:#eee}.footer-main{padding-block:75px 55px;display:grid;grid-template-columns:1.4fr .65fr 1fr 1.4fr;gap:45px}.site-footer h3{font-size:15px;margin-bottom:25px;color:#f2f0e8}.footer-intro .brand-name{font-size:26px}.footer-intro .brand-caption{font-size:7px}.footer-intro>p{color:#a1a59d;font-size:12px;margin-top:24px}.footer-intro>p+p{margin-top:10px}.footer-nav{display:flex;flex-direction:column;gap:11px;font-size:12px;color:#b7bab2}.footer-contact>a{display:block;text-align:right;font-size:14px;color:#b7bab2;margin-bottom:9px}.footer-contact .email{font-family:var(--font);font-size:11px;overflow-wrap:anywhere}.footer-offices p{font-size:11px;color:#a1a59d;margin-bottom:15px}.footer-offices p strong{color:#d4d6cf;font-weight:300}.footer-offices p span{display:block;font-size:10px;text-align:right}.social-links{display:flex;gap:12px;margin-top:23px}.social-links a{width:35px;height:35px;display:grid;place-items:center;border:1px solid #5e645a;color:#d4d6cf}.footer-nav a:hover,.footer-contact a:hover,.social-links a:hover{color:var(--gold)}.footer-bottom{background:#1a1d1b;border-top:1px solid #ffffff0b;padding:22px 0}.footer-bottom-bar{display:flex;align-items:center;justify-content:space-between;gap:20px;position:relative}.footer-bottom-bar>span{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;font-family:var(--font);transition:color .3s}.meshkee-credit:hover{color:#e0e0e0}.meshkee-credit__frame{inset:0;pointer-events:none;position:absolute}.meshkee-credit__frame i{background:#777;position:absolute;transition:transform .28s cubic-bezier(.22,1,.36,1)}.meshkee-credit__frame i:nth-child(1){height:100%;left:0;top:0;transform:scaleY(0);transform-origin:top;width:1px}.meshkee-credit__frame i:nth-child(2){bottom:0;height:1px;left:0;transform:scaleX(0);transform-origin:left;transition-delay:.14s;width:100%}.meshkee-credit__frame i:nth-child(3){height:100%;right:0;top:0;transform:scaleY(0);transform-origin:bottom;transition-delay:.28s;width:1px}.meshkee-credit__frame i:nth-child(4){height:1px;left:0;top:0;transform:scaleX(0);transform-origin:right;transition-delay:.42s;width:100%}.meshkee-credit:hover .meshkee-credit__frame i,.meshkee-credit:focus-visible .meshkee-credit__frame i{transform:scale(1)}.meshkee-credit>img{flex-shrink:0;height:42px;width:42px;object-fit:contain;opacity:.7;transition:opacity .3s}.meshkee-credit:hover>img{opacity:1}.meshkee-credit__text{display:flex;flex-direction:column;justify-content:center;line-height:1.25;min-height:42px;gap:3px}.meshkee-credit__text>span{font-size:11px}.meshkee-credit__text>strong{color:#cfcfcf;font-size:13px;font-weight:300}.back-top{border:1px solid #6666;width:38px;height:38px;display:grid;place-items:center}
@media(min-width:1600px){.hero-content{padding-top:260px}}
@media(max-width:1100px){.container{width:calc(100% - 72px)}.header-inner{width:calc(100% - 60px);gap:18px}.desktop-nav{gap:20px;font-size:12px}.header-contact{display:none}.service-row,.about-band-grid{gap:50px}.service-photo img{height:420px}.footer-main{grid-template-columns:1.2fr .7fr 1fr;gap:35px}.footer-offices{grid-column:1/-1;display:grid;grid-template-columns:repeat(4,1fr);gap:25px}.footer-offices h3{grid-column:1/-1;margin-bottom:0}.about-intro{gap:45px}.service-detail{gap:40px}.contact-info{gap:25px}.contact-info>div+div{padding-right:25px}.hero-signature{display:none}.portfolio-detail{gap:40px}}
@media(max-width:800px){.container{width:calc(100% - 48px)}.header-inner{height:92px;width:calc(100% - 40px)}.desktop-nav{display:none}.menu-button{display:grid;place-items:center}.brand-name{font-size:25px}.brand-caption{font-size:7px}.hero{min-height:680px;max-height:900px;height:90svh}.hero-content{padding-top:190px}.hero h1{font-size:48px}.hero-shade{background:linear-gradient(270deg,#172018d6,#17201855),linear-gradient(0deg,#17201899,transparent)}.section-heading{gap:40px;margin-bottom:50px}.section-heading-side{max-width:310px}.service-row{gap:35px}.service-photo img{height:370px}.service-copy h3{font-size:29px}.service-copy p{font-size:14px}.service-editorial{gap:65px}.services-home{padding-top:75px;padding-bottom:70px}.about-band-grid{gap:35px}.about-band img{height:330px}.about-band h2{font-size:28px}.about-band p{font-size:14px}.contact-cta{padding-block:65px}.contact-cta h2{font-size:27px}.contact-cta .button{gap:15px;padding-inline:18px}.page-heading{min-height:250px;padding-block:35px 40px;background-position:35% center}.page-heading h1{font-size:36px}.service-detail{grid-template-columns:1fr;gap:30px;padding-block:50px}.service-detail-image img{height:430px}.service-detail h2{font-size:30px}.about-intro{grid-template-columns:1fr;padding-block:60px;gap:35px}.about-portrait{max-width:500px;width:100%;margin:auto}.faq-section{grid-template-columns:1fr;gap:25px;padding-block:60px}.contact-info{grid-template-columns:1fr 1fr;gap:35px}.contact-info>div+div{padding-right:0;border-right:0}.offices-grid{grid-template-columns:1fr 1fr}.portfolio-image img{height:330px}.portfolio-detail{grid-template-columns:1fr;gap:35px}.portfolio-detail>img{height:450px}.footer-main{grid-template-columns:1fr 1fr}.footer-offices{grid-template-columns:1fr 1fr}.footer-intro{grid-row:span 2}.footer-bottom-bar{flex-wrap:wrap}.back-top{display:none}}
@media(max-width:560px){html{scroll-padding-top:30px}.container{width:calc(100% - 40px)}.header-inner{height:86px;width:calc(100% - 32px)}.brand-name{font-size:23px}.brand-caption{font-size:6.5px;letter-spacing:1.3px}.hero{height:calc(100svh - 20px);min-height:620px;max-height:830px}.hero-content{padding-top:178px;padding-bottom:135px}.hero h1{font-size:clamp(34px,8.8vw,48px);line-height:1.8;letter-spacing:-.7px}.hero-content .eyebrow{font-size:11px;margin-bottom:23px}.hero-content p{font-size:12px;margin:20px 0 30px}.hero-image{object-position:42% center}.hero-bottom{bottom:22px;gap:10px;padding-top:18px}.hero-bottom .scroll-cue{font-size:10px;gap:7px}.slider-controls{gap:9px}.slider-controls button{width:35px;height:38px}.slide-count{gap:8px;font-size:10px}.slide-count b{font-size:17px}.hero .button{min-height:52px;font-size:13px;gap:30px}.section-heading{display:block;margin-bottom:40px}.section-heading h2{font-size:31px}.section-heading-side{margin-top:25px;max-width:none}.section-heading-side p{font-size:14px}.services-home{padding-top:60px}.eyebrow{font-size:11px;margin-bottom:15px}.service-row,.about-band-grid{grid-template-columns:1fr;gap:25px}.service-row:nth-child(even){direction:rtl}.service-photo img{height:390px}.service-copy h3{font-size:29px;margin-bottom:15px}.service-copy p{font-size:15px;margin-bottom:17px}.service-copy .eyebrow{font-size:10px;margin-bottom:10px}.image-index{font-size:30px;padding:18px 22px 8px}.service-editorial{gap:55px}.about-band{padding-block:50px}.about-band img{height:280px}.about-band h2{font-size:32px}.contact-cta{display:block;padding-block:55px}.contact-cta h2{font-size:29px}.contact-cta .button{margin-top:27px;font-size:13px;padding-inline:25px;gap:30px}.page-heading{padding-block:25px 35px;min-height:235px;background-image:linear-gradient(90deg,#eeede8b8,#eeede8f7),url('/img/subheader/bg.jpg')}.page-heading .eyebrow{margin-top:27px;font-size:10px}.page-heading h1{font-size:34px}.page-heading p{font-size:13px}.category-tabs{gap:8px}.category-tabs button,.category-tabs a{padding:9px 12px;font-size:11px}.service-detail-image img{height:380px}.service-detail p{font-size:15px}.about-intro h2{font-size:27px}.about-portrait{padding:20px}.about-portrait>span{font-size:10px;margin-top:20px}.values-band{padding-block:45px}.values-band h2{font-size:26px}.values-grid img{width:45px;height:45px}.values-grid h3{font-size:13px}.faq-section h2{font-size:28px}.faq-section>div>p{font-size:14px}.faqs summary{font-size:14px}.contact-info{grid-template-columns:1fr;padding-block:45px;gap:30px}.contact-info>div+div{border-top:1px solid var(--line);padding-top:30px}.contact-info h2{font-size:21px;margin-bottom:14px}.gold-icon{margin-bottom:12px}.offices-section{padding-block:45px}.offices-grid{gap:30px 22px}.offices-grid p{font-size:12px}.offices-grid h3{font-size:19px}.message-section{padding-block:50px}.message-section h2{font-size:27px}.form-grid{grid-template-columns:1fr;gap:20px}.contact-form input,.contact-form textarea{font-size:16px}.form-bottom{display:block}.form-bottom button{margin-top:22px;width:100%}.portfolio-section{padding-block:35px 45px}.portfolio-grid{grid-template-columns:1fr;gap:35px}.portfolio-note{font-size:12px;margin:22px 0 30px}.portfolio-image img{height:370px}.portfolio-card-title h2{font-size:23px}.portfolio-detail{padding-block:40px}.portfolio-detail>img{height:400px}.portfolio-detail h2{font-size:28px}.portfolio-detail p{font-size:15px}.footer-main{padding-block:50px 30px;gap:35px 22px;grid-template-columns:.8fr 1.2fr}.footer-intro{grid-column:1/-1;grid-row:auto}.footer-intro>p{max-width:390px}.footer-offices{gap:18px}.footer-offices p{font-size:11px}.footer-contact .email{font-size:10px}.site-footer h3{font-size:13px;margin-bottom:19px}.footer-contact>a{font-size:13px}.footer-bottom-bar{flex-direction:column;text-align:center;gap:18px}.footer-bottom{padding-block:20px}.meshkee-credit__text>span{font-size:10px}.meshkee-credit__text>strong{font-size:12px}.mobile-menu{padding:25px}.menu-top{margin-bottom:35px}.mobile-menu nav a{font-size:20px}.not-found h1{font-size:32px}}
@media(max-width:360px){.container{width:calc(100% - 32px)}.hero h1{font-size:32px}.service-photo img{height:340px}.footer-main{grid-template-columns:1fr}.footer-intro{grid-column:auto}.footer-offices{grid-template-columns:1fr}.footer-contact .email{font-size:12px}}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*::before,*::after{animation:none!important;transition:none!important}.hero .swiper-wrapper{transition-duration:0ms!important}}
/* Client typography: real IRANYekan Bold titles; Light everywhere else. */
.brand { flex-direction: row; align-items: center; gap: 12px; }
.brand-wordmark { display: flex; flex-direction: column; }
.brand-logo { width: 62px; height: auto; flex-shrink: 0; object-fit: contain; }
.footer-intro .brand { flex-wrap: wrap; }
.footer-intro .brand-logo { width: 72px; }
@media (max-width: 560px) {
.brand { gap: 9px; }
.brand-logo { width: 44px; }
.mobile-menu .brand-logo { width: 38px; }
.mobile-menu .brand-name { font-size: 20px; }
.mobile-menu .brand-caption { font-size: 5.5px; }
.menu-top { gap: 10px; }
.menu-top .icon-button { flex-shrink: 0; }
.footer-intro .brand-logo { width: 60px; }
}
body,button,input,textarea,b,strong{font-weight:300}h1,h2,h3,.hero h1{font-weight:700}
/* Keep subheader copy legible over the photograph at every breakpoint. */
.page-heading {
background-image:
linear-gradient(rgba(0, 0, 0, .8), rgba(0, 0, 0, .8)),
url('/img/subheader/bg.jpg');
color: #fff;
}
.page-heading h1 { color: #fff; }
.page-heading .eyebrow { color: #fff; font-size: 13px; }
.page-heading p { color: #fff; font-size: 16px; line-height: 2; }
.page-heading .breadcrumb { color: #fff; font-size: 13px; flex-wrap: wrap; }
.page-heading .breadcrumb a { text-decoration: underline; text-underline-offset: 4px; }
.page-heading .breadcrumb a:hover { color: #fff; }
.page-heading a:focus-visible { outline-color: #fff; }
@keyframes gentle-entry {
from { opacity: 0; transform: translateY(18px); }
to { opacity: 1; transform: translateY(0); }
}
/* Prepare offscreen content before entry; never reset visible content to zero. */
@media (prefers-reduced-motion: no-preference) {
[data-scroll-reveal='pending'] { opacity: 0; transform: translateY(16px); }
[data-scroll-reveal='visible'] {
opacity: 1;
transform: none;
transition: opacity .5s ease-out, transform .6s cubic-bezier(.2,.65,.3,1);
}
[data-scroll-reveal='pending']:focus-within { opacity: 1; transform: none; }
}
@keyframes backdrop-in { from { background-color: #1110; backdrop-filter: blur(0); } to { background-color: #111a; backdrop-filter: blur(5px); } }
@keyframes backdrop-out { from { background-color: #111a; backdrop-filter: blur(5px); } to { background-color: #1110; backdrop-filter: blur(0); } }
@media (prefers-reduced-motion: no-preference) {
.page-heading > .container { animation: gentle-entry .65s cubic-bezier(.22,1,.36,1) both; }
.swiper-slide-active .hero-content > * { animation: gentle-entry .8s cubic-bezier(.22,1,.36,1) both; }
.swiper-slide-active .hero-content h1 { animation-delay: .08s; }
.swiper-slide-active .hero-content p { animation-delay: .16s; }
.swiper-slide-active .hero-content .button { animation-delay: .24s; }
.mobile-menu[data-phase='opening']::backdrop { animation: backdrop-in .44s ease both; }
.mobile-menu[data-phase='closing']::backdrop { animation: backdrop-out .26s ease both; }
.mobile-menu[data-phase='opening'] nav a { animation: gentle-entry .5s cubic-bezier(.22,1,.36,1) both; }
.mobile-menu nav a:nth-child(2) { animation-delay: .05s; }
.mobile-menu nav a:nth-child(3) { animation-delay: .10s; }
.mobile-menu nav a:nth-child(4) { animation-delay: .15s; }
.mobile-menu nav a:nth-child(5) { animation-delay: .20s; }
.button, .icon-button, .social-links a { transition: transform .25s ease, background .25s ease, color .25s ease; }
.button:active, .icon-button:active { transform: scale(.97); }
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
.button:hover, .social-links a:hover { transform: translateY(-3px); }
.text-link svg, .portfolio-card-title svg { transition: transform .25s ease; }
.text-link:hover svg, .portfolio-grid a:hover .portfolio-card-title svg { transform: translate(-3px,-3px); }
}
+15
View File
@@ -0,0 +1,15 @@
import './globals.css';
import './reference.css';
import localFont from 'next/font/local';
import { Header, Footer } from '../components/shell';
import PageMotion from '../components/page-motion';
import { getBranding, getBusinessInfo } from '../lib/meshkee-api';
const iranyekan = localFont({
src: [
{path:'../public/fonts/iranyekanweblightfanum.woff',weight:'300',style:'normal'},
{path:'../public/fonts/iranyekanwebboldfanum.woff',weight:'700',style:'normal'},
],
variable:'--font-iranyekan', display:'swap',
});
export async function generateMetadata(){const branding=await getBranding();return {metadataBase:new URL('https://sanazgoodarzi.ir'),title:{default:'گروه معماری ساناز گودرزی',template:'%s | گروه معماری ساناز گودرزی'},description:'طراحی معماری، دکوراسیون داخلی، سالن‌های زیبایی، فضای سبز و نمای ساختمان؛ گروه معماری ساناز گودرزی.',icons:branding.faviconUrl?[{url:branding.faviconUrl}]:[{url:'/Logo.png'}]};}
export default async function RootLayout({children}) {const [branding,business]=await Promise.all([getBranding(),getBusinessInfo()]);return <html lang="fa" dir="rtl" className={iranyekan.variable}><body>{business.schema?.enabled&&business.schema?.jsonLd&&<script type="application/ld+json" dangerouslySetInnerHTML={{__html:JSON.stringify(business.schema.jsonLd)}}/>}<a className="skip-link" href="#main">رفتن به محتوای اصلی</a><Header branding={branding}/><main id="main">{children}</main><PageMotion/><Footer branding={branding}/></body></html>;}
+2
View File
@@ -0,0 +1,2 @@
import Link from 'next/link';
export default function NotFound(){return <section className="not-found container"><span className="eyebrow">۴۰۴</span><h1>این صفحه پیدا نشد.</h1><p>از منوی سایت یا صفحهٔ اصلی مسیرتان را ادامه دهید.</p><Link href="/" className="button button-dark">بازگشت به خانه</Link></section>}
+14
View File
@@ -0,0 +1,14 @@
import { services } from '../../lib/content';
import { PageHeading } from '../../components/common';
export const metadata = { title: 'خدمات طراحی معماری و دکوراسیون', description:'خدمات طراحی ویلا و آپارتمان، فضاهای اداری و تجاری، سالن‌های زیبایی، فضای سبز و نمای ساختمان توسط گروه ساناز گودرزی.', alternates:{canonical:'/ourservices/'} };
const order = ['residential', 'commercial', 'landscape', 'beauty', 'facade'];
export default function Services() {
return <><PageHeading title="خدمات ما" en="Our Services"/>
<div className="reference-services">{order.map(id => {
const service = services.find(item => item.id === id);
return <section className="reference-service-card" id={id} key={id}>
<h2>{service.title}</h2><p className="reference-english" dir="ltr">{service.en}</p>
<div className="service-card-text"><p>{service.description}</p>{service.details.map(text => <p key={text}>{text}</p>)}</div>
</section>;
})}</div></>;
}
+3
View File
@@ -0,0 +1,3 @@
import HomeExperience from '../components/home-experience';
export const metadata={title:{absolute:'گروه معماری ساناز گودرزی | معماری و طراحی داخلی'},description:'گروه معماری ساناز گودرزی؛ طراحی ویلا و آپارتمان، فضاهای اداری و تجاری، سالن زیبایی، فضای سبز و نمای ساختمان.',alternates:{canonical:'/'}};
export default function Home() { return <HomeExperience/>; }
@@ -0,0 +1,12 @@
import { notFound } from 'next/navigation';
import Link from 'next/link';
import MediaGallery from '../../../../components/media-gallery';
import { faSlug, galleryOf, getPortfolioById, getPortfolios, imageUrlOf, plainText, titleOf } from '../../../../lib/meshkee-api';
export const dynamicParams = false;
export async function generateStaticParams() { const params=(await getPortfolios()).map((item)=>({slug:String(item.id),titleFaSlug:faSlug(titleOf(item))})); return params.length?params:[{slug:'__placeholder__',titleFaSlug:'بدون-عنوان'}]; }
export async function generateMetadata({params}) { const {slug}=await params; const item=await getPortfolioById(slug); if(!item)return{robots:{index:false,follow:false}}; const title=titleOf(item,'نمونه‌کار'); const description=item.seoMetaDescription||plainText(item.abstract||item.mainTextHtml||'').slice(0,160)||`معرفی پروژهٔ ${title} از گروه معماری ساناز گودرزی.`; const canonical=`/portfolios/${item.id}/${faSlug(title)}/`; return {title:item.seoMetaTitle||title,description,alternates:{canonical},openGraph:{title,description,url:canonical,type:'article',images:imageUrlOf(item)?[{url:imageUrlOf(item),alt:title}]:[]}}; }
export default async function PortfolioPage({params}) { const {slug}=await params; const item=await getPortfolioById(slug); if(!item)notFound(); const all=await getPortfolios(); const title=titleOf(item); const cover=imageUrlOf(item); const gallery=galleryOf(item); const images=[...(cover?[{id:'cover',url:cover,alt:`تصویر اصلی پروژهٔ ${title}`}]:[]),...gallery.filter(image=>image.url!==cover)]; const related=all.filter(candidate=>candidate.id!==item.id&&(candidate.categoryId===item.categoryId||candidate.categoryName===item.categoryName)).slice(0,3); return <>
<article className="portfolio-api-detail container"><Link className="text-link" href="/portfolios/">بازگشت به نمونه‌کارها ←</Link><header><span className="eyebrow">{item.categoryName||'نمونه‌کار معماری'}</span><h1>{title}</h1>{item.abstract&&<p>{plainText(item.abstract)}</p>}</header><MediaGallery images={images} title={title}/>{item.mainTextHtml&&<div className="rich-content" dangerouslySetInnerHTML={{__html:item.mainTextHtml}}/>}{item.projectUrl&&<a className="button button-dark" href={item.projectUrl} target="_blank" rel="noopener noreferrer">مشاهده وب‌سایت پروژه</a>}</article>
{related.length>0&&<section className="related-work container"><h2>نمونه‌کارهای مشابه</h2><div className="editorial-grid">{related.map(candidate=>{const candidateTitle=titleOf(candidate);return <article key={candidate.id}><Link href={`/portfolios/${candidate.id}/${faSlug(candidateTitle)}/`}><div className="editorial-image"><img src={imageUrlOf(candidate)||'/img/subheader/bg.jpg'} alt={`تصویر پروژهٔ ${candidateTitle}`} loading="lazy"/></div><h3>{candidateTitle}</h3></Link></article>})}</div></section>}
</>; }
+8
View File
@@ -0,0 +1,8 @@
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { portfolioItems } from '../../../lib/portfolio';
import { services } from '../../../lib/content';
import { PageHeading, ContactCTA } from '../../../components/common';
export function generateStaticParams(){return portfolioItems.map(item=>({slug:item.slug}))}
export async function generateMetadata({params}){const {slug}=await params;const item=portfolioItems.find(i=>i.slug===slug);return {title:item?.title??'نمونه‌کار',robots:{index:false,follow:true}}}
export default async function PortfolioDetail({params}){const {slug}=await params;const item=portfolioItems.find(i=>i.slug===slug);if(!item)notFound();const service=services.find(s=>s.id===slug);return <><PageHeading title={item.title} en={service.en}/><article className="container portfolio-detail"><img src={item.image} alt={`تصویر نمایشی ${item.title}`}/><div><span className="eyebrow">پیش‌نمایش حوزهٔ طراحی</span><h2>{item.title}</h2><p>{service.description}</p><div className="placeholder-notice"><strong>اطلاعات پروژه‌ها به‌زودی</strong><p>{item.description} این تصویر معرفی حوزهٔ طراحی است و به پروژهٔ مشخصی نسبت داده نشده است.</p></div><Link className="text-link" href="/portfolios/">بازگشت به نمونه‌کارها ←</Link></div></article><ContactCTA/></>}
+5
View File
@@ -0,0 +1,5 @@
import { PageHeading } from '../../components/common';
import PortfolioGrid from '../../components/portfolio-grid';
import { getPortfolioItems } from '../../lib/portfolio';
export const metadata={title:'نمونه‌کارهای معماری و طراحی داخلی',description:'مجموعه نمونه‌کارهای معماری، طراحی داخلی، فضای سبز و نمای ساختمان گروه معماری ساناز گودرزی.',alternates:{canonical:'/portfolios/'}};
export default async function Portfolios(){const items=await getPortfolioItems();return <><PageHeading title="نمونه‌کارها" en="OUR PORTFOLIO" description="نگاهی به حوزه‌های طراحی گروه معماری ساناز گودرزی."/><section className="container portfolio-section"><PortfolioGrid items={items}/></section></>}
+276
View File
@@ -0,0 +1,276 @@
/* Layout matched to the running source site; explicit client changes remain. */
:root { --paper: #fff; --dark: #000; --gold: #efc379; }
body { background:#fff; }
.site-header, .site-header.over-hero { position:absolute; top:0; background:none; border:0; color:#fff; pointer-events:none; }
.header-inner { width:calc(100% - 100px); height:112px; flex-direction:row-reverse; }
.header-inner > a, .header-actions, .menu-button { pointer-events:auto; }
.header-actions { display:flex; align-items:center; }
.account-menu, .header-actions>.account-trigger { position:fixed; right:120px; top:29px; z-index:30; }
.account-trigger { min-width:50px; height:50px; padding:0 14px; border:0; border-radius:8px; background:#2229; color:#fff; display:flex; align-items:center; justify-content:center; gap:8px; }
.account-trigger svg { width:22px; height:22px; stroke-width:1.5; }
.account-user span { font-size:13px; white-space:nowrap; }
.account-dropdown { position:absolute; top:calc(100% + 10px); right:0; min-width:190px; padding:9px; background:#fff; color:#222; border:1px solid #eee; box-shadow:0 15px 35px #0002; }
.account-dropdown a { display:flex; align-items:center; gap:10px; padding:11px 9px; font-size:13px; }
.account-dropdown a:hover { background:#f4f1ea; }
.account-dropdown svg { width:17px; height:17px; }
.header-inner .brand-wordmark { display:none; }
.header-inner .brand-logo { width:65px; max-height:76px; object-fit:contain; }
.desktop-nav, .header-contact { display:none; }
.menu-button { display:grid; place-items:center; position:fixed; right:50px; top:28px; color:white; background:#2229; border-radius:8px; width:56px; height:52px; }
.menu-button svg { width:28px; height:28px; stroke-width:1.4; }
.mobile-menu { width:100%; max-width:100%; height:100dvh; background:#222; color:#fff; padding:65px 100px; }
.mobile-menu .menu-top { flex-direction:row-reverse; margin-bottom:10px; }
.mobile-menu .brand-wordmark, .mobile-menu nav small, .mobile-menu nav svg, .menu-phone { display:none; }
.mobile-menu .icon-button { border:0; }
.mobile-menu .icon-button svg { width:32px; height:32px; }
.mobile-menu nav { width:min(420px,100%); margin-left:auto; }
.mobile-menu nav a { border:0; padding:20px 0; font-size:25px; }
.reference-home { position:relative; background:#fff; }
.reference-home.is-desktop, .reference-home.is-desktop > .swiper { height:100svh; }
.reference-home.is-desktop .swiper-slide { height:100%; }
.reference-hero { height:100svh; min-height:600px; position:relative; color:#fff; }
.reference-hero-image { width:100%; height:100%; object-fit:cover; position:absolute; inset:0; }
.reference-hero-copy { position:absolute; top:50%; left:50%; transform:translate(-50%,-35%); width:90%; text-align:center; }
.reference-hero h1 { font-size:clamp(42px,4.2vw,64px); line-height:1.5; color:#fff; text-shadow:0 2px 16px #0004; }
.reference-gold-button { background:var(--gold); color:#292929; border-radius:5px; display:inline-flex; align-items:center; justify-content:center; width:160px; height:56px; margin-top:64px; font-size:13px; }
.reference-discover { position:absolute; right:9.5%; bottom:9.5%; display:flex; flex-direction:row-reverse; align-items:center; gap:44px; background:none; border:0; color:#fff; font-size:15px; }
.reference-play { width:110px; height:110px; background:var(--gold); border-radius:50%; display:grid; place-items:center; }
.reference-feature { display:grid; grid-template-columns:1fr 1fr; direction:ltr; height:100svh; min-height:650px; }
.reference-feature-image { position:relative; min-width:0; }
.reference-feature-image img { width:100%; height:100%; position:absolute; inset:0; object-fit:cover; }
.reference-feature-info { position:relative; direction:rtl; background:#fff; display:flex; align-items:center; justify-content:flex-start; padding:70px max(9.5vw,75px) 70px 85px; }
.reference-feature-copy { width:100%; max-width:350px; }
.reference-english { letter-spacing:4px; font-size:15px; text-align:right; color:#222; margin-bottom:32px; white-space:nowrap; }
.reference-feature h2 { position:relative; font-size:32px; line-height:1.5; margin-bottom:48px; }
.reference-number { position:absolute; right:-42px; top:12px; font-size:18px; color:#333; }
.reference-description { font-size:15px; line-height:1.85; text-align:justify; color:#666; }
.reference-letter { position:absolute; left:0; top:50%; transform:translate(-50%,-50%); font-family:Arial,sans-serif; font-size:clamp(180px,20vw,310px); font-weight:300; line-height:1; color:var(--gold); z-index:2; pointer-events:none; }
.reference-project-link { display:flex; align-items:center; gap:18px; margin-top:clamp(55px,12vh,130px); font-size:13px; }
.reference-project-link svg { color:#b4b4b4; }
.feature-dark .reference-feature-info { background:#000; color:#fff; }
.feature-dark .reference-english, .feature-dark .reference-description { color:#fff; }
.feature-dark .reference-number { color:#a9a9a9; }
.reference-pagination { position:absolute; z-index:10; right:3.5%; top:50%; transform:translateY(-50%); display:flex; flex-direction:column; }
.reference-pagination button { width:30px; height:24px; display:grid; place-items:center; border:0; background:none; padding:0; }
.reference-pagination span { width:10px; height:10px; border-radius:50%; background:#eee; }
.reference-pagination.on-light span { background:#bdbdbd; }
.reference-pagination button[aria-current] span { background:var(--gold); }
.reference-home.is-stacked > .swiper > .swiper-wrapper { display:block; transform:none!important; }
.reference-home.is-stacked .swiper-slide { height:auto!important; }
.page-heading { height:690px; min-height:0; padding:0; display:grid; place-items:center; text-align:center; background-image:url('/img/subheader/bg.jpg'); background-position:center; color:#222; }
.page-heading h1, .page-heading .eyebrow { color:#222; }
.page-heading .container { display:flex; flex-direction:column; align-items:center; }
.page-heading h1 { font-size:40px; order:0; margin:0 0 12px; }
.page-heading .eyebrow { order:1; margin:0; font-size:18px; letter-spacing:5px; text-align:center; }
.page-heading .breadcrumb, .page-heading p { display:none; }
.reference-services { width:min(1100px,calc(100% - 80px)); margin:-100px auto 110px; position:relative; z-index:2; display:grid; grid-template-columns:1fr 1fr; gap:30px; align-items:start; }
.reference-service-card { background:#fff; padding:60px 48px; box-shadow:0 12px 42px #0000000c; scroll-margin-top:30px; }
.reference-service-card h2 { font-size:21px; }
.reference-service-card .reference-english { text-align:right; }
.reference-service-card .reference-english { white-space:normal; font-size:12px; letter-spacing:4px; margin:0; padding-bottom:20px; border-bottom:1px solid #ddd; }
.reference-service-card .service-card-text { margin-top:28px; }
.reference-service-card p { font-size:15px; line-height:2.1; text-align:justify; }
.reference-about-hero { min-height:100svh; background:url('/img/info/bg.jpg') center/cover; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:150px 20px 90px; text-align:center; }
.reference-about-hero .reference-english { font-size:23px; margin:0 0 25px; text-align:center; }
.reference-about-hero h1 { font-size:32px; line-height:1.5; }
.reference-about-icons { display:flex; direction:ltr; gap:100px; margin-top:65px; }
.reference-about-icons span { width:160px; height:160px; display:grid; place-items:center; border-radius:50%; background:#ef0060; }
.reference-about-icons span:nth-child(2) { background:#efb900; }
.reference-about-icons span:nth-child(3) { background:#096bd9; }
.reference-about-icons img { width:60px; height:80px; object-fit:contain; }
.reference-about-intro { display:grid; grid-template-columns:1fr 1fr; align-items:center; gap:110px; width:min(1100px,calc(100% - 80px)); margin:100px auto; }
.reference-about-intro h2 { font-size:30px; margin-bottom:8px; }
.reference-about-intro .reference-english { text-align:right; }
.reference-about-intro .reference-english { font-size:15px; letter-spacing:4px; margin-bottom:70px; }
.reference-about-intro p { font-size:15px; line-height:2; text-align:justify; }
.reference-about-intro img { width:445px; max-width:100%; justify-self:center; }
.reference-about-values { display:grid; grid-template-columns:1fr 1fr; align-items:start; gap:12%; padding:20px 30px 100px; }
.reference-about-values > img { width:100%; }
.reference-about-values-copy { max-width:330px; }
.reference-about-values h2 { font-size:30px; }
.reference-about-values .reference-english { white-space:normal; font-size:28px; letter-spacing:3px; margin-bottom:30px; }
.reference-about-values p { font-size:13px; }
.reference-progress { margin-top:40px; }
.reference-progress label { display:block; font-size:13px; margin-bottom:12px; }
.reference-progress progress { appearance:none; display:block; height:4px; width:100%; border:0; background:#f3f3f3; }
.reference-progress progress::-webkit-progress-bar { background:#f3f3f3; }
.reference-progress progress::-webkit-progress-value { background:var(--gold); }
.reference-progress progress::-moz-progress-bar { background:var(--gold); }
.reference-faq { max-width:1100px; margin:30px auto 120px; width:calc(100% - 80px); }
.reference-faq > div { width:50%; margin-left:auto; }
.reference-faq h2 { font-size:30px; }
.reference-faq .reference-english { font-size:16px; margin-bottom:35px; }
.reference-faq p { font-size:15px; margin-bottom:24px; }
.reference-contact { max-width:1100px; width:calc(100% - 80px); margin:55px auto 80px; display:grid; grid-template-columns:repeat(3,1fr); gap:65px; }
.reference-contact h2 { font-size:21px; text-align:center; margin-bottom:35px; }
.reference-contact p, .reference-contact a { font-size:17px; color:#666; }
.reference-contact p { margin-bottom:10px; }
.reference-contact .contact-address-latin { font-size:14px; }
.reference-contact-links a { display:block; text-align:center; margin-bottom:16px; overflow-wrap:anywhere; }
.reference-contact-social { display:flex; justify-content:center; gap:24px; }
.reference-contact-social a { display:flex; flex-direction:column; align-items:center; gap:9px; }
.reference-contact-social svg { padding:14px; width:50px; height:50px; background:#eee; border-radius:50%; }
.reference-message { margin:80px auto 110px; padding:0 20px; }
.reference-message h2 { text-align:center; font-size:30px; }
.reference-message .reference-english { text-align:center; margin:5px 0 45px; }
.reference-message .contact-form { width:min(540px,100%); margin:auto; }
.reference-message .form-grid { grid-template-columns:1fr; gap:26px; }
.reference-message .contact-form label { font-size:13px; }
.reference-message .contact-form input, .reference-message .contact-form textarea { background:#fff; border:1px solid #ddd; }
.form-status.success { color:#39744c; }
.form-status.error { color:#a33b35; }
.contact-form button:disabled { opacity:.58; cursor:wait; }
.toast { position:fixed; z-index:80; left:30px; bottom:30px; width:min(420px,calc(100% - 40px)); padding:17px 18px; background:#1f2721; color:#fff; display:flex; align-items:center; gap:12px; box-shadow:0 14px 40px #0004; }
.toast>svg { color:var(--gold); flex:none; }
.toast span { flex:1; font-size:14px; }
.toast button { border:0; background:none; color:#fff; padding:5px; }
.toast button svg { width:18px; height:18px; }
.editorial-list { padding-block:80px 110px; }
.editorial-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:55px 30px; }
.editorial-grid article { min-width:0; }
.editorial-image { aspect-ratio:4/3; overflow:hidden; background:#eeeae1; margin-bottom:20px; }
.editorial-image img { width:100%; height:100%; object-fit:cover; transition:transform .65s ease; }
.editorial-grid a:hover .editorial-image img { transform:scale(1.045); }
.editorial-grid small { color:#9a8055; font-size:11px; }
.editorial-grid h2, .editorial-grid h3 { font-size:22px; margin:8px 0 12px; }
.editorial-grid p { color:#777; font-size:14px; line-height:1.9; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; }
.editorial-grid a>span { display:flex; align-items:center; gap:9px; margin-top:18px; font-size:13px; }
.empty-content { text-align:center; max-width:700px; margin:auto; }
.empty-thumbnail { width:100%; aspect-ratio:16/7; background:linear-gradient(135deg,#ece8df,#f7f5f0); position:relative; }
.empty-thumbnail:after { content:'BLOG'; position:absolute; inset:0; display:grid; place-items:center; color:#c8baa0; font-family:Arial,sans-serif; letter-spacing:14px; font-size:clamp(30px,7vw,70px); }
.empty-content h2 { margin:35px 0 12px; font-size:27px; }
.empty-content p { color:#777; }
.editorial-detail, .portfolio-api-detail { max-width:1050px; padding-block:150px 100px; }
.editorial-detail .text-link, .portfolio-api-detail .text-link { display:inline-block; margin-bottom:50px; }
.editorial-detail h1, .portfolio-api-detail h1 { font-size:clamp(34px,5vw,58px); margin:12px 0 20px; }
.editorial-lead, .portfolio-api-detail header p { color:#777; font-size:18px; max-width:760px; }
.editorial-cover { width:100%; max-height:620px; object-fit:cover; margin:55px 0; }
.rich-content { font-size:16px; line-height:2.2; color:#555; }
.rich-content h2 { color:#222; font-size:28px; margin:45px 0 15px; }
.rich-content img { max-width:100%; height:auto; }
.media-gallery { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:22px; margin:55px 0; }
.media-gallery button { border:0; background:none; overflow:hidden; padding:0; aspect-ratio:4/3; cursor:zoom-in; }
.media-gallery button:first-child { grid-column:1/-1; aspect-ratio:16/9; }
.media-gallery img { width:100%; height:100%; object-fit:cover; transition:transform .6s ease; }
.media-gallery button:hover img { transform:scale(1.08); }
.lightbox { position:fixed; inset:0; z-index:100; background:#080808ed; display:grid; place-items:center; color:#fff; padding:40px 90px; }
.lightbox>img { max-width:100%; max-height:calc(100dvh - 80px); object-fit:contain; }
.lightbox button { position:absolute; border:0; background:#ffffff18; color:#fff; width:48px; height:48px; display:grid; place-items:center; }
.lightbox-close { top:25px; right:25px; }
.lightbox-prev { right:25px; top:50%; }
.lightbox-next { left:25px; top:50%; }
.lightbox>span { position:absolute; bottom:18px; font-family:Arial,sans-serif; }
.related-work { padding-bottom:110px; }
.related-work>h2 { font-size:30px; margin-bottom:35px; }
.reference-message .form-bottom { flex-direction:column; align-items:stretch; }
.reference-message .form-bottom button { align-self:center; background:var(--gold); color:#222; border-radius:4px; }
.site-footer { background:#000; }
.reference-footer-main { padding:140px 0 85px; width:calc(100% - 19%); margin:auto; display:grid; grid-template-columns:1fr 1.8fr; gap:70px; direction:ltr; }
.reference-footer-intro, .reference-footer-details { direction:rtl; }
.reference-footer-intro .brand-wordmark { display:none; }
.reference-footer-intro .brand-logo { width:110px; margin:0 auto 35px; }
.reference-footer-intro p { font-size:18px; line-height:1.65; color:white; text-align:justify; }
.reference-footer-details { display:grid; grid-template-columns:1fr 1fr; gap:50px; }
.reference-footer-details h3 { font-size:20px; margin-bottom:30px; }
.reference-footer-details p, .reference-footer-details a { color:#eee; font-size:15px; }
.reference-footer-details .footer-contact a { font-size:16px; margin-bottom:12px; }
.reference-footer-details .footer-contact .email { font-size:13px; }
.reference-footer-details .footer-offices { display:block; }
.reference-footer-details .footer-offices p { margin-bottom:16px; }
.reference-footer-nav { display:flex; justify-content:center; gap:40px; padding:0 20px 55px; font-size:14px; }
.footer-bottom { background:#111; }
@media (max-width:1200px) {
.reference-feature-info { padding-right:85px; padding-left:70px; }
.reference-feature h2 { font-size:30px; margin-bottom:30px; }
.reference-english { font-size:12px; letter-spacing:3px; }
.reference-description { font-size:14px; }
.reference-project-link { margin-top:55px; }
.reference-footer-main { width:calc(100% - 100px); gap:40px; }
.reference-footer-details { gap:30px; }
.reference-footer-intro p { font-size:16px; }
}
@media (max-width:899px), (max-height:649px) {
.reference-feature { min-height:650px; height:auto; }
.reference-feature-info { padding:90px 55px 90px 60px; }
.reference-letter { font-size:160px; }
.reference-english { white-space:normal; }
.reference-about-intro { gap:45px; }
.reference-about-values { gap:7%; }
.reference-about-icons { gap:50px; }
.reference-about-icons span { width:130px; height:130px; }
.reference-footer-main { grid-template-columns:1fr; padding-top:80px; }
.reference-footer-intro { max-width:650px; }
.reference-contact { gap:30px; }
.editorial-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@media (max-width:650px) {
.header-inner { width:calc(100% - 40px); height:90px; }
.header-inner .brand-logo { width:48px; }
.menu-button { right:20px; top:20px; width:48px; height:48px; }
.account-menu, .header-actions>.account-trigger { right:78px; top:20px; }
.account-trigger { min-width:48px; width:48px; height:48px; padding:0; }
.account-user { width:auto; max-width:180px; padding:0 12px; }
.account-user span { max-width:90px; overflow:hidden; text-overflow:ellipsis; }
.mobile-menu { padding:25px; }
.mobile-menu nav a { font-size:22px; padding:18px 0; }
.reference-hero { min-height:620px; }
.reference-hero h1 { font-size:clamp(30px,7.4vw,42px); }
.reference-hero-copy { transform:translate(-50%,-55%); }
.reference-gold-button { margin-top:38px; }
.reference-discover { right:25px; bottom:35px; gap:22px; font-size:13px; }
.reference-play { width:70px; height:70px; }
.reference-feature { display:flex; flex-direction:column; min-height:0; }
.reference-feature-image { height:52svh; min-height:300px; }
.reference-feature-info { padding:65px 40px 55px; min-height:430px; }
.reference-feature-copy { max-width:none; }
.reference-letter { left:35px; top:0; font-size:115px; transform:translateY(-50%); }
.reference-number { position:static; display:inline-block; margin-left:16px; font-size:14px; }
.reference-feature h2 { display:flex; align-items:start; font-size:28px; margin-bottom:28px; }
.reference-feature .reference-english { margin-bottom:20px; font-size:12px; }
.reference-description { font-size:15px; }
.reference-project-link { margin-top:35px; }
.page-heading { height:430px; }
.page-heading h1 { font-size:32px; }
.page-heading .eyebrow { font-size:14px; }
.reference-services { width:calc(100% - 40px); grid-template-columns:1fr; gap:25px; margin-top:-55px; margin-bottom:65px; }
.reference-service-card { padding:35px 25px; }
.reference-service-card h2 { font-size:22px; }
.reference-about-hero { min-height:700px; padding:140px 20px 90px; }
.reference-about-hero h1 { font-size:28px; }
.reference-about-icons { gap:22px; margin-top:50px; }
.reference-about-icons span { width:80px; height:80px; }
.reference-about-icons img { width:35px; height:45px; }
.reference-about-intro { grid-template-columns:1fr; width:calc(100% - 40px); margin-block:65px; gap:35px; }
.reference-about-intro h2 { font-size:27px; }
.reference-about-intro .reference-english { margin-bottom:30px; font-size:12px; letter-spacing:2px; }
.reference-about-intro img { width:340px; }
.reference-about-values { grid-template-columns:1fr; padding:0 20px 60px; gap:35px; }
.reference-about-values-copy { max-width:none; }
.reference-about-values h2 { font-size:26px; }
.reference-about-values .reference-english { font-size:22px; }
.reference-faq { width:calc(100% - 40px); margin-bottom:70px; }
.reference-faq > div { width:100%; }
.reference-contact { width:calc(100% - 40px); grid-template-columns:1fr; margin-block:50px; gap:45px; }
.reference-contact h2 { margin-bottom:20px; }
.reference-contact p, .reference-contact a { font-size:16px; }
.reference-message { margin-block:60px; }
.reference-message h2 { font-size:25px; }
.reference-footer-main { width:calc(100% - 40px); padding-top:60px; padding-bottom:40px; }
.reference-footer-details { grid-template-columns:1fr; gap:35px; }
.reference-footer-details h3 { font-size:19px; margin-bottom:22px; }
.reference-footer-intro p { font-size:16px; }
.reference-footer-nav { flex-wrap:wrap; gap:18px 25px; padding-bottom:35px; }
.editorial-list, .editorial-detail, .portfolio-api-detail { padding-block:65px 80px; }
.editorial-grid { grid-template-columns:1fr; }
.media-gallery { grid-template-columns:1fr; gap:14px; }
.media-gallery button:first-child { grid-column:auto; }
.lightbox { padding:70px 15px; }
.lightbox-prev { right:10px; }
.lightbox-next { left:10px; }
.toast { left:20px; bottom:20px; }
}
@media (prefers-reduced-motion:no-preference) {
.reference-home .swiper-slide-active .reference-feature-copy { animation:gentle-entry .6s ease both; }
.reference-project-link svg { transition:transform .25s; }
.reference-project-link:hover svg { transform:translateX(-8px); }
}
+20
View File
@@ -0,0 +1,20 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { ChevronDown, LogOut, Settings, UserRound } from 'lucide-react';
import { apiUrl, BUSINESS_URL, CUSTOMER_URL } from '../lib/meshkee-api';
function readCookie(name) {
const entries = Object.fromEntries(document.cookie.split(';').flatMap((part) => { const index=part.indexOf('='); return index<0?[]:[[part.slice(0,index).trim(),part.slice(index+1)]]; }));
if (entries[name]) return decodeURIComponent(entries[name]);
const count = Number(entries[`${name}_n`]);
if (!Number.isFinite(count) || count < 1) return '';
return decodeURIComponent(Array.from({length:count},(_,index)=>entries[`${name}_${index}`]||'').join(''));
}
export default function AccountMenu() {
const [user,setUser]=useState(null); const [open,setOpen]=useState(false); const root=useRef(null);
useEffect(()=>{const token=readCookie('meshkee_customer_access_token');if(!token)return;fetch(apiUrl('/auth/me'),{headers:{Authorization:`Bearer ${token}`,Accept:'application/json'}}).then(response=>response.ok?response.json():null).then(data=>setUser(data?.user||null)).catch(()=>{});},[]);
useEffect(()=>{if(!open)return;const close=(event)=>{if(!root.current?.contains(event.target))setOpen(false)};document.addEventListener('pointerdown',close);return()=>document.removeEventListener('pointerdown',close)},[open]);
const name=user?.firstName||user?.name||user?.displayName; const roles=[user?.role,...(Array.isArray(user?.roles)?user.roles:[])].filter(Boolean).map(String); const admin=Boolean(user?.isAdmin||roles.some(role=>/admin|owner|manager/i.test(role)));
if(!user)return <a className="account-trigger" href={CUSTOMER_URL} aria-label="ورود یا حساب کاربری"><UserRound/></a>;
return <div className="account-menu" ref={root}><button className="account-trigger account-user" onClick={()=>setOpen(value=>!value)} aria-expanded={open}><UserRound/><span>{name||'کاربر'} عزیز</span><ChevronDown size={15}/></button>{open&&<div className="account-dropdown"><a href={CUSTOMER_URL}><UserRound/>پروفایل من</a>{admin&&<a href={BUSINESS_URL}><Settings/>مدیریت وب‌سایت</a>}<a href={`${CUSTOMER_URL}/logout`}><LogOut/>خروج</a></div>}</div>;
}
+4
View File
@@ -0,0 +1,4 @@
import Link from 'next/link';
import { ArrowUpLeft } from 'lucide-react';
export function PageHeading({title,en,description}){return <section className="page-heading"><div className="container"><div className="breadcrumb"><Link href="/">خانه</Link><span>/</span><span>{title}</span></div><span className="eyebrow" dir="ltr">{en}</span><h1>{title}</h1>{description&&<p>{description}</p>}</div></section>}
export function ContactCTA(){return <section className="contact-cta container"><div><span className="eyebrow">از یک ایده شروع کنیم</span><h2>رویای شما، پروژهٔ بعدی ما.</h2></div><Link className="button button-dark" href="/contactus/">گفتگو دربارهٔ پروژه <ArrowUpLeft size={20}/></Link></section>}
+5
View File
@@ -0,0 +1,5 @@
'use client';
import { useState } from 'react';
import { ArrowUpLeft, CheckCircle2, X } from 'lucide-react';
import { apiUrl, SITE_DOMAIN } from '../lib/meshkee-api';
export default function ContactForm(){const [state,setState]=useState({type:'',message:''});const [pending,setPending]=useState(false);async function submit(event){event.preventDefault();const form=event.currentTarget;setPending(true);setState({type:'',message:''});const values=new FormData(form);try{const response=await fetch(apiUrl(`/tenants/${SITE_DOMAIN}/contact-submissions`),{method:'POST',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({title:values.get('subject'),name:values.get('name'),email:values.get('email')||undefined,cellNumber:values.get('phone')||undefined,text:values.get('message')})});const data=await response.json().catch(()=>({}));if(!response.ok)throw new Error(data.message||'ارسال پیام انجام نشد. لطفاً دوباره تلاش کنید.');form.reset();setState({type:'success',message:data.message||'پیام شما با موفقیت ثبت شد. به‌زودی با شما تماس می‌گیریم.'});}catch(error){setState({type:'error',message:error.message||'ارتباط با سرور برقرار نشد. لطفاً دوباره تلاش کنید.'});}finally{setPending(false)}}return <><form className="contact-form" onSubmit={submit}><div className="form-grid"><label>نام و نام خانوادگی<input name="name" required autoComplete="name" placeholder="نام شما"/></label><label>شمارهٔ همراه<input name="phone" type="tel" autoComplete="tel" inputMode="tel" placeholder="۰۹۱۲ ..."/></label><label>ایمیل<input name="email" type="email" autoComplete="email" placeholder="name@example.com" dir="ltr"/></label><label>موضوع<input name="subject" required placeholder="موضوع پروژه یا درخواست شما"/></label></div><label>پیام شما<textarea name="message" required rows={5} placeholder="از ایده‌ها و نیازهای پروژهٔ خود برایمان بنویسید."/></label><div className="form-bottom"><p className="form-note">پیام شما مستقیماً برای گروه معماری ساناز گودرزی ثبت می‌شود.</p><button disabled={pending} type="submit" className="button button-dark">{pending?'در حال ارسال…':'ارسال پیام'} <ArrowUpLeft size={18}/></button></div><p role="status" className={`form-status ${state.type}`}>{state.message}</p></form>{state.type==='success'&&<div className="toast" role="status"><CheckCircle2/><span>{state.message}</span><button onClick={()=>setState({type:'',message:''})} aria-label="بستن پیام"><X/></button></div>}</>}
+10
View File
@@ -0,0 +1,10 @@
'use client';
import { useRef, useState } from 'react';
import Link from 'next/link';
import { Swiper, SwiperSlide } from 'swiper/react';
import { A11y, EffectFade, Keyboard } from 'swiper/modules';
import { ArrowLeft, ArrowRight, ArrowDown, ArrowUpLeft } from 'lucide-react';
import 'swiper/css';
import 'swiper/css/effect-fade';
const slides=[{image:'1',title:<>رویای شما،<br/>تبدیل به <em>واقعیت</em> می‌شود.</>,label:'معماری، در امتداد زندگی',link:'/aboutus/',cta:'آشنایی با گروه'}, {image:'2',title:<>فضایی برای زندگی،<br/><em>به سبک شما.</em></>,label:'طراحی ویلایی و آپارتمانی',link:'/ourservices/#residential',cta:'خدمات طراحی مسکونی'}, {image:'3',title:<>زیبایی،<br/>در تمام <em>جزئیات.</em></>,label:'طراحی تخصصی سالن‌های زیبایی',link:'/ourservices/#beauty',cta:'طراحی سالن زیبایی'}];
export default function Hero(){const swiper=useRef(null);const [active,setActive]=useState(0);return <section className="hero" aria-label="معرفی گروه معماری"><Swiper modules={[A11y,EffectFade,Keyboard]} effect="fade" fadeEffect={{crossFade:true}} speed={650} loop keyboard={{enabled:true,onlyInViewport:true}} onSwiper={s=>swiper.current=s} onSlideChange={s=>setActive(s.realIndex)} a11y={{prevSlideMessage:'اسلاید قبلی',nextSlideMessage:'اسلاید بعدی',slideLabelMessage:'اسلاید {{index}} از {{slidesLength}}'}}>{slides.map((slide,i)=><SwiperSlide key={slide.image}><img className="hero-image" src={`/img/slider/${slide.image}.jpg`} alt={['دکوراسیون داخلی با جزئیات طلایی و دیوار زغالی','طراحی اتاق خواب با نور طبیعی','طراحی داخلی سالن زیبایی'][i]} fetchPriority={i===0?'high':'auto'}/><div className="hero-shade"/><div className="hero-content container"><span className="eyebrow">{slide.label}</span><h1>{slide.title}</h1><p>گروه معماری و دکوراسیون ساناز گودرزی</p><Link tabIndex={active===i?0:-1} className="button button-gold" href={slide.link}>{slide.cta}<ArrowUpLeft size={20}/></Link></div></SwiperSlide>)}</Swiper><div className="hero-bottom container"><a className="scroll-cue" href="#services"><ArrowDown size={18}/> کشف فضاهای تازه</a><div className="slider-controls"><button aria-label="اسلاید قبلی" onClick={()=>swiper.current?.slidePrev()}><ArrowRight size={20}/></button><span className="slide-count" dir="ltr"><b>0{active+1}</b><span>/</span>03</span><button aria-label="اسلاید بعدی" onClick={()=>swiper.current?.slideNext()}><ArrowLeft size={20}/></button></div><span className="hero-signature" dir="ltr">SANAZ GOODARZI — ARCHITECTURE</span></div></section>}
+76
View File
@@ -0,0 +1,76 @@
'use client';
import { useRef, useState, useSyncExternalStore } from 'react';
import Link from 'next/link';
import { Swiper, SwiperSlide } from 'swiper/react';
import { A11y, Keyboard, Mousewheel } from 'swiper/modules';
import { ArrowLeft, Play } from 'lucide-react';
import { services } from '../lib/content';
import 'swiper/css';
const desktopQuery = '(min-width: 900px) and (min-height: 650px)';
function subscribe(callback) {
const query = window.matchMedia(desktopQuery);
query.addEventListener('change', callback);
return () => query.removeEventListener('change', callback);
}
const desktopSnapshot = () => window.matchMedia(desktopQuery).matches;
const serverSnapshot = () => false;
function subscribeMotion(callback) {
const query = window.matchMedia('(prefers-reduced-motion: reduce)');
query.addEventListener('change', callback);
return () => query.removeEventListener('change', callback);
}
const motionSnapshot = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const headings = [['ویلایی', 'و آپارتمانی'], ['سالن‌های', 'زیبایی'], ['اداری', 'و تجاری'], ['طراحی', 'فضای سبز'], ['طراحی', 'نما']];
const letters = ['V', 'B', 'O', 'L', 'F'];
const descriptions = [
'مؤثرترین عامل در طراحی خانه ویلایی، پلان معماری ویلا می‌باشد که باید به دست یک مهندس معمار خلاق و خوش‌ذوق صورت بگیرد؛ فردی که علاوه بر زیبایی به فکر مسائل اجرایی در طراحی ویلا نیز باشد. توجه به اقلیم در طراحی خانه ویلایی بسیار حائز اهمیت می‌باشد.',
'طراحی دکوراسیون داخلی سالن زیبایی نقش مهمی در انجام صحیح کارها و بهبود شرایط داخلی سالن زیبایی دارد. یکی از نکات مهم و قابل توجه برای طراحی دکوراسیون داخلی سالن‌ها، توجه به نیازها و قوانین کلی چیدمان این فضا می‌باشد.',
'یکی از مهم‌ترین نکات در طراحی داخلی ساختمان اداری توجه به افرادی است که در آن ساختمان مشغول به کار هستند و ساعت‌ها از روز خود را در آن محل سپری می‌کنند؛ به همین دلیل ایجاد یک فضا با ایده‌های خلاقانه باعث نشاط کارکنان و افزایش راندمان کاری شرکت می‌شود و شرکت‌های بزرگ و خلاق با آماده کردن محیط‌های الهام‌بخش به دنبال بهرهٔ بهینه از سرمایه‌های انسانی خود هستند.',
'فضای سبز بخشی از فضای باز محیط آپارتمان یا شهر است که عرصه‌های طبیعی یا مصنوعی آن، تحت استقرار درختچه‌ها، درختان، گل‌ها، چمن‌ها و سایر گیاهانی است که بر اساس نظارت و مدیریت انسان، با در نظر گرفتن ضوابط و قوانین برای بهبود شرایط زیستی و رفاهی شهروندان و مراکز جمعیتی غیرروستایی حفظ، نگهداری و احداث می‌شود.',
'طراحی نمای ساختمان از این رو که اولین آیتم از منظر بیننده می‌باشد از اهمیت ویژه‌ای در بحث معماری ساختمان برخوردار است. در طراحی نمای ساختمان بایستی المان‌های مختلفی نظیر اقلیم منطقه، سبک معماری منطقه، معماری ساختمان‌های مجاور، ضوابط شهری، شرایط جغرافیایی و فرهنگی در نظر گرفته شود.'
];
export default function HomeExperience() {
const desktop = useSyncExternalStore(subscribe, desktopSnapshot, serverSnapshot);
const reducedMotion = useSyncExternalStore(subscribeMotion, motionSnapshot, serverSnapshot);
const swiper = useRef(null);
const [active, setActive] = useState(0);
const goTo = index => {
if (desktop) swiper.current?.slideTo(index);
else document.getElementById(`home-panel-${index}`)?.scrollIntoView({behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth'});
};
return <div className={`reference-home ${desktop ? 'is-desktop' : 'is-stacked'}`}>
<Swiper key={desktop ? 'desktop' : 'stacked'} direction="vertical" modules={[A11y, Keyboard, Mousewheel]} enabled={desktop}
speed={reducedMotion ? 0 : 650} mousewheel={{enabled:desktop, forceToAxis: true, releaseOnEdges: true, thresholdDelta: 20, thresholdTime: 650}}
keyboard={{enabled: desktop, onlyInViewport: true}} onSwiper={instance => {swiper.current = instance;}}
onSlideChange={instance => setActive(instance.activeIndex)}
a11y={{slideLabelMessage:'بخش {{index}} از {{slidesLength}}',nextSlideMessage:'بخش بعدی',prevSlideMessage:'بخش قبلی'}}>
<SwiperSlide>
<section id="home-panel-0" className="reference-hero" inert={desktop && active !== 0}>
<img src="/img/slider/1.jpg" className="reference-hero-image" alt="طراحی داخلی با دیوار زغالی و جزئیات طلایی" fetchPriority="high"/>
<div className="reference-hero-copy"><h1>رویای شما تبدیل به<br/>واقعیت می‌شود</h1><Link href="/aboutus/" className="reference-gold-button">بیشتر بدانید</Link></div>
<button type="button" className="reference-discover" onClick={() => goTo(1)} aria-label="طراحی خاص؛ مشاهدهٔ خدمات"><span>طراحی خاص</span><span className="reference-play"><Play fill="currentColor" size={30}/></span></button>
</section>
</SwiperSlide>
{services.map((service, index) => <SwiperSlide key={service.id}>
<section id={`home-panel-${index+1}`} className={`reference-feature ${index % 2 ? 'feature-dark' : ''}`} inert={desktop && active !== index+1}>
<div className="reference-feature-image"><img src={service.image} alt={service.title} loading="lazy"/></div>
<div className="reference-feature-info">
<span className="reference-letter" aria-hidden="true" dir="ltr">{letters[index]}</span>
<div className="reference-feature-copy"><p className="reference-english" dir="ltr">{service.en}</p>
<h2><span className="reference-number" dir="ltr">0{index+1}</span><Link href={`/ourservices/#${service.id}`}>{headings[index][0]}<br/>{headings[index][1]}</Link></h2>
<p className="reference-description">{descriptions[index]}</p>
<Link className="reference-project-link" href={`/portfolios/?category=${service.id}`}>مشاهده نمونه‌کارها <ArrowLeft size={68} strokeWidth={.7}/></Link>
</div>
</div>
</section>
</SwiperSlide>)}
</Swiper>
{desktop && <nav className={`reference-pagination ${active > 0 && active % 2 ? 'on-light' : ''}`} aria-label="بخش‌های صفحهٔ اصلی">
{['خانه',...services.map(service=>service.title)].map((title,index)=><button key={title} type="button" aria-label={title} aria-current={active===index?'step':undefined} onClick={()=>goTo(index)}><span/></button>)}
</nav>}
</div>;
}
+29
View File
@@ -0,0 +1,29 @@
'use client';
import { useEffect, useState } from 'react';
import { X, ChevronLeft, ChevronRight } from 'lucide-react';
export default function MediaGallery({ images, title }) {
const [active, setActive] = useState(null);
useEffect(() => {
if (active === null) return;
const onKey = (event) => {
if (event.key === 'Escape') setActive(null);
if (event.key === 'ArrowLeft') setActive((active + 1) % images.length);
if (event.key === 'ArrowRight') setActive((active - 1 + images.length) % images.length);
};
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', onKey);
return () => { document.body.style.overflow = ''; window.removeEventListener('keydown', onKey); };
}, [active, images.length]);
if (!images.length) return null;
return <>
<div className="media-gallery">{images.map((image, index) => <button key={image.id} type="button" onClick={() => setActive(index)} aria-label={`نمایش تصویر ${index + 1} از ${title}`}><img src={image.url} alt={image.alt || `${title}، تصویر ${index + 1}`} loading={index ? 'lazy' : 'eager'}/></button>)}</div>
{active !== null && <div className="lightbox" role="dialog" aria-modal="true" aria-label={`گالری ${title}`} onClick={(event) => { if (event.target === event.currentTarget) setActive(null); }}>
<button className="lightbox-close" onClick={() => setActive(null)} aria-label="بستن گالری"><X/></button>
{images.length > 1 && <button className="lightbox-prev" onClick={() => setActive((active - 1 + images.length) % images.length)} aria-label="تصویر قبلی"><ChevronRight/></button>}
<img src={images[active].url} alt={images[active].alt || title}/>
{images.length > 1 && <button className="lightbox-next" onClick={() => setActive((active + 1) % images.length)} aria-label="تصویر بعدی"><ChevronLeft/></button>}
<span>{active + 1} / {images.length}</span>
</div>}
</>;
}
+61
View File
@@ -0,0 +1,61 @@
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
const targets = '.reference-service-card, .reference-about-intro, .reference-about-values, .reference-faq, .reference-contact > div, .section-heading, .service-row, .service-detail, .about-band-grid, .contact-cta, .about-intro, .values-grid > div, .faq-section, .contact-info > div, .offices-grid > article, .contact-form, .portfolio-grid > article, .portfolio-detail';
export default function PageMotion() {
const pathname = usePathname();
useEffect(() => {
const main = document.getElementById('main');
if (!main || !('IntersectionObserver' in window)) return;
const preference = window.matchMedia('(prefers-reduced-motion: reduce)');
const seen = new WeakSet();
const prepared = new Set();
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
observer.unobserve(entry.target);
entry.target.dataset.scrollReveal = 'visible';
}
}, { threshold: 0, rootMargin: '0px 0px 48px 0px' });
const prepare = element => {
if (seen.has(element)) return;
seen.add(element);
// Never hide already-visible content, restored scroll positions, or filtered cards.
if (preference.matches || element.getBoundingClientRect().top < window.innerHeight) return;
element.dataset.scrollReveal = 'pending';
prepared.add(element);
observer.observe(element);
};
main.querySelectorAll(targets).forEach(prepare);
// Inspect added subtrees only; do not rescan the page on every Swiper update.
const mutations = new MutationObserver(records => {
for (const record of records) {
for (const node of record.addedNodes) {
if (!(node instanceof Element)) continue;
if (node.matches(targets)) prepare(node);
node.querySelectorAll(targets).forEach(prepare);
}
}
for (const element of prepared) {
if (!element.isConnected) {
observer.unobserve(element);
prepared.delete(element);
}
}
});
mutations.observe(main, { childList: true, subtree: true });
const revealAll = () => {
observer.disconnect();
prepared.forEach(element => element.removeAttribute('data-scroll-reveal'));
prepared.clear();
};
const onPreferenceChange = () => { if (preference.matches) revealAll(); };
preference.addEventListener('change', onPreferenceChange);
return () => {
mutations.disconnect();
revealAll();
preference.removeEventListener('change', onPreferenceChange);
};
}, [pathname]);
return null;
}
+7
View File
@@ -0,0 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { ArrowUpLeft } from 'lucide-react';
import { services } from '../lib/content';
import { faSlug } from '../lib/meshkee-api';
export default function PortfolioGrid({items}){const hasReal=items.some(i=>!i.isPlaceholder);const [category,setCategory]=useState('all');useEffect(()=>{const sync=()=>{const value=new URLSearchParams(window.location.search).get('category');setCategory(services.some(s=>s.id===value)?value:'all')};sync();window.addEventListener('popstate',sync);return()=>window.removeEventListener('popstate',sync)},[]);function select(id){setCategory(id);const url=new URL(window.location.href);if(id==='all')url.searchParams.delete('category');else url.searchParams.set('category',id);window.history.pushState({},'',url)}const filtered=hasReal?items:items.filter(i=>category==='all'||i.category===category);return <>{!hasReal&&<div className="category-tabs" role="group" aria-label="فیلتر نمونه‌کارها">{[{id:'all',title:'همهٔ حوزه‌ها'},...services].map(s=><button key={s.id} aria-pressed={category===s.id} onClick={()=>select(s.id)}>{s.title}</button>)}</div>}{!hasReal&&<p className="portfolio-note">هنوز نمونه‌کاری در API منتشر نشده است؛ تصاویر زیر پیش‌نمایش حوزه‌های طراحی هستند.</p>}<div className="portfolio-grid">{filtered.map(item=><article key={item.id}><Link href={item.isPlaceholder?`/portfolios/${item.slug}/`:`/portfolios/${item.id}/${faSlug(item.title)}/`}><div className="portfolio-image"><img src={item.image} alt={item.isPlaceholder?`پیش‌نمایش ${item.title}`:item.title} loading="lazy"/>{item.isPlaceholder&&<span>پیش‌نمایش</span>}</div><div className="portfolio-card-title"><div><small>{item.categoryName||'گروه معماری ساناز گودرزی'}</small><h2>{item.title}</h2></div><ArrowUpLeft size={23}/></div></Link></article>)}</div><span role="status" className="sr-only">{filtered.length} مورد نمایش داده می‌شود</span></>}
+46
View File
@@ -0,0 +1,46 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { Menu, X, ArrowUpLeft, Instagram, Mail, ArrowUp } from 'lucide-react';
import { navigation, offices, phones, emails, instagram } from '../lib/content';
import AccountMenu from './account-menu';
export function Brand({branding={},dark=false}) {const src=(dark?branding.logoDarkUrl:branding.logoUrl)||branding.logoUrl||'/Logo.png';return <span className="brand"><img className="brand-logo" src={src} alt="لوگوی ساناز گودرزی" width="462" height="473"/><span className="brand-wordmark"><span className="brand-name">ساناز گودرزی<span className="brand-dot">.</span></span><span className="brand-caption" dir="ltr">ARCHITECTURE & INTERIOR DESIGN</span></span></span>}
export function Header({branding={}}) {
const path=usePathname(); const [open,setOpen]=useState(false); const dialog=useRef(null); const trigger=useRef(null);
useEffect(() => {
const panel = dialog.current;
if (!open && !panel.open) return;
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
if (open && !panel.open) panel.showModal();
panel.dataset.phase = open ? 'opening' : 'closing';
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const animation = panel.animate(
open
? [{ transform: 'translateX(100%)', opacity: .4 }, { transform: 'translateX(0)', opacity: 1 }]
: [{ transform: 'translateX(0)', opacity: 1 }, { transform: 'translateX(100%)', opacity: 0 }],
{ duration: reduced ? 0 : open ? 440 : 260, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'both' }
);
animation.finished.then(() => {
if (!open) {
panel.close();
document.body.style.overflow = previousOverflow;
trigger.current?.focus();
}
}).catch(() => {});
return () => {
animation.cancel();
document.body.style.overflow = previousOverflow;
};
}, [open]);
return <><header className={`site-header ${path==='/'?'over-hero':''}`}><div className="header-inner"><Link href="/" aria-label="گروه معماری ساناز گودرزی، صفحهٔ اصلی"><Brand branding={branding}/></Link><nav className="desktop-nav" aria-label="ناوبری اصلی">{navigation.map(([href,label])=><Link key={href} href={href} aria-current={path===href?'page':undefined}>{label}</Link>)}</nav><div className="header-actions"><AccountMenu/><button ref={trigger} className="menu-button" onClick={()=>setOpen(true)} aria-label="باز کردن منو" aria-expanded={open} aria-controls="mobile-menu"><Menu/></button></div></div></header><dialog id="mobile-menu" className="mobile-menu" ref={dialog} onCancel={event=>{event.preventDefault();setOpen(false)}} onClick={e=>{if(e.target===dialog.current)setOpen(false)}}><div className="menu-top"><Brand branding={branding} dark/><button className="icon-button" aria-label="بستن منو" onClick={()=>setOpen(false)}><X/></button></div><nav aria-label="منوی موبایل">{navigation.map(([href,label],i)=><Link onClick={()=>setOpen(false)} key={href} href={href} aria-current={path===href?'page':undefined}><small>{String(i+1).padStart(2,'0')}</small>{label}<ArrowUpLeft/></Link>)}</nav><a href={`tel:${phones[0].value}`} className="menu-phone" dir="ltr">{phones[0].label}</a></dialog></>;
}
export function Footer({branding={}}) {
const pathname = usePathname();
if (pathname === '/') return null;
return <footer className="site-footer"><div className="reference-footer-main">
<div className="reference-footer-intro"><Link href="/"><Brand branding={branding} dark/></Link><p>این مجموعه در راستای رسیدن به اهداف بزرگ خود در حال وسعت بخشیدن به آتلیه‌های معماری خود می‌باشد.<br/>و در همین راستا در حال تلاش برای طراحی و ساخت بناهای عظیم معماری در این مرزوبوم و خارج از آن می‌باشد و با برنامه‌ریزی و اجرای طرح‌واره‌های هدفمند گام بزرگی را در این مسیر نهاده که به‌زودی همگان شاهد آن خواهیم بود.</p></div>
<div className="reference-footer-details"><div className="footer-contact"><h3>با ما در ارتباط باشید</h3>{phones.map(phone=><a key={phone.value} href={`tel:${phone.value}`} dir="ltr">{phone.label}</a>)}{emails.map(email=><a className="email" key={email} href={`mailto:${email}`} dir="ltr">{email}</a>)}<div className="social-links"><a href={instagram} target="_blank" rel="noreferrer" aria-label="اینستاگرام ساناز گودرزی"><Instagram size={20}/></a><a href={`mailto:${emails[0]}`} aria-label="ارسال ایمیل"><Mail size={20}/></a></div></div>
<div className="footer-offices"><h3>نشانی ما</h3>{offices.map(office=><p key={office.city}>{office.city} — {office.address}{office.latin&&<span dir="ltr">{office.latin}</span>}</p>)}</div></div>
</div><nav className="reference-footer-nav" aria-label="پیوندهای پایین صفحه">{navigation.map(([href,label])=><Link key={href} href={href}>{label}</Link>)}</nav><div className="footer-bottom"><div className="container footer-bottom-bar"><span>گروه معماری ساناز گودرزی</span><a href="https://meshkee.com" target="_blank" rel="noopener noreferrer" dir="ltr" className="meshkee-credit"><span className="meshkee-credit__frame" aria-hidden="true"><i/><i/><i/><i/></span><img src="/meshkee-logo.png" alt="Meshkee" width="42" height="42"/><span className="meshkee-credit__text"><span>Designed And Developed By</span><strong>Meshkee E-Commerce Team</strong></span></a><a className="back-top" href="#main" aria-label="بازگشت به بالای صفحه"><ArrowUp size={18}/></a></div></div></footer>}
+3
View File
@@ -0,0 +1,3 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
export default defineConfig([...nextVitals, globalIgnores(['out/**', '.next/**', '.npm-cache/**']), {rules: {'@next/next/no-img-element': 'off'}}]);
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 751 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

+28
View File
@@ -0,0 +1,28 @@
export const navigation = [ ['/', 'خانه'], ['/portfolios/', 'نمونه‌کارها'], ['/blog/', 'بلاگ'], ['/ourservices/', 'خدمات ما'], ['/aboutus/', 'دربارهٔ ما'], ['/contactus/', 'تماس با ما'] ];
export const services = [
{id:'residential', title:'ویلایی و آپارتمانی', en:'Villas & Apartments', image:'/img/slider/2.jpg', description:'مؤثرترین عامل در طراحی خانهٔ ویلایی، پلان معماری ویلا است؛ طرحی به دست یک معمار خلاق و خوش‌ذوق که در کنار زیبایی، به مسائل اجرایی و اقلیم توجه داشته باشد.', details:['موقعیت قرارگیری، ابعاد و نوع بازشوها تأثیر بسزایی در تهویه و نورگیری فضای داخلی ویلا دارد. دو پنجرهٔ روبه‌رو با فاصلهٔ مناسب، امکان گردش هوا و تهویهٔ فضای میان آن‌ها را فراهم می‌کنند.','با به‌کارگیری اصول چیدمان و همراهی طراحان متخصص، می‌توان دکوراسیونی کاربردی و زیبا در بخش‌های مختلف خانهٔ ویلایی یا آپارتمانی ایجاد کرد.']},
{id:'beauty', title:'سالن‌های زیبایی', en:'Beauty Salons', image:'/img/slider/3.jpg', description:'طراحی دکوراسیون داخلی سالن زیبایی نقش مهمی در انجام صحیح کارها و بهبود شرایط داخلی سالن دارد. توجه به نیازها و اصول چیدمان این فضا، نقطهٔ شروع یک طراحی مناسب است.',details:['انتخاب دکور مناسب و شیک در جذب و رضایت مشتری اهمیت زیادی دارد. دکوراسیون باید در کنار پاسخ‌گویی به نیازهای کارکنان، مورد پسند مشتری نیز باشد.','آشنایی با انواع دکوراسیون سالن زیبایی به شما کمک می‌کند فضای فعلی را ارتقا دهید یا برای تأسیس سالن جدید از ایده‌های مناسب بهره بگیرید.']},
{id:'commercial', title:'اداری و تجاری', en:'Office And Commercial', image:'/img/slider/4.jpg', description:'در طراحی داخلی ساختمان اداری، توجه به افرادی که ساعت‌های روز خود را در آن سپری می‌کنند اهمیت دارد. فضای خلاقانه و الهام‌بخش می‌تواند به نشاط کارکنان و افزایش کیفیت کار کمک کند.',details:['هدف از طراحی داخلی واحد اداری تنها زیبایی محیط نیست؛ توجه به عملکردهای ضروری ساختمان و تأمین نیازهای کارفرما نیز اهمیت بسیار زیادی دارد.','محیط کار جذاب، کارکنان را به همکاری و فعالیت ترغیب می‌کند و زمینه را برای ارائهٔ کاری با کیفیت بالاتر فراهم می‌سازد.']},
{id:'landscape', title:'طراحی فضای سبز', en:'Landscape Designer', image:'/img/slider/5.jpg', description:'فضای سبز بخشی از فضای باز محیط ساختمان یا شهر است که با درختان، درختچه‌ها، گل‌ها و سایر گیاهان شکل می‌گیرد؛ فضایی برای بهبود شرایط زیستی و رفاه ساکنان.',details:['طراحی، احداث و نگهداری این فضا با در نظر گرفتن ضوابط، شرایط محیطی و نیازهای استفاده‌کنندگان انجام می‌شود.']},
{id:'facade', title:'طراحی نما', en:'Facade Design', image:'/img/slider/6.jpg', description:'نمای ساختمان اولین تصویر از معماری آن است. در طراحی نما، اقلیم، سبک معماری منطقه، ساختمان‌های مجاور، ضوابط شهری و شرایط جغرافیایی و فرهنگی را در نظر می‌گیریم.',details:['نمای مدرن با ریتم‌های عمودی، افقی و زاویه‌دار، مصالح نوین و فناوری‌های جدید، به ساختمان پویایی می‌بخشد.','در نمای کلاسیک یا رومی، تقارن یکی از اصول اصلی طراحی است. نمای نئوکلاسیک با الهام از معماری روم و یونان باستان، رنسانس و باروک، برداشت تازه‌ای از این سبک ارائه می‌کند.']}
];
export const offices = [
{city:'تهران', address:'سعادت‌آباد، خیابان سی‌وهفتم، پلاک ۴'},
{city:'کیش', address:'شهرک صدف، فاز یک، عرفان سوم، پلاک ۱۲۳، واحد ۳'},
{city:'قم', address:'میدان ارتش، بلوار غدیر، کوچهٔ یازدهم، ساختمان ایران‌زمین، طبقهٔ ۴'},
{city:'ازمیر', address:'ترکیه، شهر ازمیر', latin:'Turkey — sok 8291, d12, Çiğil, Ataşehir mah, Izmir'}
];
export const phones = [{label:'۰۹۱۲ ۲۵۲ ۶۳۶۰',value:'+989122526360'},{label:'۰۲۵ ۳۲۸۵ ۰۶۹۰',value:'+982532850690'},{label:'۰۲۵ ۳۲۸۵ ۰۲۰۵',value:'+982532850205'}];
export const emails = ['goodarzi.sanaz@gmail.com','supercoyest@yahoo.com'];
export const instagram = 'https://www.instagram.com/sanaz.goodarzi_arch/';
export const aboutParagraphs = [
'زمینهٔ فعالیت گروه معماری و دکوراسیون ساناز گودرزی، طراحی تخصصی، نظارت و اجرای کامل پروژه، طراحی دکوراسیون داخلی، طراحی تخصصی سالن‌های زیبایی، پروژه‌های لوکس مسکونی، اداری و تجاری و مراکز مؤسسات آموزش و پژوهش است.',
'مدیریت این مجموعه را مهندس ساناز گودرزی بر عهده دارد. ایشان تحصیلات معماری خود را تا مقطع کارشناسی ارشد مهندسی معماری در دانشگاه تهران به انجام رسانده و به‌عنوان بنیان‌گذار و طراح اصلی مجموعه فعالیت می‌کند. مقالات علمی ایشان در زمینهٔ فضای معماری در سطح بین‌المللی منتخب شده و در مسیر چاپ در مجلات و ژورنال‌های معتبر بین‌المللی قرار دارد.',
'طراحان، ایده‌پردازان و مجریان پروژه‌های این مجموعه از فارغ‌التحصیلان دانشگاه تهران و اساتید مجرب دانشگاه هستند. طراحی و اجرای پروژه‌های مسکونی، سالن‌های زیبایی، مؤسسهٔ آموزش عالی، فضاهای اداری، طراحی داخلی و تجهیز هتل در کارنامهٔ این گروه قرار دارد.',
'این مجموعه در راستای رسیدن به اهداف خود در حال گسترش آتلیه‌های معماری و فعالیت در طراحی و ساخت بناهای معماری در ایران و خارج از کشور است.'
];
export const faqs = [
['اطلاعات در مورد مشاوره','مشاورهٔ مجموعه به‌صورت آنلاین و حضوری انجام می‌شود. هزینهٔ مشاوره متناسب با هر پروژه متفاوت است و جلسات به‌صورت تخصصی با طراحان اصلی مجموعه برگزار می‌شوند.'],
['رسیدگی به موارد اضطراری','همکاران گروه با بررسی و تحلیل شرایط پروژه، نحوهٔ مناسب پاسخ‌گویی را انتخاب می‌کنند. خواسته‌های کارفرما و پیگیری مسائل تا رسیدن به نتیجهٔ رضایت‌بخش در اولویت قرار دارد.'],
['اطلاعات در مورد پرسنل','پرسنل طراحی دارای مدارک کارشناسی ارشد معماری هستند و مهندسان عمران، سازه و تأسیسات نیز مجموعه را همراهی می‌کنند. روزهای کاری آتلیه‌ها شنبه تا پنجشنبه، ساعت ۸ صبح تا ۵ عصر است. احترام به مراجعان و تلاش برای تأمین خواسته‌های آنان از اصول مجموعه است.']
];
+73
View File
@@ -0,0 +1,73 @@
const API_BASE = 'https://api.meshkee.com/api/v1';
export const SITE_DOMAIN = 'sanazgoodarzi.ir';
export const CUSTOMER_URL = `https://customer.${SITE_DOMAIN}`;
export const BUSINESS_URL = `https://business.${SITE_DOMAIN}`;
async function request(path, options = {}) {
try {
const response = await fetch(`${API_BASE}${path}`, {
...options,
headers: { Accept: 'application/json', ...options.headers },
cache: options.cache ?? 'no-store',
signal: options.signal ?? AbortSignal.timeout(8000),
});
if (!response.ok) return null;
return await response.json();
} catch {
return null;
}
}
export function apiUrl(path) { return `${API_BASE}${path}`; }
export async function getBranding() {
return (await request(`/tenants/${SITE_DOMAIN}/website/favicon`, { cache: 'force-cache' })) ?? {};
}
export async function getBusinessInfo() {
return (await request(`/tenants/${SITE_DOMAIN}/website/business-info`, { cache: 'force-cache' })) ?? {};
}
export async function getBlogs() {
const data = await request(`/tenants/${SITE_DOMAIN}/blogs?page=1&pageSize=100`);
return Array.isArray(data?.items) ? data.items : [];
}
export async function getBlog(slug) {
const data = await request(`/tenants/${SITE_DOMAIN}/blogs/${encodeURIComponent(slug)}`);
return data?.blog ?? null;
}
export async function getBlogById(id) {
const data = await request(`/tenants/${SITE_DOMAIN}/blogs/by-id/${encodeURIComponent(id)}`);
return data?.blog ?? null;
}
export async function getPortfolios() {
const data = await request(`/tenants/${SITE_DOMAIN}/portfolios?page=1&pageSize=100`);
return Array.isArray(data?.items) ? data.items : [];
}
export async function getPortfolio(slug) {
const data = await request(`/tenants/${SITE_DOMAIN}/portfolios/${encodeURIComponent(slug)}`);
return data?.portfolio ?? null;
}
export async function getPortfolioById(id) {
const data = await request(`/tenants/${SITE_DOMAIN}/portfolios/by-id/${encodeURIComponent(id)}`);
return data?.portfolio ?? null;
}
export function titleOf(item, fallback = '') {
return item?.titleFa || item?.title || item?.titleEn || fallback;
}
export function imageUrlOf(item) {
return item?.titleImageUrl || item?.imageUrl || item?.coverImageUrl || item?.thumbnailUrl || '';
}
export function galleryOf(item) {
const values = Array.isArray(item?.gallery) ? item.gallery : [];
return values.map((entry, index) => ({
id: entry?.id ?? index,
url: typeof entry === 'string' ? entry : entry?.url || entry?.imageUrl || entry?.fileUrl,
alt: typeof entry === 'string' ? titleOf(item) : entry?.alt || entry?.titleFa || titleOf(item),
})).filter((entry) => entry.url);
}
export function plainText(value = '') {
return value.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
}
export function faSlug(value = '') {
return plainText(value).normalize('NFKC').replace(/[^\p{L}\p{N}\s-]/gu, '').trim().replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, '') || 'بدون-عنوان';
}
+20
View File
@@ -0,0 +1,20 @@
import { services } from './content';
import { getPortfolios, imageUrlOf, titleOf } from './meshkee-api';
// Adapter boundary: replace these explicit placeholders with the supplied API contract.
// Images are category thumbnails from the original site, not verified individual projects.
export const portfolioItems = services.map((service, index) => ({
id: `preview-${index + 1}`, slug: service.id, category: service.id,
title: service.title, image: service.image, isPlaceholder: true,
description: 'اطلاعات و تصاویر اختصاصی پروژه‌های این بخش به‌زودی تکمیل می‌شود.'
}));
export async function getPortfolioItems() {
const remote = await getPortfolios();
if (!remote.length) return portfolioItems;
return remote.map((item) => ({
...item,
title: titleOf(item, 'نمونه‌کار معماری'),
image: imageUrlOf(item) || '/img/subheader/bg.jpg',
category: item.categoryId || item.categoryName || 'all',
isPlaceholder: false,
}));
}
+2
View File
@@ -0,0 +1,2 @@
const nextConfig = { output: 'export', trailingSlash: true, images: { unoptimized: true }, devIndicators: false };
export default nextConfig;
+6137
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
{
"name": "sanaz-goodarzi",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "node scripts/next.mjs dev --hostname 0.0.0.0",
"build": "node scripts/next.mjs build",
"start": "node scripts/serve.mjs",
"lint": "node scripts/lint.mjs"
},
"dependencies": {
"@fontsource/vazirmatn": "^5.3.0",
"lucide-react": "^0.577.0",
"next": "^16.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"swiper": "^12.0.0"
},
"devDependencies": {
"eslint": "^9.0.0",
"eslint-config-next": "^16.0.0"
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.
Binary file not shown.
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+30
View File
@@ -0,0 +1,30 @@
{
"baseUrl": "https://sanazgoodarzi.ir",
"staticPages": [
{
"path": "/",
"changefreq": "weekly",
"priority": 1
},
{
"path": "/aboutus",
"priority": 0.7
},
{
"path": "/blog",
"priority": 0.8
},
{
"path": "/contactus",
"priority": 0.6
},
{
"path": "/ourservices",
"priority": 0.8
},
{
"path": "/portfolios",
"priority": 0.9
}
]
}
+32
View File
@@ -0,0 +1,32 @@
import { mkdir, readdir, writeFile } from 'node:fs/promises';
import { dirname, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const appRoot = join(projectRoot, 'app');
const outputFile = join(projectRoot, 'public', 'meshkee', 'sitemap-config.json');
const pageNames = new Set(['page.js','page.jsx','page.ts','page.tsx']);
async function findPages(directory) {
const entries = await readdir(directory,{withFileTypes:true});
const pages=[];
for(const entry of entries){
const full=join(directory,entry.name);
if(entry.isDirectory()) pages.push(...await findPages(full));
else if(pageNames.has(entry.name)) pages.push(full);
}
return pages;
}
function toRoute(file) {
const segments=relative(appRoot,dirname(file)).split(sep).filter(Boolean);
if(segments.some(segment=>segment.startsWith('[')||segment.startsWith('(')||segment==='api'))return null;
return segments.length?`/${segments.join('/')}`:'/';
}
const priorities={ '/':1, '/portfolios':.9, '/ourservices':.8, '/blog':.8, '/aboutus':.7, '/contactus':.6 };
const routes=(await findPages(appRoot)).map(toRoute).filter(Boolean).sort((a,b)=>a==='/'?-1:a.localeCompare(b));
const staticPages=[...new Set(routes)].map(path=>({path,...(path==='/'?{changefreq:'weekly'}:{}),priority:priorities[path]??.5}));
await mkdir(dirname(outputFile),{recursive:true});
await writeFile(outputFile,`${JSON.stringify({baseUrl:'https://sanazgoodarzi.ir',staticPages},null,2)}\n`,'utf8');
console.log(`Generated Meshkee sitemap config with ${staticPages.length} static routes.`);
+5
View File
@@ -0,0 +1,5 @@
import { spawn } from 'node:child_process';
import { nodeRuntime } from './runtime.mjs';
const child = spawn(nodeRuntime(), ['node_modules/eslint/bin/eslint.js', '.'], {stdio:'inherit'});
child.on('error', error => {console.error(error.message);process.exit(1)});
child.on('exit', code => process.exit(code ?? 1));
+6
View File
@@ -0,0 +1,6 @@
import { spawn } from 'node:child_process';
import { nodeRuntime } from './runtime.mjs';
if (process.argv[2] === 'build') await import('./generate-meshkee-config.mjs');
const child = spawn(nodeRuntime(), ['node_modules/next/dist/bin/next', ...process.argv.slice(2)], { stdio: 'inherit', env: process.env });
child.on('error', error => { console.error(error.message); process.exit(1); });
child.on('exit', code => process.exit(code ?? 1));
+13
View File
@@ -0,0 +1,13 @@
import { existsSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
// Laragon may expose Node 18. Prefer the current runtime, with the desktop's
// already-installed Node as a local development fallback; never download one.
export function nodeRuntime() {
const [major, minor] = process.versions.node.split('.').map(Number);
if (major > 20 || (major === 20 && minor >= 9)) return process.execPath;
const bundled = join(homedir(), '.cache', 'codex-runtimes', 'codex-primary-runtime', 'dependencies', 'node', 'bin', 'node.exe');
if (process.platform === 'win32' && existsSync(bundled)) return bundled;
throw new Error('Next.js requires Node.js 20.9 or newer. Use Node.js 24 LTS.');
}
+21
View File
@@ -0,0 +1,21 @@
import { createServer } from 'node:http';
import { createReadStream, existsSync } from 'node:fs';
import { stat } from 'node:fs/promises';
import { resolve, sep, extname } from 'node:path';
const root=resolve('out');
if(!existsSync(root)){console.error('Run npm run build before npm start.');process.exit(1)}
const types={'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.json':'application/json','.txt':'text/plain; charset=utf-8','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.svg':'image/svg+xml','.woff':'font/woff','.woff2':'font/woff2','.ico':'image/x-icon'};
createServer(async(req,res)=>{
if(!['GET','HEAD'].includes(req.method)){res.writeHead(405,{Allow:'GET, HEAD'});res.end();return}
try{
const pathname=decodeURIComponent(new URL(req.url,'http://localhost').pathname);
let file=resolve(root,`.${pathname}`);
if(file!==root&&!file.startsWith(root+sep)){res.writeHead(403);res.end();return}
let info=await stat(file).catch(()=>null);
if(info?.isDirectory()){file=resolve(file,'index.html');info=await stat(file).catch(()=>null)}
const status=info?.isFile()?200:404;
if(status===404){file=resolve(root,'404.html');info=await stat(file)}
res.writeHead(status,{'Content-Type':types[extname(file)]??'application/octet-stream','Content-Length':info.size});
if(req.method==='HEAD')res.end();else createReadStream(file).pipe(res);
}catch{res.writeHead(400);res.end('Bad request')}
}).listen(Number(process.env.PORT??3000),'127.0.0.1',()=>console.log(`Static site: http://localhost:${process.env.PORT??3000}`));
+30
View File
@@ -0,0 +1,30 @@
import { readFile, stat } from 'node:fs/promises';
import { resolve, join } from 'node:path';
import assert from 'node:assert/strict';
const root=resolve('out');
const routes=['/','/aboutus/','/contactus/','/ourservices/','/portfolios/','/blog/',...['residential','beauty','commercial','landscape','facade'].map(slug=>`/portfolios/${slug}/`)];
const assets=new Set();
for(const route of routes){
const html=await readFile(join(root,route,'index.html'),'utf8');
assert.match(html,/<html[^>]*lang="fa"/);
assert.match(html,/<html[^>]*dir="rtl"/);
assert.ok(html.includes('گودرزی'),`Missing brand: ${route}`);
if (route === '/') assert.ok(!html.includes('class="site-footer"'), 'Home must not render a footer');
else assert.ok(html.includes('https://meshkee.com'),`Missing Meshkee credit: ${route}`);
for(const match of html.matchAll(/(?:src|href)="(\/[^"?#]*)/g)){
const url=decodeURIComponent(match[1]);
if(url.startsWith('//'))continue;
assets.add(url.endsWith('/')?`${url}index.html`:url);
}
const response=await fetch(`http://localhost:3000${route}`);
assert.equal(response.status,200,route);
}
for(const asset of assets){assert.ok((await stat(join(root,asset))).isFile(),asset)}
for(const font of ['iranyekanweblightfanum.woff','iranyekanwebboldfanum.woff']){
assert.equal((await readFile(join(root,'fonts',font))).subarray(0,4).toString(),'wOFF');
}
const missing=await fetch('http://localhost:3000/this-page-does-not-exist/');
assert.equal(missing.status,404);
assert.ok((await missing.text()).includes('این صفحه پیدا نشد'));
console.log(`PASS: ${routes.length} routes, ${assets.size} linked local files, both IRANYekan fonts, Persian RTL, Meshkee credit, and real HTTP 404.`);
+23
View File
@@ -0,0 +1,23 @@
import assert from 'node:assert/strict';
import { access, readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
const output=resolve('out');
const routes=['/','/aboutus/','/blog/','/contactus/','/ourservices/','/portfolios/'];
const titles=new Set(); const descriptions=new Set();
for(const route of routes){
const html=await readFile(join(output,route,'index.html'),'utf8');
const title=html.match(/<title>([^<]+)<\/title>/)?.[1];
const description=html.match(/<meta name="description" content="([^"]+)"/)?.[1];
assert.ok(title,`Missing title: ${route}`); assert.ok(description,`Missing description: ${route}`);
assert.ok(!titles.has(title),`Duplicate title: ${title}`); assert.ok(!descriptions.has(description),`Duplicate description: ${description}`);
titles.add(title); descriptions.add(description);
assert.equal((html.match(/<h1(?:\s|>)/g)||[]).length,1,`Expected one H1: ${route}`);
assert.match(html,/<link rel="canonical" href="https:\/\/sanazgoodarzi\.ir\//,`Missing canonical: ${route}`);
for(const image of html.matchAll(/<img\b[^>]*>/g)) assert.match(image[0],/\salt="[^"]+"/,`Missing meaningful image alt in ${route}`);
}
const sitemap=JSON.parse(await readFile(join(output,'meshkee','sitemap-config.json'),'utf8'));
assert.equal(sitemap.baseUrl,'https://sanazgoodarzi.ir');
assert.deepEqual(sitemap.staticPages.map(page=>page.path),['/','/aboutus','/blog','/contactus','/ourservices','/portfolios']);
for(const forbidden of ['sitemap.xml','robots.txt']){try{await access(join(output,forbidden));assert.fail(`${forbidden} must be proxied, not shipped`)}catch(error){if(error.code!=='ENOENT')throw error}}
console.log(`PASS: SEO metadata, one H1, canonical URLs and image alt text on ${routes.length} crawlable static pages; sitemap config contains ${sitemap.staticPages.length} routes.`);