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:
2026-08-16 08:11:42 +03:30
parent d979205efb
commit f1a7ad8bf8
3 changed files with 82 additions and 3 deletions
+1
View File
@@ -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 })
}
+8
View File
@@ -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 ||
+73 -3
View File
@@ -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;