feat: redesign user authentication screens
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+79
-469
@@ -1,13 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import AuthShell from '@/components/auth/AuthShell.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconHeadset from '@/components/icons/IconHeadset.vue'
|
||||
import IconPhone from '@/components/icons/IconPhone.vue'
|
||||
import IconQemashMark from '@/components/icons/IconQemashMark.vue'
|
||||
import { getPublicContact, telHref } from '@/services/settings'
|
||||
import { normalizePhone, toPersianDigits } from '@/utils/format'
|
||||
import { validatePhone } from '@/utils/validation'
|
||||
import {
|
||||
@@ -16,15 +13,10 @@ import {
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
import fabricUrl from '@/assets/images/login-fabric.jpg'
|
||||
import fabricUrl2 from '@/assets/images/login-fabric2.png'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const supportPhone = ref<string | null>(null)
|
||||
const supportHref = computed(() => telHref(supportPhone.value))
|
||||
|
||||
const phone = ref('')
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
@@ -76,500 +68,118 @@ async function submit() {
|
||||
formError.value = result.message ?? 'خطا در ارسال کد تأیید.'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const contact = await getPublicContact()
|
||||
supportPhone.value = contact.supportPhone
|
||||
} catch {
|
||||
supportPhone.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page login-page">
|
||||
<img class="login-page__bg" :src="fabricUrl" alt="" />
|
||||
<!-- <img class="login-page__fabric" :src="fabricUrl2" alt="" />-->
|
||||
<AuthShell back-to="/welcome">
|
||||
<h1 class="auth-title">به نادمی تکس خوش آمدید</h1>
|
||||
<p class="auth-lead">برای ادامه، وارد حساب کاربری خود شوید</p>
|
||||
|
||||
<div class="login-page__scroll">
|
||||
<div class="login-page__stage">
|
||||
<header class="login-top">
|
||||
<p class="login-top__welcome">به دنیای پارچه خوش آمدید</p>
|
||||
</header>
|
||||
<form class="auth-form" @submit.prevent="submit">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="login-brand">
|
||||
<IconQemashMark :size="78" />
|
||||
<h1 class="login-brand__fa">نادمی تکس</h1>
|
||||
<p class="login-brand__en">NADEMITEX</p>
|
||||
<p class="login-brand__tag">انتخاب حرفهایها</p>
|
||||
</div>
|
||||
<label class="auth-field" :class="{ 'is-invalid': Boolean(fieldErrors.phone) }">
|
||||
<IconPhone :size="18" color="#64748b" />
|
||||
<input
|
||||
:value="displayPhone"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
autocomplete="tel"
|
||||
maxlength="11"
|
||||
placeholder="شماره موبایل"
|
||||
:aria-invalid="Boolean(fieldErrors.phone)"
|
||||
aria-label="شماره موبایل"
|
||||
@input="onPhoneInput(($event.target as HTMLInputElement).value)"
|
||||
@blur="onPhoneBlur"
|
||||
/>
|
||||
</label>
|
||||
<p v-if="fieldErrors.phone" class="auth-error">{{ fieldErrors.phone }}</p>
|
||||
|
||||
<form class="login-card" @submit.prevent="submit">
|
||||
<h2 class="login-card__title">ورود به حساب کاربری</h2>
|
||||
<p class="login-card__lead">برای ادامه، شماره موبایل خود را وارد کنید.</p>
|
||||
<button class="auth-submit" type="submit" :disabled="auth.loading">
|
||||
{{ auth.loading ? 'در حال ارسال…' : 'ورود' }}
|
||||
</button>
|
||||
|
||||
<FormErrorBanner :message="formError" />
|
||||
<p class="auth-or"><span>یا</span></p>
|
||||
|
||||
<div class="phone-field" dir="ltr" :class="{ 'is-invalid': Boolean(fieldErrors.phone) }">
|
||||
<span class="phone-field__locale" aria-hidden="true">
|
||||
<svg class="iran-flag" viewBox="0 0 21 15" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="21" height="5" fill="#239F40" />
|
||||
<rect y="5" width="21" height="5" fill="#fff" />
|
||||
<rect y="10" width="21" height="5" fill="#DA0001" />
|
||||
<circle cx="10.5" cy="7.5" r="1.35" fill="#DA0001" />
|
||||
</svg>
|
||||
<svg class="phone-field__caret" viewBox="0 0 10 6" fill="none">
|
||||
<path
|
||||
d="M1 1.2 5 5l4-3.8"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
class="phone-field__input"
|
||||
:value="displayPhone"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
autocomplete="tel"
|
||||
dir="rtl"
|
||||
maxlength="11"
|
||||
placeholder="شماره موبایل"
|
||||
:aria-invalid="Boolean(fieldErrors.phone)"
|
||||
aria-label="شماره موبایل"
|
||||
@input="onPhoneInput(($event.target as HTMLInputElement).value)"
|
||||
@blur="onPhoneBlur"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="fieldErrors.phone" class="phone-field__error">{{ fieldErrors.phone }}</p>
|
||||
|
||||
<button class="login-submit" type="submit" :disabled="auth.loading">
|
||||
<span v-if="auth.loading" class="login-submit__spinner" aria-hidden="true" />
|
||||
<span class="login-submit__chevron" aria-hidden="true">
|
||||
<IconChevronLeft :size="18" color="#F4EDE4" />
|
||||
</span>
|
||||
<span>دریافت کد تأیید</span>
|
||||
</button>
|
||||
|
||||
<p class="login-register">
|
||||
حساب کاربری ندارید؟
|
||||
<RouterLink to="/register">ثبتنام کنید</RouterLink>
|
||||
</p>
|
||||
|
||||
<div class="login-or" role="separator">
|
||||
<span>یا</span>
|
||||
</div>
|
||||
|
||||
<div class="login-help">
|
||||
<a
|
||||
class="login-help__item"
|
||||
:href="supportHref"
|
||||
:class="{ 'is-disabled': !supportHref }"
|
||||
:aria-disabled="!supportHref"
|
||||
>
|
||||
<span class="login-help__icon">
|
||||
<IconHeadset :size="22" color="#3A3936" />
|
||||
</span>
|
||||
پشتیبانی
|
||||
</a>
|
||||
<a
|
||||
class="login-help__item"
|
||||
:href="supportHref"
|
||||
:class="{ 'is-disabled': !supportHref }"
|
||||
:aria-disabled="!supportHref"
|
||||
>
|
||||
<span class="login-help__icon">
|
||||
<IconPhone :size="22" color="#3A3936" />
|
||||
</span>
|
||||
تماس با ما
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="login-card__foot">پارچه، آغاز یک اعتماد ماندگار…</p>
|
||||
</form>
|
||||
|
||||
<div class="login-spacer" aria-hidden="true" />
|
||||
<p class="login-watermark">MORE<br />THAN<br />FABRIC</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RouterLink class="auth-outline" to="/register">ثبتنام در نادمی تکس</RouterLink>
|
||||
</form>
|
||||
</AuthShell>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
color: #f4ede4;
|
||||
background: #1a1612;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-page__bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center center;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* پارچه پایین صفحه — پشت باکس لاگین، نه روی آن */
|
||||
.login-page__fabric {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center bottom;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.login-page__scroll {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.login-page__stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
margin-inline: auto;
|
||||
padding: calc(10px + env(safe-area-inset-top, 0px)) 16px
|
||||
calc(14px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.login-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.login-top__welcome {
|
||||
color: #f3e6d6;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.auth-title {
|
||||
margin: 0 0 8px;
|
||||
text-align: center;
|
||||
margin-top: 28px;
|
||||
margin-bottom: 20px;
|
||||
color: #f3e6d6;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.login-brand__fa {
|
||||
margin: 14px 0 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.login-brand__en {
|
||||
margin: 6px 0 0;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.28em;
|
||||
color: #ead7c0;
|
||||
}
|
||||
|
||||
.login-brand__tag {
|
||||
margin: 10px 0 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
color: #efe2d2;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 22px 18px 16px;
|
||||
border: none;
|
||||
border-radius: 28px;
|
||||
background: #ebe4dc;
|
||||
box-shadow: 0 18px 40px rgba(12, 10, 8, 0.22);
|
||||
color: #2a2622;
|
||||
}
|
||||
|
||||
.login-card__title {
|
||||
margin: 0;
|
||||
.auth-lead {
|
||||
margin: 0 0 22px;
|
||||
text-align: center;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
color: #1c1b19;
|
||||
}
|
||||
|
||||
.login-card__lead {
|
||||
margin: 6px 0 0;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: #8a847c;
|
||||
}
|
||||
|
||||
.login-card :deep(.form-error-banner) {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.phone-field {
|
||||
.auth-form { display: flex; flex-direction: column; gap: 12px; }
|
||||
.auth-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 48px;
|
||||
margin-top: 18px;
|
||||
padding: 0 8px 0 14px;
|
||||
border: 1px solid #e4e0db;
|
||||
height: 52px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #eaecf0;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.phone-field.is-invalid {
|
||||
border-color: color-mix(in srgb, var(--color-danger) 55%, #e4e0db);
|
||||
}
|
||||
|
||||
.phone-field__locale {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #9a9590;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.iran-flag {
|
||||
width: 22px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.phone-field__caret {
|
||||
width: 10px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.phone-field__input {
|
||||
.auth-field.is-invalid { border-color: #b42318; }
|
||||
.auth-field input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #2a2622;
|
||||
font-family: inherit;
|
||||
font-size: 0.9375rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-align: start;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.phone-field__input::placeholder {
|
||||
color: #b0aaa3;
|
||||
}
|
||||
|
||||
.phone-field__error {
|
||||
margin: 8px 4px 0;
|
||||
color: var(--color-danger);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.login-submit {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
margin-top: 12px;
|
||||
padding: 0 44px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-surface);
|
||||
font-family: inherit;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.login-submit:not(:disabled):hover,
|
||||
.login-submit:not(:disabled):focus-visible {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.login-submit:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.login-submit:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-submit__chevron {
|
||||
position: absolute;
|
||||
inset-inline-end: 16px;
|
||||
.auth-field input::placeholder { color: #98a2b3; }
|
||||
.auth-error { margin: 0; color: #b42318; font-size: 12px; }
|
||||
.auth-submit,
|
||||
.auth-outline {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.login-submit__spinner {
|
||||
position: absolute;
|
||||
inset-inline-start: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.28);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: login-spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
.login-register {
|
||||
margin: 12px 0 0;
|
||||
text-align: center;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
color: #8a847c;
|
||||
}
|
||||
|
||||
.login-register a {
|
||||
height: 52px;
|
||||
border-radius: 14px;
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-or {
|
||||
.auth-submit {
|
||||
border: 0;
|
||||
background: #1e293b;
|
||||
color: #fff;
|
||||
}
|
||||
.auth-submit:disabled { opacity: 0.7; }
|
||||
.auth-outline {
|
||||
border: 1.5px solid #1e293b;
|
||||
color: #1e293b;
|
||||
background: transparent;
|
||||
}
|
||||
.auth-or {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 16px 4px 14px;
|
||||
color: #9a9590;
|
||||
font-size: 0.75rem;
|
||||
margin: 4px 0;
|
||||
color: #98a2b3;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-or::before,
|
||||
.login-or::after {
|
||||
content: '';
|
||||
.auth-or::before,
|
||||
.auth-or::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: #e6e1db;
|
||||
}
|
||||
|
||||
.login-help {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 48px;
|
||||
}
|
||||
|
||||
.login-help__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #6f6b66;
|
||||
font-size: 0.75rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-help__item.is-disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-help__icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border: 1px solid #d8d3cd;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.login-card__foot {
|
||||
margin: 18px 0 2px;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.7;
|
||||
color: #8a847c;
|
||||
}
|
||||
|
||||
.login-spacer {
|
||||
flex: 1 1 auto;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.login-watermark {
|
||||
margin: 0;
|
||||
padding-bottom: 8px;
|
||||
color: rgba(244, 237, 228, 0.58);
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2em;
|
||||
line-height: 1.35;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@keyframes login-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 359px) {
|
||||
.login-page__stage {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.login-brand__fa {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.login-help {
|
||||
gap: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.login-page {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
background: #eaecf0;
|
||||
}
|
||||
</style>
|
||||
|
||||
+58
-365
@@ -3,11 +3,8 @@ import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useIntervalFn } from '@vueuse/core'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import AuthShell from '@/components/auth/AuthShell.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import IconEditPen from '@/components/icons/IconEditPen.vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconQemashMark from '@/components/icons/IconQemashMark.vue'
|
||||
import { toPersianDigits, extractNumericString, normalizePhone } from '@/utils/format'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
@@ -16,7 +13,6 @@ import {
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
import { getPendingRegister } from '@/utils/pendingRegister'
|
||||
import fabricUrl from '@/assets/images/login-fabric.jpg'
|
||||
|
||||
const OTP_LENGTH = 4
|
||||
const RESEND_SECONDS = 60
|
||||
@@ -39,12 +35,7 @@ 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 timerLabel = computed(() => toPersianDigits(String(secondsLeft.value)))
|
||||
|
||||
const { pause, resume } = useIntervalFn(
|
||||
() => {
|
||||
@@ -232,368 +223,70 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page otp-page">
|
||||
<img class="otp-page__bg" :src="fabricUrl" alt="" />
|
||||
<AuthShell :back-to="isRegister ? '/register' : '/login'">
|
||||
<h1 class="auth-title">کد تأیید</h1>
|
||||
<p class="auth-lead">
|
||||
کد ارسالشده به
|
||||
<span dir="ltr">{{ toPersianDigits(phone) }}</span>
|
||||
را وارد کنید
|
||||
</p>
|
||||
|
||||
<div class="otp-page__scroll">
|
||||
<div class="otp-page__stage">
|
||||
<header class="otp-top">
|
||||
<button
|
||||
type="button"
|
||||
class="otp-top__back"
|
||||
aria-label="بازگشت"
|
||||
@click="router.push(isRegister ? '/register' : '/login')"
|
||||
>
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
</header>
|
||||
<form class="auth-form" @submit.prevent="submit">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="otp-brand">
|
||||
<IconQemashMark :size="72" />
|
||||
<h1 class="otp-brand__fa">نادمی تکس</h1>
|
||||
<p class="otp-brand__en">NADEMITEX</p>
|
||||
</div>
|
||||
|
||||
<form class="otp-card" @submit.prevent="submit">
|
||||
<h2 class="otp-card__title">تأیید موبایل</h2>
|
||||
<p class="otp-card__lead">
|
||||
کد ارسالشده به
|
||||
<span dir="ltr">{{ toPersianDigits(phone) }}</span>
|
||||
را وارد کنید.
|
||||
</p>
|
||||
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div
|
||||
class="otp-row"
|
||||
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="otp-card__error">{{ fieldErrors.code }}</p>
|
||||
|
||||
<button class="otp-submit" type="submit" :disabled="auth.loading">
|
||||
<span v-if="auth.loading" class="otp-submit__spinner" aria-hidden="true" />
|
||||
<span class="otp-submit__chevron" aria-hidden="true">
|
||||
<IconChevronLeft :size="18" color="#F4EDE4" />
|
||||
</span>
|
||||
<span>تأیید و ورود</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="otp-resend"
|
||||
type="button"
|
||||
:disabled="!canResend"
|
||||
@click="resend"
|
||||
>
|
||||
<template v-if="canResend">ارسال مجدد کد</template>
|
||||
<template v-else>ارسال مجدد کد ({{ timerLabel }})</template>
|
||||
</button>
|
||||
|
||||
<button type="button" class="otp-edit" @click="changePhone">
|
||||
<span dir="ltr">{{ toPersianDigits(phone) }}</span>
|
||||
<span>ویرایش شماره</span>
|
||||
<IconEditPen :size="16" :stroke-width="1.5" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="otp-spacer" aria-hidden="true" />
|
||||
<p class="otp-watermark">MORE<br />THAN<br />FABRIC</p>
|
||||
<div class="otp-row" 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="{ 'is-invalid': 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>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="fieldErrors.code" class="auth-error">{{ fieldErrors.code }}</p>
|
||||
|
||||
<button class="otp-resend" type="button" :disabled="!canResend" @click="resend">
|
||||
<template v-if="canResend">ارسال مجدد کد</template>
|
||||
<template v-else>ارسال مجدد کد ({{ timerLabel }} ثانیه)</template>
|
||||
</button>
|
||||
|
||||
<button class="auth-submit" type="submit" :disabled="auth.loading">
|
||||
{{ auth.loading ? 'در حال بررسی…' : 'تأیید' }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="otp-edit" @click="changePhone">ویرایش شماره</button>
|
||||
</form>
|
||||
</AuthShell>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.otp-page {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
color: #f4ede4;
|
||||
background: #1a1612;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.otp-page__bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center center;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.otp-page__scroll {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.otp-page__stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
max-width: 390px;
|
||||
margin-inline: auto;
|
||||
padding: calc(10px + env(safe-area-inset-top, 0px)) 16px
|
||||
calc(14px + env(safe-area-inset-bottom, 0px));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.otp-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.otp-top__back {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: rgba(244, 237, 228, 0.14);
|
||||
color: #f3e6d6;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.otp-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 18px;
|
||||
color: #f3e6d6;
|
||||
}
|
||||
|
||||
.otp-brand__fa {
|
||||
margin: 12px 0 0;
|
||||
font-size: 1.625rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.otp-brand__en {
|
||||
margin: 6px 0 0;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.28em;
|
||||
color: #ead7c0;
|
||||
}
|
||||
|
||||
.otp-card {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 22px 18px 18px;
|
||||
border: none;
|
||||
border-radius: 28px;
|
||||
background: #f4f1ed;
|
||||
box-shadow: 0 18px 40px rgba(12, 10, 8, 0.22);
|
||||
color: #2a2622;
|
||||
}
|
||||
|
||||
.otp-card__title {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
color: #1c1b19;
|
||||
}
|
||||
|
||||
.otp-card__lead {
|
||||
margin: 6px 0 0;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.7;
|
||||
color: #8a847c;
|
||||
}
|
||||
|
||||
.otp-card :deep(.form-error-banner) {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.otp-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.auth-title { margin: 0 0 8px; text-align: center; font-size: 24px; font-weight: 800; color: #1e293b; }
|
||||
.auth-lead { margin: 0 0 22px; text-align: center; color: #667085; font-size: 13px; line-height: 1.8; }
|
||||
.auth-form { display: flex; flex-direction: column; gap: 14px; align-items: stretch; }
|
||||
.otp-row { display: flex; justify-content: center; gap: 10px; direction: ltr; }
|
||||
.otp-box {
|
||||
width: 52px;
|
||||
height: 56px;
|
||||
padding: 0;
|
||||
border: 1px solid #e4e0db;
|
||||
border-radius: 16px;
|
||||
color: #1c1b19;
|
||||
font-family: inherit;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
caret-color: var(--color-primary);
|
||||
transition: border-color 0.15s;
|
||||
width: 56px; height: 56px; border: 1px solid #eaecf0; border-radius: 14px;
|
||||
background: #fff; text-align: center; font: inherit; font-size: 22px; font-weight: 700; color: #1e293b;
|
||||
}
|
||||
|
||||
.otp-box:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
.otp-box.is-invalid { border-color: #b42318; }
|
||||
.auth-error { margin: 0; text-align: center; color: #b42318; font-size: 12px; }
|
||||
.otp-resend, .otp-edit {
|
||||
border: 0; background: transparent; color: #667085; font: inherit; font-size: 13px; cursor: pointer;
|
||||
}
|
||||
|
||||
.otp-box--error {
|
||||
border-color: color-mix(in srgb, var(--color-danger) 55%, #e4e0db);
|
||||
}
|
||||
|
||||
.otp-card__error {
|
||||
margin: 8px 4px 0;
|
||||
color: var(--color-danger);
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.otp-submit {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
margin-top: 16px;
|
||||
padding: 0 44px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-surface);
|
||||
font-family: inherit;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.otp-submit:not(:disabled):hover,
|
||||
.otp-submit:not(:disabled):focus-visible {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.otp-submit:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.otp-submit__chevron {
|
||||
position: absolute;
|
||||
inset-inline-end: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.otp-submit__spinner {
|
||||
position: absolute;
|
||||
inset-inline-start: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.28);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: otp-spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
.otp-resend {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
margin-top: 10px;
|
||||
border: 1px solid #d8d3cd;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: #6f6b66;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.otp-resend:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.otp-edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #8a847c;
|
||||
font-family: inherit;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.otp-spacer {
|
||||
flex: 1 1 auto;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.otp-watermark {
|
||||
margin: 0;
|
||||
padding-bottom: 8px;
|
||||
color: rgba(244, 237, 228, 0.58);
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2em;
|
||||
line-height: 1.35;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@keyframes otp-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.otp-page {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.otp-resend:disabled { cursor: default; }
|
||||
.auth-submit {
|
||||
height: 52px; border: 0; border-radius: 14px; background: #1e293b; color: #fff;
|
||||
font: inherit; font-size: 16px; font-weight: 700; cursor: pointer;
|
||||
}
|
||||
.auth-submit:disabled { opacity: 0.7; }
|
||||
</style>
|
||||
|
||||
+200
-591
@@ -1,15 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import AuthShell from '@/components/auth/AuthShell.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconDiamond from '@/components/icons/IconDiamond.vue'
|
||||
import IconPhone from '@/components/icons/IconPhone.vue'
|
||||
import IconProfile from '@/components/icons/IconProfile.vue'
|
||||
import IconVerified from '@/components/icons/IconVerified.vue'
|
||||
import { normalizePhone, toPersianDigits } from '@/utils/format'
|
||||
import { validatePhone } from '@/utils/validation'
|
||||
import {
|
||||
@@ -23,7 +19,15 @@ import {
|
||||
setPendingRegister,
|
||||
type JobCategory,
|
||||
} from '@/utils/pendingRegister'
|
||||
import cardUrl from '@/assets/images/register-card.jpg'
|
||||
import jobIconFactory from '@/assets/images/job-icon-factory.png'
|
||||
import jobIconMannequin from '@/assets/images/job-icon-mannequin.png'
|
||||
import jobIconFabric from '@/assets/images/job-icon-fabric.png'
|
||||
|
||||
const JOB_ICONS: Record<JobCategory, string> = {
|
||||
garment_producer: jobIconFactory,
|
||||
atelier: jobIconMannequin,
|
||||
fabric_wholesaler: jobIconFabric,
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
@@ -31,25 +35,15 @@ const auth = useAuthStore()
|
||||
const firstName = ref('')
|
||||
const lastName = ref('')
|
||||
const phone = ref('')
|
||||
const jobCategory = ref<JobCategory | ''>('')
|
||||
const jobCategory = ref<JobCategory | ''>('garment_producer')
|
||||
const acceptedTerms = ref(true)
|
||||
const jobOpen = ref(false)
|
||||
const jobRoot = ref<HTMLElement | null>(null)
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
|
||||
const displayPhone = computed(() => (phone.value ? toPersianDigits(phone.value) : ''))
|
||||
const selectedJobLabel = computed(
|
||||
() => JOB_CATEGORY_OPTIONS.find((opt) => opt.id === jobCategory.value)?.label ?? 'دستهبندی شغلی',
|
||||
)
|
||||
|
||||
onClickOutside(jobRoot, () => {
|
||||
jobOpen.value = false
|
||||
})
|
||||
|
||||
function selectJob(id: JobCategory) {
|
||||
jobCategory.value = id
|
||||
jobOpen.value = false
|
||||
clearField('jobCategory')
|
||||
}
|
||||
|
||||
@@ -70,21 +64,11 @@ async function submit() {
|
||||
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 (!acceptedTerms.value) {
|
||||
errors.terms = 'پذیرش قوانین برای ثبتنام لازم است.'
|
||||
}
|
||||
if (!firstName.value.trim()) errors.firstName = 'نام الزامی است.'
|
||||
if (!lastName.value.trim()) errors.lastName = 'نام خانوادگی الزامی است.'
|
||||
if (!validatePhone(normalized)) errors.phone = 'شماره موبایل معتبر نیست.'
|
||||
if (!jobCategory.value) errors.jobCategory = 'دسته شغلی را انتخاب کنید.'
|
||||
if (!acceptedTerms.value) errors.terms = 'پذیرش قوانین برای ثبتنام لازم است.'
|
||||
|
||||
if (hasFieldErrors(errors)) {
|
||||
fieldErrors.value = errors
|
||||
@@ -113,607 +97,232 @@ async function submit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page register-page">
|
||||
<div class="register-page__scroll">
|
||||
<div class="register-page__stage">
|
||||
<header class="register-top">
|
||||
<p class="register-top__welcome">همراه ما باشید</p>
|
||||
<button
|
||||
type="button"
|
||||
class="register-top__back"
|
||||
aria-label="بازگشت"
|
||||
@click="router.push('/login')"
|
||||
>
|
||||
<IconChevronLeft :size="20" color="#2A2622" />
|
||||
</button>
|
||||
</header>
|
||||
<AuthShell back-to="/login">
|
||||
<h1 class="auth-title">ثبتنام</h1>
|
||||
<p class="auth-lead">برای ساخت حساب کاربری، اطلاعات زیر را وارد کنید</p>
|
||||
|
||||
<h1 class="register-title">ثبتنام در نادمی تکس</h1>
|
||||
<p class="register-lead">با چند مرحله ساده، عضوی از خانواده ما شوید.</p>
|
||||
<form class="auth-form" @submit.prevent="submit">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<form class="register-form" @submit.prevent="submit">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<label class="reg-field" :class="{ 'is-invalid': Boolean(fieldErrors.firstName) }">
|
||||
<!-- <span class="reg-field__meta">-->
|
||||
<IconProfile :size="18" color="#8A847C" />
|
||||
<!-- <span>نام</span>-->
|
||||
<!-- </span>-->
|
||||
<input
|
||||
v-model="firstName"
|
||||
class="reg-field__input"
|
||||
type="text"
|
||||
autocomplete="given-name"
|
||||
@input="clearField('firstName')"
|
||||
placeholder="نام"
|
||||
/>
|
||||
</label>
|
||||
<p v-if="fieldErrors.firstName" class="reg-field__error">{{ fieldErrors.firstName }}</p>
|
||||
|
||||
<label class="reg-field" :class="{ 'is-invalid': Boolean(fieldErrors.lastName) }">
|
||||
<!-- <span class="reg-field__meta">-->
|
||||
<IconProfile :size="18" color="#8A847C" />
|
||||
<!-- <span>نام خانوادگی</span>-->
|
||||
<!-- </span>-->
|
||||
<input
|
||||
v-model="lastName"
|
||||
class="reg-field__input"
|
||||
type="text"
|
||||
dir="rtl"
|
||||
autocomplete="family-name"
|
||||
@input="clearField('lastName')"
|
||||
placeholder="نام خانوادگی"
|
||||
/>
|
||||
</label>
|
||||
<p v-if="fieldErrors.lastName" class="reg-field__error">{{ fieldErrors.lastName }}</p>
|
||||
|
||||
<label class="reg-field" :class="{ 'is-invalid': Boolean(fieldErrors.phone) }">
|
||||
<!-- <span class="reg-field__meta">-->
|
||||
<IconPhone :size="18" color="#8A847C" />
|
||||
<!-- <span>شماره موبایل</span>-->
|
||||
<!-- </span>-->
|
||||
<span class="reg-field__phone" dir="ltr">
|
||||
<span class="reg-field__locale" aria-hidden="true">
|
||||
<svg class="iran-flag" viewBox="0 0 21 15" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="21" height="5" fill="#239F40" />
|
||||
<rect y="5" width="21" height="5" fill="#fff" />
|
||||
<rect y="10" width="21" height="5" fill="#DA0001" />
|
||||
<circle cx="10.5" cy="7.5" r="1.35" fill="#DA0001" />
|
||||
</svg>
|
||||
<svg class="reg-field__caret" viewBox="0 0 10 6" fill="none">
|
||||
<path
|
||||
d="M1 1.2 5 5l4-3.8"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
class="reg-field__input reg-field__input--phone"
|
||||
:value="displayPhone"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
autocomplete="tel"
|
||||
dir="rtl"
|
||||
maxlength="11"
|
||||
placeholder="شماره موبایل"
|
||||
@input="onPhoneInput(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<p v-if="fieldErrors.phone" class="reg-field__error">{{ fieldErrors.phone }}</p>
|
||||
|
||||
<div ref="jobRoot" class="job-wrap">
|
||||
<button
|
||||
type="button"
|
||||
class="reg-field job-trigger"
|
||||
:class="{ 'is-open': jobOpen, 'is-invalid': Boolean(fieldErrors.jobCategory) }"
|
||||
aria-haspopup="listbox"
|
||||
:aria-expanded="jobOpen"
|
||||
@click="jobOpen = !jobOpen"
|
||||
>
|
||||
<span class="reg-field__meta">
|
||||
<BasicIcon icon="vuesax-linear:category" :size="18" color="#8A847C" />
|
||||
<span :class="{ 'is-placeholder': !jobCategory }">{{ selectedJobLabel }}</span>
|
||||
</span>
|
||||
<svg class="job-chevron" viewBox="0 0 12 12" aria-hidden="true">
|
||||
<path
|
||||
d="M2.5 4.5 6 8l3.5-3.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul v-if="jobOpen" class="job-menu" role="listbox">
|
||||
<li v-for="opt in JOB_CATEGORY_OPTIONS" :key="opt.id">
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
class="job-option"
|
||||
:class="{ 'is-active': jobCategory === opt.id }"
|
||||
:aria-selected="jobCategory === opt.id"
|
||||
@click="selectJob(opt.id)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="fieldErrors.jobCategory" class="reg-field__error">{{ fieldErrors.jobCategory }}</p>
|
||||
</div>
|
||||
|
||||
<button class="register-submit" type="submit" :disabled="auth.loading">
|
||||
<span v-if="auth.loading" class="register-submit__spinner" aria-hidden="true" />
|
||||
<span class="register-submit__chevron" aria-hidden="true">
|
||||
<IconChevronLeft :size="18" color="#F4EDE4" />
|
||||
</span>
|
||||
<span>دریافت کد تأیید</span>
|
||||
</button>
|
||||
|
||||
<label class="register-terms">
|
||||
<input v-model="acceptedTerms" class="register-terms__native" type="checkbox" />
|
||||
<span class="register-terms__box" :class="{ 'is-on': acceptedTerms }" aria-hidden="true">
|
||||
<svg v-if="acceptedTerms" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M2.2 6.2 4.7 8.7 9.8 3.4"
|
||||
stroke="#fff"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
با ثبتنام،
|
||||
<strong>قوانین و شرایط</strong>
|
||||
استفاده از سرویس را میپذیرم.
|
||||
</span>
|
||||
</label>
|
||||
<p v-if="fieldErrors.terms" class="reg-field__error">{{ fieldErrors.terms }}</p>
|
||||
</form>
|
||||
|
||||
<section class="register-perks" aria-label="مزایا">
|
||||
<article class="register-perk">
|
||||
<IconVerified :size="22" color="#6F6B66" />
|
||||
<strong>اطمینان از اصالت محصولات</strong>
|
||||
</article>
|
||||
<article class="register-perk">
|
||||
<BasicIcon icon="vuesax-linear:truck-time" :size="22" color="#6F6B66" />
|
||||
<strong>دسترسی سریع به کالیتههای جدید</strong>
|
||||
</article>
|
||||
<article class="register-perk">
|
||||
<IconDiamond :size="22" color="#6F6B66" />
|
||||
<strong>تجربه خرید ویژه مشتریان</strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div class="register-foot">
|
||||
<img class="register-foot__bg" :src="cardUrl" alt="" />
|
||||
<p class="register-foot__copy">همراه حرفهای<br />کسبوکار شما</p>
|
||||
</div>
|
||||
<div class="auth-row">
|
||||
<label class="auth-field" :class="{ 'is-invalid': fieldErrors.firstName }">
|
||||
<IconProfile :size="18" color="#98a2b3" />
|
||||
<input
|
||||
v-model="firstName"
|
||||
type="text"
|
||||
autocomplete="given-name"
|
||||
placeholder="نام"
|
||||
@input="clearField('firstName')"
|
||||
/>
|
||||
</label>
|
||||
<label class="auth-field" :class="{ 'is-invalid': fieldErrors.lastName }">
|
||||
<input
|
||||
v-model="lastName"
|
||||
type="text"
|
||||
autocomplete="family-name"
|
||||
placeholder="نام خانوادگی"
|
||||
@input="clearField('lastName')"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="fieldErrors.firstName || fieldErrors.lastName" class="auth-error">
|
||||
{{ fieldErrors.firstName || fieldErrors.lastName }}
|
||||
</p>
|
||||
|
||||
<label class="auth-field" :class="{ 'is-invalid': fieldErrors.phone }">
|
||||
<IconPhone :size="18" color="#98a2b3" />
|
||||
<input
|
||||
:value="displayPhone"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
autocomplete="tel"
|
||||
maxlength="11"
|
||||
placeholder="شماره موبایل"
|
||||
@input="onPhoneInput(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</label>
|
||||
<p v-if="fieldErrors.phone" class="auth-error">{{ fieldErrors.phone }}</p>
|
||||
|
||||
<p class="job-label">دسته شغلی شما</p>
|
||||
<div class="job-grid" :class="{ 'is-invalid': fieldErrors.jobCategory }">
|
||||
<button
|
||||
v-for="opt in JOB_CATEGORY_OPTIONS"
|
||||
:key="opt.id"
|
||||
type="button"
|
||||
class="job-card"
|
||||
:class="{ 'is-on': jobCategory === opt.id }"
|
||||
@click="selectJob(opt.id)"
|
||||
>
|
||||
<img class="job-card__icon" :src="JOB_ICONS[opt.id]" :alt="opt.label" draggable="false" />
|
||||
<span>{{ opt.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="fieldErrors.jobCategory" class="auth-error">{{ fieldErrors.jobCategory }}</p>
|
||||
|
||||
<button class="auth-submit" type="submit" :disabled="auth.loading">
|
||||
{{ auth.loading ? 'در حال ارسال…' : 'ثبتنام' }}
|
||||
</button>
|
||||
|
||||
<label class="terms">
|
||||
<input v-model="acceptedTerms" type="checkbox" />
|
||||
<span>با ثبتنام، قوانین و شرایط استفاده را میپذیرم.</span>
|
||||
</label>
|
||||
<p v-if="fieldErrors.terms" class="auth-error">{{ fieldErrors.terms }}</p>
|
||||
</form>
|
||||
</AuthShell>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.register-page {
|
||||
background: #f3eee8;
|
||||
color: #2a2622;
|
||||
overflow: hidden;
|
||||
.auth-title {
|
||||
margin: 0 0 8px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.register-page__scroll {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
.auth-lead {
|
||||
margin: 0 0 22px;
|
||||
text-align: center;
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.register-page__stage {
|
||||
.auth-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
max-width: 390px;
|
||||
margin-inline: auto;
|
||||
padding: calc(10px + env(safe-area-inset-top, 0px)) 20px 0;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.register-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.register-top__welcome {
|
||||
margin: 0;
|
||||
color: #6f6b66;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.register-top__back {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-inline-start: auto;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
.auth-row {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.register-title {
|
||||
margin: 18px 0 0;
|
||||
text-align: center;
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
color: #1c1b19;
|
||||
}
|
||||
|
||||
.register-lead {
|
||||
margin: 6px 0 0;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.7;
|
||||
color: #8a847c;
|
||||
}
|
||||
|
||||
.register-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.register-form :deep(.form-error-banner) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.reg-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
height: 52px;
|
||||
margin-top: 10px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #e0d9d0;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.reg-field.is-invalid,
|
||||
.job-trigger.is-invalid {
|
||||
border-color: color-mix(in srgb, var(--color-danger) 55%, #e6e1db);
|
||||
}
|
||||
|
||||
.reg-field__meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #5f5b56;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reg-field__meta .is-placeholder {
|
||||
color: #8a847c;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.reg-field__input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.auth-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: 52px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #eaecf0;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-field.is-invalid {
|
||||
border-color: #b42318;
|
||||
}
|
||||
|
||||
.auth-field input {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #2a2622;
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
color: #1e293b;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.reg-field__input::placeholder {
|
||||
color: #b0aaa3;
|
||||
.auth-field input::placeholder {
|
||||
color: #98a2b3;
|
||||
}
|
||||
|
||||
.reg-field__phone {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
.auth-error {
|
||||
margin: -4px 0 0;
|
||||
color: #b42318;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.reg-field__locale {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #9a9590;
|
||||
.job-label {
|
||||
margin: 4px 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.iran-flag {
|
||||
width: 22px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.06);
|
||||
.job-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.reg-field__caret {
|
||||
width: 10px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.reg-field__input--phone {
|
||||
text-align: start;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.reg-field__error {
|
||||
margin: 6px 8px 0;
|
||||
color: var(--color-danger);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.job-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.job-trigger {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.job-trigger.is-open {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.job-chevron {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: #9aa3af;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.job-trigger.is-open .job-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.job-menu {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
top: calc(100% + 6px);
|
||||
z-index: 20;
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
list-style: none;
|
||||
background: #fbfaf7;
|
||||
border: 1px solid #e6e1db;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 24px rgba(16, 24, 40, 0.08);
|
||||
}
|
||||
|
||||
.job-option {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: #2a2622;
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.job-option.is-active {
|
||||
background: #e8f0ee;
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.register-submit {
|
||||
position: relative;
|
||||
.job-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
margin-top: 14px;
|
||||
padding: 0 44px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-surface);
|
||||
font: inherit;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.register-submit:not(:disabled):hover,
|
||||
.register-submit:not(:disabled):focus-visible {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.register-submit:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.register-submit__chevron {
|
||||
position: absolute;
|
||||
inset-inline-end: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.register-submit__spinner {
|
||||
position: absolute;
|
||||
inset-inline-start: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.28);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: register-spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
.register-terms {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
color: #6f6b66;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.register-terms strong {
|
||||
font-weight: 700;
|
||||
color: #2a2622;
|
||||
}
|
||||
|
||||
.register-terms__native {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.register-terms__box {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
border: 1.5px solid #cfc9c2;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.register-terms__box.is-on {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.register-terms__box svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.register-perks {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin: 35px 0 -20px;
|
||||
padding: 16px 10px 14px;
|
||||
border-radius: 22px;
|
||||
background: #ebe4dc;
|
||||
box-shadow: 0 10px 28px rgba(40, 32, 24, 0.12), 0 2px 8px rgba(40, 32, 24, 0.06);
|
||||
min-height: 108px;
|
||||
padding: 10px 6px;
|
||||
border: 1.5px solid #eaecf0;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
color: #667085;
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.register-perk {
|
||||
.job-card__icon {
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.job-card.is-on {
|
||||
border-color: #1e293b;
|
||||
color: #1e293b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.auth-submit {
|
||||
margin-top: 4px;
|
||||
height: 52px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: #1e293b;
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auth-submit:disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.terms {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
color: #6f6b66;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.register-perk strong {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.register-foot {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: auto;
|
||||
margin-inline: -20px;
|
||||
min-height: 260px;
|
||||
overflow: hidden;
|
||||
padding: 48px 0 calc(16px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.register-foot__bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: 88% 32%;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
mask-image: linear-gradient(to bottom, transparent 0%, #000 42%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 42%);
|
||||
}
|
||||
|
||||
.register-foot::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.register-foot__copy {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 24px;
|
||||
top: 38%;
|
||||
margin: 0;
|
||||
max-width: 10rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.55;
|
||||
text-align: right;
|
||||
color:#1c1b19;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.register-foot__copy::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 1.375rem;
|
||||
height: 1.5px;
|
||||
margin-top: 8px;
|
||||
margin-left: auto;
|
||||
background: #c9a56a;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
@keyframes register-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.register-page {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.terms input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: #1e293b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
+142
-281
@@ -1,342 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconQemashMark from '@/components/icons/IconQemashMark.vue'
|
||||
import fabricUrl from '@/assets/images/login-fabric.jpg'
|
||||
import logoUrl from '@/assets/images/nademitex-auth-logo.png'
|
||||
import slide1 from '@/assets/images/login-fabric.jpg'
|
||||
import slide2 from '@/assets/images/fabric-slide-2.jpg'
|
||||
import slide3 from '@/assets/images/fabric-slide-3.jpg'
|
||||
import slide4 from '@/assets/images/fabric-slide-4.jpg'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
/* ترتیب DOM از راست به چپ در RTL: ستاره → کامیون → لایهها */
|
||||
const features = [
|
||||
const slides = [
|
||||
{
|
||||
icon: 'star',
|
||||
title: 'همراه همیشگی کسبوکارتان',
|
||||
subtitle: 'با خدمات ویژه مشتریان',
|
||||
image: slide1,
|
||||
title: 'پارچه، برای ایدههای بزرگ',
|
||||
lead: 'انتخاب بهتر، تأمین سریعتر، همراه حرفهای شما در مسیر تولید پوشاک',
|
||||
},
|
||||
{
|
||||
icon: 'vuesax-linear:truck-time',
|
||||
title: 'سفارش عمده',
|
||||
subtitle: 'مستقیم از تولیدکننده',
|
||||
image: slide2,
|
||||
title: 'پارچه، برای ایدههای بزرگ',
|
||||
lead: 'از کت و شلوار تا مانتو؛ بافتی که طرح شما را کامل میکند',
|
||||
},
|
||||
{
|
||||
icon: 'vuesax-linear:element-3',
|
||||
title: 'تنوع بینظیر محصولات',
|
||||
subtitle: 'از کلاسیک تا مدرن',
|
||||
image: slide3,
|
||||
title: 'پارچه، برای ایدههای بزرگ',
|
||||
lead: 'کتان، پنبه و فاستونی؛ موجودی عمده برای مزون و تولیدکننده',
|
||||
},
|
||||
] as const
|
||||
{
|
||||
image: slide4,
|
||||
title: 'پارچه، برای ایدههای بزرگ',
|
||||
lead: 'کالیته را ببینید، رنگ را لمس کنید، بعد سفارش دهید',
|
||||
},
|
||||
]
|
||||
|
||||
function goLogin() {
|
||||
const redirect = typeof router.currentRoute.value.query.redirect === 'string'
|
||||
? router.currentRoute.value.query.redirect
|
||||
: undefined
|
||||
router.push({
|
||||
name: 'login',
|
||||
query: redirect ? { redirect } : undefined,
|
||||
})
|
||||
const index = ref(0)
|
||||
let timer = 0
|
||||
let startX = 0
|
||||
|
||||
function go(next: number) {
|
||||
index.value = (next + slides.length) % slides.length
|
||||
}
|
||||
|
||||
function onTouchStart(event: TouchEvent) {
|
||||
startX = event.changedTouches[0]?.clientX ?? 0
|
||||
}
|
||||
|
||||
function onTouchEnd(event: TouchEvent) {
|
||||
const dx = (event.changedTouches[0]?.clientX ?? 0) - startX
|
||||
if (Math.abs(dx) < 40) return
|
||||
go(index.value + (dx < 0 ? 1 : -1))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
timer = window.setInterval(() => go(index.value + 1), 4500)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => window.clearInterval(timer))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page welcome-page">
|
||||
<img class="welcome-page__bg" :src="fabricUrl" alt="" />
|
||||
<div class="welcome-page__veil" aria-hidden="true" />
|
||||
<div class="welcome" @touchstart.passive="onTouchStart" @touchend.passive="onTouchEnd">
|
||||
<img
|
||||
v-for="(slide, i) in slides"
|
||||
:key="slide.lead"
|
||||
class="welcome__photo"
|
||||
:class="{ 'is-on': i === index }"
|
||||
:src="slide.image"
|
||||
alt=""
|
||||
/>
|
||||
<div class="welcome__shade" />
|
||||
|
||||
<div class="welcome-page__scroll">
|
||||
<div class="welcome-page__stage">
|
||||
<header class="welcome-top">
|
||||
<button type="button" class="welcome-skip" @click="goLogin">رد شدن</button>
|
||||
</header>
|
||||
<header class="welcome__brand">
|
||||
<img class="welcome__logo" :src="logoUrl" alt="Nademi Tex" draggable="false" />
|
||||
</header>
|
||||
|
||||
<div class="welcome-brand">
|
||||
<IconQemashMark :size="88" />
|
||||
<h1 class="welcome-brand__fa">نادمی تکس</h1>
|
||||
<p class="welcome-brand__tag">انتخاب حرفهای، شروع یک همکاری ماندگار</p>
|
||||
<span class="welcome-brand__rule" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<ul class="welcome-features">
|
||||
<li v-for="item in features" :key="item.title" class="welcome-feature">
|
||||
<span class="welcome-feature__icon" aria-hidden="true">
|
||||
<svg
|
||||
v-if="item.icon === 'star'"
|
||||
class="welcome-feature__star"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"
|
||||
stroke="#fff"
|
||||
stroke-width="1.5"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<BasicIcon v-else :icon="item.icon" :size="22" color="#ffffff" />
|
||||
</span>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span>{{ item.subtitle }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="welcome-spacer" aria-hidden="true" />
|
||||
|
||||
<div class="welcome-dots" aria-hidden="true">
|
||||
<i class="is-on" />
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</div>
|
||||
|
||||
<button type="button" class="welcome-cta" @click="goLogin">
|
||||
<span>ورود / ثبتنام</span>
|
||||
<span class="welcome-cta__chevron" aria-hidden="true">
|
||||
<IconChevronLeft :size="18" color="#ffffff" />
|
||||
</span>
|
||||
</button>
|
||||
<div class="welcome__copy">
|
||||
<h1>{{ slides[index]?.title }}</h1>
|
||||
<p>{{ slides[index]?.lead }}</p>
|
||||
<div class="welcome__dots" role="tablist" aria-label="اسلایدها">
|
||||
<button
|
||||
v-for="(slide, i) in slides"
|
||||
:key="slide.lead"
|
||||
type="button"
|
||||
:class="{ 'is-on': i === index }"
|
||||
:aria-label="`اسلاید ${i + 1}`"
|
||||
@click="go(i)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="welcome__cta" @click="router.push('/login')">ورود / ثبتنام</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.welcome-page {
|
||||
.welcome {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
color: #f4ede4;
|
||||
background: #1a1612;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.welcome-page__bg {
|
||||
.welcome__photo {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.welcome__photo.is-on {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.welcome__shade {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.15) 20%, rgba(15, 23, 42, 0.82) 100%);
|
||||
}
|
||||
|
||||
.welcome__brand,
|
||||
.welcome__copy,
|
||||
.welcome__cta {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.welcome__brand {
|
||||
padding-top: 14vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome__logo {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 110px;
|
||||
margin: 0 auto;
|
||||
object-fit: contain;
|
||||
filter: brightness(0) invert(1);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.welcome-page__veil {
|
||||
.welcome__copy {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(12, 10, 8, 0.42) 0%, rgba(12, 10, 8, 0.28) 42%, rgba(12, 10, 8, 0.72) 100%);
|
||||
pointer-events: none;
|
||||
right: 24px;
|
||||
left: 24px;
|
||||
bottom: 108px;
|
||||
}
|
||||
|
||||
.welcome-page__scroll {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.welcome-page__stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
max-width: 390px;
|
||||
margin-inline: auto;
|
||||
padding: calc(12px + env(safe-area-inset-top, 0px)) 20px
|
||||
calc(20px + env(safe-area-inset-bottom, 0px));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.welcome-top {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.welcome-skip {
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: rgba(12, 10, 8, 0.45);
|
||||
color: #f4ede4;
|
||||
font: inherit;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.welcome-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.welcome-brand__fa {
|
||||
margin: 14px 0 0;
|
||||
font-size: 1.875rem;
|
||||
.welcome__copy h1 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: #ead7c0;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.welcome-brand__tag {
|
||||
margin: 10px 0 0;
|
||||
max-width: 16rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.7;
|
||||
color: rgba(244, 237, 228, 0.92);
|
||||
.welcome__copy p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.welcome-brand__rule {
|
||||
display: block;
|
||||
width: 36px;
|
||||
height: 1.5px;
|
||||
margin-top: 16px;
|
||||
border-radius: 1px;
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.welcome-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin: 36px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.welcome-feature {
|
||||
.welcome__dots {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-feature__icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 999px;
|
||||
background: rgba(244, 237, 228, 0.22);
|
||||
color: #fff;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.welcome-feature__star {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.welcome-feature strong {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
color: #f4ede4;
|
||||
}
|
||||
|
||||
.welcome-feature span {
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: rgba(244, 237, 228, 0.72);
|
||||
}
|
||||
|
||||
.welcome-spacer {
|
||||
flex: 1 1 auto;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.welcome-dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.welcome-dots i {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
.welcome__dots button {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: rgba(244, 237, 228, 0.28);
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.welcome-dots i.is-on {
|
||||
background: #f4ede4;
|
||||
.welcome__dots button.is-on {
|
||||
width: 18px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.welcome-cta {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
.welcome__cta {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
left: 24px;
|
||||
bottom: 28px;
|
||||
height: 52px;
|
||||
padding: 0 44px;
|
||||
border: 1px solid rgba(244, 237, 228, 0.28);
|
||||
border-radius: 999px;
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--color-primary) 65%, transparent) 0%,
|
||||
color-mix(in srgb, var(--color-primary) 42%, transparent) 48%,
|
||||
color-mix(in srgb, var(--color-accent-navy) 48%, transparent) 100%
|
||||
);
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: #1e293b;
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.welcome-cta__chevron {
|
||||
position: absolute;
|
||||
inset-inline-end: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.welcome-page {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.welcome-page__stage {
|
||||
max-width: 390px;
|
||||
padding-block: 16px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 359px) {
|
||||
.welcome-page__stage {
|
||||
padding-inline: 14px;
|
||||
}
|
||||
|
||||
.welcome-brand__fa {
|
||||
font-size: 1.625rem;
|
||||
}
|
||||
|
||||
.welcome-feature__icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user