Refonte front (packages/web) : coquille applicative partagée (sidebar desktop + barre d'onglets mobile + PageHeader, langue/push/logout centralisés) pilotée par route.meta.layout ; design system (BaseButton/BaseBadge/SegmentedControl/EmptyState/ SkeletonRow, anneau de focus, icônes @lucide/vue) ; tri/filtre/recherche/pagination côté client via useListControls (+ synchro URL) sur sessions/worktrees/groupes ; section « À traiter », palette de commandes ⌘K, toasts. Aucune modif serveur/protocole/DB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
2.4 KiB
Vue
69 lines
2.4 KiB
Vue
<template>
|
|
<div class="flex items-center justify-center px-4">
|
|
<form
|
|
class="flex w-full max-w-sm flex-col gap-4 rounded-xl border border-zinc-800 bg-zinc-900/60 p-6"
|
|
@submit.prevent="onSubmit"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-2.5">
|
|
<img src="/icon.svg" alt="" class="h-9 w-9" />
|
|
<h1 class="text-lg font-semibold text-zinc-100">{{ t('common.appName') }}</h1>
|
|
</div>
|
|
<LanguageSwitcher />
|
|
</div>
|
|
<p class="text-sm text-zinc-400">{{ t('login.title') }}</p>
|
|
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
|
{{ t('login.tokenLabel') }}
|
|
<input
|
|
v-model="token"
|
|
type="password"
|
|
class="input font-mono"
|
|
:placeholder="t('login.tokenPlaceholder')"
|
|
autocomplete="current-password"
|
|
autofocus
|
|
required
|
|
/>
|
|
</label>
|
|
<p v-if="error" class="text-sm text-red-400">{{ error }}</p>
|
|
<BaseButton type="submit" variant="primary" :loading="submitting" :disabled="token.trim() === ''">
|
|
{{ submitting ? t('login.submitting') : t('login.submit') }}
|
|
</BaseButton>
|
|
</form>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useAuthStore } from '../stores/auth';
|
|
import { ApiError } from '../lib/api';
|
|
import LanguageSwitcher from '../components/LanguageSwitcher.vue';
|
|
import BaseButton from '../components/ui/BaseButton.vue';
|
|
|
|
const { t } = useI18n();
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const auth = useAuthStore();
|
|
|
|
const token = ref('');
|
|
const error = ref<string | null>(null);
|
|
const submitting = ref(false);
|
|
|
|
async function onSubmit(): Promise<void> {
|
|
submitting.value = true;
|
|
error.value = null;
|
|
try {
|
|
await auth.login(token.value.trim());
|
|
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/';
|
|
await router.replace(redirect);
|
|
} catch (err) {
|
|
if (err instanceof ApiError && err.status === 401) error.value = t('login.invalidToken');
|
|
else if (err instanceof ApiError && err.status === 429) error.value = t('login.rateLimited');
|
|
else error.value = t('login.genericError', { status: err instanceof ApiError ? err.status : '?' });
|
|
} finally {
|
|
submitting.value = false;
|
|
}
|
|
}
|
|
</script>
|