Replace the landing with the artifact design and rebuild the admin in Ant Design.

Adds consultation leads, section editors, and the one-time content reset for this release.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-01 20:36:06 +03:30
co-authored by Cursor
parent 5147c661f0
commit b0088c35d1
130 changed files with 8024 additions and 2801 deletions
@@ -0,0 +1,118 @@
<template>
<footer id="contact" class="bg-white dark:bg-[#060a14] border-t border-slate-200/80 dark:border-slate-800/80 pt-16 pb-12 text-right">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-10 pb-12 border-b border-slate-100 dark:border-slate-800/80">
<div class="lg:col-span-3 space-y-3">
<h2 class="text-sm font-bold text-slate-900 dark:text-white">{{ footer.contactTitle }}</h2>
<ul class="space-y-3 text-xs text-slate-600 dark:text-slate-400 font-medium">
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-brand-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" /></svg>
<span>{{ settings.phone }}</span>
</li>
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-brand-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
<span>{{ settings.email }}</span>
</li>
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-brand-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
<span>{{ settings.address }}</span>
</li>
</ul>
</div>
<div class="lg:col-span-3 space-y-3">
<h2 class="text-sm font-bold text-slate-900 dark:text-white">{{ footer.productsTitle }}</h2>
<ul class="space-y-2 text-xs text-slate-600 dark:text-slate-400 font-medium">
<li v-for="link in footer.products" :key="link.label">
<a :href="resolveHref(link.href)" class="hover:text-brand-600 dark:hover:text-brand-400 transition-colors">{{ link.label }}</a>
</li>
</ul>
</div>
<div class="lg:col-span-2 space-y-3">
<h2 class="text-sm font-bold text-slate-900 dark:text-white">{{ footer.quickLinksTitle }}</h2>
<ul class="space-y-2 text-xs text-slate-600 dark:text-slate-400 font-medium">
<li v-for="link in footer.quickLinks" :key="link.label">
<a :href="resolveHref(link.href)" class="hover:text-brand-600 dark:hover:text-brand-400 transition-colors">{{ link.label }}</a>
</li>
</ul>
</div>
<div class="lg:col-span-4 space-y-4">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br from-brand-600 to-sky-400 text-white">
<svg class="w-6 h-6" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<path d="M6 22L12 8L16 15L20 8L26 22" stroke="white" stroke-width="3" stroke-linecap="round" />
</svg>
</div>
<div class="flex flex-col">
<span class="text-xl font-black text-slate-900 dark:text-white">{{ footer.brandTitle }}</span>
<span class="text-[9px] font-bold text-slate-400 tracking-wider">{{ footer.brandSubtitle }}</span>
</div>
</div>
<p class="text-xs text-slate-400 dark:text-slate-500 leading-relaxed font-normal">
<template v-for="(line, i) in footer.descriptionLines" :key="i">
<br v-if="i > 0">
{{ line }}
</template>
</p>
<div class="flex items-center gap-3 pt-2">
<a
v-for="social in socials"
:key="social.kind + social.url"
:href="social.url"
:aria-label="social.label"
:target="social.url.startsWith('http') ? '_blank' : undefined"
:rel="social.url.startsWith('http') ? 'noopener noreferrer' : undefined"
class="w-8 h-8 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-brand-600 hover:text-white flex items-center justify-center transition-colors"
>
<SocialIcon :kind="social.kind" />
</a>
</div>
</div>
</div>
<div class="pt-8 flex flex-col sm:flex-row items-center justify-between text-xs text-slate-400 dark:text-slate-500 gap-4">
<div>{{ footer.copyright }}</div>
<div class="flex items-center gap-6">
<a
v-for="link in footer.legalLinks"
:key="link.label"
:href="resolveHref(link.href)"
class="hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>{{ link.label }}</a>
</div>
</div>
</div>
</footer>
</template>
<script setup lang="ts">
type SocialKind = "telegram" | "linkedin" | "instagram" | "link";
const DEFAULT_SOCIALS: Array<{ kind: SocialKind; label: string; url: string }> = [
{ kind: "telegram", label: "تلگرام", url: "#" },
{ kind: "linkedin", label: "لینکدین", url: "#" },
{ kind: "instagram", label: "اینستاگرام", url: "#" },
];
function detectKind(label: string, url: string): SocialKind {
const s = `${label} ${url}`.toLowerCase();
if (s.includes("t.me") || s.includes("telegram") || s.includes("تلگرام")) return "telegram";
if (s.includes("linkedin") || s.includes("لینکدین")) return "linkedin";
if (s.includes("instagram") || s.includes("اینستاگرام")) return "instagram";
return "link";
}
const { section, settings } = useSiteContent();
const footer = section("footer");
const resolveHref = useSectionHref();
const socials = computed(() => {
const list = settings.value.socials;
if (!list.length) return DEFAULT_SOCIALS;
return list.map((s) => ({ kind: detectKind(s.label, s.url), label: s.label, url: s.url }));
});
</script>
@@ -0,0 +1,94 @@
<template>
<header class="sticky top-0 z-50 transition-all duration-300 bg-white/90 dark:bg-[#0b1329]/90 backdrop-blur-xl border-b border-slate-200/70 dark:border-slate-800/80 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-20 flex items-center justify-between">
<div class="flex items-center gap-3">
<ThemeToggle />
<button
type="button"
class="hidden sm:inline-flex items-center gap-2.5 px-6 py-2.5 rounded-full bg-brand-600 hover:bg-brand-700 active:scale-95 text-white text-sm font-semibold transition-all duration-300 shadow-md shadow-brand-600/25 group"
@click="consult.open()"
>
<span class="w-6 h-6 rounded-full bg-white/20 flex items-center justify-center -mr-1 transition-transform group-hover:-translate-x-1">
<svg class="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</span>
<span>{{ header.ctaLabel }}</span>
</button>
<button
type="button"
class="md:hidden p-2 rounded-xl text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors focus:outline-none"
:aria-expanded="isMobileMenuOpen"
aria-controls="mobile-nav"
:aria-label="isMobileMenuOpen ? 'بستن منو' : 'باز کردن منو'"
@click="isMobileMenuOpen = !isMobileMenuOpen"
>
<svg v-if="!isMobileMenuOpen" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<svg v-else class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<nav class="hidden md:flex items-center gap-8 text-sm font-semibold text-slate-600 dark:text-slate-300" aria-label="منوی اصلی">
<a
v-for="link in desktopNav"
:key="link.href + link.label"
:href="resolveHref(link.href)"
class="hover:text-brand-600 dark:hover:text-brand-400 transition-colors duration-200"
>{{ link.label }}</a>
</nav>
<a :href="resolveHref('#')" class="flex items-center gap-3 group focus:outline-none" :aria-label="header.brandTitle">
<div class="flex flex-col text-right">
<span class="text-2xl font-black tracking-tight text-slate-900 dark:text-white group-hover:text-brand-600 dark:group-hover:text-brand-400 transition-colors">{{ header.brandTitle }}</span>
<span class="text-[9px] font-black text-slate-400 dark:text-slate-500 tracking-[0.25em] uppercase -mt-1">{{ header.brandSubtitle }}</span>
</div>
<div class="relative flex items-center justify-center w-11 h-11 rounded-2xl bg-gradient-to-br from-brand-600 via-brand-500 to-sky-400 text-white shadow-md shadow-brand-600/25 transition-all duration-500 group-hover:scale-105 group-hover:shadow-brand-600/40">
<svg class="w-7 h-7" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<path d="M6 22L12 8L16 15L20 8L26 22" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="16" cy="22" r="2.2" fill="white" />
</svg>
</div>
</a>
</div>
<transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="transform -translate-y-4 opacity-0"
enter-to-class="transform translate-y-0 opacity-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="transform translate-y-0 opacity-100"
leave-to-class="transform -translate-y-4 opacity-0"
>
<div v-if="isMobileMenuOpen" id="mobile-nav" class="md:hidden bg-white/95 dark:bg-[#0b1329]/95 backdrop-blur-xl border-b border-slate-200 dark:border-slate-800 px-6 py-5 space-y-4 shadow-2xl">
<a
v-for="link in header.nav"
:key="link.href + link.label"
:href="resolveHref(link.href)"
class="block text-base font-semibold text-slate-700 dark:text-slate-200 hover:text-brand-600 dark:hover:text-brand-400"
@click="isMobileMenuOpen = false"
>{{ link.label }}</a>
<div class="pt-2 flex flex-col gap-2">
<button type="button" class="w-full flex items-center justify-center gap-2 py-3 rounded-full bg-brand-600 text-white font-bold shadow-md" @click="consult.open(); isMobileMenuOpen = false">
<span>{{ header.ctaLabel }}</span>
<svg class="w-4 h-4 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" /></svg>
</button>
</div>
</div>
</transition>
</header>
</template>
<script setup lang="ts">
const { section } = useSiteContent();
const header = section("header");
const consult = useConsultModal();
const isMobileMenuOpen = ref(false);
const desktopNav = computed(() => [...header.value.nav].reverse());
const resolveHref = useSectionHref();
</script>
@@ -0,0 +1,22 @@
<template>
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path :d="PATHS[kind]" />
</svg>
</template>
<script setup lang="ts">
export type SocialKind = "telegram" | "linkedin" | "instagram" | "link";
defineProps<{ kind: SocialKind }>();
const PATHS: Record<SocialKind, string> = {
telegram:
"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69a.2.2 0 00-.05-.18c-.06-.05-.14-.03-.21-.02-.09.02-1.49.95-4.22 2.79-.4.27-.76.41-1.08.4-.36-.01-1.04-.2-1.55-.37-.63-.2-1.12-.31-1.08-.66.02-.18.27-.36.74-.55 2.92-1.27 4.86-2.11 5.83-2.51 2.78-1.16 3.35-1.36 3.73-1.36.08 0 .27.02.39.12.1.08.13.19.14.27-.01.06.01.24 0 .38z",
linkedin:
"M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14m-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.28 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93h2.75M6.46 8.76a1.64 1.64 0 1 0 0-3.28 1.64 1.64 0 0 0 0 3.28m1.4 9.74v-8.37H5.06v8.37h2.8z",
instagram:
"M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z",
link:
"M10.59 13.41a1 1 0 0 0 1.41 0l3.54-3.54a3 3 0 0 0-4.24-4.24l-1.77 1.77a1 1 0 1 0 1.41 1.41l1.77-1.77a1 1 0 0 1 1.42 1.42l-3.54 3.54a1 1 0 0 0 0 1.41zm2.82-2.82a1 1 0 0 0-1.41 0l-3.54 3.54a3 3 0 0 0 4.24 4.24l1.77-1.77a1 1 0 1 0-1.41-1.41l-1.77 1.77a1 1 0 0 1-1.42-1.42l3.54-3.54a1 1 0 0 0 0-1.41z",
};
</script>
@@ -0,0 +1,23 @@
<template>
<button
type="button"
:title="label"
:aria-label="label"
class="p-2.5 rounded-full border border-slate-200/90 dark:border-slate-700/80 bg-slate-100/80 dark:bg-slate-800/90 text-slate-600 dark:text-amber-400 hover:text-brand-600 dark:hover:text-amber-300 transition-all duration-300 shadow-sm hover:scale-105 active:scale-95 focus:outline-none"
@click="toggle"
>
<!-- Both icons are server-rendered; the `dark` class picks one, so SSR and hydration always agree. -->
<svg class="hidden dark:block w-5 h-5 transition-transform rotate-0 hover:rotate-45" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<svg class="dark:hidden w-5 h-5 text-slate-700 transition-transform hover:-rotate-12" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</button>
</template>
<script setup lang="ts">
const { isDark, sync, toggle } = useTheme();
const label = computed(() => (isDark.value ? "تغییر به حالت روشن" : "تغییر به حالت تاریک"));
onMounted(sync);
</script>