Split the landing page into upcoming and active class sections with colored ClassCard components, and refresh navigation and account class cards to match.
291 lines
7.2 KiB
Vue
291 lines
7.2 KiB
Vue
<template>
|
||
<div class="classes-view">
|
||
<header class="page-head">
|
||
<h1>کلاسهای من</h1>
|
||
<p>کلاسهایی که در آنها ثبتنام شدهاید.</p>
|
||
</header>
|
||
|
||
<div v-if="loading" class="loading-block">
|
||
<div class="spinner" aria-label="در حال بارگذاری"></div>
|
||
</div>
|
||
|
||
<div v-else-if="error" class="empty-state">
|
||
<h3>بارگذاری ناموفق بود</h3>
|
||
<p>{{ error }}</p>
|
||
<button type="button" class="btn btn-ink" @click="load">تلاش دوباره</button>
|
||
</div>
|
||
|
||
<div v-else-if="!classes.length" class="empty-state">
|
||
<h3>هنوز کلاسی ندارید</h3>
|
||
<p>پس از ثبتنام در کلاس توسط آموزشگاه، اینجا نمایش داده میشود.</p>
|
||
<RouterLink to="/#upcoming-classes" class="btn btn-outline">مشاهده کلاسهای پیشرو</RouterLink>
|
||
</div>
|
||
|
||
<div v-else class="class-list">
|
||
<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>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { onMounted, ref } from 'vue'
|
||
import { RouterLink } from 'vue-router'
|
||
import { classesApi } from '@/api/classesApi'
|
||
import { formatJalali, formatPrice, getApiErrorMessage } from '@/utils/format'
|
||
|
||
const WEEKDAY_LABELS = {
|
||
0: 'یکشنبه',
|
||
1: 'دوشنبه',
|
||
2: 'سهشنبه',
|
||
3: 'چهارشنبه',
|
||
4: 'پنجشنبه',
|
||
5: 'جمعه',
|
||
6: 'شنبه'
|
||
}
|
||
const IRAN_WEEK_ORDER = [6, 0, 1, 2, 3, 4, 5]
|
||
|
||
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}`
|
||
}
|
||
|
||
const classes = ref([])
|
||
const loading = ref(true)
|
||
const error = ref('')
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
error.value = ''
|
||
try {
|
||
const res = await classesApi.getMyClasses({ limit: 50 })
|
||
classes.value = res.data || []
|
||
} catch (err) {
|
||
error.value = getApiErrorMessage(err)
|
||
classes.value = []
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(load)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-head {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.page-head h1 {
|
||
font-family: var(--font-display);
|
||
font-size: clamp(1.9rem, 3.5vw, 2.4rem);
|
||
margin-bottom: 0.3rem;
|
||
}
|
||
|
||
.page-head p {
|
||
color: var(--ink-soft);
|
||
}
|
||
|
||
.class-list {
|
||
display: grid;
|
||
gap: 1.15rem;
|
||
}
|
||
|
||
.class-card {
|
||
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 {
|
||
display: flex;
|
||
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;
|
||
}
|
||
|
||
.class-top h2 {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.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.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.72rem;
|
||
color: var(--ink-soft);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.meta-grid dd {
|
||
margin: 0.2rem 0 0;
|
||
font-weight: 600;
|
||
font-size: 0.92rem;
|
||
}
|
||
|
||
@media (max-width: 760px) {
|
||
.meta-grid {
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
}
|
||
</style>
|