Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d320b8f10 | ||
|
|
55d0fad2c8 | ||
|
|
6b3e87a09d | ||
|
|
fe4c110eaf | ||
|
|
8855e09c23 | ||
|
|
4b87869acb | ||
|
|
6f7e24d21b | ||
|
|
2f6c1a547e | ||
|
|
02c7dd51d2 | ||
|
|
f341b010c0 |
@@ -0,0 +1,4 @@
|
||||
# Runtime env (set in PM2 ecosystem on the websites VM — not committed)
|
||||
|
||||
# Secret for POST /api/revalidate (on-demand cache purge after CMS publish)
|
||||
REVALIDATE_SECRET=
|
||||
@@ -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` منفی) و با کیبورد و لمس کار میکند.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,20 +3,22 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "node scripts/dev.cjs",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "15.5.4",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1"
|
||||
"next": "15.5.25",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"swiper": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint-config-next": "^15.5.25",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 443 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 632 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: 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 |
|
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 |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 561 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 482 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 88 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 |
|
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 |
@@ -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);
|
||||
});
|
||||
@@ -1,114 +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 { getPortfolios, toDisplayProject } from "@/lib/api";
|
||||
import { getRecentProjects, toDisplayProject } from "@/lib/api";
|
||||
|
||||
export const revalidate = 300;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "درباره ما",
|
||||
description:
|
||||
"از سال ۱۳۷۹، دیدگاه مهندس قاسم رئوفیمنش مسیر گروه رئوفی را تعریف کرده است؛ دیدگاهی که هر پروژه را به تجربهای اصیل و ماندگار از سبک زندگی ارتقاء میدهد.",
|
||||
description: "روایت شکلگیری، ارزشها و مسیر گروه ساختمانی رئوفی از سال ۱۳۷۹ تا امروز.",
|
||||
};
|
||||
|
||||
export default async function AboutPage() {
|
||||
const portfolios = await getPortfolios();
|
||||
const displayProjects = portfolios.map(toDisplayProject);
|
||||
const projects = await getRecentProjects(8);
|
||||
const displayProjects = projects.map(toDisplayProject);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteHeader />
|
||||
|
||||
<main>
|
||||
<PageHero
|
||||
image={heroImage}
|
||||
imageMobile={heroImageMobile}
|
||||
caption={aboutPage.hero.caption}
|
||||
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 />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||