Files
gameno-front/src/views/LoginView.vue
T
kavehhn a9135bfa09 Initial commit: public frontend for GameNo.
Vue 3 + Vite student-facing app with routing, Pinia state, and API integration.
2026-08-09 04:18:09 +02:00

218 lines
5.4 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="login-page">
<div class="container login-shell">
<div class="login-visual" aria-hidden="true">
<p class="visual-brand">گام نو</p>
<p class="visual-text">کلاسها و گواهینامههایتان در یک نگاه.</p>
<div class="visual-steps">
<span></span><span></span><span></span>
</div>
</div>
<form class="login-form" @submit.prevent="handleLogin">
<h1>ورود به حساب</h1>
<p class="form-lead">نام کاربری و رمز عبور خود را وارد کنید.</p>
<div v-if="formError" class="alert" role="alert">{{ formError }}</div>
<div class="field">
<label for="username">نام کاربری</label>
<input
id="username"
v-model.trim="form.username"
type="text"
autocomplete="username"
placeholder="نام کاربری"
/>
<span v-if="errors.username" class="field-error">{{ errors.username }}</span>
</div>
<div class="field">
<label for="password">رمز عبور</label>
<input
id="password"
v-model="form.password"
type="password"
autocomplete="current-password"
placeholder="رمز عبور"
/>
<span v-if="errors.password" class="field-error">{{ errors.password }}</span>
</div>
<button class="btn btn-ink btn-block" type="submit" :disabled="loading">
<span v-if="loading" class="spinner" aria-hidden="true"></span>
{{ loading ? 'در حال ورود…' : 'ورود' }}
</button>
<RouterLink to="/" class="back-link">بازگشت به صفحه اصلی</RouterLink>
</form>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/authStore'
import { getApiErrorMessage } from '@/utils/format'
const auth = useAuthStore()
const router = useRouter()
const route = useRoute()
const form = reactive({ username: '', password: '' })
const errors = reactive({ username: '', password: '' })
const formError = ref('')
const loading = ref(false)
function validate() {
errors.username = form.username ? '' : 'نام کاربری الزامی است'
errors.password = form.password ? '' : 'رمز عبور الزامی است'
return !errors.username && !errors.password
}
async function handleLogin() {
formError.value = ''
if (!validate()) return
loading.value = true
try {
await auth.login({
username: form.username,
password: form.password
})
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/account'
router.push(redirect)
} catch (error) {
formError.value = getApiErrorMessage(error, 'نام کاربری یا رمز عبور نادرست است.')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-page {
min-height: calc(100vh - var(--header-h));
padding: 6.5rem 0 3rem;
background:
radial-gradient(circle at 85% 15%, rgba(232, 163, 23, 0.18), transparent 35%),
linear-gradient(180deg, var(--canvas), var(--canvas-deep));
}
.login-shell {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
max-width: 920px;
border-radius: 1.75rem;
overflow: hidden;
box-shadow: var(--shadow);
border: 1px solid rgba(20, 53, 43, 0.08);
background: var(--chalk);
}
.login-visual {
background:
linear-gradient(160deg, rgba(12, 34, 27, 0.88), rgba(45, 106, 79, 0.85)),
url('https://images.unsplash.com/photo-1522202176988-66273c2fd55f?auto=format&fit=crop&w=1200&q=80') center/cover;
color: var(--chalk);
padding: 2.5rem;
display: flex;
flex-direction: column;
justify-content: flex-end;
min-height: 28rem;
}
.visual-brand {
font-family: var(--font-display);
font-size: 3rem;
font-weight: 700;
color: var(--saffron);
line-height: 1;
}
.visual-text {
margin-top: 0.75rem;
color: rgba(247, 251, 248, 0.85);
max-width: 16rem;
}
.visual-steps {
display: flex;
gap: 0.55rem;
margin-top: 1.75rem;
}
.visual-steps span {
width: 0.55rem;
height: 0.55rem;
border-radius: 50%;
background: var(--saffron);
animation: pulse 1.6s ease-in-out infinite;
}
.visual-steps span:nth-child(2) { animation-delay: 0.2s; }
.visual-steps span:nth-child(3) { animation-delay: 0.4s; }
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.55; }
50% { transform: scale(1.25); opacity: 1; }
}
.login-form {
padding: 2.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
justify-content: center;
}
.login-form h1 {
font-family: var(--font-display);
font-size: 2.2rem;
}
.form-lead {
color: var(--ink-soft);
margin-bottom: 0.5rem;
}
.alert {
background: rgba(180, 35, 24, 0.08);
color: var(--danger);
border: 1px solid rgba(180, 35, 24, 0.2);
border-radius: 0.85rem;
padding: 0.75rem 0.9rem;
font-size: 0.9rem;
}
.btn .spinner {
width: 1.1rem;
height: 1.1rem;
border-width: 2px;
}
.back-link {
text-align: center;
color: var(--ink-soft);
font-size: 0.9rem;
font-weight: 600;
margin-top: 0.25rem;
}
.back-link:hover {
color: var(--leaf);
}
@media (max-width: 800px) {
.login-shell {
grid-template-columns: 1fr;
}
.login-visual {
min-height: 12rem;
}
}
</style>