first commit
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
@@ -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>}
|
||||
@@ -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>}</>}
|
||||
@@ -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>}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>}
|
||||
</>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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></>}
|
||||
@@ -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>}
|
||||
Reference in New Issue
Block a user