First commit
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Magic Patterns - Vite Template
|
||||||
|
|
||||||
|
This code was generated by [Magic Patterns](https://magicpatterns.com) for this design: [Source Design](https://www.magicpatterns.com/c/ka4i8ikygtakmdobhws6nu)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Run `npm install`
|
||||||
|
2. Run `npm run dev`
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ArameshPlaza Luxury Website</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+4526
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "magic-patterns-vite-template",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "npx vite",
|
||||||
|
"build": "npx vite build",
|
||||||
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
|
"preview": "npx vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.13.3",
|
||||||
|
"framer-motion": "^11.5.4",
|
||||||
|
"lucide-react": "0.522.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.26.2",
|
||||||
|
"sweetalert2": "^11.26.25"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.18",
|
||||||
|
"@types/react": "^18.3.1",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
||||||
|
"@typescript-eslint/parser": "^5.54.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"autoprefixer": "latest",
|
||||||
|
"eslint": "^8.50.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.1",
|
||||||
|
"postcss": "latest",
|
||||||
|
"tailwindcss": "3.4.17",
|
||||||
|
"typescript": "^5.5.4",
|
||||||
|
"vite": "^5.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { BrowserRouter, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { Preloader } from './components/Preloader';
|
||||||
|
import { Header } from './components/Header';
|
||||||
|
import { Footer } from './components/Footer';
|
||||||
|
import { FloatingContact } from './components/FloatingContact';
|
||||||
|
import { Home } from './pages/Home';
|
||||||
|
import { About } from './pages/About';
|
||||||
|
import { ServicesPage } from './pages/ServicesPage';
|
||||||
|
import { Projects } from './pages/Projects';
|
||||||
|
import { ProjectDetail } from './pages/ProjectDetail';
|
||||||
|
import { Investment } from './pages/Investment';
|
||||||
|
import { Blog } from './pages/Blog';
|
||||||
|
import { ArticleDetail } from './pages/ArticleDetail';
|
||||||
|
import { FaqPage } from './pages/FaqPage';
|
||||||
|
import { ContactPage } from './pages/ContactPage';
|
||||||
|
|
||||||
|
function ScrollToTop() {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'auto' });
|
||||||
|
}, [pathname]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnimatedRoutes() {
|
||||||
|
const location = useLocation();
|
||||||
|
return (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.main
|
||||||
|
key={location.pathname}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}>
|
||||||
|
|
||||||
|
<Routes location={location}>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="/about" element={<About />} />
|
||||||
|
<Route path="/services" element={<ServicesPage />} />
|
||||||
|
<Route path="/projects" element={<Projects />} />
|
||||||
|
<Route path="/projects/:slug" element={<ProjectDetail />} />
|
||||||
|
<Route path="/investment" element={<Investment />} />
|
||||||
|
<Route path="/blog" element={<Blog />} />
|
||||||
|
<Route path="/blog/:slug" element={<ArticleDetail />} />
|
||||||
|
<Route path="/faq" element={<FaqPage />} />
|
||||||
|
<Route path="/contact" element={<ContactPage />} />
|
||||||
|
<Route path="*" element={<Home />} />
|
||||||
|
</Routes>
|
||||||
|
</motion.main>
|
||||||
|
</AnimatePresence>);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.setAttribute('dir', 'rtl');
|
||||||
|
document.documentElement.setAttribute('lang', 'fa');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div dir="rtl" lang="fa" className="min-h-screen w-full overflow-x-hidden bg-ink font-pinar text-ivory">
|
||||||
|
<BrowserRouter>
|
||||||
|
<Preloader />
|
||||||
|
<ScrollToTop />
|
||||||
|
<Header />
|
||||||
|
<AnimatedRoutes />
|
||||||
|
<Footer />
|
||||||
|
<FloatingContact />
|
||||||
|
</BrowserRouter>
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { ArrowLeftIcon, ClockIcon } from 'lucide-react'
|
||||||
|
import type { Article } from '../data/site'
|
||||||
|
|
||||||
|
export function ArticleCard({ article }: { article: Article }) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={`/blog/${article.slug}`}
|
||||||
|
className="group flex h-full flex-col border border-ivory/10 bg-char transition-all duration-300 ease-out hover:-translate-y-2 hover:border-copper/40 hover:shadow-lift transform-gpu will-change-transform focus:outline-none focus-visible:ring-2 focus-visible:ring-copper"
|
||||||
|
aria-label={`خواندن مقاله ${article.title}`}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={article.image}
|
||||||
|
alt={`تصویر مقاله ${article.title}`}
|
||||||
|
className="h-52 w-full object-cover transition-transform duration-300 ease-out group-hover:scale-105 transform-gpu"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-1 flex-col p-8">
|
||||||
|
<p className="text-xs font-light text-copper">{article.category}</p>
|
||||||
|
|
||||||
|
<h3 className="mt-4 text-lg font-bold leading-9 text-ivory transition-colors duration-300 ease-out group-hover:text-copper">
|
||||||
|
{article.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="mt-4 mb-8 text-sm font-light leading-[2.2rem] text-ivory/55">
|
||||||
|
{article.excerpt}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-auto flex items-center justify-between border-t border-ivory/10 pt-6 text-xs font-light text-ivory/50">
|
||||||
|
<span className="flex items-center gap-4">
|
||||||
|
<span>{article.date}</span>
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<ClockIcon className="h-3.5 w-3.5" />
|
||||||
|
{article.read}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-2 text-copper">
|
||||||
|
ادامه مطلب
|
||||||
|
<ArrowLeftIcon className="h-3.5 w-3.5 transition-transform duration-300 ease-out group-hover:-translate-x-1.5" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
type Variant = 'copper' | 'outline' | 'dark' | 'dark-ivory'
|
||||||
|
|
||||||
|
type BaseProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
variant?: Variant
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const STYLES: Record<Variant, { base: string; fill: string; text: string }> = {
|
||||||
|
copper: {
|
||||||
|
base: 'bg-copper text-char',
|
||||||
|
fill: 'bg-char',
|
||||||
|
text: 'group-hover:text-ivory',
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
base: 'border border-ivory/30 text-ivory',
|
||||||
|
fill: 'bg-copper',
|
||||||
|
text: 'group-hover:text-char',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
base: 'bg-char text-ivory',
|
||||||
|
fill: 'bg-copper',
|
||||||
|
text: 'group-hover:text-char',
|
||||||
|
},
|
||||||
|
'dark-ivory': {
|
||||||
|
base: 'bg-char text-ivory',
|
||||||
|
fill: 'bg-ivory',
|
||||||
|
text: 'group-hover:text-char',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function Inner({ children, variant = 'copper' }: { children: React.ReactNode; variant?: Variant }) {
|
||||||
|
const style = STYLES[variant]
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`absolute inset-0 translate-y-full transition-transform duration-[600ms] ease-lux group-hover:translate-y-0 ${style.fill}`}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`relative z-10 flex items-center justify-center gap-3 transition-colors duration-[600ms] ease-lux ${style.text}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHELL =
|
||||||
|
'group relative inline-flex items-center justify-center overflow-hidden px-8 py-4 text-sm font-bold focus:outline-none focus-visible:ring-2 focus-visible:ring-copper focus-visible:ring-offset-2 focus-visible:ring-offset-ink'
|
||||||
|
|
||||||
|
export function ButtonLink({
|
||||||
|
to,
|
||||||
|
children,
|
||||||
|
variant = 'copper',
|
||||||
|
className = '',
|
||||||
|
}: BaseProps & { to: string }) {
|
||||||
|
return (
|
||||||
|
<Link to={to} className={`${SHELL} ${STYLES[variant].base} ${className}`}>
|
||||||
|
<Inner variant={variant}>{children}</Inner>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ButtonAnchor({
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
variant = 'copper',
|
||||||
|
className = '',
|
||||||
|
...rest
|
||||||
|
}: BaseProps & { href: string } & React.AnchorHTMLAttributes<HTMLAnchorElement>) {
|
||||||
|
return (
|
||||||
|
<a href={href} className={`${SHELL} ${STYLES[variant].base} ${className}`} {...rest}>
|
||||||
|
<Inner variant={variant}>{children}</Inner>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
children,
|
||||||
|
variant = 'copper',
|
||||||
|
className = '',
|
||||||
|
...rest
|
||||||
|
}: BaseProps & React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||||
|
return (
|
||||||
|
<button className={`${SHELL} ${STYLES[variant].base} ${className}`} {...rest}>
|
||||||
|
<Inner variant={variant}>{children}</Inner>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { CheckCircle2Icon, MailIcon, MapPinIcon, PhoneIcon } from 'lucide-react'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { Reveal } from './Reveal'
|
||||||
|
import { Button } from './Button'
|
||||||
|
import { BRAND } from '../data/site'
|
||||||
|
|
||||||
|
export function ContactSection() {
|
||||||
|
const [form, setForm] = useState({ name: '', phone: '', note: '' })
|
||||||
|
const [sent, setSent] = useState(false)
|
||||||
|
|
||||||
|
const showErrorAlert = (title: string, text: string) => {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: title,
|
||||||
|
text: text,
|
||||||
|
background: '#1e1a16',
|
||||||
|
color: '#F5F3EF',
|
||||||
|
iconColor: '#d5874c',
|
||||||
|
confirmButtonText: 'متوجه شدم',
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
container: 'backdrop-blur-[8px] bg-ink/60',
|
||||||
|
popup: 'font-pinar border border-ivory/10 rounded-none shadow-lift',
|
||||||
|
title: 'font-pinar font-bold text-lg text-ivory pt-2',
|
||||||
|
htmlContainer: 'font-pinar font-light text-sm leading-8 text-ivory/70',
|
||||||
|
confirmButton: 'mt-6 bg-copper text-char font-bold px-10 py-3 transition-transform duration-300 ease-lux hover:scale-105',
|
||||||
|
},
|
||||||
|
showClass: {
|
||||||
|
popup: 'swal2-show',
|
||||||
|
backdrop: 'swal2-backdrop-show',
|
||||||
|
},
|
||||||
|
hideClass: {
|
||||||
|
popup: 'swal2-hide',
|
||||||
|
backdrop: 'swal2-backdrop-hide',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const nameValue = form.name.trim()
|
||||||
|
const phoneValue = form.phone.trim()
|
||||||
|
|
||||||
|
if (nameValue.length < 3) {
|
||||||
|
showErrorAlert('اطلاعات ناقص است', 'لطفاً نام و نام خانوادگی خود را کامل وارد کنید (حداقل ۳ حرف).')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const phoneRegex = /^[\d۰-۹\+\-\s]{8,15}$/
|
||||||
|
if (!phoneValue || !phoneRegex.test(phoneValue)) {
|
||||||
|
showErrorAlert('شماره تلفن نامعتبر است', 'لطفاً یک شماره تماس معتبر وارد کنید تا کارشناسان ما بتوانند با شما ارتباط بگیرند.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSent(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="contact" className="relative overflow-hidden py-24 lg:py-32" aria-labelledby="contact-title">
|
||||||
|
<img
|
||||||
|
src={BRAND.contactBg}
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<div className="mx-auto grid max-w-6xl items-stretch gap-6 lg:grid-cols-12">
|
||||||
|
|
||||||
|
<div className="border border-ivory/20 bg-char/45 p-8 backdrop-blur-[8px] md:p-10 lg:col-span-5">
|
||||||
|
<Reveal>
|
||||||
|
<p className="text-sm font-light text-copper">تماس با ما</p>
|
||||||
|
<h2 id="contact-title" className="mt-5 text-section font-bold text-ivory">
|
||||||
|
پروژهتان را با ما در میان بگذارید
|
||||||
|
</h2>
|
||||||
|
<p className="mt-6 text-base font-light leading-9 text-ivory/75">
|
||||||
|
شمارهتان را بگذارید؛ کارشناس ما در کمتر از یک روز کاری برای یک جلسه مشاوره رایگان تماس میگیرد.
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={0.1}>
|
||||||
|
<ul className="mt-10 space-y-6">
|
||||||
|
<li className="flex items-start gap-4">
|
||||||
|
<MapPinIcon className="mt-1 h-5 w-5 shrink-0 text-copper" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-ivory">دفتر مرکزی</p>
|
||||||
|
<p className="mt-1 text-sm font-light leading-7 text-ivory/70">{BRAND.address}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-4">
|
||||||
|
<PhoneIcon className="mt-1 h-5 w-5 shrink-0 text-copper" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-ivory">تلفن</p>
|
||||||
|
<a
|
||||||
|
href={BRAND.phoneHref}
|
||||||
|
dir="ltr"
|
||||||
|
className="mt-1 block text-sm font-light text-ivory/70 transition-colors hover:text-copper"
|
||||||
|
>
|
||||||
|
{BRAND.phone}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-4">
|
||||||
|
<MailIcon className="mt-1 h-5 w-5 shrink-0 text-copper" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-ivory">ایمیل</p>
|
||||||
|
<a
|
||||||
|
href={`mailto:${BRAND.email}`}
|
||||||
|
dir="ltr"
|
||||||
|
className="mt-1 block text-sm font-light text-ivory/70 transition-colors hover:text-copper"
|
||||||
|
>
|
||||||
|
{BRAND.email}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-ivory/20 bg-char/45 p-8 backdrop-blur-[9px] md:p-10 lg:col-span-7">
|
||||||
|
{sent ? (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center py-14 text-center">
|
||||||
|
<CheckCircle2Icon className="h-12 w-12 text-copper" />
|
||||||
|
<h3 className="mt-6 text-xl font-bold text-ivory">درخواست شما ثبت شد</h3>
|
||||||
|
<p className="mt-3 max-w-sm text-sm font-light leading-8 text-ivory/70">
|
||||||
|
از اعتماد شما سپاسگزاریم. کارشناسان ما بهزودی با شما تماس میگیرند.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="mt-8 font-light"
|
||||||
|
onClick={() => {
|
||||||
|
setSent(false)
|
||||||
|
setForm({ name: '', phone: '', note: '' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ثبت درخواست جدید
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={submit} noValidate className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="contact-name" className="block text-sm font-bold text-ivory/90">
|
||||||
|
نام
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="contact-name"
|
||||||
|
type="text"
|
||||||
|
value={form.name}
|
||||||
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
|
placeholder="نام و نام خانوادگی"
|
||||||
|
className="mt-3 w-full border border-ivory/20 bg-ink/45 px-5 py-4 text-sm font-light text-ivory placeholder:text-ivory/40 transition-colors duration-200 ease-lux focus:border-copper focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="contact-phone" className="block text-sm font-bold text-ivory/90">
|
||||||
|
شماره تلفن <span className="text-copper">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="contact-phone"
|
||||||
|
type="tel"
|
||||||
|
dir="ltr"
|
||||||
|
required
|
||||||
|
value={form.phone}
|
||||||
|
onChange={(e) => setForm({ ...form, phone: e.target.value })}
|
||||||
|
placeholder="09xx xxx xxxx"
|
||||||
|
className="mt-3 w-full border border-ivory/20 bg-ink/45 px-5 py-4 text-right text-sm font-light text-ivory placeholder:text-ivory/40 transition-colors duration-200 ease-lux focus:border-copper focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label htmlFor="contact-note" className="block text-sm font-bold text-ivory/90">
|
||||||
|
توضیحات <span className="font-light text-ivory/50">(اختیاری)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="contact-note"
|
||||||
|
rows={6}
|
||||||
|
value={form.note}
|
||||||
|
onChange={(e) => setForm({ ...form, note: e.target.value })}
|
||||||
|
placeholder="درباره پروژه یا درخواست خود بنویسید…"
|
||||||
|
className="mt-3 w-full resize-none border border-ivory/20 bg-ink/45 px-5 py-4 text-sm font-light leading-8 text-ivory placeholder:text-ivory/40 transition-colors duration-200 ease-lux focus:border-copper focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<Button type="submit" variant="copper" className="w-full md:w-auto md:px-12">
|
||||||
|
ارسال درخواست مشاوره
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PhoneIcon } from 'lucide-react';
|
||||||
|
import { BRAND } from '../data/site';
|
||||||
|
|
||||||
|
export function FloatingContact() {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={BRAND.phoneHref}
|
||||||
|
aria-label="تماس تلفنی با آرامش پلازا"
|
||||||
|
className="group fixed bottom-8 left-8 z-40 flex h-14 w-14 items-center justify-center rounded-full bg-copper text-char shadow-[0_10px_30px_rgba(0,0,0,0.45)] transition-transform duration-200 ease-lux hover:scale-105">
|
||||||
|
|
||||||
|
<span className="absolute inset-0 rounded-full border border-copper/50 opacity-0 transition-transform duration-300 ease-lux group-hover:scale-125 group-hover:opacity-100" />
|
||||||
|
<PhoneIcon className="h-5 w-5" />
|
||||||
|
</a>);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { InstagramIcon, LinkedinIcon, MailIcon, MapPinIcon, PhoneIcon, SendIcon } from 'lucide-react';
|
||||||
|
import { BRAND, NAV, SERVICES } from '../data/site';
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-ivory/10 bg-char">
|
||||||
|
<div className="mx-auto max-w-site px-6 py-16 lg:px-10">
|
||||||
|
<div className="grid gap-12 lg:grid-cols-12">
|
||||||
|
<div className="lg:col-span-4">
|
||||||
|
<img src={BRAND.logo} alt={BRAND.name} className="h-20 w-auto object-contain" />
|
||||||
|
<p className="mt-6 max-w-sm text-sm font-light leading-8 text-ivory/55">
|
||||||
|
آرامش پلازا از سال ۱۳۸۳ ساختمانهایی میسازد که قرار است چند نسل در آنها زندگی کنند؛ معماری آرام،
|
||||||
|
اجرای دقیق و سرمایهگذاری شفاف.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 flex gap-3">
|
||||||
|
{[
|
||||||
|
{ Icon: InstagramIcon, label: 'اینستاگرام' },
|
||||||
|
{ Icon: LinkedinIcon, label: 'لینکدین' },
|
||||||
|
{ Icon: SendIcon, label: 'تلگرام' }].
|
||||||
|
map(({ Icon, label }) =>
|
||||||
|
<a
|
||||||
|
key={label}
|
||||||
|
href="#"
|
||||||
|
aria-label={label}
|
||||||
|
className="flex h-10 w-10 items-center justify-center border border-ivory/15 text-ivory/70 transition-colors duration-200 ease-lux hover:border-copper hover:text-copper">
|
||||||
|
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<h3 className="text-sm font-bold text-copper">صفحات</h3>
|
||||||
|
<ul className="mt-5 space-y-3">
|
||||||
|
{NAV.map((item) =>
|
||||||
|
<li key={item.to}>
|
||||||
|
<Link
|
||||||
|
to={item.to}
|
||||||
|
className="text-sm font-light text-ivory/60 transition-colors duration-200 ease-lux hover:text-copper">
|
||||||
|
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-3">
|
||||||
|
<h3 className="text-sm font-bold text-copper">خدمات</h3>
|
||||||
|
<ul className="mt-5 space-y-3">
|
||||||
|
{SERVICES.map((service) =>
|
||||||
|
<li key={service.title} className="text-sm font-light text-ivory/60">
|
||||||
|
{service.title}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-3">
|
||||||
|
<h3 className="text-sm font-bold text-copper">ارتباط با ما</h3>
|
||||||
|
<ul className="mt-5 space-y-4 text-sm font-light text-ivory/60">
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<MapPinIcon className="mt-1 h-4 w-4 shrink-0 text-copper" />
|
||||||
|
<span className="leading-7">{BRAND.address}</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-3">
|
||||||
|
<PhoneIcon className="h-4 w-4 shrink-0 text-copper" />
|
||||||
|
<a href={BRAND.phoneHref} dir="ltr" className="transition-colors hover:text-copper">
|
||||||
|
{BRAND.phone}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-3">
|
||||||
|
<MailIcon className="h-4 w-4 shrink-0 text-copper" />
|
||||||
|
<a href={`mailto:${BRAND.email}`} dir="ltr" className="transition-colors hover:text-copper">
|
||||||
|
{BRAND.email}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-14 flex flex-col items-center justify-between gap-4 border-t border-ivory/10 pt-6 text-xs font-light text-ivory/40 md:flex-row">
|
||||||
|
<p>© ۱۴۰۳ آرامش پلازا — تمامی حقوق محفوظ است.</p>
|
||||||
|
<p>طراحی و ساخت با دقتِ معماری</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Link, NavLink, useLocation } from "react-router-dom";
|
||||||
|
import { MenuIcon, PhoneIcon, XIcon } from "lucide-react";
|
||||||
|
import { BRAND, NAV } from "../data/site";
|
||||||
|
import { use } from "framer-motion/m";
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onScroll = () => setScrolled(window.scrollY > 24);
|
||||||
|
onScroll();
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener("scroll", onScroll);
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={`fixed inset-x-0 top-0 z-50 transition-[background-color,border-color,backdrop-filter] duration-300 ease-lux ${
|
||||||
|
scrolled
|
||||||
|
? "border-b border-ivory/10 bg-ink/90 backdrop-blur-md"
|
||||||
|
: "border-b border-transparent bg-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="mx-auto flex max-w-site items-center justify-between gap-8 px-6 py-4 lg:px-10">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="flex items-center gap-3"
|
||||||
|
aria-label={BRAND.name}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={BRAND.logo}
|
||||||
|
alt={BRAND.name}
|
||||||
|
className={`w-auto object-contain transition-[height] duration-300 ease-lux ${
|
||||||
|
scrolled ? "h-14" : "h-20"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<nav aria-label="ناوبری اصلی" className="hidden xl:block">
|
||||||
|
<ul className="flex items-center gap-7">
|
||||||
|
{NAV.map((item) => (
|
||||||
|
<li key={item.to}>
|
||||||
|
<NavLink
|
||||||
|
to={item.to}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`group relative py-2 text-[0.95rem] font-light transition-colors duration-200 ease-lux hover:text-copper [text-shadow:_0_2px_4px_rgba(0,0,0,0.6)] ${
|
||||||
|
isActive ? "text-copper" : "text-ivory/80"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<>
|
||||||
|
{item.label}
|
||||||
|
<span
|
||||||
|
className={`absolute -bottom-0.5 left-0 right-0 h-px w-full origin-center bg-copper transition-transform duration-300 ease-lux ${
|
||||||
|
isActive
|
||||||
|
? "scale-x-100"
|
||||||
|
: "scale-x-0 group-hover:scale-x-100"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<a
|
||||||
|
href={BRAND.phoneHref}
|
||||||
|
className="group relative hidden items-center gap-2 overflow-hidden border border-copper/50 px-5 py-2.5 text-sm font-medium text-copper [text-shadow:_0_1px_2px_rgba(0,0,0,0.5)] lg:flex"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 translate-y-full bg-copper transition-transform duration-300 ease-lux group-hover:translate-y-0"
|
||||||
|
/>
|
||||||
|
<span className="relative z-10 flex items-center gap-2 transition-colors duration-300 ease-lux group-hover:text-char">
|
||||||
|
<PhoneIcon className="h-4 w-4" />
|
||||||
|
<span dir="ltr">{BRAND.phone}</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
className="flex h-11 w-11 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper xl:hidden"
|
||||||
|
aria-label={open ? "بستن منو" : "باز کردن منو"}
|
||||||
|
aria-expanded={open}
|
||||||
|
>
|
||||||
|
{open ? (
|
||||||
|
<XIcon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<MenuIcon className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<nav
|
||||||
|
aria-label="ناوبری موبایل"
|
||||||
|
className="border-t border-ivory/10 bg-ink/98 px-6 py-4 xl:hidden"
|
||||||
|
>
|
||||||
|
<ul className="grid gap-1">
|
||||||
|
{NAV.map((item) => (
|
||||||
|
<li key={item.to}>
|
||||||
|
<NavLink
|
||||||
|
to={item.to}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`block border-b border-ivory/5 py-3 text-base font-light transition-colors duration-200 ease-lux ${
|
||||||
|
isActive ? "text-copper" : "text-ivory/80"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import { XIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
images: string[]
|
||||||
|
index: number | null
|
||||||
|
onClose: () => void
|
||||||
|
onChange: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Lightbox({ images, index, onClose, onChange }: Props) {
|
||||||
|
const isOpen = index !== null
|
||||||
|
|
||||||
|
const next = useCallback(() => {
|
||||||
|
if (index === null) return
|
||||||
|
onChange((index + 1) % images.length)
|
||||||
|
}, [index, images.length, onChange])
|
||||||
|
|
||||||
|
const prev = useCallback(() => {
|
||||||
|
if (index === null) return
|
||||||
|
onChange((index - 1 + images.length) % images.length)
|
||||||
|
}, [index, images.length, onChange])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') onClose()
|
||||||
|
if (e.key === 'ArrowRight') prev()
|
||||||
|
if (e.key === 'ArrowLeft') next()
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKey)
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', onKey)
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
}, [isOpen, onClose, next, prev])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
className="fixed inset-0 z-[90] flex items-center justify-center p-5 md:p-10"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-ink/90 backdrop-blur-md"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute left-6 top-6 z-10 flex h-12 w-12 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper md:left-10 md:top-10"
|
||||||
|
>
|
||||||
|
<XIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<motion.img
|
||||||
|
key={index}
|
||||||
|
src={images[index as number]}
|
||||||
|
alt=""
|
||||||
|
className="relative max-h-[80vh] w-auto max-w-full object-contain"
|
||||||
|
initial={{ opacity: 0, scale: 0.96 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.96 }}
|
||||||
|
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute bottom-6 z-10 flex items-center gap-4 md:bottom-10">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={prev}
|
||||||
|
className="flex h-11 w-11 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper"
|
||||||
|
>
|
||||||
|
<ChevronRightIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm font-light text-ivory/70" dir="ltr">
|
||||||
|
{(index as number) + 1} / {images.length}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={next}
|
||||||
|
className="flex h-11 w-11 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronLeftIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
type PageHeroProps = {
|
||||||
|
title: string
|
||||||
|
lead: string
|
||||||
|
image: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageHero({ title, lead, image }: PageHeroProps) {
|
||||||
|
return (
|
||||||
|
<section className="relative flex h-[58vh] min-h-[420px] items-end overflow-hidden">
|
||||||
|
<img src={image} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover" />
|
||||||
|
|
||||||
|
<div className="absolute inset-0 bg-ink/50" aria-hidden="true" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{ background: 'linear-gradient(to top, #27221c 6%, rgba(39,34,28,0.1) 70%)' }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative mx-auto w-full max-w-site px-6 pb-16 lg:px-10">
|
||||||
|
<nav aria-label="مسیر صفحه" className="flex items-center gap-2 text-xs font-light text-ivory/50">
|
||||||
|
<Link to="/" className="transition-colors hover:text-copper">
|
||||||
|
صفحه اصلی
|
||||||
|
</Link>
|
||||||
|
<ChevronLeftIcon className="h-3.5 w-3.5" />
|
||||||
|
<span className="text-copper">{title}</span>
|
||||||
|
</nav>
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.7, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className="mt-5 max-w-[700px] text-hero font-bold text-ivory"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.7, delay: 0.1, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className="mt-5 max-w-2xl text-base font-light leading-9 text-ivory/65"
|
||||||
|
>
|
||||||
|
{lead}
|
||||||
|
</motion.p>
|
||||||
|
<div className="mt-8 h-px w-28 bg-copper" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import { BRAND } from '../data/site'
|
||||||
|
|
||||||
|
export function Preloader() {
|
||||||
|
const [done, setDone] = useState(false)
|
||||||
|
const [progress, setProgress] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const start = Date.now()
|
||||||
|
const total = 1800
|
||||||
|
const id = window.setInterval(() => {
|
||||||
|
const p = Math.min(100, ((Date.now() - start) / total) * 100)
|
||||||
|
setProgress(p)
|
||||||
|
if (p >= 100) {
|
||||||
|
window.clearInterval(id)
|
||||||
|
window.setTimeout(() => setDone(true), 180)
|
||||||
|
}
|
||||||
|
}, 25)
|
||||||
|
return () => window.clearInterval(id)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = done ? '' : 'hidden'
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
}, [done])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{!done && (
|
||||||
|
<motion.div
|
||||||
|
className="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-ink"
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.5, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
aria-live="polite"
|
||||||
|
aria-label="در حال بارگذاری"
|
||||||
|
>
|
||||||
|
<div className="relative h-36 w-36">
|
||||||
|
<img
|
||||||
|
src={BRAND.logo}
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 h-full w-full object-contain opacity-[0.12]"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 overflow-hidden" style={{ clipPath: `inset(${100 - progress}% 0 0 0)` }}>
|
||||||
|
<img src={BRAND.logo} alt="" aria-hidden="true" className="h-full w-full object-contain" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="absolute inset-x-0 h-px bg-copper"
|
||||||
|
style={{ bottom: `${progress}%` }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-10 h-[2px] w-48 overflow-hidden bg-ivory/10">
|
||||||
|
<div className="h-full bg-copper" style={{ width: `${progress}%`, transition: 'width 120ms linear' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 6 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
className="mt-4 text-sm font-light tracking-widest text-copper font-pinar"
|
||||||
|
>
|
||||||
|
{BRAND.name}
|
||||||
|
</motion.p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
export function ProjectCard({ project }: any) {
|
||||||
|
const isOngoing = project.status === 'progress'
|
||||||
|
|
||||||
|
const statusText = isOngoing ? 'در حال انجام' : 'انجام شده'
|
||||||
|
|
||||||
|
const dotColor = isOngoing ? 'bg-copper animate-pulse' : 'bg-ivory/60'
|
||||||
|
const textColor = isOngoing ? 'text-copper' : 'text-ivory/90'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="group flex h-full flex-col overflow-hidden border border-ivory/10 bg-char transition-[transform,box-shadow,border-color] duration-500 ease-lux hover:-translate-y-2 hover:border-copper/50 hover:shadow-lift focus:outline-none focus-visible:ring-2 focus-visible:ring-copper">
|
||||||
|
<Link to={`/projects/${project.slug || '#'}`} className="flex h-full flex-col focus:outline-none">
|
||||||
|
|
||||||
|
<div className="relative aspect-[4/3] w-full overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={project.image}
|
||||||
|
alt={project.title}
|
||||||
|
className="h-full w-full object-cover transition-transform duration-700 ease-lux group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
<div className={`absolute right-4 top-4 flex items-center gap-2 bg-ink/85 px-4 py-2 text-xs font-light backdrop-blur-md ${textColor}`}>
|
||||||
|
<span className={`h-1.5 w-1.5 rounded-full ${dotColor}`} />
|
||||||
|
{statusText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-1 flex-col p-8">
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="font-pinar text-xl font-bold text-ivory">
|
||||||
|
{project.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-7 text-sm font-light text-ivory/60">
|
||||||
|
{project.location}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="my-7 h-px w-full bg-ivory/10" />
|
||||||
|
|
||||||
|
<div className="mt-auto flex items-center justify-between text-sm font-light text-ivory/60">
|
||||||
|
<span>سال: {project.year}</span>
|
||||||
|
<span>{project.area} متر مربع</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
type RevealProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
delay?: number;
|
||||||
|
y?: number;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Reveal({ children, delay = 0, y = 28, className }: RevealProps) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={className}
|
||||||
|
initial={{ opacity: 0, y }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true, margin: '-80px' }}
|
||||||
|
transition={{ duration: 0.7, delay, ease: [0.23, 1, 0.32, 1] }}>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</motion.div>);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Reveal } from './Reveal'
|
||||||
|
|
||||||
|
type SectionHeadingProps = {
|
||||||
|
title: string
|
||||||
|
lead?: string
|
||||||
|
align?: 'right' | 'center'
|
||||||
|
tone?: 'copper' | 'ivory'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SectionHeading({
|
||||||
|
title,
|
||||||
|
lead,
|
||||||
|
align = 'right',
|
||||||
|
tone = 'ivory',
|
||||||
|
}: SectionHeadingProps) {
|
||||||
|
const isCenter = align === 'center'
|
||||||
|
return (
|
||||||
|
<Reveal className={isCenter ? 'mx-auto max-w-2xl text-center' : 'max-w-2xl text-right'}>
|
||||||
|
<h2 className={`text-section font-bold ${tone === 'copper' ? 'text-copper' : 'text-ivory'}`}>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{lead && <p className="mt-5 text-base font-light leading-9 text-ivory/60">{lead}</p>}
|
||||||
|
|
||||||
|
<div className={`mt-6 h-px w-24 bg-copper/70 ${isCenter ? 'mx-auto' : ''}`} />
|
||||||
|
</Reveal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { Service } from '../data/site';
|
||||||
|
|
||||||
|
type ServiceIconProps = {
|
||||||
|
name: Service['icon'];
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stroke = {
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
strokeWidth: 1.2,
|
||||||
|
strokeLinecap: 'round' as const,
|
||||||
|
strokeLinejoin: 'round' as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ServiceIcon({ name, className = 'h-12 w-12' }: ServiceIconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 48 48" className={className} aria-hidden="true" role="presentation">
|
||||||
|
{name === 'draft' &&
|
||||||
|
<g {...stroke}>
|
||||||
|
<path d="M6 40h36" />
|
||||||
|
<path d="M10 40V14l14-8 14 8v26" />
|
||||||
|
<path d="M10 22h28M24 6v34" />
|
||||||
|
<circle cx="24" cy="22" r="3" />
|
||||||
|
</g>
|
||||||
|
}
|
||||||
|
{name === 'tower' &&
|
||||||
|
<g {...stroke}>
|
||||||
|
<path d="M8 42h32" />
|
||||||
|
<path d="M15 42V10l9-5 9 5v32" />
|
||||||
|
<path d="M19 15h10M19 21h10M19 27h10M19 33h10" />
|
||||||
|
</g>
|
||||||
|
}
|
||||||
|
{name === 'mall' &&
|
||||||
|
<g {...stroke}>
|
||||||
|
<path d="M6 42h36" />
|
||||||
|
<path d="M9 42V20h30v22" />
|
||||||
|
<path d="M9 20l6-10h18l6 10" />
|
||||||
|
<path d="M19 42V30h10v12" />
|
||||||
|
</g>
|
||||||
|
}
|
||||||
|
{name === 'villa' &&
|
||||||
|
<g {...stroke}>
|
||||||
|
<path d="M5 42h38" />
|
||||||
|
<path d="M9 42V22l15-10 15 10v20" />
|
||||||
|
<path d="M18 42V30h12v12" />
|
||||||
|
<path d="M9 26h30" />
|
||||||
|
</g>
|
||||||
|
}
|
||||||
|
{name === 'renew' &&
|
||||||
|
<g {...stroke}>
|
||||||
|
<path d="M10 42h28V20L24 10 10 20z" />
|
||||||
|
<path d="M31 6a9 9 0 1 0 6 15" />
|
||||||
|
<path d="M38 5v7h-7" />
|
||||||
|
</g>
|
||||||
|
}
|
||||||
|
{name === 'manage' &&
|
||||||
|
<g {...stroke}>
|
||||||
|
<rect x="8" y="8" width="32" height="32" rx="2" />
|
||||||
|
<path d="M8 17h32" />
|
||||||
|
<path d="M14 25h8M14 32h14M27 24l4 4 7-8" />
|
||||||
|
</g>
|
||||||
|
}
|
||||||
|
{name === 'invest' &&
|
||||||
|
<g {...stroke}>
|
||||||
|
<path d="M7 40h34" />
|
||||||
|
<path d="M12 40V26M22 40V18M32 40V30" />
|
||||||
|
<path d="M9 16l10-6 8 5 12-9" />
|
||||||
|
<path d="M39 6h-7M39 6v7" />
|
||||||
|
</g>
|
||||||
|
}
|
||||||
|
{name === 'landscape' &&
|
||||||
|
<g {...stroke}>
|
||||||
|
<path d="M5 40h38" />
|
||||||
|
<path d="M14 40V24M14 24c-4 0-7-3-7-6s3-5 7-5 7 2 7 5-3 6-7 6z" />
|
||||||
|
<path d="M31 40V22l7-7 4 5v20" />
|
||||||
|
<path d="M24 40c2-4 5-6 9-6" />
|
||||||
|
</g>
|
||||||
|
}
|
||||||
|
</svg>);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ArrowLeftIcon } from 'lucide-react';
|
||||||
|
import { Reveal } from '../Reveal';
|
||||||
|
import { ButtonLink } from '../Button';
|
||||||
|
import { ArticleCard } from '../ArticleCard';
|
||||||
|
import { ARTICLES } from '../../data/site';
|
||||||
|
|
||||||
|
export function Articles() {
|
||||||
|
return (
|
||||||
|
<section className="bg-char py-24 lg:py-32" aria-labelledby="articles-title">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<Reveal className="max-w-2xl text-right">
|
||||||
|
<p className="text-sm font-light text-copper">وبلاگ</p>
|
||||||
|
<h2 id="articles-title" className="mt-5 text-section font-bold text-ivory">
|
||||||
|
آخرین مقالات
|
||||||
|
</h2>
|
||||||
|
<p className="mt-5 text-base font-light leading-9 text-ivory/60">
|
||||||
|
یادداشتهای تیم معماری، اجرا و سرمایهگذاری آرامش پلازا درباره آنچه در کارگاه و بازار میگذرد.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 h-px w-24 bg-copper/70" />
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<div className="mt-14 grid gap-6 lg:grid-cols-3">
|
||||||
|
{ARTICLES.slice(0, 3).map((article, i) =>
|
||||||
|
<Reveal key={article.slug} delay={0.06 * i} className="h-full">
|
||||||
|
<ArticleCard article={article} />
|
||||||
|
</Reveal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Reveal delay={0.1}>
|
||||||
|
<div className="mt-12 flex justify-center">
|
||||||
|
<ButtonLink to="/blog" variant="outline" className="font-light">
|
||||||
|
همه مقالات
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
</ButtonLink>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</section>);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { MoveHorizontalIcon } from 'lucide-react'
|
||||||
|
import { Reveal } from '../Reveal'
|
||||||
|
import { SectionHeading } from '../SectionHeading'
|
||||||
|
import { BEFORE_AFTER } from '../../data/site'
|
||||||
|
|
||||||
|
export function BeforeAfter() {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [position, setPosition] = useState(52)
|
||||||
|
const dragging = useRef(false)
|
||||||
|
|
||||||
|
const setFromClientX = useCallback((clientX: number) => {
|
||||||
|
const el = containerRef.current
|
||||||
|
if (!el) return
|
||||||
|
const rect = el.getBoundingClientRect()
|
||||||
|
const ratio = ((clientX - rect.left) / rect.width) * 100
|
||||||
|
setPosition(Math.min(98, Math.max(2, ratio)))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const move = (e: MouseEvent | TouchEvent) => {
|
||||||
|
if (!dragging.current) return
|
||||||
|
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX
|
||||||
|
setFromClientX(clientX)
|
||||||
|
}
|
||||||
|
const stop = () => {
|
||||||
|
dragging.current = false
|
||||||
|
}
|
||||||
|
window.addEventListener('mousemove', move)
|
||||||
|
window.addEventListener('touchmove', move)
|
||||||
|
window.addEventListener('mouseup', stop)
|
||||||
|
window.addEventListener('touchend', stop)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousemove', move)
|
||||||
|
window.removeEventListener('touchmove', move)
|
||||||
|
window.removeEventListener('mouseup', stop)
|
||||||
|
window.removeEventListener('touchend', stop)
|
||||||
|
}
|
||||||
|
}, [setFromClientX])
|
||||||
|
|
||||||
|
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'ArrowLeft') setPosition((p) => Math.max(2, p - 4))
|
||||||
|
if (e.key === 'ArrowRight') setPosition((p) => Math.min(98, p + 4))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-char py-24 lg:py-32" aria-labelledby="ba-title">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
|
||||||
|
<SectionHeading
|
||||||
|
title={BEFORE_AFTER.title}
|
||||||
|
lead={BEFORE_AFTER.lead}
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Reveal delay={0.1}>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="relative mx-auto mt-14 aspect-[4/3] w-full max-w-4xl select-none overflow-hidden rounded-xl border border-ivory/10 shadow-2xl md:aspect-[16/10]"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
dragging.current = true
|
||||||
|
setFromClientX(e.clientX)
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
dragging.current = true
|
||||||
|
setFromClientX(e.touches[0].clientX)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={BEFORE_AFTER.before}
|
||||||
|
alt={BEFORE_AFTER.beforeLabel}
|
||||||
|
className="absolute inset-0 h-full w-full object-cover object-center"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute inset-0 overflow-hidden" style={{ clipPath: `inset(0 0 0 ${position}%)` }} aria-hidden="true">
|
||||||
|
<img
|
||||||
|
src={BEFORE_AFTER.after}
|
||||||
|
alt=""
|
||||||
|
className="absolute inset-0 h-full w-full object-cover object-center"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="pointer-events-none absolute left-6 top-6 z-30 rounded-md bg-ink/85 px-4 py-2 text-xs font-light text-ivory backdrop-blur-md border border-ivory/10 shadow-lg">
|
||||||
|
{BEFORE_AFTER.beforeLabel}
|
||||||
|
</span>
|
||||||
|
<span className="pointer-events-none absolute right-6 top-6 z-30 rounded-md bg-ink/85 px-4 py-2 text-xs font-light text-copper backdrop-blur-md border border-copper/30 shadow-lg">
|
||||||
|
{BEFORE_AFTER.afterLabel}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 z-30" style={{ left: `${position}%` }}>
|
||||||
|
<div className="h-full w-[2px] bg-copper shadow-[0_0_12px_rgba(213,135,76,0.9)]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="slider"
|
||||||
|
aria-label="مقایسه کانسپت و پروژه ساختهشده"
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
|
aria-valuenow={Math.round(position)}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onMouseDown={() => {
|
||||||
|
dragging.current = true
|
||||||
|
}}
|
||||||
|
onTouchStart={() => {
|
||||||
|
dragging.current = true
|
||||||
|
}}
|
||||||
|
className="absolute top-1/2 z-40 flex h-14 w-14 -translate-x-1/2 -translate-y-1/2 cursor-ew-resize items-center justify-center rounded-full bg-copper text-char shadow-2xl transition-transform duration-200 ease-lux hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-ivory"
|
||||||
|
style={{ left: `${position}%` }}
|
||||||
|
>
|
||||||
|
<MoveHorizontalIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import { PlusIcon } from 'lucide-react'
|
||||||
|
import { Reveal } from '../Reveal'
|
||||||
|
import { SectionHeading } from '../SectionHeading'
|
||||||
|
import { FAQS } from '../../data/site'
|
||||||
|
|
||||||
|
type FaqProps = {
|
||||||
|
hideHeading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Faq({ hideHeading = false }: FaqProps) {
|
||||||
|
const [open, setOpen] = useState<number | null>(0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-ink py-24 lg:py-32" aria-labelledby="faq-title">
|
||||||
|
|
||||||
|
<div className={hideHeading ? "mx-auto max-w-4xl px-6 lg:px-10" : "mx-auto grid max-w-site gap-14 px-6 lg:grid-cols-12 lg:px-10"}>
|
||||||
|
|
||||||
|
{!hideHeading && (
|
||||||
|
<div className="lg:col-span-4">
|
||||||
|
<SectionHeading
|
||||||
|
title="سوالات متداول"
|
||||||
|
lead="پاسخ کوتاه به پرتکرارترین پرسشهای سرمایهگذاران و خریداران."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={hideHeading ? "" : "lg:col-span-8"}>
|
||||||
|
<ul>
|
||||||
|
{FAQS.map((faq, i) => {
|
||||||
|
const isOpen = open === i
|
||||||
|
return (
|
||||||
|
<Reveal key={faq.q} delay={0.04 * i}>
|
||||||
|
<li className="border-b border-ivory/10">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(isOpen ? null : i)}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
className="flex w-full items-center justify-between gap-6 py-6 text-right transition-colors duration-200 ease-lux hover:text-copper"
|
||||||
|
>
|
||||||
|
<span className={`text-base font-bold md:text-lg ${isOpen ? 'text-copper' : 'text-ivory'}`}>
|
||||||
|
{faq.q}
|
||||||
|
</span>
|
||||||
|
<PlusIcon
|
||||||
|
className={`h-5 w-5 shrink-0 text-copper transition-transform duration-300 ease-lux ${
|
||||||
|
isOpen ? 'rotate-45' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<p className="pb-7 pl-10 text-sm font-light leading-9 text-ivory/60">{faq.a}</p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</li>
|
||||||
|
</Reveal>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { ArrowLeftIcon } from 'lucide-react'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import { Reveal } from '../Reveal'
|
||||||
|
import { ButtonLink } from '../Button'
|
||||||
|
import { ProjectCard } from '../ProjectCard'
|
||||||
|
import { SectionHeading } from '../SectionHeading'
|
||||||
|
import { PROJECTS, type ProjectStatus } from '../../data/site'
|
||||||
|
|
||||||
|
const FILTERS: { key: ProjectStatus; label: string }[] = [
|
||||||
|
{ key: 'done', label: 'انجام شده' },
|
||||||
|
{ key: 'progress', label: 'در حال انجام' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function FeaturedProjects() {
|
||||||
|
const [status, setStatus] = useState<ProjectStatus>('done')
|
||||||
|
const list = PROJECTS.filter((p) => p.status === status).slice(0, 3)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-ink py-24 lg:py-32" aria-labelledby="projects-title">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<SectionHeading
|
||||||
|
title="پروژههای شاخص"
|
||||||
|
lead="کارهایی که بیشترین چیز را درباره استاندارد ما میگویند؛ چه آنهایی که تحویل شدهاند و چه آنهایی که همین امروز در حال ساختاند."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-10 flex flex-wrap items-center gap-3" role="tablist" aria-label="فیلتر وضعیت پروژه">
|
||||||
|
{FILTERS.map((filter) => (
|
||||||
|
<button
|
||||||
|
key={filter.key}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={status === filter.key}
|
||||||
|
onClick={() => setStatus(filter.key)}
|
||||||
|
className={`group relative overflow-hidden border px-7 py-3 text-sm transition-colors duration-300 ease-out ${
|
||||||
|
status === filter.key
|
||||||
|
? 'border-copper bg-copper font-bold text-char'
|
||||||
|
: 'border-ivory/15 font-light text-ivory/65'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status !== filter.key && (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 translate-y-full bg-copper transition-transform duration-300 ease-out group-hover:translate-y-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="relative z-10 transition-colors duration-300 ease-out group-hover:text-char">
|
||||||
|
{filter.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-10 min-h-[420px]">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={status}
|
||||||
|
initial={{ opacity: 0, y: 15 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -15 }}
|
||||||
|
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className="grid gap-6 lg:grid-cols-3"
|
||||||
|
>
|
||||||
|
{list.map((project, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={project.slug}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: i * 0.1, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className="h-full"
|
||||||
|
>
|
||||||
|
<ProjectCard project={project} />
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Reveal delay={0.1}>
|
||||||
|
<div className="mt-12 flex justify-center">
|
||||||
|
<ButtonLink to="/projects" variant="outline" className="font-light">
|
||||||
|
همه پروژهها
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
</ButtonLink>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon, XIcon } from 'lucide-react'
|
||||||
|
import { Reveal } from '../Reveal'
|
||||||
|
import { SectionHeading } from '../SectionHeading'
|
||||||
|
import { IMAGES } from '../../data/site'
|
||||||
|
|
||||||
|
const GRID = [IMAGES[0], IMAGES[1], IMAGES[2], IMAGES[3], IMAGES[4], IMAGES[1]]
|
||||||
|
|
||||||
|
export function Gallery() {
|
||||||
|
const [active, setActive] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const close = useCallback(() => setActive(null), [])
|
||||||
|
const step = useCallback((dir: number) => {
|
||||||
|
setActive((prev) => (prev === null ? prev : (prev + dir + GRID.length) % GRID.length))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (active === null) return
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') close()
|
||||||
|
if (e.key === 'ArrowRight') step(-1)
|
||||||
|
if (e.key === 'ArrowLeft') step(1)
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKey)
|
||||||
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
|
}, [active, close, step])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-ink py-24 lg:py-32" aria-labelledby="gallery-title">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<SectionHeading
|
||||||
|
title="گالری تصاویر"
|
||||||
|
lead="قابهایی از کارگاهها و پروژههای تحویلشده آرامش پلازا."
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-14 grid grid-cols-2 gap-4 lg:grid-cols-3">
|
||||||
|
{GRID.map((src, i) => (
|
||||||
|
<Reveal key={`${src}-${i}`} delay={0.04 * i}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActive(i)}
|
||||||
|
className="group block w-full overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-copper"
|
||||||
|
aria-label={`بزرگنمایی تصویر شماره ${(i + 1).toLocaleString('fa-IR')}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={`تصویری از پروژههای آرامش پلازا، شماره ${(i + 1).toLocaleString('fa-IR')}`}
|
||||||
|
className="aspect-[4/3] w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{active !== null && (
|
||||||
|
<motion.div
|
||||||
|
className="fixed inset-0 z-[80] flex items-center justify-center bg-char/80 backdrop-blur-lg p-6"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.5, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="نمایش تصویر"
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={close}
|
||||||
|
className="absolute left-8 top-8 flex h-11 w-11 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper"
|
||||||
|
aria-label="بستن"
|
||||||
|
>
|
||||||
|
<XIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
step(-1)
|
||||||
|
}}
|
||||||
|
className="absolute right-6 flex h-12 w-12 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper md:right-10"
|
||||||
|
aria-label="تصویر قبلی"
|
||||||
|
>
|
||||||
|
<ChevronRightIcon className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
step(1)
|
||||||
|
}}
|
||||||
|
className="absolute left-6 flex h-12 w-12 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper md:left-10"
|
||||||
|
aria-label="تصویر بعدی"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<motion.img
|
||||||
|
key={active}
|
||||||
|
src={GRID[active]}
|
||||||
|
alt=""
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
|
transition={{ duration: 0.5, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className="max-h-[85vh] w-auto max-w-[85vw] object-contain shadow-[0_30px_80px_-20px_rgba(0,0,0,0.9)]"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { motion, useScroll, useTransform } from 'framer-motion'
|
||||||
|
import { ArrowLeftIcon, ChevronDownIcon } from 'lucide-react'
|
||||||
|
import { ButtonLink } from '../Button'
|
||||||
|
import { BRAND } from '../../data/site'
|
||||||
|
|
||||||
|
export function Hero() {
|
||||||
|
const { scrollY } = useScroll()
|
||||||
|
const imageY = useTransform(scrollY, [0, 700], [0, 110])
|
||||||
|
const contentY = useTransform(scrollY, [0, 700], [0, -55])
|
||||||
|
const overlay = useTransform(scrollY, [0, 600], [0.5, 0.78])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative h-screen min-h-[640px] w-full overflow-hidden" aria-label="معرفی آرامش پلازا">
|
||||||
|
<motion.img
|
||||||
|
src={BRAND.hero}
|
||||||
|
alt="نمای شبانه برج مسکونی ساختهشده توسط آرامش پلازا"
|
||||||
|
style={{ y: imageY }}
|
||||||
|
className="absolute inset-0 h-[114%] w-full object-cover"
|
||||||
|
/>
|
||||||
|
<motion.div className="absolute inset-0 bg-ink" style={{ opacity: overlay }} aria-hidden="true" />
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(to top, #27221c 5%, rgba(39,34,28,0.2) 60%, rgba(39,34,28,0.6) 100%)',
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
style={{ y: contentY }}
|
||||||
|
className="relative mx-auto flex h-full max-w-site flex-col justify-center px-6 lg:px-10"
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-[700px] lg:max-w-[58%]">
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 18 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.7, delay: 2.1, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className="text-sm font-light text-copper"
|
||||||
|
>
|
||||||
|
از سال ۱۳۸۳ — تهران، اصفهان، سواحل شمال
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 28 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.85, delay: 2.2, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className="mt-6 text-hero font-bold text-ivory"
|
||||||
|
>
|
||||||
|
ساختمانی که آرامش را
|
||||||
|
<span className="text-copper"> معماری </span>
|
||||||
|
میکند
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 22 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.85, delay: 2.32, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className="mt-7 max-w-xl text-base font-light leading-9 text-ivory/70"
|
||||||
|
>
|
||||||
|
طراحی، ساخت و سرمایهگذاری در پروژههای مسکونی و تجاری لوکس؛ با استانداردی که در جزئیات دیده میشود و در
|
||||||
|
سالها دوام میآورد.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 22 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.85, delay: 2.44, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className="mt-10 flex flex-wrap items-center gap-4"
|
||||||
|
>
|
||||||
|
<ButtonLink to="/projects" variant="copper">
|
||||||
|
مشاهده پروژهها
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
</ButtonLink>
|
||||||
|
<ButtonLink to="/contact" variant="outline" className="font-light">
|
||||||
|
مشاوره سرمایهگذاری
|
||||||
|
</ButtonLink>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -24 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.9, delay: 2.6, ease: [0.23, 1, 0.32, 1] }}
|
||||||
|
className="absolute bottom-28 left-6 hidden w-60 border border-ivory/15 bg-char/60 p-6 backdrop-blur-md lg:block lg:left-10"
|
||||||
|
>
|
||||||
|
<p className="text-3xl font-bold text-copper">۴۷</p>
|
||||||
|
<p className="mt-2 text-sm font-light leading-7 text-ivory/70">
|
||||||
|
پروژه تحویلشده در بیستویک سال فعالیت حرفهای
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#story"
|
||||||
|
aria-label="رفتن به بخش بعدی"
|
||||||
|
className="absolute bottom-8 right-1/2 z-10 flex translate-x-1/2 flex-col items-center gap-2 text-ivory/50 transition-colors duration-200 ease-lux hover:text-copper"
|
||||||
|
>
|
||||||
|
<span className="text-xs font-light">اسکرول کنید</span>
|
||||||
|
<ChevronDownIcon className="h-4 w-4 animate-bounce" />
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { CheckIcon, SendIcon } from 'lucide-react'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { Reveal } from '../Reveal'
|
||||||
|
import { Button } from '../Button'
|
||||||
|
|
||||||
|
export function Newsletter() {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [status, setStatus] = useState<'idle' | 'error' | 'done'>('idle')
|
||||||
|
|
||||||
|
const showErrorAlert = (title: string, text: string) => {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: title,
|
||||||
|
text: text,
|
||||||
|
background: '#1e1a16',
|
||||||
|
color: '#F5F3EF',
|
||||||
|
iconColor: '#d5874c',
|
||||||
|
confirmButtonText: 'متوجه شدم',
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
container: 'backdrop-blur-[8px] bg-ink/60',
|
||||||
|
popup: 'font-pinar border border-ivory/10 rounded-none shadow-lift',
|
||||||
|
title: 'font-pinar font-bold text-lg text-ivory pt-2',
|
||||||
|
htmlContainer: 'font-pinar font-light text-sm leading-8 text-ivory/70',
|
||||||
|
confirmButton: 'mt-6 bg-copper text-char font-bold px-10 py-3 transition-transform duration-300 ease-lux hover:scale-105',
|
||||||
|
},
|
||||||
|
showClass: {
|
||||||
|
popup: 'swal2-show',
|
||||||
|
backdrop: 'swal2-backdrop-show',
|
||||||
|
},
|
||||||
|
hideClass: {
|
||||||
|
popup: 'swal2-hide',
|
||||||
|
backdrop: 'swal2-backdrop-hide',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const emailValue = email.trim()
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
|
||||||
|
if (!emailValue) {
|
||||||
|
setStatus('error')
|
||||||
|
showErrorAlert('اطلاعات ناقص است', 'لطفاً نشانی ایمیل خود را برای عضویت در خبرنامه وارد کنید.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emailRegex.test(emailValue)) {
|
||||||
|
setStatus('error')
|
||||||
|
showErrorAlert('فرمت ایمیل نامعتبر است', 'لطفاً ایمیل خود را به درستی وارد کنید (مثال: example@domain.com).')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('done')
|
||||||
|
setEmail('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-copper py-16" aria-labelledby="newsletter-title">
|
||||||
|
<div className="mx-auto grid max-w-site items-center gap-8 px-6 lg:grid-cols-2 lg:px-10">
|
||||||
|
<Reveal>
|
||||||
|
<h2 id="newsletter-title" className="text-2xl font-bold leading-relaxed text-char md:text-3xl">
|
||||||
|
خبرنامه آرامش پلازا
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 max-w-md text-sm font-light leading-8 text-char/75">
|
||||||
|
هر ماه یک ایمیل: پروژههای جدید، فرصتهای سرمایهگذاری و یادداشتهای تیم معماری.
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={0.08}>
|
||||||
|
<form onSubmit={submit} className="flex flex-col gap-3 sm:flex-row" noValidate>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label htmlFor="newsletter-email" className="sr-only">
|
||||||
|
نشانی ایمیل
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="newsletter-email"
|
||||||
|
type="email"
|
||||||
|
dir="ltr"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => {
|
||||||
|
setEmail(e.target.value)
|
||||||
|
if (status !== 'idle') setStatus('idle')
|
||||||
|
}}
|
||||||
|
placeholder="نشانی ایمیل شما"
|
||||||
|
className="w-full border border-transparent bg-char px-5 py-4 text-left text-sm font-light text-ivory placeholder:text-ivory/40 transition-all duration-300 ease-lux focus:border-ivory focus:outline-none"
|
||||||
|
aria-invalid={status === 'error'}
|
||||||
|
/>
|
||||||
|
{status === 'done' && (
|
||||||
|
<p className="mt-2 flex items-center gap-1.5 text-xs font-light text-char" role="status">
|
||||||
|
<CheckIcon className="h-3.5 w-3.5" />
|
||||||
|
عضویت شما ثبت شد.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" variant="dark-ivory">
|
||||||
|
<SendIcon className="h-4 w-4" />
|
||||||
|
عضویت
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Reveal } from '../Reveal'
|
||||||
|
import { ServiceIcon } from '../ServiceIcon'
|
||||||
|
import { SERVICES } from '../../data/site'
|
||||||
|
|
||||||
|
export function Services() {
|
||||||
|
return (
|
||||||
|
<section className="bg-ink py-24 lg:py-32" aria-labelledby="services-title">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<Reveal className="max-w-2xl text-right">
|
||||||
|
|
||||||
|
<h2 id="services-title" className="text-section font-bold text-ivory">
|
||||||
|
خدمات
|
||||||
|
</h2>
|
||||||
|
<p className="mt-5 text-base font-light leading-9 text-ivory/60">
|
||||||
|
از نخستین خط روی کاغذ تا تحویل کلید، مدیریت سرمایه و طراحی محوطه؛ همه مراحل در یک مجموعه و زیر یک نظام
|
||||||
|
کنترل کیفیت.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 h-px w-24 bg-copper/70" />
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<div className="mt-14 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{SERVICES.map((service, i) => (
|
||||||
|
<Reveal key={service.title} delay={0.04 * i} className="h-full">
|
||||||
|
<article className="group flex h-full flex-col items-center border border-ivory/10 bg-char p-8 text-center transition-all duration-300 ease-out hover:-translate-y-2 hover:border-copper/40 hover:shadow-lift transform-gpu will-change-transform">
|
||||||
|
<ServiceIcon
|
||||||
|
name={service.icon}
|
||||||
|
className="h-12 w-12 text-copper/85 transition-colors duration-300 ease-out group-hover:text-copper"
|
||||||
|
/>
|
||||||
|
<h3 className="mt-6 text-lg font-bold text-ivory transition-colors duration-300 ease-out group-hover:text-copper">
|
||||||
|
{service.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 text-sm font-light leading-8 text-ivory/55">{service.desc}</p>
|
||||||
|
</article>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useInView } from 'framer-motion';
|
||||||
|
import { STATS } from '../../data/site';
|
||||||
|
|
||||||
|
function toFa(n: number) {
|
||||||
|
return n.toLocaleString('fa-IR');
|
||||||
|
}
|
||||||
|
|
||||||
|
function Counter({ value }: {value: number;}) {
|
||||||
|
const ref = useRef<HTMLSpanElement>(null);
|
||||||
|
const inView = useInView(ref, { once: true, margin: '-60px' });
|
||||||
|
const [display, setDisplay] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!inView) return;
|
||||||
|
let frame = 0;
|
||||||
|
const total = 60;
|
||||||
|
const id = window.setInterval(() => {
|
||||||
|
frame += 1;
|
||||||
|
const progress = 1 - Math.pow(1 - frame / total, 3);
|
||||||
|
setDisplay(Math.round(value * progress));
|
||||||
|
if (frame >= total) window.clearInterval(id);
|
||||||
|
}, 20);
|
||||||
|
return () => window.clearInterval(id);
|
||||||
|
}, [inView, value]);
|
||||||
|
|
||||||
|
return <span ref={ref}>{toFa(display)}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Stats() {
|
||||||
|
return (
|
||||||
|
<section className="bg-char py-16" aria-label="آمار و ارقام">
|
||||||
|
<div className="mx-auto grid max-w-site grid-cols-2 gap-y-12 px-6 lg:grid-cols-4 lg:px-10">
|
||||||
|
{STATS.map((stat) =>
|
||||||
|
<div key={stat.label} className="text-center">
|
||||||
|
<p className="text-4xl font-bold text-copper md:text-5xl">
|
||||||
|
<Counter value={stat.value} />
|
||||||
|
{stat.suffix && <span className="mr-1 text-2xl">{stat.suffix}</span>}
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-sm font-light text-ivory/55">{stat.label}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { ArrowLeftIcon } from 'lucide-react'
|
||||||
|
import { Reveal } from '../Reveal'
|
||||||
|
import { ButtonLink } from '../Button'
|
||||||
|
import { IMAGES } from '../../data/site'
|
||||||
|
|
||||||
|
const BENTO = [
|
||||||
|
{ src: IMAGES[0], alt: 'نمای برج مسکونی آرامش در تهران' },
|
||||||
|
{ src: IMAGES[1], alt: 'آتریوم مرکزی مجتمع تجاری پارسه' },
|
||||||
|
{ src: IMAGES[2], alt: 'ویلاهای ساختهشده در سواحل شمال' },
|
||||||
|
{ src: IMAGES[3], alt: 'نمای شبانه برج دوقلوی نیاوران' },
|
||||||
|
]
|
||||||
|
|
||||||
|
type StoryProps = {
|
||||||
|
showLink?: boolean;
|
||||||
|
showLabel?: boolean;
|
||||||
|
alignTop?: boolean;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Story({ showLink = true, showLabel = true, alignTop = false, children }: StoryProps) {
|
||||||
|
return (
|
||||||
|
<section id="story" className="bg-ink py-24 lg:py-32" aria-labelledby="story-title">
|
||||||
|
<div className={`mx-auto grid max-w-site gap-14 px-6 lg:grid-cols-2 lg:px-10 ${alignTop ? 'items-start' : 'items-center'}`}>
|
||||||
|
<div>
|
||||||
|
{showLabel && (
|
||||||
|
<Reveal>
|
||||||
|
<h2 id="story-title" className="text-section font-bold text-ivory">
|
||||||
|
درباره ی ما
|
||||||
|
</h2>
|
||||||
|
<div className="mt-6 h-px w-24 bg-copper/70" />
|
||||||
|
</Reveal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Reveal delay={0.08}>
|
||||||
|
<p className="mt-8 text-base font-light leading-9 text-ivory/65">
|
||||||
|
آرامش پلازا با یک دفتر کوچک معماری آغاز شد و امروز یکی از مجموعههای شناختهشده ساختوساز لوکس در ایران
|
||||||
|
است. در تمام این سالها یک اصل تغییر نکرده است: ساختمان باید برای زندگی ساخته شود، نه برای فروش سریع.
|
||||||
|
</p>
|
||||||
|
<p className="mt-5 text-base font-light leading-9 text-ivory/65">
|
||||||
|
هر پروژه با مطالعه زمین، نور، اقلیم و الگوی زندگی ساکنان آغاز میشود؛ سپس تیم طراحی، سازه و اجرا در یک
|
||||||
|
فرآیند واحد آن را تا تحویل کلید پیش میبرند.
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={0.16}>
|
||||||
|
<div className="mt-10 grid grid-cols-2 gap-8 border-t border-ivory/10 pt-8">
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold text-copper">۹۴٪</p>
|
||||||
|
<p className="mt-2 text-sm font-light text-ivory/55">تحویل بهموقع پروژهها</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold text-copper">۵ سال</p>
|
||||||
|
<p className="mt-2 text-sm font-light text-ivory/55">پشتیبانی پس از تحویل</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
{showLink && (
|
||||||
|
<Reveal delay={0.22}>
|
||||||
|
<ButtonLink to="/about" variant="outline" className="mt-10 font-light">
|
||||||
|
داستان کامل ما
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
</ButtonLink>
|
||||||
|
</Reveal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children && (
|
||||||
|
<Reveal delay={0.24}>
|
||||||
|
{children}
|
||||||
|
</Reveal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{BENTO.map((item, i) => (
|
||||||
|
<Reveal key={item.src + i} delay={0.06 * i}>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={item.src}
|
||||||
|
alt={item.alt}
|
||||||
|
className="aspect-square w-full object-cover transition-transform duration-500 ease-lux hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Reveal } from '../Reveal'
|
||||||
|
import { SectionHeading } from '../SectionHeading'
|
||||||
|
import { TEAM } from '../../data/site'
|
||||||
|
|
||||||
|
export function Team() {
|
||||||
|
return (
|
||||||
|
<section className="bg-ink py-24 lg:py-32" aria-labelledby="team-title">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<SectionHeading
|
||||||
|
title="تیم ما"
|
||||||
|
lead="کسانی که تصمیمهای اصلی هر پروژه را میگیرند و پاسخگوی آن هستند."
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-16 grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{TEAM.map((member, i) => (
|
||||||
|
<Reveal key={member.name} delay={0.06 * i} className="h-full">
|
||||||
|
<article className="group flex h-full flex-col items-center border border-ivory/10 bg-char p-10 text-center transition-[transform,box-shadow,border-color] duration-500 ease-out hover:-translate-y-3 hover:border-copper/50 hover:shadow-lift">
|
||||||
|
<div className="flex h-24 w-24 items-center justify-center rounded-full border border-copper/30 text-3xl font-bold text-copper transition-colors duration-500 group-hover:border-copper">
|
||||||
|
{member.name.trim().charAt(0)}
|
||||||
|
</div>
|
||||||
|
<h3 className="mt-8 text-lg font-bold text-ivory">{member.name}</h3>
|
||||||
|
<p className="mt-2 text-sm font-light text-copper">{member.role}</p>
|
||||||
|
<p className="mt-6 border-t border-ivory/10 pt-6 text-sm font-light text-ivory/60">
|
||||||
|
{member.bio}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon, QuoteIcon } from 'lucide-react';
|
||||||
|
import { SectionHeading } from '../SectionHeading';
|
||||||
|
import { TESTIMONIALS } from '../../data/site';
|
||||||
|
|
||||||
|
export function Testimonials() {
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const item = TESTIMONIALS[index];
|
||||||
|
|
||||||
|
const go = (dir: number) => {
|
||||||
|
setIndex((prev) => (prev + dir + TESTIMONIALS.length) % TESTIMONIALS.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="border-y border-ivory/10 bg-char py-24 lg:py-32" aria-labelledby="testimonials-title">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<SectionHeading title="نظرات مشتریان" align="center" />
|
||||||
|
|
||||||
|
<div className="relative mx-auto mt-14 max-w-3xl text-center">
|
||||||
|
<QuoteIcon className="mx-auto h-8 w-8 text-copper/60" aria-hidden="true" />
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.blockquote
|
||||||
|
key={item.name}
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -12 }}
|
||||||
|
transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}>
|
||||||
|
|
||||||
|
<p className="mt-8 text-lg font-light leading-[2.4rem] text-ivory/85 md:text-xl">«{item.quote}»</p>
|
||||||
|
<footer className="mt-8">
|
||||||
|
<p className="text-base font-bold text-copper">{item.name}</p>
|
||||||
|
<p className="mt-1 text-sm font-light text-ivory/45">{item.role}</p>
|
||||||
|
</footer>
|
||||||
|
</motion.blockquote>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<div className="mt-10 flex items-center justify-center gap-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => go(1)}
|
||||||
|
aria-label="نظر قبلی"
|
||||||
|
className="flex h-11 w-11 items-center justify-center border border-ivory/20 text-ivory/70 transition-colors duration-200 ease-lux hover:border-copper hover:text-copper">
|
||||||
|
|
||||||
|
<ChevronRightIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<div className="flex gap-2" role="tablist" aria-label="انتخاب نظر">
|
||||||
|
{TESTIMONIALS.map((t, i) =>
|
||||||
|
<button
|
||||||
|
key={t.name}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={i === index}
|
||||||
|
aria-label={`نظر ${t.name}`}
|
||||||
|
onClick={() => setIndex(i)}
|
||||||
|
className={`h-1.5 transition-[width,background-color] duration-200 ease-lux ${
|
||||||
|
i === index ? 'w-8 bg-copper' : 'w-3 bg-ivory/25'}`
|
||||||
|
} />
|
||||||
|
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => go(-1)}
|
||||||
|
aria-label="نظر بعدی"
|
||||||
|
className="flex h-11 w-11 items-center justify-center border border-ivory/20 text-ivory/70 transition-colors duration-200 ease-lux hover:border-copper hover:text-copper">
|
||||||
|
|
||||||
|
<ChevronLeftIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import React, { useRef } from 'react'
|
||||||
|
import { motion, useScroll, useTransform, useInView } from 'framer-motion'
|
||||||
|
import { TIMELINE } from '../../data/site'
|
||||||
|
import { SectionHeading } from '../SectionHeading'
|
||||||
|
|
||||||
|
const ease = [0.23, 1, 0.32, 1] as const
|
||||||
|
|
||||||
|
type Milestone = (typeof TIMELINE)[0]
|
||||||
|
|
||||||
|
function Item({ item, index }: { item: Milestone; index: number }) {
|
||||||
|
const ref = useRef<HTMLLIElement>(null)
|
||||||
|
const active = useInView(ref, { once: true, amount: 0.6 })
|
||||||
|
const onRight = index % 2 === 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li ref={ref} className="relative md:grid md:grid-cols-2 md:gap-16">
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`absolute right-[-5px] top-6 z-10 h-2.5 w-2.5 rounded-full transition-all duration-500 ease-lux md:right-1/2 md:translate-x-1/2 ${
|
||||||
|
active
|
||||||
|
? 'bg-copper shadow-[0_0_0_5px_rgba(213,135,76,0.22)]'
|
||||||
|
: 'bg-ivory/25'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className={`pr-8 md:pr-0 ${
|
||||||
|
onRight ? 'md:col-start-1 md:pl-16 md:text-left' : 'md:col-start-2 md:pr-16'
|
||||||
|
}`}
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
animate={active ? { opacity: 1, y: 0 } : { opacity: 0, y: 24 }}
|
||||||
|
transition={{ duration: 0.55, ease }}
|
||||||
|
>
|
||||||
|
<div className="border border-ivory/10 bg-ink p-6 md:p-7">
|
||||||
|
<span className="text-sm font-light text-copper">{item.year}</span>
|
||||||
|
<h3 className="mt-3 text-lg font-bold text-ivory">{item.title}</h3>
|
||||||
|
<p className="mt-3 text-sm font-light leading-8 text-ivory/60">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Timeline() {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: ref,
|
||||||
|
offset: ['start 70%', 'end 60%'],
|
||||||
|
})
|
||||||
|
const scaleY = useTransform(scrollYProgress, [0, 1], [0, 1])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-char py-24 md:py-32" aria-labelledby="timeline-title">
|
||||||
|
<div className="mx-auto w-full max-w-site px-6 lg:px-10">
|
||||||
|
|
||||||
|
<div className="mb-16">
|
||||||
|
<SectionHeading
|
||||||
|
title="مسیر ما"
|
||||||
|
lead="از یک دفتر چهارنفره تا ۴۷ پروژه. خط زمان پروژههای ما را ببینید."
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={ref} className="relative mx-auto max-w-4xl">
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-y-0 right-0 w-px bg-ivory/12 md:right-1/2"
|
||||||
|
/>
|
||||||
|
<motion.span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-y-0 right-0 w-px origin-top bg-copper md:right-1/2"
|
||||||
|
style={{ scaleY }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ol className="flex flex-col gap-12 md:gap-16">
|
||||||
|
{TIMELINE.map((item, i) => (
|
||||||
|
<Item key={item.year} item={item} index={i} />
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Reveal } from '../Reveal';
|
||||||
|
import { SectionHeading } from '../SectionHeading';
|
||||||
|
import { WHY, IMAGES } from '../../data/site';
|
||||||
|
|
||||||
|
export function WhyUs() {
|
||||||
|
return (
|
||||||
|
<section className="bg-ink py-24 lg:py-32" aria-labelledby="why-title">
|
||||||
|
<div className="mx-auto grid max-w-site gap-14 px-6 lg:grid-cols-12 lg:px-10">
|
||||||
|
<div className="lg:col-span-5">
|
||||||
|
<SectionHeading
|
||||||
|
title="چرا آرامش پلازا"
|
||||||
|
lead="شش دلیلی که کارفرمایان و سرمایهگذاران بار دوم هم به ما برمیگردند." />
|
||||||
|
|
||||||
|
<Reveal delay={0.12}>
|
||||||
|
<img
|
||||||
|
src={IMAGES[3]}
|
||||||
|
alt="نمای شبانه برج دوقلوی نیاوران"
|
||||||
|
className="mt-10 h-80 w-full object-cover" />
|
||||||
|
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-7">
|
||||||
|
<div className="grid gap-x-10 gap-y-px sm:grid-cols-2">
|
||||||
|
{WHY.map((item, i) =>
|
||||||
|
<Reveal key={item.title} delay={0.05 * i}>
|
||||||
|
<div className="flex h-full flex-col border-t border-ivory/10 py-8">
|
||||||
|
<span className="text-xs font-light text-copper">۰{(i + 1).toLocaleString('fa-IR')}</span>
|
||||||
|
<h3 className="mt-4 text-lg font-bold text-ivory">{item.title}</h3>
|
||||||
|
<p className="mt-3 text-sm font-light leading-8 text-ivory/55">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
export const BRAND = {
|
||||||
|
name: 'آرامش پلازا',
|
||||||
|
logo: 'https://i.postimg.cc/XYNrsHsM/Chat-GPT-Image-Aug-6-2026-04-34-46-PM.png',
|
||||||
|
hero: 'https://i.postimg.cc/J0bKgQVg/Gemini-Generated-Image-mdh969mdh969mdh9.png',
|
||||||
|
contactBg: 'https://i.postimg.cc/BbccNQHr/Gemini-Generated-Image-dafc2hdafc2hdafc.png',
|
||||||
|
phone: '۰۲۱ ۸۸ ۷۴ ۲۲ ۱۰',
|
||||||
|
phoneHref: 'tel:+982188742210',
|
||||||
|
email: 'info@arameshplaza.com',
|
||||||
|
address: 'تهران، خیابان ولیعصر، برج آرامش، طبقه هجدهم'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IMAGES = [
|
||||||
|
'https://i.postimg.cc/wTQ0QFkG/Gemini-Generated-Image-1tjszd1tjszd1tjs.png',
|
||||||
|
'https://i.postimg.cc/1zfBPS1G/Gemini-Generated-Image-at69yhat69yhat69.png',
|
||||||
|
'https://i.postimg.cc/BbccNQHr/Gemini-Generated-Image-dafc2hdafc2hdafc.png',
|
||||||
|
'https://i.postimg.cc/4xFvW0HX/Gemini-Generated-Image-n7mhonn7mhonn7mh.png',
|
||||||
|
'https://i.postimg.cc/N036cr4s/Gemini-Generated-Image-ytbhvvytbhvvytbh.png'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NAV = [
|
||||||
|
{ label: 'صفحه اصلی', to: '/' },
|
||||||
|
{ label: 'درباره ما', to: '/about' },
|
||||||
|
{ label: 'خدمات', to: '/services' },
|
||||||
|
{ label: 'پروژهها', to: '/projects' },
|
||||||
|
{ label: 'سرمایهگذاری', to: '/investment' },
|
||||||
|
{ label: 'وبلاگ', to: '/blog' },
|
||||||
|
{ label: 'سوالات متداول', to: '/faq' },
|
||||||
|
{ label: 'تماس با ما', to: '/contact' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export type Service = {
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
icon: 'draft' | 'tower' | 'mall' | 'villa' | 'renew' | 'manage' | 'invest' | 'landscape';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SERVICES: Service[] = [
|
||||||
|
{ title: 'طراحی معماری', desc: 'خلق فرمهای معاصر با نگاه ایرانی؛ از کانسپت اولیه تا نقشههای اجرایی و جزئیات نما.', icon: 'draft' },
|
||||||
|
{ title: 'ساخت برجهای مسکونی', desc: 'اجرای برجهای بلندمرتبه با استانداردهای سازهای بینالمللی و کیفیت تحویل بینقص.', icon: 'tower' },
|
||||||
|
{ title: 'مجتمعهای تجاری', desc: 'طراحی و ساخت مراکز تجاری با مطالعه دقیق جریان مخاطب و بازدهی بلندمدت سرمایه.', icon: 'mall' },
|
||||||
|
{ title: 'ویلاسازی', desc: 'ویلاهای اختصاصی همنشین با طبیعت؛ محرمانگی، نور طبیعی و مصالح ماندگار.', icon: 'villa' },
|
||||||
|
{ title: 'بازسازی', desc: 'بازآفرینی ساختمانهای موجود با ارتقای سازه، نما و تجربه زندگی در فضا.', icon: 'renew' },
|
||||||
|
{ title: 'مدیریت پروژه', desc: 'کنترل همزمان زمان، هزینه و کیفیت با گزارشدهی شفاف در تمام مراحل ساخت.', icon: 'manage' },
|
||||||
|
{ title: 'سرمایهگذاری ساختمانی', desc: 'ساختارهای مشارکت شفاف، ارزیابی ریسک و طراحی سبد ملکی متناسب با سرمایه شما.', icon: 'invest' },
|
||||||
|
{ title: 'محوطهسازی و لنداسکیپ', desc: 'طراحی حیاط، بام سبز و مسیرهای دسترسی؛ ادامه معماری در فضای باز پروژه.', icon: 'landscape' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const STATS = [
|
||||||
|
{ value: 21, suffix: '+', label: 'سال تجربه ساخت' },
|
||||||
|
{ value: 47, suffix: '', label: 'پروژه تحویلشده' },
|
||||||
|
{ value: 860, suffix: 'هزار', label: 'مترمربع زیربنا' },
|
||||||
|
{ value: 1200, suffix: '+', label: 'خانواده ساکن' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export type ProjectStatus = 'done' | 'progress';
|
||||||
|
|
||||||
|
export type Project = {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
location: string;
|
||||||
|
year: string;
|
||||||
|
status: ProjectStatus;
|
||||||
|
image: string;
|
||||||
|
gallery: string[];
|
||||||
|
area: string;
|
||||||
|
floors: string;
|
||||||
|
units: string;
|
||||||
|
client: string;
|
||||||
|
summary: string;
|
||||||
|
body: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const STATUS_LABEL: Record<ProjectStatus, string> = {
|
||||||
|
done: 'انجام شده',
|
||||||
|
progress: 'در حال انجام'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROJECTS: Project[] = [
|
||||||
|
{ slug: 'aramesh-elahieh', title: 'برج آرامش الهیه', location: 'تهران، الهیه', year: '۱۴۰۲', status: 'done', image: IMAGES[0], gallery: [IMAGES[0], IMAGES[3], IMAGES[1]], area: '۲۴٬۰۰۰ متر مربع', floors: '۱۸ طبقه', units: '۵۶ واحد', client: 'سرمایهگذاری خصوصی', summary: 'یک برج مسکونی لوکس با نمای سنگ تراورتن و پوسته دوپوستهای که مصرف انرژی را تا ۳۵ درصد کاهش داده است.', body: ['برج آرامش الهیه روی زمینی با شیب طبیعی شمالی طراحی شد؛ همین شیب فرصت داد تا لابی، استخر و فضاهای مشترک در سه تراز مختلف بنشینند و همه واحدها دید بیواسطه به شهر داشته باشند.', 'پوسته بیرونی ترکیبی از سنگ تراورتن روشن و پنجرههای قدی با شیشه سهجداره است. سایهبانهای عمودی مسیرنگ، نور مستقیم تابستان را میشکنند و در زمستان اجازه ورود کامل نور را میدهند.', 'اجرای پروژه در سیودو ماه به پایان رسید و تمام واحدها پیش از تحویل کلید به فروش رسیدند.'] },
|
||||||
|
{ slug: 'parseh-mall', title: 'مجتمع تجاری پارسه', location: 'اصفهان، مرکز شهر', year: '۱۴۰۱', status: 'done', image: IMAGES[1], gallery: [IMAGES[1], IMAGES[2], IMAGES[4]], area: '۳۸٬۵۰۰ متر مربع', floors: '۷ طبقه', units: '۱۴۰ واحد تجاری', client: 'گروه سرمایهگذاری پارسه', summary: 'مرکز تجاری با آتریوم مرکزی و نورگیر سقفی که جریان مخاطب را بهصورت طبیعی در همه ترازها توزیع میکند.', body: ['مطالعه رفتار مخاطب پیش از طراحی نشان داد کوتاهترین مسیر عمودی، کلید موفقیت تجاری این مجموعه است؛ بنابراین آتریوم مرکزی با پلهبرقیهای قطری در قلب پلان قرار گرفت.', 'نورگیر سقفی با شیشه لو-ای، روشنایی روز را تا تراز منفی دو میرساند و مصرف روشنایی مصنوعی را بهطور محسوس کاهش میدهد.', 'مجموعه دو ماه پیش از موعد قراردادی به بهرهبرداری رسید.'] },
|
||||||
|
{ slug: 'daryakenar-villas', title: 'ویلاهای دریاکنار', location: 'مازندران، نوشهر', year: '۱۴۰۰', status: 'done', image: IMAGES[2], gallery: [IMAGES[2], IMAGES[4], IMAGES[0]], area: '۹٬۲۰۰ متر مربع', floors: '۲ طبقه', units: '۱۲ ویلا', client: 'مجموعه خصوصی دریاکنار', summary: 'دوازده ویلای اختصاصی با چیدمان شانهای که برای هر واحد، دید مستقیم به دریا و محرمانگی کامل فراهم میکند.', body: ['چیدمان شانهای ویلاها نتیجه دهها بررسی زاویه دید بود؛ هیچ ویلایی در دید ویلای دیگر نیست و همه به دریا باز میشوند.', 'مصالح، پاسخی به اقلیم مرطوب شمال است: چوب ترموود، بتن اکسپوز و پوششهای ضد نمک برای فلزات نما.', 'محوطه با گیاهان بومی طراحی شد تا نیاز آبیاری در طول سال حداقل باشد.'] },
|
||||||
|
{ slug: 'niavaran-twins', title: 'برج دوقلوی نیاوران', location: 'تهران، نیاوران', year: '۱۳۹۹', status: 'done', image: IMAGES[3], gallery: [IMAGES[3], IMAGES[0], IMAGES[2]], area: '۳۱٬۷۰۰ متر مربع', floors: '۲۰ طبقه', units: '۸۰ واحد', client: 'مشارکت با مالکان زمین', summary: 'دو برج همسان با پادست مشترک که فضاهای رفاهی و پارکینگها را در یک تراز واحد ادغام میکند.', body: ['دو برج روی یک پادست مشترک نشستهاند؛ این تصمیم اجازه داد فضاهای رفاهی، سالن اجتماعات و پارکینگها بین دو ساختمان مشترک شوند و سطح اشغال باغ کاهش یابد.', 'سازه با سیستم دیوار برشی و دال پسکشیده اجرا شد تا دهانههای داخلی آزاد و پلانها قابل شخصیسازی باشند.'] },
|
||||||
|
{ slug: 'kish-garden-tower', title: 'باغبرج کیش', location: 'کیش، مروارید', year: '۱۴۰۵ (پیشبینی)', status: 'progress', image: IMAGES[4], gallery: [IMAGES[4], IMAGES[1], IMAGES[3]], area: '۱۸٬۴۰۰ متر مربع', floors: '۱۴ طبقه', units: '۶۲ واحد اقامتی', client: 'صندوق پروژهمحور آرامش', summary: 'باغبرجی اقامتی با تراسهای سبز پیوسته که در حال ساخت است؛ پیشرفت فیزیکی پروژه ۴۸ درصد.', body: ['تراسهای سبز پیوسته، پوستهای زنده برای ساختمان میسازند که در اقلیم گرم جزیره، دمای سطح نما را بهطور محسوس پایین میآورد.', 'در حال حاضر سازه تا تراز طبقه نهم اجرا شده و نصب نما از ضلع جنوبی آغاز شده است.'] },
|
||||||
|
{ slug: 'aramesh-business-center', title: 'مرکز تجاری آرامش تهران', location: 'تهران، سعادتآباد', year: '۱۴۰۶ (پیشبینی)', status: 'progress', image: IMAGES[1], gallery: [IMAGES[1], IMAGES[3], IMAGES[4]], area: '۲۷٬۳۰۰ متر مربع', floors: '۱۲ طبقه', units: '۹۰ دفتر اداری', client: 'مشارکت با هلدینگ آرامش', summary: 'مجتمع اداری-تجاری با هسته مرکزی شفاف و لابی دوبلکس؛ در مرحله اجرای سفتکاری.', body: ['برنامهریزی فضایی این مجموعه بر اساس دفاتر انعطافپذیر انجام شد؛ هر تراز میتواند به دو، سه یا شش دفتر مستقل تقسیم شود.', 'اجرای تأسیسات مکانیکی بهصورت همزمان با سفتکاری پیش میرود تا زمان کل پروژه کوتاهتر شود.'] }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BEFORE_AFTER = {
|
||||||
|
title: 'از کانسپت تا خلق واقعیت',
|
||||||
|
lead: 'دستگیره را بکشید و ببینید نمای ساختمانی چگونه از حالت اولیه به فرم مدرن تبدیل میشود.',
|
||||||
|
before: 'https://cdn.housedesign.ai/se/housedesign/assets/housedesign-home/compare/1_before.webp',
|
||||||
|
after: 'https://cdn.housedesign.ai/se/housedesign/assets/housedesign-home/compare/1_after.webp',
|
||||||
|
beforeLabel: 'کانسپت اولیه',
|
||||||
|
afterLabel: 'پروژه تکمیلشده',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TIMELINE = [
|
||||||
|
{ year: '۱۳۸۳', title: 'آغاز مسیر', desc: 'تأسیس دفتر معماری آرامش با تیمی چهارنفره و نخستین پروژه بازسازی در تهران.', image: IMAGES[2] },
|
||||||
|
{ year: '۱۳۸۵', title: 'نخستین ساخت مستقل', desc: 'تحویل اولین ساختمان مسکونی هفتطبقه؛ آغاز همکاری با پیمانکاران سازهای بزرگ.', image: IMAGES[0] },
|
||||||
|
{ year: '۱۳۸۷', title: 'ورود به بازار تجاری', desc: 'طراحی و اجرای اولین مجتمع تجاری با رویکرد بازدهی سرمایه و جریان مخاطب.', image: IMAGES[1] },
|
||||||
|
{ year: '۱۳۸۹', title: 'استاندارد بینالمللی', desc: 'استقرار نظام کنترل کیفیت و دریافت گواهینامههای مدیریت پروژه.', image: IMAGES[4] },
|
||||||
|
{ year: '۱۳۹۵', title: 'بلندمرتبهسازی', desc: 'آغاز ساخت نخستین برج بیستطبقه شرکت در شمال تهران.', image: IMAGES[3] },
|
||||||
|
{ year: '۱۴۰۳', title: 'آرامش پلازا امروز', desc: 'پرتفوی ۴۷ پروژه، دفاتر در سه شهر و صندوق سرمایهگذاری ساختمانی اختصاصی.', image: IMAGES[0] }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const WHY = [
|
||||||
|
{ title: 'کیفیت بدون مصالحه', desc: 'انتخاب مصالح و مجریان بر اساس ماندگاری چنددههای، نه سریعترین تحویل.' },
|
||||||
|
{ title: 'شفافیت مالی کامل', desc: 'گزارش ماهانه هزینهها و پیشرفت فیزیکی برای همه شرکای پروژه.' },
|
||||||
|
{ title: 'تحویل بهموقع', desc: '۹۴٪ پروژههای ما در بازه زمانی قراردادی یا زودتر تحویل داده شدهاند.' },
|
||||||
|
{ title: 'طراحی اختصاصی', desc: 'هیچ نقشهای دو بار اجرا نمیشود؛ هر پروژه هویت معماری خودش را دارد.' },
|
||||||
|
{ title: 'خدمات پس از تحویل', desc: 'پنج سال پشتیبانی فنی و نگهداری تأسیسات پس از تحویل کلید.' },
|
||||||
|
{ title: 'پایداری و انرژی', desc: 'طراحی پوسته و تأسیسات با هدف کاهش مصرف انرژی تا ۳۵ درصد.' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TESTIMONIALS = [
|
||||||
|
{ name: 'مهندس سارا کیانی', role: 'سرمایهگذار پروژه پارسه', quote: 'گزارشهای ماهانه آرامش پلازا آنقدر دقیق بود که هیچوقت نیاز نشد نگران سرمایهام باشم. پروژه دو ماه زودتر از موعد به بهرهبرداری رسید.' },
|
||||||
|
{ name: 'دکتر امیرحسین راد', role: 'ساکن برج الهیه', quote: 'کیفیت اجرا در جزئیاتی دیده میشود که معمولاً کسی به آنها فکر نمیکند؛ از عایق صوتی پنجرهها تا نور غیرمستقیم راهروها.' },
|
||||||
|
{ name: 'مریم دلاور', role: 'مدیر مجموعه ویلاهای دریاکنار', quote: 'همکاری با تیم طراحی، تجربهای واقعاً حرفهای بود. ایدههای ما را شنیدند و به زبان معماری ترجمه کردند.' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TEAM = [
|
||||||
|
{ name: 'کامران آرامش', role: 'بنیانگذار و مدیرعامل', bio: 'بیش از سه دهه تجربه در ساخت بلندمرتبه.' },
|
||||||
|
{ name: 'نگار شریفی', role: 'مدیر ارشد طراحی', bio: 'معمار ارشد، فارغالتحصیل دانشگاه تهران و میلان.' },
|
||||||
|
{ name: 'بهزاد مرادی', role: 'مدیر اجرایی پروژهها', bio: 'مسئول کنترل کیفیت و زمانبندی همه کارگاهها.' },
|
||||||
|
{ name: 'الهام نیکآیین', role: 'مدیر سرمایهگذاری', bio: 'طراح ساختارهای مشارکت و تحلیل بازدهی ملکی.' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export type Article = {
|
||||||
|
slug: string; title: string; date: string; category: string; image: string; read: string; excerpt: string; body: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ARTICLES: Article[] = [
|
||||||
|
{ slug: 'natural-light', title: 'معماری آرام: چرا نور طبیعی گرانترین مصالح ساختمان است؟', date: '۲۴ مرداد ۱۴۰۳', category: 'معماری', image: IMAGES[1], read: '۶ دقیقه', excerpt: 'نور طبیعی نهتنها کیفیت زندگی در فضا را تعیین میکند، بلکه مستقیماً بر مصرف انرژی و ارزش بلندمدت ملک اثر میگذارد.', body: ['در بازار ساختوساز، ارزش یک واحد اغلب با متراژ سنجیده میشود؛ اما تجربه ما در بیست سال گذشته نشان داده کیفیت نور، تعیینکنندهتر از چند متر اضافه است.', 'جهتگیری ساختمان، عمق دهانه و ارتفاع کف تا سقف، سه متغیر اصلی نور طبیعی هستند. تصمیم درباره هر سه، پیش از ترسیم اولین پلان گرفته میشود و بعد از آن تقریباً برگشتپذیر نیست.', 'در برج آرامش الهیه، چرخش ۱۲ درجهای توده ساختمان نسبت به شمال، دو ساعت نور مفید بیشتر در روزهای زمستان به واحدها اضافه کرد؛ تصمیمی بیهزینه با اثری چنددههای.', 'برای انتخاب یک واحد، پیشنهاد ما ساده است: بازدید را در ساعتهای میانی صبح و اواخر بعدازظهر انجام دهید و کیفیت نور را در همان ساعتهایی بسنجید که واقعاً در خانه هستید.'] },
|
||||||
|
{ slug: 'real-estate-portfolio', title: 'راهنمای انتخاب سبد سرمایهگذاری ملکی در بازار امروز', date: '۱۰ مرداد ۱۴۰۳', category: 'سرمایهگذاری', image: IMAGES[3], read: '۹ دقیقه', excerpt: 'سه معیار کلیدی برای تشکیل سبدی متعادل از دارایی ملکی، بر اساس افق زمانی و سطح ریسک شما.', body: ['سبد ملکی متعادل، ترکیبی از داراییهای با نقدشوندگی متفاوت است؛ نه همه سرمایه در یک پروژه بلندمدت، نه همه در واحدهای آماده.', 'افق زمانی، نخستین معیار است. اگر برنامه شما کمتر از دو سال است، پیشخرید در پروژهای که تازه شروع شده انتخاب پرریسکی خواهد بود.', 'معیار دوم شفافیت گزارشدهی است. بدون گزارش ماهانه پیشرفت فیزیکی و مالی، ارزیابی وضعیت سرمایه شما عملاً غیرممکن است.', 'معیار سوم، تنوع کاربری است. ترکیب مسکونی و تجاری، نوسان بازدهی سبد را در دورههای رکود محسوس کاهش میدهد.'] },
|
||||||
|
{ slug: 'facade-quality', title: 'پنج نکته فنی که کیفیت نما را پس از ده سال مشخص میکند', date: '۲۸ تیر ۱۴۰۳', category: 'اجرا', image: IMAGES[4], read: '۵ دقیقه', excerpt: 'نمای ساختمان در سال اول زیبا به نظر میرسد؛ تفاوت کیفیت اجرا در سال دهم آشکار میشود.', body: ['اتصالات مکانیکی سنگ نما، مهمتر از خود سنگ است. اتصال چسبی در اقلیم سردوگرم ایران عمر کوتاهی دارد.', 'درزهای انقباضی باید در نقشه دیده شوند، نه در کارگاه تصمیمگیری شوند؛ نبودشان اولین دلیل ترکهای افقی نما است.', 'آببندی دور پنجره، پرتکرارترین نقطه شکست است. اجرای نوار آببند پیش از نصب قرنیز، این ریسک را حذف میکند.', 'زیرسازی گالوانیزه گرم، در برابر زیرسازی رنگشده، تفاوت قیمت اندکی دارد و تفاوت عمر بسیار زیادی.', 'در پایان، برنامه بازدید دورهای سالانه نما را جدی بگیرید؛ ترمیم یک درز، هزینهای در حد هیچ در برابر بازسازی کل نما است.'] },
|
||||||
|
{ slug: 'commercial-yield', title: 'چگونه بازدهی یک پروژه تجاری را پیش از ساخت تخمین بزنیم؟', date: '۱۵ تیر ۱۴۰۳', category: 'سرمایهگذاری', image: IMAGES[2], read: '۷ دقیقه', excerpt: 'مدل سادهای برای برآورد بازدهی، بر پایه جریان مخاطب، ترکیب واحدها و نرخ اجاره منطقه.', body: ['برآورد بازدهی تجاری با سه عدد آغاز میشود: جریان روزانه مخاطب، نرخ اجاره هر مترمربع در منطقه و نسبت سطح مفید به کل.', 'خطای رایج، نادیدهگرفتن ترازهای بالاتر است. بدون طراحی درست مسیر عمودی، ترازهای سه به بالا معمولاً نیمی از نرخ تراز همکف را هم بهدست نمیآورند.', 'در نهایت، سناریوی محافظهکارانه را هم بنویسید؛ اگر پروژه در سناریوی بدبینانه هم توجیه داشت، سرمایهگذاری منطقی است.'] },
|
||||||
|
{ slug: 'facade-materials', title: 'مصالح نما: مقایسه سنگ، کامپوزیت و ترموود در اقلیم ایران', date: '۲ تیر ۱۴۰۳', category: 'اجرا', image: IMAGES[0], read: '۸ دقیقه', excerpt: 'هر مصالح نما در اقلیم متفاوتی درست کار میکند؛ انتخاب اشتباه، هزینه نگهداری را چند برابر میکند.', body: ['سنگ در اقلیم خشک، بهترین ماندگاری را دارد؛ اما وزن آن نیازمند زیرسازی دقیق و محاسبه سازهای است.', 'کامپوزیت انتخاب سریع و سبکی است، مشروط بر انتخاب گرید ضد آتش و درنظرگرفتن انقباض حرارتی در طول ورق.', 'ترموود در اقلیم مرطوب شمال، بهترین پاسخ زیباییشناسانه است، ولی برنامه نگهداری دوسالانه دارد.'] },
|
||||||
|
{ slug: 'renovate-or-rebuild', title: 'بازسازی یا نوسازی؟ معیارهای تصمیمگیری برای مالکان', date: '۲۰ خرداد ۱۴۰۳', category: 'معماری', image: IMAGES[1], read: '۶ دقیقه', excerpt: 'اگر عمر سازه و ضوابط شهری اجازه دهند، بازسازی میتواند بازدهی بالاتری از تخریب کامل داشته باشد.', body: ['نخست وضعیت سازه ارزیابی میشود؛ اگر مقاومسازی هزینهای بیش از ۴۰ درصد ساخت نو دارد، نوسازی منطقیتر است.', 'دومین معیار، ضوابط شهری روز است. گاهی تخریب، سطح زیربنای مجاز کمتری به شما میدهد و این تصمیم را کاملاً تغییر میدهد.', 'در بازسازی، بیشترین ارزش افزوده از سه جا میآید: نما، ورودی و بازآرایی پلان داخلی.'] }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FAQS = [
|
||||||
|
{ q: 'حداقل سرمایه برای مشارکت در پروژههای آرامش پلازا چقدر است؟', a: 'ساختار مشارکت هر پروژه متفاوت است، اما معمولاً واحدهای سرمایهگذاری از سطح مشخصی آغاز میشوند و جزئیات دقیق در جلسه ارزیابی اولیه ارائه میشود.' },
|
||||||
|
{ q: 'مدت زمان معمول ساخت یک برج مسکونی چقدر است؟', a: 'بسته به تعداد طبقات و شرایط زمین، میانگین اجرای یک برج مسکونی بین ۲۸ تا ۴۲ ماه از زمان صدور پروانه تا تحویل کلید است.' },
|
||||||
|
{ q: 'آیا امکان تغییر نقشه داخلی واحد وجود دارد؟', a: 'بله. تا پیش از پایان مرحله سفتکاری، امکان شخصیسازی پلان داخلی با هماهنگی تیم طراحی و تأیید سازه فراهم است.' },
|
||||||
|
{ q: 'خدمات پس از تحویل شامل چه مواردی میشود؟', a: 'پنج سال پشتیبانی فنی تأسیسات مکانیکی و برقی، بازدید دورهای سالانه و تضمین اجرای نما و عایقکاری.' },
|
||||||
|
{ q: 'پروژهها در چه شهرهایی اجرا میشوند؟', a: 'در حال حاضر تهران، اصفهان و سواحل شمالی کشور مناطق اصلی فعالیت ما هستند و پروژهای نیز در جزیره کیش در دست ساخت است.' }
|
||||||
|
];
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/* @import url() FONT IMPORTS MUST ALWAYS BE AT THE VERY TOP OF THIS FILE, ABOVE THE TAILWIND IMPORTS — DO NOT DELETE THIS COMMENT */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100..900&display=swap');
|
||||||
|
|
||||||
|
/* CRITICAL: THE FOLLOWING TAILWIND IMPORTS MUST NEVER BE DELETED OR REORDERED — DO NOT DELETE THIS COMMENT */
|
||||||
|
@import 'tailwindcss/base';
|
||||||
|
@import 'tailwindcss/components';
|
||||||
|
@import 'tailwindcss/utilities';
|
||||||
|
|
||||||
|
/* END TAILWIND IMPORTS — ALL OTHER CSS MUST GO BELOW THIS LINE */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Pinar';
|
||||||
|
src: url('https://cdn.fontcdn.ir/Font/Persian/Pinar/Pinar.woff2') format('woff2'),
|
||||||
|
url('https://cdn.fontcdn.ir/Font/Persian/Pinar/Pinar.woff') format('woff');
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Pinar';
|
||||||
|
src: url('https://cdn.fontcdn.ir/Font/Persian/Pinar/Pinar-Bold.woff2') format('woff2'),
|
||||||
|
url('https://cdn.fontcdn.ir/Font/Persian/Pinar/Pinar-Bold.woff') format('woff');
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #27221c;
|
||||||
|
color: #f5f3ef;
|
||||||
|
font-family: 'Pinar', 'Vazirmatn', Tahoma, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
::selection {
|
||||||
|
background-color: #d5874c;
|
||||||
|
color: #1e1a16;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import "./index.css";
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { App } from "./App";
|
||||||
|
|
||||||
|
const rootEl = document.getElementById("root");
|
||||||
|
if (rootEl) {
|
||||||
|
ReactDOM.createRoot(rootEl).render(<App />);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "magic-patterns-project",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"react-router-dom": "6.30.2",
|
||||||
|
"lucide-react": "0.577.0",
|
||||||
|
"framer-motion": "11.18.2",
|
||||||
|
"@radix-ui/react-icons": "1.3.2",
|
||||||
|
"date-fns": "4.1.0",
|
||||||
|
"tailwind-merge": "2.6.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { PageHero } from '../components/PageHero'
|
||||||
|
import { Story } from '../components/home/Story'
|
||||||
|
import { Stats } from '../components/home/Stats'
|
||||||
|
import { Timeline } from '../components/home/Timeline'
|
||||||
|
import { WhyUs } from '../components/home/WhyUs'
|
||||||
|
import { Team } from '../components/home/Team'
|
||||||
|
import { ContactSection } from '../components/ContactSection'
|
||||||
|
import { IMAGES } from '../data/site'
|
||||||
|
|
||||||
|
export function About() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHero
|
||||||
|
title="درباره ما"
|
||||||
|
lead="آرامش پلازا مجموعهای از معماران، مهندسان سازه و مدیران پروژه است که از سال ۱۳۸۳ در حوزه ساختوساز لوکس فعالیت میکند."
|
||||||
|
image={IMAGES[3]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Story showLink={false} showLabel={false} alignTop={true}>
|
||||||
|
<div className="mt-12 border-r-2 border-copper bg-char/40 p-6">
|
||||||
|
<p className="text-sm font-light leading-8 text-ivory/80">
|
||||||
|
«ساختمان برای ما فقط آجر و بتن نیست؛ بلکه تعهدی است به کیفیت زندگی ساکنان. ما فضاهایی میسازیم که در گذر زمان ارزش و هویت خود را حفظ کنند.»
|
||||||
|
</p>
|
||||||
|
<p className="mt-5 text-sm font-bold text-copper">کامران آرامش — مدیرعامل شرکت ساختمانی آرامش</p>
|
||||||
|
</div>
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Stats />
|
||||||
|
<Timeline />
|
||||||
|
<WhyUs />
|
||||||
|
<Team />
|
||||||
|
<ContactSection />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
import { ArrowLeftIcon, ChevronLeftIcon, ClockIcon } from 'lucide-react';
|
||||||
|
import { Reveal } from '../components/Reveal';
|
||||||
|
import { ButtonLink } from '../components/Button';
|
||||||
|
import { ArticleCard } from '../components/ArticleCard';
|
||||||
|
import { Newsletter } from '../components/home/Newsletter';
|
||||||
|
import { ARTICLES } from '../data/site';
|
||||||
|
|
||||||
|
export function ArticleDetail() {
|
||||||
|
const { slug } = useParams<{slug: string;}>();
|
||||||
|
const article = ARTICLES.find((a) => a.slug === slug);
|
||||||
|
|
||||||
|
if (!article) {
|
||||||
|
return (
|
||||||
|
<section className="flex min-h-[70vh] flex-col items-center justify-center px-6 text-center">
|
||||||
|
<h1 className="text-section font-bold text-ivory">این مقاله پیدا نشد</h1>
|
||||||
|
<p className="mt-4 text-sm font-light text-ivory/60">فهرست کامل مقالات وبلاگ را ببینید.</p>
|
||||||
|
<ButtonLink to="/blog" variant="copper" className="mt-8">
|
||||||
|
بازگشت به وبلاگ
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
</ButtonLink>
|
||||||
|
</section>);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const related = ARTICLES.filter((a) => a.slug !== article.slug).slice(0, 3);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="relative flex h-[56vh] min-h-[400px] items-end overflow-hidden">
|
||||||
|
<img src={article.image} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover" />
|
||||||
|
<div className="absolute inset-0 bg-ink/75" aria-hidden="true" />
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{ background: 'linear-gradient(to top, #27221c 6%, rgba(39,34,28,0.15) 70%)' }}
|
||||||
|
aria-hidden="true" />
|
||||||
|
|
||||||
|
<div className="relative mx-auto w-full max-w-site px-6 pb-16 lg:px-10">
|
||||||
|
<nav aria-label="مسیر صفحه" className="flex items-center gap-2 text-xs font-light text-ivory/50">
|
||||||
|
<Link to="/" className="transition-colors hover:text-copper">
|
||||||
|
صفحه اصلی
|
||||||
|
</Link>
|
||||||
|
<ChevronLeftIcon className="h-3.5 w-3.5" />
|
||||||
|
<Link to="/blog" className="transition-colors hover:text-copper">
|
||||||
|
وبلاگ
|
||||||
|
</Link>
|
||||||
|
<ChevronLeftIcon className="h-3.5 w-3.5" />
|
||||||
|
<span className="text-copper">{article.category}</span>
|
||||||
|
</nav>
|
||||||
|
<h1 className="mt-5 max-w-[700px] text-hero font-bold text-ivory">{article.title}</h1>
|
||||||
|
<div className="mt-6 flex items-center gap-5 text-xs font-light text-ivory/60">
|
||||||
|
<span>{article.date}</span>
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<ClockIcon className="h-3.5 w-3.5" />
|
||||||
|
{article.read}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<article className="bg-ink py-20 lg:py-28">
|
||||||
|
<div className="mx-auto max-w-3xl px-6">
|
||||||
|
<Reveal>
|
||||||
|
<p className="border-r-2 border-copper pr-6 text-lg font-light leading-10 text-ivory/80">
|
||||||
|
{article.excerpt}
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
{article.body.map((paragraph, i) =>
|
||||||
|
<Reveal key={paragraph} delay={0.03 * i}>
|
||||||
|
<p className="mt-8 text-base font-light leading-10 text-ivory/70">{paragraph}</p>
|
||||||
|
</Reveal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<section className="bg-char py-20 lg:py-28" aria-label="مقالات مرتبط">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<h2 className="text-section font-bold text-ivory">مقالات مرتبط</h2>
|
||||||
|
<div className="mt-6 h-px w-24 bg-copper/70" />
|
||||||
|
<div className="mt-12 grid gap-6 lg:grid-cols-3">
|
||||||
|
{related.map((item, i) =>
|
||||||
|
<Reveal key={item.slug} delay={0.06 * i} className="h-full">
|
||||||
|
<ArticleCard article={item} />
|
||||||
|
</Reveal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Newsletter />
|
||||||
|
</>);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { ArrowLeftIcon, ClockIcon } from 'lucide-react'
|
||||||
|
import { PageHero } from '../components/PageHero'
|
||||||
|
import { Reveal } from '../components/Reveal'
|
||||||
|
import { ArticleCard } from '../components/ArticleCard'
|
||||||
|
import { Newsletter } from '../components/home/Newsletter'
|
||||||
|
import { ARTICLES, IMAGES } from '../data/site'
|
||||||
|
|
||||||
|
export function Blog() {
|
||||||
|
const [featured, ...rest] = ARTICLES
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHero
|
||||||
|
title="وبلاگ"
|
||||||
|
lead="یادداشتهای تیم معماری، اجرا و سرمایهگذاری آرامش پلازا درباره آنچه در کارگاه و بازار میگذرد."
|
||||||
|
image={IMAGES[2]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section className="bg-ink py-24 lg:py-32" aria-label="مقالات">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<Reveal>
|
||||||
|
<Link
|
||||||
|
to={`/blog/${featured.slug}`}
|
||||||
|
className="group grid items-stretch border border-ivory/10 bg-char transition-[transform,box-shadow,border-color] duration-300 ease-lux hover:-translate-y-2 hover:border-copper/50 hover:shadow-lift lg:grid-cols-2"
|
||||||
|
aria-label={`خواندن مقاله ${featured.title}`}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={featured.image}
|
||||||
|
alt={`تصویر مقاله ${featured.title}`}
|
||||||
|
className="h-full min-h-[20rem] w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-center p-8 lg:p-12">
|
||||||
|
<p className="text-xs font-light text-copper">مقاله ویژه — {featured.category}</p>
|
||||||
|
<h2 className="mt-6 text-2xl font-bold leading-[2.2] md:leading-[2.2] text-ivory transition-colors duration-200 ease-lux group-hover:text-copper md:text-3xl">
|
||||||
|
{featured.title}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-6 text-sm font-light leading-10 text-ivory/65">{featured.excerpt}</p>
|
||||||
|
<div className="mt-10 flex items-center justify-between border-t border-ivory/10 pt-8 text-xs font-light text-ivory/50">
|
||||||
|
<span className="flex items-center gap-4">
|
||||||
|
<span>{featured.date}</span>
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<ClockIcon className="h-3.5 w-3.5" />
|
||||||
|
{featured.read}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-2 text-copper">
|
||||||
|
ادامه مطلب
|
||||||
|
<ArrowLeftIcon className="h-3.5 w-3.5 transition-transform duration-200 ease-lux group-hover:-translate-x-1" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<div className="mt-12 grid gap-6 lg:grid-cols-3">
|
||||||
|
{rest.map((article, i) => (
|
||||||
|
<Reveal key={article.slug} delay={0.05 * i} className="h-full">
|
||||||
|
<ArticleCard article={article} />
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Newsletter />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { PageHero } from '../components/PageHero'
|
||||||
|
import { ContactSection } from '../components/ContactSection'
|
||||||
|
import { Reveal } from '../components/Reveal'
|
||||||
|
import { IMAGES } from '../data/site'
|
||||||
|
|
||||||
|
const OFFICES = [
|
||||||
|
{ city: 'تهران', address: 'خیابان ولیعصر، برج آرامش، طبقه هجدهم', hours: 'شنبه تا چهارشنبه، ۹ تا ۱۸' },
|
||||||
|
{ city: 'اصفهان', address: 'خیابان چهارباغ بالا، مجتمع پارسه، طبقه ششم', hours: 'شنبه تا چهارشنبه، ۹ تا ۱۷' },
|
||||||
|
{ city: 'نوشهر', address: 'بلوار دریا، دفتر پروژه دریاکنار', hours: 'شنبه تا پنجشنبه، ۱۰ تا ۱۶' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function ContactPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHero
|
||||||
|
title="تماس با ما"
|
||||||
|
lead="برای مشاوره ساخت، سرمایهگذاری یا بازدید از پروژهها، تیم ما در سه شهر آماده پاسخگویی است."
|
||||||
|
image={IMAGES[1]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section className="bg-ink py-24" aria-label="دفاتر ما">
|
||||||
|
<div className="mx-auto grid max-w-site gap-6 px-6 md:grid-cols-3 lg:px-10">
|
||||||
|
{OFFICES.map((office, i) => (
|
||||||
|
<Reveal key={office.city} delay={0.06 * i} className="h-full">
|
||||||
|
{/* اعمال فرمول برای نرم شدن بالا آمدن کارت، بدون هیچ تغییری روی متنها */}
|
||||||
|
<article className="flex h-full flex-col border border-ivory/10 bg-char px-8 py-12 text-center transition-all duration-300 ease-out hover:-translate-y-2 hover:border-copper/40 hover:shadow-lift transform-gpu will-change-transform">
|
||||||
|
<h2 className="text-2xl font-bold text-copper">{office.city}</h2>
|
||||||
|
<p className="mt-6 text-sm font-light leading-9 text-ivory/70">{office.address}</p>
|
||||||
|
<div className="mt-auto pt-10">
|
||||||
|
<p className="border-t border-ivory/10 pt-6 text-xs font-light text-ivory/50">
|
||||||
|
{office.hours}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ContactSection />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { PageHero } from '../components/PageHero'
|
||||||
|
import { Faq } from '../components/home/Faq'
|
||||||
|
import { ContactSection } from '../components/ContactSection'
|
||||||
|
import { IMAGES } from '../data/site'
|
||||||
|
|
||||||
|
export function FaqPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHero
|
||||||
|
title="سوالات متداول"
|
||||||
|
lead="پیش از تماس، شاید پاسخ پرسش شما همینجا باشد. اگر نبود، خوشحال میشویم مستقیم صحبت کنیم."
|
||||||
|
image={IMAGES[1]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Faq hideHeading={true} />
|
||||||
|
|
||||||
|
<ContactSection />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Hero } from '../components/home/Hero';
|
||||||
|
import { Story } from '../components/home/Story';
|
||||||
|
import { Stats } from '../components/home/Stats';
|
||||||
|
import { Services } from '../components/home/Services';
|
||||||
|
import { BeforeAfter } from '../components/home/BeforeAfter';
|
||||||
|
import { FeaturedProjects } from '../components/home/FeaturedProjects';
|
||||||
|
import { Gallery } from '../components/home/Gallery';
|
||||||
|
import { Timeline } from '../components/home/Timeline';
|
||||||
|
import { WhyUs } from '../components/home/WhyUs';
|
||||||
|
import { Testimonials } from '../components/home/Testimonials';
|
||||||
|
import { Team } from '../components/home/Team';
|
||||||
|
import { Articles } from '../components/home/Articles';
|
||||||
|
import { Faq } from '../components/home/Faq';
|
||||||
|
import { Newsletter } from '../components/home/Newsletter';
|
||||||
|
import { ContactSection } from '../components/ContactSection';
|
||||||
|
|
||||||
|
export function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Hero />
|
||||||
|
<Story />
|
||||||
|
<Stats />
|
||||||
|
<Services />
|
||||||
|
<BeforeAfter />
|
||||||
|
<FeaturedProjects />
|
||||||
|
<Gallery />
|
||||||
|
<Timeline />
|
||||||
|
<WhyUs />
|
||||||
|
<Testimonials />
|
||||||
|
<Team />
|
||||||
|
<Articles />
|
||||||
|
<Faq />
|
||||||
|
<Newsletter />
|
||||||
|
<ContactSection />
|
||||||
|
</>);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { PageHero } from '../components/PageHero'
|
||||||
|
import { Reveal } from '../components/Reveal'
|
||||||
|
import { SectionHeading } from '../components/SectionHeading'
|
||||||
|
import { Stats } from '../components/home/Stats'
|
||||||
|
import { ContactSection } from '../components/ContactSection'
|
||||||
|
import { IMAGES } from '../data/site'
|
||||||
|
|
||||||
|
const MODELS = [
|
||||||
|
{
|
||||||
|
title: 'مشارکت در ساخت',
|
||||||
|
desc: 'مالک زمین و آرامش پلازا در سود پروژه شریک میشوند؛ اجرا، مجوزها و فروش بر عهده ماست.',
|
||||||
|
points: ['ارزیابی رایگان زمین', 'قرارداد شفاف با ضمانت اجرا', 'گزارش ماهانه پیشرفت'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'صندوق پروژهمحور',
|
||||||
|
desc: 'سرمایهگذاری روی یک پروژه مشخص با واحدهای قابل خرید و بازدهی محاسبهشده در پایان دوره.',
|
||||||
|
points: ['حداقل دوره ۲۴ ماه', 'گزارش ارزشگذاری فصلی', 'امکان انتقال واحد سرمایه'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'پیشخرید واحد',
|
||||||
|
desc: 'خرید واحد مسکونی یا تجاری در مراحل ابتدایی ساخت با قیمت پیش از تکمیل و پرداخت مرحلهای.',
|
||||||
|
points: ['پرداخت مرحلهای متناسب با پیشرفت', 'امکان شخصیسازی پلان', 'تضمین زمان تحویل'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function Investment() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHero
|
||||||
|
title="سرمایهگذاری"
|
||||||
|
lead="سه مسیر روشن برای ورود به بازار ساختوساز لوکس، با ساختار مالی شفاف و گزارشدهی منظم."
|
||||||
|
image={IMAGES[4]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section className="bg-ink py-24 lg:py-32" aria-labelledby="models-title">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<SectionHeading
|
||||||
|
title="مدلهای همکاری"
|
||||||
|
lead="بسته به میزان سرمایه، افق زمانی و سطح ریسکی که میپذیرید، یکی از این سه مسیر مناسب شماست."
|
||||||
|
/>
|
||||||
|
<div className="mt-14 grid gap-6 lg:grid-cols-3">
|
||||||
|
{MODELS.map((model, i) => (
|
||||||
|
<Reveal key={model.title} delay={0.06 * i} className="h-full">
|
||||||
|
<article className="group flex h-full flex-col border border-ivory/10 bg-char p-10 transition-all duration-300 ease-out hover:-translate-y-2 hover:border-copper/40 hover:shadow-lift transform-gpu will-change-transform">
|
||||||
|
<p className="text-sm font-bold text-copper/80">۰{(i + 1).toLocaleString('fa-IR')}</p>
|
||||||
|
|
||||||
|
<h3 className="mt-6 text-2xl font-bold text-ivory transition-colors duration-300 ease-out group-hover:text-copper">
|
||||||
|
{model.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="mt-5 text-sm font-light leading-9 text-ivory/65">{model.desc}</p>
|
||||||
|
|
||||||
|
<div className="mt-auto pt-10">
|
||||||
|
<ul className="space-y-4 border-t border-ivory/10 pt-8">
|
||||||
|
{model.points.map((point) => (
|
||||||
|
<li key={point} className="flex items-start gap-3 text-sm font-light leading-8 text-ivory/65">
|
||||||
|
<span className="mt-3.5 h-1 w-1 shrink-0 rounded-full bg-copper transition-transform duration-300 ease-out group-hover:scale-[1.5]" aria-hidden="true" />
|
||||||
|
{point}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Stats />
|
||||||
|
<ContactSection />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Link, useParams } from 'react-router-dom'
|
||||||
|
import { ArrowLeftIcon, ChevronLeftIcon } from 'lucide-react'
|
||||||
|
import { Reveal } from '../components/Reveal'
|
||||||
|
import { ButtonLink } from '../components/Button'
|
||||||
|
import { ProjectCard } from '../components/ProjectCard'
|
||||||
|
import { ContactSection } from '../components/ContactSection'
|
||||||
|
import { Lightbox } from '../components/Lightbox'
|
||||||
|
import { PROJECTS, STATUS_LABEL } from '../data/site'
|
||||||
|
|
||||||
|
export function ProjectDetail() {
|
||||||
|
const { slug } = useParams<{ slug: string }>()
|
||||||
|
const project = PROJECTS.find((p) => p.slug === slug)
|
||||||
|
const [lightbox, setLightbox] = useState<number | null>(null)
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return (
|
||||||
|
<section className="flex min-h-[70vh] flex-col items-center justify-center px-6 text-center">
|
||||||
|
<h1 className="text-section font-bold text-ivory">این پروژه پیدا نشد</h1>
|
||||||
|
<p className="mt-4 text-sm font-light text-ivory/60">
|
||||||
|
ممکن است نشانی تغییر کرده باشد. فهرست کامل پروژهها را ببینید.
|
||||||
|
</p>
|
||||||
|
<ButtonLink to="/projects" variant="copper" className="mt-8">
|
||||||
|
بازگشت به پروژهها
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
</ButtonLink>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const related = PROJECTS.filter((p) => p.slug !== project.slug).slice(0, 3)
|
||||||
|
|
||||||
|
const facts = [
|
||||||
|
{ label: 'موقعیت', value: project.location },
|
||||||
|
{ label: 'سال', value: project.year },
|
||||||
|
{ label: 'زیربنا', value: project.area },
|
||||||
|
{ label: 'تعداد طبقات', value: project.floors },
|
||||||
|
{ label: 'واحدها', value: project.units },
|
||||||
|
{ label: 'کارفرما', value: project.client },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="relative flex h-[62vh] min-h-[440px] items-end overflow-hidden">
|
||||||
|
<img src={project.image} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover" />
|
||||||
|
<div className="absolute inset-0 bg-ink/72" aria-hidden="true" />
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{ background: 'linear-gradient(to top, #27221c 6%, rgba(39,34,28,0.15) 70%)' }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<div className="relative mx-auto w-full max-w-site px-6 pb-16 lg:px-10">
|
||||||
|
<nav aria-label="مسیر صفحه" className="flex items-center gap-2 text-xs font-light text-ivory/50">
|
||||||
|
<Link to="/" className="transition-colors hover:text-copper">
|
||||||
|
صفحه اصلی
|
||||||
|
</Link>
|
||||||
|
<ChevronLeftIcon className="h-3.5 w-3.5" />
|
||||||
|
<Link to="/projects" className="transition-colors hover:text-copper">
|
||||||
|
پروژهها
|
||||||
|
</Link>
|
||||||
|
<ChevronLeftIcon className="h-3.5 w-3.5" />
|
||||||
|
<span className="text-copper">{project.title}</span>
|
||||||
|
</nav>
|
||||||
|
<h1 className="mt-5 max-w-[700px] text-hero font-bold text-ivory">{project.title}</h1>
|
||||||
|
<p className="mt-5 max-w-2xl text-base font-light leading-9 text-ivory/70">{project.summary}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-char py-20 lg:py-28" aria-label="جزئیات پروژه">
|
||||||
|
<div className="mx-auto grid max-w-site items-start gap-12 px-6 lg:grid-cols-2 lg:gap-16 lg:px-10">
|
||||||
|
|
||||||
|
<Reveal className="order-1">
|
||||||
|
<span className="flex items-center gap-3 text-sm font-light text-copper">
|
||||||
|
<span aria-hidden="true" className="h-px w-8 bg-copper" />
|
||||||
|
{STATUS_LABEL[project.status]}
|
||||||
|
</span>
|
||||||
|
<h2 className="mt-4 text-3xl font-bold text-ivory">درباره پروژه</h2>
|
||||||
|
|
||||||
|
{project.body.map((para) => (
|
||||||
|
<p key={para} className="mt-5 text-base font-light leading-9 text-ivory/70">
|
||||||
|
{para}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<dl className="mt-10 grid grid-cols-2 gap-px border border-ivory/10 bg-ivory/10">
|
||||||
|
{facts.map((f) => (
|
||||||
|
<div key={f.label} className="bg-ink p-6 md:p-8 text-right">
|
||||||
|
<dt className="text-xs font-light text-ivory/50">{f.label}</dt>
|
||||||
|
<dd className="mt-3 text-sm font-bold text-ivory">{f.value}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div className="mt-10">
|
||||||
|
<ButtonLink to="/contact" variant="outline" className="font-light">
|
||||||
|
پروژهای مشابه دارید؟
|
||||||
|
</ButtonLink>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={0.1} className="order-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLightbox(0)}
|
||||||
|
className="group block w-full overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-copper"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={project.gallery[0] || project.image}
|
||||||
|
alt=""
|
||||||
|
className="aspect-[4/3] w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div className="mt-4 grid grid-cols-3 gap-4">
|
||||||
|
{project.gallery.slice(1).map((img, i) => (
|
||||||
|
<button
|
||||||
|
key={img}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLightbox(i + 1)}
|
||||||
|
className="group block overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-copper"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={img}
|
||||||
|
alt=""
|
||||||
|
className="aspect-square w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-ink py-20 lg:py-28" aria-label="پروژههای مرتبط">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<h2 className="text-section font-bold text-ivory">پروژههای دیگر</h2>
|
||||||
|
<div className="mt-6 h-px w-24 bg-copper/70" />
|
||||||
|
<div className="mt-12 grid gap-6 lg:grid-cols-3">
|
||||||
|
{related.map((item, i) => (
|
||||||
|
<Reveal key={item.slug} delay={0.06 * i} className="h-full">
|
||||||
|
<ProjectCard project={item} />
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Lightbox
|
||||||
|
images={project.gallery}
|
||||||
|
index={lightbox}
|
||||||
|
onClose={() => setLightbox(null)}
|
||||||
|
onChange={setLightbox}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ContactSection />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { PageHero } from '../components/PageHero';
|
||||||
|
import { Reveal } from '../components/Reveal';
|
||||||
|
import { ProjectCard } from '../components/ProjectCard';
|
||||||
|
import { Gallery } from '../components/home/Gallery';
|
||||||
|
import { ContactSection } from '../components/ContactSection';
|
||||||
|
import { IMAGES, PROJECTS, type ProjectStatus } from '../data/site';
|
||||||
|
|
||||||
|
const FILTERS: {key: ProjectStatus;label: string;}[] = [
|
||||||
|
{ key: 'done', label: 'انجام شده' },
|
||||||
|
{ key: 'progress', label: 'در حال انجام' }];
|
||||||
|
|
||||||
|
|
||||||
|
export function Projects() {
|
||||||
|
const [status, setStatus] = useState<ProjectStatus>('done');
|
||||||
|
const list = PROJECTS.filter((p) => p.status === status);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHero
|
||||||
|
title="پروژهها"
|
||||||
|
lead="مجموعهای از برجهای مسکونی، مراکز تجاری و ویلاهای اختصاصی؛ از پروژههای تحویلشده تا کارگاههای فعال امروز."
|
||||||
|
image={IMAGES[0]} />
|
||||||
|
|
||||||
|
|
||||||
|
<section className="bg-ink py-24 lg:py-32" aria-label="فهرست پروژهها">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<div className="flex flex-wrap gap-3" role="tablist" aria-label="فیلتر وضعیت پروژه">
|
||||||
|
{FILTERS.map((filter) =>
|
||||||
|
<button
|
||||||
|
key={filter.key}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={status === filter.key}
|
||||||
|
onClick={() => setStatus(filter.key)}
|
||||||
|
className={`group relative overflow-hidden border px-7 py-3 text-sm transition-colors duration-300 ease-lux ${
|
||||||
|
status === filter.key ?
|
||||||
|
'border-copper bg-copper font-bold text-char' :
|
||||||
|
'border-ivory/15 font-light text-ivory/65'}`
|
||||||
|
}>
|
||||||
|
|
||||||
|
{status !== filter.key &&
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 translate-y-full bg-copper transition-transform duration-300 ease-lux group-hover:translate-y-0" />
|
||||||
|
|
||||||
|
}
|
||||||
|
<span className="relative z-10 transition-colors duration-300 ease-lux group-hover:text-char">
|
||||||
|
{filter.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-12 grid gap-6 lg:grid-cols-3">
|
||||||
|
{list.map((project, i) =>
|
||||||
|
<Reveal key={project.slug} delay={0.05 * i} className="h-full">
|
||||||
|
<ProjectCard project={project} imageClass="h-80" />
|
||||||
|
</Reveal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Gallery />
|
||||||
|
<ContactSection />
|
||||||
|
</>);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { PageHero } from '../components/PageHero'
|
||||||
|
import { Services } from '../components/home/Services'
|
||||||
|
import { Faq } from '../components/home/Faq'
|
||||||
|
import { ContactSection } from '../components/ContactSection'
|
||||||
|
import { Reveal } from '../components/Reveal'
|
||||||
|
import { SectionHeading } from '../components/SectionHeading'
|
||||||
|
import { IMAGES } from '../data/site'
|
||||||
|
|
||||||
|
const PROCESS = [
|
||||||
|
{ step: '۰۱', title: 'مطالعه و امکانسنجی', desc: 'بررسی زمین، ضوابط شهری و تحلیل بازدهی اقتصادی پروژه.' },
|
||||||
|
{ step: '۰۲', title: 'طراحی و فاز اجرایی', desc: 'کانسپت معماری، طراحی سازه و تأسیسات و تهیه نقشههای اجرایی.' },
|
||||||
|
{ step: '۰۳', title: 'اجرا و کنترل کیفیت', desc: 'ساخت با نظارت مستمر، گزارش ماهانه پیشرفت و کنترل هزینه.' },
|
||||||
|
{ step: '۰۴', title: 'تحویل و پشتیبانی', desc: 'تحویل کلید، آموزش بهرهبرداری و پنج سال پشتیبانی فنی.' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function ServicesPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHero
|
||||||
|
title="خدمات"
|
||||||
|
lead="هشت حوزه تخصصی که چرخه کامل یک پروژه ساختمانی را از ایده تا بهرهبرداری پوشش میدهند."
|
||||||
|
image={IMAGES[1]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Services />
|
||||||
|
|
||||||
|
<section className="bg-char py-24 lg:py-32" aria-labelledby="process-title">
|
||||||
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||||
|
<SectionHeading title="فرآیند کار ما" lead="چهار مرحله شفاف، از نخستین جلسه تا تحویل کلید." align="center" />
|
||||||
|
<div className="mt-14 grid gap-px bg-ivory/10 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{PROCESS.map((item, i) => (
|
||||||
|
<Reveal key={item.step} delay={0.06 * i} className="h-full">
|
||||||
|
<div className="flex h-full flex-col justify-center bg-char px-6 py-14 text-center">
|
||||||
|
<p className="text-4xl font-bold text-copper">{item.step}</p>
|
||||||
|
<h3 className="mt-6 text-xl font-bold text-ivory">{item.title}</h3>
|
||||||
|
<p className="mt-4 text-base font-light leading-9 text-ivory/60">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Faq />
|
||||||
|
<ContactSection />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export default {
|
||||||
|
content: ['./**/*.{js,jsx,ts,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
ink: '#27221c',
|
||||||
|
char: '#1e1a16',
|
||||||
|
copper: '#d5874c',
|
||||||
|
ivory: '#F5F3EF',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
pinar: ['Pinar', 'Vazirmatn', 'Tahoma', 'sans-serif'],
|
||||||
|
},
|
||||||
|
transitionTimingFunction: {
|
||||||
|
lux: 'cubic-bezier(0.23, 1, 0.32, 1)',
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
lift: '0 24px 50px -18px rgba(0, 0, 0, 0.75)',
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
// سایز این بخش یک درجه کوچکتر شد (هم در موبایل و هم در دسکتاپ)
|
||||||
|
hero: ['clamp(1.75rem, 3.8vw, 3.5rem)', { lineHeight: '1.35' }],
|
||||||
|
section: ['clamp(1.6rem, 2.4vw, 2.4rem)', { lineHeight: '1.4' }],
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
site: '1440px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user