stockadvisor / ui /src /components /AppLayout.vue
Bibhu Mishra
feat: flashy mobile bottom nav with per-route colored bubbles
268a66c
Raw
History Blame Contribute Delete
12.5 kB
<script setup lang="ts">
import { ref } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { useNotificationsStore } from '../stores/notifications'
import NotificationBell from './NotificationBell.vue'
const auth = useAuthStore()
const router = useRouter()
const sidebarOpen = ref(true)
function logout() {
auth.logout()
router.push('/login')
}
</script>
<template>
<div class="app-shell">
<!-- Sidebar -->
<aside :class="['sidebar', { collapsed: !sidebarOpen }]">
<div class="logo">
<svg v-if="sidebarOpen" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 38" class="logo-full">
<defs>
<linearGradient id="ibg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#1e40af"/>
</linearGradient>
</defs>
<rect width="38" height="38" rx="9" fill="url(#ibg)"/>
<line x1="10" y1="30" x2="19" y2="10" stroke="white" stroke-width="2.8" stroke-linecap="round"/>
<line x1="19" y1="10" x2="28" y2="30" stroke="white" stroke-width="2.8" stroke-linecap="round"/>
<line x1="14" y1="22" x2="24" y2="22" stroke="white" stroke-width="2.3" stroke-linecap="round"/>
<circle cx="19" cy="10" r="2.8" fill="#f59e0b"/>
<line x1="19" y1="7" x2="19" y2="4" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/>
<polygon points="19,2 17,5.5 21,5.5" fill="#f59e0b"/>
<text x="46" y="15" font-family="Inter,sans-serif" font-size="12" font-weight="800" fill="white" letter-spacing="-0.2">Alpha</text>
<text x="46" y="29" font-family="Inter,sans-serif" font-size="11" font-weight="400" fill="rgba(255,255,255,0.65)" letter-spacing="1.8">FORGE</text>
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 38 38" class="logo-icon">
<defs>
<linearGradient id="ibg2" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#1e40af"/>
</linearGradient>
</defs>
<rect width="38" height="38" rx="9" fill="url(#ibg2)"/>
<line x1="10" y1="30" x2="19" y2="10" stroke="white" stroke-width="2.8" stroke-linecap="round"/>
<line x1="19" y1="10" x2="28" y2="30" stroke="white" stroke-width="2.8" stroke-linecap="round"/>
<line x1="14" y1="22" x2="24" y2="22" stroke="white" stroke-width="2.3" stroke-linecap="round"/>
<circle cx="19" cy="10" r="2.8" fill="#f59e0b"/>
<line x1="19" y1="7" x2="19" y2="4" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/>
<polygon points="19,2 17,5.5 21,5.5" fill="#f59e0b"/>
</svg>
</div>
<nav class="sidebar-nav">
<RouterLink to="/" class="nav-item"><span class="icon">πŸ“Š</span><span v-if="sidebarOpen">Dashboard</span></RouterLink>
<RouterLink to="/transactions" class="nav-item"><span class="icon">πŸ’±</span><span v-if="sidebarOpen">Transactions</span></RouterLink>
<RouterLink to="/reports" class="nav-item"><span class="icon">πŸ“‹</span><span v-if="sidebarOpen">Reports</span></RouterLink>
<RouterLink to="/strategies" class="nav-item"><span class="icon">🧠</span><span v-if="sidebarOpen">Strategies</span></RouterLink>
<RouterLink v-if="auth.isAdmin" to="/admin" class="nav-item"><span class="icon">βš™οΈ</span><span v-if="sidebarOpen">Admin</span></RouterLink>
</nav>
<button class="toggle-btn" @click="sidebarOpen = !sidebarOpen">
{{ sidebarOpen ? 'β—€' : 'β–Ά' }}
</button>
</aside>
<!-- Main -->
<div class="main-area">
<header class="topbar">
<div class="topbar-left">
<span class="mobile-brand">AlphaForge</span>
<h1 class="page-title"></h1>
</div>
<div class="topbar-right">
<NotificationBell />
<RouterLink to="/profile" class="user-info">
<span class="user-avatar">{{ (auth.user?.name ?? '?')[0].toUpperCase() }}</span>
<span class="user-name">{{ auth.user?.name }} <em>({{ auth.user?.role }})</em></span>
</RouterLink>
<button class="logout-btn" @click="logout">Logout</button>
</div>
</header>
<main class="content">
<slot />
</main>
</div>
<!-- Mobile bottom nav -->
<nav class="bottom-nav">
<RouterLink to="/" class="bn-item bn-dashboard"><span class="bn-bubble"><span class="bn-icon">πŸ“Š</span></span><span class="bn-label">Dashboard</span></RouterLink>
<RouterLink to="/transactions" class="bn-item bn-trades" ><span class="bn-bubble"><span class="bn-icon">πŸ’±</span></span><span class="bn-label">Trades</span></RouterLink>
<RouterLink to="/reports" class="bn-item bn-reports" ><span class="bn-bubble"><span class="bn-icon">πŸ“‹</span></span><span class="bn-label">Reports</span></RouterLink>
<RouterLink to="/strategies" class="bn-item bn-strategy" ><span class="bn-bubble"><span class="bn-icon">🧠</span></span><span class="bn-label">Strategy</span></RouterLink>
<RouterLink v-if="auth.isAdmin" to="/admin" class="bn-item bn-admin"><span class="bn-bubble"><span class="bn-icon">βš™οΈ</span></span><span class="bn-label">Admin</span></RouterLink>
<RouterLink to="/profile" class="bn-item bn-profile" ><span class="bn-bubble"><span class="bn-icon">πŸ‘€</span></span><span class="bn-label">Profile</span></RouterLink>
</nav>
</div>
</template>
<style scoped>
.app-shell { display: flex; height: 100vh; overflow: hidden; }
/* ── Sidebar (desktop) ─────────────────────────────────────────── */
.sidebar {
width: 220px; min-width: 220px; color: #fff;
background: linear-gradient(180deg, #162d4e 0%, #1e3a5f 60%, #1a3457 100%);
display: flex; flex-direction: column; transition: width 0.2s, min-width 0.2s;
box-shadow: 2px 0 16px rgba(0,0,0,0.12);
}
.sidebar.collapsed { width: 60px; min-width: 60px; }
.logo {
padding: 12px 12px 10px; border-bottom: 1px solid rgba(255,255,255,0.1);
white-space: nowrap; overflow: hidden; display: flex; align-items: center;
}
.logo-full { height: 38px; width: auto; display: block; }
.logo-icon { height: 36px; width: 36px; display: block; }
.sidebar-nav { flex: 1; padding-top: 12px; }
.nav-item {
display: flex; align-items: center; gap: 12px;
padding: 12px 16px; color: rgba(255,255,255,0.8); text-decoration: none;
white-space: nowrap; overflow: hidden; transition: background 0.15s;
}
.nav-item:hover, .nav-item.router-link-exact-active {
background: rgba(255,255,255,0.15); color: #fff;
}
.icon { font-size: 1.1rem; flex-shrink: 0; }
.toggle-btn {
margin: 12px; padding: 8px; background: rgba(255,255,255,0.1);
border: none; color: #fff; cursor: pointer; border-radius: 6px;
}
/* ── Main area ─────────────────────────────────────────────────── */
.main-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.topbar {
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; height: 56px;
background: rgba(255,255,255,0.8);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border-bottom: 1px solid rgba(255,255,255,0.55);
box-shadow: 0 1px 8px rgba(0,0,0,0.05);
flex-shrink: 0;
}
.mobile-brand { display: none; font-weight: 800; color: #1e3a5f; font-size: 1rem; }
.topbar-right { display: flex; align-items: center; gap: 16px; }
.user-info {
display: flex; align-items: center; gap: 8px;
font-size: 0.875rem; color: #374151; text-decoration: none;
padding: 4px 10px; border-radius: 8px; transition: background 0.15s;
}
.user-info:hover { background: #f3f4f6; }
.user-info em { color: #9ca3af; font-style: normal; }
.user-avatar {
width: 32px; height: 32px; border-radius: 50%;
background: #1e3a5f; color: #fff;
font-size: 0.8rem; font-weight: 700;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.logout-btn {
padding: 6px 14px; background: #ef4444; color: #fff; border: none;
border-radius: 6px; cursor: pointer; font-size: 0.875rem;
}
.logout-btn:hover { background: #dc2626; }
.content { flex: 1; overflow-y: auto; padding: 24px; background: transparent; }
/* ── Mobile bottom nav (hidden on desktop) ─────────────────────── */
.bottom-nav { display: none; }
/* ── Mobile breakpoint ─────────────────────────────────────────── */
@media (max-width: 767px) {
.sidebar { display: none; }
.mobile-brand { display: block; }
.page-title { display: none; }
.user-name { display: none; }
.topbar { padding: 0 14px; height: 52px; }
.topbar-right { gap: 10px; }
.logout-btn { padding: 6px 10px; font-size: 0.8rem; }
.content { padding: 14px 12px; padding-bottom: 88px; }
.bottom-nav {
display: flex;
position: fixed; bottom: 0; left: 0; right: 0;
background: rgba(6, 14, 34, 0.94);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-top: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 -6px 32px rgba(0,0,0,0.3);
z-index: 200;
padding-left: max(env(safe-area-inset-left, 0px), 6px);
padding-right: max(env(safe-area-inset-right, 0px), 6px);
padding-bottom: max(env(safe-area-inset-bottom, 0px), 6px);
}
.bn-item {
flex: 1; display: flex; flex-direction: column; align-items: center;
padding: 8px 2px 6px; color: rgba(255,255,255,0.28);
text-decoration: none; min-width: 0;
transition: color 0.22s ease;
-webkit-tap-highlight-color: transparent;
}
/* Floating bubble that holds the emoji */
.bn-bubble {
width: 46px; height: 34px;
display: flex; align-items: center; justify-content: center;
border-radius: 18px;
transition:
background 0.25s ease,
box-shadow 0.25s ease,
transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.bn-icon { font-size: 1.35rem; line-height: 1; display: block; }
.bn-label {
font-size: 0.58rem; margin-top: 3px; font-weight: 600;
letter-spacing: 0.04em; transition: color 0.22s;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
max-width: 100%; text-align: center;
}
/* ── Active state: lift + glow, per-route accent ── */
.bn-item.router-link-exact-active,
.bn-item.router-link-active { color: #fff; }
/* Dashboard β€” blue */
.bn-dashboard.router-link-exact-active .bn-bubble {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
box-shadow: 0 4px 20px rgba(59,130,246,0.55), 0 0 8px rgba(59,130,246,0.3);
transform: translateY(-6px) scale(1.1);
}
/* Trades β€” emerald */
.bn-trades.router-link-active .bn-bubble {
background: linear-gradient(135deg, #10b981, #059669);
box-shadow: 0 4px 20px rgba(16,185,129,0.55), 0 0 8px rgba(16,185,129,0.3);
transform: translateY(-6px) scale(1.1);
}
/* Reports β€” amber */
.bn-reports.router-link-active .bn-bubble {
background: linear-gradient(135deg, #f59e0b, #d97706);
box-shadow: 0 4px 20px rgba(245,158,11,0.55), 0 0 8px rgba(245,158,11,0.3);
transform: translateY(-6px) scale(1.1);
}
/* Strategy β€” violet */
.bn-strategy.router-link-active .bn-bubble {
background: linear-gradient(135deg, #8b5cf6, #6d28d9);
box-shadow: 0 4px 20px rgba(139,92,246,0.55), 0 0 8px rgba(139,92,246,0.3);
transform: translateY(-6px) scale(1.1);
}
/* Admin β€” slate */
.bn-admin.router-link-active .bn-bubble {
background: linear-gradient(135deg, #64748b, #475569);
box-shadow: 0 4px 20px rgba(100,116,139,0.55), 0 0 8px rgba(100,116,139,0.3);
transform: translateY(-6px) scale(1.1);
}
/* Profile β€” rose */
.bn-profile.router-link-active .bn-bubble {
background: linear-gradient(135deg, #f43f5e, #be123c);
box-shadow: 0 4px 20px rgba(244,63,94,0.55), 0 0 8px rgba(244,63,94,0.3);
transform: translateY(-6px) scale(1.1);
}
/* Tap press feel */
.bn-item:active .bn-bubble { transform: translateY(-2px) scale(0.96); }
}
</style>