all repos — stealth-developers @ f6f5a6423298aebc9c56231c1a1ec8c68895f891

feat(frontend): login page
vi did:web:vt3e.cat
Sat, 09 May 2026 04:16:45 +0100
commit

f6f5a6423298aebc9c56231c1a1ec8c68895f891

parent

b525b3ee0ba65406eb1105c23533d3aae33d136b

M frontend/src/App.vuefrontend/src/App.vue

@@ -48,8 +48,8 @@ <component :is="Component" />

</KeepAlive> </RouterView> </main> - <div class="mobile-header"> - <button class="hamburger" @click="toggleSidebar"> + <div class="mobile-header" @click="toggleSidebar"> + <button class="hamburger" @click.stop="toggleSidebar"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <line x1="3" y1="12" x2="21" y2="12"></line> <line x1="3" y1="6" x2="21" y2="6"></line>

@@ -215,28 +215,32 @@ }

aside.sidebar { position: fixed; - top: 0; - left: -250px; - width: 250px; - height: 100dvh; - background-color: hsla(var(--crust)); + bottom: 1rem; + left: 1rem; + transform: translateY(10rem); + width: calc(100% - 2rem); z-index: 20; padding: 0.5rem; + border-radius: 1rem; + opacity: 0; justify-content: flex-end; + pointer-events: none; &.is-open { - left: 0; + opacity: 1; + pointer-events: all; + transform: translateY(0); + z-index: 20; } .user { background-color: hsla(var(--surface0)); + justify-content: center; + align-items: center; } .links { - border-bottom: 1px solid hsla(var(--surface1)); - padding-bottom: 0.5rem; - a { text-align: center; padding: 1rem 1rem;
M frontend/src/router/index.tsfrontend/src/router/index.ts

@@ -1,5 +1,6 @@

import { createRouter, createWebHistory } from "vue-router"; -import HomeView from "../views/HomeView.vue"; +import HomeView from "@/views/HomeView.vue"; +import { useAuthStore } from "@/stores/auth"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL),

@@ -19,7 +20,21 @@ path: "/tickets/:id",

name: "Ticket Details", component: () => import("../views/TicketDetailView.vue"), }, + { + path: "/login", + name: "Login", + component: () => import("../views/LoginView.vue"), + }, ], +}); + +router.beforeEach((to, _from, next) => { + const isAuthenticated = useAuthStore().isAuthenticated; + if (to.name !== "Login" && !isAuthenticated) { + next({ name: "Login" }); + } else { + next(); + } }); export default router;
M frontend/src/stores/auth.tsfrontend/src/stores/auth.ts

@@ -1,25 +1,23 @@

-import { ref } from 'vue' -import { defineStore } from 'pinia' -import type { User } from '@/types' +import { ref } from "vue"; +import { defineStore } from "pinia"; +import type { User } from "@/types"; -export const useAuthStore = defineStore('auth', () => { - const isAuthenticated = ref(false) - const user = ref<User | null>(null) +export const useAuthStore = defineStore("auth", () => { + const isAuthenticated = ref(false); + const user = ref<User | null>(null); async function getUser() { try { - const response = await fetch('/api/me') - const data = await response.json() - user.value = data.profile - isAuthenticated.value = true - } catch { - window.location.href = '/api/login' - } + const response = await fetch("/api/me"); + const data = await response.json(); + user.value = data.profile; + isAuthenticated.value = true; + } catch {} } return { isAuthenticated, user, getUser, - } -}) + }; +});
A frontend/src/views/LoginView.vue

@@ -0,0 +1,20 @@

+<template> + <h1>Login</h1> + <p>You must authenticate with Discord to continue.</p> + + <a href="/api/login"> + Login with Discord + </a> +</template> + +<style scoped> + a { + display: inline-block; + margin: 1rem 0; + padding: 0.75rem 1.5rem; + background-color: hsl(var(--accent)); + color: hsl(var(--on-accent)); + text-decoration: none; + border-radius: 5rem; + } +</style>