Initial commit: Persian safety workwear landing page (Next.js)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@@ -0,0 +1,20 @@
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
next-env.d.ts
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# old vite backup (remove once migration is verified and no longer needed)
|
||||
_old_vite_backup/
|
||||
@@ -0,0 +1,103 @@
|
||||
# آریا ایمن — لندینگ (Next.js)
|
||||
|
||||
لندینگ شرکت آریا ایمن، مهاجرتیافته از Vite/React به Next.js (App Router) با خروجی کاملاً استاتیک، بهینه برای سئو و سرعت.
|
||||
|
||||
## نصب
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## اجرا در حالت توسعه
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
سپس آدرس `http://localhost:3000` را باز کنید.
|
||||
|
||||
## Build (تولید نسخه نهایی استاتیک)
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
خروجی در پوشه `out/` ساخته میشود — مجموعهای از فایلهای HTML/CSS/JS کاملاً استاتیک که نیازی به سرور Node ندارند.
|
||||
|
||||
برای پیشنمایش محلی خروجی build:
|
||||
|
||||
```bash
|
||||
npx serve out
|
||||
```
|
||||
|
||||
## آپلود روی هاست (Static Hosting)
|
||||
|
||||
این پروژه با `output: 'export'` پیکربندی شده، یعنی:
|
||||
|
||||
1. `npm run build` را اجرا کنید.
|
||||
2. کل محتوای پوشه `out/` را (نه خود پوشه، بلکه فایلهای داخلش) در ریشه هاست (public_html، یا مشابه آن در cPanel/Nginx) آپلود کنید.
|
||||
3. سایت بدون نیاز به Node.js یا هیچ سرور اختصاصی کار میکند.
|
||||
|
||||
### نکات مهم هاست استاتیک (cPanel / Nginx / هاست ایرانی)
|
||||
|
||||
- **ریدایرکتها و هدرها** در `next.config.ts` روی static export اجرا نمیشوند. اگر بعداً نیاز به ریدایرکت ۳۰۱ داشتید (مثلاً از `/index.html`)، باید در سطح هاست تنظیم شود:
|
||||
|
||||
**Apache (.htaccess):**
|
||||
```apache
|
||||
Redirect 301 /index.html /
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType text/css "access plus 1 year"
|
||||
ExpiresByType application/javascript "access plus 1 year"
|
||||
ExpiresByType image/webp "access plus 1 year"
|
||||
ExpiresByType font/woff2 "access plus 1 year"
|
||||
</IfModule>
|
||||
ErrorDocument 404 /404.html
|
||||
```
|
||||
|
||||
**Nginx:**
|
||||
```nginx
|
||||
location = /index.html { return 301 /; }
|
||||
location /_next/static/ { expires 1y; add_header Cache-Control "public, immutable"; }
|
||||
error_page 404 /404.html;
|
||||
gzip on; gzip_types text/css application/javascript image/svg+xml;
|
||||
```
|
||||
|
||||
## پیش از دیپلوی واقعی — کارهایی که باید انجام دهید
|
||||
|
||||
این پروژه فعلاً با دادههای تستی/placeholder برای موارد زیر ساخته شده و **باید قبل از انتشار واقعی بررسی و اصلاح شوند**:
|
||||
|
||||
1. **دامنه واقعی**: در فایل [`src/config/site.ts`](src/config/site.ts) مقدار `url: "https://ariaiman.ir"` باید با دامنه نهایی واقعی جایگزین شود (این مقدار روی canonical، Open Graph، sitemap و JSON-LD اثر مستقیم دارد).
|
||||
2. **لوگوی رسمی**: در همان فایل، `logo` فعلاً به تصویر hero اشاره میکند؛ یک فایل PNG/SVG لوگوی واقعی جایگزین شود.
|
||||
3. **آیکونهای PWA**: `src/app/manifest.ts` فعلاً بدون آیکون است. دو فایل `icon-192.png` و `icon-512.png` در `public/` قرار داده و در manifest معرفی شوند.
|
||||
4. **تصویر Open Graph**: یک فایل `opengraph-image.png` با ابعاد ۱۲۰۰×۶۳۰ در `src/app/` قرار دهید (Next.js آن را بهصورت خودکار شناسایی میکند).
|
||||
5. **شبکههای اجتماعی**: در `src/config/site.ts`، آرایه `socials` خالی است. لینکهای واقعی اینستاگرام/تلگرام/لینکدین را اضافه کنید تا در Schema.org (`sameAs`) درج شود.
|
||||
6. **فرم تماس**: فرم بخش «تماس» فعلاً فقط یک شبیهسازی محلی است (بدون ارسال واقعی به سرور یا سرویس ایمیل) — دقیقاً همان رفتاری که در نسخه قبلی هم وجود داشت. برای فعالسازی واقعی باید به یک سرویس فرم (مثل فرماسپری، ایمیل، یا API اختصاصی) متصل شود.
|
||||
7. **npm audit**: یک آسیبپذیری «high» در نسخه postcss داخلی Next.js 15.5 گزارش میشود (`npm audit`). این مربوط به مرحله build/dev است، نه کد سمت کاربر نهایی. رفع کامل آن نیازمند ارتقا به Next.js 16 (breaking change) است — در صورت تمایل بهروزرسانی جداگانه انجام شود.
|
||||
|
||||
## چکلیست سئوی پیادهسازیشده
|
||||
|
||||
| مورد | وضعیت | فایل |
|
||||
|---|---|---|
|
||||
| یک H1 با کلمه کلیدی اصلی | ✅ | `src/components/sections/Hero.tsx` |
|
||||
| سلسلهمراتب H2/H3 منطقی، بدون پرش سطح | ✅ | `src/components/ui/SectionHead.tsx` و همه سکشنها |
|
||||
| عنوان و توضیحات با طول مناسب | ✅ | `src/config/site.ts` |
|
||||
| Canonical، metadataBase، Open Graph، Twitter Card | ✅ | `src/app/layout.tsx` |
|
||||
| `lang="fa"` و `dir="rtl"` | ✅ | `src/app/layout.tsx` |
|
||||
| JSON-LD (LocalBusiness + WebSite) | ✅ | `src/lib/seo/schema.ts`, `src/components/seo/JsonLd.tsx` |
|
||||
| `sitemap.xml` | ✅ | `src/app/sitemap.ts` |
|
||||
| `robots.txt` | ✅ | `src/app/robots.ts` |
|
||||
| `manifest.webmanifest` | ✅ (بدون آیکون — نیاز به تکمیل) | `src/app/manifest.ts` |
|
||||
| صفحه ۴۰۴ واقعی (بدون ریدایرکت به خانه) | ✅ | `src/app/not-found.tsx` |
|
||||
| alt توصیفی روی همه تصاویر محتوایی | ✅ | همه کامپوننتهای سکشن |
|
||||
| تمام متن در HTML بدون نیاز به اجرای JS | ✅ | تأیید شده با بررسی خروجی build |
|
||||
| لینکهای داخلی سالم (`#anchor`) | ✅ | بدون تغییر نسبت به نسخه اصلی |
|
||||
| فونت خودمیزبان (self-hosted) بدون CDN خارجی | ✅ | `public/fonts/`, `src/app/layout.tsx` |
|
||||
| تصاویر بدون وابستگی به CDN خارجی | ✅ | تصاویر Industries از pexels دانلود و داخلی شدند |
|
||||
|
||||
## معماری فنی مهاجرت
|
||||
|
||||
- **Server Components برای محتوا**: تمام بخشهای بدون تعامل (Hero, About, Standards, TapeMarquee, Footer) بهصورت Server Component هستند و در HTML build نهایی کامل قابل مشاهدهاند.
|
||||
- **Client Components فقط برای تعامل**: منو موبایل، فرم تماس، اسلایدر نظرات، پنلهای تعاملی محصولات/خدمات، شمارنده آمار، کرسر سفارشی و Preloader، هرکدام جداگانه `'use client'` هستند تا حجم JS اولیه صفحه کمینه بماند.
|
||||
- **بدون بکاند**: هیچ `app/api`، دیتابیس یا server action اضافه نشده؛ طبق درخواست پروژه.
|
||||
- **انیمیشن ورودی Hero با CSS خالص**: برخلاف نسخه اصلی (که H1 تا پایان انیمیشن Preloader با `opacity:0` مخفی بود)، اکنون محتوای Hero همیشه در HTML اولیه قابل مشاهده است و فقط با CSS keyframes (بدون وابستگی به اجرای JS) وارد صفحه میشود — این تغییر برای رفع مشکل LCP/SEO با تأیید شما انجام شد.
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
> aria-imen-landing@0.1.0 dev
|
||||
> next dev
|
||||
|
||||
▲ Next.js 15.5.25
|
||||
- Local: http://localhost:3000
|
||||
- Network: http://172.29.208.1:3000
|
||||
|
||||
✓ Starting...
|
||||
✓ Ready in 2.3s
|
||||
○ Compiling / ...
|
||||
✓ Compiled / in 1186ms (1589 modules)
|
||||
GET / 200 in 2643ms
|
||||
GET / 200 in 209ms
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export",
|
||||
trailingSlash: false,
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
poweredByHeader: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "aria-imen-landing",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^13.2.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"next": "15.5.25",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "4.1.17",
|
||||
"@types/node": "22.19.17",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"tailwindcss": "4.1.17",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 198 KiB |
@@ -0,0 +1,91 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { JsonLd } from "@/components/seo/JsonLd";
|
||||
import { organizationSchema, websiteSchema } from "@/lib/seo/schema";
|
||||
import "@/styles/main.css";
|
||||
|
||||
const vazirmatn = localFont({
|
||||
src: "../../public/fonts/Vazirmatn-Variable.woff2",
|
||||
weight: "100 900",
|
||||
display: "swap",
|
||||
variable: "--font-vazirmatn",
|
||||
fallback: ["Tahoma", "system-ui", "sans-serif"],
|
||||
});
|
||||
|
||||
const ibmPlexMono = localFont({
|
||||
src: [
|
||||
{
|
||||
path: "../../public/fonts/IBMPlexMono-Regular.woff2",
|
||||
weight: "400",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "../../public/fonts/IBMPlexMono-Medium.woff2",
|
||||
weight: "500",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "../../public/fonts/IBMPlexMono-SemiBold.woff2",
|
||||
weight: "600",
|
||||
style: "normal",
|
||||
},
|
||||
],
|
||||
display: "swap",
|
||||
variable: "--font-ibm-plex-mono",
|
||||
fallback: ["ui-monospace", "monospace"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(siteConfig.url),
|
||||
title: {
|
||||
default: siteConfig.title,
|
||||
template: `%s | ${siteConfig.name}`,
|
||||
},
|
||||
description: siteConfig.description,
|
||||
alternates: { canonical: "/" },
|
||||
openGraph: {
|
||||
type: "website",
|
||||
url: "/",
|
||||
siteName: siteConfig.name,
|
||||
locale: siteConfig.locale,
|
||||
title: siteConfig.title,
|
||||
description: siteConfig.description,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: siteConfig.title,
|
||||
description: siteConfig.description,
|
||||
},
|
||||
robots: { index: true, follow: true },
|
||||
formatDetection: { telephone: false },
|
||||
icons: {
|
||||
icon:
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='22' fill='%23FF5A00'/%3E%3Cpath d='M50 20 L72 28 V48 C72 66 62 76 50 82 C38 76 28 66 28 48 V28 Z' fill='%230A0A0C'/%3E%3Cpath d='M40 50 L47 57 L62 42' stroke='%23FF5A00' stroke-width='7' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E",
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: siteConfig.themeColor,
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html
|
||||
lang={siteConfig.lang}
|
||||
dir={siteConfig.dir}
|
||||
className={`${vazirmatn.variable} ${ibmPlexMono.variable}`}
|
||||
>
|
||||
<body className="relative min-h-screen bg-bg font-sans text-ink antialiased">
|
||||
<JsonLd data={[organizationSchema(), websiteSchema()]} />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { siteConfig } from "@/config/site";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: siteConfig.name,
|
||||
short_name: siteConfig.name,
|
||||
lang: siteConfig.lang,
|
||||
dir: siteConfig.dir,
|
||||
start_url: "/",
|
||||
display: "standalone",
|
||||
background_color: siteConfig.themeColor,
|
||||
theme_color: siteConfig.themeColor,
|
||||
icons: [],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import Link from "next/link";
|
||||
import { ShieldAlert } from "lucide-react";
|
||||
|
||||
export const metadata = {
|
||||
title: "صفحه پیدا نشد",
|
||||
robots: { index: false },
|
||||
};
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center px-5 text-center">
|
||||
<div className="hazard absolute inset-x-0 top-0 h-2.5" />
|
||||
<ShieldAlert className="size-16 text-accent" />
|
||||
<h1 className="mt-8 text-4xl font-black md:text-6xl">
|
||||
صفحه مورد نظر پیدا نشد
|
||||
</h1>
|
||||
<p className="mt-5 max-w-md leading-8 text-muted">
|
||||
ممکن است آدرس اشتباه وارد شده باشد یا این صفحه جابهجا شده باشد.
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-9 inline-flex items-center gap-2.5 rounded-full bg-accent px-7 py-4 font-bold text-bg transition-all duration-300 hover:bg-accent-soft"
|
||||
>
|
||||
بازگشت به صفحه اصلی
|
||||
</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { AppShell } from "@/components/layout/AppShell";
|
||||
import { Footer } from "@/components/layout/Footer";
|
||||
import { Hero } from "@/components/sections/Hero";
|
||||
import { TapeMarquee } from "@/components/sections/TapeMarquee";
|
||||
import { Stats } from "@/components/sections/Stats";
|
||||
import { Products } from "@/components/sections/Products";
|
||||
import { About } from "@/components/sections/About";
|
||||
import { Services } from "@/components/sections/Services";
|
||||
import { Standards } from "@/components/sections/Standards";
|
||||
import { Industries } from "@/components/sections/Industries";
|
||||
import { Testimonials } from "@/components/sections/Testimonials";
|
||||
import { Contact } from "@/components/sections/Contact";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<AppShell>
|
||||
<main>
|
||||
<Hero />
|
||||
<TapeMarquee />
|
||||
<Stats />
|
||||
<Products />
|
||||
<About />
|
||||
<Services />
|
||||
<Standards />
|
||||
<Industries />
|
||||
<Testimonials />
|
||||
<Contact />
|
||||
</main>
|
||||
<Footer />
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { siteConfig } from "@/config/site";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: { userAgent: "*", allow: "/" },
|
||||
sitemap: `${siteConfig.url}/sitemap.xml`,
|
||||
host: siteConfig.url,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { siteConfig } from "@/config/site";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const pages = ["/"];
|
||||
return pages.map((p) => ({
|
||||
url: new URL(p, siteConfig.url).toString(),
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 1,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import { Preloader } from "./Preloader";
|
||||
import { Cursor } from "./Cursor";
|
||||
import { Header } from "./Header";
|
||||
import { BackToTop } from "./BackToTop";
|
||||
|
||||
export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
const [ready, setReady] = useState(false);
|
||||
const handleReady = useCallback(() => setReady(true), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{!ready && <Preloader onComplete={handleReady} />}
|
||||
</AnimatePresence>
|
||||
|
||||
<Cursor />
|
||||
<div className="noise" aria-hidden />
|
||||
|
||||
<Header ready={ready} />
|
||||
|
||||
{children}
|
||||
|
||||
<BackToTop />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ArrowUp } from "lucide-react";
|
||||
|
||||
export function BackToTop() {
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const on = () => setShow(window.scrollY > 700);
|
||||
on();
|
||||
window.addEventListener("scroll", on, { passive: true });
|
||||
return () => window.removeEventListener("scroll", on);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{show && (
|
||||
<motion.a
|
||||
href="#home"
|
||||
aria-label="بازگشت به بالا"
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 24 }}
|
||||
className="fixed bottom-6 left-6 z-[70] flex size-12 items-center justify-center rounded-full border border-line bg-panel/90 text-ink shadow-[0_10px_40px_rgba(0,0,0,0.5)] backdrop-blur transition-colors hover:border-accent hover:text-accent"
|
||||
>
|
||||
<ArrowUp className="size-5" />
|
||||
</motion.a>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion, useMotionValue, useSpring } from "framer-motion";
|
||||
|
||||
export function Cursor() {
|
||||
const x = useMotionValue(-100);
|
||||
const y = useMotionValue(-100);
|
||||
const sx = useSpring(x, { stiffness: 550, damping: 45, mass: 0.4 });
|
||||
const sy = useSpring(y, { stiffness: 550, damping: 45, mass: 0.4 });
|
||||
const [hover, setHover] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const move = (e: MouseEvent) => {
|
||||
x.set(e.clientX);
|
||||
y.set(e.clientY);
|
||||
setVisible(true);
|
||||
};
|
||||
const over = (e: MouseEvent) => {
|
||||
const t = e.target as HTMLElement | null;
|
||||
setHover(
|
||||
!!t?.closest("a, button, input, textarea, select, label, [data-hover]"),
|
||||
);
|
||||
};
|
||||
window.addEventListener("mousemove", move, { passive: true });
|
||||
window.addEventListener("mouseover", over, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", move);
|
||||
window.removeEventListener("mouseover", over);
|
||||
};
|
||||
}, [x, y]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none fixed inset-0 z-[120] hidden [@media(pointer:fine)]:block">
|
||||
<motion.div
|
||||
style={{ x: sx, y: sy }}
|
||||
className="absolute -left-4 -top-4 size-8"
|
||||
>
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: hover ? 1.9 : 1,
|
||||
opacity: visible ? 1 : 0,
|
||||
rotate: hover ? 45 : 0,
|
||||
}}
|
||||
transition={{ type: "spring", stiffness: 320, damping: 24 }}
|
||||
className="size-8 rounded-md border-2 border-accent/80"
|
||||
/>
|
||||
<div
|
||||
className={`absolute inset-0 m-auto size-1 rounded-full bg-accent transition-opacity duration-300 ${
|
||||
visible ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { AtSign, Globe, Phone, Send } from "lucide-react";
|
||||
import { Logo } from "@/components/ui/Logo";
|
||||
|
||||
const QUICK_LINKS = [
|
||||
{ href: "#home", label: "خانه" },
|
||||
{ href: "#products", label: "محصولات" },
|
||||
{ href: "#about", label: "درباره ما" },
|
||||
{ href: "#services", label: "خدمات" },
|
||||
{ href: "#standards", label: "استانداردها" },
|
||||
{ href: "#industries", label: "صنایع" },
|
||||
];
|
||||
|
||||
const PRODUCT_LINKS = [
|
||||
"کلاه ایمنی",
|
||||
"دستکش ضدبرش",
|
||||
"کفش ایمنی",
|
||||
"عینک ایمنی",
|
||||
"هارنس ضدسقوط",
|
||||
"لباس کار بازتابنده",
|
||||
];
|
||||
|
||||
const SOCIALS = [
|
||||
{ icon: Globe, label: "وبسایت" },
|
||||
{ icon: Send, label: "تلگرام" },
|
||||
{ icon: AtSign, label: "ایمیل" },
|
||||
{ icon: Phone, label: "تماس" },
|
||||
];
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="relative overflow-hidden border-t border-line">
|
||||
<div className="hazard h-2.5" />
|
||||
|
||||
<div className="mx-auto max-w-7xl px-5 md:px-8">
|
||||
<div className="grid gap-12 py-16 md:grid-cols-12 md:py-20">
|
||||
{/* brand */}
|
||||
<div className="md:col-span-5">
|
||||
<Logo />
|
||||
<p className="mt-6 max-w-sm text-sm leading-8 text-muted">
|
||||
تأمینکننده تخصصی تجهیزات حفاظت فردی و راهکارهای ایمنی صنعتی؛
|
||||
همراه سازمانها از سال ۱۳۸۶ در مسیر صفر شدن حوادث کاری.
|
||||
</p>
|
||||
<div className="mt-7 flex gap-3">
|
||||
{SOCIALS.map((s) => (
|
||||
<a
|
||||
key={s.label}
|
||||
href="#contact"
|
||||
aria-label={s.label}
|
||||
className="flex size-10 items-center justify-center rounded-lg border border-line text-muted transition-all duration-300 hover:border-accent hover:bg-accent hover:text-bg"
|
||||
>
|
||||
<s.icon className="size-4" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* quick links */}
|
||||
<div className="md:col-span-2">
|
||||
<div className="font-mono text-xs tracking-[0.25em] text-accent">
|
||||
{"// دسترسی"}
|
||||
</div>
|
||||
<ul className="mt-5 space-y-3">
|
||||
{QUICK_LINKS.map((l) => (
|
||||
<li key={l.href}>
|
||||
<a
|
||||
href={l.href}
|
||||
className="text-sm text-muted transition-colors hover:text-ink"
|
||||
>
|
||||
{l.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* products */}
|
||||
<div className="md:col-span-2">
|
||||
<div className="font-mono text-xs tracking-[0.25em] text-accent">
|
||||
{"// محصولات"}
|
||||
</div>
|
||||
<ul className="mt-5 space-y-3">
|
||||
{PRODUCT_LINKS.map((p) => (
|
||||
<li key={p}>
|
||||
<a
|
||||
href="#products"
|
||||
className="text-sm text-muted transition-colors hover:text-ink"
|
||||
>
|
||||
{p}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* contact */}
|
||||
<div className="md:col-span-3">
|
||||
<div className="font-mono text-xs tracking-[0.25em] text-accent">
|
||||
{"// ارتباط"}
|
||||
</div>
|
||||
<address className="mt-5 space-y-3 text-sm text-muted not-italic">
|
||||
<p className="leading-7">
|
||||
تهران، خیابان کارگر شمالی،
|
||||
<br />
|
||||
خیابان فر، کوچه دوم، پلاک ۱۲
|
||||
</p>
|
||||
<p className="font-mono tracking-wider" dir="ltr">
|
||||
۰۲۱ - ۹۱ ۰۰ ۸۸ ۰۰
|
||||
</p>
|
||||
<p className="font-mono tracking-wider" dir="ltr">
|
||||
info@ariaiman.ir
|
||||
</p>
|
||||
<p className="pt-2 text-xs leading-6">
|
||||
شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰
|
||||
<br />
|
||||
پشتیبانی اضطراری: ۲۴/۷
|
||||
</p>
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* giant wordmark */}
|
||||
<div aria-hidden className="pointer-events-none select-none pb-6">
|
||||
<div className="text-outline text-center text-[17vw] font-black leading-[0.85] md:text-[11vw]">
|
||||
آریا ایمن
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 border-t border-line py-6 text-xs text-muted">
|
||||
<div>© ۱۴۰۴ آریا ایمن — تمامی حقوق محفوظ است.</div>
|
||||
<div className="font-mono text-[10px] tracking-[0.25em]">
|
||||
ISO 9001:2015 — CE CERTIFIED PPE SUPPLIER
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Menu, Phone, X } from "lucide-react";
|
||||
import { EASE, faNum } from "@/components/ui/Reveal";
|
||||
import { Logo } from "@/components/ui/Logo";
|
||||
|
||||
const LINKS = [
|
||||
{ href: "#home", label: "خانه" },
|
||||
{ href: "#products", label: "محصولات" },
|
||||
{ href: "#about", label: "درباره ما" },
|
||||
{ href: "#services", label: "خدمات" },
|
||||
{ href: "#standards", label: "استانداردها" },
|
||||
{ href: "#industries", label: "صنایع" },
|
||||
{ href: "#contact", label: "تماس" },
|
||||
];
|
||||
|
||||
/* added for migration: start with null on the server so the first client render matches,
|
||||
then set the real Tehran time only after mount (avoids a hydration mismatch on the clock text) */
|
||||
function useTehranClock() {
|
||||
const [now, setNow] = useState<Date | null>(null);
|
||||
useEffect(() => {
|
||||
setNow(new Date());
|
||||
const t = setInterval(() => setNow(new Date()), 15000);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
if (!now) return { time: "--:--", shift: "" };
|
||||
|
||||
const time = new Intl.DateTimeFormat("fa-IR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
timeZone: "Asia/Tehran",
|
||||
}).format(now);
|
||||
const hour = Number(
|
||||
new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric",
|
||||
hour12: false,
|
||||
timeZone: "Asia/Tehran",
|
||||
}).format(now),
|
||||
);
|
||||
const shift = hour >= 6 && hour < 14 ? "صبح" : hour >= 14 && hour < 22 ? "عصر" : "شب";
|
||||
return { time, shift };
|
||||
}
|
||||
|
||||
export function Header({ ready }: { ready: boolean }) {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const { time, shift } = useTehranClock();
|
||||
|
||||
useEffect(() => {
|
||||
const on = () => setScrolled(window.scrollY > 40);
|
||||
on();
|
||||
window.addEventListener("scroll", on, { passive: true });
|
||||
return () => window.removeEventListener("scroll", on);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = open ? "hidden" : "";
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.header
|
||||
initial={{ y: -90, opacity: 0 }}
|
||||
animate={ready ? { y: 0, opacity: 1 } : {}}
|
||||
transition={{ duration: 0.9, delay: 0.15, ease: EASE }}
|
||||
className={`fixed inset-x-0 top-0 z-[80] transition-all duration-500 ${
|
||||
scrolled
|
||||
? "border-b border-line bg-bg/85 py-3 backdrop-blur-xl"
|
||||
: "py-5"
|
||||
}`}
|
||||
>
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-5 md:px-8">
|
||||
<Logo />
|
||||
|
||||
<nav className="hidden items-center gap-7 lg:flex">
|
||||
{LINKS.map((l) => (
|
||||
<a
|
||||
key={l.href}
|
||||
href={l.href}
|
||||
className="group relative py-2 text-sm text-ink/75 transition-colors hover:text-ink"
|
||||
>
|
||||
{l.label}
|
||||
<span className="absolute bottom-0 start-0 h-0.5 w-full origin-right scale-x-0 bg-accent transition-transform duration-300 group-hover:scale-x-100" />
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="hidden items-center gap-2.5 rounded-full border border-line px-4 py-2 text-[11px] text-muted xl:flex">
|
||||
<span className="size-1.5 animate-pulse rounded-full bg-emerald-400" />
|
||||
<span className="font-mono">{time}</span>
|
||||
<span className="text-muted/50">|</span>
|
||||
<span>تهران — شیفت {shift}</span>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="#contact"
|
||||
className="hidden items-center gap-2 rounded-full bg-accent px-5 py-2.5 text-sm font-bold text-bg transition-all duration-300 hover:bg-accent-soft hover:shadow-[0_0_30px_rgba(255,90,0,0.4)] md:flex"
|
||||
>
|
||||
<Phone className="size-4" />
|
||||
درخواست مشاوره
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="باز کردن منو"
|
||||
className="flex size-11 items-center justify-center rounded-lg border border-line text-ink transition-colors hover:border-accent hover:text-accent lg:hidden"
|
||||
>
|
||||
<Menu className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.header>
|
||||
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.35 }}
|
||||
className="fixed inset-0 z-[110] flex flex-col bg-bg/95 backdrop-blur-2xl"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-line px-5 py-4 md:px-8">
|
||||
<Logo />
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
aria-label="بستن منو"
|
||||
className="flex size-11 items-center justify-center rounded-lg border border-line transition-colors hover:border-accent hover:text-accent"
|
||||
>
|
||||
<X className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex flex-1 flex-col justify-center gap-1 overflow-y-auto px-8">
|
||||
{LINKS.map((l, i) => (
|
||||
<motion.a
|
||||
key={l.href}
|
||||
href={l.href}
|
||||
onClick={() => setOpen(false)}
|
||||
initial={{ opacity: 0, x: 44 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
delay: 0.08 + i * 0.055,
|
||||
duration: 0.55,
|
||||
ease: EASE,
|
||||
}}
|
||||
className="group flex items-baseline gap-5 border-b border-line/60 py-4"
|
||||
>
|
||||
<span className="font-mono text-xs text-accent">
|
||||
{faNum(String(i + 1).padStart(2, "0"))}
|
||||
</span>
|
||||
<span className="text-3xl font-black transition-colors group-hover:text-accent">
|
||||
{l.label}
|
||||
</span>
|
||||
</motion.a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-line p-8 text-sm">
|
||||
<div className="flex items-center gap-3 text-muted">
|
||||
<Phone className="size-4 text-accent" />
|
||||
<span className="font-mono">۰۲۱-۹۱۰۰۸۸۰۰</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted">
|
||||
تهران — شیفت {shift}، {time}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
|
||||
const STEPS = [
|
||||
"بررسی سیستمهای ایمنی",
|
||||
"تأیید استانداردهای بینالمللی",
|
||||
"بارگذاری تجهیزات حفاظت فردی",
|
||||
"محیط کاری آماده است",
|
||||
];
|
||||
|
||||
export function Preloader({ onComplete }: { onComplete: () => void }) {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const done = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
let v = 0;
|
||||
const t = setInterval(() => {
|
||||
v += Math.floor(Math.random() * 11) + 7;
|
||||
if (v >= 100) {
|
||||
v = 100;
|
||||
clearInterval(t);
|
||||
if (!done.current) {
|
||||
done.current = true;
|
||||
setTimeout(onComplete, 450);
|
||||
}
|
||||
}
|
||||
setProgress(v);
|
||||
}, 140);
|
||||
return () => clearInterval(t);
|
||||
}, [onComplete]);
|
||||
|
||||
const step =
|
||||
STEPS[
|
||||
Math.min(STEPS.length - 1, Math.floor((progress / 100) * STEPS.length))
|
||||
];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
exit={{ y: "-100%" }}
|
||||
transition={{ duration: 0.85, ease: [0.76, 0, 0.24, 1] }}
|
||||
className="fixed inset-0 z-[200] flex flex-col items-center justify-center bg-bg"
|
||||
>
|
||||
<div className="hazard absolute inset-x-0 top-0 h-3" />
|
||||
<div className="hazard absolute inset-x-0 bottom-0 h-3" />
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="flex size-12 items-center justify-center rounded-xl bg-accent text-bg">
|
||||
<ShieldCheck className="size-7" />
|
||||
</span>
|
||||
<span className="font-mono text-xs tracking-[0.35em] text-muted">
|
||||
ARIA IMEN CO.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 font-mono text-7xl font-semibold tabular-nums md:text-8xl">
|
||||
{progress.toLocaleString("fa-IR")}
|
||||
<span className="text-accent">٪</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 h-[3px] w-64 overflow-hidden rounded-full bg-line md:w-80">
|
||||
<div
|
||||
className="h-full bg-accent transition-all duration-200"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex items-center gap-3 text-xs text-muted">
|
||||
<span className="size-1.5 animate-pulse rounded-full bg-accent" />
|
||||
{step}
|
||||
<span className="animate-blink text-accent">▍</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import Image from "next/image";
|
||||
import { Globe, Headset, Users } from "lucide-react";
|
||||
import { Reveal } from "@/components/ui/Reveal";
|
||||
import { SectionHead } from "@/components/ui/SectionHead";
|
||||
|
||||
const TIMELINE = [
|
||||
{ year: "۱۳۸۶", text: "تأسیس آریا ایمن در تهران با تمرکز بر تجهیزات حفاظت فردی" },
|
||||
{ year: "۱۳۹۲", text: "توسعه شبکه توزیع به تمام ۳۱ استان کشور" },
|
||||
{ year: "۱۳۹۸", text: "راهاندازی آزمایشگاه کنترل کیفیت و تست تجهیزات" },
|
||||
{ year: "۱۴۰۲", text: "افتتاح مرکز تخصصی آموزش و بازرسی HSE" },
|
||||
];
|
||||
|
||||
const FEATURES = [
|
||||
{ icon: Globe, label: "نماینده رسمی ۴۰+ برند جهانی" },
|
||||
{ icon: Users, label: "تیم مهندسی و بازرسی HSE" },
|
||||
{ icon: Headset, label: "پشتیبانی ۲۴/۷ قراردادها" },
|
||||
];
|
||||
|
||||
export function About() {
|
||||
return (
|
||||
<section
|
||||
id="about"
|
||||
aria-labelledby="about-title"
|
||||
className="relative overflow-hidden border-t border-line py-24 md:py-32"
|
||||
>
|
||||
<div className="absolute inset-0 bg-grid-fine opacity-50" />
|
||||
|
||||
<div className="relative mx-auto max-w-7xl px-5 md:px-8">
|
||||
<div className="grid items-center gap-14 lg:grid-cols-2">
|
||||
{/* ---- image ---- */}
|
||||
<Reveal className="relative order-2 lg:order-1" delay={0.1}>
|
||||
<div className="relative overflow-hidden rounded-2xl border border-line">
|
||||
<Image
|
||||
src="/images/about.jpg"
|
||||
alt="انبار تجهیزات ایمنی آریا ایمن"
|
||||
width={800}
|
||||
height={600}
|
||||
className="aspect-[4/3] w-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-bg/80 via-transparent to-transparent" />
|
||||
<div className="absolute bottom-5 start-5 flex items-center gap-4 rounded-xl border border-line bg-bg/80 px-5 py-4 backdrop-blur-md">
|
||||
<span className="font-mono text-3xl font-bold text-accent">
|
||||
۱۸+
|
||||
</span>
|
||||
<span className="text-xs leading-5 text-muted">
|
||||
سال حفاظت از
|
||||
<br />
|
||||
جان نیروی کار
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hazard absolute -top-3 -end-3 h-6 w-28 rounded-full opacity-90" />
|
||||
</Reveal>
|
||||
|
||||
{/* ---- text ---- */}
|
||||
<div className="order-1 lg:order-2">
|
||||
<SectionHead
|
||||
headingId="about-title"
|
||||
tag="۰۲ — درباره ما"
|
||||
title={
|
||||
<>
|
||||
ایمنی برای ما شعار نیست؛
|
||||
<br />
|
||||
<span className="text-accent">قرارداد است.</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Reveal delay={0.2}>
|
||||
<p className="mt-6 leading-9 text-muted">
|
||||
از سال ۱۳۸۶، زمانی که اولین کلاه ایمنی را به یک کارگاه کوچک در
|
||||
جنوب تهران فروختیم، باور ما همین یک جمله بود: پشت هر تجهیز
|
||||
ایمنی، یک خانواده هست که منتظر بازگشت عزیزش است. امروز با بیش
|
||||
از ۸۵۰ سازمان، از پالایشگاهها تا معادن و سایتهای ساختمانی، در
|
||||
کنار کسانی هستیم که ایمنی را یک خط قرمز میدانند.
|
||||
</p>
|
||||
</Reveal>
|
||||
|
||||
<div className="mt-10 space-y-1">
|
||||
{TIMELINE.map((t, i) => (
|
||||
<Reveal key={t.year} delay={0.25 + i * 0.07}>
|
||||
<div className="group border-s-2 border-accent/30 ps-6 transition-colors duration-300 hover:border-accent">
|
||||
<span className="font-mono text-sm font-bold text-accent">
|
||||
{t.year}
|
||||
</span>
|
||||
<p className="mt-1 pb-5 text-sm leading-7 text-ink/75">
|
||||
{t.text}
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.5}>
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
{FEATURES.map((f) => (
|
||||
<span
|
||||
key={f.label}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-line bg-panel/60 px-4 py-2 text-xs text-ink/80 transition-colors hover:border-accent/50 hover:text-ink"
|
||||
>
|
||||
<f.icon className="size-4 text-accent" />
|
||||
{f.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { FormEvent } from "react";
|
||||
import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Loader2,
|
||||
Mail,
|
||||
MapPin,
|
||||
Phone,
|
||||
Send,
|
||||
Siren,
|
||||
} from "lucide-react";
|
||||
import { Reveal } from "@/components/ui/Reveal";
|
||||
import { SectionHead } from "@/components/ui/SectionHead";
|
||||
|
||||
const INFO = [
|
||||
{
|
||||
icon: MapPin,
|
||||
label: "نشانی",
|
||||
value: "تهران، خیابان کارگر شمالی، خیابان فر، کوچه دوم، پلاک ۱۲",
|
||||
},
|
||||
{ icon: Phone, label: "تلفن تماس", value: "۰۲۱ - ۹۱ ۰۰ ۸۸ ۰۰", ltr: true },
|
||||
{ icon: Mail, label: "ایمیل", value: "info@ariaiman.ir", ltr: true },
|
||||
{ icon: Clock, label: "ساعات کاری", value: "شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰" },
|
||||
];
|
||||
|
||||
const inputCls =
|
||||
"w-full rounded-lg border border-line bg-bg px-4 py-3.5 text-sm text-ink placeholder:text-muted/50 transition-colors focus:border-accent focus:outline-none";
|
||||
|
||||
export function Contact() {
|
||||
const [status, setStatus] = useState<"idle" | "loading" | "success">("idle");
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setStatus("loading");
|
||||
setTimeout(() => setStatus("success"), 1100);
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="contact"
|
||||
aria-labelledby="contact-title"
|
||||
className="relative overflow-hidden border-t border-line py-24 md:py-32"
|
||||
>
|
||||
<div className="absolute inset-0 bg-grid-fine opacity-40" />
|
||||
|
||||
<div className="relative mx-auto max-w-7xl px-5 md:px-8">
|
||||
<div className="grid gap-14 lg:grid-cols-2">
|
||||
{/* ---- info ---- */}
|
||||
<div>
|
||||
<SectionHead
|
||||
headingId="contact-title"
|
||||
tag="۰۷ — تماس"
|
||||
title={
|
||||
<>
|
||||
بیایید ایمنی را
|
||||
<br />
|
||||
با هم جدی بگیریم
|
||||
</>
|
||||
}
|
||||
desc="فرم را پر کنید تا کارشناسان ما برای هماهنگی جلسه بازدید رایگان سایت شما تماس بگیرند؛ یا مستقیم با ما در ارتباط باشید."
|
||||
/>
|
||||
|
||||
<div className="mt-10 space-y-1">
|
||||
{INFO.map((item, i) => (
|
||||
<Reveal key={item.label} delay={0.15 + i * 0.06}>
|
||||
<div className="group flex items-center gap-4 border-b border-line/70 py-4">
|
||||
<span className="flex size-11 shrink-0 items-center justify-center rounded-lg border border-line text-accent transition-colors duration-300 group-hover:border-accent/50">
|
||||
<item.icon className="size-5" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs text-muted">{item.label}</div>
|
||||
<div
|
||||
className={`mt-1 text-sm font-medium ${
|
||||
item.ltr ? "font-mono tracking-wider" : ""
|
||||
}`}
|
||||
dir={item.ltr ? "ltr" : "rtl"}
|
||||
>
|
||||
{item.value}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.45}>
|
||||
<div className="mt-8 flex items-start gap-4 rounded-xl border border-hazard/30 bg-hazard/5 p-5">
|
||||
<Siren className="size-6 shrink-0 text-hazard" />
|
||||
<div>
|
||||
<div className="font-bold text-hazard">
|
||||
پاسخگویی اضطراری ۲۴/۷
|
||||
</div>
|
||||
<p className="mt-1.5 text-sm leading-7 text-muted">
|
||||
برای مشتریان دارای قرارداد سازمانی، خط ویژه حوادث و تأمین
|
||||
فوری تجهیزات در تمام ساعات شبانهروز فعال است.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
{/* ---- form ---- */}
|
||||
<Reveal delay={0.15}>
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="rounded-2xl border border-line bg-panel/60 p-6 backdrop-blur md:p-9"
|
||||
>
|
||||
<div className="font-mono text-xs tracking-[0.25em] text-accent">
|
||||
{"// فرم درخواست مشاوره"}
|
||||
</div>
|
||||
<h3 className="mt-3 text-2xl font-black leading-snug">
|
||||
در کمتر از ۲۴ ساعت
|
||||
<br />
|
||||
با شما تماس میگیریم
|
||||
</h3>
|
||||
|
||||
{status === "success" ? (
|
||||
<div className="mt-10 flex flex-col items-center rounded-xl border border-emerald-500/30 bg-emerald-500/5 p-10 text-center">
|
||||
<CheckCircle2 className="size-14 text-emerald-400" />
|
||||
<div className="mt-5 text-xl font-black">
|
||||
درخواست شما ثبت شد
|
||||
</div>
|
||||
<p className="mt-2.5 max-w-sm text-sm leading-7 text-muted">
|
||||
کارشناسان ما تا ۲۴ ساعت آینده برای هماهنگی جلسه بازدید با
|
||||
شما تماس میگیرند.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStatus("idle")}
|
||||
className="mt-7 rounded-full border border-line px-7 py-3 text-sm transition-colors hover:border-accent hover:text-accent"
|
||||
>
|
||||
ثبت درخواست جدید
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-8 grid gap-5 sm:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="f-name"
|
||||
className="mb-2 block text-xs text-muted"
|
||||
>
|
||||
نام و نام خانوادگی *
|
||||
</label>
|
||||
<input
|
||||
id="f-name"
|
||||
required
|
||||
placeholder="مثلاً: علی رضایی"
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="f-org"
|
||||
className="mb-2 block text-xs text-muted"
|
||||
>
|
||||
نام سازمان *
|
||||
</label>
|
||||
<input
|
||||
id="f-org"
|
||||
required
|
||||
placeholder="مثلاً: شرکت پتروشیمی خلیج"
|
||||
className={inputCls}
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<label
|
||||
htmlFor="f-phone"
|
||||
className="mb-2 block text-xs text-muted"
|
||||
>
|
||||
شماره تماس *
|
||||
</label>
|
||||
<input
|
||||
id="f-phone"
|
||||
required
|
||||
type="tel"
|
||||
dir="ltr"
|
||||
placeholder="0912 000 0000"
|
||||
className={`${inputCls} text-left`}
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<label
|
||||
htmlFor="f-msg"
|
||||
className="mb-2 block text-xs text-muted"
|
||||
>
|
||||
شرح درخواست
|
||||
</label>
|
||||
<textarea
|
||||
id="f-msg"
|
||||
rows={4}
|
||||
placeholder="تجهیزات موردنیاز، تعداد پرسنل یا هر نکتهای که لازم است بدانیم..."
|
||||
className={`${inputCls} resize-none`}
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "loading"}
|
||||
className="flex w-full items-center justify-center gap-2.5 rounded-full bg-accent py-4 font-bold text-bg transition-all duration-300 hover:bg-accent-soft hover:shadow-[0_8px_45px_rgba(255,90,0,0.45)] disabled:opacity-60"
|
||||
>
|
||||
{status === "loading" ? (
|
||||
<>
|
||||
<Loader2 className="size-5 animate-spin" />
|
||||
در حال ارسال...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="size-5" />
|
||||
ارسال درخواست مشاوره
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useInView } from "framer-motion";
|
||||
|
||||
export function CountUp({
|
||||
end,
|
||||
suffix = "",
|
||||
duration = 1800,
|
||||
}: {
|
||||
end: number;
|
||||
suffix?: string;
|
||||
duration?: number;
|
||||
}) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const inView = useInView(ref, { once: true, margin: "-60px" });
|
||||
const [val, setVal] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inView) return;
|
||||
let raf = 0;
|
||||
const t0 = performance.now();
|
||||
const tick = (t: number) => {
|
||||
const p = Math.min(1, (t - t0) / duration);
|
||||
setVal(Math.round(end * (1 - Math.pow(1 - p, 3))));
|
||||
if (p < 1) raf = requestAnimationFrame(tick);
|
||||
};
|
||||
raf = requestAnimationFrame(tick);
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [inView, end, duration]);
|
||||
|
||||
return (
|
||||
<span ref={ref} className="tabular-nums">
|
||||
{val.toLocaleString("fa-IR")}
|
||||
{suffix && <span className="text-accent">{suffix}</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import Image from "next/image";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Award,
|
||||
BadgeCheck,
|
||||
ChevronDown,
|
||||
HardHat,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
|
||||
const CHIPS = [
|
||||
{ icon: ShieldCheck, label: "ISO 9001:2015" },
|
||||
{ icon: BadgeCheck, label: "CE CERTIFIED" },
|
||||
{ icon: Award, label: "EN STANDARDS" },
|
||||
];
|
||||
|
||||
const MINI_STATS = [
|
||||
{ value: "۱۸+", label: "سال تجربه در صنعت ایمنی" },
|
||||
{ value: "۸۵۰+", label: "سازمان تحت پوشش" },
|
||||
{ value: "۹۸٪", label: "رضایت مشتریان" },
|
||||
];
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<section id="home" className="relative flex min-h-screen flex-col overflow-hidden">
|
||||
{/* ---- background ---- */}
|
||||
<div className="absolute inset-0">
|
||||
<Image
|
||||
src="/images/hero.jpg"
|
||||
alt="کارگر صنعتی با تجهیزات ایمنی کامل"
|
||||
fill
|
||||
priority
|
||||
sizes="100vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-l from-bg via-bg/80 to-bg/25" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-bg via-transparent to-bg/60" />
|
||||
<div className="absolute inset-0 bg-grid opacity-50" />
|
||||
</div>
|
||||
|
||||
{/* ---- side tape ---- */}
|
||||
<div className="tape-v absolute bottom-0 end-0 z-10 h-44 w-1.5 opacity-70" />
|
||||
|
||||
{/* ---- content ---- */}
|
||||
<div className="relative z-10 mx-auto flex w-full max-w-7xl flex-1 items-center px-5 pb-24 pt-36 md:px-8">
|
||||
<div className="grid w-full items-center gap-12 lg:grid-cols-12">
|
||||
<div className="lg:col-span-7">
|
||||
<div className="animate-fade-up-1 inline-flex items-center gap-2.5 rounded-full border border-accent/30 bg-accent/10 px-4 py-2 backdrop-blur">
|
||||
<span className="size-1.5 animate-pulse rounded-full bg-accent" />
|
||||
<span className="text-xs font-medium text-accent">
|
||||
تأمینکننده رسمی تجهیزات حفاظت فردی (PPE)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="animate-fade-up-2 mt-7 text-5xl font-black leading-[1.15] md:text-7xl md:leading-[1.1]">
|
||||
ایمنی یعنی
|
||||
<br />
|
||||
<span className="mt-3 inline-block -rotate-1 rounded-lg bg-hazard px-4 py-1.5 text-bg shadow-[0_10px_50px_rgba(255,184,0,0.25)]">
|
||||
بازگشت به خانه
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="animate-fade-up-3 mt-7 max-w-xl text-base leading-9 text-ink/70 md:text-lg md:leading-10">
|
||||
آریا ایمن با بیش از ۱۸ سال تجربه، تأمینکننده تخصصی تجهیزات ایمنی و
|
||||
حفاظت فردی برای سازمانهایی است که جان انسانها را جدی میگیرند؛ از
|
||||
کلاه ایمنی تا هارنس نجات، از مشاوره تا بازرسی و آموزش.
|
||||
</p>
|
||||
|
||||
<div className="animate-fade-up-4 mt-9 flex flex-wrap items-center gap-4">
|
||||
<a
|
||||
href="#contact"
|
||||
className="group inline-flex items-center gap-2.5 rounded-full bg-accent px-7 py-4 font-bold text-bg transition-all duration-300 hover:bg-accent-soft hover:shadow-[0_8px_45px_rgba(255,90,0,0.5)]"
|
||||
>
|
||||
درخواست مشاوره رایگان
|
||||
<ArrowLeft className="size-5 transition-transform duration-300 group-hover:-translate-x-1.5" />
|
||||
</a>
|
||||
<a
|
||||
href="#products"
|
||||
className="inline-flex items-center gap-2.5 rounded-full border border-ink/25 px-7 py-4 font-bold text-ink backdrop-blur transition-colors duration-300 hover:border-accent hover:text-accent"
|
||||
>
|
||||
مشاهده محصولات
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="animate-fade-up-5 mt-10 flex flex-wrap gap-3">
|
||||
{CHIPS.map((c) => (
|
||||
<span
|
||||
key={c.label}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-ink/15 bg-bg/50 px-4 py-2 font-mono text-[11px] tracking-wider text-ink/70 backdrop-blur transition-colors hover:border-accent/50 hover:text-ink"
|
||||
>
|
||||
<c.icon className="size-4 text-accent" />
|
||||
{c.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---- rotating badge + status card ---- */}
|
||||
<div className="hidden flex-col items-center gap-10 lg:col-span-5 lg:flex">
|
||||
<div className="animate-fade-up-6 relative">
|
||||
<div className="relative size-44">
|
||||
<div className="animate-spin-slow absolute inset-0">
|
||||
<svg viewBox="0 0 120 120" className="size-full">
|
||||
<defs>
|
||||
<path
|
||||
id="badge-circle"
|
||||
d="M60,60 m-47,0 a47,47 0 1,1 94,0 a47,47 0 1,1 -94,0"
|
||||
/>
|
||||
</defs>
|
||||
<text
|
||||
fontSize="9"
|
||||
letterSpacing="2.6"
|
||||
className="fill-ink/60 font-mono"
|
||||
>
|
||||
<textPath href="#badge-circle">
|
||||
ARIA IMEN SAFETY • ISO 9001 • CE CERTIFIED • EST 2007 •
|
||||
</textPath>
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="flex size-20 items-center justify-center rounded-full border border-accent/30 bg-accent/10">
|
||||
<HardHat className="size-9 text-accent" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="animate-fade-up-7 w-full max-w-sm border border-line bg-bg/70 p-5 backdrop-blur-md">
|
||||
<div className="flex items-center justify-between font-mono text-[11px]">
|
||||
<span className="text-muted">SITE STATUS</span>
|
||||
<span className="flex items-center gap-2 text-emerald-400">
|
||||
<span className="size-1.5 animate-pulse rounded-full bg-emerald-400" />
|
||||
SECURE
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3.5 flex items-center justify-between font-mono text-[11px]">
|
||||
<span className="text-muted">PPE COMPLIANCE</span>
|
||||
<span className="font-semibold text-accent">۱۰۰٪</span>
|
||||
</div>
|
||||
<div className="mt-3.5 flex items-center justify-between font-mono text-[11px]">
|
||||
<span className="text-muted">آخرین بازرسی</span>
|
||||
<span>۱۴۰۴/۰۸/۱۲</span>
|
||||
</div>
|
||||
<div className="my-4 h-px bg-line" />
|
||||
<div className="font-mono text-[11px] leading-6 text-muted">
|
||||
سیستم پایش حرفهای آریا ایمن
|
||||
<span className="animate-blink text-accent">▍</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---- scroll hint ---- */}
|
||||
<div className="animate-fade-up-9 absolute bottom-28 left-1/2 z-10 hidden md:block">
|
||||
<div className="-translate-x-1/2 flex flex-col items-center gap-2">
|
||||
<span className="font-mono text-[10px] tracking-[0.35em] text-muted">
|
||||
SCROLL
|
||||
</span>
|
||||
<ChevronDown className="size-4 animate-bounce text-accent" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---- bottom bar ---- */}
|
||||
<div className="animate-fade-up-8 relative z-10 border-t border-ink/10 backdrop-blur-sm">
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-px bg-ink/10 sm:grid-cols-3">
|
||||
{MINI_STATS.map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="flex items-baseline gap-4 bg-bg/60 px-6 py-5 backdrop-blur-md md:px-8"
|
||||
>
|
||||
<span className="text-2xl font-black text-ink md:text-3xl">
|
||||
{s.value}
|
||||
</span>
|
||||
<span className="text-xs leading-5 text-muted md:text-sm">
|
||||
{s.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
AnimatePresence,
|
||||
motion,
|
||||
useMotionValue,
|
||||
useSpring,
|
||||
} from "framer-motion";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { EASE, faNum, fade } from "@/components/ui/Reveal";
|
||||
import { SectionHead } from "@/components/ui/SectionHead";
|
||||
|
||||
const INDUSTRIES = [
|
||||
{
|
||||
name: "نفت و گاز",
|
||||
en: "OIL & GAS",
|
||||
count: "۱۴۰+",
|
||||
img: "/images/industries/oil-gas.jpg",
|
||||
},
|
||||
{
|
||||
name: "ساختمان و عمران",
|
||||
en: "CONSTRUCTION",
|
||||
count: "۲۱۰+",
|
||||
img: "/images/industries/construction.jpg",
|
||||
},
|
||||
{
|
||||
name: "پتروشیمی",
|
||||
en: "PETROCHEMICAL",
|
||||
count: "۹۰+",
|
||||
img: "/images/industries/petrochemical.jpg",
|
||||
},
|
||||
{
|
||||
name: "صنایع معدنی",
|
||||
en: "MINING",
|
||||
count: "۷۵+",
|
||||
img: "/images/industries/mining.jpg",
|
||||
},
|
||||
{
|
||||
name: "برق و انرژی",
|
||||
en: "POWER & ENERGY",
|
||||
count: "۶۰+",
|
||||
img: "/images/industries/power-energy.jpg",
|
||||
},
|
||||
{
|
||||
name: "حملونقل و لجستیک",
|
||||
en: "TRANSPORT",
|
||||
count: "۴۵+",
|
||||
img: "/images/industries/transport.jpg",
|
||||
},
|
||||
];
|
||||
|
||||
export function Industries() {
|
||||
const [hovered, setHovered] = useState<number | null>(null);
|
||||
const x = useMotionValue(0);
|
||||
const y = useMotionValue(0);
|
||||
const sx = useSpring(x, { stiffness: 220, damping: 26, mass: 0.6 });
|
||||
const sy = useSpring(y, { stiffness: 220, damping: 26, mass: 0.6 });
|
||||
|
||||
return (
|
||||
<section
|
||||
id="industries"
|
||||
aria-labelledby="industries-title"
|
||||
className="relative border-t border-line py-24 md:py-32"
|
||||
onMouseMove={(e) => {
|
||||
x.set(e.clientX);
|
||||
y.set(e.clientY);
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto max-w-7xl px-5 md:px-8">
|
||||
<SectionHead
|
||||
headingId="industries-title"
|
||||
tag="۰۵ — صنایع"
|
||||
title={
|
||||
<>
|
||||
حضور در سختترین
|
||||
<br />
|
||||
میدانهای کاری
|
||||
</>
|
||||
}
|
||||
desc="جایی که دمای محیط از ۵۰ درجه گذر میکند یا ارتفاع کار به دهها متر میرسد، تجهیزات ما باید بینقص عمل کنند — و عمل میکنند."
|
||||
/>
|
||||
|
||||
<div className="mt-16" onMouseLeave={() => setHovered(null)}>
|
||||
{INDUSTRIES.map((ind, i) => (
|
||||
<motion.div
|
||||
key={ind.name}
|
||||
{...fade(i * 0.05)}
|
||||
data-hover
|
||||
onMouseEnter={() => setHovered(i)}
|
||||
className={`group relative flex cursor-pointer items-center justify-between gap-4 border-t border-line px-2 py-7 transition-colors duration-300 hover:bg-panel/40 md:px-4 md:py-9 ${
|
||||
i === INDUSTRIES.length - 1 ? "border-b" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex min-w-0 items-baseline gap-5 md:gap-8">
|
||||
<span className="shrink-0 font-mono text-xs text-muted">
|
||||
{faNum(String(i + 1).padStart(2, "0"))}
|
||||
</span>
|
||||
<h3 className="truncate text-2xl font-black transition-all duration-300 group-hover:-translate-x-2 group-hover:text-accent md:text-5xl">
|
||||
{ind.name}
|
||||
</h3>
|
||||
<span className="hidden shrink-0 font-mono text-[10px] tracking-[0.3em] text-muted/60 md:inline">
|
||||
{ind.en}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-4 md:gap-8">
|
||||
<span className="text-xs text-muted md:text-sm">
|
||||
{ind.count} پروژه
|
||||
</span>
|
||||
<span className="flex size-10 items-center justify-center rounded-full border border-line text-muted transition-all duration-300 group-hover:-translate-x-1.5 group-hover:border-accent group-hover:text-accent md:size-12">
|
||||
<ArrowLeft className="size-4 md:size-5" />
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---- floating preview (desktop) ---- */}
|
||||
<motion.div
|
||||
style={{ x: sx, y: sy }}
|
||||
className="pointer-events-none fixed left-0 top-0 z-40 hidden lg:block"
|
||||
>
|
||||
<AnimatePresence>
|
||||
{hovered !== null && (
|
||||
<motion.div
|
||||
key={hovered}
|
||||
initial={{ opacity: 0, scale: 0.82, rotate: -3 }}
|
||||
animate={{ opacity: 1, scale: 1, rotate: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, rotate: 2 }}
|
||||
transition={{ duration: 0.35, ease: EASE }}
|
||||
className="relative -ml-40 -mt-28 h-56 w-80 overflow-hidden rounded-xl border border-accent/40 shadow-[0_25px_80px_rgba(0,0,0,0.6)]"
|
||||
>
|
||||
<Image
|
||||
src={INDUSTRIES[hovered].img}
|
||||
alt={INDUSTRIES[hovered].name}
|
||||
fill
|
||||
sizes="320px"
|
||||
className="object-cover grayscale-[35%] transition-all duration-500"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-accent/15 mix-blend-multiply" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-bg/70 to-transparent" />
|
||||
<div className="absolute bottom-3 start-3 rounded-md border border-line bg-bg/80 px-3 py-1.5 font-mono text-[10px] tracking-widest text-ink backdrop-blur">
|
||||
{INDUSTRIES[hovered].en}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import {
|
||||
ArrowLeft,
|
||||
BadgeCheck,
|
||||
PackageCheck,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { EASE, Reveal, faNum } from "@/components/ui/Reveal";
|
||||
import { SectionHead } from "@/components/ui/SectionHead";
|
||||
|
||||
type Product = {
|
||||
code: string;
|
||||
name: string;
|
||||
en: string;
|
||||
desc: string;
|
||||
img: string;
|
||||
specs: string[];
|
||||
};
|
||||
|
||||
const PRODUCTS: Product[] = [
|
||||
{
|
||||
code: "AP-101",
|
||||
name: "کلاه ایمنی",
|
||||
en: "SAFETY HELMET",
|
||||
img: "/images/products/helmet.jpg",
|
||||
desc: "کلاههای ایمنی با بدنه HDPE مقاوم به ضربه و ترتیهبند ضدتعریق؛ با قابلیت نصب هدفون و شیشه محافظ صورت برای محیطهای ساختمانی و صنعتی.",
|
||||
specs: ["EN 397", "مقاوم به ضربه ۵۰ ژول", "بدنه HDPE"],
|
||||
},
|
||||
{
|
||||
code: "AP-205",
|
||||
name: "دستکش ضدبرش",
|
||||
en: "CUT-RESISTANT GLOVE",
|
||||
img: "/images/products/gloves.jpg",
|
||||
desc: "دستکشهای سطح ۵ ضدبرش با پوشش نیتریل؛ طراحیشده برای کار با ورق فلز، شیشه و ابزار برنده با چسبندگی عالی در محیط خشک و چرب.",
|
||||
specs: ["EN 388 — 4X43C", "سطح ۵ ضدبرش", "پوشش نیتریل"],
|
||||
},
|
||||
{
|
||||
code: "AP-310",
|
||||
name: "کفش ایمنی",
|
||||
en: "SAFETY BOOTS",
|
||||
img: "/images/products/shoes.jpg",
|
||||
desc: "کفشهای ایمنی با پنجه فولادی و کف مقاوم در برابر سوراخشدگی و روغن؛ راحتی تمامروز برای شیفتهای طولانی در سایت.",
|
||||
specs: ["EN ISO 20345", "پنجه فولادی", "کف ضدروغن"],
|
||||
},
|
||||
{
|
||||
code: "AP-412",
|
||||
name: "عینک ایمنی",
|
||||
en: "SAFETY GOGGLES",
|
||||
img: "/images/products/goggles.jpg",
|
||||
desc: "عینکهای محافظ ضدخش با پوشش UV400 و تهویه ضدمه؛ سازگار با عینک طبی برای کارهای جوشکاری، سنگزنی و محیطهای شیمیایی.",
|
||||
specs: ["EN 166", "عدسی ضدخش", "پوشش UV400"],
|
||||
},
|
||||
{
|
||||
code: "AP-508",
|
||||
name: "هارنس ضدسقوط",
|
||||
en: "FULL BODY HARNESS",
|
||||
img: "/images/products/harness.jpg",
|
||||
desc: "هارنس تمامبدنی با دو شیر پیچی و نوارهای دوختهشده با نخ نارنجی سیگنال؛ تستشده برای کار در ارتفاع با ظرفیت ۱۰۰ کیلوگرم.",
|
||||
specs: ["EN 361", "ظرفیت ۱۰۰ کیلوگرم", "شیر دوگانه"],
|
||||
},
|
||||
{
|
||||
code: "AP-615",
|
||||
name: "لباس کار بازتابنده",
|
||||
en: "HI-VIS WORKWEAR",
|
||||
img: "/images/products/vest.jpg",
|
||||
desc: "جلیقهها و لباسهای کار بازتابنده کلاس ۲ با نوارهای ۳M؛ دیدهشدن کامل پرسنل در شب، مه و محیطهای پرتردد خودرویی.",
|
||||
specs: ["EN ISO 20471", "کلاس ۲", "نوار ۳M"],
|
||||
},
|
||||
];
|
||||
|
||||
const TRUST = [
|
||||
{
|
||||
icon: BadgeCheck,
|
||||
title: "گارانتی اصالت کالا",
|
||||
desc: "تمام محصولات با فاکتور رسمی و کد رهگیری اصالت",
|
||||
},
|
||||
{
|
||||
icon: PackageCheck,
|
||||
title: "تست نمونه قبل از خرید",
|
||||
desc: "نمونههای نمایشی برای ارزیابی تیم HSE شما",
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: "امکان قرارداد سالانه",
|
||||
desc: "قیمت ثابت و تأمین مستمر برای سازمانهای بزرگ",
|
||||
},
|
||||
];
|
||||
|
||||
export function Products() {
|
||||
const [active, setActive] = useState(0);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="products"
|
||||
aria-labelledby="products-title"
|
||||
className="relative py-24 md:py-32"
|
||||
>
|
||||
<div className="mx-auto max-w-7xl px-5 md:px-8">
|
||||
<div className="flex flex-wrap items-end justify-between gap-6">
|
||||
<SectionHead
|
||||
headingId="products-title"
|
||||
tag="۰۱ — محصولات"
|
||||
title={
|
||||
<>
|
||||
خط اول دفاع،
|
||||
<br />
|
||||
تجهیزات ماست
|
||||
</>
|
||||
}
|
||||
desc="هر محصول پیش از عرضه، زیر ذرهبین کنترل کیفیت و تطبیق با استانداردهای جهانی قرار میگیرد. روی هر پنل کلیک کنید."
|
||||
/>
|
||||
<Reveal delay={0.2} className="hidden md:block">
|
||||
<a
|
||||
href="#contact"
|
||||
className="group inline-flex items-center gap-2 rounded-full border border-line px-6 py-3 text-sm text-muted transition-colors hover:border-accent hover:text-accent"
|
||||
>
|
||||
دریافت کاتالوگ کامل
|
||||
<ArrowLeft className="size-4 transition-transform group-hover:-translate-x-1" />
|
||||
</a>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-14 max-w-7xl px-5 md:px-8">
|
||||
<div className="flex flex-col gap-2 md:h-[74vh] md:min-h-[580px] md:flex-row">
|
||||
{PRODUCTS.map((p, i) => {
|
||||
const isActive = active === i;
|
||||
return (
|
||||
<div
|
||||
key={p.code}
|
||||
data-hover
|
||||
onMouseEnter={() => setActive(i)}
|
||||
onClick={() => setActive(i)}
|
||||
className={`group relative cursor-pointer overflow-hidden rounded-xl border transition-all duration-700 ease-[cubic-bezier(0.22,1,0.36,1)] ${
|
||||
isActive
|
||||
? "h-[500px] flex-[3.4] border-accent/60 md:h-auto"
|
||||
: "h-24 flex-[1] border-line hover:border-ink/30 md:h-auto"
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={p.img}
|
||||
alt={p.name}
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
className={`object-cover transition-all duration-700 ${
|
||||
isActive
|
||||
? "scale-100 grayscale-0"
|
||||
: "scale-110 grayscale group-hover:grayscale-0"
|
||||
}`}
|
||||
/>
|
||||
<div
|
||||
className={`absolute inset-0 transition-opacity duration-700 ${
|
||||
isActive
|
||||
? "bg-gradient-to-t from-bg via-bg/35 to-bg/5"
|
||||
: "bg-gradient-to-t from-bg/95 via-bg/30 to-transparent"
|
||||
}`}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-x-0 top-0 flex items-center justify-between p-5">
|
||||
<span className="font-mono text-[10px] tracking-widest text-ink/60">
|
||||
{p.code}
|
||||
</span>
|
||||
<span
|
||||
className={`font-mono text-xs transition-colors duration-500 ${
|
||||
isActive ? "text-accent" : "text-ink/40"
|
||||
}`}
|
||||
>
|
||||
{faNum(String(i + 1).padStart(2, "0"))}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 p-5 md:p-6">
|
||||
<h3
|
||||
className={`font-black transition-all duration-500 ${
|
||||
isActive ? "text-2xl" : "text-base md:text-sm"
|
||||
}`}
|
||||
>
|
||||
{p.name}
|
||||
</h3>
|
||||
<p className="mt-1 font-mono text-[10px] tracking-[0.2em] text-muted">
|
||||
{p.en}
|
||||
</p>
|
||||
<AnimatePresence>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 18 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
transition={{
|
||||
duration: 0.45,
|
||||
delay: 0.12,
|
||||
ease: EASE,
|
||||
}}
|
||||
>
|
||||
<p className="mt-4 max-w-md text-sm leading-8 text-ink/75">
|
||||
{p.desc}
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{p.specs.map((s) => (
|
||||
<span
|
||||
key={s}
|
||||
className="rounded-full border border-accent/40 bg-accent/10 px-3 py-1 font-mono text-[10px] text-accent"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-10 grid max-w-7xl gap-px overflow-hidden rounded-xl border border-line bg-line md:grid-cols-3">
|
||||
{TRUST.map((t, i) => (
|
||||
<Reveal key={t.title} delay={i * 0.08}>
|
||||
<div className="flex h-full items-center gap-4 bg-panel/60 p-6">
|
||||
<span className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent/10 text-accent">
|
||||
<t.icon className="size-6" />
|
||||
</span>
|
||||
<div>
|
||||
<div className="font-bold">{t.title}</div>
|
||||
<div className="mt-1 text-xs leading-6 text-muted">
|
||||
{t.desc}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import {
|
||||
ClipboardCheck,
|
||||
Gauge,
|
||||
GraduationCap,
|
||||
Plus,
|
||||
Truck,
|
||||
} from "lucide-react";
|
||||
import { EASE, Reveal, faNum } from "@/components/ui/Reveal";
|
||||
import { SectionHead } from "@/components/ui/SectionHead";
|
||||
|
||||
const SERVICES = [
|
||||
{
|
||||
icon: ClipboardCheck,
|
||||
title: "بازرسی و ارزیابی ریسک",
|
||||
desc: "تیم مهندسی ما با بازدید از سایت شما، خطرات را شناسایی، اولویتبندی و برای هر کدام راهکار اجرایی مشخص با برآورد هزینه ارائه میدهد.",
|
||||
items: [
|
||||
"ارزیابی HAZOP و HAZID",
|
||||
"شناسایی و اولویتبندی خطرات",
|
||||
"گزارش مدیریتی با راهکار اجرایی",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: GraduationCap,
|
||||
title: "آموزش تخصصی ایمنی",
|
||||
desc: "از دورههای عمومی تا تربیت مربی؛ آموزشهای عملی و میدانی که ایمنی را از دفتر کلانها به رفتار روزمره پرسنل تبدیل میکند.",
|
||||
items: [
|
||||
"دورههای عمومی و تخصصی HSE",
|
||||
"تربیت مربی ایمنی سایت",
|
||||
"مانور و شبیهسازی شرایط اضطراری",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Gauge,
|
||||
title: "تست و کالیبراسیون تجهیزات",
|
||||
desc: "تجهیزات ایمنی فقط وقتی قابل اعتمادند که تست شده باشند. تست دورهای، سرویس و صدور گواهی معتبر را به تیم تخصصی ما بسپارید.",
|
||||
items: [
|
||||
"تست هارنس و لاین نجات",
|
||||
"کالیبراسیون آشکارسازهای گاز",
|
||||
"سرویس دورهای و صدور گواهی",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Truck,
|
||||
title: "تأمین و لجستیک سازمانی",
|
||||
desc: "با قرارداد سالانه، قیمتگذاری شفاف و انبار محلی در سایت شما، نگرانی کمبود تجهیزات را از فهرست دغدغههایتان حذف کنید.",
|
||||
items: [
|
||||
"قرارداد سالانه با قیمت ثابت",
|
||||
"انبار محلی در سایت شما",
|
||||
"تحویل سراسری در ۴۸ ساعت",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Services() {
|
||||
const [open, setOpen] = useState(0);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="services"
|
||||
aria-labelledby="services-title"
|
||||
className="border-t border-line py-24 md:py-32"
|
||||
>
|
||||
<div className="mx-auto max-w-7xl px-5 md:px-8">
|
||||
<SectionHead
|
||||
headingId="services-title"
|
||||
tag="۰۳ — خدمات"
|
||||
title={
|
||||
<>
|
||||
فراتر از تأمین؛
|
||||
<br />
|
||||
همراهی کامل ایمنی
|
||||
</>
|
||||
}
|
||||
desc="ما در آریا ایمن، تجهیزات را تحویل میدهیم و کنار شما میمانیم؛ از نخستین ارزیابی ریسک تا آخرین تست دورهای."
|
||||
/>
|
||||
|
||||
<div className="mt-16">
|
||||
{SERVICES.map((s, i) => {
|
||||
const isOpen = open === i;
|
||||
const Icon = s.icon;
|
||||
return (
|
||||
<Reveal key={s.title} delay={i * 0.05}>
|
||||
<div
|
||||
className={`border-t border-line ${
|
||||
i === SERVICES.length - 1 ? "border-b" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => setOpen(isOpen ? -1 : i)}
|
||||
className="group flex w-full items-center gap-5 py-7 text-start md:gap-10 md:py-9"
|
||||
>
|
||||
<span
|
||||
className={`font-mono text-sm transition-colors duration-300 md:text-base ${
|
||||
isOpen ? "text-accent" : "text-muted"
|
||||
}`}
|
||||
>
|
||||
{faNum(String(i + 1).padStart(2, "0"))}
|
||||
</span>
|
||||
<span
|
||||
className={`flex-1 text-2xl font-black transition-colors duration-300 md:text-4xl ${
|
||||
isOpen
|
||||
? "text-accent"
|
||||
: "text-ink group-hover:text-accent"
|
||||
}`}
|
||||
>
|
||||
{s.title}
|
||||
</span>
|
||||
<span
|
||||
className={`hidden size-12 shrink-0 items-center justify-center rounded-full border transition-all duration-500 md:flex ${
|
||||
isOpen
|
||||
? "rotate-45 border-accent bg-accent text-bg"
|
||||
: "border-line text-muted group-hover:border-accent/50 group-hover:text-accent"
|
||||
}`}
|
||||
>
|
||||
<Plus className="size-5" />
|
||||
</span>
|
||||
</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.55, ease: EASE }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="grid gap-8 pb-10 md:grid-cols-2 md:ps-16">
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent/10 text-accent">
|
||||
<Icon className="size-6" />
|
||||
</span>
|
||||
<p className="text-sm leading-8 text-muted">
|
||||
{s.desc}
|
||||
</p>
|
||||
</div>
|
||||
<ul className="space-y-3.5">
|
||||
{s.items.map((it) => (
|
||||
<li
|
||||
key={it}
|
||||
className="flex items-center gap-3 text-sm text-ink/80"
|
||||
>
|
||||
<span className="size-1.5 shrink-0 rounded-full bg-accent" />
|
||||
{it}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</Reveal>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { FileSearch, ShieldCheck } from "lucide-react";
|
||||
import { Reveal } from "@/components/ui/Reveal";
|
||||
import { SectionHead } from "@/components/ui/SectionHead";
|
||||
|
||||
const STANDARDS = [
|
||||
{ code: "EN 397", label: "کلاه ایمنی صنعتی" },
|
||||
{ code: "EN 388", label: "دستکشهای مکانیکی" },
|
||||
{ code: "EN 166", label: "محافظ چشم و صورت" },
|
||||
{ code: "EN 361", label: "هارنس ضدسقوط" },
|
||||
{ code: "ISO 20345", label: "کفش ایمنی" },
|
||||
{ code: "ISO 9001", label: "سیستم مدیریت کیفیت" },
|
||||
];
|
||||
|
||||
export function Standards() {
|
||||
return (
|
||||
<section
|
||||
id="standards"
|
||||
aria-labelledby="standards-title"
|
||||
className="relative overflow-hidden border-t border-line py-24 md:py-32"
|
||||
>
|
||||
{/* watermark */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="text-outline pointer-events-none absolute -bottom-14 -start-8 select-none font-mono text-[24vw] font-bold leading-none opacity-60"
|
||||
>
|
||||
CE
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto max-w-7xl px-5 md:px-8">
|
||||
<div className="grid items-center gap-14 lg:grid-cols-12">
|
||||
<div className="lg:col-span-5">
|
||||
<SectionHead
|
||||
headingId="standards-title"
|
||||
tag="۰۴ — استانداردها"
|
||||
title={
|
||||
<>
|
||||
مهرتأیید جهانی
|
||||
<br />
|
||||
روی هر محصول
|
||||
</>
|
||||
}
|
||||
desc="ما فقط تجهیزاتی را عرضه میکنیم که تأییدیه معتبر بینالمللی و تست آزمایشگاهی داشته باشند؛ بدون استثنا."
|
||||
/>
|
||||
<Reveal delay={0.25}>
|
||||
<div className="mt-8 flex items-start gap-4 rounded-xl border border-accent/25 bg-accent/5 p-5">
|
||||
<FileSearch className="size-6 shrink-0 text-accent" />
|
||||
<p className="text-sm leading-8 text-ink/80">
|
||||
شماره سریال تمام محصولات در پایگاه داده استانداردهای اروپا
|
||||
قابل استعلام است و گواهی تست هر بچ تحویل مشتری میشود.
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.35}>
|
||||
<a
|
||||
href="#contact"
|
||||
className="group mt-8 inline-flex items-center gap-2.5 rounded-full border border-line px-6 py-3.5 text-sm font-bold transition-colors hover:border-accent hover:text-accent"
|
||||
>
|
||||
<ShieldCheck className="size-4 text-accent" />
|
||||
درخواست گواهی تست محصولات
|
||||
</a>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:col-span-7 xl:grid-cols-3">
|
||||
{STANDARDS.map((s, i) => (
|
||||
<Reveal key={s.code} delay={i * 0.06}>
|
||||
<div
|
||||
data-hover
|
||||
className="group relative h-full border border-line bg-panel/50 p-6 text-center transition-all duration-500 hover:-translate-y-1.5 hover:border-accent/50 hover:bg-panel"
|
||||
>
|
||||
<span className="absolute end-3 top-3 font-mono text-[9px] tracking-[0.2em] text-muted/50">
|
||||
CERT
|
||||
</span>
|
||||
<div className="mx-auto flex size-24 items-center justify-center rounded-full border border-dashed border-ink/20 transition-all duration-500 group-hover:rotate-12 group-hover:border-accent/60">
|
||||
<div>
|
||||
<div className="font-mono text-base font-bold text-ink transition-colors duration-300 group-hover:text-accent">
|
||||
{s.code}
|
||||
</div>
|
||||
<div className="mx-auto mt-1.5 h-px w-8 bg-line transition-colors duration-300 group-hover:bg-accent/50" />
|
||||
<div className="mt-1.5 font-mono text-[8px] tracking-[0.25em] text-muted">
|
||||
EURO NORM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 text-sm font-medium text-ink/85">
|
||||
{s.label}
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Reveal } from "@/components/ui/Reveal";
|
||||
import { CountUp } from "./CountUp";
|
||||
|
||||
const STATS = [
|
||||
{ end: 18, suffix: "+", label: "سال تجربه در صنعت ایمنی", note: "SINCE 2007" },
|
||||
{ end: 2400, suffix: "+", label: "کد کالای ایمنی", note: "PPE CATALOG" },
|
||||
{ end: 850, suffix: "+", label: "مشتری سازمانی فعال", note: "B2B CLIENTS" },
|
||||
{ end: 98, suffix: "٪", label: "رضایت مشتریان", note: "SATISFACTION" },
|
||||
];
|
||||
|
||||
export function Stats() {
|
||||
return (
|
||||
<section className="relative" aria-label="آمار و دستاوردهای آریا ایمن">
|
||||
<div className="grid grid-cols-2 gap-px border-y border-line bg-line lg:grid-cols-4">
|
||||
{STATS.map((s, i) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="group relative bg-bg p-8 transition-colors duration-500 hover:bg-panel md:p-10"
|
||||
>
|
||||
<span className="absolute end-5 top-5 font-mono text-[10px] tracking-[0.25em] text-muted/50 transition-colors duration-500 group-hover:text-accent/70">
|
||||
{s.note}
|
||||
</span>
|
||||
<Reveal delay={i * 0.08}>
|
||||
<div className="text-4xl font-black md:text-6xl">
|
||||
<CountUp end={s.end} suffix={s.suffix} />
|
||||
</div>
|
||||
<div className="mt-3 text-sm text-muted">{s.label}</div>
|
||||
</Reveal>
|
||||
<span className="absolute bottom-0 start-0 h-0.5 w-0 bg-accent transition-all duration-500 group-hover:w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
|
||||
const ITEMS = [
|
||||
"ISO 9001:2015",
|
||||
"CE CERTIFIED",
|
||||
"گارانتی اصالت کالا",
|
||||
"تحویل سراسری کشوری",
|
||||
"مشاوره و بازرسی تخصصی رایگان",
|
||||
"EN STANDARDS",
|
||||
"پشتیبانی ۲۴/۷ قراردادهای سازمانی",
|
||||
"تست نمونه قبل از خرید",
|
||||
];
|
||||
|
||||
export function TapeMarquee() {
|
||||
return (
|
||||
<section
|
||||
aria-hidden
|
||||
className="pointer-events-none relative z-20 overflow-hidden py-10"
|
||||
>
|
||||
<div className="-rotate-[1.6deg] scale-[1.04]">
|
||||
<div className="hazard h-2.5" />
|
||||
<div dir="ltr" className="overflow-hidden bg-hazard py-3.5 text-bg">
|
||||
<div className="animate-marquee flex w-max">
|
||||
{[...ITEMS, ...ITEMS].map((item, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="flex items-center gap-7 px-7 text-sm font-extrabold whitespace-nowrap"
|
||||
>
|
||||
{item}
|
||||
<ShieldCheck className="size-4 shrink-0" strokeWidth={2.5} />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="hazard h-2.5" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Quote } from "lucide-react";
|
||||
import { EASE, faNum } from "@/components/ui/Reveal";
|
||||
import { SectionHead } from "@/components/ui/SectionHead";
|
||||
|
||||
const QUOTES = [
|
||||
{
|
||||
text: "آریا ایمن فقط تأمینکننده نیست؛ شریک ایمنی ماست. بازرسیهای دورهای و آموزشهای میدانیشان نرخ حوادث سایت را به کمترین رقم تاریخ مجموعه رساند.",
|
||||
name: "مهندس رضا کاظمی",
|
||||
role: "مدیر HSE — مجتمع پتروشیمی خلیج فارس",
|
||||
},
|
||||
{
|
||||
text: "کیفیت تجهیزات و سرعت پاسخگویی در سطحی بود که قرارداد سالانه را بدون هیچ تردیدی تمدید کردیم؛ از خط تولید تا دفتر مرکزی.",
|
||||
name: "سارا محمدی",
|
||||
role: "سرپرست تدارکات — هلدینگ ساختمانی آرکان",
|
||||
},
|
||||
{
|
||||
text: "۱۴۰ کیلومتر دورتر از سایت، در عرض ۲۴ ساعت تجهیزات حیاتی را تحویل گرفتیم. به این میگویند شریک واقعی کاری.",
|
||||
name: "امیرحسین راد",
|
||||
role: "مدیر پروژه — صنایع معدنی کاوه",
|
||||
},
|
||||
];
|
||||
|
||||
export function Testimonials() {
|
||||
const [idx, setIdx] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setIdx((i) => (i + 1) % QUOTES.length), 7000);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
const q = QUOTES[idx];
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="testimonials-title"
|
||||
className="relative overflow-hidden border-t border-line py-24 md:py-32"
|
||||
>
|
||||
<div className="absolute inset-0 bg-grid opacity-40" />
|
||||
<div className="pointer-events-none absolute -top-10 -end-10 select-none font-mono text-[18vw] font-bold leading-none text-outline opacity-50">
|
||||
"
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto max-w-4xl px-5 text-center md:px-8">
|
||||
<SectionHead
|
||||
headingId="testimonials-title"
|
||||
center
|
||||
tag="۰۶ — اعتماد"
|
||||
title="آنچه مشتریان ما میگویند"
|
||||
/>
|
||||
|
||||
<div className="mt-14 flex min-h-[320px] flex-col items-center md:min-h-[280px]">
|
||||
<span className="flex size-16 items-center justify-center rounded-full border border-accent/30 bg-accent/10">
|
||||
<Quote className="size-7 text-accent" />
|
||||
</span>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.blockquote
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 26 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -26 }}
|
||||
transition={{ duration: 0.55, ease: EASE }}
|
||||
className="mt-8"
|
||||
>
|
||||
<p className="text-xl font-bold leading-[2.1] text-ink/90 md:text-2xl md:leading-[2]">
|
||||
«{q.text}»
|
||||
</p>
|
||||
<footer className="mt-8">
|
||||
<div className="text-lg font-black">{q.name}</div>
|
||||
<div className="mt-2 font-mono text-xs text-muted">
|
||||
{q.role}
|
||||
</div>
|
||||
</footer>
|
||||
</motion.blockquote>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 flex items-center justify-center gap-3">
|
||||
{QUOTES.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setIdx(i)}
|
||||
aria-label={`نظر شماره ${faNum(i + 1)}`}
|
||||
className={`h-1.5 rounded-full transition-all duration-500 ${
|
||||
i === idx ? "w-12 bg-accent" : "w-4 bg-line hover:bg-muted"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
type Props = { data: Record<string, unknown> | Record<string, unknown>[] };
|
||||
|
||||
export function JsonLd({ data }: Props) {
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => (
|
||||
<script
|
||||
key={i}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(item).replace(/</g, "\\u003c"),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
|
||||
export function Logo() {
|
||||
return (
|
||||
<a href="#home" className="flex items-center gap-3" aria-label="آریا ایمن">
|
||||
<span className="flex size-10 items-center justify-center rounded-xl bg-accent text-bg shadow-[0_0_28px_rgba(255,90,0,0.35)]">
|
||||
<ShieldCheck className="size-6" />
|
||||
</span>
|
||||
<span className="leading-tight">
|
||||
<span className="block text-lg font-black">آریا ایمن</span>
|
||||
<span className="block font-mono text-[9px] tracking-[0.32em] text-muted">
|
||||
ARIA IMEN CO.
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export const EASE: [number, number, number, number] = [0.22, 1, 0.36, 1];
|
||||
|
||||
/** Convert latin digits to Persian digits */
|
||||
export const faNum = (v: number | string): string =>
|
||||
String(v).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]);
|
||||
|
||||
/** While-in-view fade-up props for motion elements */
|
||||
export const fade = (delay = 0, y = 36) =>
|
||||
({
|
||||
initial: { opacity: 0, y },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-60px" },
|
||||
transition: { duration: 0.8, delay, ease: EASE },
|
||||
}) as any;
|
||||
|
||||
export function Reveal({
|
||||
children,
|
||||
delay = 0,
|
||||
className = "",
|
||||
}: {
|
||||
children: ReactNode;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
initial={{ opacity: 0, y: 34 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-60px" }}
|
||||
transition={{ duration: 0.8, delay, ease: EASE }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Reveal } from "./Reveal";
|
||||
|
||||
export function SectionHead({
|
||||
tag,
|
||||
title,
|
||||
desc,
|
||||
center = false,
|
||||
headingId,
|
||||
}: {
|
||||
tag: string;
|
||||
title: ReactNode;
|
||||
desc?: string;
|
||||
center?: boolean;
|
||||
headingId?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={center ? "text-center" : ""}>
|
||||
<Reveal>
|
||||
<div
|
||||
className={`flex items-center gap-3 ${center ? "justify-center" : ""}`}
|
||||
>
|
||||
<span className="font-mono text-sm text-accent">{"//"}</span>
|
||||
<span className="font-mono text-xs tracking-[0.25em] text-muted">
|
||||
{tag}
|
||||
</span>
|
||||
<span className="h-px w-14 bg-line" />
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.08}>
|
||||
<h2
|
||||
id={headingId}
|
||||
className="mt-5 text-3xl font-black leading-[1.25] md:text-5xl md:leading-[1.2]"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</Reveal>
|
||||
{desc ? (
|
||||
<Reveal delay={0.16}>
|
||||
<p
|
||||
className={`mt-5 max-w-xl leading-8 text-muted ${
|
||||
center ? "mx-auto" : ""
|
||||
}`}
|
||||
>
|
||||
{desc}
|
||||
</p>
|
||||
</Reveal>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export const siteConfig = {
|
||||
name: "آریا ایمن",
|
||||
nameEn: "Aria Imen Safety",
|
||||
url: "https://ariaiman.ir", // TODO: replace with the real production domain before deploy
|
||||
locale: "fa_IR",
|
||||
lang: "fa",
|
||||
dir: "rtl" as const,
|
||||
title: "آریا ایمن | تجهیزات ایمنی و حفاظت فردی صنعتی",
|
||||
description:
|
||||
"آریا ایمن — تأمینکننده تخصصی تجهیزات حفاظت فردی (PPE)، بازرسی، آموزش و راهکارهای ایمنی صنعتی برای سازمانهایی که جان انسانها را جدی میگیرند.",
|
||||
ogImage: "/opengraph-image.png",
|
||||
logo: "/images/hero.jpg", // TODO: replace with a dedicated PNG/SVG logo file when available
|
||||
themeColor: "#0a0a0c",
|
||||
contact: {
|
||||
phone: "+98-21-91008800",
|
||||
phoneDisplay: "۰۲۱ - ۹۱ ۰۰ ۸۸ ۰۰",
|
||||
email: "info@ariaiman.ir",
|
||||
address: {
|
||||
streetAddress: "خیابان کارگر شمالی، خیابان فر، کوچه دوم، پلاک ۱۲",
|
||||
addressLocality: "تهران",
|
||||
addressCountry: "IR",
|
||||
},
|
||||
},
|
||||
socials: [] as string[], // TODO: add real social profile URLs when available
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { siteConfig as s } from "@/config/site";
|
||||
|
||||
const abs = (p: string) => new URL(p, s.url).toString();
|
||||
const clean = <T extends object>(o: T) =>
|
||||
JSON.parse(
|
||||
JSON.stringify(o, (_, v) =>
|
||||
v === "" || (Array.isArray(v) && !v.length) ? undefined : v,
|
||||
),
|
||||
);
|
||||
|
||||
export const organizationSchema = () =>
|
||||
clean({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "LocalBusiness",
|
||||
"@id": `${s.url}/#organization`,
|
||||
name: s.name,
|
||||
url: s.url,
|
||||
logo: abs(s.logo),
|
||||
image: abs(s.logo),
|
||||
email: s.contact.email,
|
||||
telephone: s.contact.phone,
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
streetAddress: s.contact.address.streetAddress,
|
||||
addressLocality: s.contact.address.addressLocality,
|
||||
addressCountry: s.contact.address.addressCountry,
|
||||
},
|
||||
sameAs: s.socials,
|
||||
});
|
||||
|
||||
export const websiteSchema = () => ({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"@id": `${s.url}/#website`,
|
||||
url: s.url,
|
||||
name: s.name,
|
||||
inLanguage: s.lang,
|
||||
publisher: { "@id": `${s.url}/#organization` },
|
||||
});
|
||||
|
||||
export const serviceSchema = (opts: {
|
||||
name: string;
|
||||
description: string;
|
||||
areaServed?: string;
|
||||
}) =>
|
||||
clean({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Service",
|
||||
name: opts.name,
|
||||
description: opts.description,
|
||||
areaServed: opts.areaServed,
|
||||
provider: { "@id": `${s.url}/#organization` },
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: var(--font-vazirmatn), ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; /* added for migration: next/font variable */
|
||||
--font-mono: var(--font-ibm-plex-mono), "Vazirmatn", ui-monospace, monospace; /* added for migration: next/font variable */
|
||||
|
||||
--color-bg: #0a0a0c;
|
||||
--color-panel: #121216;
|
||||
--color-panel-2: #17171c;
|
||||
--color-ink: #edede6;
|
||||
--color-muted: #8e8e96;
|
||||
--color-accent: #ff5a00;
|
||||
--color-accent-soft: #ff7a2a;
|
||||
--color-hazard: #ffb800;
|
||||
--color-line: #232329;
|
||||
|
||||
--animate-marquee: marquee 32s linear infinite;
|
||||
--animate-spin-slow: spin-slow 18s linear infinite;
|
||||
--animate-blink: blink 1.1s steps(2, start) infinite;
|
||||
--animate-fade-up: fade-up 1s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
|
||||
@keyframes marquee {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
@keyframes spin-slow {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
@keyframes fade-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(44px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* added for migration: CSS-only entrance animation for Hero (no JS dependency, content visible in server HTML) */
|
||||
.animate-fade-up-1 {
|
||||
animation: var(--animate-fade-up);
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
.animate-fade-up-2 {
|
||||
animation: var(--animate-fade-up);
|
||||
animation-delay: 0.28s;
|
||||
}
|
||||
.animate-fade-up-3 {
|
||||
animation: var(--animate-fade-up);
|
||||
animation-delay: 0.42s;
|
||||
}
|
||||
.animate-fade-up-4 {
|
||||
animation: var(--animate-fade-up);
|
||||
animation-delay: 0.55s;
|
||||
}
|
||||
.animate-fade-up-5 {
|
||||
animation: var(--animate-fade-up);
|
||||
animation-delay: 0.68s;
|
||||
}
|
||||
.animate-fade-up-6 {
|
||||
animation: var(--animate-fade-up);
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
.animate-fade-up-7 {
|
||||
animation: var(--animate-fade-up);
|
||||
animation-delay: 0.82s;
|
||||
}
|
||||
.animate-fade-up-8 {
|
||||
animation: var(--animate-fade-up);
|
||||
animation-delay: 0.9s;
|
||||
}
|
||||
.animate-fade-up-9 {
|
||||
animation: var(--animate-fade-up);
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-ink);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #2c2c33;
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--color-bg);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
/* ---- hazard tape (yellow/black diagonal stripes) ---- */
|
||||
.hazard {
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
#ffb800 0 14px,
|
||||
#0a0a0c 14px 28px
|
||||
);
|
||||
}
|
||||
|
||||
/* ---- blueprint grid ---- */
|
||||
.bg-grid {
|
||||
background-image:
|
||||
linear-gradient(to left, rgba(237, 237, 230, 0.045) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(237, 237, 230, 0.045) 1px, transparent 1px);
|
||||
background-size: 72px 72px;
|
||||
}
|
||||
.bg-grid-fine {
|
||||
background-image:
|
||||
linear-gradient(to left, rgba(237, 237, 230, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(237, 237, 230, 0.03) 1px, transparent 1px);
|
||||
background-size: 28px 28px;
|
||||
}
|
||||
|
||||
/* ---- film grain ---- */
|
||||
.noise {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 90;
|
||||
pointer-events: none;
|
||||
opacity: 0.05;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* ---- outlined display text ---- */
|
||||
.text-outline {
|
||||
color: transparent;
|
||||
-webkit-text-stroke: 1px rgba(237, 237, 230, 0.16);
|
||||
}
|
||||
|
||||
/* ---- vertical safety tape strip ---- */
|
||||
.tape-v {
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
#ffb800 0 10px,
|
||||
#0a0a0c 10px 20px
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "_old_vite_backup"]
|
||||
}
|
||||