diff --git a/frontend/src/components/LangToggle.tsx b/frontend/src/components/LangToggle.tsx index a3c3301..c85a56d 100644 --- a/frontend/src/components/LangToggle.tsx +++ b/frontend/src/components/LangToggle.tsx @@ -1,18 +1,6 @@ import { useLocation, useNavigate } from 'react-router-dom' +import { LanguagesIcon } from 'lucide-react' import { counterpartPath, useLang, writeLangCookie } from '../i18n/lang' -import { track } from '../analytics' - -/** - * تنظیمات ثابت هر زبان. - * ترتیب اینجا = ترتیب نمایش در کپسول (فارسی سمت چپ، English سمت راست). - * بجای مقادیر پیکسلی، از translate-x-full استفاده شده تا دقیقاً به اندازه عرض خودش (50%) جابجا شود. - */ -const LANGS = { - fa: { label: 'فارسی', sliderTranslate: 'translate-x-0' }, - en: { label: 'English', sliderTranslate: 'translate-x-full' }, -} as const - -type LangKey = keyof typeof LANGS /** دکمه تغییر زبان — بین نسخه فارسی و /en انگلیسی همان صفحه جابه‌جا می‌شود */ export function LangToggle({ className = '' }: { className?: string }) { @@ -22,9 +10,7 @@ export function LangToggle({ className = '' }: { className?: string }) { const target = counterpartPath(pathname, lang) const switchLang = () => { - const next = lang === 'fa' ? 'en' : 'fa' - track('lang-switch', { to: next }) - writeLangCookie(next) + writeLangCookie(lang === 'fa' ? 'en' : 'fa') // پرش خودکار بازدید بعدی با انتخاب دستی همین‌جا گره نمی‌خورد sessionStorage.setItem('langAutoRedirect', '1') navigate(target) @@ -36,61 +22,10 @@ export function LangToggle({ className = '' }: { className?: string }) { onClick={switchLang} aria-label={t('switchLangAria')} title={t('switchLangAria')} - dir="ltr" - className={[ - // ── Capsule shell ────────────────────────────────────────────── - 'relative flex items-center', - 'w-[152px] h-[42px] rounded-full p-1', // استفاده از p-1 (4px) به جای [4px] - 'bg-[#1a1715]', - // deep inset shadow → recessed track feel - 'shadow-[inset_0_3px_7px_rgba(0,0,0,0.75),inset_0_-1px_2px_rgba(255,255,255,0.04)]', - // interaction - 'cursor-pointer select-none', - 'active:scale-[0.975] transition-transform duration-150 ease-lux', - className, - ].join(' ')} + className={`group flex items-center gap-1.5 rounded-full border border-white/15 bg-white/5 px-3.5 py-2 text-xs font-bold text-ivory/80 backdrop-blur-lg shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)] transition-all duration-300 ease-lux hover:border-copper/60 hover:bg-white/10 hover:text-copper active:scale-95 cursor-pointer ${className}`} > - {/* ── Inner Track ─────────────────────────────────────────────── - این دیو داخلی باعث می‌شود پدینگ دکمه اصلی روی محاسبات 50 درصدی تاثیر نگذارد - */} -
- - {/* ── Slider pill ─────────────────────────────────────────────── */} -
+ + {t('switchLang')} ) -} \ No newline at end of file +} diff --git a/frontend/src/components/home/Faq.tsx b/frontend/src/components/home/Faq.tsx index cf01a41..3dc02d1 100644 --- a/frontend/src/components/home/Faq.tsx +++ b/frontend/src/components/home/Faq.tsx @@ -18,22 +18,20 @@ export function Faq({ hideHeading = false }: FaqProps) { return (
-
- {/* Header — centered, full width */} +
+ {!hideHeading && ( -
+
)} - {/* Accordion list — constrained width, direction-aware */} -
+
    {faqs.map((faq, i) => { const isOpen = open === i @@ -45,9 +43,9 @@ export function Faq({ hideHeading = false }: FaqProps) { type="button" onClick={() => setOpen(isOpen ? null : i)} aria-expanded={isOpen} - className="flex w-full items-center justify-between gap-6 py-6 text-start transition-colors duration-200 ease-lux hover:text-copper" + className="flex w-full items-center justify-between gap-6 py-6 text-right transition-colors duration-200 ease-lux hover:text-copper" > - + {faq.question} -

    {faq.answer}

    +

    {faq.answer}

    )} diff --git a/frontend/src/pages/ProjectDetail.tsx b/frontend/src/pages/ProjectDetail.tsx index 08e906e..5ace33b 100644 --- a/frontend/src/pages/ProjectDetail.tsx +++ b/frontend/src/pages/ProjectDetail.tsx @@ -1,115 +1,75 @@ -import { useEffect, useMemo, useState } from 'react' +import React, { useState } from 'react' import { Link, useParams } from 'react-router-dom' -import { ChevronLeftIcon } from 'lucide-react' +import { ArrowLeftIcon, ChevronLeftIcon } from 'lucide-react' import { Reveal } from '../components/Reveal' -import { PremiumButton } from '../components/PremiumButton' +import { ButtonLink } from '../components/Button' import { ProjectCard } from '../components/ProjectCard' import { ContactSection } from '../components/ContactSection' import { Lightbox } from '../components/Lightbox' -import { LazyProjectsMapSingle } from '../map/LazyMaps'; -import { createPlaceholderProject, findMapProjectByName } from '../data/mapProjects' +import { useEffect } from 'react'; import { useSite, useStatusLabel } from '../api'; import { useLang } from '../i18n/lang'; import { applySeo } from '../seo'; -import { projectsData } from '../data/projectsData'; import { SingletonPill } from '../admin/Editable'; export function ProjectDetail() { const { slug } = useParams<{ slug: string }>() const { projects, brand } = useSite() const { t, href, lang } = useLang() - - // اولویت با رکورد دیتابیس است تا ویرایش پنل روی صفحهٔ جزئیات هم دیده شود؛ - // فایل استاتیک فقط برای اسلاگ‌های قدیمی که در دیتابیس نیستند به کار می‌رود. - const dbProject = projects.find((p) => p.slug === slug) - const staticProject = projectsData.find((p) => p.slug === slug) - const currentProject = dbProject - ? { - id: String(dbProject.id), - slug: dbProject.slug, - name: { en: dbProject.title_en || dbProject.title, fa: dbProject.title }, - location: { en: dbProject.location_en || dbProject.location, fa: dbProject.location }, - area: { en: dbProject.area_en || dbProject.area, fa: dbProject.area }, - status: (dbProject.status === 'progress' ? 'progress' : 'done') as 'done' | 'progress', - } - : staticProject - + const project = projects.find((p) => p.slug === slug) + const statusLabel = useStatusLabel(project?.status ?? '') const [lightbox, setLightbox] = useState(null) - if (!currentProject) { - return
    Project not found
    - } - - const backendProject = dbProject - - // Dynamic bilingual text from projectsData - const displayTitle = currentProject.name[lang as 'en' | 'fa'] || currentProject.name.en; - const displayLocation = currentProject.location[lang as 'en' | 'fa'] || currentProject.location.en; - const displayArea = currentProject.area[lang as 'en' | 'fa'] || currentProject.area.en; - const displayStatus = currentProject.status; - - const statusLabel = useStatusLabel(displayStatus) - - // Fallbacks for layout structure fields missing in projectsData - const summary = backendProject?.summary || (lang === 'en' ? 'A modern and luxurious architectural project built with high-quality materials and smart design.' : 'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ.'); - const body = backendProject?.body || (lang === 'en' - ? ['Located in the heart of the city, this project offers unparalleled access to major urban hubs.', 'It features state-of-the-art facilities, green spaces, and a sustainable approach to modern living.'] - : ['لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است.', 'چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است.']); - const gallery = backendProject?.gallery?.length ? backendProject.gallery : [ - '/seed-media/2026/09/1.png', - '/seed-media/2026/09/2.png', - '/seed-media/2026/09/3.png', - '/seed-media/2026/09/4.png' - ]; - const image = backendProject?.image || '/seed-media/2026/09/1.png'; - const year = backendProject?.year || (lang === 'en' ? '2024' : '1402'); - const floors = backendProject?.floors || '5'; - const units = backendProject?.units || '20'; - const client = backendProject?.client || (lang === 'en' ? 'Aramesh Group' : 'گروه آرامش'); - - // تصاویر پاپ‌آپ نقشه - const mapImages = useMemo( - () => [image, ...gallery].filter(Boolean), - [image, gallery] - ) - - // مختصات همین پروژه - const mapLocation = useMemo( - () => findMapProjectByName(displayTitle) ?? createPlaceholderProject(displayTitle, displayLocation), - [displayTitle, displayLocation] - ) - - // SEO + // عنوان تب، توضیح متا و OG از خود پروژه useEffect(() => { + if (!project) return; applySeo({ - title: `${displayTitle} | ${brand.name}`, - description: summary, - path: href(`/projects/${currentProject.slug}`), - image: image, + title: `${project.title} | ${brand.name}`, + description: project.summary, + path: href(`/projects/${project.slug}`), + image: project.image, lang, }); - }, [currentProject.slug, lang, href, displayTitle, summary, image, brand.name]); + }, [project?.id, lang, href]); - // پروژه‌های مرتبط — از دیتابیس، و اگر خالی بود از فهرست استاتیک قدیمی - const related = (projects.length > 0 ? projects : projectsData) - .filter((p) => p.slug !== currentProject.slug) - .slice(0, 3) + if (!project) { + return ( +
    +

    {t('projectNotFound')}

    +

    + {t('projectNotFoundText')} +

    + + {t('backToProjects')} + + +
    + ) + } + + const related = projects.filter((p) => p.slug !== project.slug).slice(0, 3) const facts = [ - { label: t('specLocation'), value: displayLocation }, - { label: t('specYear'), value: year }, - { label: t('specArea'), value: displayArea }, - { label: t('specFloors'), value: floors }, - { label: t('specUnits'), value: units }, - { label: t('specClient'), value: client }, + { label: t('specLocation'), value: project.location }, + { label: t('specYear'), value: project.year }, + { label: t('specArea'), value: project.area }, + { label: t('specFloors'), value: project.floors }, + { label: t('specUnits'), value: project.units }, + { label: t('specClient'), value: project.client }, ] return ( <> -
    -
    - -
    setLightbox(null)} onChange={setLightbox}