Initialize the user app and load home banners from the API.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import BaseInput from '@/components/ui/BaseInput.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
|
||||
import { normalizePhone, toPersianDigits } from '@/utils/format'
|
||||
import { validatePhone } from '@/utils/validation'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
clearFieldError,
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const phone = ref('')
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
|
||||
const displayPhone = computed(() => (phone.value ? toPersianDigits(phone.value) : ''))
|
||||
|
||||
function phoneErrorMessage(value: string): string {
|
||||
if (!value) return 'شماره موبایل را وارد کنید.'
|
||||
if (!validatePhone(value)) return 'شماره موبایل معتبر نیست. (مثال: ۰۹۱۲۱۲۳۴۵۶۷)'
|
||||
return ''
|
||||
}
|
||||
|
||||
function clearPhoneError() {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'phone')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
}
|
||||
|
||||
function onPhoneInput(value: string) {
|
||||
phone.value = normalizePhone(value)
|
||||
clearPhoneError()
|
||||
}
|
||||
|
||||
function onPhoneBlur() {
|
||||
if (!phone.value) return
|
||||
const message = phoneErrorMessage(phone.value)
|
||||
if (message) {
|
||||
fieldErrors.value = { ...fieldErrors.value, phone: message }
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
|
||||
const normalized = normalizePhone(phone.value)
|
||||
phone.value = normalized
|
||||
|
||||
const message = phoneErrorMessage(normalized)
|
||||
if (message) {
|
||||
fieldErrors.value = { phone: message }
|
||||
formError.value = FORM_VALIDATION_SUMMARY
|
||||
return
|
||||
}
|
||||
|
||||
const result = await auth.requestOtp(normalized, { isRegister: false })
|
||||
if (result.success) {
|
||||
router.push({ name: 'otp', query: { phone: normalized, mode: 'login' } })
|
||||
} else {
|
||||
formError.value = result.message ?? 'خطا در ارسال کد تأیید.'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page page-shell auth-page">
|
||||
<AuthThemeToggle />
|
||||
|
||||
<div class="page__scroll flex flex-col justify-center py-6">
|
||||
<div class="auth-header">
|
||||
<div class="text-5xl mb-3">🧵</div>
|
||||
<h1 class="page-title">ورود به سامانه</h1>
|
||||
<p class="page-subtitle">سامانه سفارش عمده پارچه</p>
|
||||
</div>
|
||||
|
||||
<form class="card" @submit.prevent="submit">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<BaseInput
|
||||
:model-value="displayPhone"
|
||||
label="شماره موبایل"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
autocomplete="tel"
|
||||
dir="ltr"
|
||||
align="center"
|
||||
maxlength="11"
|
||||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||||
:error="fieldErrors.phone"
|
||||
hint="کد تأیید به این شماره ارسال میشود"
|
||||
@update:model-value="onPhoneInput"
|
||||
@blur="onPhoneBlur"
|
||||
/>
|
||||
<BaseButton block type="submit" :loading="auth.loading">دریافت کد تأیید</BaseButton>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 w-full max-w-[var(--form-max-width)] text-center">
|
||||
<p class="m-0 text-sm text-muted">
|
||||
حساب کاربری ندارید؟
|
||||
<RouterLink
|
||||
to="/register"
|
||||
class="ms-1 font-bold text-primary no-underline hover:underline"
|
||||
>
|
||||
ثبت نام کنید
|
||||
</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,363 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useIntervalFn } from '@vueuse/core'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
|
||||
import IconEditPen from '@/components/icons/IconEditPen.vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import { toPersianDigits, extractNumericString, normalizePhone } from '@/utils/format'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
clearFieldError,
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
import { getPendingRegister } from '@/utils/pendingRegister'
|
||||
|
||||
const OTP_LENGTH = 4
|
||||
const RESEND_SECONDS = 60
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const phone = computed(() => {
|
||||
const raw = route.query.phone
|
||||
const value = Array.isArray(raw) ? raw[0] : raw
|
||||
return normalizePhone(typeof value === 'string' ? value : '')
|
||||
})
|
||||
const isRegister = computed(() => route.query.mode === 'register')
|
||||
const digits = ref<string[]>(Array.from({ length: OTP_LENGTH }, () => ''))
|
||||
const inputRefs = ref<(HTMLInputElement | null)[]>([])
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
const secondsLeft = ref(RESEND_SECONDS)
|
||||
|
||||
const code = computed(() => digits.value.join(''))
|
||||
const canResend = computed(() => secondsLeft.value <= 0 && !auth.loading)
|
||||
const timerLabel = computed(() => {
|
||||
const minutes = Math.floor(secondsLeft.value / 60)
|
||||
const seconds = secondsLeft.value % 60
|
||||
const formatted = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
|
||||
return toPersianDigits(formatted)
|
||||
})
|
||||
|
||||
const { pause, resume } = useIntervalFn(
|
||||
() => {
|
||||
if (secondsLeft.value <= 1) {
|
||||
secondsLeft.value = 0
|
||||
pause()
|
||||
return
|
||||
}
|
||||
secondsLeft.value -= 1
|
||||
},
|
||||
1000,
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
function clearCodeError() {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'code')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
}
|
||||
|
||||
function setInputRef(el: unknown, index: number) {
|
||||
inputRefs.value[index] = el as HTMLInputElement | null
|
||||
}
|
||||
|
||||
function focusInput(index: number) {
|
||||
const clamped = Math.max(0, Math.min(OTP_LENGTH - 1, index))
|
||||
void nextTick(() => inputRefs.value[clamped]?.focus())
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
secondsLeft.value = RESEND_SECONDS
|
||||
resume()
|
||||
}
|
||||
|
||||
function fillDigits(value: string, startIndex = 0) {
|
||||
const numeric = extractNumericString(value).slice(0, OTP_LENGTH - startIndex)
|
||||
if (!numeric) return startIndex
|
||||
|
||||
const next = [...digits.value]
|
||||
let cursor = startIndex
|
||||
for (const digit of numeric) {
|
||||
if (cursor >= OTP_LENGTH) break
|
||||
next[cursor] = digit
|
||||
cursor += 1
|
||||
}
|
||||
digits.value = next
|
||||
clearCodeError()
|
||||
return Math.min(cursor, OTP_LENGTH - 1)
|
||||
}
|
||||
|
||||
function tryAutoSubmit() {
|
||||
if (code.value.length === OTP_LENGTH && !auth.loading) {
|
||||
void submit()
|
||||
}
|
||||
}
|
||||
|
||||
function onDigitInput(index: number, event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const numeric = extractNumericString(target.value)
|
||||
|
||||
if (numeric.length > 1) {
|
||||
const focusAt = fillDigits(numeric, index)
|
||||
target.value = digits.value[index] ?? ''
|
||||
focusInput(focusAt)
|
||||
tryAutoSubmit()
|
||||
return
|
||||
}
|
||||
|
||||
const digit = numeric.slice(-1)
|
||||
digits.value[index] = digit
|
||||
target.value = digit
|
||||
clearCodeError()
|
||||
|
||||
if (digit && index < OTP_LENGTH - 1) {
|
||||
focusInput(index + 1)
|
||||
} else if (digit) {
|
||||
tryAutoSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
function onDigitKeydown(index: number, event: KeyboardEvent) {
|
||||
if (event.key === 'Backspace') {
|
||||
if (digits.value[index]) {
|
||||
digits.value[index] = ''
|
||||
clearCodeError()
|
||||
return
|
||||
}
|
||||
if (index > 0) {
|
||||
digits.value[index - 1] = ''
|
||||
clearCodeError()
|
||||
focusInput(index - 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault()
|
||||
focusInput(index - 1)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault()
|
||||
focusInput(index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
function onDigitPaste(index: number, event: ClipboardEvent) {
|
||||
event.preventDefault()
|
||||
const pasted = event.clipboardData?.getData('text') ?? ''
|
||||
const focusAt = fillDigits(pasted, index)
|
||||
focusInput(focusAt)
|
||||
tryAutoSubmit()
|
||||
}
|
||||
|
||||
function displayDigit(value: string) {
|
||||
return value ? toPersianDigits(value) : ''
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (auth.loading) return
|
||||
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
|
||||
const errors: FieldErrors = {}
|
||||
if (code.value.length < OTP_LENGTH) {
|
||||
errors.code = 'کد تأیید را کامل وارد کنید.'
|
||||
}
|
||||
|
||||
if (hasFieldErrors(errors)) {
|
||||
fieldErrors.value = errors
|
||||
formError.value = FORM_VALIDATION_SUMMARY
|
||||
return
|
||||
}
|
||||
|
||||
const result = await auth.confirmOtp(phone.value, extractNumericString(code.value))
|
||||
if (result.success) {
|
||||
router.replace('/')
|
||||
} else {
|
||||
formError.value = result.message ?? 'کد تأیید نامعتبر است.'
|
||||
digits.value = Array.from({ length: OTP_LENGTH }, () => '')
|
||||
focusInput(0)
|
||||
}
|
||||
}
|
||||
|
||||
async function resend() {
|
||||
if (!canResend.value) return
|
||||
|
||||
formError.value = ''
|
||||
fieldErrors.value = {}
|
||||
|
||||
const pending = isRegister.value ? getPendingRegister() : null
|
||||
|
||||
if (isRegister.value && !pending) {
|
||||
formError.value = 'برای ارسال مجدد کد، نام و نام خانوادگی لازم است. لطفاً دوباره ثبتنام را شروع کنید.'
|
||||
return
|
||||
}
|
||||
|
||||
const result = await auth.requestOtp(phone.value, {
|
||||
isRegister: isRegister.value,
|
||||
firstName: pending?.firstName,
|
||||
lastName: pending?.lastName,
|
||||
})
|
||||
if (result.success) {
|
||||
digits.value = Array.from({ length: OTP_LENGTH }, () => '')
|
||||
startTimer()
|
||||
focusInput(0)
|
||||
} else {
|
||||
formError.value = result.message ?? 'خطا در ارسال مجدد کد.'
|
||||
}
|
||||
}
|
||||
|
||||
function changePhone() {
|
||||
router.replace({ name: isRegister.value ? 'register' : 'login' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!phone.value) {
|
||||
router.replace({ name: 'login' })
|
||||
return
|
||||
}
|
||||
startTimer()
|
||||
focusInput(0)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page page-shell auth-page">
|
||||
<button
|
||||
type="button"
|
||||
class="auth-back"
|
||||
aria-label="بازگشت"
|
||||
@click="router.push(isRegister ? '/register' : '/login')"
|
||||
>
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
<AuthThemeToggle />
|
||||
|
||||
<div class="page__scroll flex flex-col justify-center py-6">
|
||||
<div class="auth-header">
|
||||
<h1 class="page-title">تأیید شماره موبایل</h1>
|
||||
<p class="page-subtitle">کد ارسالشده به {{ toPersianDigits(phone) }} را وارد کنید</p>
|
||||
</div>
|
||||
|
||||
<form class="card" @submit.prevent="submit">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="form-group">
|
||||
<div
|
||||
class="otp-row flex w-full max-w-full justify-center ltr"
|
||||
dir="ltr"
|
||||
role="group"
|
||||
aria-label="کد تأیید"
|
||||
>
|
||||
<input
|
||||
v-for="(digit, index) in digits"
|
||||
:id="`otp-${index}`"
|
||||
:key="index"
|
||||
:ref="(el) => setInputRef(el, index)"
|
||||
class="otp-box"
|
||||
:class="{ 'otp-box--error': fieldErrors.code }"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
maxlength="1"
|
||||
:value="displayDigit(digit)"
|
||||
:aria-label="`رقم ${toPersianDigits(String(index + 1))}`"
|
||||
@input="onDigitInput(index, $event)"
|
||||
@keydown="onDigitKeydown(index, $event)"
|
||||
@paste="onDigitPaste(index, $event)"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="fieldErrors.code" class="input-error-text">{{ fieldErrors.code }}</p>
|
||||
</div>
|
||||
|
||||
<BaseButton block type="submit" :loading="auth.loading">تأیید و ورود</BaseButton>
|
||||
<BaseButton
|
||||
block
|
||||
variant="outline"
|
||||
type="button"
|
||||
class="mt-2"
|
||||
:disabled="!canResend"
|
||||
@click="resend"
|
||||
>
|
||||
<template v-if="canResend">ارسال مجدد کد</template>
|
||||
<template v-else>ارسال مجدد کد ({{ timerLabel }})</template>
|
||||
</BaseButton>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 mx-auto flex items-center justify-center gap-1.5 border-0 bg-transparent p-0 text-sm text-muted cursor-pointer transition-colors hover:text-text"
|
||||
@click="changePhone"
|
||||
>
|
||||
<span class="ltr tabular-nums" dir="ltr">{{ toPersianDigits(phone) }}</span>
|
||||
<span>ویرایش شماره</span>
|
||||
<IconEditPen :size="18" :stroke-width="1.5" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.otp-row {
|
||||
gap: clamp(0.25rem, 1.8vw, 0.625rem);
|
||||
}
|
||||
|
||||
.otp-box {
|
||||
flex: 1 1 0;
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
max-width: 48px;
|
||||
aspect-ratio: 48 / 52;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-size: clamp(0.95rem, 4.2vw, 1.25rem);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
caret-color: var(--color-primary);
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.otp-box:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.otp-box--error {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.input-error-text {
|
||||
color: var(--color-danger);
|
||||
font-size: 0.8125rem;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.auth-back {
|
||||
position: fixed;
|
||||
top: calc(12px + env(safe-area-inset-top, 0px));
|
||||
inset-inline-start: 16px;
|
||||
z-index: 100;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--color-text);
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import BaseInput from '@/components/ui/BaseInput.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import { normalizePhone } from '@/utils/format'
|
||||
import { validatePhone } from '@/utils/validation'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
clearFieldError,
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
import {
|
||||
JOB_CATEGORY_OPTIONS,
|
||||
setPendingRegister,
|
||||
type JobCategory,
|
||||
} from '@/utils/pendingRegister'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const firstName = ref('')
|
||||
const lastName = ref('')
|
||||
const phone = ref('')
|
||||
const jobCategory = ref<JobCategory | ''>('')
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
|
||||
function clearField(key: string) {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, key)
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
|
||||
const normalized = normalizePhone(phone.value)
|
||||
const errors: FieldErrors = {}
|
||||
|
||||
if (!firstName.value.trim()) {
|
||||
errors.firstName = 'نام الزامی است.'
|
||||
}
|
||||
if (!lastName.value.trim()) {
|
||||
errors.lastName = 'نام خانوادگی الزامی است.'
|
||||
}
|
||||
if (!validatePhone(normalized)) {
|
||||
errors.phone = 'شماره موبایل معتبر نیست.'
|
||||
}
|
||||
if (!jobCategory.value) {
|
||||
errors.jobCategory = 'دستهبندی شغلی الزامی است.'
|
||||
}
|
||||
|
||||
if (hasFieldErrors(errors)) {
|
||||
fieldErrors.value = errors
|
||||
formError.value = FORM_VALIDATION_SUMMARY
|
||||
return
|
||||
}
|
||||
|
||||
const names = {
|
||||
firstName: firstName.value.trim(),
|
||||
lastName: lastName.value.trim(),
|
||||
jobCategory: jobCategory.value as JobCategory,
|
||||
}
|
||||
setPendingRegister(names)
|
||||
|
||||
const result = await auth.requestOtp(normalized, {
|
||||
isRegister: true,
|
||||
firstName: names.firstName,
|
||||
lastName: names.lastName,
|
||||
})
|
||||
if (result.success) {
|
||||
router.push({ name: 'otp', query: { phone: normalized, mode: 'register' } })
|
||||
} else {
|
||||
formError.value = result.message ?? 'خطا در ارسال کد تأیید.'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page page-shell auth-page">
|
||||
<button type="button" class="auth-back" aria-label="بازگشت" @click="router.push('/login')">
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
<AuthThemeToggle />
|
||||
|
||||
<div class="page__scroll flex flex-col justify-center py-6">
|
||||
<div class="auth-header">
|
||||
<div class="text-5xl mb-3">🧵</div>
|
||||
<h1 class="page-title">ثبتنام</h1>
|
||||
<p class="page-subtitle">ایجاد حساب کاربری جدید</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<BaseInput
|
||||
v-model="firstName"
|
||||
label="نام"
|
||||
placeholder="نام خود را وارد کنید"
|
||||
:error="fieldErrors.firstName"
|
||||
@update:model-value="clearField('firstName')"
|
||||
/>
|
||||
<BaseInput
|
||||
v-model="lastName"
|
||||
label="نام خانوادگی"
|
||||
placeholder="نام خانوادگی"
|
||||
:error="fieldErrors.lastName"
|
||||
@update:model-value="clearField('lastName')"
|
||||
/>
|
||||
<BaseInput
|
||||
v-model="phone"
|
||||
label="شماره موبایل"
|
||||
type="tel"
|
||||
inputmode="tel"
|
||||
dir="ltr"
|
||||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||||
:error="fieldErrors.phone"
|
||||
@update:model-value="clearField('phone')"
|
||||
/>
|
||||
|
||||
<label class="job-label">
|
||||
دستهبندی شغلی
|
||||
<select
|
||||
v-model="jobCategory"
|
||||
class="job-select"
|
||||
:aria-invalid="!!fieldErrors.jobCategory"
|
||||
@change="clearField('jobCategory')"
|
||||
>
|
||||
<option disabled value="">انتخاب کنید</option>
|
||||
<option v-for="opt in JOB_CATEGORY_OPTIONS" :key="opt.id" :value="opt.id">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="fieldErrors.jobCategory" class="job-error">{{ fieldErrors.jobCategory }}</span>
|
||||
</label>
|
||||
|
||||
<BaseButton block :loading="auth.loading" @click="submit">دریافت کد تأیید</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 w-full max-w-[var(--form-max-width)] text-center">
|
||||
<p class="m-0 text-sm text-muted">
|
||||
قبلاً ثبتنام کردهاید؟
|
||||
<RouterLink
|
||||
to="/login"
|
||||
class="ms-1 font-bold text-primary no-underline hover:underline"
|
||||
>
|
||||
وارد شوید
|
||||
</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth-back {
|
||||
position: fixed;
|
||||
top: calc(12px + env(safe-area-inset-top, 0px));
|
||||
inset-inline-start: 16px;
|
||||
z-index: 100;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--color-text);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.job-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 14px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.job-select {
|
||||
height: 48px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0 12px;
|
||||
font: inherit;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.job-error {
|
||||
color: var(--color-danger);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user