انتقال تمام محتوای متنی سایت به دیتابیس — همه متنهای فرانتاند از GET /api/site خوانده میشوند
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>);
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user