Initial commit: public frontend for GameNo.

Vue 3 + Vite student-facing app with routing, Pinia state, and API integration.
This commit is contained in:
2026-08-09 04:18:09 +02:00
commit a9135bfa09
32 changed files with 5394 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
<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>