chore(ui): remove 4 orphaned views (Dashboard/GenPersona/MySessions/WeakAreas) + mobile polish

- Delete unused views not referenced by router/components (dead files from earlier builds).
- Mobile (640px/460px/380px): nav tabs wrap to a centered full-width row + hide brand at
  <380; logout shows icon-only; stat cards center when collapsing to one column
  (Moreminimore convention); chat bubbles/width handled.
Rebuilt dist.
This commit is contained in:
Macky
2026-08-08 16:12:30 +07:00
parent 15c60ae400
commit b41bfee03b
29 changed files with 50 additions and 250 deletions

View File

@@ -18,7 +18,7 @@
<router-link to="/settings" class="icon-btn" :title="i18n.t('settings')">
<SettingsIcon :size="20" :stroke-width="1.8" />
</router-link>
<button @click="logout" class="logout-btn"><LogOut :size="18" :stroke-width="1.8" /> {{ i18n.t('logout') }}</button>
<button @click="logout" class="logout-btn"><LogOut :size="18" :stroke-width="1.8" /> <span class="txt">{{ i18n.t('logout') }}</span></button>
</div>
</nav>
<main class="main">
@@ -83,8 +83,16 @@ onMounted(() => {
.icon-btn:hover { background: #f1f3f9; }
.main { max-width: 1080px; margin: 0 auto; padding: 24px; }
@media (max-width: 640px) {
.topnav { padding: 10px 12px; }
.tab { padding: 8px 12px; font-size: 13px; }
.logout-btn span { display: none; }
.topnav { padding: 8px 10px; gap: 8px; }
.brand { font-size: 14px; }
.tabs { order: 3; width: 100%; justify-content: center; }
.tab { padding: 6px 10px; font-size: 12px; flex: 1; text-align: center; }
.nav-right { margin-left: auto; }
.lang { padding: 6px 8px; font-size: 12px; }
.logout-btn { padding: 6px 8px; }
.logout-btn .txt { display: none; }
}
@media (max-width: 380px) {
.brand { display: none; }
}
</style>

View File

@@ -155,4 +155,11 @@ button:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }
.main { padding: 16px; }
.row { gap: 10px; }
.msg-seller, .msg-customer { max-width: 84%; }
/* Moreminimore convention: centered content when a section collapses to one column */
.stat-row > .card, .row.stat > .card { flex: 1 1 40%; text-align: center; }
.stat div { text-align: center; }
}
@media (max-width: 460px) {
.row.stat, .stat-row, .row.stat-row { justify-content: center; }
.stat-row > .card { min-width: 42%; }
}

View File

@@ -1,71 +0,0 @@
<template>
<div>
<div class="row" style="margin-bottom:16px">
<h2 style="margin:0">{{ i18n.t('dashboard') }}</h2>
<div style="margin-left:auto" v-if="auth.isAdmin">
<router-link to="/admin/new-group"><button class="primary">{{ i18n.t('groupBuilder') }} +</button></router-link>
</div>
</div>
<div class="row" v-if="auth.isAdmin" style="gap:16px;margin-bottom:20px">
<router-link to="/admin/users" style="text-decoration:none"><div class="card link-card">👥 {{ i18n.t('users') }}</div></router-link>
<router-link to="/admin/analytics" style="text-decoration:none"><div class="card link-card">📊 {{ i18n.t('analytics') }}</div></router-link>
</div>
<div v-if="auth.role === 'user'" class="row" style="gap:16px;margin-bottom:20px">
<router-link to="/my/sessions" style="text-decoration:none"><div class="card link-card">🎯 {{ i18n.t('myTraining') }}</div></router-link>
<router-link to="/my/weak-areas" style="text-decoration:none"><div class="card link-card"> {{ i18n.t('weakAreas') }}</div></router-link>
<router-link to="/my/generate" style="text-decoration:none"><div class="card link-card"> {{ i18n.t('generatePersona') }}</div></router-link>
</div>
<h3>{{ i18n.t('groups') }}</h3>
<div v-if="loading" class="card" style="min-height:120px">
<div class="skeleton" style="height:60px"></div>
<div class="skeleton" style="height:60px;margin-top:10px"></div>
</div>
<div v-else-if="groups.length === 0" class="card empty-state">
<strong>{{ auth.isAdmin ? 'No persona groups yet' : 'No groups available' }}</strong>
<span v-if="auth.isAdmin">{{ i18n.t('groupBuilder') }} to start.</span>
<span v-else>Ask an admin to create a group.</span>
</div>
<div class="grid">
<div v-for="g in groups" :key="g.id" class="card group-card lift">
<div class="row" style="justify-content:space-between">
<strong>{{ g.title }}</strong>
<span class="badge" :class="g.status">{{ g.status }}</span>
</div>
<div class="muted" style="margin:6px 0 12px">{{ (g.sales_kit && g.sales_kit.productName) || (g.input && g.input.product) || '' }}</div>
<router-link v-if="auth.isAdmin" :to="`/admin/groups/${g.id}/edit`">
<button>{{ i18n.t('personas') }} / {{ i18n.t('create') }}</button>
</router-link>
<router-link v-else-if="g.status === 'ready'" :to="`/groups/${g.id}/personas`">
<button class="primary">{{ i18n.t('selectPersona') }}</button>
</router-link>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { api } from '../api'
import { auth } from '../store/auth'
import { i18n } from '../i18n'
const groups = ref([])
const loading = ref(true)
onMounted(async () => {
try {
groups.value = (await api.listGroups()).groups
} finally {
loading.value = false
}
})
</script>
<style scoped>
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
.group-card { display: flex; flex-direction: column; }
.group-card a { margin-top: auto; }
.link-card { text-align: center; min-width: 150px; }
</style>

View File

@@ -1,77 +0,0 @@
<template>
<div>
<h2>{{ i18n.t('generatePersona') }}</h2>
<div class="card" style="margin-bottom:16px">
<label>Mode</label>
<div class="row">
<button :class="{ active: mode === 'manual' }" @click="mode = 'manual'"> {{ i18n.t('manual') }}</button>
<button :class="{ active: mode === 'weak-area' }" @click="mode = 'weak-area'">🔒 Weak-area lock</button>
</div>
<template v-if="mode === 'manual'">
<label>Describe the persona you want to practice against</label>
<textarea v-model="spec" rows="4" placeholder="e.g. a price-hardball restaurant owner on LINE who stalls when I bring up costs"></textarea>
</template>
<template v-else>
<p class="muted">The system will analyze your losses and auto-generate a harder persona targeting your weak points.</p>
</template>
<button class="primary" style="margin-top:16px" :disabled="busy || (mode === 'manual' && !spec.trim())" @click="gen">
{{ busy ? '...' : i18n.t('generatePersona') }}
</button>
<div class="error" v-if="error">{{ error }}</div>
</div>
<h3>My personas</h3>
<div class="grid">
<div v-for="p in mine" :key="p.id" class="card pcard">
<strong>{{ p.name }}</strong>
<span class="badge" :class="p.tier">Tier {{ p.tier }}</span>
<div class="muted">{{ p.profession }} · {{ p.age_group }}</div>
<router-link :to="`/groups/${myGid}/chat/${p.id}`" style="margin-top:auto">
<button class="primary" style="width:100%">{{ i18n.t('chat') }}</button>
</router-link>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { api } from '../api'
import { i18n } from '../i18n'
const route = useRoute()
const mode = ref(route.query.mode === 'weak' ? 'weak-area' : 'manual')
const spec = ref('')
const busy = ref(false)
const error = ref('')
const mine = ref([])
const myGid = ref(null)
async function loadMine() {
const d = await api.myPersonas()
myGid.value = d.group.id
mine.value = d.personas
}
onMounted(loadMine)
async function gen() {
busy.value = true
error.value = ''
try {
const body = mode.value === 'weak-area'
? { mode: 'weak-area', spec: {} }
: { mode: 'manual', spec: { description: spec.value } }
await api.generatePersona(body)
await loadMine()
} catch (e) { error.value = e.message }
finally { busy.value = false }
}
</script>
<style scoped>
button.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
.pcard { display: flex; flex-direction: column; min-height: 140px; }
</style>

View File

@@ -1,28 +0,0 @@
<template>
<div>
<h2>{{ i18n.t('myTraining') }}</h2>
<div class="card" v-for="s in sessions" :key="s.id" style="margin-bottom:10px">
<div class="row" style="justify-content:space-between">
<strong>{{ s.persona_name }}</strong>
<span class="badge" :class="s.outcome || 'not_tried'">{{ s.outcome || '—' }}</span>
</div>
<div class="muted">{{ s.persona_id }} · {{ (new Date(s.created_at)).toLocaleString() }}</div>
<div v-if="s.debrief" class="muted" style="margin-top:4px">
Score {{ s.debrief.score }} {{ s.debrief.why }}
</div>
</div>
<div v-if="sessions.length === 0" class="card empty-state">
<strong>No training sessions yet</strong>
<span>Pick a persona from a group and practice closing a sale.</span>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { api } from '../api'
import { i18n } from '../i18n'
const sessions = ref([])
onMounted(async () => { sessions.value = (await api.mySessions()).sessions })
</script>

View File

@@ -1,39 +0,0 @@
<template>
<div>
<div class="row" style="align-items:center">
<h2 style="margin:0">{{ i18n.t('weakAreas') }}</h2>
<router-link :to="`/my/generate?mode=weak`" style="margin-left:auto"><button class="primary">🔒 Generate a lock persona</button></router-link>
</div>
<div class="row" style="gap:16px;margin:16px 0">
<div class="card stat"><div>Wins</div><strong>{{ insight.wins }}</strong></div>
<div class="card stat"><div>Losses</div><strong>{{ insight.losses }}</strong></div>
<div class="card stat"><div>Total</div><strong>{{ insight.total_sessions }}</strong></div>
</div>
<div v-for="g in insight.by_tier" :key="g.value" class="card" style="margin-bottom:8px">
<span class="badge" :class="g.value">Tier {{ g.value }}</span> {{ g.losses }} losses
</div>
<h3 style="margin-top:20px">Top loss personas</h3>
<div v-if="!insight.top_loss_personas || !insight.top_loss_personas.length" class="card muted">No losses yet 🎉</div>
<div class="card" v-for="(p, i) in insight.top_loss_personas" :key="i" style="margin-bottom:8px">
<strong>{{ p.persona_name }}</strong> score {{ p.score }}<br />
<span class="muted">{{ p.why }}</span>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { api } from '../api'
import { i18n } from '../i18n'
const insight = ref({ wins: 0, losses: 0, total_sessions: 0, by_tier: [], top_loss_personas: [] })
onMounted(async () => { insight.value = (await api.weakAreas()).insight })
</script>
<style scoped>
.stat { text-align: center; min-width: 100px; }
.stat div { color: var(--muted); font-size: 12px; }
.stat strong { font-size: 22px; }
</style>