feat: redesign site pages and connect contact API
@@ -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 |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 586 KiB |
|
After Width: | Height: | Size: 340 KiB |
|
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 |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 496 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 586 KiB |
|
After Width: | Height: | Size: 515 KiB |
|
After Width: | Height: | Size: 344 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 876 KiB |
|
After Width: | Height: | Size: 469 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 143 KiB |
@@ -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 />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OmniyatHome } from "@/components/omniyat/OmniyatHome";
|
||||
import { RaoofiHome } from "@/components/home/RaoofiHome";
|
||||
|
||||
export default function HomePage() {
|
||||
return <OmniyatHome />;
|
||||
return <RaoofiHome />;
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}}
|
||||
@@ -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);
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}}
|
||||
@@ -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;
|
||||
|
||||
|
||||