merge(mobile): ادغام شاخهٔ new-one2 — طراحی ریسپانسیو صفحهٔ اصلی (کلاسهای breakpoint در همهٔ بخشها) با حفظ تغییرات طراحی ما: تیم وسطچین بدون دایره، پروژههای شاخص بدون تب، تناوب پسزمینه (tone)، پسزمینهٔ پیوستهٔ تماس و منطق دوزبانهٔ مقالات
This commit is contained in:
@@ -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,45 +10,45 @@ 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-52 md:h-full w-full object-cover transition-transform duration-700 ease-lux group-hover:scale-105"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
{/* برچسب دستهبندی کپسولی شیشهای روی تصویر */}
|
{/* برچسب دستهبندی کپسولی شیشهای روی تصویر */}
|
||||||
<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>{lang === 'en' ? formatEnDate(article.date) : formatJalali(article.date)}</span>
|
<span>{lang === 'en' ? formatEnDate(article.date) : 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" />
|
||||||
{lang === 'en' ? article.read : toPersianDigits(article.read)}
|
{lang === 'en' ? 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>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Reveal } from './Reveal'
|
|||||||
import { Button } from './Button'
|
import { Button } from './Button'
|
||||||
import { useSite } from '../api'
|
import { useSite } from '../api'
|
||||||
import { useLang } from '../i18n/lang'
|
import { useLang } from '../i18n/lang'
|
||||||
|
import { track } from '../analytics'
|
||||||
import { useFieldSaver } from '../admin/AdminContext'
|
import { useFieldSaver } from '../admin/AdminContext'
|
||||||
import { InlineEdit } from './admin/InlineEdit'
|
import { InlineEdit } from './admin/InlineEdit'
|
||||||
import { SingletonPill } from '../admin/Editable'
|
import { SingletonPill } from '../admin/Editable'
|
||||||
@@ -14,7 +15,7 @@ export function ContactSection({ bare = false }: { bare?: boolean } = {}) {
|
|||||||
const { brand, contact_section: content } = useSite()
|
const { brand, contact_section: content } = useSite()
|
||||||
const { t } = useLang()
|
const { t } = useLang()
|
||||||
const save = useFieldSaver()
|
const save = useFieldSaver()
|
||||||
const [form, setForm] = useState({ name: '', phone: '', note: '' })
|
const [form, setForm] = useState({ name: '', phone: '', note: '', website: '' })
|
||||||
const [sent, setSent] = useState(false)
|
const [sent, setSent] = useState(false)
|
||||||
const [sending, setSending] = useState(false)
|
const [sending, setSending] = useState(false)
|
||||||
|
|
||||||
@@ -98,6 +99,8 @@ export function ContactSection({ bare = false }: { bare?: boolean } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 overflow-hidden py-24 lg:py-32" aria-labelledby="contact-title">
|
||||||
{!bare && (
|
{!bare && (
|
||||||
@@ -112,8 +115,127 @@ export function ContactSection({ bare = false }: { bare?: boolean } = {}) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<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">
|
||||||
@@ -204,7 +326,7 @@ export function ContactSection({ bare = false }: { bare?: boolean } = {}) {
|
|||||||
className="mt-8 font-light"
|
className="mt-8 font-light"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSent(false)
|
setSent(false)
|
||||||
setForm({ name: '', phone: '', note: '' })
|
setForm({ name: '', phone: '', note: '', website: '' })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<InlineEdit value={content.success_button_label} onSave={(v) => saveContent('success_button_label', v)} />
|
<InlineEdit value={content.success_button_label} onSave={(v) => saveContent('success_button_label', v)} />
|
||||||
@@ -244,7 +366,7 @@ export function ContactSection({ bare = false }: { bare?: boolean } = {}) {
|
|||||||
|
|
||||||
<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"
|
||||||
@@ -277,6 +399,7 @@ export function ContactSection({ bare = false }: { bare?: boolean } = {}) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,19 +35,22 @@ 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">
|
||||||
<div className="grid gap-12 lg:grid-cols-12">
|
|
||||||
<div className="lg:col-span-4">
|
{/* 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">
|
<div className="relative w-fit">
|
||||||
<img src={brand.logo} alt={brand.name} className="h-20 w-auto object-contain" loading="lazy" decoding="async" />
|
<img src={brand.logo} alt={brand.name} className="h-20 w-auto object-contain" loading="lazy" decoding="async" />
|
||||||
<SingletonPill resource="brand" label="ویرایش برند، لوگو و تماس" className="absolute top-1/2 -translate-y-1/2 ltr:-right-10 rtl:-left-10" />
|
<SingletonPill resource="brand" label="ویرایش برند، لوگو و تماس" className="absolute top-1/2 -translate-y-1/2 ltr:-right-10 rtl:-left-10" />
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-6 max-w-sm text-sm font-light leading-8 text-ivory/60">
|
<p className="mt-4 text-xs font-light leading-relaxed text-ivory/60 max-w-xs">
|
||||||
<InlineEdit multiline value={footer.about_text} onSave={(v) => saveFooter('about_text', v)} />
|
{footer.about_text}
|
||||||
</p>
|
</p>
|
||||||
{/* آیکونهای شبکههای اجتماعی: گویهای شیشهای شناور */}
|
{/* Social icons */}
|
||||||
<div className="mt-8 flex items-center gap-3">
|
<div className="mt-4 flex items-center justify-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;
|
||||||
return (
|
return (
|
||||||
@@ -54,100 +58,234 @@ export function Footer() {
|
|||||||
key={label}
|
key={label}
|
||||||
href={href}
|
href={href}
|
||||||
aria-label={label}
|
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>
|
</a>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lg:col-span-2">
|
{/* Collapsible Link Groups */}
|
||||||
<h3 className="text-sm font-bold text-copper">
|
<div className="border-t border-white/10">
|
||||||
<InlineEdit value={footer.pages_heading} onSave={(v) => saveFooter('pages_heading', v)} />
|
{/* Pages Accordion */}
|
||||||
</h3>
|
<div className="border-b border-white/10">
|
||||||
<ul className="mt-5 space-y-3">
|
<button
|
||||||
{nav.map((item) => (
|
type="button"
|
||||||
<EditableItem key={item.id} resource="nav" item={item}>
|
onClick={() => setOpenSection(openSection === 'pages' ? null : 'pages')}
|
||||||
<li>
|
className="flex w-full items-center justify-between py-3.5 text-xs font-bold text-copper text-start cursor-pointer"
|
||||||
<Link
|
>
|
||||||
to={href(item.href)}
|
<span>{footer.pages_heading}</span>
|
||||||
className="inline-block text-sm font-light text-ivory/60 transition-colors duration-200 ease-lux hover:text-copper"
|
<span className={`text-[10px] text-copper transition-transform duration-300 ${openSection === 'pages' ? 'rotate-180' : ''}`}>
|
||||||
>
|
▼
|
||||||
<InlineEdit
|
</span>
|
||||||
value={item.label}
|
</button>
|
||||||
onSave={(v) => saveNav(item.id, { label: v })}
|
<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">
|
||||||
</Link>
|
{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>
|
||||||
</EditableItem>
|
<li className="flex items-center gap-2.5">
|
||||||
))}
|
<PhoneIcon className="h-3.5 w-3.5 shrink-0 text-copper" />
|
||||||
</ul>
|
<a href={brand.phone_href} dir="ltr" className="transition-colors hover:text-copper">
|
||||||
<div className="mt-4">
|
{brand.phone}
|
||||||
<AddItemButton resource="nav" />
|
</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>
|
</div>
|
||||||
|
|
||||||
<div className="lg:col-span-3">
|
{/* Bottom Copyright & Meta for Mobile */}
|
||||||
<h3 className="text-sm font-bold text-copper">
|
<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">
|
||||||
<InlineEdit value={footer.services_heading} onSave={(v) => saveFooter('services_heading', v)} />
|
<button
|
||||||
</h3>
|
type="button"
|
||||||
<ul className="mt-5 space-y-3">
|
onClick={scrollToTop}
|
||||||
{services.map((service) => (
|
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"
|
||||||
<li key={service.id} className="text-sm font-light text-ivory/60">
|
>
|
||||||
{service.title}
|
<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>
|
</li>
|
||||||
))}
|
<li className="flex items-center gap-3">
|
||||||
</ul>
|
<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>
|
||||||
|
|
||||||
<div className="lg:col-span-3">
|
{/* خط پایانی به همراه دکمه کپسولی بازگشت به بالا */}
|
||||||
<h3 className="text-sm font-bold text-copper">
|
<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">
|
||||||
<InlineEdit value={footer.contact_heading} onSave={(v) => saveFooter('contact_heading', v)} />
|
<p>
|
||||||
</h3>
|
<InlineEdit value={footer.copyright} onSave={(v) => saveFooter('copyright', v)} />
|
||||||
<ul className="mt-5 space-y-4 text-sm font-light text-ivory/60">
|
</p>
|
||||||
<li className="flex items-start gap-3">
|
|
||||||
<MapPinIcon className="mt-1 h-4 w-4 shrink-0 text-copper" />
|
<button
|
||||||
<span className="leading-7">{brand.address}</span>
|
type="button"
|
||||||
</li>
|
onClick={scrollToTop}
|
||||||
<li className="flex items-center gap-3">
|
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"
|
||||||
<PhoneIcon className="h-4 w-4 shrink-0 text-copper" />
|
>
|
||||||
<a href={brand.phone_href} dir="ltr" className="transition-colors hover:text-copper">
|
<span>{t('backToTop')}</span>
|
||||||
{brand.phone}
|
<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">
|
||||||
</a>
|
<ArrowUpIcon className="h-3 w-3" />
|
||||||
</li>
|
</span>
|
||||||
<li className="flex items-center gap-3">
|
</button>
|
||||||
<MailIcon className="h-4 w-4 shrink-0 text-copper" />
|
|
||||||
<a href={`mailto:${brand.email}`} dir="ltr" className="transition-colors hover:text-copper">
|
<p>
|
||||||
{brand.email}
|
<InlineEdit value={footer.tagline} onSave={(v) => saveFooter('tagline', v)} />
|
||||||
</a>
|
</p>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ 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"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
@@ -60,25 +60,25 @@ export function ProjectCard({ project, imageUrl, detailHref }: ProjectCardProps)
|
|||||||
|
|
||||||
{/* نشانگر وضعیت کپسولی شیشهای */}
|
{/* نشانگر وضعیت کپسولی شیشهای */}
|
||||||
<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>
|
||||||
@@ -88,7 +88,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>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { ArrowLeftIcon } from 'lucide-react';
|
import { ArrowLeftIcon } from 'lucide-react';
|
||||||
import { Reveal } from '../Reveal';
|
import { Reveal } from '../Reveal';
|
||||||
import { ButtonLink } from '../Button';
|
import { ButtonLink } from '../Button';
|
||||||
@@ -8,6 +9,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 +19,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 +30,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>
|
||||||
@@ -64,6 +142,6 @@ export function Articles() {
|
|||||||
<AddItemButton resource="articles" />
|
<AddItemButton resource="articles" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>);
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -22,18 +22,18 @@ export function FeaturedProjects() {
|
|||||||
.sort((a, b) => FEATURED_TITLES.indexOf(a.title) - FEATURED_TITLES.indexOf(b.title))
|
.sort((a, b) => FEATURED_TITLES.indexOf(a.title) - FEATURED_TITLES.indexOf(b.title))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-char py-24 lg:py-32" aria-labelledby="projects-title">
|
<section className="w-full max-w-full overflow-x-clip bg-char 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 min-h-[420px]">
|
<div className="mt-8 min-h-[420px] md:mt-10">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="featured"
|
key="featured"
|
||||||
initial={{ opacity: 0, y: 15 }}
|
initial={{ opacity: 0, y: 15 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
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 imageUrl = project.image || undefined;
|
const imageUrl = project.image || undefined;
|
||||||
@@ -44,7 +44,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}
|
||||||
@@ -61,7 +61,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}
|
||||||
|
|||||||
@@ -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({ tone = 'black' }: { tone?: 'black' | 'gray' } = {}) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`${tone === 'black' ? 'bg-char' : 'bg-ink'} py-24 lg:py-32`} aria-labelledby="services-title">
|
<section className={`w-full max-w-full overflow-hidden ${tone === 'black' ? 'bg-char' : '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">
|
||||||
<ServiceIcon
|
<div className="flex h-10 w-10 items-center justify-center md:h-14 md:w-14">
|
||||||
name={service.icon}
|
<ServiceIcon
|
||||||
className="h-12 w-12 text-copper/85 transition-colors duration-300 ease-out group-hover:text-copper"
|
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"
|
||||||
<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>
|
||||||
|
|||||||
@@ -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({ tone = 'black' }: { tone?: 'black' | 'gray' } = {}) {
|
|||||||
const save = useFieldSaver();
|
const save = useFieldSaver();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`${tone === 'black' ? 'bg-char' : 'bg-ink'} py-16`} aria-label={t('statsAria')}>
|
<section className={`w-full max-w-full overflow-x-clip ${tone === 'black' ? 'bg-char' : 'bg-ink'} 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({ tone = 'black' }: { tone?: 'black' | 'gray' } = {}) {
|
|||||||
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) => {
|
||||||
|
|||||||
@@ -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({ tone = 'gray' }: { tone?: 'black' | 'gray' } = {}) {
|
|||||||
const { team } = useSite()
|
const { team } = useSite()
|
||||||
const { t } = useLang()
|
const { t } = useLang()
|
||||||
return (
|
return (
|
||||||
<section className={`${tone === 'black' ? 'bg-char' : 'bg-ink'} py-24 lg:py-32`} aria-labelledby="team-title" >
|
<section className={`w-full max-w-full overflow-x-clip ${tone === 'black' ? 'bg-char' : '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,12 +17,12 @@ export function Team({ tone = 'gray' }: { tone?: 'black' | 'gray' } = {}) {
|
|||||||
align="center"
|
align="center"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mx-auto mt-16 flex max-w-5xl flex-wrap justify-center gap-8">
|
<div className="mx-auto mt-8 flex max-w-5xl flex-wrap justify-center gap-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 w-full sm:w-[calc(50%-1rem)] lg:w-[19rem]">
|
<Reveal key={member.id} delay={0.06 * i} className="h-full w-[calc(50%-0.5rem)] md:w-[calc(50%-1rem)] lg:w-[19rem]">
|
||||||
<EditableItem resource="team" item={member} className="h-full">
|
<EditableItem resource="team" item={member} className="h-full">
|
||||||
{/* اضافه شدن rounded-2xl برای گرد شدن گوشهها */}
|
{/* اضافه شدن 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">
|
<article className="group flex h-full flex-col items-center rounded-xl border border-ivory/10 bg-char p-4 text-center md:rounded-2xl md:p-10 transition-[transform,box-shadow,border-color] duration-500 ease-out hover:-translate-y-3 hover:border-copper/50 hover:shadow-lift">
|
||||||
<h3 className="text-lg font-bold text-ivory">{member.name}</h3>
|
<h3 className="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-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">
|
<p className="mt-6 border-t border-ivory/10 pt-6 text-sm font-light text-ivory/60">
|
||||||
|
|||||||
@@ -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" />
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<motion.blockquote
|
<div className="relative min-h-[170px] overflow-hidden md:min-h-[190px]">
|
||||||
key={current.id}
|
<AnimatePresence mode="wait">
|
||||||
initial={{ opacity: 0, y: 16 }}
|
<motion.blockquote
|
||||||
animate={{ opacity: 1, y: 0 }}
|
key={current.id}
|
||||||
exit={{ opacity: 0, y: -12 }}
|
drag="x"
|
||||||
transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}>
|
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}>
|
<div className="mt-6 flex items-center justify-center gap-5 md:mt-10">
|
||||||
<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">
|
|
||||||
<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>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function Item({ item, index, featured = false, onLi }: { item: Milestone; index:
|
|||||||
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>
|
||||||
@@ -85,10 +85,10 @@ export function Timeline({ tone = 'gray' }: { tone?: 'black' | 'gray' } = {}) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`${tone === 'black' ? 'bg-char' : 'bg-ink'} py-24 md:py-32`} aria-labelledby="timeline-title">
|
<section className={`w-full max-w-full overflow-x-clip ${tone === 'black' ? 'bg-char' : 'bg-ink'} 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')}
|
||||||
@@ -120,6 +120,7 @@ export function Timeline({ tone = 'gray' }: { tone?: 'black' | 'gray' } = {}) {
|
|||||||
<AddItemButton resource="timeline" />
|
<AddItemButton resource="timeline" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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({ tone = 'black' }: { tone?: 'black' | 'gray' } = {}) {
|
|||||||
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={`${tone === 'black' ? 'bg-char' : 'bg-ink'} py-24 lg:py-32`} aria-labelledby="why-title">
|
<section className={`w-full max-w-full overflow-x-clip ${tone === 'black' ? 'bg-char' : '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}
|
||||||
@@ -42,35 +47,78 @@ export function WhyUs({ tone = 'black' }: { tone?: 'black' | 'gray' } = {}) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lg:col-span-7">
|
<div className="lg:col-span-7">
|
||||||
<div className="grid gap-x-10 gap-y-px sm:grid-cols-2">
|
{/* Mobile 2-Column Luxury Micro-Card Grid */}
|
||||||
{why_us.map((item, i) =>
|
<div className="block md:hidden px-3.5 py-2">
|
||||||
<Reveal key={item.id} delay={0.05 * i}>
|
<div className="grid grid-cols-2 gap-2.5 items-stretch">
|
||||||
<EditableItem resource="why_us" item={item}>
|
{why_us.map((item, i) => (
|
||||||
<div className="flex h-full flex-col border-t border-ivory/10 py-8">
|
<EditableItem key={item.id} resource="why_us" item={item} className="h-full">
|
||||||
<span className="text-xs font-light text-copper">{lang === 'fa' ? `۰${formatNumber(i + 1, lang)}` : formatNumber(i + 1, lang).padStart(2, '0')}</span>
|
<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">
|
||||||
<h3 className="mt-4 text-lg font-bold text-ivory">
|
<div>
|
||||||
<InlineEdit
|
{/* Top Row: Index Badge */}
|
||||||
value={item.title}
|
<div className="flex items-center justify-between mb-2">
|
||||||
onSave={(v) => saveItem(item.id, { title: v })}
|
<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}`}
|
||||||
</h3>
|
</span>
|
||||||
<p className="mt-3 text-sm font-light leading-8 text-ivory/55">
|
</div>
|
||||||
<InlineEdit
|
|
||||||
multiline
|
{/* Title */}
|
||||||
value={item.desc}
|
<h4 className="text-xs font-bold text-ivory mb-1 line-clamp-1 leading-snug">
|
||||||
onSave={(v) => saveItem(item.id, { desc: v })}
|
<InlineEdit
|
||||||
/>
|
value={item.title}
|
||||||
</p>
|
onSave={(v) => saveItem(item.id, { title: v })}
|
||||||
</div>
|
/>
|
||||||
</EditableItem>
|
</h4>
|
||||||
</Reveal>
|
|
||||||
)}
|
{/* 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>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>);
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user