feat: complete UI/UX improvements, vuesax icons, media server, and section management
This commit is contained in:
@@ -1,16 +1,7 @@
|
||||
<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 VuesaxIcon from "~/components/ui/VuesaxIcon.vue";
|
||||
import Lines from "~/components/ui/Lines.vue";
|
||||
import Reveal from "~/components/ui/Reveal.vue";
|
||||
import SectionHead from "~/components/ui/SectionHead.vue";
|
||||
@@ -21,10 +12,10 @@ const content = computed(() => site.sections.contact);
|
||||
const INFO = computed(() => {
|
||||
const s = site.settings;
|
||||
return [
|
||||
{ icon: MapPin, label: "نشانی", value: `${s.address.locality}، ${s.address.street}` },
|
||||
{ icon: Phone, label: "تلفن تماس", value: s.phoneDisplay, ltr: true, href: `tel:${s.phone}` },
|
||||
{ icon: Mail, label: "ایمیل", value: s.email, ltr: true, href: `mailto:${s.email}` },
|
||||
{ icon: Clock, label: "ساعات کاری", value: s.workingHours },
|
||||
{ icon: "location", label: "نشانی", value: `${s.address.locality}، ${s.address.street}` },
|
||||
{ icon: "call", label: "تلفن تماس", value: s.phoneDisplay, ltr: true, href: `tel:${s.phone}` },
|
||||
{ icon: "sms", label: "ایمیل", value: s.email, ltr: true, href: `mailto:${s.email}` },
|
||||
{ icon: "clock", label: "ساعات کاری", value: s.workingHours },
|
||||
];
|
||||
});
|
||||
|
||||
@@ -61,7 +52,7 @@ const contact = useContactStore();
|
||||
<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" />
|
||||
<VuesaxIcon :name="item.icon" class="size-5" />
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-muted">{{ item.label }}</div>
|
||||
@@ -80,7 +71,7 @@ const contact = useContactStore();
|
||||
|
||||
<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" />
|
||||
<VuesaxIcon name="lamp-on" class="size-6 shrink-0 text-hazard" />
|
||||
<div>
|
||||
<div class="font-bold text-hazard">{{ content.emergencyTitle }}</div>
|
||||
<p class="mt-1.5 text-sm leading-7 text-muted">
|
||||
@@ -105,7 +96,7 @@ const contact = useContactStore();
|
||||
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" />
|
||||
<VuesaxIcon name="tick-circle" class="size-14 text-emerald-400" />
|
||||
<div class="mt-5 text-xl font-black">{{ content.successTitle }}</div>
|
||||
<p class="mt-2.5 max-w-sm text-sm leading-7 text-muted">
|
||||
{{ content.successText }}
|
||||
@@ -176,11 +167,11 @@ const contact = useContactStore();
|
||||
:disabled="contact.status === 'loading'"
|
||||
>
|
||||
<template v-if="contact.status === 'loading'">
|
||||
<Loader2 class="size-5 animate-spin" />
|
||||
<VuesaxIcon name="refresh-circle" class="size-5 animate-spin" />
|
||||
در حال ارسال...
|
||||
</template>
|
||||
<template v-else>
|
||||
<Send class="size-5" />
|
||||
<VuesaxIcon name="send-2" class="size-5" />
|
||||
ارسال درخواست مشاوره
|
||||
</template>
|
||||
</AButton>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeft, ChevronDown, HardHat } from "lucide-vue-next";
|
||||
import VuesaxIcon from "~/components/ui/VuesaxIcon.vue";
|
||||
import DynamicIcon from "~/components/ui/DynamicIcon.vue";
|
||||
|
||||
const hero = computed(() => useSiteStore().sections.hero);
|
||||
@@ -16,68 +16,81 @@ const hero = computed(() => useSiteStore().sections.hero);
|
||||
height="1008"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
class="absolute inset-0 size-full object-cover"
|
||||
class="absolute inset-0 size-full object-cover object-[center_15%] sm:object-right-top"
|
||||
/>
|
||||
<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" />
|
||||
<!-- Mobile Gradient: 0 at the top so image is fully clear, reaches 100% at the texts -->
|
||||
<div
|
||||
class="sm:hidden absolute inset-0 pointer-events-none"
|
||||
style="background: linear-gradient(to bottom, transparent 0%, transparent 26%, rgba(11, 18, 36, 0.45) 42%, rgba(11, 18, 36, 0.95) 54%, #0b1224 62%, #0b1224 100%);"
|
||||
/>
|
||||
|
||||
<!-- Desktop Gradient: Right side dark for text, left side fading out as requested -->
|
||||
<div class="hidden sm:block absolute inset-0 bg-gradient-to-l from-bg via-bg/65 via-25% to-transparent" />
|
||||
<div class="hidden sm:block absolute inset-0 bg-gradient-to-t from-bg via-transparent to-bg/50" />
|
||||
<div class="hidden sm:block absolute inset-0 bg-grid opacity-30" />
|
||||
</div>
|
||||
|
||||
<!-- ---- side tape ---- -->
|
||||
<div class="tape-v absolute bottom-0 end-0 z-10 h-44 w-1.5 opacity-70" />
|
||||
<div class="tape-v absolute bottom-0 end-0 z-10 h-32 sm:h-44 w-1 sm:w-1.5 opacity-70" />
|
||||
|
||||
<!-- ---- content ---- -->
|
||||
<div
|
||||
class="relative z-10 mx-auto flex w-full max-w-[1480px] flex-1 items-center px-5 pb-24 pt-36 md:px-8"
|
||||
class="relative z-10 mx-auto flex w-full max-w-[1480px] flex-1 items-end sm:items-center px-4 pb-6 pt-32 sm:pt-32 md:pt-36 sm:px-6 md:px-8 md:pb-24"
|
||||
>
|
||||
<div class="grid w-full items-center gap-12 lg:grid-cols-12">
|
||||
<div class="lg:col-span-7">
|
||||
<div class="w-full max-w-3xl">
|
||||
<div>
|
||||
<!-- Badge -->
|
||||
<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"
|
||||
class="animate-fade-up-1 inline-flex items-center gap-1.5 sm:gap-2.5 rounded-full border border-accent/30 bg-accent/10 px-3 py-1 sm:px-4 sm:py-2 backdrop-blur"
|
||||
>
|
||||
<span class="size-1.5 animate-pulse rounded-full bg-accent" />
|
||||
<span class="text-xs font-medium text-accent">
|
||||
<span class="text-[11px] sm:text-xs font-medium text-accent">
|
||||
{{ hero.badge }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Title & Highlight -->
|
||||
<h1
|
||||
class="animate-fade-up-2 mt-7 text-5xl font-black leading-[1.15] md:text-7xl md:leading-[1.1]"
|
||||
class="animate-fade-up-2 mt-3 sm:mt-7 text-2xl sm:text-4xl md:text-6xl font-black leading-tight sm:leading-[1.15] md:leading-[1.1]"
|
||||
>
|
||||
{{ hero.title }}
|
||||
<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)]"
|
||||
class="mt-2 inline-block whitespace-nowrap rounded-md bg-hazard px-2.5 py-1 text-lg font-black text-bg shadow-sm sm:mt-3 sm:rounded-lg sm:px-4 sm:py-1.5 sm:text-3xl sm:-rotate-1 sm:shadow-[0_10px_50px_rgba(255,184,0,0.25)] md:text-5xl"
|
||||
>
|
||||
{{ hero.titleHighlight }}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<!-- Intro text -->
|
||||
<p
|
||||
class="animate-fade-up-3 mt-7 max-w-xl text-base leading-9 text-ink/70 md:text-lg md:leading-10"
|
||||
class="animate-fade-up-3 mt-3 sm:mt-6 max-w-xl text-[12px] sm:text-base leading-relaxed sm:leading-9 text-ink/75 md:text-lg md:leading-10"
|
||||
>
|
||||
{{ hero.intro }}
|
||||
</p>
|
||||
|
||||
<div class="animate-fade-up-4 mt-9 flex flex-wrap items-center gap-4">
|
||||
<!-- CTA buttons -->
|
||||
<div class="animate-fade-up-4 mt-5 sm:mt-9 flex flex-wrap items-center gap-2.5 sm:gap-4">
|
||||
<a
|
||||
:href="hero.primaryCta.href"
|
||||
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)]"
|
||||
class="group inline-flex items-center gap-1.5 sm:gap-2.5 rounded-full bg-accent px-4 py-2.5 sm:px-7 sm:py-4 text-xs sm:text-sm md:text-base font-bold text-bg transition-all duration-300 hover:bg-accent-soft hover:shadow-[0_8px_45px_rgba(255,90,0,0.5)]"
|
||||
>
|
||||
{{ hero.primaryCta.label }}
|
||||
<ArrowLeft
|
||||
class="size-5 transition-transform duration-300 group-hover:-translate-x-1.5"
|
||||
<VuesaxIcon
|
||||
name="arrow-left"
|
||||
class="size-3.5 sm:size-5 transition-transform duration-300 group-hover:-translate-x-1.5"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
:href="hero.secondaryCta.href"
|
||||
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"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-ink/25 px-4 py-2.5 sm:px-7 sm:py-4 text-xs sm:text-sm md:text-base font-bold text-ink backdrop-blur transition-colors duration-300 hover:border-accent hover:text-accent"
|
||||
>
|
||||
{{ hero.secondaryCta.label }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="animate-fade-up-5 mt-10 flex flex-wrap gap-3">
|
||||
<!-- Feature Chips (desktop/tablet only to keep mobile clean) -->
|
||||
<div class="animate-fade-up-5 mt-6 sm:mt-10 hidden sm:flex flex-wrap gap-3">
|
||||
<span
|
||||
v-for="c in hero.chips"
|
||||
:key="c.label"
|
||||
@@ -88,84 +101,27 @@ const hero = computed(() => useSiteStore().sections.hero);
|
||||
</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">
|
||||
IMEN PISHGAMAN 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
|
||||
v-if="hero.lastInspection"
|
||||
class="mt-3.5 flex items-center justify-between font-mono text-[11px]"
|
||||
>
|
||||
<span class="text-muted">آخرین بازرسی</span>
|
||||
<span>{{ hero.lastInspection }}</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="animate-fade-up-9 absolute bottom-24 sm: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" />
|
||||
<VuesaxIcon name="arrow-down-1" 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-[1480px] grid-cols-1 gap-px bg-ink/10 sm:grid-cols-3">
|
||||
<div class="mx-auto grid max-w-[1480px] grid-cols-3 gap-px bg-ink/10">
|
||||
<div
|
||||
v-for="s in hero.miniStats"
|
||||
:key="s.label"
|
||||
class="flex items-baseline gap-4 bg-bg/60 px-6 py-5 backdrop-blur-md md:px-8"
|
||||
class="flex flex-col sm:flex-row sm:items-baseline sm:gap-4 bg-bg/60 px-2.5 py-3 text-center sm:text-right backdrop-blur-md sm:px-6 sm:py-5 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>
|
||||
<span class="text-base font-black text-ink sm:text-2xl md:text-3xl">{{ s.value }}</span>
|
||||
<span class="mt-0.5 sm:mt-0 text-[10px] sm:text-xs leading-tight text-muted md:text-sm">{{ s.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { AnimatePresence, motion, useMotionValue, useSpring } from "motion-v";
|
||||
import { ArrowLeft } from "lucide-vue-next";
|
||||
import VuesaxIcon from "~/components/ui/VuesaxIcon.vue";
|
||||
import SectionHead from "~/components/ui/SectionHead.vue";
|
||||
|
||||
const industries = computed(() => useSiteStore().sections.industries);
|
||||
@@ -59,7 +59,7 @@ const onMove = (e: MouseEvent) => {
|
||||
<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" />
|
||||
<VuesaxIcon name="arrow-left" class="size-4 md:size-5" />
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeft } from "lucide-vue-next";
|
||||
import DynamicIcon from "~/components/ui/DynamicIcon.vue";
|
||||
import Reveal from "~/components/ui/Reveal.vue";
|
||||
import SectionHead from "~/components/ui/SectionHead.vue";
|
||||
@@ -8,11 +7,65 @@ const site = useSiteStore();
|
||||
const intro = computed(() => site.sections.productsIntro);
|
||||
const categories = computed(() => site.categories);
|
||||
|
||||
// Desktop active accordion index
|
||||
const active = ref(0);
|
||||
|
||||
// Mobile interactive state
|
||||
const activeIndex = ref(0);
|
||||
const scrollContainer = ref<HTMLElement | null>(null);
|
||||
|
||||
function scrollToCategory(index: number) {
|
||||
activeIndex.value = index;
|
||||
if (!scrollContainer.value) return;
|
||||
const cards = scrollContainer.value.children;
|
||||
if (cards[index]) {
|
||||
(cards[index] as HTMLElement).scrollIntoView({
|
||||
behavior: "smooth",
|
||||
inline: "center",
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (!scrollContainer.value) return;
|
||||
const el = scrollContainer.value;
|
||||
const containerRect = el.getBoundingClientRect();
|
||||
const centerX = containerRect.left + containerRect.width / 2;
|
||||
|
||||
let closestIndex = 0;
|
||||
let minDistance = Infinity;
|
||||
|
||||
Array.from(el.children).forEach((child, i) => {
|
||||
const rect = (child as HTMLElement).getBoundingClientRect();
|
||||
const childCenterX = rect.left + rect.width / 2;
|
||||
const dist = Math.abs(centerX - childCenterX);
|
||||
if (dist < minDistance) {
|
||||
minDistance = dist;
|
||||
closestIndex = i;
|
||||
}
|
||||
});
|
||||
|
||||
activeIndex.value = closestIndex;
|
||||
}
|
||||
|
||||
function nextCategory() {
|
||||
const count = categories.value.length;
|
||||
if (count === 0) return;
|
||||
const next = (activeIndex.value + 1) % count;
|
||||
scrollToCategory(next);
|
||||
}
|
||||
|
||||
function prevCategory() {
|
||||
const count = categories.value.length;
|
||||
if (count === 0) return;
|
||||
const prev = (activeIndex.value - 1 + count) % count;
|
||||
scrollToCategory(prev);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section id="products" aria-labelledby="products-title" class="relative py-24 md:py-32">
|
||||
<section id="products" aria-labelledby="products-title" class="relative py-20 sm:py-24 md:py-32">
|
||||
<div class="mx-auto max-w-[1480px] px-5 md:px-8">
|
||||
<div class="flex flex-wrap items-end justify-between gap-6">
|
||||
<SectionHead heading-id="products-title" v-bind="intro.head" />
|
||||
@@ -28,38 +81,96 @@ const active = ref(0);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ---- mobile: card grid ---- -->
|
||||
<div class="mx-auto mt-14 max-w-[1480px] px-5 md:hidden">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<Reveal v-for="(cat, i) in categories" :key="cat.code" :delay="i * 0.05">
|
||||
<NuxtLink
|
||||
:to="`/categories/${cat.slug}`"
|
||||
class="group relative block aspect-[3/4] overflow-hidden rounded-xl border border-line transition-colors duration-300 active:border-accent/60"
|
||||
<!-- ---- mobile: compact interactive deck ---- -->
|
||||
<div class="mx-auto mt-4 max-w-[1480px] px-4 md:hidden">
|
||||
<!-- Snapping Cards Deck (compact size so more of side cards are visible) -->
|
||||
<div
|
||||
ref="scrollContainer"
|
||||
class="flex items-center gap-4 overflow-x-auto snap-x snap-mandatory no-scrollbar -mx-4 px-8 py-8 scroll-smooth"
|
||||
@scroll.passive="handleScroll"
|
||||
>
|
||||
<div
|
||||
v-for="(cat, i) in categories"
|
||||
:key="cat.code"
|
||||
:class="`w-[54vw] max-w-[215px] aspect-[4/5] snap-center shrink-0 relative overflow-hidden rounded-xl border transition-all duration-500 ease-out ${
|
||||
activeIndex === i
|
||||
? 'border-accent/90 shadow-[0_0_20px_rgba(255,90,0,0.36)] ring-1 ring-accent/30 scale-100'
|
||||
: 'border-line/70 opacity-55 scale-[0.92]'
|
||||
}`"
|
||||
>
|
||||
<!-- Background Image -->
|
||||
<img
|
||||
v-if="cat.coverImage"
|
||||
:src="cat.coverImage"
|
||||
:alt="cat.name"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
:class="`absolute inset-0 size-full object-cover transition-transform duration-700 ${
|
||||
activeIndex === i ? 'scale-105' : 'scale-100'
|
||||
}`"
|
||||
/>
|
||||
<div v-else class="absolute inset-0 bg-panel" />
|
||||
|
||||
<!-- Clean Dark Gradient for Text Legibility -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-bg via-bg/50 via-50% to-bg/15" />
|
||||
|
||||
<!-- Industrial Watermark Number in background -->
|
||||
<div
|
||||
class="absolute -top-2 end-1.5 font-mono text-[56px] font-black leading-none text-ink/5 select-none pointer-events-none"
|
||||
>
|
||||
<img
|
||||
v-if="cat.coverImage"
|
||||
:src="cat.coverImage"
|
||||
:alt="cat.name"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="absolute inset-0 size-full object-cover transition-transform duration-500 group-active:scale-105"
|
||||
/>
|
||||
<div v-else class="absolute inset-0 bg-panel" />
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-bg via-bg/50 to-transparent" />
|
||||
{{ String(i + 1).padStart(2, "0") }}
|
||||
</div>
|
||||
|
||||
<div class="absolute inset-x-0 top-0 flex items-center justify-between p-3">
|
||||
<span class="font-mono text-[9px] tracking-widest text-ink/50">{{ cat.code }}</span>
|
||||
<span class="font-mono text-[10px] text-accent">
|
||||
{{ faNum(String(i + 1).padStart(2, "0")) }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Top Row Badges -->
|
||||
<div class="absolute inset-x-0 top-0 flex items-center justify-between p-2.5 z-10">
|
||||
<span class="inline-flex items-center gap-1 rounded-full border border-ink/20 bg-bg/80 px-1.5 py-0.5 font-mono text-[8.5px] font-bold text-accent backdrop-blur-md">
|
||||
<span class="size-1 rounded-full bg-accent animate-pulse" />
|
||||
{{ cat.code }}
|
||||
</span>
|
||||
<span class="rounded-full bg-bg/75 border border-ink/10 px-1.5 py-0.5 font-mono text-[8.5px] font-bold text-ink/80 backdrop-blur-md">
|
||||
{{ faNum(String(i + 1).padStart(2, "0")) }} / {{ faNum(String(categories.length).padStart(2, "0")) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="absolute inset-x-0 bottom-0 p-3">
|
||||
<h3 class="text-sm font-black leading-6">{{ cat.name }}</h3>
|
||||
<p class="mt-0.5 font-mono text-[8px] tracking-[0.15em] text-muted">{{ cat.nameEn }}</p>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</Reveal>
|
||||
<!-- Bottom Content Area -->
|
||||
<div class="absolute inset-x-0 bottom-0 p-3 z-10">
|
||||
<span class="font-mono text-[8.5px] tracking-[0.15em] text-hazard uppercase">
|
||||
{{ cat.nameEn }}
|
||||
</span>
|
||||
<h3 class="mt-0.5 text-xs sm:text-sm font-black text-ink leading-tight line-clamp-1">
|
||||
{{ cat.name }}
|
||||
</h3>
|
||||
|
||||
<!-- Action Button -->
|
||||
<NuxtLink
|
||||
:to="`/categories/${cat.slug}`"
|
||||
class="mt-2 inline-flex w-full items-center justify-between rounded-md bg-accent px-2.5 py-1.5 text-[10.5px] font-bold text-bg transition-all duration-300 hover:bg-accent-soft active:scale-[0.98] shadow-sm shadow-accent/25"
|
||||
>
|
||||
<span>مشاهده محصولات</span>
|
||||
<ArrowLeft class="size-3" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Arrows Only (RTL correct: Left arrow scrolls left to next card, Right arrow scrolls right to previous card) -->
|
||||
<div class="mt-1 flex items-center justify-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="دسته قبلی (سمت راست)"
|
||||
class="flex size-10 items-center justify-center rounded-full border border-line bg-panel text-ink transition-colors hover:border-accent hover:text-accent active:scale-95 shadow-sm"
|
||||
@click="prevCategory"
|
||||
>
|
||||
<ArrowRight class="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="دسته بعدی (سمت چپ)"
|
||||
class="flex size-10 items-center justify-center rounded-full border border-line bg-panel text-ink transition-colors hover:border-accent hover:text-accent active:scale-95 shadow-sm"
|
||||
@click="nextCategory"
|
||||
>
|
||||
<ArrowLeft class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,7 +207,7 @@ const active = ref(0);
|
||||
: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'
|
||||
: 'bg-gradient-to-t from-bg/95 via-bg/0 to-transparent'
|
||||
}`"
|
||||
/>
|
||||
|
||||
@@ -125,19 +236,20 @@ const active = ref(0);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ---- trust section ---- -->
|
||||
<div
|
||||
class="mx-auto mt-10 grid max-w-[1480px] gap-px overflow-hidden rounded-xl border border-line bg-line md:grid-cols-3"
|
||||
>
|
||||
<Reveal v-for="(t, i) in intro.trust" :key="t.title" :delay="i * 0.08">
|
||||
<div class="flex h-full items-center gap-4 bg-panel/60 p-6">
|
||||
<div class="flex h-full items-center gap-4 bg-panel/60 p-4 sm:p-6">
|
||||
<span
|
||||
class="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent/10 text-accent"
|
||||
class="flex size-11 sm:size-12 shrink-0 items-center justify-center rounded-xl bg-accent/10 text-accent"
|
||||
>
|
||||
<DynamicIcon :name="t.icon" class="size-6" />
|
||||
<DynamicIcon :name="t.icon" class="size-5 sm: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 class="font-bold text-sm sm:text-base">{{ t.title }}</div>
|
||||
<div class="mt-1 text-xs leading-5 sm:leading-6 text-muted">{{ t.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { AnimatePresence, motion } from "motion-v";
|
||||
import { Plus } from "lucide-vue-next";
|
||||
import VuesaxIcon from "~/components/ui/VuesaxIcon.vue";
|
||||
import DynamicIcon from "~/components/ui/DynamicIcon.vue";
|
||||
import Reveal from "~/components/ui/Reveal.vue";
|
||||
import SectionHead from "~/components/ui/SectionHead.vue";
|
||||
@@ -50,7 +50,7 @@ const open = ref(0);
|
||||
: 'border-line text-muted group-hover:border-accent/50 group-hover:text-accent'
|
||||
}`"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
<VuesaxIcon name="add" class="size-5" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { FileSearch, ShieldCheck } from "lucide-vue-next";
|
||||
import VuesaxIcon from "~/components/ui/VuesaxIcon.vue";
|
||||
import Reveal from "~/components/ui/Reveal.vue";
|
||||
import SectionHead from "~/components/ui/SectionHead.vue";
|
||||
|
||||
@@ -26,7 +26,7 @@ const standards = computed(() => useSiteStore().sections.standards);
|
||||
<SectionHead heading-id="standards-title" v-bind="standards.head" />
|
||||
<Reveal v-if="standards.note" :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" />
|
||||
<VuesaxIcon name="document-filter" class="size-6 shrink-0 text-accent" />
|
||||
<p class="text-sm leading-8 text-ink/80">
|
||||
{{ standards.note }}
|
||||
</p>
|
||||
@@ -37,7 +37,7 @@ const standards = computed(() => useSiteStore().sections.standards);
|
||||
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" />
|
||||
<VuesaxIcon name="shield-tick" class="size-4 text-accent" />
|
||||
{{ standards.ctaLabel }}
|
||||
</a>
|
||||
</Reveal>
|
||||
|
||||
@@ -11,18 +11,18 @@ const stats = computed(() => useSiteStore().sections.stats.items);
|
||||
<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"
|
||||
class="group relative bg-bg p-4 sm: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"
|
||||
class="absolute end-3 top-3 sm:end-5 sm:top-5 font-mono text-[9px] sm: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">
|
||||
<div class="text-2xl sm:text-4xl md:text-6xl font-black">
|
||||
<CountUp :end="s.end" :suffix="s.suffix" />
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-muted">{{ s.label }}</div>
|
||||
<div class="mt-1.5 sm:mt-3 text-xs sm: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"
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { ShieldCheck } from "lucide-vue-next";
|
||||
import VuesaxIcon from "~/components/ui/VuesaxIcon.vue";
|
||||
|
||||
const items = computed(() => useSiteStore().sections.tape.items);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section aria-hidden="true" class="pointer-events-none relative z-20 overflow-hidden py-10">
|
||||
<section aria-hidden="true" class="pointer-events-none relative z-20 overflow-hidden py-6 sm: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="hazard h-2 sm:h-2.5" />
|
||||
<div dir="ltr" class="overflow-hidden bg-hazard py-2.5 sm: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"
|
||||
class="flex items-center gap-5 sm:gap-7 px-5 sm:px-7 text-xs sm:text-sm font-extrabold whitespace-nowrap"
|
||||
>
|
||||
{{ item }}
|
||||
<ShieldCheck class="size-4 shrink-0" :stroke-width="2.5" />
|
||||
<VuesaxIcon name="shield-tick" class="size-3.5 sm:size-4 shrink-0" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hazard h-2.5" />
|
||||
<div class="hazard h-2 sm:h-2.5" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { AnimatePresence, motion } from "motion-v";
|
||||
import { Quote } from "lucide-vue-next";
|
||||
import VuesaxIcon from "~/components/ui/VuesaxIcon.vue";
|
||||
import SectionHead from "~/components/ui/SectionHead.vue";
|
||||
|
||||
const testimonials = computed(() => useSiteStore().sections.testimonials);
|
||||
@@ -36,7 +36,7 @@ onBeforeUnmount(() => clearInterval(t));
|
||||
<span
|
||||
class="flex size-16 items-center justify-center rounded-full border border-accent/30 bg-accent/10"
|
||||
>
|
||||
<Quote class="size-7 text-accent" />
|
||||
<VuesaxIcon name="quote-up" class="size-7 text-accent" />
|
||||
</span>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
|
||||
Reference in New Issue
Block a user