Compare commits

..
3 Commits
152 changed files with 1888 additions and 1105 deletions
+4 -4
View File
@@ -110,12 +110,12 @@ src/
## صفحه «درباره ما»
ظاهر این صفحه الهام‌گرفته از [omniyat.com/omniyat-atelier](https://www.omniyat.com/omniyat-atelier)
ظاهر این صفحه بر پایه زبان بصری جدید وب‌سایت رئوفی طراحی شده است.
است، اما **تمام محتوا از صفحه درباره ما سایت رئوفی** برداشته شده است.
سیستم چیدمانِ برگرفته از مرجع:
| ویژگی | مرجع (Omniyat) | پیاده‌سازی |
| ویژگی | طراحی هدف | پیاده‌سازی |
| --- | --- | --- |
| ریتم عمودی بلوک متن | `192px 48px` | `192px 48px` (موبایل: `96px 24px`) |
| کپشن بالای تیتر | ۱۴px، `letter-spacing: 1.4px` | همان — فقط روی بخش لاتین |
@@ -137,7 +137,7 @@ src/
## صفحه «خدمات ما»
همان سیستم چیدمان صفحه درباره ما (الهام‌گرفته از Omniyat Atelier).
همان سیستم چیدمان صفحه درباره ما و زبان بصری جدید وب‌سایت.
**نکته‌ی مهم درباره‌ی محتوا:** صفحه‌ی خدمات سایت فعلی فقط شامل شش عنوان خدمت و تیتر
«با خدمات ما بیشتر آشنا شوید...» است — هیچ توضیحی برای خدمات ندارد و لینک همه‌ی
@@ -159,7 +159,7 @@ src/
## نکات
- لینک‌های ناوبری به مسیرهای واقعی سایت (`/portfolios`، `/news`، …) اشاره می‌کنند
- لینک‌های ناوبری به مسیرهای واقعی سایت (`/projects`، `/news`، …) اشاره می‌کنند
ولی تاکنون فقط صفحه اصلی و درباره ما طراحی شده‌اند.
- انیمیشن‌ها به `prefers-reduced-motion` احترام می‌گذارند.
- کروسل پروژه‌ها برای RTL نوشته شده (`scrollLeft` منفی) و با کیبورد و لمس کار می‌کند.
+15 -1
View File
@@ -7,7 +7,7 @@ const nextConfig: NextConfig = {
formats: ["image/avif", "image/webp"],
// Every quality value used across the page. Required from Next.js 16 on.
qualities: [70, 72, 75, 80, 82, 88, 90],
// Portfolio media served live from the meshkee API. The storage host has
// Project media served live from the meshkee API. The storage host has
// already changed once mid-project (parminstorage.ir -> parspack.net) —
// keep both around since either could be what a given item still points to.
remotePatterns: [
@@ -16,6 +16,20 @@ const nextConfig: NextConfig = {
{ protocol: "https", hostname: "c804387.parspack.net" },
],
},
async redirects() {
return [
{
source: "/our-services",
destination: "/atelier",
permanent: true,
},
{
source: "/portfolios/:path*",
destination: "/projects/:path*",
permanent: true,
},
];
},
};
export default nextConfig;
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "node scripts/dev.cjs",
"build": "next build",
"start": "next start"
},
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

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

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

Before

Width:  |  Height:  |  Size: 610 KiB

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

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

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 561 KiB

After

Width:  |  Height:  |  Size: 561 KiB

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Before

Width:  |  Height:  |  Size: 482 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 277 KiB

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

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

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

+48
View File
@@ -0,0 +1,48 @@
const { execFileSync, spawn } = require("node:child_process");
const path = require("node:path");
function supportsNext(nodePath) {
try {
const version = execFileSync(nodePath, ["--version"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
const [major, minor] = version.replace(/^v/, "").split(".").map(Number);
return major >= 20 || (major === 18 && minor >= 18) || (major === 19 && minor >= 8);
} catch {
return false;
}
}
function installedNodes() {
const command = process.platform === "win32" ? "where.exe" : "which";
const args = process.platform === "win32" ? ["node"] : ["-a", "node"];
try {
return execFileSync(command, args, { encoding: "utf8" })
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean);
} catch {
return [];
}
}
const nodePath = [process.execPath, ...installedNodes()].find(supportsNext);
if (!nodePath) {
console.error("Next.js به Node.js 18.18 یا جدیدتر نیاز دارد. لطفاً Node.js LTS را نصب کنید.");
process.exit(1);
}
const nextCli = require.resolve("next/dist/bin/next");
const child = spawn(nodePath, [nextCli, "dev", ...process.argv.slice(2)], {
cwd: path.resolve(__dirname, ".."),
stdio: "inherit",
env: process.env,
});
child.on("exit", (code, signal) => {
if (signal) process.kill(process.pid, signal);
process.exit(code ?? 1);
});
+7 -97
View File
@@ -1,115 +1,25 @@
import type { Metadata } from "next";
import atelier from "@/assets/images/about/atelier.jpg";
import about from "@/assets/images/about/about.jpg";
import heroImage from "@/assets/images/hero/hero-2.jpg";
import heroImageMobile from "@/assets/images/hero/hero-2-mobile.jpg";
import { Pillars } from "@/components/about/Pillars";
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 { Reveal } from "@/components/Reveal";
import { RaoofiAbout } from "@/components/about/RaoofiAbout";
import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader";
import { aboutPage } from "@/data/site";
import { getRecentPortfolios, toDisplayProject } from "@/lib/api";
import { getRecentProjects, 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 AboutPage() {
const portfolios = await getRecentPortfolios(12);
const displayProjects = portfolios.map(toDisplayProject);
const projects = await getRecentProjects(8);
const displayProjects = projects.map(toDisplayProject);
return (
<>
<SiteHeader />
<main>
<PageHero
image={heroImage}
imageMobile={heroImageMobile}
title={aboutPage.hero.title}
breadcrumb={aboutPage.hero.breadcrumb}
/>
{/* Overview */}
<section className="bg-paper">
<Editorial
caption={aboutPage.overview.caption}
captionLatin={aboutPage.overview.captionLatin}
title={aboutPage.overview.title}
body={aboutPage.overview.body}
/>
<FullBleed image={atelier} alt="معماری پروژه‌های گروه ساختمانی رئوفی" />
</section>
{/* Philosophy */}
<section className="bg-sand">
<Editorial
caption={aboutPage.philosophy.caption}
captionLatin={aboutPage.philosophy.captionLatin}
title={aboutPage.philosophy.title}
body={aboutPage.philosophy.body}
/>
</section>
{/* Foundations */}
<section className="bg-paper">
<Editorial
caption={aboutPage.pillars.caption}
captionLatin={aboutPage.pillars.captionLatin}
title={aboutPage.pillars.title}
body={aboutPage.pillars.body}
/>
<Pillars />
</section>
{/* Portfolio */}
<section className="bg-sand">
<Editorial
caption={aboutPage.portfolio.caption}
captionLatin={aboutPage.portfolio.captionLatin}
title={aboutPage.portfolio.title}
body={aboutPage.portfolio.body}
/>
<ProjectGallery projects={displayProjects} />
</section>
{/* Closing statement */}
<section className="bg-paper">
<div className="mx-auto max-w-[1400px] px-6 py-24 text-center lg:px-12 lg:py-48">
<Reveal>
<p className="mx-auto max-w-[860px] text-[1.35rem] leading-[2.1] font-light text-ink lg:text-[1.75rem] lg:leading-[2]">
{aboutPage.closing.body}
</p>
</Reveal>
<Reveal delay={140}>
<span
aria-hidden="true"
className="mx-auto mt-14 block h-px w-24 bg-gold"
/>
</Reveal>
</div>
<FullBleed image={about} alt="نمای پروژه‌های گروه ساختمانی رئوفی" />
</section>
<PageCta
caption={aboutPage.cta.caption}
title={aboutPage.cta.title}
action={aboutPage.cta.action}
/>
</main>
<RaoofiAbout projects={displayProjects} />
<SiteFooter />
</>
);
+72
View File
@@ -0,0 +1,72 @@
import { NextResponse } from "next/server";
const CONTACT_ENDPOINT =
"https://api.meshkee.com/api/v1/tenants/raoofigroup.com/contact-submissions";
const REQUEST_TIMEOUT_MS = 10_000;
type ContactRequest = {
name?: unknown;
phone?: unknown;
email?: unknown;
message?: unknown;
};
function clean(value: unknown, maxLength: number) {
return typeof value === "string" ? value.trim().slice(0, maxLength) : "";
}
export async function POST(request: Request) {
let body: ContactRequest;
try {
body = (await request.json()) as ContactRequest;
} catch {
return NextResponse.json({ message: "اطلاعات فرم معتبر نیست." }, { status: 400 });
}
const name = clean(body.name, 120);
const cellNumber = clean(body.phone, 16);
const email = clean(body.email, 254);
const text = clean(body.message, 4_000);
if (!name || !text || !/^\+989\d{9}$/.test(cellNumber)) {
return NextResponse.json({ message: "لطفاً اطلاعات فرم را به‌درستی وارد کنید." }, { status: 400 });
}
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return NextResponse.json({ message: "نشانی ایمیل معتبر نیست." }, { status: 400 });
}
try {
const response = await fetch(CONTACT_ENDPOINT, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "درخواست تماس از وب‌سایت رئوفی",
name,
cellNumber,
text,
...(email ? { email } : {}),
}),
cache: "no-store",
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
if (!response.ok) {
return NextResponse.json(
{ message: "ارسال پیام با خطا روبه‌رو شد. لطفاً دوباره تلاش کنید." },
{ status: response.status >= 500 ? 502 : 400 },
);
}
return NextResponse.json({ message: "پیام شما با موفقیت ارسال شد." }, { status: 201 });
} catch {
return NextResponse.json(
{ message: "در حال حاضر ارتباط با سرویس پیام برقرار نیست. لطفاً کمی بعد دوباره تلاش کنید." },
{ status: 503 },
);
}
}
+5 -4
View File
@@ -4,7 +4,7 @@ import { NextRequest, NextResponse } from "next/server";
type RevalidateBody = {
/** Revalidate a fetch cache tag (portfolios, blogs, portfolio:slug, blog:slug, …). */
tag?: string;
/** Revalidate a path (/portfolios/foo, /, …). */
/** Revalidate a path (/projects/foo, /, …). */
path?: string;
/** Shorthand: portfolios | blogs | all */
scope?: "portfolios" | "blogs" | "all";
@@ -15,7 +15,7 @@ type RevalidateBody = {
*
* POST /api/revalidate
* Header: x-revalidate-secret: <REVALIDATE_SECRET>
* Body (JSON, optional): { "scope": "all" } | { "tag": "portfolios" } | { "path": "/portfolios/item" }
* Body (JSON, optional): { "scope": "all" } | { "tag": "portfolios" } | { "path": "/projects/item" }
*
* nginx may still serve cached HTML for up to its TTL (5m) unless purged separately.
*/
@@ -50,9 +50,10 @@ export async function POST(req: NextRequest) {
} else if (scope === "portfolios" || scope === "all") {
revalidateTag("portfolios");
revalidatePath("/");
revalidatePath("/portfolios");
revalidatePath("/projects");
revalidatePath("/aboutus");
revalidatePath("/our-services");
revalidatePath("/atelier");
revalidatePath("/raoofi-home");
revalidated.push("scope:portfolios");
}
+25
View File
@@ -0,0 +1,25 @@
import type { Metadata } from "next";
import { RaoofiAtelier } from "@/components/atelier/RaoofiAtelier";
import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader";
import { getRecentProjects, toDisplayProject } from "@/lib/api";
export const revalidate = 300;
export const metadata: Metadata = {
title: "آتلیه رئوفی",
description: "آتلیه رئوفی؛ خدمات یکپارچه از نخستین ایده و طراحی تا مدیریت، اجرا و تحویل نهایی.",
};
export default async function AtelierPage() {
const projects = await getRecentProjects(12);
return (
<>
<SiteHeader />
<RaoofiAtelier projects={projects.map(toDisplayProject)} />
<SiteFooter />
</>
);
}

Some files were not shown because too many files have changed in this diff Show More