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:
@@ -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>
|
||||
|
||||
@@ -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%; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user