a8b24ca46e
The admin course form sent a stray upload id (imageId/uploadId) that didn't match any field the API accepts; the API expects cover_media_id with the media id returned from the upload endpoint. Also switch course displays to the coverUrl field the API actually returns.
218 lines
6.2 KiB
Vue
218 lines
6.2 KiB
Vue
<template>
|
|
<BasicModal
|
|
title="جزئیات دوره"
|
|
title-en="details"
|
|
width="95%"
|
|
max-width="80rem"
|
|
min-width="auto"
|
|
:show-close-button="true"
|
|
>
|
|
<template #default="{ close }">
|
|
<div class="course-details">
|
|
<SkeletonLoaderBlock v-if="isLoading" :rows="3" :cols-per-row="1" />
|
|
|
|
<template v-else-if="course">
|
|
<div class="course-details__hero">
|
|
<div class="course-details__image">
|
|
<img v-if="course.coverUrl" :src="course.coverUrl" :alt="course.title" />
|
|
<SvgIcon v-else name="book" :size="32" color="#bcbcbc" />
|
|
</div>
|
|
<div class="course-details__grid">
|
|
<LineInfoBlock title="عنوان دوره" :desc="course.title || '—'" />
|
|
<LineInfoBlock title="تعداد جلسات" :numeric-desc="course.sessionsCount ?? 0" />
|
|
<LineInfoBlock title="پیشنیاز دوره" :desc="prerequisiteCourses" />
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="course.description" class="course-details__description">
|
|
<LineInfoBlock title="توضیحات دوره" :desc="course.description" />
|
|
</div>
|
|
|
|
<section class="course-details__sessions">
|
|
<div class="course-details__panel-header">
|
|
<LineTitleBlock title="لیست جلسات" title-en="Sessions" />
|
|
<BaseButton
|
|
text="افزودن جلسه به این دوره"
|
|
custom-class="course-details__panel-btn"
|
|
@click="onOpenAddSession"
|
|
>
|
|
<template #appendIcon>
|
|
<SvgIcon name="arrow-left" :size="16" color="#fff" />
|
|
</template>
|
|
</BaseButton>
|
|
</div>
|
|
|
|
<SkeletonLoaderBlock v-if="sessionsPending" :rows="4" :cols-per-row="1" />
|
|
<div v-else-if="sessions.length > 0">
|
|
<CourseSessionItem v-for="session in sessions" :key="session.id" :session="session" />
|
|
</div>
|
|
<NoItems v-else title="بدون جلسه" desc="جلسهای برای این دوره ثبت نشده است." />
|
|
|
|
<PaginationBlock
|
|
v-if="sessionsPaginationMeta.lastPage > 1"
|
|
:pagination="sessionsPaginationMeta"
|
|
@update:page="setSessionsPage"
|
|
/>
|
|
</section>
|
|
</template>
|
|
|
|
<NoItems v-else title="یافت نشد" desc="اطلاعات این دوره در دسترس نیست." />
|
|
|
|
<div class="course-details__divider" />
|
|
|
|
<div class="course-details__footer">
|
|
<BaseButton text="تایید" custom-class="course-details__close-btn" @click="close">
|
|
<template #appendIcon>
|
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
|
</template>
|
|
</BaseButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</BasicModal>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
import useModal from '@/composables/useModal'
|
|
import BasicModal from '@/components/BasicModal.vue'
|
|
import BaseButton from '@/components/BaseButton.vue'
|
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|
import NoItems from '@/components/blocks/NoItems.vue'
|
|
import { usePagination } from '@/composables/usePagination'
|
|
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
|
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
|
import { useAdminCourseQuery } from '@/services/query/admin-courses'
|
|
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
|
|
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
|
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
|
import CourseSessionItem from '@/features/admin/courses/components/CourseSessionItem.vue'
|
|
|
|
defineOptions({ name: 'CourseDetailsModal' })
|
|
|
|
const { openModal, getModal } = useModal()
|
|
|
|
const modalData = computed(() => getModal('CourseDetailsModal')?.data ?? {})
|
|
const courseId = computed(() => modalData.value.id ?? null)
|
|
|
|
const { data: course, isLoading } = useAdminCourseQuery(courseId, {
|
|
enabled: () => !!courseId.value,
|
|
})
|
|
|
|
const prerequisiteCourses = computed(() =>
|
|
course.value.prerequisiteCourses.map((course_) => course_.title).join(' ,')
|
|
)
|
|
|
|
const sessionsFilters = computed(() => ({ courseId: courseId.value }))
|
|
const { pagination: sessionsPagination, setPage: setSessionsPage } = usePagination({
|
|
page: 1,
|
|
perPage: 10,
|
|
})
|
|
|
|
const { data: sessionsData, isLoading: sessionsPending } = useAdminSessionsListQuery(
|
|
sessionsFilters,
|
|
sessionsPagination,
|
|
{
|
|
enabled: () => !!courseId.value,
|
|
keepPreviousData: true,
|
|
}
|
|
)
|
|
|
|
const sessions = computed(() => sessionsData.value?.data ?? [])
|
|
const sessionsPaginationMeta = computed(() => ({
|
|
page: sessionsPagination.value.page,
|
|
perPage: sessionsPagination.value.perPage,
|
|
...sessionsData.value?.meta,
|
|
}))
|
|
|
|
const onOpenAddSession = () => {
|
|
openModal('AddSessionToCourseModal', { courseId: courseId.value })
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.course-details {
|
|
width: 100%;
|
|
text-align: start;
|
|
|
|
&__hero {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 1rem;
|
|
margin: 1rem 0 0.5rem;
|
|
|
|
@media (max-width: 768px) {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
|
|
&__image {
|
|
width: 5rem;
|
|
height: 5rem;
|
|
border-radius: 0.5rem;
|
|
background: #f5f5f5;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
flex-shrink: 0;
|
|
|
|
img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
}
|
|
|
|
&__grid {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 0.25rem;
|
|
|
|
@media (min-width: 768px) {
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
|
|
@media (min-width: 1280px) {
|
|
grid-template-columns: repeat(5, 1fr);
|
|
}
|
|
}
|
|
|
|
&__description {
|
|
margin-block: 0.5rem 1rem;
|
|
}
|
|
|
|
&__sessions {
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
&__panel-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.625rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
&__panel-btn {
|
|
min-width: 14rem;
|
|
padding: 0 1rem;
|
|
margin-inline-start: auto;
|
|
}
|
|
|
|
&__divider {
|
|
border-block-end: 1px solid var(--color-thd-gray);
|
|
margin-block: 1.5rem;
|
|
}
|
|
|
|
&__footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
&__close-btn {
|
|
min-width: 12rem;
|
|
}
|
|
}
|
|
</style>
|