انتقال تمام محتوای متنی سایت به دیتابیس — همه متن‌های فرانت‌اند از GET /api/site خوانده می‌شوند

This commit is contained in:
MMD
2026-09-06 20:16:17 +03:30
parent 8bb303dda2
commit 1fde4e30fc
36 changed files with 1096 additions and 454 deletions
+17 -17
View File
@@ -7,7 +7,7 @@ import { useSite } from '../api'
import { API_URL } from '../config'
export function ContactSection() {
const { brand } = useSite()
const { brand, contact_section: content } = useSite()
const [form, setForm] = useState({ name: '', phone: '', note: '' })
const [sent, setSent] = useState(false)
const [sending, setSending] = useState(false)
@@ -96,12 +96,12 @@ export function ContactSection() {
<div className="border border-ivory/20 bg-char/45 p-8 backdrop-blur-[8px] md:p-10 lg:col-span-5">
<Reveal>
<p className="text-sm font-light text-copper">تماس با ما</p>
<p className="text-sm font-light text-copper">{content.label}</p>
<h2 id="contact-title" className="mt-5 text-section font-bold text-ivory">
پروژه‌تان را با ما در میان بگذارید
{content.title}
</h2>
<p className="mt-6 text-base font-light leading-9 text-ivory/75">
شماره‌تان را بگذارید؛ کارشناس ما در کمتر از یک روز کاری برای یک جلسه مشاوره رایگان تماس می‌گیرد.
{content.lead}
</p>
</Reveal>
@@ -110,14 +110,14 @@ export function ContactSection() {
<li className="flex items-start gap-4">
<MapPinIcon className="mt-1 h-5 w-5 shrink-0 text-copper" />
<div>
<p className="text-sm font-bold text-ivory">دفتر مرکزی</p>
<p className="text-sm font-bold text-ivory">{content.address_label}</p>
<p className="mt-1 text-sm font-light leading-7 text-ivory/70">{brand.address}</p>
</div>
</li>
<li className="flex items-start gap-4">
<PhoneIcon className="mt-1 h-5 w-5 shrink-0 text-copper" />
<div>
<p className="text-sm font-bold text-ivory">تلفن</p>
<p className="text-sm font-bold text-ivory">{content.phone_label}</p>
<a
href={brand.phone_href}
dir="ltr"
@@ -130,7 +130,7 @@ export function ContactSection() {
<li className="flex items-start gap-4">
<MailIcon className="mt-1 h-5 w-5 shrink-0 text-copper" />
<div>
<p className="text-sm font-bold text-ivory">ایمیل</p>
<p className="text-sm font-bold text-ivory">{content.email_label}</p>
<a
href={`mailto:${brand.email}`}
dir="ltr"
@@ -148,9 +148,9 @@ export function ContactSection() {
{sent ? (
<div className="flex h-full flex-col items-center justify-center py-14 text-center">
<CheckCircle2Icon className="h-12 w-12 text-copper" />
<h3 className="mt-6 text-xl font-bold text-ivory">درخواست شما ثبت شد</h3>
<h3 className="mt-6 text-xl font-bold text-ivory">{content.success_title}</h3>
<p className="mt-3 max-w-sm text-sm font-light leading-8 text-ivory/70">
از اعتماد شما سپاسگزاریم. کارشناسان ما به‌زودی با شما تماس می‌گیرند.
{content.success_text}
</p>
<Button
type="button"
@@ -161,28 +161,28 @@ export function ContactSection() {
setForm({ name: '', phone: '', note: '' })
}}
>
ثبت درخواست جدید
{content.success_button_label}
</Button>
</div>
) : (
<form onSubmit={submit} noValidate className="grid gap-6 md:grid-cols-2">
<div>
<label htmlFor="contact-name" className="block text-sm font-bold text-ivory/90">
نام
{content.name_label}
</label>
<input
id="contact-name"
type="text"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="نام و نام خانوادگی"
placeholder={content.name_placeholder}
className="mt-3 w-full border border-ivory/20 bg-ink/45 px-5 py-4 text-sm font-light text-ivory placeholder:text-ivory/40 transition-colors duration-200 ease-lux focus:border-copper focus:outline-none"
/>
</div>
<div>
<label htmlFor="contact-phone" className="block text-sm font-bold text-ivory/90">
شماره تلفن <span className="text-copper">*</span>
{content.phone_field_label} <span className="text-copper">*</span>
</label>
<input
id="contact-phone"
@@ -191,28 +191,28 @@ export function ContactSection() {
required
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
placeholder="09xx xxx xxxx"
placeholder={content.phone_placeholder}
className="mt-3 w-full border border-ivory/20 bg-ink/45 px-5 py-4 text-right text-sm font-light text-ivory placeholder:text-ivory/40 transition-colors duration-200 ease-lux focus:border-copper focus:outline-none"
/>
</div>
<div className="md:col-span-2">
<label htmlFor="contact-note" className="block text-sm font-bold text-ivory/90">
توضیحات <span className="font-light text-ivory/50">(اختیاری)</span>
{content.note_label} <span className="font-light text-ivory/50">{content.note_optional_label}</span>
</label>
<textarea
id="contact-note"
rows={6}
value={form.note}
onChange={(e) => setForm({ ...form, note: e.target.value })}
placeholder="درباره پروژه یا درخواست خود بنویسید…"
placeholder={content.note_placeholder}
className="mt-3 w-full resize-none border border-ivory/20 bg-ink/45 px-5 py-4 text-sm font-light leading-8 text-ivory placeholder:text-ivory/40 transition-colors duration-200 ease-lux focus:border-copper focus:outline-none"
/>
</div>
<div className="md:col-span-2">
<Button type="submit" variant="copper" className="w-full md:w-auto md:px-12" disabled={sending}>
{sending ? 'در حال ارسال…' : 'ارسال درخواست مشاوره'}
{sending ? content.sending_label : content.submit_label}
</Button>
</div>
</form>
+31 -28
View File
@@ -1,11 +1,16 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { InstagramIcon, LinkedinIcon, MailIcon, MapPinIcon, PhoneIcon, SendIcon } from 'lucide-react';
import { NAV } from '../data/site';
import { useSite } from '../api';
const SOCIAL_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
instagram: InstagramIcon,
linkedin: LinkedinIcon,
telegram: SendIcon,
};
export function Footer() {
const { brand, services } = useSite();
const { brand, services, nav, footer } = useSite();
return (
<footer className="border-t border-ivory/10 bg-char">
<div className="mx-auto max-w-site px-6 py-16 lg:px-10">
@@ -13,36 +18,34 @@ export function Footer() {
<div className="lg:col-span-4">
<img src={brand.logo} alt={brand.name} className="h-20 w-auto object-contain" />
<p className="mt-6 max-w-sm text-sm font-light leading-8 text-ivory/55">
گروه ساختمانی آرامش از سال ۱۳۸۳ ساختمان‌هایی می‌سازد که قرار است چند نسل در آن‌ها زندگی کنند؛ معماری آرام،
اجرای دقیق و سرمایه‌گذاری شفاف.
{footer.about_text}
</p>
<div className="mt-6 flex gap-3">
{[
{ Icon: InstagramIcon, label: 'اینستاگرام' },
{ Icon: LinkedinIcon, label: 'لینکدین' },
{ Icon: SendIcon, label: 'تلگرام' }].
map(({ Icon, label }) =>
<a
key={label}
href="#"
aria-label={label}
className="flex h-10 w-10 items-center justify-center border border-ivory/15 text-ivory/70 transition-colors duration-200 ease-lux hover:border-copper hover:text-copper">
<Icon className="h-4 w-4" />
</a>
)}
{footer.socials.map(({ label, href, icon }) => {
const Icon = SOCIAL_ICONS[icon] ?? InstagramIcon;
return (
<a
key={label}
href={href}
aria-label={label}
className="flex h-10 w-10 items-center justify-center border border-ivory/15 text-ivory/70 transition-colors duration-200 ease-lux hover:border-copper hover:text-copper">
<Icon className="h-4 w-4" />
</a>
);
})}
</div>
</div>
<div className="lg:col-span-2">
<h3 className="text-sm font-bold text-copper">صفحات</h3>
<h3 className="text-sm font-bold text-copper">{footer.pages_heading}</h3>
<ul className="mt-5 space-y-3">
{NAV.map((item) =>
<li key={item.to}>
{nav.map((item) =>
<li key={item.href}>
<Link
to={item.to}
to={item.href}
className="text-sm font-light text-ivory/60 transition-colors duration-200 ease-lux hover:text-copper">
{item.label}
</Link>
</li>
@@ -51,7 +54,7 @@ export function Footer() {
</div>
<div className="lg:col-span-3">
<h3 className="text-sm font-bold text-copper">خدمات</h3>
<h3 className="text-sm font-bold text-copper">{footer.services_heading}</h3>
<ul className="mt-5 space-y-3">
{services.map((service) =>
<li key={service.title} className="text-sm font-light text-ivory/60">
@@ -62,7 +65,7 @@ export function Footer() {
</div>
<div className="lg:col-span-3">
<h3 className="text-sm font-bold text-copper">ارتباط با ما</h3>
<h3 className="text-sm font-bold text-copper">{footer.contact_heading}</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" />
@@ -85,10 +88,10 @@ export function Footer() {
</div>
<div className="mt-14 flex flex-col items-center justify-between gap-4 border-t border-ivory/10 pt-6 text-xs font-light text-ivory/40 md:flex-row">
<p>© ۱۴۰۳ گروه ساختمانی آرامش — تمامی حقوق محفوظ است.</p>
<p>طراحی و ساخت با دقتِ معماری</p>
<p>{footer.copyright}</p>
<p>{footer.tagline}</p>
</div>
</div>
</footer>);
}
}
+7 -9
View File
@@ -1,12 +1,10 @@
import React, { useEffect, useState } from "react";
import { Link, NavLink, useLocation } from "react-router-dom";
import { MenuIcon, PhoneIcon, XIcon } from "lucide-react";
import { NAV } from "../data/site";
import { useSite } from "../api";
import { use } from "framer-motion/m";
export function Header() {
const { brand } = useSite();
const { brand, nav } = useSite();
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
const { pathname } = useLocation();
@@ -46,10 +44,10 @@ export function Header() {
<nav aria-label="ناوبری اصلی" className="hidden xl:block">
<ul className="flex items-center gap-7">
{NAV.map((item) => (
<li key={item.to}>
{nav.map((item) => (
<li key={item.href}>
<NavLink
to={item.to}
to={item.href}
className={({ isActive }) =>
`group relative py-2 text-[0.95rem] font-light transition-colors duration-200 ease-lux hover:text-copper [text-shadow:_0_2px_4px_rgba(0,0,0,0.6)] ${
isActive ? "text-copper" : "text-ivory/80"
@@ -110,10 +108,10 @@ export function Header() {
className="border-t border-ivory/10 bg-ink/98 px-6 py-4 xl:hidden"
>
<ul className="grid gap-1">
{NAV.map((item) => (
<li key={item.to}>
{nav.map((item) => (
<li key={item.href}>
<NavLink
to={item.to}
to={item.href}
className={({ isActive }) =>
`block border-b border-ivory/5 py-3 text-base font-light transition-colors duration-200 ease-lux ${
isActive ? "text-copper" : "text-ivory/80"
+1 -1
View File
@@ -12,7 +12,7 @@ type PageHeroProps = {
export function PageHero({ title, lead, image }: PageHeroProps) {
return (
<section className="relative flex h-[58vh] min-h-[420px] items-end overflow-hidden">
<img src={image} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover" />
{image && <img src={image} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover" />}
<div className="absolute inset-0 bg-ink/50" aria-hidden="true" />
+13 -5
View File
@@ -1,11 +1,19 @@
import React, { useEffect, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { BRAND } from '../data/site'
import { useSiteData } from '../api'
// برندینگ پیش از رسیدن پاسخ سرور — پس از بارگذاری داده‌ها از دیتابیس استفاده می‌شود
const FALLBACK_LOGO = '/images/logo.png'
const FALLBACK_NAME = 'گروه ساختمانی آرامش'
export function Preloader() {
const { data } = useSiteData()
const [done, setDone] = useState(false)
const [progress, setProgress] = useState(0)
const logo = data?.brand.logo || FALLBACK_LOGO
const name = data?.brand.name || FALLBACK_NAME
useEffect(() => {
const start = Date.now()
const total = 1800
@@ -39,13 +47,13 @@ export function Preloader() {
>
<div className="relative h-36 w-36">
<img
src={BRAND.logo}
src={logo}
alt=""
aria-hidden="true"
className="absolute inset-0 h-full w-full object-contain opacity-[0.12]"
/>
<div className="absolute inset-0 overflow-hidden" style={{ clipPath: `inset(${100 - progress}% 0 0 0)` }}>
<img src={BRAND.logo} alt="" aria-hidden="true" className="h-full w-full object-contain" />
<img src={logo} alt="" aria-hidden="true" className="h-full w-full object-contain" />
</div>
<div
className="absolute inset-x-0 h-px bg-copper"
@@ -64,10 +72,10 @@ export function Preloader() {
transition={{ duration: 0.5, delay: 0.2 }}
className="mt-4 text-sm font-light tracking-widest text-copper font-pinar"
>
{BRAND.name}
{name}
</motion.p>
</motion.div>
)}
</AnimatePresence>
)
}
}
+3 -2
View File
@@ -1,10 +1,11 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { useStatusLabel } from '../api'
export function ProjectCard({ project }: any) {
const isOngoing = project.status === 'progress'
const statusText = isOngoing ? 'در حال انجام' : 'انجام شده'
const statusText = useStatusLabel(project.status)
const dotColor = isOngoing ? 'bg-copper animate-pulse' : 'bg-ivory/60'
const textColor = isOngoing ? 'text-copper' : 'text-ivory/90'
+15 -12
View File
@@ -3,20 +3,21 @@ import { ArrowLeftIcon } from 'lucide-react';
import { Reveal } from '../Reveal';
import { ButtonLink } from '../Button';
import { ArticleCard } from '../ArticleCard';
import { useSite } from '../../api';
import { useSectionHeading, useSite } from '../../api';
export function Articles() {
const { articles } = useSite();
const heading = useSectionHeading('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">
<p className="text-sm font-light text-copper">وبلاگ</p>
<p className="text-sm font-light text-copper">{heading.label}</p>
<h2 id="articles-title" className="mt-5 text-section font-bold text-ivory">
آخرین مقالات
{heading.title}
</h2>
<p className="mt-5 text-base font-light leading-9 text-ivory/60">
یادداشت‌های تیم معماری، اجرا و سرمایه‌گذاری گروه ساختمانی آرامش درباره آنچه در کارگاه و بازار می‌گذرد.
{heading.lead}
</p>
<div className="mt-6 h-px w-24 bg-copper/70" />
</Reveal>
@@ -29,14 +30,16 @@ export function Articles() {
)}
</div>
<Reveal delay={0.1}>
<div className="mt-12 flex justify-center">
<ButtonLink to="/blog" variant="outline" className="font-light">
همه مقالات
<ArrowLeftIcon className="h-4 w-4" />
</ButtonLink>
</div>
</Reveal>
{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">
{heading.cta_label}
<ArrowLeftIcon className="h-4 w-4" />
</ButtonLink>
</div>
</Reveal>
)}
</div>
</section>);
+4 -3
View File
@@ -3,7 +3,7 @@ import { AnimatePresence, motion } from 'framer-motion'
import { PlusIcon } from 'lucide-react'
import { Reveal } from '../Reveal'
import { SectionHeading } from '../SectionHeading'
import { useSite } from '../../api'
import { useSectionHeading, useSite } from '../../api'
type FaqProps = {
hideHeading?: boolean;
@@ -11,6 +11,7 @@ type FaqProps = {
export function Faq({ hideHeading = false }: FaqProps) {
const { faqs } = useSite()
const heading = useSectionHeading('faq')
const [open, setOpen] = useState<number | null>(0)
return (
@@ -21,8 +22,8 @@ export function Faq({ hideHeading = false }: FaqProps) {
{!hideHeading && (
<div className="lg:col-span-4">
<SectionHeading
title="سوالات متداول"
lead="پاسخ کوتاه به پرتکرارترین پرسش‌های سرمایه‌گذاران و خریداران."
title={heading.title}
lead={heading.lead}
/>
</div>
)}
@@ -5,26 +5,25 @@ import { Reveal } from '../Reveal'
import { ButtonLink } from '../Button'
import { ProjectCard } from '../ProjectCard'
import { SectionHeading } from '../SectionHeading'
import { useSite } from '../../api'
import { useSectionHeading, useSite, useStatusLabel } from '../../api'
import type { ProjectStatus } from '../../data/site'
const FILTERS: { key: ProjectStatus; label: string }[] = [
{ key: 'done', label: 'انجام شده' },
{ key: 'progress', label: 'در حال انجام' },
]
export function FeaturedProjects() {
const { projects } = useSite()
const heading = useSectionHeading('featured_projects')
const doneLabel = useStatusLabel('done')
const progressLabel = useStatusLabel('progress')
const FILTERS: { key: ProjectStatus; label: string }[] = [
{ key: 'done', label: doneLabel },
{ key: 'progress', label: progressLabel },
]
const [status, setStatus] = useState<ProjectStatus>('done')
const list = projects.filter((p) => p.status === status).slice(0, 3)
return (
<section className="bg-ink py-24 lg:py-32" aria-labelledby="projects-title">
<div className="mx-auto max-w-site px-6 lg:px-10">
<SectionHeading
title="پروژه‌های شاخص"
lead="کارهایی که بیشترین چیز را درباره استاندارد ما می‌گویند؛ چه آن‌هایی که تحویل شده‌اند و چه آن‌هایی که همین امروز در حال ساخت‌اند."
/>
<SectionHeading title={heading.title} lead={heading.lead} />
<div className="mt-10 flex flex-wrap items-center gap-3" role="tablist" aria-label="فیلتر وضعیت پروژه">
{FILTERS.map((filter) => (
@@ -78,14 +77,16 @@ export function FeaturedProjects() {
</AnimatePresence>
</div>
<Reveal delay={0.1}>
<div className="mt-12 flex justify-center">
<ButtonLink to="/projects" variant="outline" className="font-light">
همه پروژه‌ها
<ArrowLeftIcon className="h-4 w-4" />
</ButtonLink>
</div>
</Reveal>
{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">
{heading.cta_label}
<ArrowLeftIcon className="h-4 w-4" />
</ButtonLink>
</div>
</Reveal>
)}
</div>
</section>
)
+12 -12
View File
@@ -3,17 +3,17 @@ import { AnimatePresence, motion } from 'framer-motion'
import { ChevronLeftIcon, ChevronRightIcon, XIcon } from 'lucide-react'
import { Reveal } from '../Reveal'
import { SectionHeading } from '../SectionHeading'
import { IMAGES } from '../../data/site'
const GRID = [IMAGES[0], IMAGES[1], IMAGES[2], IMAGES[3], IMAGES[4], IMAGES[1]]
import { useSectionHeading, useSite } from '../../api'
export function Gallery() {
const { gallery } = useSite()
const heading = useSectionHeading('gallery')
const [active, setActive] = useState<number | null>(null)
const close = useCallback(() => setActive(null), [])
const step = useCallback((dir: number) => {
setActive((prev) => (prev === null ? prev : (prev + dir + GRID.length) % GRID.length))
}, [])
setActive((prev) => (prev === null ? prev : (prev + dir + gallery.length) % gallery.length))
}, [gallery.length])
useEffect(() => {
if (active === null) return
@@ -30,14 +30,14 @@ 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={heading.title}
lead={heading.lead}
align="center"
/>
<div className="mt-14 grid grid-cols-2 gap-4 lg:grid-cols-3">
{GRID.map((src, i) => (
<Reveal key={`${src}-${i}`} delay={0.04 * i}>
{gallery.map((image, i) => (
<Reveal key={image.id} delay={0.04 * i}>
<button
type="button"
onClick={() => setActive(i)}
@@ -45,8 +45,8 @@ export function Gallery() {
aria-label={`بزرگ‌نمایی تصویر شماره ${(i + 1).toLocaleString('fa-IR')}`}
>
<img
src={src}
alt={`تصویری از پروژه‌های گروه ساختمانی آرامش، شماره ${(i + 1).toLocaleString('fa-IR')}`}
src={image.src}
alt={image.alt}
className="aspect-[4/3] w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
/>
</button>
@@ -102,7 +102,7 @@ export function Gallery() {
<motion.img
key={active}
src={GRID[active]}
src={gallery[active].src}
alt=""
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
+26 -16
View File
@@ -5,12 +5,27 @@ import { ButtonLink } from '../Button'
import { useSite } from '../../api'
export function Hero() {
const { brand } = useSite()
const { brand, hero } = useSite()
const { scrollY } = useScroll()
const imageY = useTransform(scrollY, [0, 700], [0, 110])
const contentY = useTransform(scrollY, [0, 700], [0, -55])
const overlay = useTransform(scrollY, [0, 600], [0.5, 0.78])
// واژه highlight در میانه تیتر با رنگ مسی رندر می‌شود
const renderTitle = () => {
if (hero.title_highlight && hero.title.includes(hero.title_highlight)) {
const [before, ...rest] = hero.title.split(hero.title_highlight)
return (
<>
{before}
<span className="text-copper">{hero.title_highlight}</span>
{rest.join(hero.title_highlight)}
</>
)
}
return hero.title
}
return (
<section className="relative h-screen min-h-[640px] w-full overflow-hidden" aria-label="معرفی گروه ساختمانی آرامش">
<motion.img
@@ -40,7 +55,7 @@ export function Hero() {
transition={{ duration: 0.7, delay: 2.1, ease: [0.23, 1, 0.32, 1] }}
className="text-sm font-light text-copper"
>
از سال ۱۳۸۳ — تهران، اصفهان، سواحل شمال
{hero.kicker}
</motion.p>
<motion.h1
@@ -49,9 +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"
>
ساختمانی که آرامش را
<span className="text-copper"> معماری </span>
می‌کند
{renderTitle()}
</motion.h1>
<motion.p
@@ -60,8 +73,7 @@ export function Hero() {
transition={{ duration: 0.85, delay: 2.32, ease: [0.23, 1, 0.32, 1] }}
className="mt-7 max-w-xl text-base font-light leading-9 text-ivory/70"
>
طراحی، ساخت و سرمایه‌گذاری در پروژه‌های مسکونی و تجاری لوکس؛ با استانداردی که در جزئیات دیده می‌شود و در
سال‌ها دوام می‌آورد.
{hero.subtitle}
</motion.p>
<motion.div
@@ -70,12 +82,12 @@ export function Hero() {
transition={{ duration: 0.85, delay: 2.44, ease: [0.23, 1, 0.32, 1] }}
className="mt-10 flex flex-wrap items-center gap-4"
>
<ButtonLink to="/projects" variant="copper">
مشاهده پروژه‌ها
<ButtonLink to={hero.cta_primary_href} variant="copper">
{hero.cta_primary_label}
<ArrowLeftIcon className="h-4 w-4" />
</ButtonLink>
<ButtonLink to="/contact" variant="outline" className="font-light">
مشاوره سرمایه‌گذاری
<ButtonLink to={hero.cta_secondary_href} variant="outline" className="font-light">
{hero.cta_secondary_label}
</ButtonLink>
</motion.div>
</div>
@@ -86,10 +98,8 @@ export function Hero() {
transition={{ duration: 0.9, delay: 2.6, ease: [0.23, 1, 0.32, 1] }}
className="absolute bottom-28 left-6 hidden w-60 border border-ivory/15 bg-char/60 p-6 backdrop-blur-md lg:block lg:left-10"
>
<p className="text-3xl font-bold text-copper">۴۷</p>
<p className="mt-2 text-sm font-light leading-7 text-ivory/70">
پروژه تحویل‌شده در بیست‌ویک سال فعالیت حرفه‌ای
</p>
<p className="text-3xl font-bold text-copper">{hero.badge_value}</p>
<p className="mt-2 text-sm font-light leading-7 text-ivory/70">{hero.badge_label}</p>
</motion.div>
</motion.div>
@@ -98,7 +108,7 @@ export function Hero() {
aria-label="رفتن به بخش بعدی"
className="absolute bottom-8 right-1/2 z-10 flex translate-x-1/2 flex-col items-center gap-2 text-ivory/50 transition-colors duration-200 ease-lux hover:text-copper"
>
<span className="text-xs font-light">اسکرول کنید</span>
<span className="text-xs font-light">{hero.scroll_label}</span>
<ChevronDownIcon className="h-4 w-4 animate-bounce" />
</a>
</section>
+9 -7
View File
@@ -3,9 +3,11 @@ import { CheckIcon, SendIcon } from 'lucide-react'
import Swal from 'sweetalert2'
import { Reveal } from '../Reveal'
import { Button } from '../Button'
import { useSite } from '../../api'
import { API_URL } from '../../config'
export function Newsletter() {
const { newsletter } = useSite()
const [email, setEmail] = useState('')
const [status, setStatus] = useState<'idle' | 'error' | 'done'>('idle')
const [sending, setSending] = useState(false)
@@ -89,10 +91,10 @@ export function Newsletter() {
<div className="mx-auto grid max-w-site items-center gap-8 px-6 lg:grid-cols-2 lg:px-10">
<Reveal>
<h2 id="newsletter-title" className="text-2xl font-bold leading-relaxed text-char md:text-3xl">
خبرنامه گروه ساختمانی آرامش
{newsletter.title}
</h2>
<p className="mt-3 max-w-md text-sm font-light leading-8 text-char/75">
هر ماه یک ایمیل: پروژه‌های جدید، فرصت‌های سرمایه‌گذاری و یادداشت‌های تیم معماری.
{newsletter.lead}
</p>
</Reveal>
@@ -100,7 +102,7 @@ export function Newsletter() {
<form onSubmit={submit} className="flex flex-col gap-3 sm:flex-row" noValidate>
<div className="flex-1">
<label htmlFor="newsletter-email" className="sr-only">
نشانی ایمیل
{newsletter.placeholder}
</label>
<input
id="newsletter-email"
@@ -111,21 +113,21 @@ export function Newsletter() {
setEmail(e.target.value)
if (status !== 'idle') setStatus('idle')
}}
placeholder="نشانی ایمیل شما"
placeholder={newsletter.placeholder}
className="w-full border border-transparent bg-char px-5 py-4 text-left text-sm font-light text-ivory placeholder:text-ivory/40 transition-all duration-300 ease-lux focus:border-ivory focus:outline-none"
aria-invalid={status === 'error'}
/>
{status === 'done' && (
<p className="mt-2 flex items-center gap-1.5 text-xs font-light text-char" role="status">
<CheckIcon className="h-3.5 w-3.5" />
عضویت شما ثبت شد.
{newsletter.success_text}
</p>
)}
</div>
<Button type="submit" variant="dark-ivory" disabled={sending}>
<SendIcon className="h-4 w-4" />
{sending ? 'در حال ارسال…' : 'عضویت'}
{sending ? newsletter.sending_label : newsletter.button_label}
</Button>
</form>
</Reveal>
+5 -5
View File
@@ -1,21 +1,21 @@
import React from 'react'
import { Reveal } from '../Reveal'
import { ServiceIcon } from '../ServiceIcon'
import { useSite } from '../../api'
import { useSectionHeading, useSite } from '../../api'
export function Services() {
const { services } = useSite()
const heading = useSectionHeading('services')
return (
<section className="bg-ink py-24 lg:py-32" aria-labelledby="services-title">
<div className="mx-auto max-w-site px-6 lg:px-10">
<Reveal className="max-w-2xl text-right">
<h2 id="services-title" className="text-section font-bold text-ivory">
خدمات
{heading.title}
</h2>
<p className="mt-5 text-base font-light leading-9 text-ivory/60">
از نخستین خط روی کاغذ تا تحویل کلید، مدیریت سرمایه و طراحی محوطه؛ همه مراحل در یک مجموعه و زیر یک نظام
کنترل کیفیت.
{heading.lead}
</p>
<div className="mt-6 h-px w-24 bg-copper/70" />
</Reveal>
+27 -26
View File
@@ -2,23 +2,18 @@ import React from 'react'
import { ArrowLeftIcon } from 'lucide-react'
import { Reveal } from '../Reveal'
import { ButtonLink } from '../Button'
import { IMAGES } from '../../data/site'
const BENTO = [
{ src: IMAGES[0], alt: 'نمای برج مسکونی آرامش در تهران' },
{ src: IMAGES[1], alt: 'آتریوم مرکزی مجتمع تجاری پارسه' },
{ src: IMAGES[2], alt: 'ویلاهای ساخته‌شده در سواحل شمال' },
{ src: IMAGES[3], alt: 'نمای شبانه برج دوقلوی نیاوران' },
]
import { useSite } from '../../api'
type StoryProps = {
showLink?: boolean;
showLabel?: boolean;
showQuote?: boolean;
alignTop?: boolean;
children?: React.ReactNode;
}
export function Story({ showLink = true, showLabel = true, alignTop = false, children }: StoryProps) {
export function Story({ showLink = true, showLabel = true, showQuote = false, alignTop = false, children }: StoryProps) {
const { story } = useSite()
return (
<section id="story" className="bg-ink py-24 lg:py-32" aria-labelledby="story-title">
<div className={`mx-auto grid max-w-site gap-14 px-6 lg:grid-cols-2 lg:px-10 ${alignTop ? 'items-start' : 'items-center'}`}>
@@ -26,40 +21,46 @@ export function Story({ showLink = true, showLabel = true, alignTop = false, chi
{showLabel && (
<Reveal>
<h2 id="story-title" className="text-section font-bold text-ivory">
درباره ی ما
{story.title}
</h2>
<div className="mt-6 h-px w-24 bg-copper/70" />
</Reveal>
)}
<Reveal delay={0.08}>
<p className="mt-8 text-base font-light leading-9 text-ivory/65">
گروه ساختمانی آرامش با یک دفتر کوچک معماری آغاز شد و امروز یکی از مجموعه‌های شناخته‌شده ساخت‌وساز لوکس در ایران
است. در تمام این سال‌ها یک اصل تغییر نکرده است: ساختمان باید برای زندگی ساخته شود، نه برای فروش سریع.
</p>
<p className="mt-5 text-base font-light leading-9 text-ivory/65">
هر پروژه با مطالعه زمین، نور، اقلیم و الگوی زندگی ساکنان آغاز می‌شود؛ سپس تیم طراحی، سازه و اجرا در یک
فرآیند واحد آن را تا تحویل کلید پیش می‌برند.
</p>
{story.paragraphs.map((paragraph, i) => (
<p key={i} className={`text-base font-light leading-9 text-ivory/65 ${i === 0 ? 'mt-8' : 'mt-5'}`}>
{paragraph}
</p>
))}
</Reveal>
<Reveal delay={0.16}>
<div className="mt-10 grid grid-cols-2 gap-8 border-t border-ivory/10 pt-8">
<div>
<p className="text-2xl font-bold text-copper">۹۴٪</p>
<p className="mt-2 text-sm font-light text-ivory/55">تحویل به‌موقع پروژه‌ها</p>
<p className="text-2xl font-bold text-copper">{story.stat1_value}</p>
<p className="mt-2 text-sm font-light text-ivory/55">{story.stat1_label}</p>
</div>
<div>
<p className="text-2xl font-bold text-copper">۵ سال</p>
<p className="mt-2 text-sm font-light text-ivory/55">پشتیبانی پس از تحویل</p>
<p className="text-2xl font-bold text-copper">{story.stat2_value}</p>
<p className="mt-2 text-sm font-light text-ivory/55">{story.stat2_label}</p>
</div>
</div>
</Reveal>
{showQuote && story.quote && (
<Reveal delay={0.2}>
<div className="mt-12 border-r-2 border-copper bg-char/40 p-6">
<p className="text-sm font-light leading-8 text-ivory/80">{story.quote}</p>
<p className="mt-5 text-sm font-bold text-copper">{story.quote_author}</p>
</div>
</Reveal>
)}
{showLink && (
<Reveal delay={0.22}>
<ButtonLink to="/about" variant="outline" className="mt-10 font-light">
داستان کامل ما
<ButtonLink to={story.link_href} variant="outline" className="mt-10 font-light">
{story.link_label}
<ArrowLeftIcon className="h-4 w-4" />
</ButtonLink>
</Reveal>
@@ -73,7 +74,7 @@ export function Story({ showLink = true, showLabel = true, alignTop = false, chi
</div>
<div className="grid grid-cols-2 gap-4">
{BENTO.map((item, i) => (
{story.bento.map((item, i) => (
<Reveal key={item.src + i} delay={0.06 * i}>
<div className="overflow-hidden">
<img
@@ -88,4 +89,4 @@ export function Story({ showLink = true, showLabel = true, alignTop = false, chi
</div>
</section>
)
}
}
+4 -3
View File
@@ -1,16 +1,17 @@
import React from 'react'
import { Reveal } from '../Reveal'
import { SectionHeading } from '../SectionHeading'
import { useSite } from '../../api'
import { useSectionHeading, useSite } from '../../api'
export function Team() {
const { team } = useSite()
const heading = useSectionHeading('team')
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={heading.title}
lead={heading.lead}
align="center"
/>
@@ -2,10 +2,11 @@ import React, { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { ChevronLeftIcon, ChevronRightIcon, QuoteIcon } from 'lucide-react';
import { SectionHeading } from '../SectionHeading';
import { useSite } from '../../api';
import { useSectionHeading, useSite } from '../../api';
export function Testimonials() {
const { testimonials } = useSite();
const heading = useSectionHeading('testimonials');
const [index, setIndex] = useState(0);
const item = testimonials[index];
@@ -16,7 +17,7 @@ export function Testimonials() {
return (
<section className="border-y border-ivory/10 bg-char py-24 lg:py-32" aria-labelledby="testimonials-title">
<div className="mx-auto max-w-site px-6 lg:px-10">
<SectionHeading title="نظرات مشتریان" align="center" />
<SectionHeading title={heading.title} align="center" />
<div className="relative mx-auto mt-14 max-w-3xl text-center">
<QuoteIcon className="mx-auto h-8 w-8 text-copper/60" aria-hidden="true" />
+4 -3
View File
@@ -1,6 +1,6 @@
import React, { useRef } from 'react'
import { motion, useScroll, useTransform, useInView } from 'framer-motion'
import { useSite, type TimelineItem } from '../../api'
import { useSectionHeading, useSite, type TimelineItem } from '../../api'
import { SectionHeading } from '../SectionHeading'
const ease = [0.23, 1, 0.32, 1] as const
@@ -43,6 +43,7 @@ function Item({ item, index }: { item: Milestone; index: number }) {
export function Timeline() {
const { timeline } = useSite()
const heading = useSectionHeading('timeline')
const ref = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: ref,
@@ -56,8 +57,8 @@ export function Timeline() {
<div className="mb-16">
<SectionHeading
title="مسیر ما"
lead="از یک دفتر چهارنفره تا ۴۷ پروژه. خط زمان پروژه‌های ما را ببینید."
title={heading.title}
lead={heading.lead}
align="center"
/>
</div>
+14 -12
View File
@@ -1,26 +1,28 @@
import React from 'react';
import { Reveal } from '../Reveal';
import { SectionHeading } from '../SectionHeading';
import { IMAGES } from '../../data/site';
import { useSite } from '../../api';
import { useSectionHeading, useSite } from '../../api';
export function WhyUs() {
const { why_us } = useSite();
const heading = useSectionHeading('why_us');
return (
<section className="bg-ink py-24 lg:py-32" aria-labelledby="why-title">
<div className="mx-auto grid max-w-site gap-14 px-6 lg:grid-cols-12 lg:px-10">
<div className="lg:col-span-5">
<SectionHeading
title="چرا گروه ساختمانی آرامش"
lead="شش دلیلی که کارفرمایان و سرمایه‌گذاران بار دوم هم به ما برمی‌گردند." />
<Reveal delay={0.12}>
<img
src={IMAGES[3]}
alt="نمای شبانه برج دوقلوی نیاوران"
className="mt-10 h-80 w-full object-cover" />
</Reveal>
title={heading.title}
lead={heading.lead} />
{heading.image && (
<Reveal delay={0.12}>
<img
src={heading.image}
alt={heading.title}
className="mt-10 h-80 w-full object-cover" />
</Reveal>
)}
</div>
<div className="lg:col-span-7">