Files
aranroig.com/frontend/app/pages/admin/login.vue
Aran Roig 626612b8f0
All checks were successful
Build and Deploy Nuxt / build (push) Successful in 40s
Reduction
2026-09-21 23:37:27 +02:00

190 lines
4.0 KiB
Vue

<script setup lang="ts">
import TableHeader from '~/components/parts/TableHeader.vue';
import FixedLayout from '~/components/layouts/FixedLayout.vue';
definePageMeta({ middleware: 'admin-auth' });
useHead({
title: 'Admin — Login',
meta: [{ name: 'robots', content: 'noindex, nofollow', tagPriority: 'high' }]
});
const auth = useAuth();
const username = ref('');
const password = ref('');
const error = ref('');
const loading = ref(false);
const onSubmit = async () => {
if (loading.value) return;
error.value = '';
loading.value = true;
try {
await auth.login(username.value, password.value);
await navigateTo('/admin');
} catch (e: any) {
error.value = e?.status === 401
? $t('admin.login.error_invalid')
: $t('admin.login.error_generic');
password.value = '';
} finally {
loading.value = false;
}
};
</script>
<template>
<div class="no-sprite">
<TableHeader />
</div>
<FixedLayout>
<Container>
<h1 class="admin-title">&gt; {{ $t('admin.login.title') }}</h1>
<p class="admin-prompt">{{ $t('admin.login.prompt') }}</p>
<form class="login-form" @submit.prevent="onSubmit">
<div class="field">
<label for="admin-user">{{ $t('admin.login.username_label') }}:</label>
<input
id="admin-user"
v-model="username"
type="text"
name="username"
autocomplete="username"
spellcheck="false"
:disabled="loading"
required
/>
</div>
<div class="field">
<label for="admin-pass">{{ $t('admin.login.password_label') }}:</label>
<input
id="admin-pass"
v-model="password"
type="password"
name="password"
autocomplete="current-password"
:disabled="loading"
required
/>
</div>
<p v-if="error" class="login-error" role="alert">✗ {{ error }}</p>
<button type="submit" class="login-btn" :disabled="loading">
<span v-if="!loading">&gt; {{ $t('admin.login.submit') }}</span>
<span v-else class="blink">{{ $t('admin.login.submitting') }}</span>
</button>
</form>
</Container>
</FixedLayout>
<Footer />
</template>
<style lang="scss" scoped>
.admin-title {
font-family: 'Hurmit', monospace;
margin-bottom: 8px;
}
.admin-prompt {
font-size: 0.92rem;
color: var(--color-text);
opacity: 0.75;
margin-bottom: 24px;
}
.login-form {
max-width: 420px;
}
.field {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
label {
font-family: 'Hurmit', monospace;
font-size: 0.9rem;
color: var(--color-link);
min-width: 80px;
text-align: right;
}
input {
flex: 1;
font-family: 'Hurmit', monospace;
font-size: 0.95rem;
color: var(--color-text);
background: var(--color-background);
border: 1px solid var(--color-border-color);
padding: 8px 10px;
outline: none;
min-width: 0;
&:focus {
border-color: var(--color-link);
box-shadow: 2px -2px 0 0 var(--color-frame-glow);
}
}
}
.login-error {
font-family: 'Hurmit', monospace;
font-size: 0.85rem;
color: #e05c5c;
margin: 4px 0 12px 90px;
}
.login-btn {
margin-left: 90px;
font-family: 'Hurmit', monospace;
font-size: 0.9rem;
color: var(--color-background);
background: var(--color-link);
border: none;
padding: 8px 18px;
cursor: pointer;
transition: all 120ms steps(3);
&:hover:not(:disabled) {
background: var(--color-text);
}
&:disabled {
opacity: 0.6;
cursor: wait;
}
}
.blink {
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0; }
}
@media screen and (max-width: 600px) {
.field {
flex-direction: column;
align-items: stretch;
gap: 4px;
label {
text-align: left;
min-width: 0;
}
}
.login-error,
.login-btn {
margin-left: 0;
}
}
</style>