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:
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<article
|
||||
class="class-card"
|
||||
:class="[`class-card--${variant}`, `class-card--tone-${tone}`]"
|
||||
>
|
||||
<div class="class-card-accent" aria-hidden="true"></div>
|
||||
|
||||
<div class="class-card-inner">
|
||||
<header class="class-card-head">
|
||||
<span class="class-card-course">{{ cls.course?.title || 'کلاس' }}</span>
|
||||
<span class="class-card-badge" :class="badgeClass">{{ badgeLabel }}</span>
|
||||
</header>
|
||||
|
||||
<h3 class="class-card-title">{{ cls.name }}</h3>
|
||||
|
||||
<p v-if="cls.professor" class="class-card-professor">
|
||||
<span class="class-card-icon" aria-hidden="true">◆</span>
|
||||
{{ cls.professor.name }} {{ cls.professor.surname || '' }}
|
||||
</p>
|
||||
|
||||
<ul class="class-card-stats">
|
||||
<li v-if="daysLabel">
|
||||
<span class="stat-icon" aria-hidden="true">⏱</span>
|
||||
{{ daysLabel }}
|
||||
</li>
|
||||
<li v-if="cls.startDate">
|
||||
<span class="stat-icon" aria-hidden="true">📅</span>
|
||||
شروع {{ formatJalali(cls.startDate) }}
|
||||
</li>
|
||||
<li v-if="scheduleLabel">
|
||||
<span class="stat-icon" aria-hidden="true">🗓</span>
|
||||
{{ scheduleLabel }}
|
||||
</li>
|
||||
<li v-if="cls.freeSpots != null">
|
||||
<span class="stat-icon" aria-hidden="true">👥</span>
|
||||
{{ toPersianDigits(cls.freeSpots) }} ظرفیت آزاد
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<footer class="class-card-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>
|
||||
<span v-if="cls.hasDiscount && cls.discount" class="discount-tag">
|
||||
تخفیف {{ formatPrice(cls.discount) }}
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
<a href="#contact" class="class-card-cta">{{ ctaLabel }}</a>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
formatJalali,
|
||||
formatPrice,
|
||||
formatDaysUntilStart,
|
||||
toPersianDigits
|
||||
} from '@/utils/format'
|
||||
|
||||
const WEEKDAY_LABELS = {
|
||||
0: 'یکشنبه',
|
||||
1: 'دوشنبه',
|
||||
2: 'سهشنبه',
|
||||
3: 'چهارشنبه',
|
||||
4: 'پنجشنبه',
|
||||
5: 'جمعه',
|
||||
6: 'شنبه'
|
||||
}
|
||||
const IRAN_WEEK_ORDER = [6, 0, 1, 2, 3, 4, 5]
|
||||
|
||||
const props = defineProps({
|
||||
cls: { type: Object, required: true },
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'upcoming',
|
||||
validator: (v) => ['upcoming', 'active'].includes(v)
|
||||
},
|
||||
tone: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const daysLabel = computed(() => formatDaysUntilStart(props.cls.daysUntilStart))
|
||||
|
||||
const scheduleLabel = computed(() => {
|
||||
const days = formatClassDays(props.cls.days)
|
||||
const time = formatClassTime(props.cls.startTime, props.cls.endTime)
|
||||
if (days === '—' && time === '—') return null
|
||||
if (days !== '—' && time !== '—') return `${days} · ${time}`
|
||||
return days !== '—' ? days : time
|
||||
})
|
||||
|
||||
const badgeLabel = computed(() => (props.variant === 'upcoming' ? 'پیشرو' : 'در حال برگزاری'))
|
||||
|
||||
const badgeClass = computed(() =>
|
||||
props.variant === 'upcoming' ? 'badge-upcoming' : 'badge-active'
|
||||
)
|
||||
|
||||
const ctaLabel = computed(() =>
|
||||
props.variant === 'upcoming' ? 'رزرو جایگاه' : 'درخواست تشکیل کلاس جدید'
|
||||
)
|
||||
|
||||
function joinFaList(items) {
|
||||
if (!items.length) return '—'
|
||||
if (items.length === 1) return items[0]
|
||||
if (items.length === 2) return `${items[0]} و ${items[1]}`
|
||||
return `${items.slice(0, -1).join('، ')} و ${items[items.length - 1]}`
|
||||
}
|
||||
|
||||
function formatClassDays(days = []) {
|
||||
const unique = [...new Set((days || []).map(Number).filter((day) => day >= 0 && day <= 6))]
|
||||
unique.sort((a, b) => IRAN_WEEK_ORDER.indexOf(a) - IRAN_WEEK_ORDER.indexOf(b))
|
||||
return joinFaList(unique.map((day) => WEEKDAY_LABELS[day]).filter(Boolean))
|
||||
}
|
||||
|
||||
function formatClassTime(startTime, endTime) {
|
||||
const start = String(startTime || '').trim()
|
||||
const end = String(endTime || '').trim()
|
||||
if (!start && !end) return '—'
|
||||
if (!end) return start
|
||||
if (!start) return end
|
||||
return `${start} – ${end}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.class-card {
|
||||
position: relative;
|
||||
border-radius: 1.25rem;
|
||||
overflow: hidden;
|
||||
background: var(--chalk);
|
||||
border: 1px solid rgba(20, 53, 43, 0.07);
|
||||
box-shadow: 0 12px 32px rgba(20, 53, 43, 0.06);
|
||||
transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease);
|
||||
}
|
||||
|
||||
.class-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.class-card-accent {
|
||||
height: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.class-card--upcoming.class-card--tone-0 .class-card-accent {
|
||||
background: linear-gradient(90deg, var(--saffron), #f5c842);
|
||||
}
|
||||
.class-card--upcoming.class-card--tone-1 .class-card-accent {
|
||||
background: linear-gradient(90deg, #3d8b66, #52a87d);
|
||||
}
|
||||
.class-card--upcoming.class-card--tone-2 .class-card-accent {
|
||||
background: linear-gradient(90deg, #5b7fd4, #7a9ae8);
|
||||
}
|
||||
|
||||
.class-card--active.class-card--tone-0 .class-card-accent {
|
||||
background: linear-gradient(90deg, var(--leaf), var(--leaf-bright));
|
||||
}
|
||||
.class-card--active.class-card--tone-1 .class-card-accent {
|
||||
background: linear-gradient(90deg, #2a6b5a, #3d8b78);
|
||||
}
|
||||
.class-card--active.class-card--tone-2 .class-card-accent {
|
||||
background: linear-gradient(90deg, #4a7c59, #6b9f7a);
|
||||
}
|
||||
|
||||
.class-card--upcoming.class-card--tone-0 {
|
||||
background: linear-gradient(165deg, #fffdf6 0%, var(--chalk) 45%);
|
||||
}
|
||||
.class-card--upcoming.class-card--tone-1 {
|
||||
background: linear-gradient(165deg, #f3faf6 0%, var(--chalk) 45%);
|
||||
}
|
||||
.class-card--upcoming.class-card--tone-2 {
|
||||
background: linear-gradient(165deg, #f4f7fd 0%, var(--chalk) 45%);
|
||||
}
|
||||
|
||||
.class-card--active.class-card--tone-0 {
|
||||
background: linear-gradient(165deg, #eef7f1 0%, var(--chalk) 45%);
|
||||
}
|
||||
.class-card--active.class-card--tone-1 {
|
||||
background: linear-gradient(165deg, #eaf4ef 0%, var(--chalk) 45%);
|
||||
}
|
||||
.class-card--active.class-card--tone-2 {
|
||||
background: linear-gradient(165deg, #e8f2eb 0%, var(--chalk) 45%);
|
||||
}
|
||||
|
||||
.class-card-inner {
|
||||
padding: 1.25rem 1.3rem 1.35rem;
|
||||
}
|
||||
|
||||
.class-card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.65rem;
|
||||
}
|
||||
|
||||
.class-card-course {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: var(--ink-soft);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(20, 53, 43, 0.06);
|
||||
}
|
||||
|
||||
.class-card-badge {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
padding: 0.22rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-upcoming {
|
||||
background: rgba(232, 163, 23, 0.2);
|
||||
color: var(--saffron-deep);
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background: rgba(45, 106, 79, 0.16);
|
||||
color: var(--leaf);
|
||||
}
|
||||
|
||||
.class-card-title {
|
||||
font-size: 1.18rem;
|
||||
margin-bottom: 0.45rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.class-card-professor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.88rem;
|
||||
color: var(--ink-soft);
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
|
||||
.class-card-icon {
|
||||
color: var(--saffron);
|
||||
font-size: 0.55rem;
|
||||
}
|
||||
|
||||
.class-card--active .class-card-icon {
|
||||
color: var(--leaf);
|
||||
}
|
||||
|
||||
.class-card-stats {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.class-card-stats li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
font-size: 0.82rem;
|
||||
color: var(--ink-soft);
|
||||
padding: 0.35rem 0.65rem;
|
||||
border-radius: 0.65rem;
|
||||
background: rgba(20, 53, 43, 0.04);
|
||||
}
|
||||
|
||||
.class-card--upcoming .class-card-stats li:nth-child(1) {
|
||||
background: rgba(232, 163, 23, 0.12);
|
||||
color: var(--saffron-deep);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.class-card--active .class-card-stats li:nth-child(1) {
|
||||
background: rgba(45, 106, 79, 0.12);
|
||||
color: var(--leaf);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.class-card-foot {
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.95rem;
|
||||
border-top: 1px dashed rgba(20, 53, 43, 0.12);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.price-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.price-stack strong {
|
||||
font-size: 1.05rem;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.price-old {
|
||||
font-size: 0.8rem;
|
||||
color: var(--ink-soft);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.discount-tag {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
padding: 0.25rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(232, 163, 23, 0.18);
|
||||
color: var(--saffron-deep);
|
||||
}
|
||||
|
||||
.class-card-cta {
|
||||
display: inline-flex;
|
||||
margin-top: 0.9rem;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 800;
|
||||
padding: 0.55rem 1rem;
|
||||
border-radius: 999px;
|
||||
transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.class-card--upcoming .class-card-cta {
|
||||
background: rgba(232, 163, 23, 0.15);
|
||||
color: var(--saffron-deep);
|
||||
}
|
||||
|
||||
.class-card--upcoming .class-card-cta:hover {
|
||||
background: var(--saffron);
|
||||
color: var(--deep);
|
||||
}
|
||||
|
||||
.class-card--active .class-card-cta {
|
||||
background: rgba(45, 106, 79, 0.12);
|
||||
color: var(--leaf);
|
||||
}
|
||||
|
||||
.class-card--active .class-card-cta:hover {
|
||||
background: var(--leaf);
|
||||
color: var(--chalk);
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,8 @@
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<RouterLink to="/login">ورود</RouterLink>
|
||||
<a href="/#courses">دورهها</a>
|
||||
<a href="/#upcoming-classes">کلاسهای پیشرو</a>
|
||||
<a href="/#active-classes">کلاسهای فعال</a>
|
||||
<a href="/#contact">تماس با ما</a>
|
||||
<RouterLink to="/account">پنل کاربری</RouterLink>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
</RouterLink>
|
||||
|
||||
<nav class="header-nav" aria-label="منوی اصلی">
|
||||
<a href="/#courses">دورهها</a>
|
||||
<a href="/#upcoming-classes">کلاسهای پیشرو</a>
|
||||
<a href="/#active-classes">کلاسهای فعال</a>
|
||||
<a href="/#path">مسیر یادگیری</a>
|
||||
<a href="/#contact">تماس با ما</a>
|
||||
<RouterLink v-if="auth.isAuthenticated" :to="{ name: 'account' }">حساب من</RouterLink>
|
||||
|
||||
+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 {
|
||||
|
||||
@@ -18,50 +18,65 @@
|
||||
<div v-else-if="!classes.length" class="empty-state">
|
||||
<h3>هنوز کلاسی ندارید</h3>
|
||||
<p>پس از ثبتنام در کلاس توسط آموزشگاه، اینجا نمایش داده میشود.</p>
|
||||
<RouterLink to="/#courses" class="btn btn-outline">مشاهده دورهها</RouterLink>
|
||||
<RouterLink to="/#upcoming-classes" class="btn btn-outline">مشاهده کلاسهای پیشرو</RouterLink>
|
||||
</div>
|
||||
|
||||
<div v-else class="class-list">
|
||||
<article v-for="item in classes" :key="item._id" class="class-card">
|
||||
<div class="class-top">
|
||||
<h2>{{ item.name }}</h2>
|
||||
<span class="badge" :class="item.isActive ? '' : 'badge-muted'">
|
||||
{{ item.isActive ? 'فعال' : 'غیرفعال' }}
|
||||
</span>
|
||||
<article
|
||||
v-for="(item, index) in classes"
|
||||
:key="item._id"
|
||||
class="class-card"
|
||||
:class="[
|
||||
item.isActive ? 'class-card--active' : 'class-card--inactive',
|
||||
`class-card--tone-${index % 3}`
|
||||
]"
|
||||
>
|
||||
<div class="class-card-accent" aria-hidden="true"></div>
|
||||
|
||||
<div class="class-card-body">
|
||||
<div class="class-top">
|
||||
<div>
|
||||
<span class="course-tag">{{ item.course?.title || 'بدون دوره' }}</span>
|
||||
<h2>{{ item.name }}</h2>
|
||||
</div>
|
||||
<span class="status-badge" :class="item.isActive ? 'status-active' : 'status-muted'">
|
||||
{{ item.isActive ? 'فعال' : 'غیرفعال' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<dl class="meta-grid">
|
||||
<div class="meta-item">
|
||||
<dt>استاد</dt>
|
||||
<dd>
|
||||
{{
|
||||
item.professor
|
||||
? `${item.professor.name || ''} ${item.professor.surname || ''}`.trim()
|
||||
: '—'
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<dt>شروع</dt>
|
||||
<dd>{{ formatJalali(item.startDate) }}</dd>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<dt>پایان</dt>
|
||||
<dd>{{ formatJalali(item.endDate) }}</dd>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<dt>روزها</dt>
|
||||
<dd>{{ formatClassDays(item.days) }}</dd>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<dt>ساعت</dt>
|
||||
<dd>{{ formatClassTime(item.startTime, item.endTime) }}</dd>
|
||||
</div>
|
||||
<div class="meta-item meta-item--price">
|
||||
<dt>شهریه</dt>
|
||||
<dd>{{ formatPrice(item.finalTuitionFee ?? item.tuitionFee) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<p class="course-title">{{ item.course?.title || 'بدون دوره' }}</p>
|
||||
<dl class="meta-grid">
|
||||
<div>
|
||||
<dt>استاد</dt>
|
||||
<dd>
|
||||
{{
|
||||
item.professor
|
||||
? `${item.professor.name || ''} ${item.professor.surname || ''}`.trim()
|
||||
: '—'
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>شروع</dt>
|
||||
<dd>{{ formatJalali(item.startDate) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>پایان</dt>
|
||||
<dd>{{ formatJalali(item.endDate) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>روزها</dt>
|
||||
<dd>{{ formatClassDays(item.days) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>ساعت</dt>
|
||||
<dd>{{ formatClassTime(item.startTime, item.endTime) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>شهریه</dt>
|
||||
<dd>{{ formatPrice(item.tuitionFee) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,15 +159,53 @@ onMounted(load)
|
||||
|
||||
.class-list {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
gap: 1.15rem;
|
||||
}
|
||||
|
||||
.class-card {
|
||||
padding: 1.35rem 1.4rem;
|
||||
border-radius: 1.2rem;
|
||||
border-radius: 1.25rem;
|
||||
overflow: hidden;
|
||||
background: var(--chalk);
|
||||
border: 1px solid rgba(20, 53, 43, 0.08);
|
||||
box-shadow: 0 10px 28px rgba(20, 53, 43, 0.05);
|
||||
transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease);
|
||||
}
|
||||
|
||||
.class-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.class-card-accent {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.class-card--active.class-card--tone-0 .class-card-accent {
|
||||
background: linear-gradient(90deg, var(--leaf), var(--leaf-bright));
|
||||
}
|
||||
.class-card--active.class-card--tone-1 .class-card-accent {
|
||||
background: linear-gradient(90deg, #2a6b5a, #3d8b78);
|
||||
}
|
||||
.class-card--active.class-card--tone-2 .class-card-accent {
|
||||
background: linear-gradient(90deg, #4a7c59, #6b9f7a);
|
||||
}
|
||||
|
||||
.class-card--inactive .class-card-accent {
|
||||
background: linear-gradient(90deg, #9aa8a0, #b8cfc0);
|
||||
}
|
||||
|
||||
.class-card--active.class-card--tone-0 {
|
||||
background: linear-gradient(165deg, #eef7f1 0%, var(--chalk) 40%);
|
||||
}
|
||||
.class-card--active.class-card--tone-1 {
|
||||
background: linear-gradient(165deg, #eaf4ef 0%, var(--chalk) 40%);
|
||||
}
|
||||
.class-card--active.class-card--tone-2 {
|
||||
background: linear-gradient(165deg, #e8f2eb 0%, var(--chalk) 40%);
|
||||
}
|
||||
|
||||
.class-card-body {
|
||||
padding: 1.25rem 1.35rem 1.35rem;
|
||||
}
|
||||
|
||||
.class-top {
|
||||
@@ -160,6 +213,17 @@ onMounted(load)
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.course-tag {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--ink-soft);
|
||||
padding: 0.18rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(20, 53, 43, 0.06);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
@@ -167,28 +231,55 @@ onMounted(load)
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
.status-badge {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
padding: 0.25rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: rgba(45, 106, 79, 0.16);
|
||||
color: var(--leaf);
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.status-muted {
|
||||
background: rgba(20, 53, 43, 0.08);
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.85rem;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
padding: 0.65rem 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(20, 53, 43, 0.04);
|
||||
}
|
||||
|
||||
.meta-item--price {
|
||||
background: rgba(45, 106, 79, 0.1);
|
||||
}
|
||||
|
||||
.meta-item--price dd {
|
||||
color: var(--leaf);
|
||||
}
|
||||
|
||||
.meta-grid dt {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.72rem;
|
||||
color: var(--ink-soft);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.meta-grid dd {
|
||||
margin: 0.15rem 0 0;
|
||||
margin: 0.2rem 0 0;
|
||||
font-weight: 600;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
|
||||
Reference in New Issue
Block a user