Benard John
feat: implement feedback API endpoint and add new frontend views for status, legal, and documentation pages.
2a72aeb | <template> | |
| <div class="app-shell"> | |
| <!-- Top Bar --> | |
| <header class="app-header"> | |
| <div class="flag-stripes" aria-hidden="true"> | |
| <span class="stripe black" /> | |
| <span class="stripe fimbriation" /> | |
| <span class="stripe red" /> | |
| <span class="stripe fimbriation" /> | |
| <span class="stripe green" /> | |
| </div> | |
| <div class="header-inner"> | |
| <router-link to="/" class="brand"> | |
| <div class="brand-mark" aria-hidden="true"> | |
| <svg viewBox="0 0 48 56" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M4 6 L24 2 L44 6 L44 30 C44 42 35 50 24 54 C13 50 4 42 4 30 Z" | |
| stroke="currentColor" stroke-width="2" fill="none" /> | |
| <path d="M14 18 H34 M14 26 H34 M14 34 H28" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" /> | |
| <circle cx="24" cy="10" r="2" fill="currentColor" /> | |
| </svg> | |
| </div> | |
| <div class="brand-text"> | |
| <h1 class="brand-name">Ukweli</h1> | |
| <span class="brand-tagline">ukweli · truth</span> | |
| </div> | |
| </router-link> | |
| <nav class="main-nav" :class="{ 'mobile-open': mobileMenuOpen }"> | |
| <router-link | |
| v-for="item in navItems" | |
| :key="item.path" | |
| :to="item.path" | |
| class="nav-link" | |
| :class="{ active: route.path.startsWith(item.path) && (item.path !== '/' || route.path === '/') }" | |
| @click="mobileMenuOpen = false" | |
| > | |
| <n-icon size="18" class="nav-icon"> | |
| <component :is="item.icon" /> | |
| </n-icon> | |
| <span class="nav-label">{{ item.label }}</span> | |
| </router-link> | |
| </nav> | |
| <div class="header-actions"> | |
| <RateLimitPill /> | |
| <n-tooltip placement="bottom"> | |
| <template #trigger> | |
| <button class="icon-btn" @click="themeStore.toggle()" :aria-label="`Switch to ${themeStore.theme === 'light' ? 'dark' : 'light'} mode`"> | |
| <n-icon size="18"> | |
| <sunny-outline v-if="themeStore.theme === 'dark'" /> | |
| <moon-outline v-else /> | |
| </n-icon> | |
| </button> | |
| </template> | |
| {{ themeStore.theme === 'dark' ? 'Light mode' : 'Dark mode' }} | |
| </n-tooltip> | |
| <UserMenu v-if="authStore.isAuthenticated" /> | |
| <n-button | |
| v-else | |
| size="small" | |
| type="primary" | |
| @click="authStore.openAuthModal()" | |
| class="signin-btn" | |
| > | |
| Sign in | |
| </n-button> | |
| <button | |
| class="icon-btn mobile-menu-btn" | |
| @click="mobileMenuOpen = !mobileMenuOpen" | |
| aria-label="Menu" | |
| > | |
| <n-icon size="20"> | |
| <close-outline v-if="mobileMenuOpen" /> | |
| <menu-outline v-else /> | |
| </n-icon> | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="app-main"> | |
| <router-view v-slot="{ Component, route }"> | |
| <transition name="fade" mode="out-in"> | |
| <component :is="Component" :key="route.path" /> | |
| </transition> | |
| </router-view> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="app-footer"> | |
| <div class="footer-inner"> | |
| <div class="footer-brand"> | |
| <div class="brand-mark small" aria-hidden="true"> | |
| <svg viewBox="0 0 48 56" fill="none"> | |
| <path d="M4 6 L24 2 L44 6 L44 30 C44 42 35 50 24 54 C13 50 4 42 4 30 Z" | |
| stroke="currentColor" stroke-width="2" fill="none" /> | |
| <path d="M14 18 H34 M14 26 H34 M14 34 H28" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" /> | |
| </svg> | |
| </div> | |
| <p class="footer-tag"> | |
| <strong>Ukweli</strong> — Democratizing access to Kenya's public | |
| audit information. Built in the spirit of Article 35. | |
| </p> | |
| </div> | |
| <div class="footer-cols"> | |
| <div class="footer-col"> | |
| <h5>Resources</h5> | |
| <router-link to="/about">About Ukweli</router-link> | |
| <router-link to="/methodology">Methodology</router-link> | |
| <router-link to="/coverage">Data Coverage</router-link> | |
| <router-link to="/changelog">Changelog</router-link> | |
| <router-link to="/status">System Status</router-link> | |
| </div> | |
| <div class="footer-col"> | |
| <h5>Legal</h5> | |
| <router-link to="/terms">Terms of Service</router-link> | |
| <router-link to="/privacy">Privacy Policy</router-link> | |
| <router-link to="/acceptable-use">Acceptable Use</router-link> | |
| <router-link to="/disclaimer">Disclaimer</router-link> | |
| </div> | |
| <div class="footer-col"> | |
| <h5>External</h5> | |
| <a href="https://www.oagkenya.go.ke" target="_blank" rel="noopener">OAG Kenya</a> | |
| <a href="https://www.treasury.go.ke" target="_blank" rel="noopener">National Treasury</a> | |
| <p style="font-size: 11px; margin-top: 8px; color: var(--text-muted);">Built in the spirit of Article 35.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="footer-bottom"> | |
| <div class="flag-stripes small" aria-hidden="true"> | |
| <span class="stripe black" /> | |
| <span class="stripe fimbriation" /> | |
| <span class="stripe red" /> | |
| <span class="stripe fimbriation" /> | |
| <span class="stripe green" /> | |
| </div> | |
| <p>Built for transparency. Harambee.</p> | |
| </div> | |
| </footer> | |
| <AuthModal /> | |
| </div> | |
| </template> | |
| <script setup lang="ts"> | |
| import { ref, onMounted } from 'vue' | |
| import { useRoute } from 'vue-router' | |
| import { useAuthStore } from '@/stores/auth' | |
| import { useThemeStore } from '@/stores/theme' | |
| import { | |
| SearchOutline, | |
| ChatbubblesOutline, | |
| CompassOutline, | |
| ShareSocialOutline, | |
| InformationCircleOutline, | |
| SunnyOutline, | |
| MoonOutline, | |
| MenuOutline, | |
| CloseOutline, | |
| KeyOutline, | |
| } from '@vicons/ionicons5' | |
| import AuthModal from './AuthModal.vue' | |
| import RateLimitPill from './RateLimitPill.vue' | |
| import UserMenu from './UserMenu.vue' | |
| const route = useRoute() | |
| const authStore = useAuthStore() | |
| const themeStore = useThemeStore() | |
| const mobileMenuOpen = ref(false) | |
| const navItems = [ | |
| { path: '/', label: 'Search', icon: SearchOutline }, | |
| { path: '/chat', label: 'Ask', icon: ChatbubblesOutline }, | |
| { path: '/explore', label: 'Explore', icon: CompassOutline }, | |
| { path: '/graph', label: 'Graph', icon: ShareSocialOutline }, | |
| { path: '/about', label: 'About', icon: InformationCircleOutline }, | |
| { path: '/api-keys', label: 'API Keys', icon: KeyOutline }, | |
| ] | |
| onMounted(() => { | |
| authStore.initialize() | |
| }) | |
| </script> | |
| <style scoped lang="scss"> | |
| .app-shell { | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| background: var(--bg-page); | |
| } | |
| // ============================================================================= | |
| // HEADER | |
| // ============================================================================= | |
| .app-header { | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| background: var(--bg-card); | |
| border-bottom: 1px solid var(--border); | |
| backdrop-filter: blur(12px); | |
| } | |
| .flag-stripes { | |
| display: flex; | |
| width: 100%; | |
| height: 4px; | |
| .stripe { flex: 1; transition: filter 0.3s var(--ease-out); } | |
| .stripe.black { background: var(--ke-black); flex: 1.4; } | |
| .stripe.fimbriation { background: var(--ke-white); flex: 0.15; } | |
| .stripe.red { background: var(--ke-red); flex: 1.2; } | |
| .stripe.green { background: var(--ke-green); flex: 1.4; } | |
| &.small { | |
| height: 2px; | |
| max-width: 80px; | |
| border-radius: 2px; | |
| overflow: hidden; | |
| } | |
| } | |
| .header-inner { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 0 28px; | |
| height: 68px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 16px; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| text-decoration: none; | |
| color: var(--text-primary); | |
| flex-shrink: 0; | |
| transition: opacity 0.2s var(--ease-out); | |
| &:hover { opacity: 0.85; } | |
| } | |
| .brand-mark { | |
| width: 38px; | |
| height: 44px; | |
| color: var(--primary); | |
| &.small { width: 28px; height: 32px; } | |
| } | |
| .brand-text { | |
| display: flex; | |
| flex-direction: column; | |
| line-height: 1.1; | |
| } | |
| .brand-name { | |
| font-family: var(--font-display); | |
| font-size: 22px; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| letter-spacing: -0.02em; | |
| margin: 0; | |
| } | |
| .brand-tagline { | |
| font-size: 10px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.12em; | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| margin-top: 2px; | |
| } | |
| .main-nav { | |
| display: flex; | |
| gap: 2px; | |
| background: var(--bg-elevated); | |
| padding: 4px; | |
| border-radius: 999px; | |
| border: 1px solid var(--border); | |
| } | |
| .nav-link { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 7px 14px; | |
| border-radius: 999px; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| font-weight: 600; | |
| font-size: 13px; | |
| transition: all 0.2s var(--ease-out); | |
| white-space: nowrap; | |
| &:hover { | |
| color: var(--text-primary); | |
| background: var(--bg-card); | |
| } | |
| &.active { | |
| background: var(--primary); | |
| color: white; | |
| box-shadow: 0 2px 6px rgba(0, 102, 0, 0.25); | |
| } | |
| } | |
| .nav-label { letter-spacing: 0.01em; } | |
| .header-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| flex-shrink: 0; | |
| } | |
| .icon-btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 10px; | |
| background: transparent; | |
| border: 1px solid transparent; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: all 0.2s var(--ease-out); | |
| &:hover { | |
| background: var(--bg-elevated); | |
| color: var(--text-primary); | |
| } | |
| } | |
| .signin-btn { | |
| font-weight: 700; | |
| letter-spacing: 0.02em; | |
| border-radius: 10px; | |
| } | |
| .mobile-menu-btn { display: none; } | |
| // ============================================================================= | |
| // MAIN | |
| // ============================================================================= | |
| .app-main { | |
| flex: 1; | |
| max-width: 1400px; | |
| width: 100%; | |
| margin: 0 auto; | |
| padding: 32px 28px; | |
| } | |
| // ============================================================================= | |
| // FOOTER | |
| // ============================================================================= | |
| .app-footer { | |
| background: var(--bg-card); | |
| border-top: 1px solid var(--border); | |
| margin-top: 64px; | |
| } | |
| .footer-inner { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 56px 28px 32px; | |
| display: grid; | |
| grid-template-columns: 1.2fr 2fr; | |
| gap: 56px; | |
| } | |
| .footer-brand { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| .footer-tag { | |
| font-size: 14px; | |
| line-height: 1.6; | |
| color: var(--text-secondary); | |
| max-width: 340px; | |
| strong { | |
| font-family: var(--font-display); | |
| color: var(--text-primary); | |
| font-size: 16px; | |
| } | |
| } | |
| .footer-cols { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 32px; | |
| } | |
| .footer-col { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| h5 { | |
| font-family: var(--font-body); | |
| font-size: 11px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.12em; | |
| color: var(--text-muted); | |
| font-weight: 700; | |
| margin-bottom: 8px; | |
| } | |
| a, p { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| line-height: 1.7; | |
| transition: color 0.2s var(--ease-out); | |
| } | |
| a:hover { color: var(--primary); } | |
| } | |
| .footer-bottom { | |
| border-top: 1px solid var(--border); | |
| padding: 24px 28px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| gap: 16px; | |
| p { | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| font-style: italic; | |
| } | |
| } | |
| // ============================================================================= | |
| // MOBILE | |
| // ============================================================================= | |
| @media (max-width: 768px) { | |
| .header-inner { | |
| padding: 0 16px; | |
| height: 60px; | |
| } | |
| .brand-name { font-size: 18px; } | |
| .brand-tagline { display: none; } | |
| .main-nav { | |
| position: fixed; | |
| top: 64px; | |
| left: 0; | |
| right: 0; | |
| background: var(--bg-card); | |
| flex-direction: column; | |
| padding: 16px; | |
| gap: 4px; | |
| border-radius: 0; | |
| border: none; | |
| border-bottom: 1px solid var(--border); | |
| box-shadow: var(--shadow-lg); | |
| transform: translateY(-110%); | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: all 0.3s var(--ease-out); | |
| z-index: 99; | |
| &.mobile-open { | |
| transform: translateY(0); | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .nav-link { | |
| width: 100%; | |
| border-radius: var(--radius-md); | |
| padding: 12px 16px; | |
| justify-content: flex-start; | |
| } | |
| } | |
| .mobile-menu-btn { display: inline-flex; } | |
| .signin-btn { display: none; } | |
| .app-main { padding: 20px 16px; } | |
| .footer-inner { | |
| grid-template-columns: 1fr; | |
| padding: 40px 16px 24px; | |
| gap: 32px; | |
| } | |
| .footer-cols { grid-template-columns: 1fr; gap: 24px; } | |
| .footer-bottom { flex-direction: column; align-items: flex-start; } | |
| } | |
| </style> | |