Initial commit: Persian safety workwear landing page (Next.js)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-09-23 10:13:57 +03:30
co-authored by Claude Sonnet 5
commit 62b022298f
57 changed files with 4498 additions and 0 deletions
+20
View File
@@ -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/
+103
View File
@@ -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 با تأیید شما انجام شد.
+14
View File
@@ -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
Binary file not shown.
+12
View File
@@ -0,0 +1,12 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
trailingSlash: false,
images: {
unoptimized: true,
},
poweredByHeader: false,
};
export default nextConfig;
+1824
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -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"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

+91
View File
@@ -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>
);
}
+18
View File
@@ -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: [],
};
}
+28
View File
@@ -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>
);
}
+32
View File
@@ -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>
);
}
+12
View File
@@ -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,
};
}
+14
View File
@@ -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,
}));
}
+30
View File
@@ -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 />
</>
);
}
+33
View File
@@ -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>
);
}
+57
View File
@@ -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>
);
}
+137
View File
@@ -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>
);
}
+180
View File
@@ -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>
</>
);
}
+77
View File
@@ -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>
);
}
+111
View File
@@ -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>
);
}
+225
View File
@@ -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>
);
}
+38
View File
@@ -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>
);
}
+185
View File
@@ -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>
);
}
+153
View File
@@ -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>
);
}
+241
View File
@@ -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>
);
}
+164
View File
@@ -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>
);
}
+96
View File
@@ -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>
);
}
+35
View File
@@ -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>
);
}
+39
View File
@@ -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>
);
}
+97
View File
@@ -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>
);
}
+18
View File
@@ -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"),
}}
/>
))}
</>
);
}
+17
View File
@@ -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>
);
}
+41
View File
@@ -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>
);
}
+51
View File
@@ -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>
);
}
+25
View File
@@ -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
};
+53
View File
@@ -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` },
});
+166
View File
@@ -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
);
}
+27
View File
@@ -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"]
}