feat: migrate landing from Next.js to Nuxt 4 (Vue 3)

Full 1:1 migration preserving all visuals, animations and behavior:
- Nuxt 4 + Vue 3 (script setup, TypeScript strict) with SSG output
- Tailwind CSS v4 with @tailwindcss/vite
- Ant Design Vue 4 (dark theme, RTL, Persian locale) for design system
- Pinia for state management (ui + contact stores)
- motion-v (framer-motion port) for all animations
- lucide-vue-next for icons
- SEO improvements: og:image, canonical fix, real 404 page, JSON-LD services
- JS bundle reduced from 302KB to 239KB gzip
- Docker/nginx deployment preserved (out/ -> nginx)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
S. Esmaeili
2026-10-06 16:28:05 +03:30
co-authored by Claude Opus 4.6
parent 1af5b555b8
commit e31ef5a21f
127 changed files with 12160 additions and 3464 deletions
+24
View File
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { AnimatePresence } from "motion-v";
import Preloader from "./Preloader.vue";
import Cursor from "./Cursor.vue";
import Header from "./Header.vue";
import BackToTop from "./BackToTop.vue";
const ui = useUiStore();
</script>
<template>
<AnimatePresence>
<Preloader v-if="!ui.ready" @complete="ui.setReady" />
</AnimatePresence>
<Cursor />
<div class="noise" aria-hidden="true" />
<Header />
<slot />
<BackToTop />
</template>
+22
View File
@@ -0,0 +1,22 @@
<script setup lang="ts">
import { AnimatePresence, motion } from "motion-v";
import { ArrowUp } from "lucide-vue-next";
const show = useScrolledPast(700);
</script>
<template>
<AnimatePresence>
<motion.a
v-if="show"
href="#home"
aria-label="بازگشت به بالا"
:initial="{ opacity: 0, y: 24 }"
:animate="{ opacity: 1, y: 0 }"
:exit="{ opacity: 0, y: 24 }"
class="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 class="size-5" />
</motion.a>
</AnimatePresence>
</template>
+52
View File
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { motion, useMotionValue, useSpring } from "motion-v";
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 = ref(false);
const visible = ref(false);
const move = (e: MouseEvent) => {
x.set(e.clientX);
y.set(e.clientY);
visible.value = true;
};
const over = (e: MouseEvent) => {
const t = e.target as HTMLElement | null;
hover.value = !!t?.closest?.(
"a, button, input, textarea, select, label, [data-hover]",
);
};
onMounted(() => {
window.addEventListener("mousemove", move, { passive: true });
window.addEventListener("mouseover", over, { passive: true });
});
onBeforeUnmount(() => {
window.removeEventListener("mousemove", move);
window.removeEventListener("mouseover", over);
});
</script>
<template>
<div class="pointer-events-none fixed inset-0 z-[120] hidden [@media(pointer:fine)]:block">
<motion.div :style="{ x: sx, y: sy }" class="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 }"
class="size-8 rounded-md border-2 border-accent/80"
/>
<div
:class="`absolute inset-0 m-auto size-1 rounded-full bg-accent transition-opacity duration-300 ${
visible ? 'opacity-100' : 'opacity-0'
}`"
/>
</motion.div>
</div>
</template>
+118
View File
@@ -0,0 +1,118 @@
<script setup lang="ts">
import { AtSign, Globe, Phone, Send } from "lucide-vue-next";
import Logo from "~/components/ui/Logo.vue";
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: "تماس" },
];
</script>
<template>
<footer class="relative overflow-hidden border-t border-line">
<div class="hazard h-2.5" />
<div class="mx-auto max-w-7xl px-5 md:px-8">
<div class="grid gap-12 py-16 md:grid-cols-12 md:py-20">
<!-- brand -->
<div class="md:col-span-5">
<Logo />
<p class="mt-6 max-w-sm text-sm leading-8 text-muted">
تأمین‌کننده تخصصی تجهیزات حفاظت فردی و راهکارهای ایمنی صنعتی؛
همراه سازمان‌ها از سال ۱۳۸۶ در مسیر صفر شدن حوادث کاری.
</p>
<div class="mt-7 flex gap-3">
<a
v-for="s in SOCIALS"
:key="s.label"
href="#contact"
:aria-label="s.label"
class="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"
>
<component :is="s.icon" class="size-4" />
</a>
</div>
</div>
<!-- quick links -->
<div class="md:col-span-2">
<div class="font-mono text-xs tracking-[0.25em] text-accent">// دسترسی</div>
<ul class="mt-5 space-y-3">
<li v-for="l in QUICK_LINKS" :key="l.href">
<a :href="l.href" class="text-sm text-muted transition-colors hover:text-ink">
{{ l.label }}
</a>
</li>
</ul>
</div>
<!-- products -->
<div class="md:col-span-2">
<div class="font-mono text-xs tracking-[0.25em] text-accent">// محصولات</div>
<ul class="mt-5 space-y-3">
<li v-for="p in PRODUCT_LINKS" :key="p">
<a href="#products" class="text-sm text-muted transition-colors hover:text-ink">
{{ p }}
</a>
</li>
</ul>
</div>
<!-- contact -->
<div class="md:col-span-3">
<div class="font-mono text-xs tracking-[0.25em] text-accent">// ارتباط</div>
<address class="mt-5 space-y-3 text-sm text-muted not-italic">
<p class="leading-7">
تهران، خیابان کارگر شمالی،
<br />
خیابان فر، کوچه دوم، پلاک ۱۲
</p>
<p class="font-mono tracking-wider" dir="ltr">۰۲۱ - ۹۱ ۰۰ ۸۸ ۰۰</p>
<p class="font-mono tracking-wider" dir="ltr">info@ariaiman.ir</p>
<p class="pt-2 text-xs leading-6">
شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰
<br />
پشتیبانی اضطراری: ۲۴/۷
</p>
</address>
</div>
</div>
<!-- giant wordmark -->
<div aria-hidden="true" class="pointer-events-none select-none pb-6">
<div class="text-outline text-center text-[17vw] font-black leading-[0.85] md:text-[11vw]">
آریا ایمن
</div>
</div>
<div
class="flex flex-wrap items-center justify-between gap-4 border-t border-line py-6 text-xs text-muted"
>
<div>© ۱۴۰۴ آریا ایمن — تمامی حقوق محفوظ است.</div>
<div class="font-mono text-[10px] tracking-[0.25em]">
ISO 9001:2015 — CE CERTIFIED PPE SUPPLIER
</div>
</div>
</div>
</footer>
</template>
+135
View File
@@ -0,0 +1,135 @@
<script setup lang="ts">
import { AnimatePresence, motion } from "motion-v";
import { Menu, Phone, X } from "lucide-vue-next";
import Logo from "~/components/ui/Logo.vue";
const LINKS = [
{ href: "#home", label: "خانه" },
{ href: "#products", label: "محصولات" },
{ href: "#about", label: "درباره ما" },
{ href: "#services", label: "خدمات" },
{ href: "#standards", label: "استانداردها" },
{ href: "#industries", label: "صنایع" },
{ href: "#contact", label: "تماس" },
];
const ui = useUiStore();
const scrolled = useScrolledPast(40);
const { time, shift } = useTehranClock();
watch(
() => ui.menuOpen,
(open) => {
document.body.style.overflow = open ? "hidden" : "";
},
);
onBeforeUnmount(() => {
document.body.style.overflow = "";
});
</script>
<template>
<motion.header
:initial="{ y: -90, opacity: 0 }"
:animate="ui.ready ? { y: 0, opacity: 1 } : {}"
:transition="{ duration: 0.9, delay: 0.15, ease: EASE }"
:class="`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 class="mx-auto flex max-w-7xl items-center justify-between px-5 md:px-8">
<Logo />
<nav class="hidden items-center gap-7 lg:flex">
<a
v-for="l in LINKS"
:key="l.href"
:href="l.href"
class="group relative py-2 text-sm text-ink/75 transition-colors hover:text-ink"
>
{{ l.label }}
<span
class="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 class="flex items-center gap-3">
<div
class="hidden items-center gap-2.5 rounded-full border border-line px-4 py-2 text-[11px] text-muted xl:flex"
>
<span class="size-1.5 animate-pulse rounded-full bg-emerald-400" />
<span class="font-mono">{{ time }}</span>
<span class="text-muted/50">|</span>
<span>تهران — شیفت {{ shift }}</span>
</div>
<a
href="#contact"
class="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 class="size-4" />
درخواست مشاوره
</a>
<button
aria-label="باز کردن منو"
class="flex size-11 items-center justify-center rounded-lg border border-line text-ink transition-colors hover:border-accent hover:text-accent lg:hidden"
@click="ui.openMenu"
>
<Menu class="size-5" />
</button>
</div>
</div>
</motion.header>
<AnimatePresence>
<motion.div
v-if="ui.menuOpen"
:initial="{ opacity: 0 }"
:animate="{ opacity: 1 }"
:exit="{ opacity: 0 }"
:transition="{ duration: 0.35 }"
class="fixed inset-0 z-[110] flex flex-col bg-bg/95 backdrop-blur-2xl"
>
<div class="flex items-center justify-between border-b border-line px-5 py-4 md:px-8">
<Logo />
<button
aria-label="بستن منو"
class="flex size-11 items-center justify-center rounded-lg border border-line transition-colors hover:border-accent hover:text-accent"
@click="ui.closeMenu"
>
<X class="size-5" />
</button>
</div>
<nav class="flex flex-1 flex-col justify-center gap-1 overflow-y-auto px-8">
<motion.a
v-for="(l, i) in LINKS"
:key="l.href"
:href="l.href"
:initial="{ opacity: 0, x: 44 }"
:animate="{ opacity: 1, x: 0 }"
:transition="{ delay: 0.08 + i * 0.055, duration: 0.55, ease: EASE }"
class="group flex items-baseline gap-5 border-b border-line/60 py-4"
@click="ui.closeMenu"
>
<span class="font-mono text-xs text-accent">
{{ faNum(String(i + 1).padStart(2, "0")) }}
</span>
<span class="text-3xl font-black transition-colors group-hover:text-accent">
{{ l.label }}
</span>
</motion.a>
</nav>
<div class="flex items-center justify-between border-t border-line p-8 text-sm">
<div class="flex items-center gap-3 text-muted">
<Phone class="size-4 text-accent" />
<span class="font-mono">۰۲۱-۹۱۰۰۸۸۰۰</span>
</div>
<div class="text-xs text-muted">تهران — شیفت {{ shift }}، {{ time }}</div>
</div>
</motion.div>
</AnimatePresence>
</template>
+78
View File
@@ -0,0 +1,78 @@
<script setup lang="ts">
import { motion } from "motion-v";
import { ShieldCheck } from "lucide-vue-next";
const STEPS = [
"بررسی سیستم‌های ایمنی",
"تأیید استانداردهای بین‌المللی",
"بارگذاری تجهیزات حفاظت فردی",
"محیط کاری آماده است",
];
const emit = defineEmits<{ complete: [] }>();
const progress = ref(0);
let done = false;
let t: ReturnType<typeof setInterval> | undefined;
onMounted(() => {
let v = 0;
t = setInterval(() => {
v += Math.floor(Math.random() * 11) + 7;
if (v >= 100) {
v = 100;
clearInterval(t);
if (!done) {
done = true;
setTimeout(() => emit("complete"), 450);
}
}
progress.value = v;
}, 140);
});
onBeforeUnmount(() => clearInterval(t));
const step = computed(
() =>
STEPS[
Math.min(STEPS.length - 1, Math.floor((progress.value / 100) * STEPS.length))
],
);
</script>
<template>
<motion.div
:exit="{ y: '-100%' }"
:transition="{ duration: 0.85, ease: [0.76, 0, 0.24, 1] }"
class="fixed inset-0 z-[200] flex flex-col items-center justify-center bg-bg"
>
<div class="hazard absolute inset-x-0 top-0 h-3" />
<div class="hazard absolute inset-x-0 bottom-0 h-3" />
<div class="flex items-center gap-4">
<span class="flex size-12 items-center justify-center rounded-xl bg-accent text-bg">
<ShieldCheck class="size-7" />
</span>
<span class="font-mono text-xs tracking-[0.35em] text-muted">
ARIA IMEN CO.
</span>
</div>
<div class="mt-12 font-mono text-7xl font-semibold tabular-nums md:text-8xl">
{{ progress.toLocaleString("fa-IR") }}<span class="text-accent">٪</span>
</div>
<div class="mt-8 h-[3px] w-64 overflow-hidden rounded-full bg-line md:w-80">
<div
class="h-full bg-accent transition-all duration-200"
:style="{ width: `${progress}%` }"
/>
</div>
<div class="mt-6 flex items-center gap-3 text-xs text-muted">
<span class="size-1.5 animate-pulse rounded-full bg-accent" />
{{ step }}
<span class="animate-blink text-accent">▍</span>
</div>
</motion.div>
</template>
+103
View File
@@ -0,0 +1,103 @@
<script setup lang="ts">
import { Globe, Headset, Users } from "lucide-vue-next";
import Reveal from "~/components/ui/Reveal.vue";
import SectionHead from "~/components/ui/SectionHead.vue";
const TIMELINE = [
{ year: "۱۳۸۶", text: "تأسیس آریا ایمن در تهران با تمرکز بر تجهیزات حفاظت فردی" },
{ year: "۱۳۹۲", text: "توسعه شبکه توزیع به تمام ۳۱ استان کشور" },
{ year: "۱۳۹۸", text: "راه‌اندازی آزمایشگاه کنترل کیفیت و تست تجهیزات" },
{ year: "۱۴۰۲", text: "افتتاح مرکز تخصصی آموزش و بازرسی HSE" },
];
const FEATURES = [
{ icon: Globe, label: "نماینده رسمی ۴۰+ برند جهانی" },
{ icon: Users, label: "تیم مهندسی و بازرسی HSE" },
{ icon: Headset, label: "پشتیبانی ۲۴/۷ قراردادها" },
];
</script>
<template>
<section
id="about"
aria-labelledby="about-title"
class="relative overflow-hidden border-t border-line py-24 md:py-32"
>
<div class="absolute inset-0 bg-grid-fine opacity-50" />
<div class="relative mx-auto max-w-7xl px-5 md:px-8">
<div class="grid items-center gap-14 lg:grid-cols-2">
<!-- ---- image ---- -->
<Reveal class="relative order-2 lg:order-1" :delay="0.1">
<div class="relative overflow-hidden rounded-2xl border border-line">
<img
src="/images/about.jpg"
alt="انبار تجهیزات ایمنی آریا ایمن"
width="800"
height="600"
loading="lazy"
decoding="async"
class="aspect-[4/3] w-full object-cover"
/>
<div class="absolute inset-0 bg-gradient-to-t from-bg/80 via-transparent to-transparent" />
<div
class="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 class="font-mono text-3xl font-bold text-accent">۱۸+</span>
<span class="text-xs leading-5 text-muted">
سال حفاظت از
<br />
جان نیروی کار
</span>
</div>
</div>
<div class="hazard absolute -top-3 -end-3 h-6 w-28 rounded-full opacity-90" />
</Reveal>
<!-- ---- text ---- -->
<div class="order-1 lg:order-2">
<SectionHead heading-id="about-title" tag="۰۲ — درباره ما">
<template #title>
ایمنی برای ما شعار نیست؛
<br />
<span class="text-accent">قرارداد است.</span>
</template>
</SectionHead>
<Reveal :delay="0.2">
<p class="mt-6 leading-9 text-muted">
از سال ۱۳۸۶، زمانی که اولین کلاه ایمنی را به یک کارگاه کوچک در
جنوب تهران فروختیم، باور ما همین یک جمله بود: پشت هر تجهیز
ایمنی، یک خانواده هست که منتظر بازگشت عزیزش است. امروز با بیش
از ۸۵۰ سازمان، از پالایشگاه‌ها تا معادن و سایت‌های ساختمانی، در
کنار کسانی هستیم که ایمنی را یک خط قرمز می‌دانند.
</p>
</Reveal>
<div class="mt-10 space-y-1">
<Reveal v-for="(t, i) in TIMELINE" :key="t.year" :delay="0.25 + i * 0.07">
<div
class="group border-s-2 border-accent/30 ps-6 transition-colors duration-300 hover:border-accent"
>
<span class="font-mono text-sm font-bold text-accent">{{ t.year }}</span>
<p class="mt-1 pb-5 text-sm leading-7 text-ink/75">{{ t.text }}</p>
</div>
</Reveal>
</div>
<Reveal :delay="0.5">
<div class="mt-4 flex flex-wrap gap-3">
<span
v-for="f in FEATURES"
:key="f.label"
class="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"
>
<component :is="f.icon" class="size-4 text-accent" />
{{ f.label }}
</span>
</div>
</Reveal>
</div>
</div>
</div>
</section>
</template>
+190
View File
@@ -0,0 +1,190 @@
<script setup lang="ts">
import { Button as AButton, Form as AForm, FormItem as AFormItem, Input as AInput, Textarea as ATextarea } from "ant-design-vue";
import type { Rule } from "ant-design-vue/es/form";
import {
CheckCircle2,
Clock,
Loader2,
Mail,
MapPin,
Phone,
Send,
Siren,
} from "lucide-vue-next";
import Reveal from "~/components/ui/Reveal.vue";
import SectionHead from "~/components/ui/SectionHead.vue";
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 required = (message: string): Rule[] => [
{ required: true, whitespace: true, message, trigger: "blur" },
];
const rules: Record<string, Rule[]> = {
name: required("لطفاً نام و نام خانوادگی را وارد کنید"),
org: required("لطفاً نام سازمان را وارد کنید"),
phone: required("لطفاً شماره تماس را وارد کنید"),
};
const contact = useContactStore();
</script>
<template>
<section
id="contact"
aria-labelledby="contact-title"
class="relative overflow-hidden border-t border-line py-24 md:py-32"
>
<div class="absolute inset-0 bg-grid-fine opacity-40" />
<div class="relative mx-auto max-w-7xl px-5 md:px-8">
<div class="grid gap-14 lg:grid-cols-2">
<!-- ---- info ---- -->
<div>
<SectionHead
heading-id="contact-title"
tag="۰۷ — تماس"
desc="فرم را پر کنید تا کارشناسان ما برای هماهنگی جلسه بازدید رایگان سایت شما تماس بگیرند؛ یا مستقیم با ما در ارتباط باشید."
>
<template #title>
بیایید ایمنی را
<br />
با هم جدی بگیریم
</template>
</SectionHead>
<div class="mt-10 space-y-1">
<Reveal v-for="(item, i) in INFO" :key="item.label" :delay="0.15 + i * 0.06">
<div class="group flex items-center gap-4 border-b border-line/70 py-4">
<span
class="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"
>
<component :is="item.icon" class="size-5" />
</span>
<div class="min-w-0">
<div class="text-xs text-muted">{{ item.label }}</div>
<div
:class="`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 class="mt-8 flex items-start gap-4 rounded-xl border border-hazard/30 bg-hazard/5 p-5">
<Siren class="size-6 shrink-0 text-hazard" />
<div>
<div class="font-bold text-hazard">پاسخ‌گویی اضطراری ۲۴/۷</div>
<p class="mt-1.5 text-sm leading-7 text-muted">
برای مشتریان دارای قرارداد سازمانی، خط ویژه حوادث و تأمین
فوری تجهیزات در تمام ساعات شبانه‌روز فعال است.
</p>
</div>
</div>
</Reveal>
</div>
<!-- ---- form ---- -->
<Reveal :delay="0.15">
<div class="rounded-2xl border border-line bg-panel/60 p-6 backdrop-blur md:p-9">
<div class="font-mono text-xs tracking-[0.25em] text-accent">// فرم درخواست مشاوره</div>
<h3 class="mt-3 text-2xl font-black leading-snug">
در کمتر از ۲۴ ساعت
<br />
با شما تماس می‌گیریم
</h3>
<div
v-if="contact.status === 'success'"
class="mt-10 flex flex-col items-center rounded-xl border border-emerald-500/30 bg-emerald-500/5 p-10 text-center"
>
<CheckCircle2 class="size-14 text-emerald-400" />
<div class="mt-5 text-xl font-black">درخواست شما ثبت شد</div>
<p class="mt-2.5 max-w-sm text-sm leading-7 text-muted">
کارشناسان ما تا ۲۴ ساعت آینده برای هماهنگی جلسه بازدید با
شما تماس می‌گیرند.
</p>
<button
type="button"
class="mt-7 rounded-full border border-line px-7 py-3 text-sm transition-colors hover:border-accent hover:text-accent"
@click="contact.reset"
>
ثبت درخواست جدید
</button>
</div>
<AForm
v-else
class="contact-form"
layout="vertical"
:model="contact.form"
:rules="rules"
hide-required-mark
@finish="contact.submit"
>
<div class="mt-8 grid gap-5 sm:grid-cols-2">
<AFormItem name="name" label="نام و نام خانوادگی *" html-for="f-name">
<AInput id="f-name" v-model:value="contact.form.name" placeholder="مثلاً: علی رضایی" />
</AFormItem>
<AFormItem name="org" label="نام سازمان *" html-for="f-org">
<AInput
id="f-org"
v-model:value="contact.form.org"
placeholder="مثلاً: شرکت پتروشیمی خلیج"
/>
</AFormItem>
<AFormItem class="sm:col-span-2" name="phone" label="شماره تماس *" html-for="f-phone">
<AInput
id="f-phone"
v-model:value="contact.form.phone"
type="tel"
dir="ltr"
placeholder="0912 000 0000"
/>
</AFormItem>
<AFormItem class="sm:col-span-2" name="message" label="شرح درخواست" html-for="f-msg">
<ATextarea
id="f-msg"
v-model:value="contact.form.message"
:rows="4"
placeholder="تجهیزات موردنیاز، تعداد پرسنل یا هر نکته‌ای که لازم است بدانیم..."
/>
</AFormItem>
<div class="sm:col-span-2">
<AButton
type="primary"
html-type="submit"
class="contact-submit"
:disabled="contact.status === 'loading'"
>
<template v-if="contact.status === 'loading'">
<Loader2 class="size-5 animate-spin" />
در حال ارسال...
</template>
<template v-else>
<Send class="size-5" />
ارسال درخواست مشاوره
</template>
</AButton>
</div>
</div>
</AForm>
</div>
</Reveal>
</div>
</div>
</section>
</template>
+29
View File
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { useInView } from "motion-v";
const props = withDefaults(
defineProps<{ end: number; suffix?: string; duration?: number }>(),
{ suffix: "", duration: 1800 },
);
const el = ref<HTMLSpanElement | null>(null);
const inView = useInView(el, { once: true, margin: "-60px" });
const val = ref(0);
let raf = 0;
watch(inView, (v) => {
if (!v) return;
const t0 = performance.now();
const tick = (t: number) => {
const p = Math.min(1, (t - t0) / props.duration);
val.value = Math.round(props.end * (1 - Math.pow(1 - p, 3)));
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
});
onBeforeUnmount(() => cancelAnimationFrame(raf));
</script>
<template>
<span ref="el" class="tabular-nums">{{ val.toLocaleString("fa-IR") }}<span v-if="suffix" class="text-accent">{{ suffix }}</span></span>
</template>
+188
View File
@@ -0,0 +1,188 @@
<script setup lang="ts">
import {
ArrowLeft,
Award,
BadgeCheck,
ChevronDown,
HardHat,
ShieldCheck,
} from "lucide-vue-next";
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: "رضایت مشتریان" },
];
</script>
<template>
<section id="home" class="relative flex min-h-screen flex-col overflow-hidden">
<!-- ---- background ---- -->
<div class="absolute inset-0">
<img
src="/images/hero.jpg"
alt="کارگر صنعتی با تجهیزات ایمنی کامل"
width="1792"
height="1008"
fetchpriority="high"
decoding="async"
class="absolute inset-0 size-full object-cover"
/>
<div class="absolute inset-0 bg-gradient-to-l from-bg via-bg/80 to-bg/25" />
<div class="absolute inset-0 bg-gradient-to-t from-bg via-transparent to-bg/60" />
<div class="absolute inset-0 bg-grid opacity-50" />
</div>
<!-- ---- side tape ---- -->
<div class="tape-v absolute bottom-0 end-0 z-10 h-44 w-1.5 opacity-70" />
<!-- ---- content ---- -->
<div
class="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 class="grid w-full items-center gap-12 lg:grid-cols-12">
<div class="lg:col-span-7">
<div
class="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 class="size-1.5 animate-pulse rounded-full bg-accent" />
<span class="text-xs font-medium text-accent">
تأمین‌کننده رسمی تجهیزات حفاظت فردی (PPE)
</span>
</div>
<h1
class="animate-fade-up-2 mt-7 text-5xl font-black leading-[1.15] md:text-7xl md:leading-[1.1]"
>
ایمنی یعنی
<br />
<span
class="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
class="animate-fade-up-3 mt-7 max-w-xl text-base leading-9 text-ink/70 md:text-lg md:leading-10"
>
آریا ایمن با بیش از ۱۸ سال تجربه، تأمین‌کننده تخصصی تجهیزات ایمنی و
حفاظت فردی برای سازمان‌هایی است که جان انسان‌ها را جدی می‌گیرند؛ از
کلاه ایمنی تا هارنس نجات، از مشاوره تا بازرسی و آموزش.
</p>
<div class="animate-fade-up-4 mt-9 flex flex-wrap items-center gap-4">
<a
href="#contact"
class="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
class="size-5 transition-transform duration-300 group-hover:-translate-x-1.5"
/>
</a>
<a
href="#products"
class="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 class="animate-fade-up-5 mt-10 flex flex-wrap gap-3">
<span
v-for="c in CHIPS"
:key="c.label"
class="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"
>
<component :is="c.icon" class="size-4 text-accent" />
{{ c.label }}
</span>
</div>
</div>
<!-- ---- rotating badge + status card ---- -->
<div class="hidden flex-col items-center gap-10 lg:col-span-5 lg:flex">
<div class="animate-fade-up-6 relative">
<div class="relative size-44">
<div class="animate-spin-slow absolute inset-0">
<svg viewBox="0 0 120 120" class="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 font-size="9" letter-spacing="2.6" class="fill-ink/60 font-mono">
<textPath href="#badge-circle">
ARIA IMEN SAFETY • ISO 9001 • CE CERTIFIED • EST 2007 •
</textPath>
</text>
</svg>
</div>
<div class="absolute inset-0 flex items-center justify-center">
<span
class="flex size-20 items-center justify-center rounded-full border border-accent/30 bg-accent/10"
>
<HardHat class="size-9 text-accent" />
</span>
</div>
</div>
</div>
<div
class="animate-fade-up-7 w-full max-w-sm border border-line bg-bg/70 p-5 backdrop-blur-md"
>
<div class="flex items-center justify-between font-mono text-[11px]">
<span class="text-muted">SITE STATUS</span>
<span class="flex items-center gap-2 text-emerald-400">
<span class="size-1.5 animate-pulse rounded-full bg-emerald-400" />
SECURE
</span>
</div>
<div class="mt-3.5 flex items-center justify-between font-mono text-[11px]">
<span class="text-muted">PPE COMPLIANCE</span>
<span class="font-semibold text-accent">۱۰۰٪</span>
</div>
<div class="mt-3.5 flex items-center justify-between font-mono text-[11px]">
<span class="text-muted">آخرین بازرسی</span>
<span>۱۴۰۴/۰۸/۱۲</span>
</div>
<div class="my-4 h-px bg-line" />
<div class="font-mono text-[11px] leading-6 text-muted">
سیستم پایش حرفه‌ای آریا ایمن<span class="animate-blink text-accent">▍</span>
</div>
</div>
</div>
</div>
</div>
<!-- ---- scroll hint ---- -->
<div class="animate-fade-up-9 absolute bottom-28 left-1/2 z-10 hidden md:block">
<div class="-translate-x-1/2 flex flex-col items-center gap-2">
<span class="font-mono text-[10px] tracking-[0.35em] text-muted">SCROLL</span>
<ChevronDown class="size-4 animate-bounce text-accent" />
</div>
</div>
<!-- ---- bottom bar ---- -->
<div class="animate-fade-up-8 relative z-10 border-t border-ink/10 backdrop-blur-sm">
<div class="mx-auto grid max-w-7xl grid-cols-1 gap-px bg-ink/10 sm:grid-cols-3">
<div
v-for="s in MINI_STATS"
:key="s.label"
class="flex items-baseline gap-4 bg-bg/60 px-6 py-5 backdrop-blur-md md:px-8"
>
<span class="text-2xl font-black text-ink md:text-3xl">{{ s.value }}</span>
<span class="text-xs leading-5 text-muted md:text-sm">{{ s.label }}</span>
</div>
</div>
</div>
</section>
</template>
+137
View File
@@ -0,0 +1,137 @@
<script setup lang="ts">
import { AnimatePresence, motion, useMotionValue, useSpring } from "motion-v";
import { ArrowLeft } from "lucide-vue-next";
import SectionHead from "~/components/ui/SectionHead.vue";
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",
},
];
const hovered = ref<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 });
const onMove = (e: MouseEvent) => {
x.set(e.clientX);
y.set(e.clientY);
};
</script>
<template>
<section
id="industries"
aria-labelledby="industries-title"
class="relative border-t border-line py-24 md:py-32"
@mousemove="onMove"
>
<div class="mx-auto max-w-7xl px-5 md:px-8">
<SectionHead
heading-id="industries-title"
tag="۰۵ — صنایع"
desc="جایی که دمای محیط از ۵۰ درجه گذر می‌کند یا ارتفاع کار به ده‌ها متر می‌رسد، تجهیزات ما باید بی‌نقص عمل کنند — و عمل می‌کنند."
>
<template #title>
حضور در سخت‌ترین
<br />
میدان‌های کاری
</template>
</SectionHead>
<div class="mt-16" @mouseleave="hovered = null">
<motion.div
v-for="(ind, i) in INDUSTRIES"
:key="ind.name"
v-bind="fade(i * 0.05)"
data-hover
:class="`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' : ''
}`"
@mouseenter="hovered = i"
>
<div class="flex min-w-0 items-baseline gap-5 md:gap-8">
<span class="shrink-0 font-mono text-xs text-muted">
{{ faNum(String(i + 1).padStart(2, "0")) }}
</span>
<h3
class="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
class="hidden shrink-0 font-mono text-[10px] tracking-[0.3em] text-muted/60 md:inline"
>
{{ ind.en }}
</span>
</div>
<div class="flex shrink-0 items-center gap-4 md:gap-8">
<span class="text-xs text-muted md:text-sm">{{ ind.count }} پروژه</span>
<span
class="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 class="size-4 md:size-5" />
</span>
</div>
</motion.div>
</div>
</div>
<!-- ---- floating preview (desktop) ---- -->
<motion.div
:style="{ x: sx, y: sy }"
class="pointer-events-none fixed left-0 top-0 z-40 hidden lg:block"
>
<AnimatePresence>
<motion.div
v-if="hovered !== null"
: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 }"
class="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)]"
>
<img
:src="INDUSTRIES[hovered]!.img"
:alt="INDUSTRIES[hovered]!.name"
decoding="async"
class="absolute inset-0 size-full object-cover grayscale-[35%] transition-all duration-500"
/>
<div class="absolute inset-0 bg-accent/15 mix-blend-multiply" />
<div class="absolute inset-0 bg-gradient-to-t from-bg/70 to-transparent" />
<div
class="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>
</template>
+209
View File
@@ -0,0 +1,209 @@
<script setup lang="ts">
import { AnimatePresence, motion } from "motion-v";
import { ArrowLeft, BadgeCheck, PackageCheck, ShieldCheck } from "lucide-vue-next";
import Reveal from "~/components/ui/Reveal.vue";
import SectionHead from "~/components/ui/SectionHead.vue";
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: "قیمت ثابت و تأمین مستمر برای سازمان‌های بزرگ",
},
];
const active = ref(0);
</script>
<template>
<section id="products" aria-labelledby="products-title" class="relative py-24 md:py-32">
<div class="mx-auto max-w-7xl px-5 md:px-8">
<div class="flex flex-wrap items-end justify-between gap-6">
<SectionHead
heading-id="products-title"
tag="۰۱ — محصولات"
desc="هر محصول پیش از عرضه، زیر ذره‌بین کنترل کیفیت و تطبیق با استانداردهای جهانی قرار می‌گیرد. روی هر پنل کلیک کنید."
>
<template #title>
خط اول دفاع،
<br />
تجهیزات ماست
</template>
</SectionHead>
<Reveal :delay="0.2" class="hidden md:block">
<a
href="#contact"
class="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 class="size-4 transition-transform group-hover:-translate-x-1" />
</a>
</Reveal>
</div>
</div>
<div class="mx-auto mt-14 max-w-7xl px-5 md:px-8">
<div class="flex flex-col gap-2 md:h-[74vh] md:min-h-[580px] md:flex-row">
<div
v-for="(p, i) in PRODUCTS"
:key="p.code"
data-hover
:class="`group relative cursor-pointer overflow-hidden rounded-xl border transition-all duration-700 ease-[cubic-bezier(0.22,1,0.36,1)] ${
active === i
? 'h-[500px] flex-[3.4] border-accent/60 md:h-auto'
: 'h-24 flex-[1] border-line hover:border-ink/30 md:h-auto'
}`"
@mouseenter="active = i"
@click="active = i"
>
<img
:src="p.img"
:alt="p.name"
loading="lazy"
decoding="async"
:class="`absolute inset-0 size-full object-cover transition-all duration-700 ${
active === i ? 'scale-100 grayscale-0' : 'scale-110 grayscale group-hover:grayscale-0'
}`"
/>
<div
:class="`absolute inset-0 transition-opacity duration-700 ${
active === i
? '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 class="absolute inset-x-0 top-0 flex items-center justify-between p-5">
<span class="font-mono text-[10px] tracking-widest text-ink/60">{{ p.code }}</span>
<span
:class="`font-mono text-xs transition-colors duration-500 ${
active === i ? 'text-accent' : 'text-ink/40'
}`"
>
{{ faNum(String(i + 1).padStart(2, "0")) }}
</span>
</div>
<div class="absolute inset-x-0 bottom-0 p-5 md:p-6">
<h3
:class="`font-black transition-all duration-500 ${
active === i ? 'text-2xl' : 'text-base md:text-sm'
}`"
>
{{ p.name }}
</h3>
<p class="mt-1 font-mono text-[10px] tracking-[0.2em] text-muted">{{ p.en }}</p>
<AnimatePresence>
<motion.div
v-if="active === i"
: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 class="mt-4 max-w-md text-sm leading-8 text-ink/75">{{ p.desc }}</p>
<div class="mt-4 flex flex-wrap gap-2">
<span
v-for="s in p.specs"
:key="s"
class="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
class="mx-auto mt-10 grid max-w-7xl gap-px overflow-hidden rounded-xl border border-line bg-line md:grid-cols-3"
>
<Reveal v-for="(t, i) in TRUST" :key="t.title" :delay="i * 0.08">
<div class="flex h-full items-center gap-4 bg-panel/60 p-6">
<span
class="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent/10 text-accent"
>
<component :is="t.icon" class="size-6" />
</span>
<div>
<div class="font-bold">{{ t.title }}</div>
<div class="mt-1 text-xs leading-6 text-muted">{{ t.desc }}</div>
</div>
</div>
</Reveal>
</div>
</section>
</template>
+142
View File
@@ -0,0 +1,142 @@
<script setup lang="ts">
import { AnimatePresence, motion } from "motion-v";
import { ClipboardCheck, Gauge, GraduationCap, Plus, Truck } from "lucide-vue-next";
import Reveal from "~/components/ui/Reveal.vue";
import SectionHead from "~/components/ui/SectionHead.vue";
import { serviceSchema } from "#shared/seo/schema";
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: [
"قرارداد سالانه با قیمت ثابت",
"انبار محلی در سایت شما",
"تحویل سراسری در ۴۸ ساعت",
],
},
];
useJsonLd(
SERVICES.map((s) => serviceSchema({ name: s.title, description: s.desc })),
"services",
);
const open = ref(0);
</script>
<template>
<section id="services" aria-labelledby="services-title" class="border-t border-line py-24 md:py-32">
<div class="mx-auto max-w-7xl px-5 md:px-8">
<SectionHead
heading-id="services-title"
tag="۰۳ — خدمات"
desc="ما در آریا ایمن، تجهیزات را تحویل می‌دهیم و کنار شما می‌مانیم؛ از نخستین ارزیابی ریسک تا آخرین تست دوره‌ای."
>
<template #title>
فراتر از تأمین؛
<br />
همراهی کامل ایمنی
</template>
</SectionHead>
<div class="mt-16">
<Reveal v-for="(s, i) in SERVICES" :key="s.title" :delay="i * 0.05">
<div :class="`border-t border-line ${i === SERVICES.length - 1 ? 'border-b' : ''}`">
<button
class="group flex w-full items-center gap-5 py-7 text-start md:gap-10 md:py-9"
@click="open = open === i ? -1 : i"
>
<span
:class="`font-mono text-sm transition-colors duration-300 md:text-base ${
open === i ? 'text-accent' : 'text-muted'
}`"
>
{{ faNum(String(i + 1).padStart(2, "0")) }}
</span>
<span
:class="`flex-1 text-2xl font-black transition-colors duration-300 md:text-4xl ${
open === i ? 'text-accent' : 'text-ink group-hover:text-accent'
}`"
>
{{ s.title }}
</span>
<span
:class="`hidden size-12 shrink-0 items-center justify-center rounded-full border transition-all duration-500 md:flex ${
open === i
? 'rotate-45 border-accent bg-accent text-bg'
: 'border-line text-muted group-hover:border-accent/50 group-hover:text-accent'
}`"
>
<Plus class="size-5" />
</span>
</button>
<AnimatePresence :initial="false">
<motion.div
v-if="open === i"
:initial="{ height: 0, opacity: 0 }"
:animate="{ height: 'auto', opacity: 1 }"
:exit="{ height: 0, opacity: 0 }"
:transition="{ duration: 0.55, ease: EASE }"
class="overflow-hidden"
>
<div class="grid gap-8 pb-10 md:grid-cols-2 md:ps-16">
<div class="flex items-start gap-4">
<span
class="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent/10 text-accent"
>
<component :is="s.icon" class="size-6" />
</span>
<p class="text-sm leading-8 text-muted">{{ s.desc }}</p>
</div>
<ul class="space-y-3.5">
<li
v-for="it in s.items"
:key="it"
class="flex items-center gap-3 text-sm text-ink/80"
>
<span class="size-1.5 shrink-0 rounded-full bg-accent" />
{{ it }}
</li>
</ul>
</div>
</motion.div>
</AnimatePresence>
</div>
</Reveal>
</div>
</div>
</section>
</template>
+97
View File
@@ -0,0 +1,97 @@
<script setup lang="ts">
import { FileSearch, ShieldCheck } from "lucide-vue-next";
import Reveal from "~/components/ui/Reveal.vue";
import SectionHead from "~/components/ui/SectionHead.vue";
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: "سیستم مدیریت کیفیت" },
];
</script>
<template>
<section
id="standards"
aria-labelledby="standards-title"
class="relative overflow-hidden border-t border-line py-24 md:py-32"
>
<!-- watermark -->
<div
aria-hidden="true"
class="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 class="relative mx-auto max-w-7xl px-5 md:px-8">
<div class="grid items-center gap-14 lg:grid-cols-12">
<div class="lg:col-span-5">
<SectionHead
heading-id="standards-title"
tag="۰۴ — استانداردها"
desc="ما فقط تجهیزاتی را عرضه می‌کنیم که تأییدیه معتبر بین‌المللی و تست آزمایشگاهی داشته باشند؛ بدون استثنا."
>
<template #title>
مهرتأیید جهانی
<br />
روی هر محصول
</template>
</SectionHead>
<Reveal :delay="0.25">
<div class="mt-8 flex items-start gap-4 rounded-xl border border-accent/25 bg-accent/5 p-5">
<FileSearch class="size-6 shrink-0 text-accent" />
<p class="text-sm leading-8 text-ink/80">
شماره سریال تمام محصولات در پایگاه داده استانداردهای اروپا
قابل استعلام است و گواهی تست هر بچ تحویل مشتری می‌شود.
</p>
</div>
</Reveal>
<Reveal :delay="0.35">
<a
href="#contact"
class="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 class="size-4 text-accent" />
درخواست گواهی تست محصولات
</a>
</Reveal>
</div>
<div class="grid gap-4 sm:grid-cols-2 lg:col-span-7 xl:grid-cols-3">
<Reveal v-for="(s, i) in STANDARDS" :key="s.code" :delay="i * 0.06">
<div
data-hover
class="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 class="absolute end-3 top-3 font-mono text-[9px] tracking-[0.2em] text-muted/50">
CERT
</span>
<div
class="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
class="font-mono text-base font-bold text-ink transition-colors duration-300 group-hover:text-accent"
>
{{ s.code }}
</div>
<div
class="mx-auto mt-1.5 h-px w-8 bg-line transition-colors duration-300 group-hover:bg-accent/50"
/>
<div class="mt-1.5 font-mono text-[8px] tracking-[0.25em] text-muted">
EURO NORM
</div>
</div>
</div>
<div class="mt-4 text-sm font-medium text-ink/85">{{ s.label }}</div>
</div>
</Reveal>
</div>
</div>
</div>
</section>
</template>
+38
View File
@@ -0,0 +1,38 @@
<script setup lang="ts">
import Reveal from "~/components/ui/Reveal.vue";
import CountUp from "./CountUp.vue";
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" },
];
</script>
<template>
<section class="relative" aria-label="آمار و دستاوردهای آریا ایمن">
<div class="grid grid-cols-2 gap-px border-y border-line bg-line lg:grid-cols-4">
<div
v-for="(s, i) in STATS"
:key="s.label"
class="group relative bg-bg p-8 transition-colors duration-500 hover:bg-panel md:p-10"
>
<span
class="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 class="text-4xl font-black md:text-6xl">
<CountUp :end="s.end" :suffix="s.suffix" />
</div>
<div class="mt-3 text-sm text-muted">{{ s.label }}</div>
</Reveal>
<span
class="absolute bottom-0 start-0 h-0.5 w-0 bg-accent transition-all duration-500 group-hover:w-full"
/>
</div>
</div>
</section>
</template>
+35
View File
@@ -0,0 +1,35 @@
<script setup lang="ts">
import { ShieldCheck } from "lucide-vue-next";
const ITEMS = [
"ISO 9001:2015",
"CE CERTIFIED",
"گارانتی اصالت کالا",
"تحویل سراسری کشوری",
"مشاوره و بازرسی تخصصی رایگان",
"EN STANDARDS",
"پشتیبانی ۲۴/۷ قراردادهای سازمانی",
"تست نمونه قبل از خرید",
];
</script>
<template>
<section aria-hidden="true" class="pointer-events-none relative z-20 overflow-hidden py-10">
<div class="-rotate-[1.6deg] scale-[1.04]">
<div class="hazard h-2.5" />
<div dir="ltr" class="overflow-hidden bg-hazard py-3.5 text-bg">
<div class="animate-marquee flex w-max">
<span
v-for="(item, i) in [...ITEMS, ...ITEMS]"
:key="i"
class="flex items-center gap-7 px-7 text-sm font-extrabold whitespace-nowrap"
>
{{ item }}
<ShieldCheck class="size-4 shrink-0" :stroke-width="2.5" />
</span>
</div>
</div>
<div class="hazard h-2.5" />
</div>
</section>
</template>
+94
View File
@@ -0,0 +1,94 @@
<script setup lang="ts">
import { AnimatePresence, motion } from "motion-v";
import { Quote } from "lucide-vue-next";
import SectionHead from "~/components/ui/SectionHead.vue";
const QUOTES = [
{
text: "آریا ایمن فقط تأمین‌کننده نیست؛ شریک ایمنی ماست. بازرسی‌های دوره‌ای و آموزش‌های میدانی‌شان نرخ حوادث سایت را به کمترین رقم تاریخ مجموعه رساند.",
name: "مهندس رضا کاظمی",
role: "مدیر HSE — مجتمع پتروشیمی خلیج فارس",
},
{
text: "کیفیت تجهیزات و سرعت پاسخ‌گویی در سطحی بود که قرارداد سالانه را بدون هیچ تردیدی تمدید کردیم؛ از خط تولید تا دفتر مرکزی.",
name: "سارا محمدی",
role: "سرپرست تدارکات — هلدینگ ساختمانی آرکان",
},
{
text: "۱۴۰ کیلومتر دورتر از سایت، در عرض ۲۴ ساعت تجهیزات حیاتی را تحویل گرفتیم. به این می‌گویند شریک واقعی کاری.",
name: "امیرحسین راد",
role: "مدیر پروژه — صنایع معدنی کاوه",
},
];
const idx = ref(0);
const q = computed(() => QUOTES[idx.value]!);
let t: ReturnType<typeof setInterval> | undefined;
onMounted(() => {
t = setInterval(() => (idx.value = (idx.value + 1) % QUOTES.length), 7000);
});
onBeforeUnmount(() => clearInterval(t));
</script>
<template>
<section
aria-labelledby="testimonials-title"
class="relative overflow-hidden border-t border-line py-24 md:py-32"
>
<div class="absolute inset-0 bg-grid opacity-40" />
<div
class="pointer-events-none absolute -top-10 -end-10 select-none font-mono text-[18vw] font-bold leading-none text-outline opacity-50"
>
"
</div>
<div class="relative mx-auto max-w-4xl px-5 text-center md:px-8">
<SectionHead
heading-id="testimonials-title"
center
tag="۰۶ — اعتماد"
title="آنچه مشتریان ما می‌گویند"
/>
<div class="mt-14 flex min-h-[320px] flex-col items-center md:min-h-[280px]">
<span
class="flex size-16 items-center justify-center rounded-full border border-accent/30 bg-accent/10"
>
<Quote class="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 }"
class="mt-8"
>
<p class="text-xl font-bold leading-[2.1] text-ink/90 md:text-2xl md:leading-[2]">
«{{ q.text }}»
</p>
<footer class="mt-8">
<div class="text-lg font-black">{{ q.name }}</div>
<div class="mt-2 font-mono text-xs text-muted">{{ q.role }}</div>
</footer>
</motion.blockquote>
</AnimatePresence>
</div>
<div class="mt-12 flex items-center justify-center gap-3">
<button
v-for="(_, i) in QUOTES"
:key="i"
:aria-label="`نظر شماره ${faNum(i + 1)}`"
:class="`h-1.5 rounded-full transition-all duration-500 ${
i === idx ? 'w-12 bg-accent' : 'w-4 bg-line hover:bg-muted'
}`"
@click="idx = i"
/>
</div>
</div>
</section>
</template>
+19
View File
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { ShieldCheck } from "lucide-vue-next";
</script>
<template>
<a href="#home" class="flex items-center gap-3" aria-label="آریا ایمن">
<span
class="flex size-10 items-center justify-center rounded-xl bg-accent text-bg shadow-[0_0_28px_rgba(255,90,0,0.35)]"
>
<ShieldCheck class="size-6" />
</span>
<span class="leading-tight">
<span class="block text-lg font-black">آریا ایمن</span>
<span class="block font-mono text-[9px] tracking-[0.32em] text-muted">
ARIA IMEN CO.
</span>
</span>
</a>
</template>
+12
View File
@@ -0,0 +1,12 @@
<script setup lang="ts">
import { motion } from "motion-v";
const props = withDefaults(defineProps<{ delay?: number }>(), { delay: 0 });
const motionProps = computed(() => fade(props.delay, 34));
</script>
<template>
<motion.div v-bind="motionProps">
<slot />
</motion.div>
</template>
+42
View File
@@ -0,0 +1,42 @@
<script setup lang="ts">
import Reveal from "./Reveal.vue";
withDefaults(
defineProps<{
tag: string;
/** plain-text title; use the #title slot for rich markup (line breaks, spans) */
title?: string;
desc?: string;
center?: boolean;
headingId?: string;
}>(),
{ center: false },
);
</script>
<template>
<div :class="center ? 'text-center' : ''">
<Reveal>
<div :class="`flex items-center gap-3 ${center ? 'justify-center' : ''}`">
<span class="font-mono text-sm text-accent">//</span>
<span class="font-mono text-xs tracking-[0.25em] text-muted">
{{ tag }}
</span>
<span class="h-px w-14 bg-line" />
</div>
</Reveal>
<Reveal :delay="0.08">
<h2
:id="headingId"
class="mt-5 text-3xl font-black leading-[1.25] md:text-5xl md:leading-[1.2]"
>
<slot name="title">{{ title }}</slot>
</h2>
</Reveal>
<Reveal v-if="desc" :delay="0.16">
<p :class="`mt-5 max-w-xl leading-8 text-muted ${center ? 'mx-auto' : ''}`">
{{ desc }}
</p>
</Reveal>
</div>
</template>