feat: show public classes with discount, start countdown, and free spots
Add a landing page classes section fed by the public classes API with pricing and availability details.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import axiosInstance from './axiosInstance'
|
||||
|
||||
export const classesApi = {
|
||||
getAll: (params = {}) => axiosInstance.get('/classes/user/get-all', { params }),
|
||||
getMyClasses: (params = {}) => axiosInstance.get('/classes/user/my-classes', { params })
|
||||
}
|
||||
|
||||
@@ -17,6 +17,14 @@ export function formatPrice(amount) {
|
||||
return `${toPersianDigits(Number(amount).toLocaleString('en-US'))} تومان`
|
||||
}
|
||||
|
||||
export function formatDaysUntilStart(days) {
|
||||
if (days == null || Number.isNaN(Number(days))) return null
|
||||
const value = Number(days)
|
||||
if (value > 0) return `${toPersianDigits(value)} روز تا شروع`
|
||||
if (value === 0) return 'امروز شروع میشود'
|
||||
return `${toPersianDigits(Math.abs(value))} روز از شروع گذشته`
|
||||
}
|
||||
|
||||
export function getApiErrorMessage(error, fallback = 'خطایی رخ داد. دوباره تلاش کنید.') {
|
||||
return (
|
||||
error?.response?.data?.error?.message ||
|
||||
|
||||
@@ -87,6 +87,51 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="classes" class="section classes-section">
|
||||
<div class="container">
|
||||
<header class="section-head">
|
||||
<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>
|
||||
<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 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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="path" class="section path-section">
|
||||
<div class="container path-grid">
|
||||
<div class="path-copy">
|
||||
@@ -132,21 +177,30 @@ import { onMounted, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import ContactInquiryForm from '@/components/ContactInquiryForm.vue'
|
||||
import { coursesApi } from '@/api/coursesApi'
|
||||
import { classesApi } from '@/api/classesApi'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { formatPrice, toPersianDigits } from '@/utils/format'
|
||||
import { formatPrice, formatJalali, formatDaysUntilStart, toPersianDigits } from '@/utils/format'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const courses = ref([])
|
||||
const classes = ref([])
|
||||
const coursesLoading = ref(true)
|
||||
const classesLoading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await coursesApi.getAll({ limit: 50, sort: '-createdAt' })
|
||||
courses.value = res.data || []
|
||||
const [coursesRes, classesRes] = await Promise.all([
|
||||
coursesApi.getAll({ limit: 50, sort: '-createdAt' }),
|
||||
classesApi.getAll({ limit: 50 })
|
||||
])
|
||||
courses.value = coursesRes.data || []
|
||||
classes.value = classesRes.data || []
|
||||
} catch {
|
||||
courses.value = []
|
||||
classes.value = []
|
||||
} finally {
|
||||
coursesLoading.value = false
|
||||
classesLoading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -396,6 +450,22 @@ onMounted(async () => {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user