feat(mobile): complete responsive design and layout optimizations across landing page
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ArrowLeftIcon, ClockIcon } from 'lucide-react'
|
||||
import type { Article } from '../data/site'
|
||||
@@ -11,43 +10,43 @@ export function ArticleCard({ article }: { article: Article }) {
|
||||
return (
|
||||
<Link
|
||||
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}`}
|
||||
>
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="relative overflow-hidden aspect-[16/10] w-full">
|
||||
<img
|
||||
src={article.image}
|
||||
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}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col 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">
|
||||
<div className="flex flex-1 flex-col p-3.5 md:p-8">
|
||||
<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}
|
||||
</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}
|
||||
</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">
|
||||
<span className="flex items-center gap-4">
|
||||
<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-2.5 md:gap-4">
|
||||
<span>{formatJalali(article.date)}</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<ClockIcon className="h-3.5 w-3.5 text-copper/80" />
|
||||
<span className="flex items-center gap-1.5 md:gap-2">
|
||||
<ClockIcon className="h-3 w-3 md:h-3.5 md:w-3.5 text-copper/80" />
|
||||
{toPersianDigits(article.read)}
|
||||
</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')}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { CheckCircle2Icon, MailIcon, MapPinIcon, PhoneIcon } from 'lucide-react'
|
||||
import Swal from 'sweetalert2'
|
||||
import { Reveal } from './Reveal'
|
||||
@@ -100,18 +100,139 @@ export function ContactSection() {
|
||||
}
|
||||
}
|
||||
|
||||
const cleanOptionalLabel = `(${content.note_optional_label?.replace(/^\(+|\)+$/g, '') || 'اختیاری'})`
|
||||
|
||||
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
|
||||
src={brand.contact_bg}
|
||||
alt=""
|
||||
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="mx-auto grid max-w-6xl items-stretch gap-8 lg:grid-cols-12">
|
||||
<div className="relative mx-auto max-w-site px-4 md:px-6 lg:px-10">
|
||||
|
||||
{/* 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">
|
||||
@@ -252,7 +373,7 @@ export function ContactSection() {
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<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>
|
||||
<textarea
|
||||
id="contact-note"
|
||||
@@ -281,6 +402,7 @@ export function ContactSection() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowUpIcon, InstagramIcon, LinkedinIcon, MailIcon, MapPinIcon, PhoneIcon, SendIcon } from 'lucide-react';
|
||||
import { useSite } from '../api';
|
||||
@@ -17,6 +17,7 @@ export function Footer() {
|
||||
const { brand, services, nav, footer } = useSite();
|
||||
const { t, href } = useLang();
|
||||
const save = useFieldSaver();
|
||||
const [openSection, setOpenSection] = useState<string | null>(null);
|
||||
|
||||
const saveFooter = (field: string, value: string) =>
|
||||
save('footer', footer.id, { [field]: value }, (d) => {
|
||||
@@ -34,19 +35,22 @@ export function Footer() {
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="relative 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="grid gap-12 lg:grid-cols-12">
|
||||
<div className="lg:col-span-4">
|
||||
<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-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-20 w-auto object-contain" />
|
||||
<SingletonPill resource="brand" label="ویرایش برند، لوگو و تماس" className="absolute top-1/2 -translate-y-1/2 ltr:-right-10 rtl:-left-10" />
|
||||
<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-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)} />
|
||||
<p className="mt-4 text-xs font-light leading-relaxed text-ivory/60 max-w-xs">
|
||||
{footer.about_text}
|
||||
</p>
|
||||
{/* آیکونهای شبکههای اجتماعی: گویهای شیشهای شناور */}
|
||||
<div className="mt-8 flex items-center gap-3">
|
||||
{/* 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 (
|
||||
@@ -54,100 +58,234 @@ export function Footer() {
|
||||
key={label}
|
||||
href={href}
|
||||
aria-label={label}
|
||||
className="group flex h-11 w-11 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:-translate-y-1 hover:border-copper/70 hover:bg-copper/10 hover:text-copper hover:shadow-[0_0_20px_rgba(197,143,92,0.4)] active:scale-95"
|
||||
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-4 w-4 transition-transform duration-300 group-hover:scale-110" />
|
||||
<Icon className="h-3.5 w-3.5 transition-transform duration-300 group-hover:scale-110" />
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
<h3 className="text-sm font-bold text-copper">
|
||||
<InlineEdit value={footer.pages_heading} onSave={(v) => saveFooter('pages_heading', v)} />
|
||||
</h3>
|
||||
<ul className="mt-5 space-y-3">
|
||||
{nav.map((item) => (
|
||||
<EditableItem key={item.id} resource="nav" item={item}>
|
||||
<li>
|
||||
<Link
|
||||
to={href(item.href)}
|
||||
className="inline-block text-sm font-light text-ivory/60 transition-colors duration-200 ease-lux hover:text-copper"
|
||||
>
|
||||
<InlineEdit
|
||||
value={item.label}
|
||||
onSave={(v) => saveNav(item.id, { label: v })}
|
||||
/>
|
||||
</Link>
|
||||
{/* 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>
|
||||
</EditableItem>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-4">
|
||||
<AddItemButton resource="nav" />
|
||||
<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>
|
||||
|
||||
<div className="lg:col-span-3">
|
||||
<h3 className="text-sm font-bold text-copper">
|
||||
<InlineEdit value={footer.services_heading} onSave={(v) => saveFooter('services_heading', v)} />
|
||||
</h3>
|
||||
<ul className="mt-5 space-y-3">
|
||||
{services.map((service) => (
|
||||
<li key={service.id} className="text-sm font-light text-ivory/60">
|
||||
{service.title}
|
||||
{/* 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="lg:col-span-4">
|
||||
<div className="relative w-fit">
|
||||
<img src={brand.logo} alt={brand.name} className="h-20 w-auto object-contain" />
|
||||
<SingletonPill resource="brand" label="ویرایش برند، لوگو و تماس" className="absolute top-1/2 -translate-y-1/2 ltr:-right-10 rtl:-left-10" />
|
||||
</div>
|
||||
<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)} />
|
||||
</p>
|
||||
{/* آیکونهای شبکههای اجتماعی */}
|
||||
<div className="mt-8 flex items-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-11 w-11 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:-translate-y-1 hover:border-copper/70 hover:bg-copper/10 hover:text-copper hover:shadow-[0_0_20px_rgba(197,143,92,0.4)] active:scale-95"
|
||||
>
|
||||
<Icon className="h-4 w-4 transition-transform duration-300 group-hover:scale-110" />
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
<h3 className="text-sm font-bold text-copper">
|
||||
<InlineEdit value={footer.pages_heading} onSave={(v) => saveFooter('pages_heading', v)} />
|
||||
</h3>
|
||||
<ul className="mt-5 space-y-3">
|
||||
{nav.map((item) => (
|
||||
<EditableItem key={item.id} resource="nav" item={item}>
|
||||
<li>
|
||||
<Link
|
||||
to={href(item.href)}
|
||||
className="inline-block text-sm font-light text-ivory/60 transition-colors duration-200 ease-lux hover:text-copper"
|
||||
>
|
||||
<InlineEdit
|
||||
value={item.label}
|
||||
onSave={(v) => saveNav(item.id, { label: v })}
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
</EditableItem>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-4">
|
||||
<AddItemButton resource="nav" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-3">
|
||||
<h3 className="text-sm font-bold text-copper">
|
||||
<InlineEdit value={footer.services_heading} onSave={(v) => saveFooter('services_heading', v)} />
|
||||
</h3>
|
||||
<ul className="mt-5 space-y-3">
|
||||
{services.map((service) => (
|
||||
<li key={service.id} className="text-sm font-light text-ivory/60">
|
||||
{service.title}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-3">
|
||||
<h3 className="text-sm font-bold text-copper">
|
||||
<InlineEdit value={footer.contact_heading} onSave={(v) => saveFooter('contact_heading', v)} />
|
||||
</h3>
|
||||
<ul className="mt-5 space-y-4 text-sm font-light text-ivory/60">
|
||||
<li className="flex items-start gap-3">
|
||||
<MapPinIcon className="mt-1 h-4 w-4 shrink-0 text-copper" />
|
||||
<span className="leading-7">{brand.address}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<li className="flex items-center gap-3">
|
||||
<PhoneIcon className="h-4 w-4 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-3">
|
||||
<MailIcon className="h-4 w-4 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 className="lg:col-span-3">
|
||||
<h3 className="text-sm font-bold text-copper">
|
||||
<InlineEdit value={footer.contact_heading} onSave={(v) => saveFooter('contact_heading', v)} />
|
||||
</h3>
|
||||
<ul className="mt-5 space-y-4 text-sm font-light text-ivory/60">
|
||||
<li className="flex items-start gap-3">
|
||||
<MapPinIcon className="mt-1 h-4 w-4 shrink-0 text-copper" />
|
||||
<span className="leading-7">{brand.address}</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-3">
|
||||
<PhoneIcon className="h-4 w-4 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-3">
|
||||
<MailIcon className="h-4 w-4 shrink-0 text-copper" />
|
||||
<a href={`mailto:${brand.email}`} dir="ltr" className="transition-colors hover:text-copper">
|
||||
{brand.email}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{/* خط پایانی به همراه دکمه کپسولی بازگشت به بالا */}
|
||||
<div className="mt-14 flex flex-col items-center justify-between gap-6 border-t border-white/10 pt-6 text-xs font-light text-ivory/40 md:flex-row">
|
||||
<p>
|
||||
<InlineEdit value={footer.copyright} onSave={(v) => saveFooter('copyright', v)} />
|
||||
</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={scrollToTop}
|
||||
className="group flex items-center gap-2 rounded-full border border-white/15 bg-white/[0.04] px-4 py-2 text-xs font-medium text-ivory/70 backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)] transition-all duration-300 ease-lux hover:border-copper hover:bg-copper/10 hover:text-copper hover:shadow-[0_0_20px_rgba(197,143,92,0.3)] active:scale-95 cursor-pointer"
|
||||
>
|
||||
<span>{t('backToTop')}</span>
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-white/10 text-copper transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:bg-copper group-hover:text-char">
|
||||
<ArrowUpIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<p>
|
||||
<InlineEdit value={footer.tagline} onSave={(v) => saveFooter('tagline', v)} />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* خط پایانی به همراه دکمه کپسولی بازگشت به بالا */}
|
||||
<div className="mt-14 flex flex-col items-center justify-between gap-6 border-t border-white/10 pt-6 text-xs font-light text-ivory/40 md:flex-row">
|
||||
<p>
|
||||
<InlineEdit value={footer.copyright} onSave={(v) => saveFooter('copyright', v)} />
|
||||
</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={scrollToTop}
|
||||
className="group flex items-center gap-2 rounded-full border border-white/15 bg-white/[0.04] px-4 py-2 text-xs font-medium text-ivory/70 backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)] transition-all duration-300 ease-lux hover:border-copper hover:bg-copper/10 hover:text-copper hover:shadow-[0_0_20px_rgba(197,143,92,0.3)] active:scale-95 cursor-pointer"
|
||||
>
|
||||
<span>{t('backToTop')}</span>
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-white/10 text-copper transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:bg-copper group-hover:text-char">
|
||||
<ArrowUpIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<p>
|
||||
<InlineEdit value={footer.tagline} onSave={(v) => saveFooter('tagline', v)} />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -45,31 +45,31 @@ export function ProjectCard({ project, imageUrl, detailHref }: ProjectCardProps)
|
||||
<img
|
||||
src={finalImageUrl}
|
||||
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
|
||||
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)]' : ''}`} />
|
||||
{statusText}
|
||||
</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">
|
||||
<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}
|
||||
</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>
|
||||
|
||||
{area && (
|
||||
<>
|
||||
<div className="my-6 h-px w-full bg-white/10" />
|
||||
<div className="mt-auto flex items-center justify-end text-sm font-light text-ivory/60">
|
||||
<div className="my-3 h-px w-full bg-white/10 md:my-6" />
|
||||
<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>
|
||||
</div>
|
||||
@@ -79,7 +79,7 @@ export function ProjectCard({ project, imageUrl, detailHref }: ProjectCardProps)
|
||||
</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">
|
||||
{t('viewProject')}
|
||||
</PremiumButton>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Reveal } from '../Reveal';
|
||||
import { PremiumButton } from '../PremiumButton';
|
||||
import { ArticleCard } from '../ArticleCard';
|
||||
@@ -8,6 +7,7 @@ import { useLang } from '../../i18n/lang';
|
||||
import { useFieldSaver } from '../../admin/AdminContext';
|
||||
import { InlineEdit } from '../admin/InlineEdit';
|
||||
import { AddItemButton, EditableItem } from '../../admin/Editable';
|
||||
import { formatJalali } from '../../utils/date';
|
||||
|
||||
export function Articles() {
|
||||
const { articles } = useSite();
|
||||
@@ -17,6 +17,9 @@ export function Articles() {
|
||||
|
||||
// پیشنویسها فقط در صفحه وبلاگ (برای ادمین) دیده میشوند، نه در خانه
|
||||
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) =>
|
||||
save('section-headings', heading.id, { [field]: value }, (d) => {
|
||||
@@ -25,24 +28,97 @@ export function Articles() {
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="bg-char py-24 lg:py-32" aria-labelledby="articles-title">
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
<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-4 md:px-6 lg:px-10">
|
||||
<Reveal className="max-w-2xl ltr:text-left rtl:text-right">
|
||||
<p className="text-sm font-light text-copper">
|
||||
<InlineEdit value={heading.label} onSave={(v) => saveHeading('label', v)} />
|
||||
</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)} />
|
||||
</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)} />
|
||||
</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>
|
||||
|
||||
<div className="mt-14 grid gap-6 lg:grid-cols-3">
|
||||
{published.slice(0, 3).map((article, i) =>
|
||||
<Reveal key={article.id} delay={0.06 * i} className="h-full">
|
||||
{/* Mobile View: Magazine Editorial layout (1 Featured Spotlight + Compact Rows) */}
|
||||
<div className="block md:hidden mt-8">
|
||||
{/* 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}`}>
|
||||
<ArticleCard article={article} />
|
||||
</EditableItem>
|
||||
@@ -52,7 +128,7 @@ export function Articles() {
|
||||
|
||||
{heading.cta_label && heading.cta_href && (
|
||||
<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">
|
||||
<InlineEdit value={heading.cta_label} onSave={(v) => saveHeading('cta_label', v)} />
|
||||
</PremiumButton>
|
||||
@@ -63,6 +139,6 @@ export function Articles() {
|
||||
<AddItemButton resource="articles" />
|
||||
</div>
|
||||
</div>
|
||||
</section>);
|
||||
|
||||
}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { Reveal } from '../Reveal'
|
||||
import { PremiumButton } from '../PremiumButton'
|
||||
@@ -39,18 +38,18 @@ export function FeaturedProjects() {
|
||||
const BASE_IMG_URL = "/seed-media/2026/09/";
|
||||
|
||||
return (
|
||||
<section className="bg-ink py-24 lg:py-32" aria-labelledby="projects-title">
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
<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-4 md:px-6 lg:px-10">
|
||||
<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) => {
|
||||
const isSelected = status === filter.key
|
||||
return (
|
||||
<React.Fragment key={filter.key}>
|
||||
{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
|
||||
@@ -58,7 +57,7 @@ export function FeaturedProjects() {
|
||||
role="tab"
|
||||
aria-selected={isSelected}
|
||||
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
|
||||
? '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'
|
||||
@@ -76,7 +75,7 @@ export function FeaturedProjects() {
|
||||
})}
|
||||
</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">
|
||||
<motion.div
|
||||
key={status}
|
||||
@@ -84,7 +83,7 @@ export function FeaturedProjects() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -15 }}
|
||||
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) => {
|
||||
const dummyImageName = `${(i % 6) + 1}.png`;
|
||||
@@ -96,7 +95,7 @@ export function FeaturedProjects() {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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
|
||||
project={project}
|
||||
@@ -113,7 +112,7 @@ export function FeaturedProjects() {
|
||||
{/* دکمه مشاهده همه پروژهها */}
|
||||
{heading.cta_label && heading.cta_href && (
|
||||
<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)}>
|
||||
<InlineEdit
|
||||
value={heading.cta_label}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import { Reveal } from '../Reveal'
|
||||
import { ServiceIcon } from '../ServiceIcon'
|
||||
import { useSectionHeading, useSite } from '../../api'
|
||||
@@ -18,32 +17,35 @@ export function Services() {
|
||||
})
|
||||
|
||||
return (
|
||||
<section className="bg-ink py-24 lg:py-32" aria-labelledby="services-title">
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
<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-4 md:px-6 lg:px-10">
|
||||
<Reveal className="max-w-2xl text-start">
|
||||
|
||||
<h2 id="services-title" className="text-section font-bold text-ivory">
|
||||
<InlineEdit value={heading.title} onSave={(v) => saveHeading('title', v)} />
|
||||
</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)} />
|
||||
</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>
|
||||
|
||||
<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) => (
|
||||
<Reveal key={service.id} delay={0.04 * i} 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">
|
||||
<ServiceIcon
|
||||
name={service.icon}
|
||||
className="h-12 w-12 text-copper/85 transition-colors duration-300 ease-out group-hover:text-copper"
|
||||
/>
|
||||
<h3 className="mt-6 text-lg font-bold text-ivory transition-colors duration-300 ease-out group-hover:text-copper">
|
||||
<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
|
||||
name={service.icon}
|
||||
className="h-10 w-10 text-copper/85 transition-colors duration-300 ease-out group-hover:text-copper md:h-14 md:w-14"
|
||||
/>
|
||||
</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}
|
||||
</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>
|
||||
</EditableItem>
|
||||
</Reveal>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useSite } from '../../api';
|
||||
import { useLang, formatNumber } from '../../i18n/lang';
|
||||
import { useFieldSaver } from '../../admin/AdminContext';
|
||||
@@ -71,12 +71,12 @@ export function Stats() {
|
||||
const save = useFieldSaver();
|
||||
|
||||
return (
|
||||
<section className="bg-char 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">
|
||||
<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-3.5 p-4 md:grid-cols-4 md:gap-y-12 md:px-10 md:py-0">
|
||||
{stats.map((stat) =>
|
||||
<EditableItem key={stat.id} resource="stats" item={stat}>
|
||||
<div className="text-center">
|
||||
<p className="text-4xl font-bold text-copper md:text-5xl">
|
||||
<div className="p-3.5 text-center md:p-0">
|
||||
<p className="text-2xl font-bold text-copper md:text-5xl">
|
||||
<Counter value={stat.value} />
|
||||
<InlineEdit
|
||||
value={stat.suffix}
|
||||
@@ -84,10 +84,10 @@ export function Stats() {
|
||||
const s = d.stats.find((x) => x.id === stat.id);
|
||||
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 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
|
||||
value={stat.label}
|
||||
onSave={(v) => save('stats', stat.id, { label: v }, (d) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import { Reveal } from '../Reveal'
|
||||
import { SectionHeading } from '../SectionHeading'
|
||||
import { useSite } from '../../api'
|
||||
@@ -9,8 +8,8 @@ export function Team() {
|
||||
const { team } = useSite()
|
||||
const { t } = useLang()
|
||||
return (
|
||||
<section className="bg-ink py-24 lg:py-32" aria-labelledby="team-title" >
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10 ">
|
||||
<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-4 md:px-6 lg:px-10">
|
||||
<SectionHeading
|
||||
title={t('fallbackTeamTitle')}
|
||||
lead={t('fallbackTeamLead')}
|
||||
@@ -18,18 +17,17 @@ export function Team() {
|
||||
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) => (
|
||||
<Reveal key={member.id} delay={0.06 * i} className="h-full">
|
||||
<EditableItem resource="team" item={member} className="h-full">
|
||||
{/* اضافه شدن rounded-2xl برای گرد شدن گوشهها */}
|
||||
<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-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">
|
||||
<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">
|
||||
<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">
|
||||
{member.name.trim().charAt(0)}
|
||||
</div>
|
||||
<h3 className="mt-8 text-lg font-bold text-ivory">{member.name}</h3>
|
||||
<p className="mt-2 text-sm font-light text-copper">{member.role}</p>
|
||||
<p className="mt-6 border-t border-ivory/10 pt-6 text-sm font-light text-ivory/60">
|
||||
<h3 className="mt-2 text-xs font-bold text-ivory md:mt-8 md:text-lg">{member.name}</h3>
|
||||
<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-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}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { SectionHeading } from '../SectionHeading';
|
||||
import { useSite } from '../../api';
|
||||
@@ -17,39 +17,57 @@ export function Testimonials() {
|
||||
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 (
|
||||
<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">
|
||||
<SectionHeading headingKey="testimonials" align="center" />
|
||||
|
||||
<div className="relative mx-auto mt-14 max-w-3xl text-center">
|
||||
<QuoteIcon className="mx-auto h-8 w-8 text-copper/60" aria-hidden="true" />
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.blockquote
|
||||
key={current.id}
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}>
|
||||
<div className="relative mx-auto mt-8 max-w-3xl text-center select-none">
|
||||
<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">
|
||||
<motion.blockquote
|
||||
key={current.id}
|
||||
drag="x"
|
||||
dragConstraints={{ left: 0, right: 0 }}
|
||||
dragElastic={0.2}
|
||||
onDragEnd={handleDragEnd}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}
|
||||
className="cursor-grab active:cursor-grabbing"
|
||||
>
|
||||
<EditableItem resource="testimonials" item={current}>
|
||||
<p className="mt-5 text-base font-light leading-8 text-ivory/85 md:mt-8 md:text-xl md:leading-[2.4rem]">
|
||||
«{current.quote}»
|
||||
</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>
|
||||
</EditableItem>
|
||||
</motion.blockquote>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<footer className="mt-8">
|
||||
<p className="text-base font-bold text-copper">{current.name}</p>
|
||||
<p className="mt-1 text-sm font-light text-ivory/45">{current.role}</p>
|
||||
</footer>
|
||||
</EditableItem>
|
||||
</motion.blockquote>
|
||||
</AnimatePresence>
|
||||
|
||||
<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
|
||||
type="button"
|
||||
onClick={() => go(1)}
|
||||
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>
|
||||
|
||||
<div className="flex items-center gap-2" role="tablist" aria-label={t('selectTestimonialAria')}>
|
||||
@@ -63,8 +81,8 @@ export function Testimonials() {
|
||||
onClick={() => setIndex(i)}
|
||||
className={`h-2 rounded-full transition-all duration-300 ease-lux ${
|
||||
i === index
|
||||
? 'w-8 bg-copper shadow-[0_0_12px_rgba(197,143,92,0.6)]'
|
||||
: 'w-2.5 bg-ivory/20 hover:bg-ivory/40'
|
||||
? 'w-7 bg-copper shadow-[0_0_12px_rgba(197,143,92,0.6)] md:w-8'
|
||||
: 'w-2 bg-ivory/20 hover:bg-ivory/40 md:w-2.5'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
@@ -74,9 +92,9 @@ export function Testimonials() {
|
||||
type="button"
|
||||
onClick={() => go(-1)}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import { motion, useScroll, useTransform, useInView } from 'framer-motion'
|
||||
import { useSite, type TimelineItem } from '../../api'
|
||||
import { useLang } from '../../i18n/lang'
|
||||
@@ -9,17 +9,17 @@ const ease = [0.23, 1, 0.32, 1] as const
|
||||
|
||||
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 active = useInView(ref, { once: true, amount: 0.6 })
|
||||
const onRight = index % 2 === 0
|
||||
|
||||
return (
|
||||
<li ref={ref} className="relative md:grid md:grid-cols-2 md:gap-16">
|
||||
{/* دایرهها: مرکز دایره در موبایل و دسکتاپ دقیقاً روی مرکز خط 2px است */}
|
||||
{/* دایرهها: مرکز دایره دقیقاً روی مرکز خط 2px است */}
|
||||
<span
|
||||
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
|
||||
? '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'
|
||||
@@ -27,15 +27,13 @@ function Item({ item, index }: { item: Milestone; index: number }) {
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className={`pr-8 md:pr-0 ${
|
||||
onRight ? 'md:col-start-1 md:pl-16 md:text-left' : 'md:col-start-2 md:pr-16'
|
||||
}`}
|
||||
className={onRight ? 'md:col-start-1 md:pl-16 md:text-left' : 'md:col-start-2 md:pr-16'}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={active ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
|
||||
transition={{ duration: 0.55, ease }}
|
||||
>
|
||||
<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" />
|
||||
|
||||
<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() {
|
||||
const { timeline } = useSite()
|
||||
const { t } = useLang()
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const desktopRef = useRef<HTMLDivElement>(null)
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: ref,
|
||||
target: desktopRef,
|
||||
offset: ['start 70%', 'end 60%'],
|
||||
})
|
||||
const scaleY = useTransform(scrollYProgress, [0, 1], [0, 1])
|
||||
|
||||
return (
|
||||
<section className="bg-char py-24 md:py-32" aria-labelledby="timeline-title">
|
||||
<div className="mx-auto w-full max-w-site px-6 lg:px-10">
|
||||
<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-4 md:px-6 lg:px-10">
|
||||
|
||||
<div className="mb-16">
|
||||
<div className="mb-8 md:mb-16">
|
||||
<SectionHeading
|
||||
title={t('fallbackTimelineTitle')}
|
||||
lead={t('fallbackTimelineLead')}
|
||||
@@ -71,28 +70,65 @@ export function Timeline() {
|
||||
/>
|
||||
</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 و همراستایی دقیق با مرکز */}
|
||||
<span
|
||||
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 و همراستایی دقیق با مرکز */}
|
||||
<motion.span
|
||||
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 }}
|
||||
/>
|
||||
|
||||
<ol className="flex flex-col gap-12 md:gap-16">
|
||||
<ol className="flex flex-col gap-16">
|
||||
{timeline.map((item, i) => (
|
||||
<Item key={item.id} item={item} index={i} />
|
||||
<DesktopTimelineItem key={item.id} item={item} index={i} />
|
||||
))}
|
||||
</ol>
|
||||
<div className="mt-12">
|
||||
<AddItemButton resource="timeline" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Reveal } from '../Reveal';
|
||||
import { SectionHeading } from '../SectionHeading';
|
||||
import { useSectionHeading, useSite } from '../../api';
|
||||
import { AddItemButton, EditableItem, SingletonPill } from '../../admin/Editable';
|
||||
import { AddItemButton, EditableItem } from '../../admin/Editable';
|
||||
import { InlineEdit } from '../admin/InlineEdit';
|
||||
import { useFieldSaver } from '../../admin/AdminContext';
|
||||
import { useLang, formatNumber } from '../../i18n/lang';
|
||||
@@ -19,9 +18,15 @@ export function WhyUs() {
|
||||
if (it) Object.assign(it, changes);
|
||||
});
|
||||
|
||||
const formatIndex = (index: number) => {
|
||||
return lang === 'fa'
|
||||
? `۰${formatNumber(index + 1, lang)}`
|
||||
: String(index + 1).padStart(2, '0');
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="bg-ink 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">
|
||||
<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-8 px-4 md:gap-14 md:px-6 lg:grid-cols-12 lg:px-10">
|
||||
<div className="lg:col-span-5">
|
||||
<SectionHeading
|
||||
title={heading.title}
|
||||
@@ -33,42 +38,84 @@ export function WhyUs() {
|
||||
<img
|
||||
src={heading.image}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-7">
|
||||
<div className="grid gap-x-10 gap-y-px sm:grid-cols-2">
|
||||
{why_us.map((item, i) =>
|
||||
<Reveal key={item.id} delay={0.05 * i}>
|
||||
<EditableItem resource="why_us" item={item}>
|
||||
<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>
|
||||
<h3 className="mt-4 text-lg font-bold text-ivory">
|
||||
<InlineEdit
|
||||
value={item.title}
|
||||
onSave={(v) => saveItem(item.id, { title: v })}
|
||||
/>
|
||||
</h3>
|
||||
<p className="mt-3 text-sm font-light leading-8 text-ivory/55">
|
||||
<InlineEdit
|
||||
multiline
|
||||
value={item.desc}
|
||||
onSave={(v) => saveItem(item.id, { desc: v })}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</EditableItem>
|
||||
</Reveal>
|
||||
)}
|
||||
{/* 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>
|
||||
<div className="mt-8">
|
||||
<AddItemButton resource="why_us" />
|
||||
|
||||
{/* Desktop Grid Layout */}
|
||||
<div className="hidden md:block">
|
||||
<div className="grid gap-x-10 gap-y-px sm:grid-cols-2">
|
||||
{why_us.map((item, i) => (
|
||||
<Reveal key={item.id} delay={0.05 * i}>
|
||||
<EditableItem resource="why_us" item={item}>
|
||||
<div className="flex h-full flex-col border-t border-ivory/10 py-8">
|
||||
<span className="text-xs font-light text-copper">{formatIndex(i)}</span>
|
||||
<h3 className="mt-4 text-lg font-bold text-ivory">
|
||||
<InlineEdit
|
||||
value={item.title}
|
||||
onSave={(v) => saveItem(item.id, { title: v })}
|
||||
/>
|
||||
</h3>
|
||||
<p className="mt-3 text-sm font-light leading-8 text-ivory/55">
|
||||
<InlineEdit
|
||||
multiline
|
||||
value={item.desc}
|
||||
onSave={(v) => saveItem(item.id, { desc: v })}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</EditableItem>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<AddItemButton resource="why_us" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>);
|
||||
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user