Replace active courses with upcoming classes and improve class card UI.
Split the landing page into upcoming and active class sections with colored ClassCard components, and refresh navigation and account class cards to match.
This commit is contained in:
+96
-220
@@ -24,7 +24,7 @@
|
||||
>
|
||||
{{ auth.isAuthenticated ? 'ورود به پنل کاربری' : 'ورود به حساب' }}
|
||||
</RouterLink>
|
||||
<a href="#courses" class="btn btn-ghost">مشاهده دورهها</a>
|
||||
<a href="#upcoming-classes" class="btn btn-ghost">کلاسهای پیشرو</a>
|
||||
<a href="#contact" class="btn btn-ghost">درخواست مشاوره</a>
|
||||
</div>
|
||||
|
||||
@@ -34,100 +34,60 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="courses" class="section courses-section">
|
||||
<section id="upcoming-classes" class="section upcoming-section">
|
||||
<div class="container">
|
||||
<header class="section-head">
|
||||
<h2>دورههای فعال</h2>
|
||||
<p>از میان دورههای جاری انتخاب کنید و از پنل کاربری کلاسهای ثبتشدهتان را ببینید.</p>
|
||||
</header>
|
||||
|
||||
<div v-if="coursesLoading" class="loading-block">
|
||||
<div class="spinner" aria-label="در حال بارگذاری"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!courses.length" class="empty-state">
|
||||
<h3>هنوز دورهای ثبت نشده</h3>
|
||||
<p>به زودی فهرست دورهها اینجا نمایش داده میشود.</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="course-rail">
|
||||
<article v-for="course in courses" :key="course._id" class="course-item">
|
||||
<div class="course-meta">
|
||||
<span class="badge" :class="course.type === 'Private' ? 'badge-gold' : ''">
|
||||
{{ course.type === 'Private' ? 'خصوصی' : 'عمومی' }}
|
||||
</span>
|
||||
<span v-if="course.isOfficial" class="badge badge-gold">رسمی</span>
|
||||
</div>
|
||||
<h3>{{ course.title }}</h3>
|
||||
<p>{{ course.description || 'جزئیات دوره پس از ثبتنام در پنل کاربری در دسترس است.' }}</p>
|
||||
|
||||
<ul v-if="course.highlights?.length" class="course-highlights">
|
||||
<li v-for="(item, idx) in course.highlights" :key="idx">{{ item }}</li>
|
||||
</ul>
|
||||
|
||||
<div class="course-stats">
|
||||
<span v-if="course.sectionCount">
|
||||
{{ toPersianDigits(course.sectionCount) }} جلسه
|
||||
</span>
|
||||
<span v-if="course.hoursPerSection">
|
||||
{{ toPersianDigits(course.hoursPerSection) }} ساعت هر جلسه
|
||||
</span>
|
||||
<span v-if="course.sectionCount && course.hoursPerSection">
|
||||
مجموع {{ toPersianDigits(Number(course.sectionCount) * Number(course.hoursPerSection)) }} ساعت
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="course-foot">
|
||||
<strong>{{ formatPrice(course.price) }}</strong>
|
||||
<span v-if="course.rating">امتیاز {{ toPersianDigits(course.rating) }}</span>
|
||||
</div>
|
||||
<a href="#contact" class="course-cta">علاقهمندی و مشاوره</a>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="classes" class="section classes-section">
|
||||
<div class="container">
|
||||
<header class="section-head">
|
||||
<h2>کلاسهای فعال</h2>
|
||||
<p>ظرفیت، تخفیف و زمان شروع هر کلاس را ببینید و برای ثبتنام درخواست مشاوره دهید.</p>
|
||||
<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="!classes.length" class="empty-state">
|
||||
<h3>فعلاً کلاسی برای نمایش نیست</h3>
|
||||
<div v-else-if="!upcomingClasses.length" class="empty-state">
|
||||
<h3>فعلاً کلاس پیشرویی نیست</h3>
|
||||
<p>به زودی کلاسهای جدید اینجا منتشر میشوند.</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="course-rail">
|
||||
<article v-for="cls in classes" :key="cls._id" class="course-item class-item">
|
||||
<div class="course-meta">
|
||||
<span class="badge">{{ cls.course?.title || 'کلاس' }}</span>
|
||||
<span v-if="cls.hasDiscount && cls.discount" class="badge badge-gold">تخفیف {{ formatPrice(cls.discount) }}</span>
|
||||
</div>
|
||||
<h3>{{ cls.name }}</h3>
|
||||
<p v-if="cls.professor">
|
||||
استاد: {{ cls.professor.name }} {{ cls.professor.surname || '' }}
|
||||
</p>
|
||||
<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>
|
||||
|
||||
<div class="course-stats">
|
||||
<span v-if="cls.daysUntilStart != null">{{ formatDaysUntilStart(cls.daysUntilStart) }}</span>
|
||||
<span v-if="cls.freeSpots != null">{{ toPersianDigits(cls.freeSpots) }} ظرفیت آزاد</span>
|
||||
<span v-if="cls.startDate">شروع {{ formatJalali(cls.startDate) }}</span>
|
||||
</div>
|
||||
<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 class="course-foot">
|
||||
<div class="price-stack">
|
||||
<strong>{{ formatPrice(cls.finalTuitionFee ?? cls.tuitionFee) }}</strong>
|
||||
<span v-if="cls.hasDiscount && cls.discount" class="price-old">{{ formatPrice(cls.tuitionFee) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#contact" class="course-cta">درخواست ثبتنام</a>
|
||||
</article>
|
||||
<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>
|
||||
@@ -173,33 +133,46 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import ClassCard from '@/components/ClassCard.vue'
|
||||
import ContactInquiryForm from '@/components/ContactInquiryForm.vue'
|
||||
import { coursesApi } from '@/api/coursesApi'
|
||||
import { classesApi } from '@/api/classesApi'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { formatPrice, formatJalali, formatDaysUntilStart, toPersianDigits } from '@/utils/format'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const courses = ref([])
|
||||
const classes = ref([])
|
||||
const coursesLoading = ref(true)
|
||||
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 [coursesRes, classesRes] = await Promise.all([
|
||||
coursesApi.getAll({ limit: 50, sort: '-createdAt' }),
|
||||
classesApi.getAll({ limit: 50 })
|
||||
])
|
||||
courses.value = coursesRes.data || []
|
||||
classes.value = classesRes.data || []
|
||||
const res = await classesApi.getAll({ limit: 50 })
|
||||
allClasses.value = res.data || []
|
||||
} catch {
|
||||
courses.value = []
|
||||
classes.value = []
|
||||
allClasses.value = []
|
||||
} finally {
|
||||
coursesLoading.value = false
|
||||
classesLoading.value = false
|
||||
}
|
||||
})
|
||||
@@ -334,6 +307,21 @@ onMounted(async () => {
|
||||
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 {
|
||||
@@ -348,134 +336,22 @@ onMounted(async () => {
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.courses-section {
|
||||
.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%);
|
||||
}
|
||||
|
||||
.course-rail {
|
||||
.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(240px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.course-item {
|
||||
padding: 1.35rem 1.25rem;
|
||||
border-radius: 1.15rem;
|
||||
background: var(--chalk);
|
||||
border: 1px solid rgba(20, 53, 43, 0.06);
|
||||
box-shadow: 0 10px 30px rgba(20, 53, 43, 0.05);
|
||||
transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease);
|
||||
}
|
||||
|
||||
.course-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.course-meta {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
|
||||
.course-item h3 {
|
||||
font-size: 1.15rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.course-item p {
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.92rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
min-height: 4.4em;
|
||||
}
|
||||
|
||||
.course-highlights {
|
||||
list-style: none;
|
||||
margin: 0.9rem 0 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.course-highlights li {
|
||||
position: relative;
|
||||
padding-inline-start: 1rem;
|
||||
font-size: 0.86rem;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.course-highlights li::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
top: 0.55em;
|
||||
width: 0.35rem;
|
||||
height: 0.35rem;
|
||||
border-radius: 50%;
|
||||
background: var(--leaf);
|
||||
}
|
||||
|
||||
.course-stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.55rem;
|
||||
margin-top: 0.9rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.course-stats span {
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(45, 106, 79, 0.08);
|
||||
}
|
||||
|
||||
.course-foot {
|
||||
margin-top: 1.1rem;
|
||||
padding-top: 0.9rem;
|
||||
border-top: 1px solid rgba(20, 53, 43, 0.08);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.course-foot span {
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.price-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.price-old {
|
||||
font-size: 0.82rem;
|
||||
color: var(--ink-soft);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.classes-section {
|
||||
background: var(--chalk);
|
||||
}
|
||||
|
||||
.course-cta {
|
||||
display: inline-flex;
|
||||
margin-top: 0.85rem;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
color: var(--leaf);
|
||||
}
|
||||
|
||||
.course-cta:hover {
|
||||
color: var(--saffron-deep);
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.35rem;
|
||||
}
|
||||
|
||||
.path-section {
|
||||
|
||||
Reference in New Issue
Block a user