feat: redesign site pages and connect contact API

This commit is contained in:
amirhosein.ashourloo
2026-09-22 02:00:26 +03:30
parent 6b3e87a09d
commit 55d0fad2c8
92 changed files with 611 additions and 224 deletions
+3 -3
View File
@@ -110,12 +110,12 @@ src/
## صفحه «درباره ما»
ظاهر این صفحه الهام‌گرفته از [omniyat.com/omniyat-atelier](https://www.omniyat.com/omniyat-atelier)
ظاهر این صفحه بر پایه زبان بصری جدید وب‌سایت رئوفی طراحی شده است.
است، اما **تمام محتوا از صفحه درباره ما سایت رئوفی** برداشته شده است.
سیستم چیدمانِ برگرفته از مرجع:
| ویژگی | مرجع (Omniyat) | پیاده‌سازی |
| ویژگی | طراحی هدف | پیاده‌سازی |
| --- | --- | --- |
| ریتم عمودی بلوک متن | `192px 48px` | `192px 48px` (موبایل: `96px 24px`) |
| کپشن بالای تیتر | ۱۴px، `letter-spacing: 1.4px` | همان — فقط روی بخش لاتین |
@@ -137,7 +137,7 @@ src/
## صفحه «خدمات ما»
همان سیستم چیدمان صفحه درباره ما (الهام‌گرفته از Omniyat Atelier).
همان سیستم چیدمان صفحه درباره ما و زبان بصری جدید وب‌سایت.
**نکته‌ی مهم درباره‌ی محتوا:** صفحه‌ی خدمات سایت فعلی فقط شامل شش عنوان خدمت و تیتر
«با خدمات ما بیشتر آشنا شوید...» است — هیچ توضیحی برای خدمات ندارد و لینک همه‌ی

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before

Width:  |  Height:  |  Size: 443 KiB

After

Width:  |  Height:  |  Size: 443 KiB

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Before

Width:  |  Height:  |  Size: 632 KiB

After

Width:  |  Height:  |  Size: 632 KiB

Before

Width:  |  Height:  |  Size: 610 KiB

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 561 KiB

After

Width:  |  Height:  |  Size: 561 KiB

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Before

Width:  |  Height:  |  Size: 482 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 277 KiB

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

+2 -2
View File
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { OmniyatAbout } from "@/components/about/OmniyatAbout";
import { RaoofiAbout } from "@/components/about/RaoofiAbout";
import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader";
import { getRecentPortfolios, toDisplayProject } from "@/lib/api";
@@ -19,7 +19,7 @@ export default async function AboutPage() {
return (
<>
<SiteHeader />
<OmniyatAbout projects={displayProjects} />
<RaoofiAbout projects={displayProjects} />
<SiteFooter />
</>
);
+72
View File
@@ -0,0 +1,72 @@
import { NextResponse } from "next/server";
const CONTACT_ENDPOINT =
"https://api.meshkee.com/api/v1/tenants/raoofigroup.com/contact-submissions";
const REQUEST_TIMEOUT_MS = 10_000;
type ContactRequest = {
name?: unknown;
phone?: unknown;
email?: unknown;
message?: unknown;
};
function clean(value: unknown, maxLength: number) {
return typeof value === "string" ? value.trim().slice(0, maxLength) : "";
}
export async function POST(request: Request) {
let body: ContactRequest;
try {
body = (await request.json()) as ContactRequest;
} catch {
return NextResponse.json({ message: "اطلاعات فرم معتبر نیست." }, { status: 400 });
}
const name = clean(body.name, 120);
const cellNumber = clean(body.phone, 16);
const email = clean(body.email, 254);
const text = clean(body.message, 4_000);
if (!name || !text || !/^\+989\d{9}$/.test(cellNumber)) {
return NextResponse.json({ message: "لطفاً اطلاعات فرم را به‌درستی وارد کنید." }, { status: 400 });
}
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return NextResponse.json({ message: "نشانی ایمیل معتبر نیست." }, { status: 400 });
}
try {
const response = await fetch(CONTACT_ENDPOINT, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "درخواست تماس از وب‌سایت رئوفی",
name,
cellNumber,
text,
...(email ? { email } : {}),
}),
cache: "no-store",
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
if (!response.ok) {
return NextResponse.json(
{ message: "ارسال پیام با خطا روبه‌رو شد. لطفاً دوباره تلاش کنید." },
{ status: response.status >= 500 ? 502 : 400 },
);
}
return NextResponse.json({ message: "پیام شما با موفقیت ارسال شد." }, { status: 201 });
} catch {
return NextResponse.json(
{ message: "در حال حاضر ارتباط با سرویس پیام برقرار نیست. لطفاً کمی بعد دوباره تلاش کنید." },
{ status: 503 },
);
}
}
+20
View File
@@ -0,0 +1,20 @@
import type { Metadata } from "next";
import { RaoofiAtelier } from "@/components/atelier/RaoofiAtelier";
import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader";
export const metadata: Metadata = {
title: "آتلیه رئوفی",
description: "آتلیه رئوفی؛ خلق فضاهای شخصی، کمیاب و ماندگار با طراحی و اجرای یکپارچه.",
};
export default function AtelierPage() {
return (
<>
<SiteHeader />
<RaoofiAtelier />
<SiteFooter />
</>
);
}
+1 -1
View File
@@ -50,7 +50,7 @@ export default async function BlogListPage({
/>
</section>
<section className="bg-sand py-16 lg:py-24">
<section className="bg-[#f1efec] py-24 lg:py-32">
<NewsListing
items={blog}
page={page}
+12 -11
View File
@@ -44,13 +44,14 @@ export default function ContactPage() {
/>
</section>
<section className="bg-sand py-16 lg:py-24">
<div className="mx-auto grid max-w-[1400px] gap-14 px-6 lg:grid-cols-2 lg:gap-20 lg:px-12">
<section className="bg-[#f1efec] py-24 lg:py-36">
<div className="mx-auto grid max-w-[1400px] px-5 lg:grid-cols-2 lg:px-10">
{/* Contact info + map */}
<Reveal>
<Reveal className="bg-ink p-7 text-white lg:p-12">
<SectionLabel
fa={contactPage.info.caption}
en={contactPage.info.captionLatin}
tone="dark"
align="start"
className="mb-10"
/>
@@ -58,14 +59,14 @@ export default function ContactPage() {
<ul className="space-y-6">
<li className="flex gap-4">
<PinIcon className="mt-1 h-5 w-5 shrink-0 text-gold" />
<p className="leading-[2]">{contact.address}</p>
<p className="leading-[2] text-white/75">{contact.address}</p>
</li>
{contact.phones.map((phone, i) => (
<li key={phone}>
<Link
href={`tel:${contact.phoneLinks[i]}`}
className="group flex items-center gap-4 transition-colors duration-300 hover:text-gold"
className="group flex items-center gap-4 text-white/75 transition-colors duration-300 hover:text-gold"
>
<PhoneIcon className="h-5 w-5 shrink-0 text-gold" />
<span className="latin">{phone}</span>
@@ -76,7 +77,7 @@ export default function ContactPage() {
<li>
<Link
href={`mailto:${contact.email}`}
className="group flex items-center gap-4 transition-colors duration-300 hover:text-gold"
className="group flex items-center gap-4 text-white/75 transition-colors duration-300 hover:text-gold"
>
<MailIcon className="h-5 w-5 shrink-0 text-gold" />
<span className="latin">{contact.email}</span>
@@ -86,7 +87,7 @@ export default function ContactPage() {
<li>
<Link
href={contact.instagramUrl}
className="group flex items-center gap-4 transition-colors duration-300 hover:text-gold"
className="group flex items-center gap-4 text-white/75 transition-colors duration-300 hover:text-gold"
>
<InstagramIcon className="h-5 w-5 shrink-0 text-gold" />
<span className="latin">{contact.instagram}</span>
@@ -94,21 +95,21 @@ export default function ContactPage() {
</li>
</ul>
<div className="mt-10 aspect-[4/3] w-full overflow-hidden">
<div className="mt-12 aspect-[4/3] w-full overflow-hidden border border-white/15">
<iframe
src={MAP_EMBED_SRC}
title="نقشه‌ی موقعیت گروه ساختمانی رئوفی"
loading="lazy"
allowFullScreen
referrerPolicy="strict-origin-when-cross-origin"
className="h-full w-full grayscale-[15%]"
className="h-full w-full grayscale-[35%]"
/>
</div>
</Reveal>
{/* Form */}
<Reveal delay={90}>
<div className="bg-paper p-7 lg:p-10">
<Reveal delay={90} className="bg-white p-7 lg:p-12">
<div>
<SectionLabel
fa={contactPage.form.caption}
en={contactPage.form.captionLatin}
+14
View File
@@ -151,6 +151,20 @@
color: var(--color-ink);
font-weight: 700;
}
.cms-body--editorial {
max-width: 860px;
font-size: 1.05rem;
line-height: 2.25;
text-align: center;
}
.cms-body--editorial h1,
.cms-body--editorial h2,
.cms-body--editorial h3 {
font-size: clamp(1.45rem, 2.4vw, 2.25rem);
line-height: 1.55;
}
}
@utility no-scrollbar {
+5 -5
View File
@@ -53,13 +53,13 @@ export default async function NewsAndBlogPage() {
</section>
{hasNothing && (
<section className="bg-sand py-16 lg:py-24">
<section className="bg-[#f1efec] py-24 lg:py-32">
<p className="text-center text-muted">{newsAndBlogPage.emptyMessage}</p>
</section>
)}
{hasNews && (
<section className="bg-sand py-16 lg:py-24">
<section className="bg-[#f1efec] py-24 lg:py-32">
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
<Reveal>
<SectionLabel
@@ -75,7 +75,7 @@ export default async function NewsAndBlogPage() {
<div className="mx-auto mt-10 max-w-[1400px] px-6 text-center lg:px-12">
<Link
href="/news"
className="inline-block rounded-full bg-gold px-9 py-3.5 text-white transition-colors duration-300 hover:bg-ink"
className="inline-block border-b border-ink px-1 pb-2 font-bold text-ink transition-colors duration-300 hover:border-gold hover:text-gold"
>
{newsAndBlogPage.news.action}
</Link>
@@ -84,7 +84,7 @@ export default async function NewsAndBlogPage() {
)}
{hasBlog && (
<section className={`py-16 lg:py-24 ${hasNews ? "bg-paper" : "bg-sand"}`}>
<section className={`py-24 lg:py-32 ${hasNews ? "bg-paper" : "bg-[#f1efec]"}`}>
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
<Reveal>
<SectionLabel
@@ -100,7 +100,7 @@ export default async function NewsAndBlogPage() {
<div className="mx-auto mt-10 max-w-[1400px] px-6 text-center lg:px-12">
<Link
href="/blog"
className="inline-block rounded-full bg-gold px-9 py-3.5 text-white transition-colors duration-300 hover:bg-ink"
className="inline-block border-b border-ink px-1 pb-2 font-bold text-ink transition-colors duration-300 hover:border-gold hover:text-gold"
>
{newsAndBlogPage.blog.action}
</Link>
+1 -1
View File
@@ -50,7 +50,7 @@ export default async function NewsListPage({
/>
</section>
<section className="bg-sand py-16 lg:py-24">
<section className="bg-[#f1efec] py-24 lg:py-32">
<NewsListing
items={news}
page={page}
+4 -61
View File
@@ -1,81 +1,24 @@
import type { Metadata } from "next";
import atelier from "@/assets/images/about/atelier.jpg";
import servicesHero from "@/assets/images/services/services-hero.jpg";
import { ServiceGrid } from "@/components/services/ServiceGrid";
import { Editorial } from "@/components/shared/Editorial";
import { FullBleed } from "@/components/shared/FullBleed";
import { PageCta } from "@/components/shared/PageCta";
import { PageHero } from "@/components/shared/PageHero";
import { ProjectGallery } from "@/components/shared/ProjectGallery";
import { RaoofiServices } from "@/components/services/RaoofiServices";
import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader";
import { servicesPage } from "@/data/site";
import { getRecentPortfolios, toDisplayProject } from "@/lib/api";
export const revalidate = 300; // keep in sync with api.ts PAGE_REVALIDATE_SECONDS
export const revalidate = 300;
export const metadata: Metadata = {
title: "خدمات ما",
description:
"توسعه، سرمایه‌گذاری، طراحی و معماری، مدیریت ساخت و ساز، اجرا و نظارت و طراحی دکوراسیون؛ شش حوزه‌ی تخصصی گروه ساختمانی رئوفی.",
description: "خدمات یکپارچه گروه ساختمانی رئوفی؛ از نخستین ایده و طراحی تا مدیریت، اجرا و تحویل نهایی.",
};
export default async function ServicesPage() {
const portfolios = await getRecentPortfolios(12);
const displayProjects = portfolios.map(toDisplayProject);
return (
<>
<SiteHeader />
<main>
<PageHero
image={servicesHero}
title={servicesPage.hero.title}
breadcrumb={servicesPage.hero.breadcrumb}
/>
{/* Overview */}
<section className="bg-paper">
<Editorial
caption={servicesPage.overview.caption}
captionLatin={servicesPage.overview.captionLatin}
title={servicesPage.overview.title}
body={servicesPage.overview.body}
/>
<FullBleed image={atelier} alt="پروژه‌های گروه ساختمانی رئوفی" />
</section>
{/* The six disciplines */}
<section className="bg-sand">
<Editorial
caption={servicesPage.disciplines.caption}
captionLatin={servicesPage.disciplines.captionLatin}
title={servicesPage.disciplines.title}
body={servicesPage.disciplines.body}
/>
<ServiceGrid />
</section>
{/* Proof of work */}
<section className="bg-paper">
<Editorial
caption={servicesPage.portfolio.caption}
captionLatin={servicesPage.portfolio.captionLatin}
title={servicesPage.portfolio.title}
body={servicesPage.portfolio.body}
/>
<ProjectGallery projects={displayProjects} />
</section>
<PageCta
caption={servicesPage.cta.caption}
title={servicesPage.cta.title}
action={servicesPage.cta.action}
/>
</main>
<RaoofiServices projects={portfolios.map(toDisplayProject)} />
<SiteFooter />
</>
);
+2 -2
View File
@@ -1,5 +1,5 @@
import { OmniyatHome } from "@/components/omniyat/OmniyatHome";
import { RaoofiHome } from "@/components/home/RaoofiHome";
export default function HomePage() {
return <OmniyatHome />;
return <RaoofiHome />;
}
+9 -9
View File
@@ -63,8 +63,8 @@ export default async function PortfolioDetailPage({
// Backgrounds alternate by what actually renders, not by fixed section
// name — otherwise skipping the video section (no link set) could leave
// two sections the same colour back to back.
const videoBg = "bg-sand";
const galleryBg = videoUrl ? "bg-paper" : "bg-sand";
const videoBg = "bg-[#f1efec]";
const galleryBg = videoUrl ? "bg-paper" : "bg-[#f1efec]";
return (
<>
@@ -83,13 +83,13 @@ export default async function PortfolioDetailPage({
{/* Jump nav — only rendered when there is a second section to jump to. */}
{navItems.length > 1 && (
<nav aria-label="بخش‌های صفحه" className="border-b border-line bg-paper">
<ul className="no-scrollbar mx-auto flex max-w-[1400px] items-center gap-8 overflow-x-auto px-6 lg:px-12">
<nav aria-label="بخش‌های صفحه" className="border-b border-line bg-[#f1efec]">
<ul className="no-scrollbar mx-auto flex max-w-[1400px] items-center justify-center gap-10 overflow-x-auto px-6 lg:px-12">
{navItems.map((navItem) => (
<li key={navItem.id}>
<a
href={`#${navItem.id}`}
className="block scroll-mt-24 py-5 text-sm whitespace-nowrap text-muted transition-colors duration-300 hover:text-gold"
className="block scroll-mt-24 border-b-2 border-transparent py-5 text-sm font-bold whitespace-nowrap text-ink transition-colors duration-300 hover:border-gold hover:text-gold"
>
{navItem.label}
</a>
@@ -99,7 +99,7 @@ export default async function PortfolioDetailPage({
</nav>
)}
<section id="overview" className="scroll-mt-20 bg-paper py-16 lg:py-24">
<section id="overview" className="scroll-mt-20 bg-paper py-24 lg:py-36">
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
<Reveal>
<SectionLabel fa="معرفی پروژه" en="PROJECT OVERVIEW" className="mb-14" />
@@ -107,14 +107,14 @@ export default async function PortfolioDetailPage({
{project.bodyHtml && (
<Reveal delay={90}>
<div className="cms-body" dangerouslySetInnerHTML={{ __html: project.bodyHtml }} />
<div className="cms-body cms-body--editorial" dangerouslySetInnerHTML={{ __html: project.bodyHtml }} />
</Reveal>
)}
</div>
</section>
{videoUrl && (
<section id="video" className={`scroll-mt-20 ${videoBg} py-16 lg:py-24`}>
<section id="video" className={`scroll-mt-20 ${videoBg} py-24 lg:py-32`}>
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
<Reveal>
<SectionLabel fa="ویدیو" en="VIDEO" className="mb-14" />
@@ -128,7 +128,7 @@ export default async function PortfolioDetailPage({
)}
{hasGallery && (
<section id="gallery" className={`scroll-mt-20 ${galleryBg} py-16 lg:py-24`}>
<section id="gallery" className={`scroll-mt-20 ${galleryBg} py-24 lg:py-32`}>
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
<Reveal>
<SectionLabel fa="گالری تصاویر" en="GALLERY" className="mb-14" />
+2 -1
View File
@@ -43,7 +43,8 @@ export default async function PortfoliosPage() {
body={portfoliosPage.overview.body}
compact
/>
</section>
<section className="bg-[#f1efec] pt-24 lg:pt-32">
<PortfolioBrowser projects={displayProjects} categories={categories} />
</section>
@@ -1,6 +1,5 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { Swiper, SwiperSlide } from "swiper/react";
@@ -8,9 +7,10 @@ import type { Swiper as SwiperInstance } from "swiper";
import "swiper/css";
import type { DisplayProject } from "@/lib/api";
import "./omniyat-about.css";
import { EditorialCardCarousel } from "@/components/shared/EditorialCardCarousel";
import "./raoofi-about.css";
const asset = (name: string) => `/omniyat-reference/about/${name}`;
const asset = (name: string) => `/raoofi-media/about/${name}`;
const fallbackProjects = [
{ slug: "", name: "فصل نخست یک رؤیا", type: "۱۳۷۹", image: asset("timeline-1.webp") },
@@ -26,11 +26,9 @@ const pillars = [
{ image: asset("pillar-4.webp"), title: "جسارت رؤیاپردازی", text: "مرزهای ممکن را جابه‌جا می‌کنیم و ایده‌های بلندپروازانه را با جزئیات بی‌نقص به واقعیت می‌رسانیم." },
];
export function OmniyatAbout({ projects: _projects }: { projects: DisplayProject[] }) {
export function RaoofiAbout({ projects: _projects }: { projects: DisplayProject[] }) {
const timelineRef = useRef<SwiperInstance | null>(null);
const pillarsRef = useRef<SwiperInstance | null>(null);
const [timelineIndex, setTimelineIndex] = useState(0);
const [pillarIndex, setPillarIndex] = useState(0);
// This timeline follows the reference page's editorial sequence and uses
// locally bundled assets so it never depends on an external image host.
const timeline = fallbackProjects;
@@ -53,7 +51,7 @@ export function OmniyatAbout({ projects: _projects }: { projects: DisplayProject
<main className="oas-about" dir="rtl">
<section className="oas-hero">
<div className="oas-hero-media">
<video autoPlay muted loop playsInline preload="metadata" poster="/omniyat-reference/hero.jpg">
<video autoPlay muted loop playsInline preload="metadata" poster="/raoofi-media/hero.jpg">
<source src={asset("hero-mobile.mp4")} media="(max-width: 700px)" type="video/mp4" />
<source src={asset("hero-desktop.mp4")} type="video/mp4" />
</video>
@@ -124,27 +122,8 @@ export function OmniyatAbout({ projects: _projects }: { projects: DisplayProject
<p>پیوندی عمیق میان هنر و معماری، همراه با جاه‌طلبی همیشگی برای دستیابی به آنچه دست‌نیافتنی به نظر می‌رسد.</p>
</header>
<div className="oas-pillar-carousel oas-reveal">
<Swiper dir="ltr" loop speed={700} slidesPerView="auto" spaceBetween={34} grabCursor onSwiper={(swiper) => { pillarsRef.current = swiper; setPillarIndex(swiper.realIndex); }} onSlideChange={(swiper) => setPillarIndex(swiper.realIndex)}>
{pillars.map((pillar) => (
<SwiperSlide key={pillar.title}>
<article dir="ltr">
<div className="oas-pillar-image"><Image src={pillar.image} alt={pillar.title} fill sizes="(max-width: 700px) 82vw, 400px" /></div>
<div className="oas-pillar-copy">
<h3>{pillar.title}</h3>
<p>{pillar.text}</p>
</div>
</article>
</SwiperSlide>
))}
</Swiper>
<div className="oas-pillar-progress" aria-hidden="true">
<span style={{ width: `${100 / pillars.length}%`, transform: `translateX(${pillarIndex * 100}%)` }} />
</div>
<div className="oas-carousel-buttons oas-pillar-buttons">
<button type="button" onClick={() => pillarsRef.current?.slidePrev()} aria-label="ستون قبلی">←</button>
<button type="button" onClick={() => pillarsRef.current?.slideNext()} aria-label="ستون بعدی">→</button>
</div>
<div className="oas-reveal">
<EditorialCardCarousel items={pillars} previousLabel="ستون قبلی" nextLabel="ستون بعدی" />
</div>
<div className="oas-statements oas-reveal">
-10
View File
@@ -1,10 +0,0 @@
.oas-about{background:#fff;color:#111a3b;overflow-x:clip}.oas-about h1,.oas-about h2,.oas-about h3,.oas-about p{margin:0}.oas-shade{position:absolute;inset:0;background:linear-gradient(0deg,rgba(4,9,18,.58),rgba(4,9,18,.08) 60%)}
.oas-hero{position:relative;height:150svh;min-height:900px;background:#111}.oas-hero-media{position:sticky;top:0;width:100%;height:100svh;overflow:hidden}.oas-hero video{display:block;width:100%;height:100%;object-fit:cover}.oas-hero-media>.oas-shade{position:absolute;inset:0}.oas-hero-title{position:absolute;z-index:2;top:40%;left:5%;right:5%;text-align:center;color:#fff}.oas-hero-title h1,.oas-hero-title h2{font-size:clamp(34px,4.6vw,67px);font-weight:300;line-height:1.35;color:#fff}.oas-hero-title h2{margin-top:5px}
.oas-story{padding:145px 5vw 0}.oas-copy{max-width:1360px;margin:auto;text-align:center}.oas-copy>span,.oas-section-heading>span{display:block;color:#9a8568;font-size:12px;letter-spacing:.08em}.oas-copy h2{max-width:1100px;margin:42px auto 55px;font-size:clamp(30px,calc(5.5vw - 25px),54px);font-weight:300;line-height:1.25}.oas-copy>div{display:grid;gap:54px;max-width:1190px;margin:auto;color:#111a3b;font-size:clamp(16px,1.55vw,22px);line-height:2;text-align:center}.oas-copy>div p{max-width:1150px;margin-inline:auto}.oas-stats{display:grid;grid-template-columns:repeat(3,1fr);max-width:1060px;margin:150px auto 180px}.oas-stats div{display:flex;min-height:128px;flex-direction:column;justify-content:center;gap:10px;padding-inline:32px;border-inline-start:1px solid #c9cbd3}.oas-stats strong{font-size:clamp(50px,6vw,84px);font-weight:300;line-height:1}.oas-stats span{color:#777b85;font-size:14px}.oas-copy-secondary{padding-bottom:160px}.oas-copy-secondary>div{max-width:1100px}
.oas-timeline{position:relative;height:100vh;height:100svh;min-height:650px;background:#111;overflow:hidden}.oas-timeline>.swiper{position:absolute;inset:0;height:auto}.oas-timeline .swiper-wrapper,.oas-timeline .swiper-slide,.oas-timeline article{height:100%}.oas-timeline article{position:relative;overflow:hidden;background:#111}.oas-timeline-media{position:absolute;z-index:0;inset:0;background-position:center;background-size:cover;background-repeat:no-repeat}.oas-timeline article>.oas-shade{z-index:1}.oas-timeline-caption{position:absolute;z-index:2;right:7%;bottom:12%;direction:rtl;color:#fff}.oas-timeline-caption small{font-size:12px}.oas-timeline-caption h3{max-width:700px;margin:12px 0 25px;font-size:clamp(37px,5vw,72px);font-weight:300;color:#fff}.oas-timeline-caption a{display:inline-block;border-bottom:1px solid #fff;padding-bottom:8px;color:#fff;font-size:13px}.oas-carousel-buttons{position:absolute;z-index:5;left:5%;bottom:8%;display:flex;gap:12px}.oas-carousel-buttons button{display:grid;width:48px;height:48px;place-items:center;border:1px solid rgba(255,255,255,.7);border-radius:50%;background:transparent;color:#fff;font-size:21px;cursor:pointer;transition:.3s}.oas-carousel-buttons button:hover{background:#fff;color:#111a3b}.oas-timeline-dots{position:absolute;z-index:5;right:7%;left:18%;bottom:6%;display:flex;direction:ltr}.oas-timeline-dots button{height:2px;flex:1;border:0;background:rgba(255,255,255,.35);padding:0;cursor:pointer}.oas-timeline-dots button.is-active{background:#fff}
.oas-pillars{padding:150px 0;background:#f1efec}.oas-section-heading{width:min(1180px,90%);margin:0 auto 90px;text-align:center}.oas-section-heading h2{margin-top:25px;font-size:clamp(39px,5vw,70px);font-weight:300;line-height:1.25}.oas-section-heading p{max-width:760px;margin:28px auto 0;color:#676b75;line-height:2}.oas-pillar-carousel{position:relative;padding-left:2vw;padding-bottom:115px}.oas-pillar-carousel .swiper-slide{width:38vw;height:auto}.oas-pillar-carousel article{display:grid;height:19vw;grid-template-columns:1fr 1fr;background:#fff}.oas-pillar-image{position:relative;min-height:0;overflow:hidden}.oas-pillar-image img{object-fit:cover;transition:transform .8s}.oas-pillar-carousel article:hover img{transform:scale(1.035)}.oas-pillar-copy{display:flex;min-width:0;flex-direction:column;justify-content:space-between;padding:clamp(14px,1.8vw,44px) clamp(14px,1.5vw,36px);background:#fff;direction:rtl;text-align:right}.oas-pillar-carousel h3{margin:0;font-size:clamp(18px,1.65vw,36px);font-weight:300;line-height:1.35}.oas-pillar-carousel p{margin:0;color:#111a3b;font-size:clamp(11px,.9vw,16px);line-height:1.8}.oas-pillar-progress{position:absolute;z-index:4;right:38%;bottom:45px;width:23%;height:2px;background:#d0cfcc;overflow:hidden;direction:ltr}.oas-pillar-progress span{display:block;height:100%;background:#111a3b;transition:transform .7s cubic-bezier(.22,1,.36,1)}.oas-pillar-buttons{right:5vw;bottom:22px;left:auto;direction:ltr}.oas-pillar-buttons button{border:0;color:#111a3b;font-size:34px}.oas-pillar-buttons button:hover{background:transparent;color:#9a8568}.oas-statements{display:grid;grid-template-columns:1fr 1fr;gap:12vw;width:min(1060px,85%);margin:120px auto 0}.oas-statements p{font-size:clamp(20px,2.1vw,29px);font-weight:300;line-height:1.9}
.oas-founder{padding:145px 0 0}.oas-founder-image{display:block;width:100%;height:min(58vw,760px);overflow:hidden}.oas-founder-image img{width:100%;height:100%;object-fit:cover}.oas-founder-copy{display:grid;grid-template-columns:.8fr 1.2fr;gap:10vw;width:min(1160px,90%);margin:90px auto 150px}.oas-founder-copy h3{font-size:clamp(30px,3vw,47px);font-weight:300}.oas-founder-copy span{display:block;margin-top:10px;color:#9a8568}.oas-founder-copy p{color:#575c6b;font-size:16px;line-height:2.1}
.oas-enquire{position:relative;height:650px;margin:0 5vw 80px;overflow:hidden}.oas-enquire video{width:100%;height:100%;object-fit:cover}.oas-enquire-copy{position:absolute;z-index:2;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;text-align:center;color:#fff}.oas-enquire-copy span{font-size:12px}.oas-enquire-copy h2{margin:18px 0 5px;font-size:clamp(42px,5vw,72px);font-weight:300;color:#fff}.oas-enquire-copy p{margin-bottom:35px}.oas-enquire-copy a{min-width:220px;border:1px solid #fff;padding:13px 25px;color:#fff;transition:.3s}.oas-enquire-copy a:hover{background:#fff;color:#111a3b}
.oas-reveal{opacity:0;transform:translateY(35px);transition:opacity .9s cubic-bezier(.22,1,.36,1),transform .9s cubic-bezier(.22,1,.36,1)}.oas-reveal.is-visible{opacity:1;transform:none}
@media(max-width:700px){.oas-hero{height:120svh;min-height:720px}.oas-hero-title{top:36%}.oas-hero-title h1,.oas-hero-title h2{font-size:clamp(33px,9vw,50px)}.oas-story{padding:95px 22px 0}.oas-copy h2{margin:28px auto 38px;font-size:clamp(38px,10vw,52px)}.oas-copy>div{gap:32px;font-size:14px;line-height:2.15}.oas-stats{margin:90px 0 105px}.oas-stats div{min-height:95px;padding-inline:10px}.oas-stats strong{font-size:clamp(38px,11vw,56px)}.oas-stats span{font-size:10px}.oas-copy-secondary{padding-bottom:100px}.oas-timeline{height:78svh;min-height:570px}.oas-timeline-caption{right:25px;left:25px;bottom:18%}.oas-timeline-caption h3{font-size:36px}.oas-carousel-buttons{left:22px;bottom:5%}.oas-timeline-dots{right:22px;left:140px;bottom:8%}.oas-pillars{padding:100px 0}.oas-section-heading{margin-bottom:60px}.oas-section-heading h2{font-size:clamp(38px,10vw,54px)}.oas-section-heading p{font-size:14px}.oas-pillar-carousel{padding-left:18px;padding-bottom:100px}.oas-pillar-carousel .swiper-slide{width:88vw}.oas-pillar-carousel article{height:auto;min-height:0;grid-template-columns:1fr;aspect-ratio:auto}.oas-pillar-image{min-height:0;aspect-ratio:1}.oas-pillar-copy{min-height:260px;padding:30px 24px}.oas-pillar-carousel h3{font-size:24px}.oas-pillar-carousel p{font-size:14px}.oas-pillar-progress{right:150px;bottom:42px;width:calc(100% - 175px)}.oas-pillar-buttons{right:18px;bottom:19px;left:auto}.oas-statements{grid-template-columns:1fr;gap:55px;margin-top:80px}.oas-statements p{font-size:20px}.oas-founder{padding-top:100px}.oas-founder-image{height:125vw;max-height:680px}.oas-founder-copy{grid-template-columns:1fr;gap:35px;margin-block:65px 100px}.oas-founder-copy p{font-size:14px}.oas-enquire{height:560px;margin:0}.oas-enquire-copy{padding:25px}.oas-enquire-copy p{font-size:14px}}
@media(prefers-reduced-motion:reduce){.oas-reveal{opacity:1;transform:none}.oas-pillar-image img{transition:none}}
+10
View File
@@ -0,0 +1,10 @@
.oas-about{background:#fff;color:#111a3b;overflow-x:clip}.oas-about h1,.oas-about h2,.oas-about h3,.oas-about p{margin:0}.oas-shade{position:absolute;inset:0;background:linear-gradient(0deg,rgba(4,9,18,.58),rgba(4,9,18,.08) 60%)}
.oas-hero{position:relative;height:150svh;min-height:900px;background:#111}.oas-hero-media{position:sticky;top:0;width:100%;height:100svh;overflow:hidden}.oas-hero video{display:block;width:100%;height:100%;object-fit:cover}.oas-hero-media>.oas-shade{position:absolute;inset:0}.oas-hero-title{position:absolute;z-index:2;top:40%;left:5%;right:5%;text-align:center;color:#fff}.oas-hero-title h1,.oas-hero-title h2{font-size:clamp(30px,3.5vw,52px);font-weight:700;line-height:1.35;color:#fff}.oas-hero-title h2{margin-top:5px}
.oas-story{padding:145px 5vw 0}.oas-copy{max-width:1360px;margin:auto;text-align:center}.oas-copy>span,.oas-section-heading>span{display:block;color:#9a8568;font-size:12px;letter-spacing:.08em}.oas-copy h2{max-width:900px;margin:38px auto 50px;font-size:clamp(28px,3vw,42px);font-weight:700;line-height:1.35}.oas-copy>div{display:grid;gap:54px;max-width:1190px;margin:auto;color:#111a3b;font-size:clamp(16px,1.55vw,22px);line-height:2;text-align:center}.oas-copy>div p{max-width:1150px;margin-inline:auto}.oas-stats{display:grid;grid-template-columns:repeat(3,1fr);max-width:1060px;margin:150px auto 180px}.oas-stats div{display:flex;min-height:128px;flex-direction:column;justify-content:center;gap:10px;padding-inline:32px;border-inline-start:1px solid #c9cbd3}.oas-stats strong{font-size:clamp(50px,6vw,84px);font-weight:700;line-height:1}.oas-stats span{color:#777b85;font-size:14px}.oas-copy-secondary{padding-bottom:160px}.oas-copy-secondary>div{max-width:1100px}
.oas-timeline{position:relative;height:100vh;height:100svh;min-height:650px;background:#111;overflow:hidden}.oas-timeline>.swiper{position:absolute;inset:0;height:auto}.oas-timeline .swiper-wrapper,.oas-timeline .swiper-slide,.oas-timeline article{height:100%}.oas-timeline article{position:relative;overflow:hidden;background:#111}.oas-timeline-media{position:absolute;z-index:0;inset:0;background-position:center;background-size:cover;background-repeat:no-repeat}.oas-timeline article>.oas-shade{z-index:1}.oas-timeline-caption{position:absolute;z-index:2;right:7%;bottom:12%;direction:rtl;color:#fff}.oas-timeline-caption small{font-size:12px}.oas-timeline-caption h3{max-width:700px;margin:12px 0 25px;font-size:clamp(28px,3.5vw,46px);font-weight:700;color:#fff}.oas-timeline-caption a{display:inline-block;border-bottom:1px solid #fff;padding-bottom:8px;color:#fff;font-size:13px}.oas-carousel-buttons{position:absolute;z-index:5;left:5%;bottom:8%;display:flex;gap:12px}.oas-carousel-buttons button{display:grid;width:48px;height:48px;place-items:center;border:1px solid rgba(255,255,255,.7);border-radius:50%;background:transparent;color:#fff;font-size:21px;cursor:pointer;transition:.3s}.oas-carousel-buttons button:hover{background:#fff;color:#111a3b}.oas-timeline-dots{position:absolute;z-index:5;right:7%;left:18%;bottom:6%;display:flex;direction:ltr}.oas-timeline-dots button{height:2px;flex:1;border:0;background:rgba(255,255,255,.35);padding:0;cursor:pointer}.oas-timeline-dots button.is-active{background:#fff}
.oas-pillars{padding:150px 0;background:#f1efec}.oas-section-heading{width:min(1180px,90%);margin:0 auto 90px;text-align:center}.oas-section-heading h2{margin-top:25px;font-size:clamp(28px,3vw,44px);font-weight:700;line-height:1.35}.oas-section-heading p{max-width:760px;margin:28px auto 0;color:#676b75;line-height:2}.oas-statements{display:grid;grid-template-columns:1fr 1fr;gap:12vw;width:min(1060px,85%);margin:120px auto 0}.oas-statements p{font-size:clamp(20px,2.1vw,29px);font-weight:300;line-height:1.9}
.oas-founder{padding:145px 0 0}.oas-founder-image{display:block;width:100%;height:min(58vw,760px);overflow:hidden}.oas-founder-image img{width:100%;height:100%;object-fit:cover}.oas-founder-copy{display:grid;grid-template-columns:.8fr 1.2fr;gap:10vw;width:min(1160px,90%);margin:90px auto 150px}.oas-founder-copy h3{font-size:clamp(24px,2.4vw,36px);font-weight:700}.oas-founder-copy span{display:block;margin-top:10px;color:#9a8568}.oas-founder-copy p{color:#575c6b;font-size:16px;line-height:2.1}
.oas-enquire{position:relative;height:650px;margin:0 5vw 80px;overflow:hidden}.oas-enquire video{width:100%;height:100%;object-fit:cover}.oas-enquire-copy{position:absolute;z-index:2;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;text-align:center;color:#fff}.oas-enquire-copy span{font-size:12px}.oas-enquire-copy h2{margin:18px 0 5px;font-size:clamp(30px,3.2vw,46px);font-weight:700;color:#fff}.oas-enquire-copy p{margin-bottom:35px}.oas-enquire-copy a{min-width:220px;border:1px solid #fff;padding:13px 25px;color:#fff;transition:.3s}.oas-enquire-copy a:hover{background:#fff;color:#111a3b}
.oas-reveal{opacity:0;transform:translateY(35px);transition:opacity .9s cubic-bezier(.22,1,.36,1),transform .9s cubic-bezier(.22,1,.36,1)}.oas-reveal.is-visible{opacity:1;transform:none}
@media(max-width:700px){.oas-hero{height:120svh;min-height:720px}.oas-hero-title{top:36%}.oas-hero-title h1,.oas-hero-title h2{font-size:clamp(28px,8vw,38px)}.oas-story{padding:95px 22px 0}.oas-copy h2{margin:28px auto 38px;font-size:clamp(28px,8vw,38px)}.oas-copy>div{gap:32px;font-size:14px;line-height:2.15}.oas-stats{margin:90px 0 105px}.oas-stats div{min-height:95px;padding-inline:10px}.oas-stats strong{font-size:clamp(38px,11vw,56px)}.oas-stats span{font-size:10px}.oas-copy-secondary{padding-bottom:100px}.oas-timeline{height:78svh;min-height:570px}.oas-timeline-caption{right:25px;left:25px;bottom:18%}.oas-timeline-caption h3{font-size:30px}.oas-carousel-buttons{left:22px;bottom:5%}.oas-timeline-dots{right:22px;left:140px;bottom:8%}.oas-pillars{padding:100px 0}.oas-section-heading{margin-bottom:60px}.oas-section-heading h2{font-size:clamp(28px,8vw,38px)}.oas-section-heading p{font-size:14px}.oas-statements{grid-template-columns:1fr;gap:55px;margin-top:80px}.oas-statements p{font-size:20px}.oas-founder{padding-top:100px}.oas-founder-image{height:125vw;max-height:680px}.oas-founder-copy{grid-template-columns:1fr;gap:35px;margin-block:65px 100px}.oas-founder-copy p{font-size:14px}.oas-enquire{height:560px;margin:0}.oas-enquire-copy{padding:25px}.oas-enquire-copy p{font-size:14px}}
@media(prefers-reduced-motion:reduce){.oas-reveal{opacity:1;transform:none}}
+127
View File
@@ -0,0 +1,127 @@
"use client";
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
import type { Swiper as SwiperInstance } from "swiper";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "./raoofi-atelier.css";
const media = (name: string) => `/raoofi-media/atelier/${name}`;
const collection = [
{ image: media("collection-1.webp"), title: "اقامتگاهی فراتر از تعریف", subtitle: "فضایی برای یک زندگی یگانه" },
{ image: media("collection-2.webp"), title: "خانه‌ای در اوج آسمان", subtitle: "روایتی شخصی از آرامش" },
{ image: media("collection-3.webp"), title: "شاهکاری برای یک نفر", subtitle: "طراحی‌شده برای ماندن" },
];
export function RaoofiAtelier() {
const galleryRef = useRef<SwiperInstance | null>(null);
const [activeSlide, setActiveSlide] = useState(0);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => entries.forEach((entry) => entry.isIntersecting && entry.target.classList.add("is-visible")),
{ threshold: 0.12 },
);
document.querySelectorAll(".atelier-reveal").forEach((element) => observer.observe(element));
return () => observer.disconnect();
}, []);
return (
<main className="atelier-page" dir="rtl">
<section className="atelier-hero">
<video autoPlay muted loop playsInline preload="metadata" poster={media("hero-poster.jpg")}>
<source src={media("hero-mobile.mp4")} media="(max-width:700px)" type="video/mp4" />
<source src={media("hero-desktop.mp4")} type="video/mp4" />
</video>
<div className="atelier-shade" />
<div className="atelier-hero__brand">
<strong>آتلیه رئوفی</strong>
<span>هنرِ خلق یک زندگی منحصربه‌فرد</span>
</div>
<a className="atelier-hero__link" href="#collection">کشف مجموعه</a>
<span className="atelier-scroll" aria-hidden="true">⌄</span>
</section>
<section className="atelier-intro atelier-reveal">
<span>آتلیه رئوفی</span>
<h1>همکاری در بالاترین سطح</h1>
<div>
<p>آتلیه رئوفی برای کسانی شکل گرفته است که معیارهای زندگی خود را تعریف می‌کنند و می‌خواهند چشم‌انداز شخصی‌شان در تمام جزئیات فضا دیده شود.</p>
<p>این مسیر با همراهی معماران، طراحان و استادکاران برجسته، کمیاب‌ترین ایده‌ها را به آثاری دقیق و ماندگار تبدیل می‌کند.</p>
<p>ریشه در هنر، مهندسی و ساخت سفارشی دارد؛ تعریفی امروزی از کیفیت که عمیقاً شخصی و همواره یگانه است.</p>
</div>
</section>
<section className="atelier-wide-image atelier-reveal">
<Image src={media("story-1.webp")} alt="فضای معماری اختصاصی" fill sizes="100vw" />
</section>
<section className="atelier-intro atelier-intro--second atelier-reveal">
<span>خلق منحصربه‌فرد</span>
<h2>آثاری که تنها یک‌بار ساخته می‌شوند</h2>
<div>
<p>هر پروژه پاسخی اختصاصی به شیوه زندگی، خواسته‌ها و رویاهای صاحب آن است؛ اثری که از تعریف‌های معمول تجمل و شکوه فراتر می‌رود.</p>
<p>از معماری و طراحی داخلی تا انتخاب هنر، متریال و جزئیات نهایی، همه‌چیز برای شکل‌دادن به یک جهان کاملاً شخصی کنار هم قرار می‌گیرد.</p>
<p>نتیجه، مجموعه‌ای محدود از فضاهای کمیاب است که مشابهی ندارند و با گذر زمان ارزشمندتر می‌شوند.</p>
</div>
</section>
<section className="atelier-editorial">
<div className="atelier-editorial__pair atelier-reveal">
<div className="atelier-editorial__image"><Image src={media("story-2.webp")} alt="جزئیات معماری لوکس" fill sizes="50vw" /></div>
<div className="atelier-editorial__image"><Image src={media("story-3.jpg")} alt="خانه‌ای با طراحی سفارشی" fill sizes="50vw" /></div>
</div>
<div className="atelier-editorial__copy atelier-reveal">
<p>مسیر آتلیه رئوفی با یک چشم‌انداز روشن آغاز شد: جابه‌جایی معیارهای بازار از طریق پروژه‌هایی که در طراحی، کیفیت ساخت و تجربه زندگی پیشگام هستند.</p>
</div>
<div className="atelier-editorial__feature atelier-reveal">
<div className="atelier-editorial__image"><Image src={media("story-4.webp")} alt="فضای داخلی معاصر" fill sizes="62vw" /></div>
<div className="atelier-editorial__copy"><p>هر اثر تازه، استاندارد قبلی را پشت سر می‌گذارد. پیوستگی میان ایده، طراحی و اجرا باعث می‌شود هر خانه هویتی مستقل و کیفیتی تکرارنشدنی پیدا کند.</p></div>
</div>
</section>
<section id="collection" className="atelier-collection">
<header className="atelier-section-title atelier-reveal">
<span>مجموعه آتلیه</span>
<h2>فصلی تازه از یک زندگی استثنایی</h2>
</header>
<div className="atelier-gallery atelier-reveal">
<Swiper
dir="ltr"
loop
speed={850}
slidesPerView={1}
grabCursor
onSwiper={(swiper) => { galleryRef.current = swiper; setActiveSlide(swiper.realIndex); }}
onSlideChange={(swiper) => setActiveSlide(swiper.realIndex)}
>
{collection.map((item) => (
<SwiperSlide key={item.title}>
<article dir="rtl">
<Image src={item.image} alt={item.title} fill sizes="100vw" />
<div className="atelier-shade" />
<div className="atelier-gallery__caption"><small>{item.subtitle}</small><h3>{item.title}</h3></div>
</article>
</SwiperSlide>
))}
</Swiper>
<div className="atelier-gallery__progress" aria-hidden="true">
{collection.map((item, index) => <button key={item.title} type="button" className={activeSlide === index ? "is-active" : ""} onClick={() => galleryRef.current?.slideToLoop(index)} />)}
</div>
<div className="atelier-gallery__buttons">
<button type="button" onClick={() => galleryRef.current?.slidePrev()} aria-label="اثر قبلی">→</button>
<button type="button" onClick={() => galleryRef.current?.slideNext()} aria-label="اثر بعدی">←</button>
</div>
</div>
</section>
<section className="atelier-manifesto atelier-reveal">
<p>شوقِ جان‌بخشیدن به رویاها و چشم‌اندازهای کسانی که آینده را می‌سازند، تعهد استوار آتلیه رئوفی را به تصور امر خارق‌العاده و تبدیل آن به واقعیت شکل می‌دهد.</p>
<p>آثار منحصربه‌فرد و معیارساز آتلیه رئوفی، خانه‌ها و پناهگاه‌هایی برای اندیشمندان، رهبران و سازندگان فرداست؛ فضاهایی برای کسانی که جسارت متفاوت‌بودن دارند.</p>
</section>
</main>
);
}
+10
View File
@@ -0,0 +1,10 @@
.atelier-page{overflow-x:clip;background:#f4f3f0;color:#10183b}.atelier-page h1,.atelier-page h2,.atelier-page h3,.atelier-page p{margin:0}.atelier-shade{position:absolute;inset:0;background:linear-gradient(0deg,rgba(5,8,15,.5),rgba(5,8,15,.04) 62%)}
.atelier-hero{position:relative;height:100svh;min-height:650px;overflow:hidden;background:#111}.atelier-hero video{width:100%;height:100%;object-fit:cover}.atelier-hero__brand{position:absolute;z-index:2;top:49%;right:0;left:0;display:flex;align-items:center;flex-direction:column;padding:30px 20px;background:rgba(7,10,15,.58);backdrop-filter:blur(16px);color:#fff;text-align:center}.atelier-hero__brand strong{font-size:clamp(34px,4vw,58px);font-weight:700;line-height:1.35}.atelier-hero__brand span{margin-top:12px;font-size:13px;letter-spacing:.08em}.atelier-hero__link{position:absolute;z-index:2;top:112px;left:50%;transform:translateX(-50%);border-bottom:1px solid rgba(255,255,255,.75);padding-bottom:6px;color:#fff;font-size:12px}.atelier-scroll{position:absolute;z-index:2;bottom:22px;left:50%;color:#fff;font-size:32px;animation:atelier-bob 1.8s ease-in-out infinite}
.atelier-intro{width:min(1040px,86%);margin:auto;padding:150px 0;text-align:center}.atelier-intro>span,.atelier-section-title>span{display:block;color:#9b8668;font-size:12px;letter-spacing:.08em}.atelier-intro h1,.atelier-intro h2,.atelier-section-title h2{margin:28px auto 45px;font-size:clamp(28px,3.2vw,46px);font-weight:700;line-height:1.45}.atelier-intro>div{display:grid;gap:34px;font-size:clamp(15px,1.25vw,19px);line-height:2}.atelier-intro--second{padding-top:165px}
.atelier-wide-image{position:relative;height:min(68vw,850px);overflow:hidden;background:#ddd}.atelier-wide-image img,.atelier-editorial img{object-fit:cover}
.atelier-editorial{padding:0 4vw 150px}.atelier-editorial__pair{display:grid;grid-template-columns:1fr 1fr;gap:3vw}.atelier-editorial__image{position:relative;height:46vw;max-height:720px;overflow:hidden}.atelier-editorial__copy{width:min(760px,86%);margin:90px auto;font-size:clamp(17px,1.7vw,24px);font-weight:500;line-height:2;text-align:center}.atelier-editorial__feature{display:grid;grid-template-columns:1.35fr .65fr;align-items:stretch;gap:0;background:#fff}.atelier-editorial__feature .atelier-editorial__image{height:48vw;max-height:760px}.atelier-editorial__feature .atelier-editorial__copy{display:flex;align-items:center;width:auto;margin:0;padding:8vw 5vw;font-size:clamp(16px,1.45vw,21px);text-align:right}
.atelier-collection{padding:145px 0 0;background:#fff}.atelier-section-title{width:min(980px,88%);margin:0 auto 90px;text-align:center}.atelier-gallery{position:relative;height:min(72vw,850px);overflow:hidden;background:#111}.atelier-gallery .swiper,.atelier-gallery .swiper-wrapper,.atelier-gallery .swiper-slide,.atelier-gallery article{height:100%}.atelier-gallery article{position:relative}.atelier-gallery article>img{object-fit:cover}.atelier-gallery__caption{position:absolute;z-index:2;right:7%;bottom:13%;color:#fff}.atelier-gallery__caption small{font-size:13px}.atelier-gallery__caption h3{max-width:700px;margin-top:12px;color:#fff;font-size:clamp(28px,3.4vw,46px);font-weight:700}.atelier-gallery__progress{position:absolute;z-index:4;right:7%;bottom:6%;left:22%;display:flex;direction:ltr}.atelier-gallery__progress button{height:2px;flex:1;border:0;background:rgba(255,255,255,.35);padding:0}.atelier-gallery__progress button.is-active{background:#fff}.atelier-gallery__buttons{position:absolute;z-index:5;bottom:3%;left:5%;display:flex;gap:12px}.atelier-gallery__buttons button{display:grid;width:50px;height:50px;place-items:center;border:1px solid rgba(255,255,255,.75);border-radius:50%;background:transparent;color:#fff;font-size:21px;cursor:pointer}.atelier-gallery__buttons button:hover{background:#fff;color:#10183b}
.atelier-manifesto{display:grid;gap:30px;width:min(820px,calc(100% - 40px));margin:auto;padding:105px 0 115px;color:#10183b;font-size:clamp(15px,1.15vw,18px);font-weight:500;line-height:1.9;text-align:center}
.atelier-reveal{opacity:0;transform:translateY(34px);transition:opacity .9s cubic-bezier(.22,1,.36,1),transform .9s cubic-bezier(.22,1,.36,1)}.atelier-reveal.is-visible{opacity:1;transform:none}@keyframes atelier-bob{50%{transform:translateY(8px)}}
@media(max-width:700px){.atelier-hero{min-height:620px}.atelier-hero__brand strong{font-size:34px}.atelier-hero__brand span{max-width:250px;font-size:11px}.atelier-hero__link{top:88px}.atelier-intro{width:calc(100% - 40px);padding:100px 0}.atelier-intro h1,.atelier-intro h2,.atelier-section-title h2{margin:24px auto 34px;font-size:clamp(28px,8vw,38px)}.atelier-intro>div{gap:26px;font-size:14px}.atelier-wide-image{height:120vw}.atelier-editorial{padding:0 18px 100px}.atelier-editorial__pair{grid-template-columns:1fr;gap:18px}.atelier-editorial__image{height:110vw}.atelier-editorial__pair>div:nth-child(2){height:80vw}.atelier-editorial__copy{margin:65px auto;font-size:17px}.atelier-editorial__feature{grid-template-columns:1fr}.atelier-editorial__feature .atelier-editorial__image{height:110vw}.atelier-editorial__feature .atelier-editorial__copy{padding:55px 25px}.atelier-collection{padding-top:100px}.atelier-section-title{margin-bottom:60px}.atelier-gallery{height:76svh;min-height:570px}.atelier-gallery__caption{right:24px;left:24px;bottom:18%}.atelier-gallery__caption h3{font-size:30px}.atelier-gallery__progress{right:22px;bottom:8%;left:140px}.atelier-gallery__buttons{bottom:5%;left:20px}.atelier-manifesto{gap:24px;padding:75px 0 85px;font-size:14px;line-height:2}}
@media(prefers-reduced-motion:reduce){.atelier-reveal{opacity:1;transform:none}.atelier-scroll{animation:none}}
+76 -28
View File
@@ -2,27 +2,39 @@
import { useState } from "react";
import { contact, contactPage } from "@/data/site";
import { contactPage } from "@/data/site";
type FormState = { name: string; phone: string; email: string; message: string };
type Errors = Partial<Record<keyof FormState, string>>;
const EMPTY: FormState = { name: "", phone: "", email: "", message: "" };
const IRAN_PHONE = /^0\d{10}$/;
const IRAN_PHONE = /^(?:09\d{9}|\+989\d{9})$/;
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function toEnglishDigits(value: string) {
const persian = "۰۱۲۳۴۵۶۷۸۹";
return value.replace(/[۰-۹]/g, (d) => String(persian.indexOf(d)));
const arabic = "٠١٢٣٤٥٦٧٨٩";
return value
.replace(/[۰-۹]/g, (digit) => String(persian.indexOf(digit)))
.replace(/[٠-٩]/g, (digit) => String(arabic.indexOf(digit)));
}
function normalizePhone(value: string) {
return toEnglishDigits(value).replace(/[\s()-]/g, "");
}
function toE164(value: string) {
const normalized = normalizePhone(value);
return normalized.startsWith("0") ? `+98${normalized.slice(1)}` : normalized;
}
function validate(form: FormState): Errors {
const errors: Errors = {};
if (!form.name.trim()) errors.name = "نام را وارد کنید.";
const phone = toEnglishDigits(form.phone.trim());
const phone = normalizePhone(form.phone.trim());
if (!phone) errors.phone = "شماره تماس را وارد کنید.";
else if (!IRAN_PHONE.test(phone)) errors.phone = "شماره تماس معتبر نیست (مثال: ۰۹۱۲۱۲۳۴۵۶۷).";
else if (!IRAN_PHONE.test(phone)) errors.phone = "شماره موبایل معتبر نیست (مثال: ۰۹۱۲۱۲۳۴۵۶۷).";
if (form.email.trim() && !EMAIL_PATTERN.test(form.email.trim())) {
errors.email = "ایمیل معتبر نیست.";
@@ -34,33 +46,56 @@ function validate(form: FormState): Errors {
}
const fieldClass =
"w-full rounded-md border border-line bg-paper px-4 py-3 text-ink placeholder:text-muted/60 transition-colors duration-300 focus:border-gold focus:outline-none";
"w-full border-0 border-b border-line bg-transparent px-0 py-3 text-ink placeholder:text-muted/60 transition-colors duration-300 focus:border-gold focus:outline-none";
export function ContactForm() {
const [form, setForm] = useState<FormState>(EMPTY);
const [errors, setErrors] = useState<Errors>({});
const [sent, setSent] = useState(false);
const [status, setStatus] = useState<{ kind: "success" | "error"; message: string } | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const update = (field: keyof FormState) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
setForm((f) => ({ ...f, [field]: e.target.value }));
const update = (field: keyof FormState) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setForm((current) => ({ ...current, [field]: e.target.value }));
setErrors((current) => ({ ...current, [field]: undefined }));
setStatus(null);
};
const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const nextErrors = validate(form);
setErrors(nextErrors);
if (Object.keys(nextErrors).length > 0) return;
// TODO: replace with POST /tenants/{domain}/contact-submissions once the
// API is ready — for now this opens a pre-filled email as a real,
// working fallback rather than faking a "sent" confirmation.
const subject = encodeURIComponent(`پیام از سایت — ${form.name}`);
const body = encodeURIComponent(
`نام: ${form.name}\nتلفن: ${toEnglishDigits(form.phone)}${
form.email ? `\nایمیل: ${form.email}` : ""
}\n\nپیام:\n${form.message}`,
);
window.location.href = `mailto:${contact.email}?subject=${subject}&body=${body}`;
setSent(true);
setIsSubmitting(true);
setStatus(null);
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: form.name.trim(),
phone: toE164(form.phone),
email: form.email.trim(),
message: form.message.trim(),
}),
});
const result = (await response.json().catch(() => null)) as { message?: string } | null;
if (!response.ok) {
throw new Error(result?.message || "ارسال پیام انجام نشد. لطفاً دوباره تلاش کنید.");
}
setForm(EMPTY);
setStatus({ kind: "success", message: result?.message || "پیام شما با موفقیت ارسال شد." });
} catch (error) {
setStatus({
kind: "error",
message: error instanceof Error ? error.message : "ارسال پیام انجام نشد. لطفاً دوباره تلاش کنید.",
});
} finally {
setIsSubmitting(false);
}
};
return (
@@ -72,6 +107,9 @@ export function ContactForm() {
<input
id="contact-name"
type="text"
autoComplete="name"
maxLength={120}
required
value={form.name}
onChange={update("name")}
placeholder={contactPage.form.namePlaceholder}
@@ -90,6 +128,9 @@ export function ContactForm() {
id="contact-phone"
type="tel"
inputMode="numeric"
autoComplete="tel"
maxLength={16}
required
value={form.phone}
onChange={update("phone")}
placeholder={contactPage.form.phonePlaceholder}
@@ -106,6 +147,8 @@ export function ContactForm() {
<input
id="contact-email"
type="email"
autoComplete="email"
maxLength={254}
value={form.email}
onChange={update("email")}
placeholder={contactPage.form.emailPlaceholder}
@@ -123,6 +166,8 @@ export function ContactForm() {
<textarea
id="contact-message"
rows={5}
maxLength={4000}
required
value={form.message}
onChange={update("message")}
placeholder={contactPage.form.messagePlaceholder}
@@ -134,16 +179,19 @@ export function ContactForm() {
<button
type="submit"
className="mt-2 rounded-full bg-gold px-9 py-3.5 text-white transition-colors duration-300 hover:bg-ink"
disabled={isSubmitting}
className="mt-4 border border-ink bg-ink px-9 py-3.5 font-bold text-white transition-colors duration-300 hover:bg-transparent hover:text-ink disabled:cursor-wait disabled:opacity-60"
>
{contactPage.form.submit}
{isSubmitting ? contactPage.form.submitting : contactPage.form.submit}
</button>
<p className="text-xs leading-relaxed text-muted">{contactPage.form.fallbackNotice}</p>
{sent && (
<p role="status" className="text-sm text-gold">
نرم‌افزار ایمیل شما باز شد — لطفاً پیام را از آنجا ارسال کنید.
{status && (
<p
role="status"
aria-live="polite"
className={`text-sm ${status.kind === "success" ? "text-emerald-700" : "text-red-600"}`}
>
{status.message}
</p>
)}
</form>
@@ -9,9 +9,9 @@ import "swiper/css";
import logo from "@/assets/images/brand/logo.png";
import { contact } from "@/data/site";
import "./omniyat-home.css";
import "./raoofi-home.css";
const asset = (name: string) => `/omniyat-reference/${name}`;
const asset = (name: string) => `/raoofi-media/${name}`;
const stories: { title: string; image: string; href: string }[] = [
{ title: "گشایش فصل تازه‌ای از زندگی در الیزیوم", image: asset("news-1.jpg"), href: "/news-and-blog" },
@@ -38,6 +38,7 @@ const menu = [
{ label: "صفحه اصلی", href: "/" },
{ label: "پروژه‌ها", href: "/portfolios" },
{ label: "خدمات ما", href: "/our-services" },
{ label: "آتلیه رئوفی", href: "/atelier" },
{ label: "داستان ما", href: "/aboutus" },
{ label: "اخبار", href: "/news-and-blog" },
{ label: "تماس با ما", href: "/contactus" },
@@ -47,7 +48,7 @@ function ArrowLink({ href, children }: { href: string; children: React.ReactNode
return <Link className="om-link" href={href}><span>{children}</span><span aria-hidden="true">←</span></Link>;
}
export function OmniyatHome() {
export function RaoofiHome() {
const [menuOpen, setMenuOpen] = useState(false);
const [story, setStory] = useState(0);
const [scrolled, setScrolled] = useState(false);
+6 -6
View File
@@ -47,13 +47,13 @@ export function NewsArticleDetail({
/>
{navItems.length > 1 && (
<nav aria-label="بخش‌های صفحه" className="border-b border-line bg-paper">
<ul className="no-scrollbar mx-auto flex max-w-[1400px] items-center gap-8 overflow-x-auto px-6 lg:px-12">
<nav aria-label="بخش‌های صفحه" className="border-b border-line bg-[#f1efec]">
<ul className="no-scrollbar mx-auto flex max-w-[1400px] items-center justify-center gap-10 overflow-x-auto px-6 lg:px-12">
{navItems.map((navItem) => (
<li key={navItem.id}>
<a
href={`#${navItem.id}`}
className="block scroll-mt-24 py-5 text-sm whitespace-nowrap text-muted transition-colors duration-300 hover:text-gold"
className="block scroll-mt-24 border-b-2 border-transparent py-5 text-sm font-bold whitespace-nowrap text-ink transition-colors duration-300 hover:border-gold hover:text-gold"
>
{navItem.label}
</a>
@@ -63,7 +63,7 @@ export function NewsArticleDetail({
</nav>
)}
<section id="overview" className="scroll-mt-20 bg-paper py-16 lg:py-24">
<section id="overview" className="scroll-mt-20 bg-paper py-24 lg:py-36">
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
<Reveal>
<SectionLabel fa={sectionLabel} en={sectionLabelEn} className="mb-14" />
@@ -71,14 +71,14 @@ export function NewsArticleDetail({
{item.bodyHtml && (
<Reveal delay={90}>
<div className="cms-body" dangerouslySetInnerHTML={{ __html: item.bodyHtml }} />
<div className="cms-body cms-body--editorial" dangerouslySetInnerHTML={{ __html: item.bodyHtml }} />
</Reveal>
)}
</div>
</section>
{hasGallery && (
<section id="gallery" className="scroll-mt-20 bg-sand py-16 lg:py-24">
<section id="gallery" className="scroll-mt-20 bg-[#f1efec] py-24 lg:py-32">
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
<Reveal>
<SectionLabel fa="گالری تصاویر" en="GALLERY" className="mb-14" />
+7 -7
View File
@@ -59,7 +59,7 @@ export function NewsCarousel({
onClick={() => step("prev")}
disabled={atStart}
aria-label="اخبار قبلی"
className="flex h-11 w-11 items-center justify-center rounded-full border border-line text-ink transition-colors duration-300 hover:border-gold hover:bg-gold hover:text-white disabled:pointer-events-none disabled:opacity-30"
className="flex h-11 w-11 items-center justify-center border-0 text-ink transition-colors duration-300 hover:text-gold disabled:pointer-events-none disabled:opacity-30"
>
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<path d="M5 12h14m0 0-6-6m6 6-6 6" stroke="currentColor" strokeWidth="1.6" />
@@ -70,7 +70,7 @@ export function NewsCarousel({
onClick={() => step("next")}
disabled={atEnd}
aria-label="اخبار بعدی"
className="flex h-11 w-11 items-center justify-center rounded-full border border-line text-ink transition-colors duration-300 hover:border-gold hover:bg-gold hover:text-white disabled:pointer-events-none disabled:opacity-30"
className="flex h-11 w-11 items-center justify-center border-0 text-ink transition-colors duration-300 hover:text-gold disabled:pointer-events-none disabled:opacity-30"
>
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<path d="M19 12H5m0 0 6-6m-6 6 6 6" stroke="currentColor" strokeWidth="1.6" />
@@ -86,18 +86,18 @@ export function NewsCarousel({
{items.map((item, i) => (
<li
key={item.slug}
className="w-[82vw] shrink-0 snap-start sm:w-[52vw] lg:w-[34vw] xl:w-[26rem]"
className="w-[86vw] shrink-0 snap-start sm:w-[62vw] lg:w-[48vw] xl:w-[42rem]"
>
<Reveal delay={(i % 4) * 80}>
<Link href={`${basePath}/${item.slug}`} className="group block h-full">
<article className="flex h-full flex-col bg-paper">
<div className="relative aspect-[16/11] overflow-hidden">
<article className="grid h-full bg-paper lg:grid-cols-2">
<div className="relative aspect-square overflow-hidden">
<Image
src={item.image}
alt={item.title}
fill
quality={80}
sizes="(max-width: 640px) 82vw, (max-width: 1024px) 52vw, 26rem"
sizes="(max-width: 640px) 86vw, (max-width: 1024px) 62vw, 22rem"
loading={i < 2 ? "eager" : "lazy"}
className="object-cover transition-transform duration-[1100ms] ease-out-soft group-hover:scale-[1.06]"
/>
@@ -117,7 +117,7 @@ export function NewsCarousel({
{item.title}
</h3>
<span className="mt-auto flex items-center gap-2 pt-7 text-sm text-muted transition-colors duration-300 group-hover:text-gold">
<span className="mt-auto flex w-max items-center gap-2 border-b border-ink/45 pt-7 pb-1 text-sm text-muted transition-colors duration-300 group-hover:border-gold group-hover:text-gold">
ادامه مطلب
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-3.5 w-3.5 transition-transform duration-300 group-hover:-translate-x-1">
<path d="M19 12H5m0 0 6-6m-6 6 6 6" stroke="currentColor" strokeWidth="1.8" />
+7 -7
View File
@@ -32,25 +32,25 @@ export function NewsListing({
{pageItems.length === 0 ? (
<p className="py-16 text-center text-muted">{emptyMessage}</p>
) : (
<ul className="grid gap-8 md:grid-cols-3 lg:gap-10">
<ul className="grid gap-7 md:grid-cols-2 lg:gap-10">
{pageItems.map((item, i) => (
<Reveal as="li" key={item.slug} delay={(i % 6) * 70}>
<Link href={`${basePath}/${item.slug}`} className="group block h-full">
<article className="flex h-full flex-col bg-paper">
<div className="relative aspect-[16/11] overflow-hidden">
<article className="grid h-full bg-paper lg:grid-cols-[1.1fr_.9fr]">
<div className="relative aspect-[4/3] overflow-hidden lg:aspect-auto lg:min-h-[360px]">
<Image
src={item.image}
alt={item.title}
fill
quality={80}
sizes="(max-width: 768px) 100vw, 33vw"
sizes="(max-width: 768px) 100vw, 28vw"
loading={i < 3 ? "eager" : "lazy"}
className="object-cover transition-transform duration-[1100ms] ease-out-soft group-hover:scale-[1.07]"
/>
<div className="absolute inset-0 bg-ink/0 transition-colors duration-500 group-hover:bg-ink/20" />
</div>
<div className="flex flex-1 flex-col p-7 lg:p-8">
<div className="flex flex-1 flex-col justify-between p-7 lg:p-8">
<time className="flex items-center gap-2.5 text-[0.8rem] text-muted">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-3.5 w-3.5 text-gold">
<rect x="3.5" y="5" width="17" height="15.5" rx="2" stroke="currentColor" strokeWidth="1.4" />
@@ -59,11 +59,11 @@ export function NewsListing({
{item.date}
</time>
<h3 className="mt-4 text-[1.22rem] leading-[1.75] font-bold text-ink transition-colors duration-300 group-hover:text-gold">
<h3 className="mt-4 text-[clamp(1.15rem,1.5vw,1.55rem)] leading-[1.7] font-bold text-ink transition-colors duration-300 group-hover:text-gold">
{item.title}
</h3>
<span className="mt-auto flex items-center gap-2 pt-7 text-sm text-muted transition-colors duration-300 group-hover:text-gold">
<span className="mt-8 flex w-max items-center gap-2 border-b border-ink/50 pb-1 text-sm text-muted transition-colors duration-300 group-hover:border-gold group-hover:text-gold">
ادامه مطلب
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-3.5 w-3.5 transition-transform duration-300 group-hover:-translate-x-1">
<path d="M19 12H5m0 0 6-6m-6 6 6 6" stroke="currentColor" strokeWidth="1.8" />
+8 -14
View File
@@ -93,36 +93,30 @@ export function PortfolioBrowser({
پروژه‌ای در این دسته یافت نشد.
</p>
) : (
<ul className="mx-auto grid max-w-[1400px] gap-6 px-6 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:px-12">
<ul className="mx-auto grid max-w-[1400px] gap-7 px-5 md:grid-cols-2 lg:gap-10 lg:px-10">
{pageItems.map((project, i) => (
<Reveal as="li" key={project.slug} delay={(i % 6) * 70}>
<Link href={`/portfolios/${project.slug}`} className="group block">
<div className="relative aspect-[3/4] overflow-hidden bg-ink">
<Link href={`/portfolios/${project.slug}`} className="group block bg-white">
<div className="relative aspect-[4/3] overflow-hidden bg-ink">
<Image
src={project.image}
alt={`${project.type} ${project.name}`}
fill
quality={80}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
sizes="(max-width: 768px) 100vw, 50vw"
loading={i < 3 ? "eager" : "lazy"}
className="object-cover transition-transform duration-[1100ms] ease-out-soft group-hover:scale-[1.06]"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/85 via-black/15 to-transparent" />
<div
aria-hidden="true"
className="pointer-events-none absolute inset-4 border border-gold opacity-0 transition-opacity duration-500 group-hover:opacity-100"
/>
<div className="absolute inset-x-0 bottom-0 p-6">
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent" />
<div className="absolute inset-x-0 bottom-0 p-6 lg:p-8">
<span className="latin block text-[0.66rem] tracking-[0.28em] text-gold-soft">
{project.nameLatin}
</span>
<h3 className="mt-2.5 text-xl font-bold text-white">{project.name}</h3>
<h3 className="mt-2.5 text-[clamp(1.35rem,2vw,2rem)] font-bold text-white">{project.name}</h3>
<span className="mt-1.5 block text-sm text-white/65">{project.type}</span>
<span className="mt-4 flex h-0 items-center gap-2 overflow-hidden text-sm text-white opacity-0 transition-all duration-500 ease-out-soft group-hover:h-6 group-hover:opacity-100">
<span className="mt-5 flex w-max items-center gap-3 border-b border-white/80 pb-1 text-sm text-white transition-colors duration-300 group-hover:text-gold-soft">
مشاهده پروژه
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-3.5 w-3.5">
<path d="M19 12H5m0 0 6-6m-6 6 6 6" stroke="currentColor" strokeWidth="1.8" />
@@ -0,0 +1,79 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import type { Swiper as SwiperInstance } from "swiper";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import { serviceDetails, services } from "@/data/site";
import { EditorialCardCarousel } from "@/components/shared/EditorialCardCarousel";
import type { DisplayProject } from "@/lib/api";
import "./raoofi-services.css";
const base = "/raoofi-media/services";
const serviceImages = Array.from({ length: 6 }, (_, index) => `${base}/service-${(index % 5) + 1}.jpg`);
const fallbackProjects = [
["فضایی برای زندگی معاصر", "portfolio-1.webp"], ["جزئیاتی فراتر از انتظار", "portfolio-2.webp"],
["آرامش در قلب معماری", "portfolio-3.webp"], ["تعادل فرم و عملکرد", "portfolio-4.jpg"],
["روایتی تازه از کیفیت", "portfolio-5.jpg"], ["خانه‌ای با هویت مستقل", "portfolio-6.webp"],
["ساخته‌شده برای ماندن", "portfolio-7.webp"],
].map(([name, file], index) => ({ slug: "", name, type: `پروژه ${index + 1}`, image: `${base}/${file}` }));
export function RaoofiServices({ projects }: { projects: DisplayProject[] }) {
const projectSwiper = useRef<SwiperInstance | null>(null);
const [projectIndex, setProjectIndex] = useState(0);
const portfolio = projects.length
? projects.slice(0, 7).map((project) => ({ slug: project.slug, name: project.name, type: project.type, image: project.image }))
: fallbackProjects;
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => entries.forEach((entry) => entry.isIntersecting && entry.target.classList.add("is-visible")),
{ threshold: 0.12 },
);
document.querySelectorAll(".osa-reveal").forEach((element) => observer.observe(element));
return () => observer.disconnect();
}, []);
return (
<main className="osa-page" dir="rtl">
<section className="osa-hero">
<video autoPlay muted loop playsInline preload="metadata"><source src={`${base}/hero-desktop.mp4`} type="video/mp4" /></video>
<div className="osa-overlay" />
<div className="osa-hero-mark"><strong>RAOOFI</strong><span>Atelier</span></div>
<span className="osa-scroll">⌄</span>
</section>
<section className="osa-overview">
<div className="osa-heading osa-reveal"><span>نگاه کلی</span><h1>چشم‌انداز شما را<br />به واقعیت تبدیل می‌کنیم</h1><div><p>هنر و تجربهٔ گروه رئوفی را به طراحی، تأمین، مدیریت و اجرای کامل پروژه پیوند می‌دهیم تا هر فضا بازتابی دقیق از سبک زندگی و خواسته‌های شما باشد.</p><p>با همراهی معماران، هنرمندان و متخصصان برجسته، فضاهایی منحصربه‌فرد خلق می‌کنیم که زیبایی، عملکرد و کیفیت ماندگار را در کنار هم قرار می‌دهند.</p></div></div>
<div className="osa-collage osa-reveal"><div className="osa-collage-main"><Image src={`${base}/overview-1.webp`} alt="طراحی داخلی اختصاصی" fill sizes="65vw" /></div><div className="osa-collage-side"><Image src={`${base}/overview-2.jpg`} alt="فضای داخلی لوکس" fill sizes="35vw" /></div><div className="osa-sketch" aria-hidden="true">خطوط، نور، بافت<br />و جزئیات دقیق</div></div>
</section>
<section className="osa-services">
<div className="osa-heading osa-reveal"><span>خدمات</span><h2>از ایده تا تکمیل</h2><div><p>هر پروژه با دیدگاهی منحصربه‌فرد آغاز می‌شود. از نخستین جرقهٔ ایده تا اجرای نهایی، در تمام مسیر کنار شما هستیم.</p><p>می‌توانید مسیر کامل طراحی و اجرا یا هر یک از خدمات تخصصی را متناسب با نیاز پروژه انتخاب کنید.</p></div></div>
<div className="osa-reveal">
<EditorialCardCarousel
items={services.map((service, index) => ({ image: serviceImages[index], title: service.name, text: serviceDetails[service.slug].blurb }))}
previousLabel="خدمت قبلی"
nextLabel="خدمت بعدی"
/>
</div>
</section>
<section className="osa-portfolio">
<div className="osa-heading osa-reveal"><span>نمونه پروژه‌ها</span><h2>از طرح تا واقعیت</h2><div><p>پروژه‌های ما گسترهٔ خدمات، دقت اجرا و کیفیتی را نشان می‌دهند که از یک مسیر یکپارچهٔ طراحی و مدیریت شکل گرفته است.</p></div></div>
<div className="osa-project-nav osa-reveal">{portfolio.map((project, index) => <button key={`${project.name}-${index}`} className={projectIndex === index ? "is-active" : ""} onClick={() => projectSwiper.current?.slideToLoop(index)}>{project.name}</button>)}</div>
<div className="osa-project-stage osa-reveal">
<Swiper dir="ltr" loop={portfolio.length > 1} speed={850} slidesPerView={1} grabCursor onSwiper={(swiper) => { projectSwiper.current = swiper; setProjectIndex(swiper.realIndex); }} onSlideChange={(swiper) => setProjectIndex(swiper.realIndex)}>
{portfolio.map((project, index) => <SwiperSlide key={`${project.name}-image-${index}`}><article dir="rtl"><Image src={project.image} alt={project.name} fill sizes="100vw" unoptimized={project.image.startsWith("http")} /><div className="osa-overlay" /><div className="osa-project-caption"><small>{project.type}</small><h3>{project.name}</h3>{project.slug && <Link href={`/portfolios/${project.slug}`}>مشاهده پروژه</Link>}</div></article></SwiperSlide>)}
</Swiper>
<div className="osa-arrows osa-project-arrows"><button type="button" onClick={() => projectSwiper.current?.slidePrev()} aria-label="پروژه قبلی">←</button><button type="button" onClick={() => projectSwiper.current?.slideNext()} aria-label="پروژه بعدی">→</button></div>
</div>
</section>
<section className="osa-cta"><video autoPlay muted loop playsInline preload="metadata"><source src="/raoofi-media/about/cta-mobile.mp4" media="(max-width:700px)" type="video/mp4" /><source src="/raoofi-media/about/cta-desktop.mp4" type="video/mp4" /></video><div className="osa-overlay" /><div className="osa-cta-copy osa-reveal"><h2>فضایی منحصربه‌فرد کشف کنید</h2><p>برای آغاز پروژه و دریافت مشاوره با ما گفت‌وگو کنید.</p><Link href="/contactus">درخواست مشاوره</Link></div></section>
</main>
);
}
@@ -0,0 +1,9 @@
.osa-page{overflow-x:clip;background:#f5f4f1;color:#10183b}.osa-page h1,.osa-page h2,.osa-page h3,.osa-page p{margin:0}.osa-overlay{position:absolute;inset:0;background:linear-gradient(0deg,rgba(7,10,16,.48),rgba(7,10,16,.06) 62%)}
.osa-hero{position:relative;height:100vh;height:100svh;min-height:650px;overflow:hidden;background:#111}.osa-hero video{width:100%;height:100%;object-fit:cover}.osa-hero-mark{position:absolute;z-index:2;top:48%;left:50%;display:flex;width:100%;transform:translate(-50%,-50%);flex-direction:column;align-items:center;padding:30px 0;background:rgba(7,10,15,.55);backdrop-filter:blur(16px);color:#fff}.osa-hero-mark strong{font-family:Arial,sans-serif;font-size:clamp(34px,4vw,58px);font-weight:700;letter-spacing:.22em}.osa-hero-mark span{margin-top:-4px;font-family:Georgia,serif;font-size:clamp(22px,2.2vw,34px);font-weight:700;font-style:italic}.osa-scroll{position:absolute;z-index:2;bottom:28px;left:50%;color:#fff;font-size:32px;animation:osa-bob 1.8s ease-in-out infinite}
.osa-overview,.osa-services,.osa-portfolio{padding-top:150px}.osa-heading{width:min(1120px,86%);margin:0 auto 130px;text-align:center}.osa-heading>span{display:block;color:#9b8668;font-size:12px;letter-spacing:.08em}.osa-heading h1,.osa-heading h2{margin:30px auto 42px;font-size:clamp(28px,3.2vw,46px);font-weight:700;line-height:1.4}.osa-heading>div{display:grid;gap:34px;max-width:960px;margin:auto;font-size:clamp(15px,1.35vw,20px);line-height:2}.osa-collage{display:grid;grid-template-columns:1.05fr .95fr;grid-template-rows:450px 260px;gap:14px;padding:0 2vw 150px}.osa-collage-main,.osa-collage-side{position:relative;overflow:hidden}.osa-collage-main{grid-row:1/3}.osa-collage-side{grid-column:2}.osa-collage img{object-fit:cover;transition:transform 1s}.osa-collage:hover img{transform:scale(1.025)}.osa-sketch{display:flex;align-items:center;justify-content:center;background:#fff;color:#9b8668;font-size:clamp(20px,2vw,30px);font-weight:700;line-height:1.7;text-align:center}
.osa-services{padding-bottom:145px;background:#f0efec}.osa-arrows{position:absolute;z-index:5;right:5vw;bottom:18px;display:flex;direction:ltr;gap:18px}.osa-arrows button{border:0;background:transparent;color:#10183b;font-size:35px;cursor:pointer;transition:color .25s}.osa-arrows button:hover{color:#9b8668}
.osa-portfolio{padding-bottom:150px;background:#fff}.osa-project-nav{display:flex;gap:42px;width:92%;margin:0 auto 55px;padding-bottom:16px;overflow-x:auto;scrollbar-width:none;direction:rtl}.osa-project-nav::-webkit-scrollbar{display:none}.osa-project-nav button{flex:none;border:0;border-bottom:1px solid transparent;background:transparent;padding:0 0 12px;color:#8b8d94;font-size:17px;cursor:pointer}.osa-project-nav button.is-active{border-color:#10183b;color:#10183b}.osa-project-stage{position:relative;width:94%;height:min(68vw,780px);margin:auto;overflow:hidden;background:#111}.osa-project-stage,.osa-project-stage .swiper,.osa-project-stage .swiper-wrapper,.osa-project-stage .swiper-slide,.osa-project-stage article{height:min(68vw,780px)}.osa-project-stage article{position:relative}.osa-project-stage img{object-fit:cover}.osa-project-caption{position:absolute;z-index:2;right:6%;bottom:10%;color:#fff}.osa-project-caption h3{max-width:720px;margin:10px 0 20px;color:#fff;font-size:clamp(28px,3.4vw,46px);font-weight:700}.osa-project-caption a{border-bottom:1px solid #fff;padding-bottom:7px;color:#fff}.osa-project-arrows{right:auto;bottom:6%;left:4%;}.osa-project-arrows button{display:grid;width:52px;height:52px;place-items:center;border:1px solid rgba(255,255,255,.75);border-radius:50%;color:#fff;font-size:22px}.osa-project-arrows button:hover{background:#fff;color:#10183b}
.osa-cta{position:relative;height:650px;margin:0 5vw 80px;overflow:hidden;background:#111}.osa-cta video{width:100%;height:100%;object-fit:cover}.osa-cta-copy{position:absolute;z-index:2;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;padding:25px;color:#fff;text-align:center}.osa-cta-copy h2{color:#fff;font-size:clamp(30px,3.2vw,46px);font-weight:700}.osa-cta-copy p{margin:12px 0 35px}.osa-cta-copy a{min-width:220px;border:1px solid #fff;padding:13px 25px;color:#fff}.osa-reveal{opacity:0;transform:translateY(34px);transition:opacity .9s cubic-bezier(.22,1,.36,1),transform .9s cubic-bezier(.22,1,.36,1)}.osa-reveal.is-visible{opacity:1;transform:none}
@keyframes osa-bob{50%{transform:translateY(8px)}}
@media(max-width:700px){.osa-hero{min-height:620px}.osa-hero-mark strong{font-size:34px}.osa-overview,.osa-services,.osa-portfolio{padding-top:95px}.osa-heading{width:calc(100% - 40px);margin-bottom:75px}.osa-heading h1,.osa-heading h2{margin:25px auto 35px;font-size:clamp(28px,8vw,38px)}.osa-heading>div{gap:25px;font-size:14px}.osa-collage{grid-template-columns:1fr;grid-template-rows:115vw 72vw 45vw;padding:0 18px 95px}.osa-collage-main{grid-row:auto}.osa-collage-side{grid-column:auto}.osa-arrows{right:18px;bottom:15px}.osa-project-nav{gap:26px;margin-bottom:35px}.osa-project-nav button{font-size:14px}.osa-project-stage,.osa-project-stage .swiper,.osa-project-stage .swiper-wrapper,.osa-project-stage .swiper-slide,.osa-project-stage article{height:72svh;min-height:570px}.osa-project-stage{width:100%}.osa-project-caption{right:24px;left:24px;bottom:18%}.osa-project-caption h3{font-size:30px}.osa-project-arrows{bottom:5%;left:22px}.osa-cta{height:560px;margin:0}.osa-cta-copy h2{font-size:34px}}
@media(prefers-reduced-motion:reduce){.osa-reveal{opacity:1;transform:none}.osa-scroll{animation:none}}
+4 -4
View File
@@ -29,8 +29,8 @@ export function Editorial({
return (
<div
className={`mx-auto flex max-w-[1400px] flex-col items-center gap-6 px-6 text-center lg:px-12 ${
compact ? "py-12 lg:py-16" : "py-16 lg:py-24"
className={`mx-auto flex max-w-[1400px] flex-col items-center gap-7 px-6 text-center lg:px-12 ${
compact ? "py-24 lg:py-36" : "py-28 lg:py-40"
}`}
>
{caption && captionLatin && (
@@ -42,7 +42,7 @@ export function Editorial({
{title && (
<Reveal delay={90}>
<h2
className={`max-w-[927px] text-[1.9rem] leading-[1.45] font-normal sm:text-4xl lg:text-5xl lg:leading-[1.3] ${heading}`}
className={`max-w-[900px] text-[clamp(1.75rem,3vw,2.75rem)] leading-[1.45] font-bold ${heading}`}
>
{title}
</h2>
@@ -51,7 +51,7 @@ export function Editorial({
{body?.map((paragraph, i) => (
<Reveal key={i} delay={170 + i * 80}>
<p className={`max-w-[738px] text-base leading-[2.1] ${prose}`}>{paragraph}</p>
<p className={`max-w-[820px] text-[15px] leading-[2.15] lg:text-lg ${prose}`}>{paragraph}</p>
</Reveal>
))}
</div>
@@ -0,0 +1,66 @@
"use client";
import Image from "next/image";
import { useRef, useState } from "react";
import type { Swiper as SwiperInstance } from "swiper";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "./editorial-card-carousel.css";
export type EditorialCardItem = {
image: string;
title: string;
text: string;
};
type EditorialCardCarouselProps = {
items: EditorialCardItem[];
previousLabel: string;
nextLabel: string;
};
export function EditorialCardCarousel({ items, previousLabel, nextLabel }: EditorialCardCarouselProps) {
const swiperRef = useRef<SwiperInstance | null>(null);
const [activeIndex, setActiveIndex] = useState(0);
return (
<div className="editorial-carousel">
<Swiper
dir="ltr"
loop={items.length > 1}
speed={700}
slidesPerView="auto"
spaceBetween={34}
grabCursor
onSwiper={(swiper) => {
swiperRef.current = swiper;
setActiveIndex(swiper.realIndex);
}}
onSlideChange={(swiper) => setActiveIndex(swiper.realIndex)}
>
{items.map((item) => (
<SwiperSlide key={item.title}>
<article dir="ltr">
<div className="editorial-carousel__image">
<Image src={item.image} alt={item.title} fill sizes="(max-width: 700px) 88vw, 19vw" />
</div>
<div className="editorial-carousel__copy" dir="rtl">
<h3>{item.title}</h3>
<p>{item.text}</p>
</div>
</article>
</SwiperSlide>
))}
</Swiper>
<div className="editorial-carousel__progress" aria-hidden="true">
<span style={{ width: `${100 / items.length}%`, transform: `translateX(${activeIndex * 100}%)` }} />
</div>
<div className="editorial-carousel__buttons">
<button type="button" onClick={() => swiperRef.current?.slidePrev()} aria-label={previousLabel}>←</button>
<button type="button" onClick={() => swiperRef.current?.slideNext()} aria-label={nextLabel}>→</button>
</div>
</div>
);
}
+4 -4
View File
@@ -16,7 +16,7 @@ export function PageCta({
action: string;
}) {
return (
<section className="relative isolate flex min-h-[560px] items-center overflow-hidden py-28 lg:py-36">
<section className="relative isolate mx-0 flex min-h-[560px] items-center overflow-hidden py-28 lg:mx-[5vw] lg:mb-20 lg:min-h-[650px] lg:py-36">
<Image
src={ctaImage}
alt=""
@@ -34,20 +34,20 @@ export function PageCta({
</Reveal>
<Reveal delay={90}>
<h2 className="mt-7 text-3xl font-light text-white lg:text-5xl">{title}</h2>
<h2 className="mt-7 text-[clamp(1.9rem,3.2vw,2.9rem)] leading-[1.4] font-bold text-white">{title}</h2>
</Reveal>
<Reveal delay={180}>
<div className="mt-11 flex flex-wrap items-center justify-center gap-4">
<Link
href={`tel:${contact.phoneLinks[0]}`}
className="rounded-full bg-gold px-9 py-3.5 text-white transition-colors duration-300 hover:bg-white hover:text-ink"
className="min-w-[190px] border border-white bg-white px-8 py-3.5 text-ink transition-colors duration-300 hover:bg-transparent hover:text-white"
>
{action}
</Link>
<Link
href="/contactus"
className="rounded-full border border-white/35 px-9 py-3.5 text-white transition-colors duration-300 hover:border-white hover:bg-white/10"
className="min-w-[190px] border border-white/70 px-8 py-3.5 text-white transition-colors duration-300 hover:bg-white hover:text-ink"
>
تماس با ما
</Link>
+8 -6
View File
@@ -46,7 +46,7 @@ export function PageHero({
});
return (
<section className="relative flex h-[86svh] min-h-[520px] items-end overflow-hidden bg-ink">
<section className="relative flex h-[100svh] min-h-[620px] items-center overflow-hidden bg-ink">
<picture>
<source media="(min-width: 768px)" srcSet={desktop.srcSet} sizes={desktop.sizes} />
<source media="(max-width: 767px)" srcSet={mobile.srcSet} sizes={mobile.sizes} />
@@ -54,11 +54,11 @@ export function PageHero({
<img {...mobile} alt="" className="animate-slow-zoom object-cover" />
</picture>
<div className="absolute inset-0 bg-gradient-to-b from-black/55 via-black/25 to-black/75" />
<div className="absolute inset-0 bg-gradient-to-b from-black/45 via-black/20 to-black/70" />
<div className="relative z-10 mx-auto w-full max-w-[1400px] px-6 pb-20 text-center lg:px-12 lg:pb-28">
<div className="relative z-10 mx-auto w-full max-w-[1400px] px-6 pt-16 text-center lg:px-12">
<h1
className="animate-fade-up text-4xl font-light text-white sm:text-5xl lg:text-6xl"
className="animate-fade-up text-[clamp(2rem,4vw,3.6rem)] leading-[1.35] font-bold text-white"
style={{ animationDelay: "140ms" }}
>
{title}
@@ -66,10 +66,10 @@ export function PageHero({
<nav
aria-label="مسیر صفحه"
className="mt-8 animate-fade-up"
className="mt-7 animate-fade-up"
style={{ animationDelay: "280ms" }}
>
<ol className="flex items-center justify-center gap-3 text-sm text-white/60">
<ol className="flex flex-wrap items-center justify-center gap-3 text-xs text-white/65 lg:text-sm">
{breadcrumb.map((crumb, i, all) => (
<li key={crumb.href} className="flex items-center gap-3">
{i === all.length - 1 ? (
@@ -89,6 +89,8 @@ export function PageHero({
</ol>
</nav>
</div>
<span aria-hidden="true" className="absolute bottom-7 left-1/2 z-10 -translate-x-1/2 animate-bounce text-3xl text-white">⌄</span>
</section>
);
}
+2 -2
View File
@@ -26,8 +26,8 @@ export function SectionLabel({
<div className={`flex flex-col gap-4 ${alignClass} ${className}`}>
<span aria-hidden="true" className="h-px w-10 bg-gold" />
<div className={`flex flex-col gap-1 ${alignClass}`}>
<p className={`text-xl ${color}`}>{fa}</p>
<p className={`latin text-sm tracking-[0.15em] opacity-70 ${color}`}>{en}</p>
<p className={`text-base font-bold ${color}`}>{fa}</p>
<p className={`latin text-xs tracking-[0.15em] opacity-70 ${color}`}>{en}</p>
</div>
</div>
);
@@ -0,0 +1,28 @@
.editorial-carousel{position:relative;padding-left:2vw;padding-bottom:115px}
.editorial-carousel .swiper-slide{width:38vw;height:auto}
.editorial-carousel article{display:grid;height:19vw;grid-template-columns:1fr 1fr;background:#fff}
.editorial-carousel__image{position:relative;min-height:0;overflow:hidden}
.editorial-carousel__image img{object-fit:cover;transition:transform .8s}
.editorial-carousel article:hover img{transform:scale(1.035)}
.editorial-carousel__copy{display:flex;min-width:0;flex-direction:column;justify-content:space-between;padding:clamp(14px,1.8vw,44px) clamp(14px,1.5vw,36px);background:#fff;text-align:right}
.editorial-carousel__copy h3{margin:0;font-size:clamp(20px,1.5vw,28px);font-weight:700;line-height:1.45}
.editorial-carousel__copy p{margin:0;color:#111a3b;font-size:clamp(11px,.9vw,16px);line-height:1.8}
.editorial-carousel__progress{position:absolute;z-index:4;right:38%;bottom:45px;width:23%;height:2px;background:#d0cfcc;overflow:hidden;direction:ltr}
.editorial-carousel__progress span{display:block;height:100%;background:#111a3b;transition:transform .7s cubic-bezier(.22,1,.36,1)}
.editorial-carousel__buttons{position:absolute;z-index:5;right:5vw;bottom:22px;display:flex;gap:12px;direction:ltr}
.editorial-carousel__buttons button{display:grid;width:48px;height:48px;place-items:center;border:0;background:transparent;color:#111a3b;font-size:34px;cursor:pointer;transition:color .3s}
.editorial-carousel__buttons button:hover{color:#9a8568}
@media(max-width:700px){
.editorial-carousel{padding-left:18px;padding-bottom:100px}
.editorial-carousel .swiper-slide{width:88vw}
.editorial-carousel article{height:auto;min-height:0;grid-template-columns:1fr}
.editorial-carousel__image{aspect-ratio:1}
.editorial-carousel__copy{min-height:240px;padding:28px 24px}
.editorial-carousel__copy h3{font-size:22px}
.editorial-carousel__copy p{font-size:14px}
.editorial-carousel__progress{right:150px;bottom:42px;width:calc(100% - 175px)}
.editorial-carousel__buttons{right:18px;bottom:19px}
}
@media(prefers-reduced-motion:reduce){.editorial-carousel__image img{transition:none}}
+2 -9
View File
@@ -26,6 +26,7 @@ export const site = {
export const nav = [
{ label: "صفحه اصلی", href: "/" },
{ label: "خدمات ما", href: "/our-services" },
{ label: "آتلیه رئوفی", href: "/atelier" },
{ label: "پروژه ها", href: "/portfolios" },
{ label: "مقالات و اخبار", href: "/news-and-blog" },
{ label: "درباره ما", href: "/aboutus" },
@@ -42,11 +43,6 @@ export const contact = {
loginUrl: "https://app.raoofigroup.com/auth/login",
} as const;
/**
* Contact page copy. The form itself doesn't post anywhere yet — see
* src/components/contact/ContactForm.tsx — it falls back to a pre-filled
* mailto: link until the meshkee contact-submissions endpoint is wired up.
*/
export const contactPage = {
hero: {
title: "تماس با ما",
@@ -80,9 +76,7 @@ export const contactPage = {
messageLabel: "پیام",
messagePlaceholder: "پیام خود را بنویسید...",
submit: "ارسال پیام",
submitting: "در حال آماده‌سازی...",
fallbackNotice:
"فرم هنوز به سرویس ارسال متصل نشده — با کلیک روی «ارسال پیام»، ایمیل شما با متن پیام باز می‌شود.",
submitting: "در حال ارسال...",
},
} as const;
@@ -424,4 +418,3 @@ export const blogListingPage = {
},
emptyMessage: "هنوز مقاله‌ای منتشر نشده است.",
} as const;