Files
banu-front/src/features/auth/pages/StudentRegisterPage.vue
T
madresebanoo 4402fb6a3d fix: branding, 401 logout, and blocked-user message on register page
- Copyright.vue / AuthCopyright.vue: change link from mugweb.ir to madresebanoo.ir
- SelectField.vue: add min-width:0 to prevent flex overflow in narrow layouts
- useRedirectAfterLogin: check roles array (multi-role support) instead of single role
- StudentRegisterPage: show block reason when user's status is blocked
- http.js: only clear session on 401 if a token already existed (login
  form 401 should not trigger logout); clear via authStore so WordPress
  cookie is also cleaned up

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-02 15:54:55 +04:30

352 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="student-register">
<div class="student-register__container">
<aside class="student-register__sidebar">
<SideBarSteps :tabs="tabs" :active-step="store.currentStep" @select-step="goToStep" />
</aside>
<main class="student-register__main">
<header class="student-register__heading">
<div class="student-register__step-meta">
<span>مرحله {{ currentStepNumber }} از {{ totalSteps }}</span>
<span>{{ progressPercent }}٪</span>
</div>
<div class="student-register__progress" aria-hidden="true">
<span :style="{ width: `${progressPercent}%` }" />
</div>
<h1 class="student-register__title">{{ stepLabel.title }}</h1>
<p class="student-register__desc">{{ stepLabel.desc }}</p>
</header>
<div
v-if="auth.user?.status === 'blocked'"
class="student-register__rejection"
role="alert"
>
<p class="student-register__rejection-title">احراز هویت شما توسط ادمین رد شده است.</p>
<p class="student-register__rejection-reason">
{{ auth.user?.blockReason || 'دلیلی ثبت نشده است.' }}
</p>
</div>
<div v-if="isRegistrationDataPending" class="student-register__loading" aria-live="polite">
در حال بازیابی اطلاعات ثبت‌نام…
</div>
<component :is="currentComponent" v-else />
</main>
</div>
<SuccessModal v-if="isModal('StudentRegisterSuccessModal')" />
</div>
</template>
<script setup>
import { useAuthStore } from '@/store/auth'
import useModal from '@/composables/useModal'
import { computed, markRaw, watch } from 'vue'
import { STUDENT_REGISTRATION } from '@/enums'
import SideBarSteps from '@/features/auth/components/studentRegister/SideBarSteps.vue'
import SuccessModal from '@/features/auth/components/studentRegister/SuccessModal.vue'
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
import { useGetRegisterDataQuery, useGetRegistrationVideoQuery } from '@/services/query/auth'
import PersonalInformation from '@/features/auth/components/studentRegister/PersonalInformation.vue'
import SkillAssessmentPart1 from '@/features/auth/components/studentRegister/SkillAssessmentPart1.vue'
import SkillAssessmentPart2 from '@/features/auth/components/studentRegister/SkillAssessmentPart2.vue'
import SkillAssessmentPart3 from '@/features/auth/components/studentRegister/SkillAssessmentPart3.vue'
import SkillAssessmentPart4 from '@/features/auth/components/studentRegister/SkillAssessmentPart4.vue'
import SkillAssessmentPart5 from '@/features/auth/components/studentRegister/SkillAssessmentPart5.vue'
import SkillAssessmentPart6 from '@/features/auth/components/studentRegister/SkillAssessmentPart6.vue'
import SkillAssessmentPart7 from '@/features/auth/components/studentRegister/SkillAssessmentPart7.vue'
import EducationalInformation from '@/features/auth/components/studentRegister/EducationalInformation.vue'
import ProfessionalInformation from '@/features/auth/components/studentRegister/ProfessionalInformation.vue'
const store = useStudentRegistrationStore()
const auth = useAuthStore()
const { isModal, openModal } = useModal()
const { data: registerData, isPending: isRegistrationDataPending } = useGetRegisterDataQuery()
watch(
registerData,
(value) => {
if (value && Object.keys(value).length > 0) store.hydrateFromRegisterData(value)
},
{ immediate: true, flush: 'sync' }
)
const { data: registrationMedia } = useGetRegistrationVideoQuery({ retry: false })
watch(registrationMedia, (value) => store.setRegistrationMedia(value), {
immediate: true,
flush: 'sync',
})
const STEP_TO_COMPONENT = {
[STUDENT_REGISTRATION.PERSONAL_INFORMATION]: markRaw(PersonalInformation),
[STUDENT_REGISTRATION.EDUCATIONAL_INFORMATION]: markRaw(EducationalInformation),
[STUDENT_REGISTRATION.PROFESSIONAL_INFORMATION]: markRaw(ProfessionalInformation),
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_1]: markRaw(SkillAssessmentPart1),
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_2]: markRaw(SkillAssessmentPart2),
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_3]: markRaw(SkillAssessmentPart3),
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_4]: markRaw(SkillAssessmentPart4),
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_5]: markRaw(SkillAssessmentPart5),
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_6]: markRaw(SkillAssessmentPart6),
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_7]: markRaw(SkillAssessmentPart7),
}
const STEP_LABELS = {
[STUDENT_REGISTRATION.PERSONAL_INFORMATION]: {
title: 'اطلاعـــــات شخصـــــی',
desc: 'لطفاً اطلاعات شخصی خود را به‌صورت کامل و دقیق وارد کنید',
},
[STUDENT_REGISTRATION.EDUCATIONAL_INFORMATION]: {
title: 'اطلاعـــــات تحصیلــــی و شغلی',
desc: 'لطفاً اطلاعات تحصیلی و شغلی خود را به‌صورت کامل و دقیق وارد کنید',
},
[STUDENT_REGISTRATION.PROFESSIONAL_INFORMATION]: {
title: 'اطلاعات فعالیت‌های تبلیغی',
desc: 'لطفاً اطلاعات فعالیت‌های تبلیغی خود را به‌صورت کامل و دقیق وارد کنید',
},
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_1]: {
title: 'بخش مهارت (بخش اول)',
desc: 'مهارت‌ها و قابلیت‌های خود را معرفی کنید.',
},
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_2]: {
title: 'بخش مهارت (بخش دوم)',
desc: 'مهارت‌ها و قابلیت‌های خود را معرفی کنید.',
},
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_3]: {
title: 'بخش مهارت (بخش سوم)',
desc: 'مهارت‌ها و قابلیت‌های خود را معرفی کنید.',
},
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_4]: {
title: 'بخش مهارت (بخش چهارم)',
desc: 'مهارت‌ها و قابلیت‌های خود را معرفی کنید.',
},
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_5]: {
title: 'بخش مهارت (بخش پنجم)',
desc: 'مهارت‌ها و قابلیت‌های خود را معرفی کنید.',
},
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_6]: {
title: 'بخش مهارت (بخش ششم)',
desc: 'مهارت‌ها و قابلیت‌های خود را معرفی کنید.',
},
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_7]: {
title: 'بخش مهارت (بخش هفتم)',
desc: 'مهارت‌ها و قابلیت‌های خود را معرفی کنید.',
},
}
const STEP_NAVIGATION = [
{ step: STUDENT_REGISTRATION.PERSONAL_INFORMATION, title: 'مشخصات فردی', icon: 'user' },
{ step: STUDENT_REGISTRATION.EDUCATIONAL_INFORMATION, title: 'اطلاعات تحصیلی', icon: 'book' },
{ step: STUDENT_REGISTRATION.PROFESSIONAL_INFORMATION, title: 'فعالیت‌های تبلیغی', icon: 'file' },
{ step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_1, title: 'مهارت ۱', icon: 'check-square' },
{ step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_2, title: 'مهارت ۲', icon: 'check-square' },
{ step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_3, title: 'مهارت ۳', icon: 'check-square' },
{ step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_4, title: 'مهارت ۴', icon: 'check-square' },
{ step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_5, title: 'مهارت ۵', icon: 'check-square' },
{ step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_6, title: 'مهارت ۶', icon: 'check-square' },
{ step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_7, title: 'مهارت ۷', icon: 'check-square' },
]
const tabs = computed(() =>
STEP_NAVIGATION.map((tab) => ({
...tab,
completed: store.completedSteps.includes(tab.step),
disabled: !store.canGoToStep(tab.step),
}))
)
const totalSteps = STEP_NAVIGATION.length
const currentStepNumber = computed(() => {
const index = STEP_NAVIGATION.findIndex((item) => item.step === store.currentStep)
return index >= 0 ? index + 1 : 1
})
const progressPercent = computed(() => Math.round((currentStepNumber.value / totalSteps) * 100))
const currentComponent = computed(
() =>
STEP_TO_COMPONENT[store.currentStep] ||
STEP_TO_COMPONENT[STUDENT_REGISTRATION.PERSONAL_INFORMATION]
)
const stepLabel = computed(
() => STEP_LABELS[store.currentStep] || STEP_LABELS[STUDENT_REGISTRATION.PERSONAL_INFORMATION]
)
const goToStep = (step) => {
store.updateCurrentStep(step)
}
watch(
() => store.isProfileCompleted,
(val) => {
if (val) openModal('StudentRegisterSuccessModal')
},
{ immediate: true }
)
</script>
<style lang="scss" scoped>
.student-register {
min-height: 100dvh;
width: 100%;
&__container {
height: 100dvh;
max-width: 95%;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem;
@media (min-width: 1024px) {
flex-direction: row;
align-items: stretch;
padding: 1rem;
gap: 0.75rem;
}
}
&__sidebar {
width: 100%;
@media (min-width: 1024px) {
width: 13.5rem;
min-width: 13.5rem;
}
}
&__main {
flex: 1;
min-width: 0;
overflow-y: auto;
padding: 0.5rem;
@media (min-width: 1024px) {
padding: 1rem 1.5rem;
}
}
&__heading {
margin-bottom: 1.25rem;
}
&__step-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.375rem;
color: #6f6f6f;
font-family: var(--font-family-number-fa);
font-size: 0.875rem;
font-weight: 500;
}
&__progress {
width: 100%;
height: 0.35rem;
margin-bottom: 1rem;
overflow: hidden;
border-radius: 999px;
background: #f2f2f2;
span {
display: block;
height: 100%;
border-radius: inherit;
background: var(--color-primary);
transition: width 0.2s ease;
}
}
&__title {
font-family: var(--font-family-fa);
font-weight: 600;
font-size: 1.375rem;
color: #3f3f3f;
margin: 0 0 0.375rem;
}
&__desc {
font-family: var(--font-family-fa);
font-weight: 400;
font-size: 0.9375rem;
line-height: 1.8;
color: #747474;
margin: 0;
}
&__loading {
padding: 3rem 1rem;
color: #747474;
font-family: var(--font-family-fa);
font-size: 1rem;
font-weight: 500;
text-align: center;
}
&__rejection {
background: #fdecec;
border: 1px solid #f5b8b8;
border-radius: 0.75rem;
padding: 1rem 1.25rem;
margin-bottom: 1.25rem;
}
&__rejection-title {
font-family: var(--font-family-fa);
font-weight: 600;
font-size: 0.9375rem;
color: #c62828;
margin: 0 0 0.375rem;
}
&__rejection-reason {
font-family: var(--font-family-fa);
font-weight: 400;
font-size: 0.875rem;
line-height: 1.8;
color: #7a2f2f;
margin: 0;
text-align: justify;
}
&__main :deep(.text-field__label),
&__main :deep(.textarea-field__label),
&__main :deep(.select-field__label),
&__main :deep(.datepicker-field__label) {
color: #626262;
font-size: 0.9375rem;
font-weight: 400;
}
&__main :deep(.text-field__input),
&__main :deep(.textarea-field__input),
&__main :deep(.select-field__trigger),
&__main :deep(.datepicker-field__trigger) {
min-height: 2.6rem;
font-size: 1rem;
font-weight: 400;
}
&__main :deep(.line-title__fa) {
font-size: 1rem;
font-weight: 600;
}
&__main :deep(.line-title__en) {
font-size: 0.8125rem;
font-weight: 400;
}
&__main :deep(.text-field__error),
&__main :deep(.textarea-field__error),
&__main :deep(.select-field__error),
&__main :deep(.datepicker-field__error) {
font-size: 0.8125rem;
font-weight: 400;
}
}
</style>