Redesign login/register with Mento branding and email/mobile OTP.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
<script setup lang="ts">
|
||||
import MentoLogo from '@/components/brand/MentoLogo.vue';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
headline?: string;
|
||||
description?: string;
|
||||
}>(),
|
||||
{
|
||||
headline: 'همه ارتباطات، یکجا مدیریت میشوند',
|
||||
description:
|
||||
'با منتو فروش، پشتیبانی و ارتباط با مشتری را در یک پنل یکپارچه مدیریت کنید و رشد پایدار بسازید.',
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="brand-panel" aria-hidden="false">
|
||||
<div class="brand-head">
|
||||
<MentoLogo :size="44" :show-text="false" />
|
||||
<div class="brand-copy">
|
||||
<strong>منتو</strong>
|
||||
<span>نرمافزار مدیریت ارتباط با مشتری</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brand-body">
|
||||
<h2>{{ headline }}</h2>
|
||||
<p>{{ description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="illustration" aria-hidden="true">
|
||||
<div class="dash">
|
||||
<div class="dash-top">
|
||||
<span class="avatar" />
|
||||
<span class="bar short" />
|
||||
<span class="bar" />
|
||||
</div>
|
||||
<div class="chart-line" />
|
||||
<div class="chart-bars">
|
||||
<i /><i /><i /><i /><i />
|
||||
</div>
|
||||
</div>
|
||||
<span class="float chat" />
|
||||
<span class="float people" />
|
||||
<span class="float check" />
|
||||
<span class="float ring" />
|
||||
<span class="plant" />
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.brand-panel {
|
||||
position: relative;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: clamp(1.4rem, 3vw, 2rem);
|
||||
background:
|
||||
radial-gradient(circle at 20% 10%, rgba(45, 212, 191, 0.18), transparent 42%),
|
||||
linear-gradient(165deg, #f0fdfa 0%, #e6fffa 48%, #f8fafc 100%);
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
display: grid;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.brand-copy strong {
|
||||
font-size: 1.2rem;
|
||||
color: #0f172a;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.brand-copy span {
|
||||
font-size: 0.78rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.brand-body h2 {
|
||||
margin: 0 0 0.55rem;
|
||||
font-size: clamp(1.35rem, 2.2vw, 1.75rem);
|
||||
line-height: 1.45;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.brand-body p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
line-height: 1.75;
|
||||
font-size: 0.95rem;
|
||||
max-width: 28ch;
|
||||
}
|
||||
|
||||
.illustration {
|
||||
position: relative;
|
||||
margin-top: auto;
|
||||
min-height: 220px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dash {
|
||||
position: absolute;
|
||||
inset-inline-start: 12%;
|
||||
bottom: 18%;
|
||||
width: min(78%, 280px);
|
||||
aspect-ratio: 1.35;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.1);
|
||||
padding: 1rem;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.dash-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(145deg, #5eead4, #0d9488);
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #e2e8f0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bar.short {
|
||||
flex: 0.55;
|
||||
}
|
||||
|
||||
.chart-line {
|
||||
height: 54px;
|
||||
border-radius: 12px;
|
||||
background:
|
||||
linear-gradient(180deg, transparent 55%, rgba(13, 148, 136, 0.12) 55%),
|
||||
radial-gradient(circle at 20% 60%, #14b8a6 3px, transparent 4px),
|
||||
radial-gradient(circle at 45% 35%, #14b8a6 3px, transparent 4px),
|
||||
radial-gradient(circle at 70% 50%, #14b8a6 3px, transparent 4px),
|
||||
radial-gradient(circle at 88% 28%, #14b8a6 3px, transparent 4px);
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
|
||||
.chart-bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.45rem;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.chart-bars i {
|
||||
flex: 1;
|
||||
border-radius: 6px 6px 2px 2px;
|
||||
background: linear-gradient(180deg, #5eead4, #0f766e);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.chart-bars i:nth-child(1) { height: 45%; }
|
||||
.chart-bars i:nth-child(2) { height: 70%; }
|
||||
.chart-bars i:nth-child(3) { height: 55%; }
|
||||
.chart-bars i:nth-child(4) { height: 88%; }
|
||||
.chart-bars i:nth-child(5) { height: 62%; }
|
||||
|
||||
.float {
|
||||
position: absolute;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.float.chat {
|
||||
top: 18%;
|
||||
inset-inline-end: 8%;
|
||||
background:
|
||||
radial-gradient(circle at 35% 40%, #0d9488 0 28%, transparent 29%),
|
||||
linear-gradient(145deg, #ecfdf5, #fff);
|
||||
}
|
||||
|
||||
.float.people {
|
||||
top: 42%;
|
||||
inset-inline-start: 4%;
|
||||
background:
|
||||
radial-gradient(circle at 50% 38%, #14b8a6 0 22%, transparent 23%),
|
||||
linear-gradient(145deg, #f0fdfa, #fff);
|
||||
}
|
||||
|
||||
.float.check {
|
||||
bottom: 28%;
|
||||
inset-inline-end: 6%;
|
||||
background:
|
||||
linear-gradient(135deg, transparent 42%, #10b981 42% 58%, transparent 58%),
|
||||
linear-gradient(45deg, transparent 48%, #10b981 48% 62%, transparent 62%),
|
||||
#ecfdf5;
|
||||
}
|
||||
|
||||
.float.ring {
|
||||
top: 8%;
|
||||
inset-inline-start: 18%;
|
||||
border-radius: 999px;
|
||||
background:
|
||||
conic-gradient(#0d9488 0 70%, #e2e8f0 70% 100%);
|
||||
mask: radial-gradient(farthest-side, transparent calc(100% - 7px), #000 calc(100% - 6px));
|
||||
}
|
||||
|
||||
.plant {
|
||||
position: absolute;
|
||||
bottom: 8%;
|
||||
inset-inline-start: 8%;
|
||||
width: 28px;
|
||||
height: 36px;
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 20%, #34d399 0 35%, transparent 36%),
|
||||
linear-gradient(180deg, #0d9488 0 55%, #134e4a 55%);
|
||||
border-radius: 40% 40% 8px 8px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.brand-panel {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+42
-13
@@ -1,6 +1,12 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { AuthUserDto, LoginResponse } from '@mento/shared';
|
||||
import type {
|
||||
AuthUserDto,
|
||||
LoginChallengeResponse,
|
||||
LoginResponse,
|
||||
RegisterChallengeResponse,
|
||||
RegisterOrganizationRequest,
|
||||
} from '@mento/shared';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
const TOKEN_KEY = 'mento_token';
|
||||
@@ -39,19 +45,40 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
clearLegacy();
|
||||
}
|
||||
|
||||
async function login(email: string, password: string) {
|
||||
const { data } = await api.post<LoginResponse>('/auth/login', { email, password });
|
||||
async function startLogin(identifier: string, password: string) {
|
||||
const { data } = await api.post<LoginChallengeResponse>('/auth/login', {
|
||||
identifier,
|
||||
password,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
async function verifyLogin(challengeId: string, code: string) {
|
||||
const { data } = await api.post<LoginResponse>('/auth/login/verify', {
|
||||
challengeId,
|
||||
code,
|
||||
});
|
||||
persist(data);
|
||||
}
|
||||
|
||||
async function register(payload: {
|
||||
organizationName: string;
|
||||
fullName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
businessField: string;
|
||||
}) {
|
||||
const { data } = await api.post<LoginResponse>('/auth/register', payload);
|
||||
async function startRegister(payload: RegisterOrganizationRequest) {
|
||||
const { data } = await api.post<RegisterChallengeResponse>(
|
||||
'/auth/register',
|
||||
payload,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function verifyRegister(
|
||||
challengeId: string,
|
||||
emailCode: string,
|
||||
mobileCode: string,
|
||||
) {
|
||||
const { data } = await api.post<LoginResponse>('/auth/register/verify', {
|
||||
challengeId,
|
||||
emailCode,
|
||||
mobileCode,
|
||||
});
|
||||
persist(data);
|
||||
}
|
||||
|
||||
@@ -67,8 +94,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
token,
|
||||
user,
|
||||
isAuthenticated,
|
||||
login,
|
||||
register,
|
||||
startLogin,
|
||||
verifyLogin,
|
||||
startRegister,
|
||||
verifyRegister,
|
||||
logout,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,117 +1,440 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter, useRoute, RouterLink } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import AuthBrandPanel from '@/components/auth/AuthBrandPanel.vue';
|
||||
import MentoLogo from '@/components/brand/MentoLogo.vue';
|
||||
import axios from 'axios';
|
||||
import type { LoginChallengeResponse } from '@mento/shared';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const email = ref('owner@demo.local');
|
||||
const step = ref<'credentials' | 'otp'>('credentials');
|
||||
const identifier = ref('owner@demo.local');
|
||||
const password = ref('demo1234');
|
||||
const showPassword = ref(false);
|
||||
const remember = ref(true);
|
||||
const otpCode = ref('');
|
||||
const challenge = ref<LoginChallengeResponse | null>(null);
|
||||
const error = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
async function submit() {
|
||||
const otpHint = computed(() => {
|
||||
if (!challenge.value) return '';
|
||||
const dest = challenge.value.maskedDestination ?? '';
|
||||
return challenge.value.channel === 'email'
|
||||
? `کد ارسالشده به ایمیل ${dest} را وارد کنید`
|
||||
: `کد ارسالشده به موبایل ${dest} را وارد کنید`;
|
||||
});
|
||||
|
||||
function apiError(e: unknown, fallback: string) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
if (!e.response) {
|
||||
return 'اتصال به سرور برقرار نشد. مطمئن شوید API روی پورت ۳۰۰۰ در حال اجراست.';
|
||||
}
|
||||
const msg = (e.response.data as { message?: string | string[] })?.message;
|
||||
return Array.isArray(msg) ? msg.join('، ') : (msg ?? fallback);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function submitCredentials() {
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
try {
|
||||
await auth.login(email.value, password.value);
|
||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/';
|
||||
await router.push(redirect);
|
||||
} catch (e) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
if (!e.response) {
|
||||
error.value = 'اتصال به سرور برقرار نشد. مطمئن شوید API روی پورت ۳۰۰۰ در حال اجراست.';
|
||||
} else {
|
||||
const msg = (e.response.data as { message?: string | string[] })?.message;
|
||||
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'ورود ناموفق بود');
|
||||
}
|
||||
} else {
|
||||
error.value = 'ورود ناموفق بود';
|
||||
challenge.value = await auth.startLogin(identifier.value.trim(), password.value);
|
||||
if (challenge.value.devCode) {
|
||||
otpCode.value = challenge.value.devCode;
|
||||
}
|
||||
step.value = 'otp';
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'ورود ناموفق بود');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitOtp() {
|
||||
if (!challenge.value) return;
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
try {
|
||||
await auth.verifyLogin(challenge.value.challengeId, otpCode.value.trim());
|
||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/';
|
||||
await router.push(redirect);
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'تأیید کد ناموفق بود');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function backToCredentials() {
|
||||
step.value = 'credentials';
|
||||
otpCode.value = '';
|
||||
challenge.value = null;
|
||||
error.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth">
|
||||
<section class="panel">
|
||||
<p class="eyebrow">منتو CRM</p>
|
||||
<h1>ورود به پنل</h1>
|
||||
<p class="lead">همترازی قابلیت با دیدار، با تجربه و هویت مستقل منتو (Mento).</p>
|
||||
<div class="auth-page">
|
||||
<div class="auth-shell">
|
||||
<AuthBrandPanel />
|
||||
|
||||
<form class="form" @submit.prevent="submit">
|
||||
<div class="field">
|
||||
<label for="email">ایمیل</label>
|
||||
<input id="email" v-model="email" type="email" required autocomplete="username" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">رمز عبور</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<button class="btn" type="submit" :disabled="loading">
|
||||
{{ loading ? 'در حال ورود…' : 'ورود' }}
|
||||
</button>
|
||||
</form>
|
||||
<section class="form-panel">
|
||||
<header class="mobile-brand">
|
||||
<MentoLogo :size="52" :show-text="false" />
|
||||
<strong>منتو</strong>
|
||||
<span>نرمافزار مدیریت ارتباط با مشتری</span>
|
||||
</header>
|
||||
|
||||
<p class="switch">
|
||||
سازمان ندارید؟
|
||||
<RouterLink to="/register">ثبتنام سازمان</RouterLink>
|
||||
</p>
|
||||
</section>
|
||||
<div class="form-inner">
|
||||
<template v-if="step === 'credentials'">
|
||||
<h1>خوش آمدید 👋</h1>
|
||||
<p class="lead">برای ورود به حساب کاربری خود، اطلاعاتتان را وارد کنید</p>
|
||||
|
||||
<form class="form" @submit.prevent="submitCredentials">
|
||||
<div class="field">
|
||||
<label for="identifier">ایمیل یا شماره موبایل</label>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
id="identifier"
|
||||
v-model="identifier"
|
||||
type="text"
|
||||
required
|
||||
autocomplete="username"
|
||||
placeholder="ایمیل یا شماره موبایل خود را وارد کنید"
|
||||
dir="ltr"
|
||||
/>
|
||||
<span class="ico user" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="password">رمز عبور</label>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
placeholder="رمز عبور خود را وارد کنید"
|
||||
dir="ltr"
|
||||
/>
|
||||
<span class="ico lock" aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
class="eye"
|
||||
:aria-label="showPassword ? 'مخفی کردن رمز' : 'نمایش رمز'"
|
||||
@click="showPassword = !showPassword"
|
||||
>
|
||||
{{ showPassword ? 'مخفی' : 'نمایش' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row-actions">
|
||||
<label class="remember">
|
||||
<input v-model="remember" type="checkbox" />
|
||||
<span>مرا به خاطر بسپار</span>
|
||||
</label>
|
||||
<span class="forgot muted">رمز عبور خود را فراموش کردهاید؟</span>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<button class="btn submit" type="submit" :disabled="loading">
|
||||
{{ loading ? 'در حال ارسال کد…' : 'ورود' }}
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<button type="button" class="back" @click="backToCredentials">بازگشت</button>
|
||||
<h1>تأیید هویت</h1>
|
||||
<p class="lead">{{ otpHint }}</p>
|
||||
<p v-if="challenge?.devCode" class="dev-hint">
|
||||
کد توسعه: <strong>{{ challenge.devCode }}</strong>
|
||||
</p>
|
||||
|
||||
<form class="form" @submit.prevent="submitOtp">
|
||||
<div class="field">
|
||||
<label for="otp">کد یکبارمصرف</label>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
id="otp"
|
||||
v-model="otpCode"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
maxlength="8"
|
||||
required
|
||||
autocomplete="one-time-code"
|
||||
placeholder="کد را وارد کنید"
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<button class="btn submit" type="submit" :disabled="loading">
|
||||
{{ loading ? 'در حال تأیید…' : 'تأیید و ورود' }}
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<p class="switch">
|
||||
حساب کاربری ندارید؟
|
||||
<RouterLink to="/register">ثبتنام کنید</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="auth-footer">
|
||||
<p>۱۴۰۵ منتو. تمامی حقوق محفوظ است.</p>
|
||||
<nav>
|
||||
<a href="#">حریم خصوصی</a>
|
||||
<span>|</span>
|
||||
<a href="#">شرایط و قوانین</a>
|
||||
<span>|</span>
|
||||
<a href="#">تماس با ما</a>
|
||||
</nav>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth {
|
||||
.auth-page {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
padding-bottom: max(1rem, env(safe-area-inset-bottom));
|
||||
background:
|
||||
radial-gradient(circle at 90% 0%, rgba(13, 148, 136, 0.08), transparent 36%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: min(440px, 100%);
|
||||
.auth-shell {
|
||||
flex: 1;
|
||||
width: min(1040px, 100%);
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: calc(var(--radius) + 4px);
|
||||
border-radius: 28px;
|
||||
box-shadow: var(--shadow);
|
||||
padding: clamp(1.1rem, 4vw, 1.6rem);
|
||||
overflow: hidden;
|
||||
min-height: min(640px, calc(100dvh - 5.5rem));
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
.form-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: clamp(1.2rem, 3vw, 2.4rem);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-brand {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: 1.4rem;
|
||||
padding-top: 0.4rem;
|
||||
}
|
||||
|
||||
.mobile-brand strong {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.mobile-brand span {
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.form-inner {
|
||||
width: min(420px, 100%);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0.35rem 0 0.4rem;
|
||||
font-size: 1.8rem;
|
||||
margin: 0 0 0.4rem;
|
||||
font-size: clamp(1.45rem, 3vw, 1.85rem);
|
||||
}
|
||||
|
||||
.lead,
|
||||
.switch {
|
||||
.lead {
|
||||
margin: 0 0 1.4rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
margin: 1.4rem 0 1rem;
|
||||
gap: 0.95rem;
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-wrap input {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 0.85rem 2.6rem 0.85rem 0.95rem;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.input-wrap input:focus {
|
||||
outline: 2px solid var(--accent-soft);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ico {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
inset-inline-end: 0.85rem;
|
||||
transform: translateY(-50%);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ico.user {
|
||||
background:
|
||||
radial-gradient(circle at 50% 35%, currentColor 0 28%, transparent 29%),
|
||||
radial-gradient(circle at 50% 85%, currentColor 0 38%, transparent 39%);
|
||||
color: #64748b;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.ico.lock {
|
||||
background:
|
||||
linear-gradient(currentColor, currentColor) center 62%/10px 8px no-repeat,
|
||||
radial-gradient(circle at 50% 38%, transparent 34%, currentColor 35% 55%, transparent 56%);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.eye {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
inset-inline-start: 0.55rem;
|
||||
transform: translateY(-50%);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.4rem;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.remember {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.forgot {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.muted {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.submit {
|
||||
width: 100%;
|
||||
padding: 0.9rem 1.1rem;
|
||||
border-radius: 14px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.switch {
|
||||
margin: 1.35rem 0 0;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.switch a {
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.back {
|
||||
align-self: flex-start;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dev-hint {
|
||||
margin: -0.6rem 0 1rem;
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: 12px;
|
||||
background: var(--accent-soft);
|
||||
color: #0f766e;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
width: min(1040px, 100%);
|
||||
margin: 0.85rem auto 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem 1rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
padding-inline: 0.35rem;
|
||||
}
|
||||
|
||||
.auth-footer nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.auth-footer a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.auth-shell {
|
||||
grid-template-columns: minmax(280px, 0.9fr) minmax(340px, 1.1fr);
|
||||
}
|
||||
|
||||
.mobile-brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-inner {
|
||||
margin: 0;
|
||||
width: min(440px, 100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,133 +3,611 @@ import { ref } from 'vue';
|
||||
import { useRouter, RouterLink } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { BUSINESS_FIELDS, BUSINESS_FIELD_LABELS } from '@/lib/business-fields';
|
||||
import AuthBrandPanel from '@/components/auth/AuthBrandPanel.vue';
|
||||
import MentoLogo from '@/components/brand/MentoLogo.vue';
|
||||
import axios from 'axios';
|
||||
import type { RegisterChallengeResponse } from '@mento/shared';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
const step = ref<'form' | 'otp'>('form');
|
||||
const organizationName = ref('');
|
||||
const businessField = ref('');
|
||||
const fullName = ref('');
|
||||
const email = ref('');
|
||||
const mobile = ref('');
|
||||
const password = ref('');
|
||||
const passwordConfirm = ref('');
|
||||
const referralCode = ref('');
|
||||
const acceptedTerms = ref(false);
|
||||
const showPassword = ref(false);
|
||||
const showPasswordConfirm = ref(false);
|
||||
const emailCode = ref('');
|
||||
const mobileCode = ref('');
|
||||
const challenge = ref<RegisterChallengeResponse | null>(null);
|
||||
const error = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
async function submit() {
|
||||
function apiError(e: unknown, fallback: string) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
if (!e.response) {
|
||||
return 'اتصال به سرور برقرار نشد. مطمئن شوید API روی پورت ۳۰۰۰ در حال اجراست.';
|
||||
}
|
||||
const msg = (e.response.data as { message?: string | string[] })?.message;
|
||||
return Array.isArray(msg) ? msg.join('، ') : (msg ?? fallback);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
error.value = '';
|
||||
if (!acceptedTerms.value) {
|
||||
error.value = 'پذیرش شرایط و قوانین الزامی است';
|
||||
return;
|
||||
}
|
||||
if (password.value !== passwordConfirm.value) {
|
||||
error.value = 'تکرار رمز عبور مطابقت ندارد';
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
await auth.register({
|
||||
organizationName: organizationName.value,
|
||||
businessField: businessField.value,
|
||||
fullName: fullName.value,
|
||||
email: email.value,
|
||||
challenge.value = await auth.startRegister({
|
||||
organizationName: organizationName.value.trim(),
|
||||
businessField: businessField.value as (typeof BUSINESS_FIELDS)[number],
|
||||
fullName: fullName.value.trim(),
|
||||
email: email.value.trim(),
|
||||
mobile: mobile.value.trim(),
|
||||
password: password.value,
|
||||
passwordConfirm: passwordConfirm.value,
|
||||
referralCode: referralCode.value.trim() || undefined,
|
||||
acceptedTerms: true,
|
||||
});
|
||||
await router.push('/');
|
||||
if (challenge.value.devEmailCode) emailCode.value = challenge.value.devEmailCode;
|
||||
if (challenge.value.devMobileCode) mobileCode.value = challenge.value.devMobileCode;
|
||||
step.value = 'otp';
|
||||
} catch (e) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
if (!e.response) {
|
||||
error.value = 'اتصال به سرور برقرار نشد. مطمئن شوید API روی پورت ۳۰۰۰ در حال اجراست.';
|
||||
} else {
|
||||
const msg = (e.response.data as { message?: string | string[] })?.message;
|
||||
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'ثبتنام ناموفق بود');
|
||||
}
|
||||
} else {
|
||||
error.value = 'ثبتنام ناموفق بود';
|
||||
}
|
||||
error.value = apiError(e, 'ثبتنام ناموفق بود');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitOtp() {
|
||||
if (!challenge.value) return;
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
try {
|
||||
await auth.verifyRegister(
|
||||
challenge.value.challengeId,
|
||||
emailCode.value.trim(),
|
||||
mobileCode.value.trim(),
|
||||
);
|
||||
await router.push('/');
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'تأیید کد ناموفق بود');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function backToForm() {
|
||||
step.value = 'form';
|
||||
emailCode.value = '';
|
||||
mobileCode.value = '';
|
||||
challenge.value = null;
|
||||
error.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth">
|
||||
<section class="panel">
|
||||
<p class="eyebrow">شروع با منتو</p>
|
||||
<h1>ساخت سازمان</h1>
|
||||
<p class="lead">اولین کاربر بهعنوان مالک سازمان ثبت میشود. زمینه فعالیت کمک میکند منتو فضای کاری را با صنعت شما همتراز کند.</p>
|
||||
<div class="auth-page">
|
||||
<div class="auth-shell">
|
||||
<AuthBrandPanel
|
||||
headline="همه ارتباطات، یکجا مدیریت میشوند"
|
||||
description="با ساخت حساب منتو، سازمان خود را راهاندازی کنید و فروش و پشتیبانی را از یکجا پیش ببرید."
|
||||
/>
|
||||
|
||||
<form class="form" @submit.prevent="submit">
|
||||
<div class="field">
|
||||
<label for="org">نام سازمان</label>
|
||||
<input id="org" v-model="organizationName" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="field">زمینه فعالیت</label>
|
||||
<select id="field" v-model="businessField" required>
|
||||
<option value="" disabled>انتخاب کنید</option>
|
||||
<option v-for="id in BUSINESS_FIELDS" :key="id" :value="id">
|
||||
{{ BUSINESS_FIELD_LABELS[id] }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="name">نام شما</label>
|
||||
<input id="name" v-model="fullName" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="email">ایمیل</label>
|
||||
<input id="email" v-model="email" type="email" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">رمز عبور</label>
|
||||
<input id="password" v-model="password" type="password" minlength="6" required />
|
||||
</div>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<button class="btn" type="submit" :disabled="loading">
|
||||
{{ loading ? 'در حال ثبت…' : 'ایجاد سازمان' }}
|
||||
</button>
|
||||
</form>
|
||||
<section class="form-panel">
|
||||
<header class="mobile-brand">
|
||||
<RouterLink class="back-link" to="/login">بازگشت</RouterLink>
|
||||
<MentoLogo :size="48" :show-text="false" />
|
||||
<strong>منتو</strong>
|
||||
<span>نرمافزار مدیریت ارتباط با مشتری</span>
|
||||
</header>
|
||||
|
||||
<p class="switch">
|
||||
حساب دارید؟
|
||||
<RouterLink to="/login">ورود</RouterLink>
|
||||
</p>
|
||||
</section>
|
||||
<div class="form-inner">
|
||||
<template v-if="step === 'form'">
|
||||
<div class="title-row">
|
||||
<span class="title-ico" aria-hidden="true" />
|
||||
<div>
|
||||
<h1>ایجاد حساب کاربری</h1>
|
||||
<p class="lead">برای شروع، اطلاعات خود را وارد کنید</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="form" @submit.prevent="submitForm">
|
||||
<div class="grid-2">
|
||||
<div class="field">
|
||||
<label for="org">نام سازمان <em>*</em></label>
|
||||
<div class="input-wrap">
|
||||
<input id="org" v-model="organizationName" required placeholder="نام سازمان" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="field">زمینه فعالیت <em>*</em></label>
|
||||
<select id="field" v-model="businessField" required>
|
||||
<option value="" disabled>انتخاب کنید</option>
|
||||
<option v-for="id in BUSINESS_FIELDS" :key="id" :value="id">
|
||||
{{ BUSINESS_FIELD_LABELS[id] }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="field">
|
||||
<label for="name">نام و نام خانوادگی <em>*</em></label>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
id="name"
|
||||
v-model="fullName"
|
||||
required
|
||||
placeholder="نام و نام خانوادگی خود را وارد کنید"
|
||||
/>
|
||||
<span class="ico user" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="email">ایمیل <em>*</em></label>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="ایمیل خود را وارد کنید"
|
||||
dir="ltr"
|
||||
/>
|
||||
<span class="ico mail" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="field">
|
||||
<label for="mobile">شماره موبایل <em>*</em></label>
|
||||
<div class="input-wrap mobile-wrap">
|
||||
<span class="cc">+۹۸</span>
|
||||
<input
|
||||
id="mobile"
|
||||
v-model="mobile"
|
||||
type="tel"
|
||||
required
|
||||
placeholder="۹۱۲۱۲۳۴۵۶۷"
|
||||
dir="ltr"
|
||||
/>
|
||||
<span class="ico phone" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">رمز عبور <em>*</em></label>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
minlength="6"
|
||||
required
|
||||
placeholder="رمز عبور خود را وارد کنید"
|
||||
dir="ltr"
|
||||
/>
|
||||
<span class="ico lock" aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
class="eye"
|
||||
@click="showPassword = !showPassword"
|
||||
>
|
||||
{{ showPassword ? 'مخفی' : 'نمایش' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="field">
|
||||
<label for="passwordConfirm">تکرار رمز عبور <em>*</em></label>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
id="passwordConfirm"
|
||||
v-model="passwordConfirm"
|
||||
:type="showPasswordConfirm ? 'text' : 'password'"
|
||||
minlength="6"
|
||||
required
|
||||
placeholder="رمز عبور خود را تکرار کنید"
|
||||
dir="ltr"
|
||||
/>
|
||||
<span class="ico lock" aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
class="eye"
|
||||
@click="showPasswordConfirm = !showPasswordConfirm"
|
||||
>
|
||||
{{ showPasswordConfirm ? 'مخفی' : 'نمایش' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="referral">کد معرف (اختیاری)</label>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
id="referral"
|
||||
v-model="referralCode"
|
||||
placeholder="اگر کد معرف دارید وارد کنید"
|
||||
/>
|
||||
<span class="ico gift" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="terms">
|
||||
<input v-model="acceptedTerms" type="checkbox" />
|
||||
<span>با شرایط و قوانین و سیاست حفظ حریم خصوصی موافقم.</span>
|
||||
</label>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<button class="btn submit" type="submit" :disabled="loading">
|
||||
{{ loading ? 'در حال ارسال کد…' : 'ثبت نام' }}
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<button type="button" class="back" @click="backToForm">بازگشت</button>
|
||||
<h1>تأیید ایمیل و موبایل</h1>
|
||||
<p class="lead">
|
||||
کدهای یکبارمصرف ارسالشده به
|
||||
{{ challenge?.maskedEmail }} و {{ challenge?.maskedMobile }} را وارد کنید.
|
||||
</p>
|
||||
<p v-if="challenge?.devEmailCode || challenge?.devMobileCode" class="dev-hint">
|
||||
کد ایمیل: <strong>{{ challenge?.devEmailCode }}</strong>
|
||||
—
|
||||
کد موبایل: <strong>{{ challenge?.devMobileCode }}</strong>
|
||||
</p>
|
||||
|
||||
<form class="form" @submit.prevent="submitOtp">
|
||||
<div class="grid-2">
|
||||
<div class="field">
|
||||
<label for="emailCode">کد ایمیل</label>
|
||||
<input
|
||||
id="emailCode"
|
||||
v-model="emailCode"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
required
|
||||
dir="ltr"
|
||||
placeholder="کد ایمیل"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="mobileCode">کد موبایل</label>
|
||||
<input
|
||||
id="mobileCode"
|
||||
v-model="mobileCode"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
required
|
||||
dir="ltr"
|
||||
placeholder="کد موبایل"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<button class="btn submit" type="submit" :disabled="loading">
|
||||
{{ loading ? 'در حال تأیید…' : 'تأیید و ایجاد حساب' }}
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<div class="login-cta">
|
||||
<p>قبلاً حساب کاربری دارید؟</p>
|
||||
<RouterLink class="btn-outline" to="/login">ورود به حساب کاربری</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="auth-footer">
|
||||
<p>۱۴۰۵ منتو. تمامی حقوق محفوظ است.</p>
|
||||
<nav>
|
||||
<a href="#">پشتیبانی</a>
|
||||
<span>|</span>
|
||||
<a href="#">تماس با ما</a>
|
||||
</nav>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth {
|
||||
.auth-page {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
padding-bottom: max(1rem, env(safe-area-inset-bottom));
|
||||
background:
|
||||
radial-gradient(circle at 8% 0%, rgba(13, 148, 136, 0.07), transparent 34%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: min(480px, 100%);
|
||||
.auth-shell {
|
||||
flex: 1;
|
||||
width: min(1100px, 100%);
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: calc(var(--radius) + 4px);
|
||||
border-radius: 28px;
|
||||
box-shadow: var(--shadow);
|
||||
padding: clamp(1.1rem, 4vw, 1.6rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
.form-panel {
|
||||
padding: clamp(1rem, 2.5vw, 2rem);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-brand {
|
||||
position: relative;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
top: 0.2rem;
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mobile-brand strong {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.mobile-brand span {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.form-inner {
|
||||
width: min(640px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.1rem;
|
||||
}
|
||||
|
||||
.title-ico {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
background:
|
||||
radial-gradient(circle at 50% 38%, #0d9488 0 18%, transparent 19%),
|
||||
linear-gradient(145deg, #ccfbf1, #f0fdfa);
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 25%, white);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title-ico::after {
|
||||
content: '+';
|
||||
position: absolute;
|
||||
inset-inline-end: 6px;
|
||||
bottom: 4px;
|
||||
color: #0d9488;
|
||||
font-weight: 800;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0.35rem 0 0.4rem;
|
||||
font-size: 1.8rem;
|
||||
margin: 0 0 0.25rem;
|
||||
font-size: clamp(1.3rem, 2.5vw, 1.65rem);
|
||||
}
|
||||
|
||||
.lead,
|
||||
.switch {
|
||||
.lead {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
margin: 1.4rem 0 1rem;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.field em {
|
||||
color: var(--danger);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.field select,
|
||||
.input-wrap input,
|
||||
.form > .grid-2 .field > input {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 0.8rem 0.95rem;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-wrap input {
|
||||
padding-inline-end: 2.5rem;
|
||||
}
|
||||
|
||||
.mobile-wrap input {
|
||||
padding-inline-start: 3.4rem;
|
||||
}
|
||||
|
||||
.cc {
|
||||
position: absolute;
|
||||
inset-inline-start: 0.7rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ico {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
inset-inline-end: 0.8rem;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
background: #64748b;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ico.user {
|
||||
border-radius: 999px;
|
||||
background:
|
||||
radial-gradient(circle at 50% 35%, #64748b 0 28%, transparent 29%),
|
||||
radial-gradient(circle at 50% 85%, #64748b 0 38%, transparent 39%);
|
||||
}
|
||||
|
||||
.ico.mail {
|
||||
background: linear-gradient(#64748b, #64748b);
|
||||
mask: linear-gradient(#000 0 0);
|
||||
opacity: 0.4;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.ico.lock,
|
||||
.ico.phone,
|
||||
.ico.gift {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.eye {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
inset-inline-start: 0.45rem;
|
||||
transform: translateY(-50%);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 0.72rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.terms {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.55rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.submit {
|
||||
width: 100%;
|
||||
padding: 0.9rem 1.1rem;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.login-cta {
|
||||
margin-top: 1.4rem;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.login-cta p {
|
||||
margin: 0 0 0.55rem;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
min-width: min(100%, 280px);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 14px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--line));
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.back {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dev-hint {
|
||||
margin: 0.75rem 0 1rem;
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: 12px;
|
||||
background: var(--accent-soft);
|
||||
color: #0f766e;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
width: min(1100px, 100%);
|
||||
margin: 0.85rem auto 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem 1rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
padding-inline: 0.35rem;
|
||||
}
|
||||
|
||||
.auth-footer nav {
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.grid-2 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.auth-shell {
|
||||
grid-template-columns: minmax(260px, 0.85fr) minmax(380px, 1.15fr);
|
||||
}
|
||||
|
||||
.mobile-brand {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user