فرانت‌اند: نسخه انگلیسی با مسیر /en/ — مسیریابی دوزبانه، دیکشنری رابط، بومی‌سازی داده، تغییر زبان در هدر، ویرایش انگلیسی در پنل و پشتیبانی LTR

This commit is contained in:
MMD
2026-09-12 23:37:55 +03:30
parent 6bb6226e06
commit 6b49d035c2
43 changed files with 1194 additions and 262 deletions
+5 -3
View File
@@ -4,12 +4,14 @@ import { Reveal } from '../Reveal';
import { ButtonLink } from '../Button';
import { ArticleCard } from '../ArticleCard';
import { useSectionHeading, useSite } from '../../api';
import { useLang } from '../../i18n/lang';
import { useFieldSaver } from '../../admin/AdminContext';
import { InlineEdit } from '../admin/InlineEdit';
import { AddItemButton, EditableItem } from '../../admin/Editable';
export function Articles() {
const { articles } = useSite();
const { href } = useLang();
const heading = useSectionHeading('articles');
const save = useFieldSaver();
@@ -25,7 +27,7 @@ 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">
<Reveal className="max-w-2xl text-right">
<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>
@@ -51,9 +53,9 @@ export function Articles() {
{heading.cta_label && heading.cta_href && (
<Reveal delay={0.1}>
<div className="mt-12 flex justify-center">
<ButtonLink to={heading.cta_href} variant="outline" className="font-light">
<ButtonLink to={href(heading.cta_href)} variant="outline" className="font-light">
<InlineEdit value={heading.cta_label} onSave={(v) => saveHeading('cta_label', v)} />
<ArrowLeftIcon className="h-4 w-4" />
<ArrowLeftIcon className="h-4 w-4 ltr:-scale-x-100" />
</ButtonLink>
</div>
</Reveal>
+3 -1
View File
@@ -3,10 +3,12 @@ import { MoveHorizontalIcon } from 'lucide-react'
import { Reveal } from '../Reveal'
import { SectionHeading } from '../SectionHeading'
import { useSite } from '../../api'
import { useLang } from '../../i18n/lang'
import { SingletonPill } from '../../admin/Editable'
export function BeforeAfter() {
const { before_after } = useSite()
const { t } = useLang()
const containerRef = useRef<HTMLDivElement>(null)
const [position, setPosition] = useState(52)
const dragging = useRef(false)
@@ -102,7 +104,7 @@ export function BeforeAfter() {
<button
type="button"
role="slider"
aria-label="مقایسه کانسپت و پروژه ساخته‌شده"
aria-label={t('compareAria')}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(position)}
+4 -2
View File
@@ -4,6 +4,7 @@ import { PlusIcon } from 'lucide-react'
import { Reveal } from '../Reveal'
import { SectionHeading } from '../SectionHeading'
import { useSite } from '../../api'
import { useLang } from '../../i18n/lang'
import { AddItemButton, EditableItem } from '../../admin/Editable'
type FaqProps = {
@@ -12,6 +13,7 @@ type FaqProps = {
export function Faq({ hideHeading = false }: FaqProps) {
const { faqs } = useSite()
const { t } = useLang()
const [open, setOpen] = useState<number | null>(0)
return (
@@ -22,8 +24,8 @@ export function Faq({ hideHeading = false }: FaqProps) {
{!hideHeading && (
<div className="lg:col-span-4">
<SectionHeading
title="سوالات متداول"
lead="پاسخ کوتاه به پرتکرارترین پرسش‌های سرمایه‌گذاران و خریداران."
title={t('fallbackFaqTitle')}
lead={t('fallbackFaqLead')}
headingKey="faq"
/>
</div>
@@ -7,12 +7,14 @@ import { ProjectCard } from '../ProjectCard'
import { SectionHeading } from '../SectionHeading'
import { useSectionHeading, useSite, useStatusLabel } from '../../api'
import { useFieldSaver } from '../../admin/AdminContext'
import { useLang } from '../../i18n/lang'
import { InlineEdit } from '../admin/InlineEdit'
import { AddItemButton, EditableItem } from '../../admin/Editable'
import type { ProjectStatus } from '../../data/site'
export function FeaturedProjects() {
const { projects, status_labels } = useSite()
const { t, href } = useLang()
const heading = useSectionHeading('featured_projects')
const save = useFieldSaver()
const saveStatusLabel = (key: string, value: string) => {
@@ -37,7 +39,7 @@ export function FeaturedProjects() {
<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="فیلتر وضعیت پروژه">
<div className="mt-10 flex items-center justify-center gap-4 md:gap-6" role="tablist" aria-label={t('projectFilterAria')}>
{FILTERS.map((filter, index) => {
const isSelected = status === filter.key
return (
@@ -102,7 +104,7 @@ export function FeaturedProjects() {
<Reveal delay={0.1}>
<div className="mt-12 flex justify-center">
<ButtonLink
to={heading.cta_href}
to={href(heading.cta_href)}
variant="outline"
className="group flex items-center gap-3 rounded-full border border-white/20 bg-white/[0.04] px-8 py-3.5 text-sm backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.15),0_8px_20px_rgba(0,0,0,0.3)] transition-all duration-300 hover:border-copper hover:bg-copper/10 hover:shadow-[0_0_30px_rgba(197,143,92,0.35)]"
>
@@ -111,12 +113,12 @@ export function FeaturedProjects() {
onSave={(v) =>
save('section-headings', heading.id, { cta_label: v }, (d) => {
const h = d.section_headings.find((s) => s.id === heading.id)
if (h) h.cta_label = v
if (h) (h as unknown as Record<string, unknown>)["cta_label"] = v
})
}
/>
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-white/10 text-copper transition-all duration-300 group-hover:-translate-x-1 group-hover:bg-copper group-hover:text-char">
<ArrowLeftIcon className="h-3.5 w-3.5" />
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-white/10 text-copper transition-all duration-300 rtl:group-hover:-translate-x-1 ltr:group-hover:translate-x-1 group-hover:bg-copper group-hover:text-char">
<ArrowLeftIcon className="h-3.5 w-3.5 ltr:-scale-x-100" />
</span>
</ButtonLink>
</div>
+14 -12
View File
@@ -4,10 +4,12 @@ import { ChevronLeftIcon, ChevronRightIcon, XIcon } from 'lucide-react'
import { Reveal } from '../Reveal'
import { SectionHeading } from '../SectionHeading'
import { useSite } from '../../api'
import { useLang, formatNumber } from '../../i18n/lang'
import { AddItemButton, EditableItem } from '../../admin/Editable'
export function Gallery() {
const { gallery } = useSite()
const { t, lang } = useLang()
const [active, setActive] = useState<number | null>(null)
const close = useCallback(() => setActive(null), [])
@@ -30,8 +32,8 @@ export function Gallery() {
<section className="bg-ink py-24 lg:py-32" aria-labelledby="gallery-title">
<div className="mx-auto max-w-site px-6 lg:px-10">
<SectionHeading
title="گالری تصاویر"
lead="قاب‌هایی از کارگاه‌ها و پروژه‌های تحویل‌شده گروه ساختمانی آرامش."
title={t('fallbackGalleryTitle')}
lead={t('fallbackGalleryLead')}
headingKey="gallery"
align="center"
/>
@@ -44,7 +46,7 @@ export function Gallery() {
type="button"
onClick={() => setActive(i)}
className="group block w-full overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-copper"
aria-label={`بزرگ‌نمایی تصویر شماره ${(i + 1).toLocaleString('fa-IR')}`}
aria-label={`${t('zoomImagePrefix')} ${formatNumber(i + 1, lang)}`}
>
<img
src={image.src}
@@ -69,14 +71,14 @@ export function Gallery() {
transition={{ duration: 0.5, ease: [0.23, 1, 0.32, 1] }}
role="dialog"
aria-modal="true"
aria-label="نمایش تصویر"
aria-label={t('viewImage')}
onClick={close}
>
<button
type="button"
onClick={close}
className="absolute left-8 top-8 flex h-11 w-11 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper"
aria-label="بستن"
className="absolute top-8 ltr:right-8 rtl:left-8 flex h-11 w-11 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper"
aria-label={t('close')}
>
<XIcon className="h-5 w-5" />
</button>
@@ -87,10 +89,10 @@ export function Gallery() {
e.stopPropagation()
step(-1)
}}
className="absolute right-6 flex h-12 w-12 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper md:right-10"
aria-label="تصویر قبلی"
className="absolute flex h-12 w-12 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper rtl:right-6 md:rtl:right-10 ltr:left-6 md:ltr:left-10"
aria-label={t('prevImage')}
>
<ChevronRightIcon className="h-6 w-6" />
<ChevronRightIcon className="h-6 w-6 ltr:rotate-180" />
</button>
<button
type="button"
@@ -98,10 +100,10 @@ export function Gallery() {
e.stopPropagation()
step(1)
}}
className="absolute left-6 flex h-12 w-12 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper md:left-10"
aria-label="تصویر بعدی"
className="absolute flex h-12 w-12 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper rtl:left-6 md:rtl:left-10 ltr:right-6 md:ltr:right-10"
aria-label={t('nextImage')}
>
<ChevronLeftIcon className="h-6 w-6" />
<ChevronLeftIcon className="h-6 w-6 ltr:rotate-180" />
</button>
<motion.img
+11 -9
View File
@@ -3,12 +3,14 @@ import { motion, useScroll, useTransform } from 'framer-motion'
import { ArrowLeftIcon, ChevronDownIcon } from 'lucide-react'
import { ButtonLink } from '../Button'
import { useSite } from '../../api'
import { useLang } from '../../i18n/lang'
import { useFieldSaver } from '../../admin/AdminContext'
import { InlineEdit } from '../admin/InlineEdit'
import { SingletonPill } from '../../admin/Editable'
export function Hero() {
const { brand, hero } = useSite()
const { t, href } = useLang()
const save = useFieldSaver()
const { scrollY } = useScroll()
const imageY = useTransform(scrollY, [0, 700], [0, 110])
@@ -21,11 +23,11 @@ export function Hero() {
})
return (
<section className="relative h-screen min-h-[640px] w-full overflow-hidden" aria-label="معرفی گروه ساختمانی آرامش">
<section className="relative h-screen min-h-[640px] w-full overflow-hidden" aria-label={t('heroAria')}>
{/* تصویر پس‌زمینه */}
<motion.img
src={brand.hero}
alt="نمای شبانه برج مسکونی ساخته‌شده توسط گروه ساختمانی آرامش"
alt={t('heroImageAlt')}
style={{ y: imageY }}
className="absolute inset-0 h-[114%] w-full object-cover"
/>
@@ -62,7 +64,7 @@ export function Hero() {
transition={{ duration: 0.85, delay: 2.2, ease: [0.23, 1, 0.32, 1] }}
className="mt-6 text-hero font-bold text-ivory leading-[1.3]"
>
<InlineEdit value={hero.title} onSave={(v) => saveHero('title', v)} aria-label="تیتر اصلی صفحه" />
<InlineEdit value={hero.title} onSave={(v) => saveHero('title', v)} aria-label={t('heroTitleAria')} />
</motion.h1>
{/* زیرتیتر */}
@@ -84,21 +86,21 @@ export function Hero() {
>
{/* دکمه اصلی: کپسولی شیشه‌ای دودی با دایره کهربایی اختصاصی برای آیکون */}
<ButtonLink
to={hero.cta_primary_href}
to={href(hero.cta_primary_href)}
variant="copper"
className="!p-1.5 !pr-7 !rounded-full shadow-lg shadow-black/40 hover:shadow-copper/25 transition-all duration-300"
>
<span className="text-sm font-medium tracking-wide text-ivory">
<InlineEdit value={hero.cta_primary_label} onSave={(v) => saveHero('cta_primary_label', v)} />
</span>
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-copper text-char shadow-md transition-all duration-300 group-hover:scale-110 group-hover:-translate-x-1">
<ArrowLeftIcon className="h-4 w-4 stroke-[2.5]" />
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-copper text-char shadow-md transition-all duration-300 group-hover:scale-110 rtl:group-hover:-translate-x-1 ltr:group-hover:translate-x-1">
<ArrowLeftIcon className="h-4 w-4 stroke-[2.5] ltr:-scale-x-100" />
</span>
</ButtonLink>
{/* دکمه ثانویه: شیشه دودی مات (Smoked Glass) با کادر ظریف */}
<ButtonLink
to={hero.cta_secondary_href}
to={href(hero.cta_secondary_href)}
variant="outline"
className="!rounded-full !px-8 !py-3.5 bg-char/40 backdrop-blur-md border border-ivory/20 text-ivory hover:border-copper/70 hover:text-copper transition-all duration-300 shadow-sm"
>
@@ -112,7 +114,7 @@ export function Hero() {
initial={{ opacity: 0, x: -24 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.9, delay: 2.6, ease: [0.23, 1, 0.32, 1] }}
className="absolute bottom-28 left-6 hidden w-64 rounded-2xl border border-ivory/15 bg-char/70 p-6 backdrop-blur-xl shadow-2xl shadow-black/40 lg:block lg:left-10"
className="absolute bottom-28 ltr:right-6 rtl:left-6 hidden w-64 rounded-2xl border border-ivory/15 bg-char/70 p-6 backdrop-blur-xl shadow-2xl shadow-black/40 lg:block lg:ltr:right-10 lg:rtl:left-10"
>
<p className="text-3xl font-bold text-copper font-mono">
<InlineEdit value={hero.badge_value} onSave={(v) => saveHero('badge_value', v)} />
@@ -126,7 +128,7 @@ export function Hero() {
{/* دکمه اسکرول پایین: تبدیل‌شده به یک گوی شیشه‌ای دایره‌ای */}
<a
href="#story"
aria-label="رفتن به بخش بعدی"
aria-label={t('scrollDown')}
className="group absolute bottom-7 right-1/2 z-10 flex translate-x-1/2 flex-col items-center gap-2 text-ivory/60 transition-colors duration-300 hover:text-copper"
>
<span className="text-[11px] font-light tracking-widest opacity-80 group-hover:opacity-100">
+9 -7
View File
@@ -4,6 +4,7 @@ import Swal from 'sweetalert2'
import { Reveal } from '../Reveal'
import { Button } from '../Button'
import { useSite } from '../../api'
import { useLang } from '../../i18n/lang'
import { useFieldSaver } from '../../admin/AdminContext'
import { InlineEdit } from '../admin/InlineEdit'
import { SingletonPill } from '../../admin/Editable'
@@ -11,6 +12,7 @@ import { API_URL } from '../../config'
export function Newsletter() {
const { newsletter } = useSite()
const { t } = useLang()
const save = useFieldSaver()
const [email, setEmail] = useState('')
const [status, setStatus] = useState<'idle' | 'error' | 'done'>('idle')
@@ -29,7 +31,7 @@ export function Newsletter() {
background: '#1e1a16',
color: '#F5F3EF',
iconColor: '#d5874c',
confirmButtonText: 'متوجه شدم',
confirmButtonText: t('understood'),
buttonsStyling: false,
customClass: {
container: 'backdrop-blur-md bg-ink/70',
@@ -58,13 +60,13 @@ export function Newsletter() {
if (!emailValue) {
setStatus('error')
showErrorAlert('اطلاعات ناقص است', 'لطفاً نشانی ایمیل خود را برای عضویت در خبرنامه وارد کنید.')
showErrorAlert(t('incompleteInfo'), t('emailRequired'))
return
}
if (!emailRegex.test(emailValue)) {
setStatus('error')
showErrorAlert('فرمت ایمیل نامعتبر است', 'لطفاً ایمیل خود را به درستی وارد کنید (مثال: example@domain.com).')
showErrorAlert(t('emailInvalidTitle'), t('emailInvalidText'))
return
}
@@ -80,8 +82,8 @@ export function Newsletter() {
setStatus('error')
const data = await res.json().catch(() => null)
const detail =
typeof data?.detail === 'string' ? data.detail : 'لطفاً ایمیل را بررسی و دوباره تلاش کنید.'
showErrorAlert('خطا در عضویت', detail)
typeof data?.detail === 'string' ? data.detail : t('emailCheck')
showErrorAlert(t('subscribeErrorTitle'), detail)
return
}
@@ -89,7 +91,7 @@ export function Newsletter() {
setEmail('')
} catch {
setStatus('error')
showErrorAlert('خطا در اتصال به سرور', 'ارتباط با سرور برقرار نشد. لطفاً بعداً دوباره تلاش کنید.')
showErrorAlert(t('connectionErrorTitle'), t('connectionErrorText'))
} finally {
setSending(false)
}
@@ -146,7 +148,7 @@ export function Newsletter() {
disabled={sending}
className="group flex items-center justify-center gap-2 rounded-full px-8 py-4 shadow-[0_8px_20px_rgba(0,0,0,0.3)] transition-all duration-300 hover:shadow-[0_12px_25px_rgba(0,0,0,0.45)] active:scale-95"
>
<SendIcon className="h-4 w-4 transition-transform duration-300 group-hover:-translate-x-1" />
<SendIcon className="h-4 w-4 transition-transform duration-300 rtl:group-hover:-translate-x-1 ltr:group-hover:translate-x-1" />
<span>
{sending ? newsletter.sending_label : (
<InlineEdit value={newsletter.button_label} onSave={(v) => saveContent('button_label', v)} />
+6 -7
View File
@@ -1,15 +1,13 @@
import React, { useEffect, useRef, useState } from 'react';
import { useInView } from 'framer-motion';
import { useSite } from '../../api';
import { useLang, formatNumber } from '../../i18n/lang';
import { useFieldSaver } from '../../admin/AdminContext';
import { AddItemButton, EditableItem } from '../../admin/Editable';
import { InlineEdit } from '../admin/InlineEdit';
function toFa(n: number) {
return n.toLocaleString('fa-IR');
}
function Counter({ value }: {value: number;}) {
const { lang } = useLang();
const ref = useRef<HTMLSpanElement>(null);
const inView = useInView(ref, { once: true, margin: '-60px' });
const [display, setDisplay] = useState(0);
@@ -27,15 +25,16 @@ function Counter({ value }: {value: number;}) {
return () => window.clearInterval(id);
}, [inView, value]);
return <span ref={ref}>{toFa(display)}</span>;
return <span ref={ref}>{formatNumber(display, lang)}</span>;
}
export function Stats() {
const { stats } = useSite();
const { t } = useLang();
const save = useFieldSaver();
return (
<section className="bg-char py-16" aria-label="آمار و ارقام">
<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">
{stats.map((stat) =>
<EditableItem key={stat.id} resource="stats" item={stat}>
@@ -48,7 +47,7 @@ export function Stats() {
const s = d.stats.find((x) => x.id === stat.id);
if (s) s.suffix = v;
})}
className="mr-1 text-2xl"
className="ltr:ml-1 rtl:mr-1 text-2xl"
/>
</p>
<p className="mt-3 text-sm font-light text-ivory/55">
+5 -3
View File
@@ -3,6 +3,7 @@ import { ArrowLeftIcon } from 'lucide-react'
import { Reveal } from '../Reveal'
import { ButtonLink } from '../Button'
import { useSite } from '../../api'
import { useLang } from '../../i18n/lang'
import { useFieldSaver } from '../../admin/AdminContext'
import { InlineEdit } from '../admin/InlineEdit'
import { SingletonPill } from '../../admin/Editable'
@@ -17,6 +18,7 @@ type StoryProps = {
export function Story({ showLink = true, showLabel = true, showQuote = false, alignTop = false, children }: StoryProps) {
const { story } = useSite()
const { href } = useLang()
const save = useFieldSaver()
const saveStory = (field: string, value: unknown) =>
@@ -87,9 +89,9 @@ export function Story({ showLink = true, showLabel = true, showQuote = false, al
{showLink && (
<Reveal delay={0.22}>
<ButtonLink to={story.link_href} variant="outline" className="mt-10 font-light">
<ButtonLink to={href(story.link_href)} variant="outline" className="mt-10 font-light">
<InlineEdit value={story.link_label} onSave={(v) => saveStory('link_label', v)} />
<ArrowLeftIcon className="h-4 w-4" />
<ArrowLeftIcon className="h-4 w-4 ltr:-scale-x-100" />
</ButtonLink>
</Reveal>
)}
@@ -102,7 +104,7 @@ export function Story({ showLink = true, showLabel = true, showQuote = false, al
</div>
<div className="relative grid grid-cols-2 gap-4">
<SingletonPill resource="story" label="تصاویر و نقل‌قول بخش درباره ما" className="absolute -top-10 right-0" />
<SingletonPill resource="story" label="تصاویر و نقل‌قول بخش درباره ما" className="absolute -top-10 ltr:left-0 rtl:right-0" />
{story.bento.map((item, i) => (
<Reveal key={item.src + i} delay={0.06 * i}>
<div className="overflow-hidden">
+4 -2
View File
@@ -2,16 +2,18 @@ import React from 'react'
import { Reveal } from '../Reveal'
import { SectionHeading } from '../SectionHeading'
import { useSite } from '../../api'
import { useLang } from '../../i18n/lang'
import { AddItemButton, EditableItem } from '../../admin/Editable'
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">
<SectionHeading
title="تیم ما"
lead="کسانی که تصمیم‌های اصلی هر پروژه را می‌گیرند و پاسخ‌گوی آن هستند."
title={t('fallbackTeamTitle')}
lead={t('fallbackTeamLead')}
headingKey="team"
align="center"
/>
+10 -8
View File
@@ -3,10 +3,12 @@ import { AnimatePresence, motion } from 'framer-motion';
import { ChevronLeftIcon, ChevronRightIcon, QuoteIcon } from 'lucide-react';
import { SectionHeading } from '../SectionHeading';
import { useSite } from '../../api';
import { useLang } from '../../i18n/lang';
import { EditableItem } from '../../admin/Editable';
export function Testimonials() {
const { testimonials } = useSite();
const { t } = useLang();
const [index, setIndex] = useState(0);
const current = testimonials[Math.min(index, Math.max(testimonials.length - 1, 0))];
if (!current) return null;
@@ -44,20 +46,20 @@ export function Testimonials() {
<button
type="button"
onClick={() => go(1)}
aria-label="نظر قبلی"
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"
>
<ChevronRightIcon className="h-5 w-5 transition-transform duration-300 ease-lux group-hover:translate-x-0.5" />
<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" />
</button>
<div className="flex items-center gap-2" role="tablist" aria-label="انتخاب نظر">
{testimonials.map((t, i) => (
<div className="flex items-center gap-2" role="tablist" aria-label={t('selectTestimonialAria')}>
{testimonials.map((item, i) => (
<button
key={t.id}
key={item.id}
type="button"
role="tab"
aria-selected={i === index}
aria-label={`نظر ${t.name}`}
aria-label={`${t("testimonialByPrefix")} ${item.name}`}
onClick={() => setIndex(i)}
className={`h-2 rounded-full transition-all duration-300 ease-lux ${
i === index
@@ -71,10 +73,10 @@ export function Testimonials() {
<button
type="button"
onClick={() => go(-1)}
aria-label="نظر بعدی"
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"
>
<ChevronLeftIcon className="h-5 w-5 transition-transform duration-300 ease-lux group-hover:-translate-x-0.5" />
<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" />
</button>
</div>
</div>
+4 -2
View File
@@ -1,6 +1,7 @@
import React, { useRef } from 'react'
import { motion, useScroll, useTransform, useInView } from 'framer-motion'
import { useSite, type TimelineItem } from '../../api'
import { useLang } from '../../i18n/lang'
import { SectionHeading } from '../SectionHeading'
import { AddItemButton, EditableItem } from '../../admin/Editable'
@@ -49,6 +50,7 @@ function Item({ item, index }: { item: Milestone; index: number }) {
export function Timeline() {
const { timeline } = useSite()
const { t } = useLang()
const ref = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: ref,
@@ -62,8 +64,8 @@ export function Timeline() {
<div className="mb-16">
<SectionHeading
title="مسیر ما"
lead="از یک دفتر چهارنفره تا ۴۷ پروژه. خط زمان پروژه‌های ما را ببینید."
title={t('fallbackTimelineTitle')}
lead={t('fallbackTimelineLead')}
headingKey="timeline"
align="center"
/>
+3 -1
View File
@@ -5,9 +5,11 @@ import { useSectionHeading, useSite } from '../../api';
import { AddItemButton, EditableItem, SingletonPill } from '../../admin/Editable';
import { InlineEdit } from '../admin/InlineEdit';
import { useFieldSaver } from '../../admin/AdminContext';
import { useLang, formatNumber } from '../../i18n/lang';
export function WhyUs() {
const { why_us } = useSite();
const { lang } = useLang();
const heading = useSectionHeading('why_us');
const save = useFieldSaver();
@@ -43,7 +45,7 @@ export function WhyUs() {
<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">۰{(i + 1).toLocaleString('fa-IR')}</span>
<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}