feat(mobile): complete responsive design and layout optimizations across landing page

This commit is contained in:
Behrad
2026-10-04 23:48:19 +03:30
parent 846682ac4a
commit 575e4868be
13 changed files with 686 additions and 251 deletions
+13 -14
View File
@@ -1,4 +1,3 @@
import React from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ArrowLeftIcon, ClockIcon } from 'lucide-react' import { ArrowLeftIcon, ClockIcon } from 'lucide-react'
import type { Article } from '../data/site' import type { Article } from '../data/site'
@@ -11,43 +10,43 @@ export function ArticleCard({ article }: { article: Article }) {
return ( return (
<Link <Link
to={href(`/blog/${article.slug}`)} to={href(`/blog/${article.slug}`)}
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-white/10 bg-char/80 backdrop-blur-xl shadow-[0_10px_30px_rgba(0,0,0,0.3)] transition-all duration-500 ease-lux hover:-translate-y-2 hover:border-copper/50 hover:shadow-[0_20px_40px_rgba(0,0,0,0.6),0_0_30px_rgba(197,143,92,0.18)] focus:outline-none focus-visible:ring-2 focus-visible:ring-copper" className="group flex h-full flex-col overflow-hidden rounded-xl md:rounded-2xl border border-white/10 bg-char/80 backdrop-blur-xl shadow-[0_10px_30px_rgba(0,0,0,0.3)] transition-all duration-500 ease-lux hover:-translate-y-2 hover:border-copper/50 hover:shadow-[0_20px_40px_rgba(0,0,0,0.6),0_0_30px_rgba(197,143,92,0.18)] focus:outline-none focus-visible:ring-2 focus-visible:ring-copper"
aria-label={`${t('readArticlePrefix')} ${article.title}`} aria-label={`${t('readArticlePrefix')} ${article.title}`}
> >
<div className="relative overflow-hidden"> <div className="relative overflow-hidden aspect-[16/10] w-full">
<img <img
src={article.image} src={article.image}
alt={`${t('articleImagePrefix')} ${article.title}`} alt={`${t('articleImagePrefix')} ${article.title}`}
className="h-52 w-full object-cover transition-transform duration-700 ease-lux group-hover:scale-105" className="h-full w-full object-cover transition-transform duration-700 ease-lux group-hover:scale-105"
/> />
{/* برچسب دسته‌بندی کپسولی شیشه‌ای روی تصویر */} {/* برچسب دسته‌بندی کپسولی شیشه‌ای روی تصویر */}
<div className="absolute top-4 ltr:left-4 rtl:right-4 rounded-full border border-white/20 bg-ink/75 px-3.5 py-1 text-xs font-light text-copper backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.2),0_4px_15px_rgba(0,0,0,0.4)]"> <div className="absolute top-3 ltr:left-3 rtl:right-3 md:top-4 md:ltr:left-4 md:rtl:right-4 rounded-full border border-white/20 bg-ink/75 px-2.5 py-0.5 md:px-3.5 md:py-1 text-[11px] md:text-xs font-light text-copper backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.2),0_4px_15px_rgba(0,0,0,0.4)]">
{article.category} {article.category}
</div> </div>
</div> </div>
<div className="flex flex-1 flex-col p-8"> <div className="flex flex-1 flex-col p-3.5 md:p-8">
<h3 className="mb-4 md:mb-6 text-xl font-bold leading-relaxed text-ivory transition-colors duration-300 group-hover:text-copper"> <h3 className="mb-1.5 md:mb-6 text-xs md:text-xl font-bold leading-snug md:leading-relaxed text-ivory line-clamp-2 transition-colors duration-300 group-hover:text-copper">
{article.title} {article.title}
</h3> </h3>
<p className="mb-6 md:mb-10 text-[15px] font-light leading-loose text-ivory/60 line-clamp-3"> <p className="mb-2.5 md:mb-10 text-[11px] md:text-[15px] font-light leading-relaxed md:leading-loose text-ivory/60 line-clamp-2 md:line-clamp-3">
{article.excerpt} {article.excerpt}
</p> </p>
<div className="mt-auto flex items-center justify-between border-t border-white/10 pt-6 md:pt-8 text-xs font-light text-ivory/50"> <div className="mt-auto flex items-center justify-between border-t border-white/10 pt-2.5 md:pt-8 text-[11px] md:text-xs font-light text-ivory/50">
<span className="flex items-center gap-4"> <span className="flex items-center gap-2.5 md:gap-4">
<span>{formatJalali(article.date)}</span> <span>{formatJalali(article.date)}</span>
<span className="flex items-center gap-2"> <span className="flex items-center gap-1.5 md:gap-2">
<ClockIcon className="h-3.5 w-3.5 text-copper/80" /> <ClockIcon className="h-3 w-3 md:h-3.5 md:w-3.5 text-copper/80" />
{toPersianDigits(article.read)} {toPersianDigits(article.read)}
</span> </span>
</span> </span>
{/* دکمه کپسولی ادامه مطلب با افکت شیشه‌ای */} {/* دکمه کپسولی ادامه مطلب با افکت شیشه‌ای */}
<span className="inline-flex items-center gap-2 rounded-full border border-copper/40 bg-copper/5 px-3.5 py-1.5 text-xs font-medium text-copper backdrop-blur-md shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)] transition-all duration-300 ease-lux hover:border-copper hover:bg-copper hover:text-char hover:shadow-[0_0_15px_rgba(197,143,92,0.5)] rtl:hover:[&>svg]:-translate-x-1 ltr:hover:[&>svg]:translate-x-1"> <span className="inline-flex items-center gap-1.5 md:gap-2 rounded-full border border-copper/40 bg-copper/5 px-2.5 py-1 md:px-3.5 md:py-1.5 text-[11px] md:text-xs font-medium text-copper backdrop-blur-md shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)] transition-all duration-300 ease-lux group-hover:border-copper group-hover:bg-copper group-hover:text-char group-hover:shadow-[0_0_15px_rgba(197,143,92,0.5)] rtl:group-hover:[&>svg]:-translate-x-1 ltr:group-hover:[&>svg]:translate-x-1">
{t('readMore')} {t('readMore')}
<ArrowLeftIcon className="h-3.5 w-3.5 ltr:-scale-x-100 transition-transform duration-300" /> <ArrowLeftIcon className="h-3 w-3 md:h-3.5 md:w-3.5 ltr:-scale-x-100 transition-transform duration-300" />
</span> </span>
</div> </div>
</div> </div>
+129 -7
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react' import { useState } from 'react'
import { CheckCircle2Icon, MailIcon, MapPinIcon, PhoneIcon } from 'lucide-react' import { CheckCircle2Icon, MailIcon, MapPinIcon, PhoneIcon } from 'lucide-react'
import Swal from 'sweetalert2' import Swal from 'sweetalert2'
import { Reveal } from './Reveal' import { Reveal } from './Reveal'
@@ -100,18 +100,139 @@ export function ContactSection() {
} }
} }
const cleanOptionalLabel = `(${content.note_optional_label?.replace(/^\(+|\)+$/g, '') || 'اختیاری'})`
return ( return (
<section id="contact" className="relative overflow-hidden py-24 lg:py-32" aria-labelledby="contact-title"> <section id="contact" className="relative w-full max-w-full overflow-x-clip border-t border-copper/15 py-16 md:py-24 lg:py-32" aria-labelledby="contact-title">
<img <img
src={brand.contact_bg} src={brand.contact_bg}
alt="" alt=""
aria-hidden="true" aria-hidden="true"
className="absolute inset-0 h-full w-full object-cover" className="absolute inset-0 h-full w-full object-cover object-center"
/> />
<div className="absolute inset-0 bg-ink/75 backdrop-blur-[2px]" /> <div className="absolute inset-0 bg-gradient-to-b from-ink/90 via-ink/75 to-ink backdrop-blur-[2px]" />
<div className="relative mx-auto max-w-site px-6 lg:px-10"> <div className="relative mx-auto max-w-site px-4 md:px-6 lg:px-10">
<div className="mx-auto grid max-w-6xl items-stretch gap-8 lg:grid-cols-12">
{/* Mobile View: High-Conversion Streamlined Layout */}
<div className="block md:hidden px-1 py-1">
{/* Header */}
<div className="mb-4 text-center">
<p className="text-xs font-light tracking-wide text-copper">
<InlineEdit value={content.label} onSave={(v) => saveContent('label', v)} />
</p>
<h2 className="mt-1.5 text-xl font-bold text-ivory">
<InlineEdit value={content.title} onSave={(v) => saveContent('title', v)} />
</h2>
<p className="mt-1.5 text-xs font-light leading-relaxed text-ivory/70 max-w-sm mx-auto">
<InlineEdit multiline value={content.lead} onSave={(v) => saveContent('lead', v)} />
</p>
</div>
{/* High-Conversion Form Box */}
<div className="rounded-2xl border border-white/10 bg-char/60 p-4 backdrop-blur-xl shadow-lg space-y-3">
{sent ? (
<div className="flex flex-col items-center justify-center py-6 text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-copper/40 bg-copper/15 text-copper">
<CheckCircle2Icon className="h-6 w-6" />
</div>
<h3 className="mt-4 text-base font-bold text-ivory">
{content.success_title}
</h3>
<p className="mt-2 text-xs font-light leading-relaxed text-ivory/70">
{content.success_text}
</p>
<button
type="button"
onClick={() => {
setSent(false)
setForm({ name: '', phone: '', note: '', website: '' })
}}
className="mt-4 rounded-xl bg-copper px-6 py-2.5 text-xs font-bold text-char transition-all cursor-pointer"
>
{content.success_button_label}
</button>
</div>
) : (
<form onSubmit={submit} noValidate className="space-y-3">
<input
type="text"
name="website_mobile"
value={form.website}
onChange={(e) => setForm({ ...form, website: e.target.value })}
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
className="pointer-events-none absolute -left-[9999px] h-0 w-0 opacity-0"
/>
<div>
<label htmlFor="contact-name-mobile" className="text-[11px] font-semibold text-ivory/80 mb-1 block">
{content.name_label}
</label>
<input
id="contact-name-mobile"
type="text"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder={content.name_placeholder}
className="w-full py-2.5 px-3 text-xs rounded-xl border border-white/10 bg-ink/60 text-ivory placeholder:text-ivory/35 backdrop-blur-md focus:border-copper/70 focus:outline-none"
/>
</div>
<div>
<label htmlFor="contact-phone-mobile" className="text-[11px] font-semibold text-ivory/80 mb-1 block">
{content.phone_field_label} <span className="text-copper">*</span>
</label>
<input
id="contact-phone-mobile"
type="tel"
dir="ltr"
required
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
placeholder={content.phone_placeholder}
className="w-full py-2.5 px-3 text-xs rounded-xl border border-white/10 bg-ink/60 text-ivory placeholder:text-ivory/35 backdrop-blur-md focus:border-copper/70 focus:outline-none text-left font-mono"
/>
</div>
<div>
<label htmlFor="contact-note-mobile" className="text-[11px] font-semibold text-ivory/80 mb-1 block">
{content.note_label} <span className="font-light text-ivory/50">{cleanOptionalLabel}</span>
</label>
<textarea
id="contact-note-mobile"
rows={2}
value={form.note}
onChange={(e) => setForm({ ...form, note: e.target.value })}
placeholder={content.note_placeholder}
className="w-full py-2.5 px-3 text-xs rounded-xl border border-white/10 bg-ink/60 text-ivory placeholder:text-ivory/35 backdrop-blur-md focus:border-copper/70 focus:outline-none resize-none leading-relaxed"
/>
</div>
<button
type="submit"
disabled={sending}
className="w-full py-3 rounded-xl bg-copper text-char font-bold text-xs shadow-md hover:brightness-110 active:scale-95 transition-all flex items-center justify-center gap-2 cursor-pointer"
>
{sending ? content.sending_label : content.submit_label}
</button>
{/* Direct Call Fallback */}
<a
href={brand.phone_href}
onClick={() => track('phone-click', { from: 'contact-section-mobile-hotline' })}
className="flex items-center justify-center gap-1.5 pt-2 text-[11px] text-ivory/60 hover:text-copper transition-colors"
>
<span>نیاز به گفتگوی فوری دارید؟</span>
<span className="font-bold text-copper dir-ltr" dir="ltr">{brand.phone}</span>
</a>
</form>
)}
</div>
</div>
{/* Desktop View */}
<div className="hidden md:grid mx-auto max-w-6xl items-stretch gap-8 lg:grid-cols-12">
{/* باکس اطلاعات تماس */} {/* باکس اطلاعات تماس */}
<div className="rounded-3xl border border-white/10 bg-char/60 p-8 backdrop-blur-2xl shadow-[0_20px_50px_rgba(0,0,0,0.4),inset_0_1px_1px_rgba(255,255,255,0.15)] md:p-10 lg:col-span-5"> <div className="rounded-3xl border border-white/10 bg-char/60 p-8 backdrop-blur-2xl shadow-[0_20px_50px_rgba(0,0,0,0.4),inset_0_1px_1px_rgba(255,255,255,0.15)] md:p-10 lg:col-span-5">
@@ -252,7 +373,7 @@ export function ContactSection() {
<div className="md:col-span-2"> <div className="md:col-span-2">
<label htmlFor="contact-note" className="block text-sm font-medium text-ivory/90"> <label htmlFor="contact-note" className="block text-sm font-medium text-ivory/90">
<InlineEdit value={content.note_label} onSave={(v) => saveContent('note_label', v)} /> <span className="font-light text-ivory/50">{content.note_optional_label}</span> <InlineEdit value={content.note_label} onSave={(v) => saveContent('note_label', v)} /> <span className="font-light text-ivory/50">{cleanOptionalLabel}</span>
</label> </label>
<textarea <textarea
id="contact-note" id="contact-note"
@@ -281,6 +402,7 @@ export function ContactSection() {
)} )}
</div> </div>
</div> </div>
</div> </div>
</section> </section>
) )
+142 -4
View File
@@ -1,4 +1,4 @@
import React from 'react'; import { useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ArrowUpIcon, InstagramIcon, LinkedinIcon, MailIcon, MapPinIcon, PhoneIcon, SendIcon } from 'lucide-react'; import { ArrowUpIcon, InstagramIcon, LinkedinIcon, MailIcon, MapPinIcon, PhoneIcon, SendIcon } from 'lucide-react';
import { useSite } from '../api'; import { useSite } from '../api';
@@ -17,6 +17,7 @@ export function Footer() {
const { brand, services, nav, footer } = useSite(); const { brand, services, nav, footer } = useSite();
const { t, href } = useLang(); const { t, href } = useLang();
const save = useFieldSaver(); const save = useFieldSaver();
const [openSection, setOpenSection] = useState<string | null>(null);
const saveFooter = (field: string, value: string) => const saveFooter = (field: string, value: string) =>
save('footer', footer.id, { [field]: value }, (d) => { save('footer', footer.id, { [field]: value }, (d) => {
@@ -34,8 +35,143 @@ export function Footer() {
}; };
return ( return (
<footer className="relative border-t border-white/10 bg-char/95 backdrop-blur-2xl"> <footer className="relative w-full max-w-full overflow-x-clip border-t border-white/10 bg-char/95 backdrop-blur-2xl">
<div className="mx-auto max-w-site px-6 py-16 lg:px-10"> <div className="mx-auto max-w-site px-4 py-8 md:px-6 md:py-16 lg:px-10">
{/* Mobile View: Collapsible Luxury Accordions */}
<div className="block md:hidden">
{/* Top Brand Block */}
<div className="mb-6 flex flex-col items-center text-center">
<div className="relative w-fit">
<img src={brand.logo} alt={brand.name} className="h-16 w-auto object-contain" />
<SingletonPill resource="brand" label="ویرایش برند" className="absolute top-1/2 -translate-y-1/2 ltr:-right-8 rtl:-left-8" />
</div>
<p className="mt-4 text-xs font-light leading-relaxed text-ivory/60 max-w-xs">
{footer.about_text}
</p>
{/* Social icons */}
<div className="mt-4 flex items-center justify-center gap-3">
{footer.socials.map(({ label, href, icon }) => {
const Icon = SOCIAL_ICONS[icon] ?? InstagramIcon;
return (
<a
key={label}
href={href}
aria-label={label}
className="group flex h-10 w-10 items-center justify-center rounded-full border border-white/15 bg-white/[0.04] text-ivory/70 backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.15),0_4px_12px_rgba(0,0,0,0.3)] transition-all duration-300 ease-lux hover:border-copper/70 hover:bg-copper/10 hover:text-copper active:scale-95"
>
<Icon className="h-3.5 w-3.5 transition-transform duration-300 group-hover:scale-110" />
</a>
);
})}
</div>
</div>
{/* Collapsible Link Groups */}
<div className="border-t border-white/10">
{/* Pages Accordion */}
<div className="border-b border-white/10">
<button
type="button"
onClick={() => setOpenSection(openSection === 'pages' ? null : 'pages')}
className="flex w-full items-center justify-between py-3.5 text-xs font-bold text-copper text-start cursor-pointer"
>
<span>{footer.pages_heading}</span>
<span className={`text-[10px] text-copper transition-transform duration-300 ${openSection === 'pages' ? 'rotate-180' : ''}`}>
▼
</span>
</button>
<div className={`overflow-hidden transition-all duration-300 ease-in-out ${openSection === 'pages' ? 'max-h-96 pb-3 opacity-100' : 'max-h-0 opacity-0'}`}>
<ul className="space-y-2 rtl:pr-2 ltr:pl-2 text-xs text-ivory/70">
{nav.map((item) => (
<li key={item.id}>
<Link
to={href(item.href)}
className="block py-1 transition-colors hover:text-copper"
>
{item.label}
</Link>
</li>
))}
</ul>
</div>
</div>
{/* Services Accordion */}
<div className="border-b border-white/10">
<button
type="button"
onClick={() => setOpenSection(openSection === 'services' ? null : 'services')}
className="flex w-full items-center justify-between py-3.5 text-xs font-bold text-copper text-start cursor-pointer"
>
<span>{footer.services_heading}</span>
<span className={`text-[10px] text-copper transition-transform duration-300 ${openSection === 'services' ? 'rotate-180' : ''}`}>
▼
</span>
</button>
<div className={`overflow-hidden transition-all duration-300 ease-in-out ${openSection === 'services' ? 'max-h-96 pb-3 opacity-100' : 'max-h-0 opacity-0'}`}>
<ul className="space-y-2 rtl:pr-2 ltr:pl-2 text-xs text-ivory/70">
{services.map((service) => (
<li key={service.id} className="py-1">
{service.title}
</li>
))}
</ul>
</div>
</div>
{/* Contact Accordion */}
<div className="border-b border-white/10">
<button
type="button"
onClick={() => setOpenSection(openSection === 'contact' ? null : 'contact')}
className="flex w-full items-center justify-between py-3.5 text-xs font-bold text-copper text-start cursor-pointer"
>
<span>{footer.contact_heading}</span>
<span className={`text-[10px] text-copper transition-transform duration-300 ${openSection === 'contact' ? 'rotate-180' : ''}`}>
▼
</span>
</button>
<div className={`overflow-hidden transition-all duration-300 ease-in-out ${openSection === 'contact' ? 'max-h-96 pb-3 opacity-100' : 'max-h-0 opacity-0'}`}>
<ul className="space-y-3 rtl:pr-2 ltr:pl-2 text-xs text-ivory/70">
<li className="flex items-start gap-2.5">
<MapPinIcon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-copper" />
<span className="leading-relaxed">{brand.address}</span>
</li>
<li className="flex items-center gap-2.5">
<PhoneIcon className="h-3.5 w-3.5 shrink-0 text-copper" />
<a href={brand.phone_href} dir="ltr" className="transition-colors hover:text-copper">
{brand.phone}
</a>
</li>
<li className="flex items-center gap-2.5">
<MailIcon className="h-3.5 w-3.5 shrink-0 text-copper" />
<a href={`mailto:${brand.email}`} dir="ltr" className="transition-colors hover:text-copper">
{brand.email}
</a>
</li>
</ul>
</div>
</div>
</div>
{/* Bottom Copyright & Meta for Mobile */}
<div className="mt-6 flex flex-col items-center gap-3 border-t border-white/10 pt-4 text-[11px] text-ivory/50 text-center">
<button
type="button"
onClick={scrollToTop}
className="group flex items-center gap-2 rounded-full border border-white/15 bg-white/[0.04] px-3.5 py-1.5 text-xs font-medium text-ivory/70 backdrop-blur-xl transition-all hover:border-copper hover:bg-copper/10 hover:text-copper active:scale-95 cursor-pointer mb-1"
>
<span>{t('backToTop')}</span>
<ArrowUpIcon className="h-3 w-3 text-copper transition-transform duration-300 group-hover:-translate-y-0.5" />
</button>
<p>{footer.copyright}</p>
<p className="text-[10px] text-ivory/40">{footer.tagline}</p>
</div>
</div>
{/* Desktop View: Multi-Column Layout */}
<div className="hidden md:block">
<div className="grid gap-12 lg:grid-cols-12"> <div className="grid gap-12 lg:grid-cols-12">
<div className="lg:col-span-4"> <div className="lg:col-span-4">
<div className="relative w-fit"> <div className="relative w-fit">
@@ -45,7 +181,7 @@ export function Footer() {
<p className="mt-6 max-w-sm text-sm font-light leading-8 text-ivory/60"> <p className="mt-6 max-w-sm text-sm font-light leading-8 text-ivory/60">
<InlineEdit multiline value={footer.about_text} onSave={(v) => saveFooter('about_text', v)} /> <InlineEdit multiline value={footer.about_text} onSave={(v) => saveFooter('about_text', v)} />
</p> </p>
{/* آیکون‌های شبکه‌های اجتماعی: گوی‌های شیشه‌ای شناور */} {/* آیکون‌های شبکه‌های اجتماعی */}
<div className="mt-8 flex items-center gap-3"> <div className="mt-8 flex items-center gap-3">
{footer.socials.map(({ label, href, icon }) => { {footer.socials.map(({ label, href, icon }) => {
const Icon = SOCIAL_ICONS[icon] ?? InstagramIcon; const Icon = SOCIAL_ICONS[icon] ?? InstagramIcon;
@@ -149,6 +285,8 @@ export function Footer() {
</p> </p>
</div> </div>
</div> </div>
</div>
</footer> </footer>
); );
} }
+8 -8
View File
@@ -45,31 +45,31 @@ export function ProjectCard({ project, imageUrl, detailHref }: ProjectCardProps)
<img <img
src={finalImageUrl} src={finalImageUrl}
alt={title} alt={title}
className="w-full h-[250px] object-cover transition-transform duration-700 ease-lux group-hover:scale-110" className="w-full h-44 md:h-[250px] object-cover transition-transform duration-700 ease-lux group-hover:scale-110"
/> />
)} )}
{/* نشانگر وضعیت کپسولی شیشه‌ای */} {/* نشانگر وضعیت کپسولی شیشه‌ای */}
<div <div
className={`absolute top-4 ltr:left-4 rtl:right-4 flex items-center gap-2 rounded-full border border-white/15 bg-ink/75 px-4 py-1.5 text-xs font-light backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.2),0_4px_15px_rgba(0,0,0,0.4)] ${textColor}`} className={`absolute top-3 ltr:left-3 rtl:right-3 md:top-4 md:ltr:left-4 md:rtl:right-4 flex items-center gap-1.5 md:gap-2 rounded-full border border-white/15 bg-ink/75 px-3 py-1 md:px-4 md:py-1.5 text-xs font-light backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.2),0_4px_15px_rgba(0,0,0,0.4)] ${textColor}`}
> >
<span className={`h-2 w-2 rounded-full ${dotColor} ${isOngoing ? 'shadow-[0_0_10px_rgba(197,143,92,0.9)]' : ''}`} /> <span className={`h-2 w-2 rounded-full ${dotColor} ${isOngoing ? 'shadow-[0_0_10px_rgba(197,143,92,0.9)]' : ''}`} />
{statusText} {statusText}
</div> </div>
</div> </div>
<div className="flex flex-1 flex-col p-7 pb-4"> <div className="flex flex-1 flex-col p-4 pb-3 md:p-7 md:pb-4">
<div className="text-center"> <div className="text-center">
<h3 className="font-pinar text-xl font-bold text-ivory transition-colors duration-300 group-hover:text-copper"> <h3 className="font-pinar text-lg font-bold text-ivory transition-colors duration-300 group-hover:text-copper md:text-xl">
{title} {title}
</h3> </h3>
{location && <p className="mt-4 text-sm font-light text-ivory/60">{location}</p>} {location && <p className="mt-2 text-xs font-light text-ivory/60 md:mt-4 md:text-sm">{location}</p>}
</div> </div>
{area && ( {area && (
<> <>
<div className="my-6 h-px w-full bg-white/10" /> <div className="my-3 h-px w-full bg-white/10 md:my-6" />
<div className="mt-auto flex items-center justify-end text-sm font-light text-ivory/60"> <div className="mt-auto flex items-center justify-end text-xs font-light text-ivory/60 md:text-sm">
{/* واحد زیربنا در محتوای انگلیسی داخل خود مقدار است — فقط در فارسی اضافه می‌شود */} {/* واحد زیربنا در محتوای انگلیسی داخل خود مقدار است — فقط در فارسی اضافه می‌شود */}
<span>{lang === 'fa' ? `${area} ${t('sqm')}` : area}</span> <span>{lang === 'fa' ? `${area} ${t('sqm')}` : area}</span>
</div> </div>
@@ -79,7 +79,7 @@ export function ProjectCard({ project, imageUrl, detailHref }: ProjectCardProps)
</Link> </Link>
{/* پایهٔ کارت: یک دکمه واحد */} {/* پایهٔ کارت: یک دکمه واحد */}
<div className="flex flex-col gap-2 px-7 pb-7"> <div className="flex flex-col gap-2 px-4 pb-4 md:px-7 md:pb-7">
<PremiumButton to={finalDetailHref} className="w-full"> <PremiumButton to={finalDetailHref} className="w-full">
{t('viewProject')} {t('viewProject')}
</PremiumButton> </PremiumButton>
+89 -13
View File
@@ -1,5 +1,4 @@
import React from 'react'; import { Link } from 'react-router-dom';
import { Reveal } from '../Reveal'; import { Reveal } from '../Reveal';
import { PremiumButton } from '../PremiumButton'; import { PremiumButton } from '../PremiumButton';
import { ArticleCard } from '../ArticleCard'; import { ArticleCard } from '../ArticleCard';
@@ -8,6 +7,7 @@ import { useLang } from '../../i18n/lang';
import { useFieldSaver } from '../../admin/AdminContext'; import { useFieldSaver } from '../../admin/AdminContext';
import { InlineEdit } from '../admin/InlineEdit'; import { InlineEdit } from '../admin/InlineEdit';
import { AddItemButton, EditableItem } from '../../admin/Editable'; import { AddItemButton, EditableItem } from '../../admin/Editable';
import { formatJalali } from '../../utils/date';
export function Articles() { export function Articles() {
const { articles } = useSite(); const { articles } = useSite();
@@ -17,6 +17,9 @@ export function Articles() {
// پیش‌نویس‌ها فقط در صفحه وبلاگ (برای ادمین) دیده می‌شوند، نه در خانه // پیش‌نویس‌ها فقط در صفحه وبلاگ (برای ادمین) دیده می‌شوند، نه در خانه
const published = articles.filter((a) => a.status !== 'draft'); const published = articles.filter((a) => a.status !== 'draft');
const topArticles = published.slice(0, 3);
const spotlight = topArticles[0];
const compactList = topArticles.slice(1, 3);
const saveHeading = (field: 'label' | 'title' | 'lead' | 'cta_label', value: string) => const saveHeading = (field: 'label' | 'title' | 'lead' | 'cta_label', value: string) =>
save('section-headings', heading.id, { [field]: value }, (d) => { save('section-headings', heading.id, { [field]: value }, (d) => {
@@ -25,24 +28,97 @@ export function Articles() {
}); });
return ( return (
<section className="bg-char py-24 lg:py-32" aria-labelledby="articles-title"> <section className="w-full max-w-full overflow-x-clip bg-char py-16 md:py-24 lg:py-32" aria-labelledby="articles-title">
<div className="mx-auto max-w-site px-6 lg:px-10"> <div className="mx-auto max-w-site px-4 md:px-6 lg:px-10">
<Reveal className="max-w-2xl ltr:text-left rtl:text-right"> <Reveal className="max-w-2xl ltr:text-left rtl:text-right">
<p className="text-sm font-light text-copper"> <p className="text-sm font-light text-copper">
<InlineEdit value={heading.label} onSave={(v) => saveHeading('label', v)} /> <InlineEdit value={heading.label} onSave={(v) => saveHeading('label', v)} />
</p> </p>
<h2 id="articles-title" className="mt-5 text-section font-bold text-ivory"> <h2 id="articles-title" className="mt-3 md:mt-5 text-section font-bold text-ivory">
<InlineEdit value={heading.title} onSave={(v) => saveHeading('title', v)} /> <InlineEdit value={heading.title} onSave={(v) => saveHeading('title', v)} />
</h2> </h2>
<p className="mt-5 text-base font-light leading-9 text-ivory/60"> <p className="mt-3 md:mt-5 text-sm md:text-base font-light leading-7 md:leading-9 text-ivory/60">
<InlineEdit multiline value={heading.lead} onSave={(v) => saveHeading('lead', v)} /> <InlineEdit multiline value={heading.lead} onSave={(v) => saveHeading('lead', v)} />
</p> </p>
<div className="mt-6 h-px w-24 bg-copper/70" /> <div className="mt-4 md:mt-6 h-px w-24 bg-copper/70" />
</Reveal> </Reveal>
<div className="mt-14 grid gap-6 lg:grid-cols-3"> {/* Mobile View: Magazine Editorial layout (1 Featured Spotlight + Compact Rows) */}
{published.slice(0, 3).map((article, i) => <div className="block md:hidden mt-8">
<Reveal key={article.id} delay={0.06 * i} className="h-full"> {/* First Article (Spotlight) */}
{spotlight && (
<EditableItem resource="articles" item={spotlight} editHref={`/admin/write/${spotlight.id}`}>
<Link
className="block rounded-xl border border-ivory/10 bg-char/40 overflow-hidden mb-3 group"
to={href(`/blog/${spotlight.slug}`)}
>
<img
src={spotlight.image}
alt={spotlight.title}
className="w-full aspect-[16/9] object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="p-3.5">
<div className="flex items-center gap-2 text-[11px] text-copper font-medium mb-1">
<span>{spotlight.category}</span>
<span>•</span>
<span>{formatJalali(spotlight.date)}</span>
</div>
<h4 className="text-sm font-bold text-ivory leading-snug line-clamp-2 mb-1 group-hover:text-copper transition-colors">
{spotlight.title}
</h4>
<p className="text-xs text-ivory/60 line-clamp-2 leading-relaxed">
{spotlight.excerpt}
</p>
</div>
</Link>
</EditableItem>
)}
{/* Remaining Articles (Compact List Rows) */}
{compactList.length > 0 && (
<div className="divide-y divide-ivory/15 border-t border-b border-ivory/15 mb-4">
{compactList.map((article) => (
<EditableItem key={article.id} resource="articles" item={article} editHref={`/admin/write/${article.id}`}>
<Link
className="py-2.5 flex items-center gap-3 group"
to={href(`/blog/${article.slug}`)}
>
<img
src={article.image}
alt={article.title}
className="w-14 h-14 rounded-lg object-cover shrink-0 border border-ivory/10"
/>
<div className="flex-1 min-w-0">
<span className="text-[10px] text-copper block mb-0.5">{formatJalali(article.date)}</span>
<h5 className="text-xs font-semibold text-ivory line-clamp-2 leading-snug group-hover:text-copper transition-colors">
{article.title}
</h5>
</div>
</Link>
</EditableItem>
))}
</div>
)}
{/* Bottom Button ("همه مقالات") */}
{heading.cta_label && heading.cta_href && (
<Link
to={href(heading.cta_href)}
className="w-full py-2.5 rounded-xl border border-ivory/20 bg-char/60 hover:bg-copper/10 hover:border-copper/40 text-xs font-semibold text-ivory text-center block transition-all active:scale-[0.98]"
>
<InlineEdit value={heading.cta_label} onSave={(v) => saveHeading('cta_label', v)} />
</Link>
)}
</div>
{/* Desktop View: 3-Column Grid */}
<div className="hidden md:grid mt-14 grid-cols-3 gap-6">
{topArticles.map((article, i) =>
<Reveal
key={article.id}
delay={0.06 * i}
className="h-full"
>
<EditableItem resource="articles" item={article} className="h-full" editHref={`/admin/write/${article.id}`}> <EditableItem resource="articles" item={article} className="h-full" editHref={`/admin/write/${article.id}`}>
<ArticleCard article={article} /> <ArticleCard article={article} />
</EditableItem> </EditableItem>
@@ -52,7 +128,7 @@ export function Articles() {
{heading.cta_label && heading.cta_href && ( {heading.cta_label && heading.cta_href && (
<Reveal delay={0.1}> <Reveal delay={0.1}>
<div className="mt-12 flex justify-center"> <div className="mt-12 hidden md:flex justify-center">
<PremiumButton to={href(heading.cta_href)} className="font-light"> <PremiumButton to={href(heading.cta_href)} className="font-light">
<InlineEdit value={heading.cta_label} onSave={(v) => saveHeading('cta_label', v)} /> <InlineEdit value={heading.cta_label} onSave={(v) => saveHeading('cta_label', v)} />
</PremiumButton> </PremiumButton>
@@ -63,6 +139,6 @@ export function Articles() {
<AddItemButton resource="articles" /> <AddItemButton resource="articles" />
</div> </div>
</div> </div>
</section>); </section>
);
} }
@@ -1,5 +1,4 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion' import { AnimatePresence, motion } from 'framer-motion'
import { Reveal } from '../Reveal' import { Reveal } from '../Reveal'
import { PremiumButton } from '../PremiumButton' import { PremiumButton } from '../PremiumButton'
@@ -39,18 +38,18 @@ export function FeaturedProjects() {
const BASE_IMG_URL = "/seed-media/2026/09/"; const BASE_IMG_URL = "/seed-media/2026/09/";
return ( return (
<section className="bg-ink py-24 lg:py-32" aria-labelledby="projects-title"> <section className="w-full max-w-full overflow-x-clip bg-ink py-16 md:py-24 lg:py-32" aria-labelledby="projects-title">
<div className="mx-auto max-w-site px-6 lg:px-10"> <div className="mx-auto max-w-site px-4 md:px-6 lg:px-10">
<SectionHeading title={heading.title} lead={heading.lead} headingKey="featured_projects" /> <SectionHeading title={heading.title} lead={heading.lead} headingKey="featured_projects" />
{/* دکمه‌های فیلتر مینیمال و شیشه‌ای (بدون کادر) */} {/* دکمه‌های فیلتر مینیمال و شیشه‌ای (بدون کادر) */}
<div className="mt-10 flex items-center justify-center gap-4 md:gap-6" role="tablist" aria-label={t('projectFilterAria')}> <div className="mt-8 flex flex-wrap items-center justify-center gap-3 md:mt-10 md:gap-6" role="tablist" aria-label={t('projectFilterAria')}>
{FILTERS.map((filter, index) => { {FILTERS.map((filter, index) => {
const isSelected = status === filter.key const isSelected = status === filter.key
return ( return (
<React.Fragment key={filter.key}> <React.Fragment key={filter.key}>
{index > 0 && ( {index > 0 && (
<span className="h-6 w-[2px] rounded-full bg-ivory/10" aria-hidden="true" /> <span className="hidden h-6 w-[2px] rounded-full bg-ivory/10 sm:inline-block" aria-hidden="true" />
)} )}
<button <button
@@ -58,7 +57,7 @@ export function FeaturedProjects() {
role="tab" role="tab"
aria-selected={isSelected} aria-selected={isSelected}
onClick={() => setStatus(filter.key)} onClick={() => setStatus(filter.key)}
className={`group relative rounded-lg px-6 py-2.5 text-sm font-medium transition-all duration-300 ease-lux active:scale-95 cursor-pointer select-none backdrop-blur-md ${ className={`group relative rounded-lg px-4 py-2 text-xs font-medium transition-all duration-300 ease-lux active:scale-95 cursor-pointer select-none backdrop-blur-md md:px-6 md:py-2.5 md:text-sm ${
isSelected isSelected
? 'bg-copper/15 text-copper shadow-[0_4px_15px_rgba(197,143,92,0.1)]' ? 'bg-copper/15 text-copper shadow-[0_4px_15px_rgba(197,143,92,0.1)]'
: 'bg-transparent text-ivory/60 hover:bg-white/5 hover:text-ivory' : 'bg-transparent text-ivory/60 hover:bg-white/5 hover:text-ivory'
@@ -76,7 +75,7 @@ export function FeaturedProjects() {
})} })}
</div> </div>
<div className="mt-10 min-h-[420px]"> <div className="mt-8 min-h-[380px] md:mt-10 md:min-h-[420px]">
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<motion.div <motion.div
key={status} key={status}
@@ -84,7 +83,7 @@ export function FeaturedProjects() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }} exit={{ opacity: 0, y: -15 }}
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }} transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
className="grid gap-6 lg:grid-cols-3" className="flex gap-4 overflow-x-auto pb-4 snap-x snap-mandatory [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none] md:grid md:grid-cols-3 md:gap-6 md:overflow-visible md:pb-0"
> >
{list.map((project, i) => { {list.map((project, i) => {
const dummyImageName = `${(i % 6) + 1}.png`; const dummyImageName = `${(i % 6) + 1}.png`;
@@ -96,7 +95,7 @@ export function FeaturedProjects() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: i * 0.1, ease: [0.23, 1, 0.32, 1] }} transition={{ duration: 0.5, delay: i * 0.1, ease: [0.23, 1, 0.32, 1] }}
className="h-full" className="h-full w-[82vw] max-w-[320px] shrink-0 snap-center md:w-auto md:max-w-none md:shrink"
> >
<ProjectCard <ProjectCard
project={project} project={project}
@@ -113,7 +112,7 @@ export function FeaturedProjects() {
{/* دکمه مشاهده همه پروژه‌ها */} {/* دکمه مشاهده همه پروژه‌ها */}
{heading.cta_label && heading.cta_href && ( {heading.cta_label && heading.cta_href && (
<Reveal delay={0.1}> <Reveal delay={0.1}>
<div className="mt-12 flex justify-center"> <div className="mt-10 flex justify-center md:mt-12">
<PremiumButton to={href(heading.cta_href)}> <PremiumButton to={href(heading.cta_href)}>
<InlineEdit <InlineEdit
value={heading.cta_label} value={heading.cta_label}
+13 -11
View File
@@ -1,4 +1,3 @@
import React from 'react'
import { Reveal } from '../Reveal' import { Reveal } from '../Reveal'
import { ServiceIcon } from '../ServiceIcon' import { ServiceIcon } from '../ServiceIcon'
import { useSectionHeading, useSite } from '../../api' import { useSectionHeading, useSite } from '../../api'
@@ -18,32 +17,35 @@ export function Services() {
}) })
return ( return (
<section className="bg-ink py-24 lg:py-32" aria-labelledby="services-title"> <section className="w-full max-w-full overflow-hidden bg-ink py-12 md:py-24 lg:py-32" aria-labelledby="services-title">
<div className="mx-auto max-w-site px-6 lg:px-10"> <div className="mx-auto max-w-site px-4 md:px-6 lg:px-10">
<Reveal className="max-w-2xl text-start"> <Reveal className="max-w-2xl text-start">
<h2 id="services-title" className="text-section font-bold text-ivory"> <h2 id="services-title" className="text-section font-bold text-ivory">
<InlineEdit value={heading.title} onSave={(v) => saveHeading('title', v)} /> <InlineEdit value={heading.title} onSave={(v) => saveHeading('title', v)} />
</h2> </h2>
<p className="mt-5 text-base font-light leading-9 text-ivory/60"> <p className="mt-3 text-sm font-light leading-7 text-ivory/60 md:mt-5 md:text-base md:leading-9">
<InlineEdit multiline value={heading.lead} onSave={(v) => saveHeading('lead', v)} /> <InlineEdit multiline value={heading.lead} onSave={(v) => saveHeading('lead', v)} />
</p> </p>
<div className="mt-6 h-px w-24 bg-copper/70" /> <div className="mt-4 h-px w-24 bg-copper/70 md:mt-6" />
</Reveal> </Reveal>
<div className="mt-14 grid gap-5 sm:grid-cols-2 lg:grid-cols-4"> <div className="mt-8 grid grid-cols-2 gap-3 md:mt-14 md:gap-5 md:grid-cols-2 lg:grid-cols-4">
{services.map((service, i) => ( {services.map((service, i) => (
<Reveal key={service.id} delay={0.04 * i} className="h-full"> <Reveal key={service.id} delay={0.04 * i} className="h-full">
<EditableItem resource="services" item={service} className="h-full"> <EditableItem resource="services" item={service} className="h-full">
<article className="group flex h-full flex-col items-center border border-ivory/10 bg-char p-8 text-center transition-all duration-300 ease-out hover:-translate-y-2 hover:border-copper/40 hover:shadow-lift transform-gpu will-change-transform"> <article className="group flex h-full flex-col items-center rounded-xl border border-ivory/10 bg-char p-3.5 text-center transition-all duration-300 ease-out hover:-translate-y-2 hover:border-copper/40 hover:shadow-lift transform-gpu will-change-transform md:rounded-2xl md:p-8">
<div className="flex h-10 w-10 items-center justify-center md:h-14 md:w-14">
<ServiceIcon <ServiceIcon
name={service.icon} name={service.icon}
className="h-12 w-12 text-copper/85 transition-colors duration-300 ease-out group-hover:text-copper" className="h-10 w-10 text-copper/85 transition-colors duration-300 ease-out group-hover:text-copper md:h-14 md:w-14"
/> />
<h3 className="mt-6 text-lg font-bold text-ivory transition-colors duration-300 ease-out group-hover:text-copper"> </div>
<h3 className="mt-3 text-sm font-bold text-ivory transition-colors duration-300 ease-out group-hover:text-copper md:mt-6 md:text-lg">
{service.title} {service.title}
</h3> </h3>
<p className="mt-3 text-sm font-light leading-8 text-ivory/55">{service.desc}</p> <p className="mt-2 text-xs font-light leading-relaxed text-ivory/55 line-clamp-2 md:mt-3 md:text-sm md:leading-8 md:line-clamp-none">
{service.desc}
</p>
</article> </article>
</EditableItem> </EditableItem>
</Reveal> </Reveal>
+7 -7
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useSite } from '../../api'; import { useSite } from '../../api';
import { useLang, formatNumber } from '../../i18n/lang'; import { useLang, formatNumber } from '../../i18n/lang';
import { useFieldSaver } from '../../admin/AdminContext'; import { useFieldSaver } from '../../admin/AdminContext';
@@ -71,12 +71,12 @@ export function Stats() {
const save = useFieldSaver(); const save = useFieldSaver();
return ( return (
<section className="bg-char py-16" aria-label={t('statsAria')}> <section className="w-full max-w-full overflow-x-clip bg-char py-8 md:py-16" aria-label={t('statsAria')}>
<div className="mx-auto grid max-w-site grid-cols-2 gap-y-12 px-6 lg:grid-cols-4 lg:px-10"> <div className="mx-auto grid max-w-site grid-cols-2 gap-3.5 p-4 md:grid-cols-4 md:gap-y-12 md:px-10 md:py-0">
{stats.map((stat) => {stats.map((stat) =>
<EditableItem key={stat.id} resource="stats" item={stat}> <EditableItem key={stat.id} resource="stats" item={stat}>
<div className="text-center"> <div className="p-3.5 text-center md:p-0">
<p className="text-4xl font-bold text-copper md:text-5xl"> <p className="text-2xl font-bold text-copper md:text-5xl">
<Counter value={stat.value} /> <Counter value={stat.value} />
<InlineEdit <InlineEdit
value={stat.suffix} value={stat.suffix}
@@ -84,10 +84,10 @@ export function Stats() {
const s = d.stats.find((x) => x.id === stat.id); const s = d.stats.find((x) => x.id === stat.id);
if (s) s.suffix = v; if (s) s.suffix = v;
})} })}
className="ltr:ml-1 rtl:mr-1 text-2xl" className="ltr:ml-1 rtl:mr-1 text-lg md:text-2xl"
/> />
</p> </p>
<p className="mt-3 text-sm font-light text-ivory/55"> <p className="mt-2 text-xs font-light text-ivory/55 md:mt-3 md:text-sm">
<InlineEdit <InlineEdit
value={stat.label} value={stat.label}
onSave={(v) => save('stats', stat.id, { label: v }, (d) => { onSave={(v) => save('stats', stat.id, { label: v }, (d) => {
+8 -10
View File
@@ -1,4 +1,3 @@
import React from 'react'
import { Reveal } from '../Reveal' import { Reveal } from '../Reveal'
import { SectionHeading } from '../SectionHeading' import { SectionHeading } from '../SectionHeading'
import { useSite } from '../../api' import { useSite } from '../../api'
@@ -9,8 +8,8 @@ export function Team() {
const { team } = useSite() const { team } = useSite()
const { t } = useLang() const { t } = useLang()
return ( return (
<section className="bg-ink py-24 lg:py-32" aria-labelledby="team-title" > <section className="w-full max-w-full overflow-x-clip bg-ink py-16 md:py-24 lg:py-32" aria-labelledby="team-title">
<div className="mx-auto max-w-site px-6 lg:px-10 "> <div className="mx-auto max-w-site px-4 md:px-6 lg:px-10">
<SectionHeading <SectionHeading
title={t('fallbackTeamTitle')} title={t('fallbackTeamTitle')}
lead={t('fallbackTeamLead')} lead={t('fallbackTeamLead')}
@@ -18,18 +17,17 @@ export function Team() {
align="center" align="center"
/> />
<div className="mt-16 grid gap-8 sm:grid-cols-2 lg:grid-cols-4"> <div className="mt-8 grid grid-cols-2 gap-3 sm:grid-cols-2 lg:grid-cols-4 md:mt-16 md:gap-8">
{team.map((member, i) => ( {team.map((member, i) => (
<Reveal key={member.id} delay={0.06 * i} className="h-full"> <Reveal key={member.id} delay={0.06 * i} className="h-full">
<EditableItem resource="team" item={member} className="h-full"> <EditableItem resource="team" item={member} className="h-full">
{/* اضافه شدن rounded-2xl برای گرد شدن گوشه‌ها */} <article className="group flex h-full flex-col items-center rounded-xl border border-ivory/10 bg-char p-3 text-center transition-[transform,box-shadow,border-color] duration-500 ease-out hover:-translate-y-3 hover:border-copper/50 hover:shadow-lift md:rounded-2xl md:p-10">
<article className="group flex h-full flex-col items-center rounded-2xl border border-ivory/10 bg-char p-10 text-center transition-[transform,box-shadow,border-color] duration-500 ease-out hover:-translate-y-3 hover:border-copper/50 hover:shadow-lift"> <div className="flex h-12 w-12 items-center justify-center rounded-full border border-copper/30 text-sm font-bold text-copper transition-colors duration-500 group-hover:border-copper md:h-24 md:w-24 md:text-3xl">
<div className="flex h-24 w-24 items-center justify-center rounded-full border border-copper/30 text-3xl font-bold text-copper transition-colors duration-500 group-hover:border-copper">
{member.name.trim().charAt(0)} {member.name.trim().charAt(0)}
</div> </div>
<h3 className="mt-8 text-lg font-bold text-ivory">{member.name}</h3> <h3 className="mt-2 text-xs font-bold text-ivory md:mt-8 md:text-lg">{member.name}</h3>
<p className="mt-2 text-sm font-light text-copper">{member.role}</p> <p className="mt-0.5 text-[11px] font-medium text-copper line-clamp-1 md:mt-2 md:text-sm md:font-light md:line-clamp-none">{member.role}</p>
<p className="mt-6 border-t border-ivory/10 pt-6 text-sm font-light text-ivory/60"> <p className="mt-2 w-full border-t border-ivory/10 pt-2 text-[10px] font-light leading-relaxed text-ivory/60 line-clamp-2 md:mt-6 md:pt-6 md:text-sm md:line-clamp-none">
{member.bio} {member.bio}
</p> </p>
</article> </article>
+36 -18
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion, PanInfo } from 'framer-motion';
import { ChevronLeftIcon, ChevronRightIcon, QuoteIcon } from 'lucide-react'; import { ChevronLeftIcon, ChevronRightIcon, QuoteIcon } from 'lucide-react';
import { SectionHeading } from '../SectionHeading'; import { SectionHeading } from '../SectionHeading';
import { useSite } from '../../api'; import { useSite } from '../../api';
@@ -17,39 +17,57 @@ export function Testimonials() {
setIndex((prev) => (prev + dir + testimonials.length) % testimonials.length); setIndex((prev) => (prev + dir + testimonials.length) % testimonials.length);
}; };
const handleDragEnd = (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {
if (info.offset.x > 50) {
go(1); // سوایپ به راست
} else if (info.offset.x < -50) {
go(-1); // سوایپ به چپ
}
};
return ( return (
<section className="border-y border-ivory/10 bg-char py-24 lg:py-32" aria-labelledby="testimonials-title"> <section className="border-y border-ivory/10 bg-char py-14 lg:py-28" aria-labelledby="testimonials-title">
<div className="mx-auto max-w-site px-6 lg:px-10"> <div className="mx-auto max-w-site px-6 lg:px-10">
<SectionHeading headingKey="testimonials" align="center" /> <SectionHeading headingKey="testimonials" align="center" />
<div className="relative mx-auto mt-14 max-w-3xl text-center"> <div className="relative mx-auto mt-8 max-w-3xl text-center select-none">
<QuoteIcon className="mx-auto h-8 w-8 text-copper/60" aria-hidden="true" /> <QuoteIcon className="mx-auto h-6 w-6 text-copper/60 md:h-8 md:w-8" aria-hidden="true" />
<div className="relative min-h-[170px] overflow-hidden md:min-h-[190px]">
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<motion.blockquote <motion.blockquote
key={current.id} key={current.id}
initial={{ opacity: 0, y: 16 }} drag="x"
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.2}
onDragEnd={handleDragEnd}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }} exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}> transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}
className="cursor-grab active:cursor-grabbing"
>
<EditableItem resource="testimonials" item={current}> <EditableItem resource="testimonials" item={current}>
<p className="mt-8 text-lg font-light leading-[2.4rem] text-ivory/85 md:text-xl">«{current.quote}»</p> <p className="mt-5 text-base font-light leading-8 text-ivory/85 md:mt-8 md:text-xl md:leading-[2.4rem]">
<footer className="mt-8"> «{current.quote}»
<p className="text-base font-bold text-copper">{current.name}</p> </p>
<p className="mt-1 text-sm font-light text-ivory/45">{current.role}</p> <footer className="mt-5 md:mt-8">
<p className="text-sm font-bold text-copper md:text-base">{current.name}</p>
<p className="mt-0.5 text-xs font-light text-ivory/45 md:text-sm">{current.role}</p>
</footer> </footer>
</EditableItem> </EditableItem>
</motion.blockquote> </motion.blockquote>
</AnimatePresence> </AnimatePresence>
</div>
<div className="mt-12 flex items-center justify-center gap-5"> <div className="mt-6 flex items-center justify-center gap-5 md:mt-10">
<button <button
type="button" type="button"
onClick={() => go(1)} onClick={() => go(1)}
aria-label={t('prevTestimonial')} aria-label={t('prevTestimonial')}
className="group flex h-12 w-12 items-center justify-center rounded-full border border-copper/35 bg-char/80 text-ivory/80 backdrop-blur-md shadow-[0_4px_20px_rgba(0,0,0,0.35)] transition-all duration-300 ease-lux hover:scale-110 hover:border-copper hover:bg-copper hover:text-char hover:shadow-[0_0_25px_rgba(197,143,92,0.45)] active:scale-95" className="group flex h-10 w-10 items-center justify-center rounded-full border border-copper/35 bg-char/80 text-ivory/80 shadow-[0_4px_20px_rgba(0,0,0,0.35)] backdrop-blur-md transition-all duration-300 ease-lux hover:scale-110 hover:border-copper hover:bg-copper hover:text-char hover:shadow-[0_0_25px_rgba(197,143,92,0.45)] active:scale-95 md:h-12 md:w-12"
> >
<ChevronRightIcon className="h-5 w-5 ltr:rotate-180 transition-transform duration-300 ease-lux rtl:group-hover:translate-x-0.5 ltr:group-hover:-translate-x-0.5" /> <ChevronRightIcon className="h-4 w-4 transition-transform duration-300 ease-lux ltr:rotate-180 rtl:group-hover:translate-x-0.5 ltr:group-hover:-translate-x-0.5 md:h-5 md:w-5" />
</button> </button>
<div className="flex items-center gap-2" role="tablist" aria-label={t('selectTestimonialAria')}> <div className="flex items-center gap-2" role="tablist" aria-label={t('selectTestimonialAria')}>
@@ -63,8 +81,8 @@ export function Testimonials() {
onClick={() => setIndex(i)} onClick={() => setIndex(i)}
className={`h-2 rounded-full transition-all duration-300 ease-lux ${ className={`h-2 rounded-full transition-all duration-300 ease-lux ${
i === index i === index
? 'w-8 bg-copper shadow-[0_0_12px_rgba(197,143,92,0.6)]' ? 'w-7 bg-copper shadow-[0_0_12px_rgba(197,143,92,0.6)] md:w-8'
: 'w-2.5 bg-ivory/20 hover:bg-ivory/40' : 'w-2 bg-ivory/20 hover:bg-ivory/40 md:w-2.5'
}`} }`}
/> />
))} ))}
@@ -74,9 +92,9 @@ export function Testimonials() {
type="button" type="button"
onClick={() => go(-1)} onClick={() => go(-1)}
aria-label={t('nextTestimonial')} aria-label={t('nextTestimonial')}
className="group flex h-12 w-12 items-center justify-center rounded-full border border-copper/35 bg-char/80 text-ivory/80 backdrop-blur-md shadow-[0_4px_20px_rgba(0,0,0,0.35)] transition-all duration-300 ease-lux hover:scale-110 hover:border-copper hover:bg-copper hover:text-char hover:shadow-[0_0_25px_rgba(197,143,92,0.45)] active:scale-95" className="group flex h-10 w-10 items-center justify-center rounded-full border border-copper/35 bg-char/80 text-ivory/80 shadow-[0_4px_20px_rgba(0,0,0,0.35)] backdrop-blur-md transition-all duration-300 ease-lux hover:scale-110 hover:border-copper hover:bg-copper hover:text-char hover:shadow-[0_0_25px_rgba(197,143,92,0.45)] active:scale-95 md:h-12 md:w-12"
> >
<ChevronLeftIcon className="h-5 w-5 ltr:rotate-180 transition-transform duration-300 ease-lux rtl:group-hover:-translate-x-0.5 ltr:group-hover:translate-x-0.5" /> <ChevronLeftIcon className="h-4 w-4 transition-transform duration-300 ease-lux ltr:rotate-180 rtl:group-hover:-translate-x-0.5 ltr:group-hover:translate-x-0.5 md:h-5 md:w-5" />
</button> </button>
</div> </div>
</div> </div>
+54 -18
View File
@@ -1,4 +1,4 @@
import React, { useRef } from 'react' import { useRef } from 'react'
import { motion, useScroll, useTransform, useInView } from 'framer-motion' import { motion, useScroll, useTransform, useInView } from 'framer-motion'
import { useSite, type TimelineItem } from '../../api' import { useSite, type TimelineItem } from '../../api'
import { useLang } from '../../i18n/lang' import { useLang } from '../../i18n/lang'
@@ -9,17 +9,17 @@ const ease = [0.23, 1, 0.32, 1] as const
type Milestone = TimelineItem type Milestone = TimelineItem
function Item({ item, index }: { item: Milestone; index: number }) { function DesktopTimelineItem({ item, index }: { item: Milestone; index: number }) {
const ref = useRef<HTMLLIElement>(null) const ref = useRef<HTMLLIElement>(null)
const active = useInView(ref, { once: true, amount: 0.6 }) const active = useInView(ref, { once: true, amount: 0.6 })
const onRight = index % 2 === 0 const onRight = index % 2 === 0
return ( return (
<li ref={ref} className="relative md:grid md:grid-cols-2 md:gap-16"> <li ref={ref} className="relative md:grid md:grid-cols-2 md:gap-16">
{/* دایره‌ها: مرکز دایره در موبایل و دسکتاپ دقیقاً روی مرکز خط 2px است */} {/* دایره‌ها: مرکز دایره دقیقاً روی مرکز خط 2px است */}
<span <span
aria-hidden="true" aria-hidden="true"
className={`absolute right-[-5px] top-8 z-10 h-3 w-3 rounded-full transition-all duration-500 ease-lux md:right-1/2 md:translate-x-1/2 ${ className={`absolute right-1/2 top-8 z-10 h-3 w-3 translate-x-1/2 rounded-full transition-all duration-500 ease-lux ${
active active
? 'bg-copper shadow-[0_0_15px_rgba(197,143,92,0.6),0_0_0_6px_rgba(213,135,76,0.15)]' ? 'bg-copper shadow-[0_0_15px_rgba(197,143,92,0.6),0_0_0_6px_rgba(213,135,76,0.15)]'
: 'bg-ivory/20' : 'bg-ivory/20'
@@ -27,15 +27,13 @@ function Item({ item, index }: { item: Milestone; index: number }) {
/> />
<motion.div <motion.div
className={`pr-8 md:pr-0 ${ className={onRight ? 'md:col-start-1 md:pl-16 md:text-left' : 'md:col-start-2 md:pr-16'}
onRight ? 'md:col-start-1 md:pl-16 md:text-left' : 'md:col-start-2 md:pr-16'
}`}
initial={{ opacity: 0, y: 24 }} initial={{ opacity: 0, y: 24 }}
animate={active ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }} animate={active ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
transition={{ duration: 0.55, ease }} transition={{ duration: 0.55, ease }}
> >
<EditableItem resource="timeline" item={item}> <EditableItem resource="timeline" item={item}>
<div className="group relative overflow-hidden rounded-2xl border border-ivory/10 bg-ink/40 p-6 backdrop-blur-md transition-all duration-500 ease-lux hover:-translate-y-1.5 hover:border-copper/50 hover:bg-ink/60 hover:shadow-[0_12px_40px_-10px_rgba(197,143,92,0.35)] md:p-8"> <div className="group relative overflow-hidden rounded-2xl border border-ivory/10 bg-ink/40 p-8 backdrop-blur-md transition-all duration-500 ease-lux hover:-translate-y-1.5 hover:border-copper/50 hover:bg-ink/60 hover:shadow-[0_12px_40px_-10px_rgba(197,143,92,0.35)]">
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.03] to-transparent opacity-0 transition-opacity duration-500 group-hover:opacity-100" /> <div className="absolute inset-0 bg-gradient-to-br from-white/[0.03] to-transparent opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
<span className="relative z-10 text-sm font-medium tracking-widest text-copper">{item.year}</span> <span className="relative z-10 text-sm font-medium tracking-widest text-copper">{item.year}</span>
@@ -51,18 +49,19 @@ function Item({ item, index }: { item: Milestone; index: number }) {
export function Timeline() { export function Timeline() {
const { timeline } = useSite() const { timeline } = useSite()
const { t } = useLang() const { t } = useLang()
const ref = useRef<HTMLDivElement>(null)
const desktopRef = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({ const { scrollYProgress } = useScroll({
target: ref, target: desktopRef,
offset: ['start 70%', 'end 60%'], offset: ['start 70%', 'end 60%'],
}) })
const scaleY = useTransform(scrollYProgress, [0, 1], [0, 1]) const scaleY = useTransform(scrollYProgress, [0, 1], [0, 1])
return ( return (
<section className="bg-char py-24 md:py-32" aria-labelledby="timeline-title"> <section className="w-full max-w-full overflow-x-clip bg-char py-16 md:py-24 lg:py-32" aria-labelledby="timeline-title">
<div className="mx-auto w-full max-w-site px-6 lg:px-10"> <div className="mx-auto w-full max-w-site px-4 md:px-6 lg:px-10">
<div className="mb-16"> <div className="mb-8 md:mb-16">
<SectionHeading <SectionHeading
title={t('fallbackTimelineTitle')} title={t('fallbackTimelineTitle')}
lead={t('fallbackTimelineLead')} lead={t('fallbackTimelineLead')}
@@ -71,28 +70,65 @@ export function Timeline() {
/> />
</div> </div>
<div ref={ref} className="relative mx-auto mt-20 max-w-4xl"> {/* Mobile View: Minimalist Editorial Spine */}
<div className="block md:hidden px-5 py-4">
<div className="relative rtl:pr-6 rtl:border-r ltr:pl-6 ltr:border-l border-copper/25 space-y-6 my-2">
{timeline.map((item) => (
<EditableItem key={item.id} resource="timeline" item={item}>
<div className="relative group">
{/* Timeline Node on the vertical spine */}
<span
aria-hidden="true"
className="absolute rtl:-right-[29px] ltr:-left-[29px] top-1.5 h-2.5 w-2.5 rounded-full bg-copper ring-4 ring-copper/15"
/>
{/* Milestone Content */}
<div className="p-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-[11px] font-bold tracking-wider text-copper bg-copper/10 px-2 py-0.5 rounded-md">
{item.year}
</span>
<h3 className="text-sm font-semibold text-ivory">
{item.title}
</h3>
</div>
<p className="text-xs text-ivory/60 leading-relaxed line-clamp-2 rtl:pr-0.5 ltr:pl-0.5">
{item.desc}
</p>
</div>
</div>
</EditableItem>
))}
</div>
<div className="mt-6">
<AddItemButton resource="timeline" />
</div>
</div>
{/* Desktop View: Original Vertical Timeline */}
<div ref={desktopRef} className="relative mx-auto mt-20 hidden max-w-4xl md:block">
{/* خط پس‌زمینه ثابت - عرض 2px و همراستایی دقیق با مرکز */} {/* خط پس‌زمینه ثابت - عرض 2px و همراستایی دقیق با مرکز */}
<span <span
aria-hidden="true" aria-hidden="true"
className="absolute inset-y-0 right-0 w-[2px] bg-ivory/10 md:right-1/2 md:translate-x-1/2" className="absolute inset-y-0 right-1/2 w-[2px] translate-x-1/2 bg-ivory/10"
/> />
{/* خط متحرک - عرض 2px و همراستایی دقیق با مرکز */} {/* خط متحرک - عرض 2px و همراستایی دقیق با مرکز */}
<motion.span <motion.span
aria-hidden="true" aria-hidden="true"
className="absolute inset-y-0 right-0 w-[2px] origin-top bg-gradient-to-b from-copper/80 via-copper to-copper/20 shadow-[0_0_10px_rgba(197,143,92,0.5)] md:right-1/2 md:translate-x-1/2" className="absolute inset-y-0 right-1/2 w-[2px] origin-top translate-x-1/2 bg-gradient-to-b from-copper/80 via-copper to-copper/20 shadow-[0_0_10px_rgba(197,143,92,0.5)]"
style={{ scaleY }} style={{ scaleY }}
/> />
<ol className="flex flex-col gap-12 md:gap-16"> <ol className="flex flex-col gap-16">
{timeline.map((item, i) => ( {timeline.map((item, i) => (
<Item key={item.id} item={item} index={i} /> <DesktopTimelineItem key={item.id} item={item} index={i} />
))} ))}
</ol> </ol>
<div className="mt-12"> <div className="mt-12">
<AddItemButton resource="timeline" /> <AddItemButton resource="timeline" />
</div> </div>
</div> </div>
</div> </div>
</section> </section>
) )
+58 -11
View File
@@ -1,8 +1,7 @@
import React from 'react';
import { Reveal } from '../Reveal'; import { Reveal } from '../Reveal';
import { SectionHeading } from '../SectionHeading'; import { SectionHeading } from '../SectionHeading';
import { useSectionHeading, useSite } from '../../api'; import { useSectionHeading, useSite } from '../../api';
import { AddItemButton, EditableItem, SingletonPill } from '../../admin/Editable'; import { AddItemButton, EditableItem } from '../../admin/Editable';
import { InlineEdit } from '../admin/InlineEdit'; import { InlineEdit } from '../admin/InlineEdit';
import { useFieldSaver } from '../../admin/AdminContext'; import { useFieldSaver } from '../../admin/AdminContext';
import { useLang, formatNumber } from '../../i18n/lang'; import { useLang, formatNumber } from '../../i18n/lang';
@@ -19,9 +18,15 @@ export function WhyUs() {
if (it) Object.assign(it, changes); if (it) Object.assign(it, changes);
}); });
const formatIndex = (index: number) => {
return lang === 'fa'
? `۰${formatNumber(index + 1, lang)}`
: String(index + 1).padStart(2, '0');
};
return ( return (
<section className="bg-ink py-24 lg:py-32" aria-labelledby="why-title"> <section className="w-full max-w-full overflow-x-clip bg-ink py-16 md:py-24 lg:py-32" aria-labelledby="why-title">
<div className="mx-auto grid max-w-site gap-14 px-6 lg:grid-cols-12 lg:px-10"> <div className="mx-auto grid max-w-site gap-8 px-4 md:gap-14 md:px-6 lg:grid-cols-12 lg:px-10">
<div className="lg:col-span-5"> <div className="lg:col-span-5">
<SectionHeading <SectionHeading
title={heading.title} title={heading.title}
@@ -33,19 +38,60 @@ export function WhyUs() {
<img <img
src={heading.image} src={heading.image}
alt={heading.title} alt={heading.title}
className="mt-10 h-80 w-full object-cover" /> className="mt-6 hidden h-80 w-full object-cover md:mt-10 md:block" />
</Reveal> </Reveal>
)} )}
</div> </div>
<div className="lg:col-span-7"> <div className="lg:col-span-7">
{/* Mobile 2-Column Luxury Micro-Card Grid */}
<div className="block md:hidden px-3.5 py-2">
<div className="grid grid-cols-2 gap-2.5 items-stretch">
{why_us.map((item, i) => (
<EditableItem key={item.id} resource="why_us" item={item} className="h-full">
<div className="h-full p-3 rounded-xl border border-ivory/10 bg-char/40 flex flex-col justify-between hover:border-copper/40 transition-all duration-200">
<div>
{/* Top Row: Index Badge */}
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-mono font-bold text-copper tracking-wider px-1.5 py-0.5 rounded bg-copper/10 border border-copper/20">
{`0${i + 1}`}
</span>
</div>
{/* Title */}
<h4 className="text-xs font-bold text-ivory mb-1 line-clamp-1 leading-snug">
<InlineEdit
value={item.title}
onSave={(v) => saveItem(item.id, { title: v })}
/>
</h4>
{/* Description */}
<p className="text-[10px] text-ivory/60 leading-relaxed line-clamp-2">
<InlineEdit
multiline
value={item.desc}
onSave={(v) => saveItem(item.id, { desc: v })}
/>
</p>
</div>
</div>
</EditableItem>
))}
</div>
<div className="mt-6">
<AddItemButton resource="why_us" />
</div>
</div>
{/* Desktop Grid Layout */}
<div className="hidden md:block">
<div className="grid gap-x-10 gap-y-px sm:grid-cols-2"> <div className="grid gap-x-10 gap-y-px sm:grid-cols-2">
{why_us.map((item, i) => {why_us.map((item, i) => (
<Reveal key={item.id} delay={0.05 * i}> <Reveal key={item.id} delay={0.05 * i}>
<EditableItem resource="why_us" item={item}> <EditableItem resource="why_us" item={item}>
<div className="flex h-full flex-col border-t border-ivory/10 py-8"> <div className="flex h-full flex-col border-t border-ivory/10 py-8">
<span className="text-xs font-light text-copper">{lang === 'fa' ? `۰${formatNumber(i + 1, lang)}` : formatNumber(i + 1, lang).padStart(2, '0')}</span> <span className="text-xs font-light text-copper">{formatIndex(i)}</span>
<h3 className="mt-4 text-lg font-bold text-ivory"> <h3 className="mt-4 text-lg font-bold text-ivory">
<InlineEdit <InlineEdit
value={item.title} value={item.title}
@@ -62,13 +108,14 @@ export function WhyUs() {
</div> </div>
</EditableItem> </EditableItem>
</Reveal> </Reveal>
)} ))}
</div> </div>
<div className="mt-8"> <div className="mt-8">
<AddItemButton resource="why_us" /> <AddItemButton resource="why_us" />
</div> </div>
</div> </div>
</div> </div>
</section>); </div>
</section>
);
} }
BIN
View File
Binary file not shown.