Wire class cards to payment flows with plan terms, user provisioning, and a disabled pay button until the gateway is ready.
457 lines
11 KiB
Vue
457 lines
11 KiB
Vue
<template>
|
||
<div class="landing">
|
||
<section class="hero" aria-labelledby="hero-brand">
|
||
<div class="hero-media" aria-hidden="true">
|
||
<img
|
||
src="https://images.unsplash.com/photo-1524178232363-1fb2b075b655?auto=format&fit=crop&w=2000&q=80"
|
||
alt=""
|
||
width="2000"
|
||
height="1333"
|
||
/>
|
||
<div class="hero-veil"></div>
|
||
</div>
|
||
|
||
<div class="container hero-content">
|
||
<p class="hero-kicker">آموزشگاه تخصصی</p>
|
||
<h1 id="hero-brand" class="hero-brand">گام نو</h1>
|
||
<p class="hero-lead">
|
||
کلاسهای حضوری، اساتید مجرب و گواهینامه رسمی — مسیر یادگیریتان را اینجا ادامه دهید.
|
||
</p>
|
||
<div class="hero-actions">
|
||
<RouterLink
|
||
:to="auth.isAuthenticated ? { name: 'account' } : { name: 'login' }"
|
||
class="btn btn-primary"
|
||
>
|
||
{{ auth.isAuthenticated ? 'ورود به پنل کاربری' : 'ورود به حساب' }}
|
||
</RouterLink>
|
||
<a href="#upcoming-classes" class="btn btn-ghost">کلاسهای پیشرو</a>
|
||
<a href="#contact" class="btn btn-ghost">درخواست مشاوره</a>
|
||
</div>
|
||
|
||
<div class="step-path" aria-hidden="true">
|
||
<span v-for="n in 5" :key="n" class="step" :style="{ '--i': n }"></span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="upcoming-classes" class="section upcoming-section">
|
||
<div class="container">
|
||
<header class="section-head">
|
||
<p class="section-eyebrow">ثبتنام باز</p>
|
||
<h2>کلاسهای پیشرو</h2>
|
||
<p>کلاسهایی که به زودی شروع میشوند — ظرفیت، زمان و شهریه را ببینید و برای ثبتنام درخواست مشاوره دهید.</p>
|
||
</header>
|
||
|
||
<div v-if="classesLoading" class="loading-block">
|
||
<div class="spinner" aria-label="در حال بارگذاری"></div>
|
||
</div>
|
||
|
||
<div v-else-if="!upcomingClasses.length" class="empty-state">
|
||
<h3>فعلاً کلاس پیشرویی نیست</h3>
|
||
<p>به زودی کلاسهای جدید اینجا منتشر میشوند.</p>
|
||
</div>
|
||
|
||
<div v-else class="class-rail">
|
||
<ClassCard
|
||
v-for="(cls, index) in upcomingClasses"
|
||
:key="cls._id"
|
||
:cls="cls"
|
||
variant="upcoming"
|
||
:tone="index % 3"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="active-classes" class="section active-section">
|
||
<div class="container">
|
||
<header class="section-head">
|
||
<p class="section-eyebrow">در حال برگزاری</p>
|
||
<h2>کلاسهای فعال</h2>
|
||
<p>کلاسهای در حال برگزاری — برای تشکیل کلاس جدید همان دوره، درخواست دهید و بیعانه پرداخت کنید.</p>
|
||
</header>
|
||
|
||
<div v-if="classesLoading" class="loading-block">
|
||
<div class="spinner" aria-label="در حال بارگذاری"></div>
|
||
</div>
|
||
|
||
<div v-else-if="!activeClasses.length" class="empty-state">
|
||
<h3>فعلاً کلاس فعالی برای نمایش نیست</h3>
|
||
<p>کلاسهای در حال برگزاری به محض شروع اینجا نمایش داده میشوند.</p>
|
||
</div>
|
||
|
||
<div v-else class="class-rail">
|
||
<ClassCard
|
||
v-for="(cls, index) in activeClasses"
|
||
:key="cls._id"
|
||
:cls="cls"
|
||
variant="active"
|
||
:tone="index % 3"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="path" class="section path-section">
|
||
<div class="container path-grid">
|
||
<div class="path-copy">
|
||
<h2>سه قدم تا گواهینامه</h2>
|
||
<p>از ثبتنام تا دریافت گواهی، همه چیز در حساب کاربریتان یکجا جمع است.</p>
|
||
</div>
|
||
<ol class="path-steps">
|
||
<li>
|
||
<strong>ورود به حساب</strong>
|
||
<span>با نام کاربری و رمز عبور وارد شوید.</span>
|
||
</li>
|
||
<li>
|
||
<strong>پیگیری کلاسها</strong>
|
||
<span>کلاسهای ثبتشده، استاد و بازه زمانی را ببینید.</span>
|
||
</li>
|
||
<li>
|
||
<strong>دریافت گواهی</strong>
|
||
<span>گواهینامههای صادرشده را دانلود کنید.</span>
|
||
</li>
|
||
</ol>
|
||
</div>
|
||
</section>
|
||
|
||
<ContactInquiryForm />
|
||
|
||
<section class="section cta-section">
|
||
<div class="container cta-panel">
|
||
<h2>آماده برداشتن گام بعدی؟</h2>
|
||
<p>وارد حساب شوید و کلاسها و گواهینامههایتان را یکجا ببینید.</p>
|
||
<RouterLink
|
||
:to="auth.isAuthenticated ? { name: 'account' } : { name: 'login' }"
|
||
class="btn btn-ink"
|
||
>
|
||
{{ auth.isAuthenticated ? 'رفتن به پنل' : 'ورود به حساب' }}
|
||
</RouterLink>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { RouterLink } from 'vue-router'
|
||
import ClassCard from '@/components/ClassCard.vue'
|
||
import ContactInquiryForm from '@/components/ContactInquiryForm.vue'
|
||
import { classesApi } from '@/api/classesApi'
|
||
import { useAuthStore } from '@/stores/authStore'
|
||
|
||
const auth = useAuthStore()
|
||
const allClasses = ref([])
|
||
const classesLoading = ref(true)
|
||
|
||
const todayStart = () => {
|
||
const d = new Date()
|
||
d.setHours(0, 0, 0, 0)
|
||
return d.getTime()
|
||
}
|
||
|
||
const upcomingClasses = computed(() =>
|
||
allClasses.value.filter((cls) => {
|
||
if (cls.daysUntilStart == null) return false
|
||
return cls.daysUntilStart > 0
|
||
})
|
||
)
|
||
|
||
const activeClasses = computed(() =>
|
||
allClasses.value.filter((cls) => {
|
||
if (cls.daysUntilStart == null) return cls.isActive !== false
|
||
if (cls.daysUntilStart > 0) return false
|
||
if (!cls.endDate) return true
|
||
return new Date(cls.endDate).getTime() >= todayStart()
|
||
})
|
||
)
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
const res = await classesApi.getAll({ limit: 50 })
|
||
allClasses.value = res.data || []
|
||
} catch {
|
||
allClasses.value = []
|
||
} finally {
|
||
classesLoading.value = false
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.hero {
|
||
position: relative;
|
||
min-height: 100vh;
|
||
min-height: 100dvh;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
color: var(--chalk);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.hero-media {
|
||
position: absolute;
|
||
inset: 0;
|
||
}
|
||
|
||
.hero-media img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
transform: scale(1.04);
|
||
animation: heroDrift 18s ease-in-out infinite alternate;
|
||
}
|
||
|
||
@keyframes heroDrift {
|
||
from { transform: scale(1.04) translate3d(0, 0, 0); }
|
||
to { transform: scale(1.08) translate3d(-1.5%, -1%, 0); }
|
||
}
|
||
|
||
.hero-veil {
|
||
position: absolute;
|
||
inset: 0;
|
||
background:
|
||
linear-gradient(180deg, rgba(12, 34, 27, 0.35) 0%, rgba(12, 34, 27, 0.55) 42%, rgba(12, 34, 27, 0.92) 100%),
|
||
radial-gradient(ellipse at 70% 20%, rgba(232, 163, 23, 0.22), transparent 55%);
|
||
}
|
||
|
||
.hero-content {
|
||
position: relative;
|
||
z-index: 1;
|
||
padding-block: 7rem 4.5rem;
|
||
max-width: 40rem;
|
||
margin-inline: 0 auto 0;
|
||
animation: heroIn 0.9s var(--ease) both;
|
||
}
|
||
|
||
@keyframes heroIn {
|
||
from { opacity: 0; transform: translateY(24px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.hero-kicker {
|
||
font-size: 0.9rem;
|
||
font-weight: 700;
|
||
letter-spacing: 0.08em;
|
||
color: var(--saffron);
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.hero-brand {
|
||
font-family: var(--font-display);
|
||
font-size: clamp(3.8rem, 12vw, 6.5rem);
|
||
font-weight: 700;
|
||
line-height: 0.95;
|
||
margin-bottom: 1rem;
|
||
text-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
|
||
}
|
||
|
||
.hero-lead {
|
||
font-size: clamp(1rem, 2.4vw, 1.2rem);
|
||
color: rgba(247, 251, 248, 0.88);
|
||
max-width: 32rem;
|
||
margin-bottom: 1.75rem;
|
||
}
|
||
|
||
.hero-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.75rem;
|
||
margin-bottom: 2.5rem;
|
||
}
|
||
|
||
.step-path {
|
||
display: flex;
|
||
gap: 1.1rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.step {
|
||
width: 0.7rem;
|
||
height: 0.7rem;
|
||
border-radius: 50%;
|
||
background: var(--saffron);
|
||
opacity: 0;
|
||
animation: stepIn 0.6s var(--ease) forwards;
|
||
animation-delay: calc(var(--i) * 0.12s + 0.5s);
|
||
box-shadow: 0 0 0 6px rgba(232, 163, 23, 0.18);
|
||
position: relative;
|
||
}
|
||
|
||
.step::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset-inline-start: 100%;
|
||
top: 50%;
|
||
width: 1.1rem;
|
||
height: 2px;
|
||
background: linear-gradient(90deg, rgba(232, 163, 23, 0.7), transparent);
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
.step:last-child::after {
|
||
display: none;
|
||
}
|
||
|
||
@keyframes stepIn {
|
||
from { opacity: 0; transform: translateY(8px) scale(0.6); }
|
||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||
}
|
||
|
||
.section {
|
||
padding-block: 5rem;
|
||
}
|
||
|
||
.section-head {
|
||
margin-bottom: 2rem;
|
||
max-width: 36rem;
|
||
}
|
||
|
||
.section-eyebrow {
|
||
font-size: 0.82rem;
|
||
font-weight: 800;
|
||
letter-spacing: 0.06em;
|
||
margin-bottom: 0.45rem;
|
||
}
|
||
|
||
.upcoming-section .section-eyebrow {
|
||
color: var(--saffron-deep);
|
||
}
|
||
|
||
.active-section .section-eyebrow {
|
||
color: var(--leaf);
|
||
}
|
||
|
||
.section-head h2,
|
||
.path-copy h2,
|
||
.cta-panel h2 {
|
||
font-family: var(--font-display);
|
||
font-size: clamp(2rem, 4vw, 2.8rem);
|
||
margin-bottom: 0.6rem;
|
||
}
|
||
|
||
.section-head p,
|
||
.path-copy p,
|
||
.cta-panel p {
|
||
color: var(--ink-soft);
|
||
}
|
||
|
||
.upcoming-section {
|
||
background:
|
||
radial-gradient(ellipse at 10% 0%, rgba(232, 163, 23, 0.08), transparent 50%),
|
||
linear-gradient(180deg, var(--canvas) 0%, #e8f0ea 100%);
|
||
}
|
||
|
||
.active-section {
|
||
background:
|
||
radial-gradient(ellipse at 90% 0%, rgba(45, 106, 79, 0.08), transparent 50%),
|
||
var(--chalk);
|
||
}
|
||
|
||
.class-rail {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 1.35rem;
|
||
}
|
||
|
||
.path-section {
|
||
background: var(--ink);
|
||
color: var(--chalk);
|
||
}
|
||
|
||
.path-grid {
|
||
display: grid;
|
||
grid-template-columns: 0.9fr 1.1fr;
|
||
gap: 2.5rem;
|
||
align-items: start;
|
||
}
|
||
|
||
.path-copy p {
|
||
color: rgba(247, 251, 248, 0.75);
|
||
}
|
||
|
||
.path-steps {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: grid;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.path-steps li {
|
||
display: grid;
|
||
gap: 0.25rem;
|
||
padding: 1.1rem 1.2rem;
|
||
border-radius: 1rem;
|
||
background: rgba(247, 251, 248, 0.06);
|
||
border: 1px solid rgba(247, 251, 248, 0.1);
|
||
counter-increment: step;
|
||
position: relative;
|
||
padding-inline-start: 4rem;
|
||
}
|
||
|
||
.path-steps {
|
||
counter-reset: step;
|
||
}
|
||
|
||
.path-steps li::before {
|
||
content: counter(step, persian);
|
||
position: absolute;
|
||
inset-inline-start: 1rem;
|
||
top: 1rem;
|
||
width: 2.1rem;
|
||
height: 2.1rem;
|
||
border-radius: 50%;
|
||
display: grid;
|
||
place-items: center;
|
||
background: var(--saffron);
|
||
color: var(--deep);
|
||
font-weight: 700;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.path-steps strong {
|
||
font-size: 1.05rem;
|
||
}
|
||
|
||
.path-steps span {
|
||
color: rgba(247, 251, 248, 0.72);
|
||
font-size: 0.92rem;
|
||
}
|
||
|
||
.cta-section {
|
||
padding-block: 4rem 5rem;
|
||
}
|
||
|
||
.cta-panel {
|
||
text-align: center;
|
||
padding: 3rem 1.5rem;
|
||
border-radius: 1.75rem;
|
||
background:
|
||
radial-gradient(circle at 20% 20%, rgba(232, 163, 23, 0.2), transparent 45%),
|
||
linear-gradient(135deg, #e7f1ea, #cfe0d4);
|
||
border: 1px solid rgba(20, 53, 43, 0.08);
|
||
}
|
||
|
||
.cta-panel p {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
@media (max-width: 860px) {
|
||
.path-grid {
|
||
grid-template-columns: 1fr;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.hero-content {
|
||
padding-block: 6rem 3.5rem;
|
||
}
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.hero-media img {
|
||
animation: none;
|
||
}
|
||
}
|
||
</style>
|