Files
sales-trainer/frontend/src/App.vue
Macky c263cc81df feat(ui): app UX/UI redesign + marketing landing site
App (Vue SPA) — 100% presentational, global design-token rebuild in
style.css + polished App/Login/Chat/MyBoard/Personas/Setup/Training.
No logic/API/route/i18n changes (verified: build clean, 4/4 vitest,
independent review PASS 5/5).

website/ — new self-contained static marketing landing site (TH-primary
+ EN toggle, CSS chat mockup, Hallmark theme), verified responsive at
320-768 (no horizontal scroll). Pricing uses placeholders pending owner.

Docs: engineering-log 2026-08-19 + HANDOFF updated.

NOT pushed (push to main auto-deploys to EasyPanel production;
operator approval + JWT_SECRET check required before deploy).
2026-08-19 15:46:21 +07:00

162 lines
6.4 KiB
Vue

<template>
<div class="app">
<nav v-if="auth.user" class="topnav">
<router-link to="/" class="brand">
<span class="brand-mark"><Target :size="17" :stroke-width="2" /></span>
{{ i18n.t('app') }}
</router-link>
<div class="tabs">
<router-link v-if="auth.isAdmin" to="/" class="tab" :class="{ active: $route.path === '/' }">
{{ i18n.t('adminOverview') }}
</router-link>
<router-link to="/my/board" class="tab" :class="{ active: $route.path === '/my/board' }">
{{ i18n.t('myDashboard') }}
</router-link>
<router-link to="/training" class="tab" :class="{ active: $route.path === '/training' }">
{{ i18n.t('training') }}
</router-link>
</div>
<div class="nav-right">
<button @click="toggleLang" class="lang">{{ i18n.locale === 'th' ? 'EN' : 'TH' }}</button>
<div class="user-menu" ref="menuRef">
<button @click="menuOpen = !menuOpen" class="user-btn" :class="{ open: menuOpen }">
<UserCircle :size="20" :stroke-width="1.8" />
<span class="txt">{{ auth.user.username || auth.user.email || auth.user.name || auth.user.id }}</span>
<ChevronDown :size="14" :stroke-width="2" class="chev" :class="{ up: menuOpen }" />
</button>
<div v-if="menuOpen" class="dropdown">
<router-link to="/settings" class="item" @click="menuOpen = false">
<SettingsIcon :size="16" :stroke-width="1.8" /> {{ i18n.t('settings') }}
</router-link>
<button class="item" @click="logout"><LogOut :size="16" :stroke-width="1.8" /> {{ i18n.t('logout') }}</button>
</div>
</div>
</div>
</nav>
<main class="main">
<!-- :key forces each view to re-evaluate i18n.t() when locale changes -->
<router-view :key="i18n.locale" />
</main>
</div>
</template>
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Settings as SettingsIcon, LogOut, UserCircle, ChevronDown, Target } from 'lucide-vue-next'
import { auth } from './store/auth'
import { i18n } from './i18n'
const router = useRouter()
const menuOpen = ref(false)
const menuRef = ref(null)
function toggleLang() {
i18n.set(i18n.locale === 'th' ? 'en' : 'th')
}
function logout() {
menuOpen.value = false
auth.logout()
router.push('/login')
}
function onDocClick(e) {
if (menuRef.value && !menuRef.value.contains(e.target)) menuOpen.value = false
}
onMounted(() => {
document.addEventListener('click', onDocClick)
if (auth.token && !auth.user) auth.load()
})
onBeforeUnmount(() => document.removeEventListener('click', onDocClick))
</script>
<style scoped>
.topnav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 10px 24px;
background: rgba(255,255,255,.86);
backdrop-filter: saturate(180%) blur(12px);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 10;
flex-wrap: wrap;
}
.brand {
font-weight: 800; text-decoration: none; color: var(--ink); white-space: nowrap;
display: inline-flex; align-items: center; gap: 9px;
font-size: 15px; letter-spacing: -0.01em;
}
.brand-mark {
width: 30px; height: 30px; border-radius: 9px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
display: inline-flex; align-items: center; justify-content: center;
color: #fff; box-shadow: var(--shadow-accent); flex-shrink: 0;
}
.tabs { display: flex; gap: 4px; flex-wrap: wrap; background: var(--paper-2); padding: 4px; border-radius: 12px; border: 1px solid var(--border); }
.tab {
text-decoration: none;
color: var(--muted);
padding: 8px 16px;
border-radius: 9px;
font-weight: 650;
font-size: 14px;
white-space: nowrap;
transition: color .15s var(--ease), background .15s var(--ease), box-shadow .15s var(--ease);
}
.tab:hover { background: var(--card); color: var(--ink); }
.tab.active { background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #fff; box-shadow: var(--shadow-accent); }
.nav-right { display: flex; align-items: center; gap: 8px; }
.lang { padding: 8px 12px; font-weight: 700; }
.user-menu { position: relative; }
.user-btn {
display: inline-flex; align-items: center; gap: 7px;
padding: 8px 12px; border-radius: 10px; font-weight: 650; color: var(--ink);
border: 1px solid transparent;
}
.user-btn:hover, .user-btn.open { background: var(--paper-2); border-color: var(--border); }
.user-btn svg { color: var(--muted); }
.chev { transition: transform .15s var(--ease); color: var(--muted); }
.chev.up { transform: rotate(180deg); }
.dropdown {
position: absolute; right: 0; top: calc(100% + 8px); z-index: 30;
background: #fff; border: 1px solid var(--border); border-radius: 12px;
box-shadow: var(--shadow-md); min-width: 200px; overflow: hidden;
animation: drop-in .16s var(--ease);
}
@keyframes drop-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
.dropdown .item {
display: flex; align-items: center; gap: 9px; width: 100%;
padding: 11px 14px; border-radius: 0; background: transparent; color: var(--ink);
text-align: left; font-size: 14px; font-weight: 600; min-height: 44px;
}
.dropdown .item:hover { background: var(--paper-2); color: var(--accent); }
.dropdown .item + .item { border-top: 1px solid var(--border); }
.dropdown a.item { text-decoration: none; }
.dropdown .item svg { color: var(--muted); }
.dropdown .item:hover svg { color: var(--accent); }
.icon-btn { text-decoration: none; display: inline-flex; align-items: center; padding: 6px 10px; border-radius: 8px; color: var(--ink); }
.icon-btn:hover { background: #f1f3f9; }
.main { max-width: 1120px; margin: 0 auto; padding: 28px 24px; }
@media (max-width: 640px) {
.topnav { padding: 10px 12px; gap: 8px; }
.brand { font-size: 14px; }
.tabs { order: 3; width: 100%; justify-content: center; margin-top: 2px; }
.tab { padding: 8px 10px; font-size: 12px; flex: 1; text-align: center; }
.nav-right { margin-left: auto; }
.lang { padding: 6px 8px; font-size: 12px; }
.user-btn { padding: 6px 8px; }
.user-btn .txt { max-width: 88px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.main { padding: 18px 14px; }
}
@media (max-width: 380px) {
.brand { display: none; }
.brand-mark { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.dropdown, .card.lift, .bar-fill { animation: none !important; transition: none !important; }
}
</style>