From e10cfba09d953ddd451cea70f7a0e1c42d7d0b15 Mon Sep 17 00:00:00 2001 From: madresebanoo Date: Wed, 2 Sep 2026 11:17:20 +0430 Subject: [PATCH] feat(batch-1): multi-role sidebar, video display, certificate layout, upload progress, counselor access - roleMenuItems.js: merged menu builder that unions items from all user roles (student + missionary, student + counselor, etc.) additively - auth store: expose `roles` array in addition to singular `role`; wire in WordPress auth-status cookie sync so the public site can show/hide the account button - router guards: use roles.includes() instead of role === for multi-role access - layouts (Student/Counselor/Missionary): delegate menuItems to buildMergedMenuItems so dual-role users see sections from both roles - StudentSessionDetailsPage: contentKind now returns 'video' (not 'link') when session.videoUrl is set; adds embed iframe branch for online sessions - CertificateItem.vue: fix CSS order/alignment for desktop + mobile layout - StudentCertificatesPage.vue: map backend payload fields to component props with Jalaali date formatting - SessionFormPage: upload progress bar (%) during video uploads; embed_code textarea for online sessions (frontend-only; dormant until batch-2 backend migration is deployed) - wp-sync/auth-status-cookie.js: cookie helper for WordPress login-state sync [batch-1-low-risk] --- .../admin/sessions/pages/SessionFormPage.vue | 74 ++++++++++- src/features/admin/sessions/schema/index.js | 1 + .../components/CertificateItem.vue | 18 ++- .../pages/StudentCertificatesPage.vue | 13 +- .../pages/StudentSessionDetailsPage.vue | 35 ++++- src/layouts/CounselorLayout.vue | 18 +-- src/layouts/MissionaryLayout.vue | 30 +---- src/layouts/StudentLayout.vue | 50 +------ src/layouts/roleMenuItems.js | 124 ++++++++++++++++++ src/router/guards.js | 4 +- src/services/wp-sync/auth-status-cookie.js | 61 +++++++++ src/store/auth.js | 26 +++- 12 files changed, 349 insertions(+), 105 deletions(-) create mode 100644 src/layouts/roleMenuItems.js create mode 100644 src/services/wp-sync/auth-status-cookie.js diff --git a/src/features/admin/sessions/pages/SessionFormPage.vue b/src/features/admin/sessions/pages/SessionFormPage.vue index d07764d..983279b 100644 --- a/src/features/admin/sessions/pages/SessionFormPage.vue +++ b/src/features/admin/sessions/pages/SessionFormPage.vue @@ -77,6 +77,19 @@ /> + +
+ +
+
+
+
+
+
+ + {{ contentUploadProgress }}% بارگذاری شده + +
@@ -200,6 +224,7 @@ const form = ref({ contentType: '', type: '', link: '', + embedCode: '', description: '', }) @@ -207,6 +232,7 @@ const image = ref(null) const contentFiles = ref([]) const coverMediaId = ref(null) const contentMediaId = ref(null) +const contentUploadProgress = ref(0) const contentAccept = computed(() => COURSE_CONTENT_TYPE_ACCEPT[form.value.contentType] || '*') const contentMaxSize = computed( @@ -238,7 +264,10 @@ const onContentTypeChange = () => { } const onSessionTypeChange = () => { - if (form.value.type !== 'online') form.value.link = '' + if (form.value.type !== 'online') { + form.value.link = '' + form.value.embedCode = '' + } } const { data: existingSession } = useAdminSessionQuery(sessionId, { @@ -267,6 +296,7 @@ watch( contentType, type: session.type ?? '', link: session.link ?? '', + embedCode: session.embedCode ?? '', description: session.description ?? '', } coverMediaId.value = coverMedia?.id ?? null @@ -298,13 +328,20 @@ const purposeForContentType = (contentType) => { const onContentSelect = async (files) => { const file = files?.[0] if (!file) return + contentUploadProgress.value = 0 try { const formData = objectToFormData({ file, purpose: purposeForContentType(form.value.contentType), context: 'session', }) - const response = await contentUploadMutation.mutateAsync(formData) + const response = await contentUploadMutation.mutateAsync({ + formData, + onUploadProgress: (event) => { + if (!event.total) return + contentUploadProgress.value = Math.round((event.loaded / event.total) * 100) + }, + }) const payload = response?.data ?? response const id = payload?.id ?? payload?.uploadId contentFiles.value = [{ id, name: file.name, size: file.size, url: payload?.url }] @@ -342,6 +379,7 @@ const buildPayload = (values) => { courseId: values.courseId, type: values.type, link: values.type === 'online' ? values.link : undefined, + embedCode: values.type === 'online' ? values.embedCode : undefined, description: values.description, mediaIds, } @@ -477,6 +515,38 @@ const onCancel = () => router.push({ name: 'admin-sessions' }) color: var(--color-prim-gray); } + &__upload-progress { + display: flex; + align-items: center; + gap: 0.75rem; + margin-top: -0.25rem; + margin-bottom: 0.75rem; + } + + &__upload-progress-track { + flex: 1; + height: 0.5rem; + border-radius: 999px; + background: var(--color-thd-gray); + overflow: hidden; + } + + &__upload-progress-bar { + height: 100%; + border-radius: 999px; + background: var(--color-primary); + transition: width 0.2s ease; + } + + &__upload-progress-text { + flex-shrink: 0; + font-family: var(--font-family-en); + font-size: 0.75rem; + color: var(--color-sec-gray); + min-width: 3.5rem; + text-align: left; + } + &__preview { place-items: center center; } diff --git a/src/features/admin/sessions/schema/index.js b/src/features/admin/sessions/schema/index.js index ecdc4f3..cf5682d 100644 --- a/src/features/admin/sessions/schema/index.js +++ b/src/features/admin/sessions/schema/index.js @@ -9,5 +9,6 @@ export const sessionSchema = object().shape({ then: (schema) => schema.required(), otherwise: (schema) => schema.nullable().notRequired(), }), + embedCode: string().nullable().notRequired(), description: string().nullable().notRequired(), }) diff --git a/src/features/student/certificates/components/CertificateItem.vue b/src/features/student/certificates/components/CertificateItem.vue index e65c76a..d8574cc 100644 --- a/src/features/student/certificates/components/CertificateItem.vue +++ b/src/features/student/certificates/components/CertificateItem.vue @@ -120,7 +120,7 @@ const downloadItems = computed(() => [ @media (min-width: 1024px) { flex: 1; - order: 3; + order: 1; min-width: 12rem; } } @@ -146,8 +146,8 @@ const downloadItems = computed(() => [ &__course-info { display: flex; flex-direction: column; - align-items: flex-start; - text-align: end; + align-items: flex-end; + text-align: right; gap: 0.25rem; min-width: 0; } @@ -186,6 +186,7 @@ const downloadItems = computed(() => [ background: rgba(107, 107, 107, 4%); font-family: var(--font-family-fa); white-space: nowrap; + align-self: center; @media (min-width: 1024px) { order: 2; @@ -222,15 +223,20 @@ const downloadItems = computed(() => [ justify-content: center; @media (min-width: 1024px) { - order: 1; - justify-content: flex-start; + order: 3; + justify-content: flex-end; } } &__download-btn { - min-width: 9rem; + width: 100%; height: 2rem; padding: 0 1.25rem; + + @media (min-width: 1024px) { + width: auto; + min-width: 9rem; + } } } diff --git a/src/features/student/certificates/pages/StudentCertificatesPage.vue b/src/features/student/certificates/pages/StudentCertificatesPage.vue index 3356bec..2f86b7e 100644 --- a/src/features/student/certificates/pages/StudentCertificatesPage.vue +++ b/src/features/student/certificates/pages/StudentCertificatesPage.vue @@ -35,6 +35,7 @@ import { computed, ref } from 'vue' import { toast } from 'vue3-toastify' import SvgIcon from '@/components/icons/SvgIcon.vue' import NoItems from '@/components/blocks/NoItems.vue' +import { formatJalaaliDate } from '@/utils/date-utils' import { usePagination } from '@/composables/usePagination' import PaginationBlock from '@/components/blocks/PaginationBlock.vue' import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue' @@ -53,7 +54,17 @@ const { data, isLoading } = useStudentCertificatesQuery(filters, pagination, { keepPreviousData: true, }) -const certificates = computed(() => data.value?.data ?? []) +// Bridge the backend payload (title/teacherName/startedAt/endedAt) into the +// display shape CertificateItem expects, without touching its template. +const certificates = computed(() => + (data.value?.data ?? []).map((c) => ({ + ...c, + courseTitle: c.title ?? '', + teacherName: c.teacherName ?? '', + startDate: formatJalaaliDate(c.startedAt) || '', + endDate: formatJalaaliDate(c.endedAt) || '', + })) +) const meta = computed(() => ({ page: pagination.value.page, perPage: pagination.value.perPage, diff --git a/src/features/student/sessions/pages/StudentSessionDetailsPage.vue b/src/features/student/sessions/pages/StudentSessionDetailsPage.vue index 77fa8fd..bd22943 100644 --- a/src/features/student/sessions/pages/StudentSessionDetailsPage.vue +++ b/src/features/student/sessions/pages/StudentSessionDetailsPage.vue @@ -37,12 +37,23 @@

{{ session.text }}

+
+