Files
banu-front/src/features/auth/pages/StudentRegisterPage.vue
T
Server Migration b64f528c0d
Deploy banu-front / deploy (push) Successful in 1m40s
fix: complete registration frontend flow
2026-08-15 17:28:40 +03:30

313 lines
11 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="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 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 { 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;
}
&__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>