feat(frontend): mobile sidebar
vi did:web:vt3e.cat
Sat, 09 May 2026 03:54:48 +0100
4 files changed,
169 insertions(+),
37 deletions(-)
M
frontend/src/App.vue
→
frontend/src/App.vue
@@ -1,14 +1,25 @@
<script setup lang="ts"> -import { RouterView, RouterLink } from 'vue-router' +import { RouterView, RouterLink, useRoute } from 'vue-router' import { useAuthStore } from './stores/auth' -import { computed, onMounted } from 'vue' +import { computed, onMounted, ref } from 'vue' import { avatar } from './utils/discord' const authStore = useAuthStore() +const route = useRoute() const user = computed(() => authStore.user) +const sidebarOpen = ref(false) + +const toggleSidebar = () => { + sidebarOpen.value = !sidebarOpen.value +} + +const closeSidebar = () => { + sidebarOpen.value = false +} + onMounted(async () => { await authStore.getUser() })@@ -16,8 +27,9 @@ </script>
<template> <div class="layout"> - <aside class="sidebar"> - <div class="links"> + <div class="sidebar-overlay" :class="{ 'is-open': sidebarOpen }" @click="closeSidebar"></div> + <aside class="sidebar" :class="{ 'is-open': sidebarOpen }"> + <div class="links" @click="closeSidebar"> <RouterLink to="/">Home</RouterLink> <RouterLink to="/tickets">Tickets</RouterLink> </div>@@ -36,6 +48,16 @@ <component :is="Component" />
</KeepAlive> </RouterView> </main> + <div class="mobile-header"> + <button class="hamburger" @click="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> + <line x1="3" y1="18" x2="21" y2="18"></line> + </svg> + </button> + <span class="title">{{ route.name }}</span> + </div> </div> </template>@@ -50,6 +72,13 @@ height: 100vh;
padding: 0.5rem; gap: 0.5rem; + .mobile-header { + display: none; + } + .sidebar-overlay { + display: none; + } + aside.sidebar { flex: 0 0 auto; width: 200px;@@ -97,6 +126,7 @@ border-radius: 2rem;
background-color: hsla(var(--mantle)); gap: 0.5rem; font-weight: 550; + overflow: hidden; .profile-picture { flex: 0 0 auto;@@ -111,6 +141,12 @@ object-fit: cover;
border-radius: 50%; } } + + .username { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } }@@ -120,6 +156,103 @@ background-color: hsla(var(--mantle));
border-radius: 2rem; height: 100%; overflow-y: auto; + } + + @media (max-width: 768px) { + flex-direction: column; + + .mobile-header { + display: flex; + align-items: center; + gap: 1ch; + padding: 0.5rem; + background-color: hsla(var(--surface0)); + border-radius: 2rem; + + .hamburger { + background: none; + border: none; + color: hsla(var(--text)); + cursor: pointer; + padding: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + + &:hover { + background-color: hsla(var(--surface2)); + } + &:active { + background-color: hsla(var(--surface1)); + } + } + + .title { + font-weight: 700; + font-size: 1.25rem; + } + } + + .sidebar-overlay { + display: block; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(8px); + z-index: 10; + opacity: 0; + pointer-events: none; + + &.is-open { + opacity: 1; + pointer-events: auto; + } + } + + aside.sidebar { + position: fixed; + top: 0; + left: -250px; + width: 250px; + height: 100vh; + background-color: hsla(var(--crust)); + z-index: 20; + padding: 0.5rem; + + justify-content: flex-end; + + &.is-open { + left: 0; + } + + .user { + background-color: hsla(var(--surface0)); + } + + .links { + border-bottom: 1px solid hsla(var(--surface1)); + padding-bottom: 0.5rem; + + a { + text-align: center; + padding: 1rem 1rem; + border-radius: 5rem; + background-color: hsla(var(--mantle)); + text-decoration: none; + color: hsla(var(--subtext1)); + font-weight: 700; + font-size: 1.25rem; + } + } + } + + main { + border-radius: 1.5rem; + } } } </style>
M
frontend/src/css/index.css
→
frontend/src/css/index.css
@@ -27,7 +27,7 @@ --transition-properties:
margin, grid-template-rows, grid-template-columns, stroke-dasharray, stroke-dashoffset, outline-color, outline-offset, color, background-color, box-shadow, outline, border-color, border-radius, font-weight, opacity, transform, backdrop-filter, text-decoration-color, filter, - scale, height; + scale, height, left; --transition-duration: 0.2s; --transition: var(--transition-duration) var(--ease); }
M
frontend/src/router/index.ts
→
frontend/src/router/index.ts
@@ -1,25 +1,25 @@
-import { createRouter, createWebHistory } from 'vue-router' -import HomeView from '../views/HomeView.vue' +import { createRouter, createWebHistory } from "vue-router"; +import HomeView from "../views/HomeView.vue"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { - path: '/', - name: 'home', + path: "/", + name: "Home", component: HomeView, }, { - path: '/tickets', - name: 'tickets', - component: () => import('../views/TicketsView.vue'), + path: "/tickets", + name: "Tickets", + component: () => import("../views/TicketsView.vue"), }, { - path: '/tickets/:id', - name: 'ticket-detail', - component: () => import('../views/TicketDetailView.vue'), + path: "/tickets/:id", + name: "Ticket Details", + component: () => import("../views/TicketDetailView.vue"), }, ], -}) +}); -export default router +export default router;
M
frontend/vite.config.ts
→
frontend/vite.config.ts
@@ -1,24 +1,23 @@
-import { fileURLToPath, URL } from 'node:url' +import { fileURLToPath, URL } from "node:url"; -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' -import vueDevTools from 'vite-plugin-vue-devtools' +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import vueDevTools from "vite-plugin-vue-devtools"; // https://vite.dev/config/ export default defineConfig({ - plugins: [vue(), vueDevTools()], - server: { - proxy: { - '/api': { - target: 'http://127.0.0.1:3000', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, ''), - }, - }, - }, - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - }, -}) + plugins: [vue(), vueDevTools()], + server: { + proxy: { + "/api": { + target: "http://127.0.0.1:3000", + changeOrigin: true, + }, + }, + }, + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, +});