Files
gameno-front/src/views/LandingView.vue
T
kavehhn 15b699a7c7 feat: add class registration pages for enrollment and class requests.
Wire class cards to payment flows with plan terms, user provisioning, and a disabled pay button until the gateway is ready.
2026-08-16 08:42:25 +03:30

457 lines
11 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>