feat(redesign): re-skin admin-ops/platform-admin/public views to DS (Phase D batch 4 — panel re-skin complete)
All checks were successful
Test Asgard Runner / test (push) Successful in 3s

Final re-skin batch: admin ops (Console/FileManager[VueFinder preserved]/WipeCalendar/WipeHistory/Changelog/Migration), platform-admin (Dashboard/Licenses/Servers/Subscriptions/Users), public product pages (ServerInfo/StatusPage/StoreView) + PublicLayout, WarpEditor, ErrorBoundary. All logic/store/router/WebSocket/handlers preserved. Marketing views (Landing/Pricing/FAQ/HowItWorks/Roadmap/EarlyAccess + MarketingLayout) intentionally deferred to the dedicated marketing-site redesign. Build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-06-11 02:55:02 -04:00
parent 376ed9a98d
commit 29615cb4f3
17 changed files with 2843 additions and 1301 deletions

View File

@@ -1,8 +1,12 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useApi } from '@/composables/useApi'
import { CreditCard, Package, DollarSign, Users } from 'lucide-vue-next'
import { safeCurrency, safeLocaleString } from '@/utils/formatters'
import Panel from '@/components/ds/data/Panel.vue'
import StatCard from '@/components/ds/data/StatCard.vue'
import Badge from '@/components/ds/core/Badge.vue'
import Icon from '@/components/ds/core/Icon.vue'
import EmptyState from '@/components/ds/feedback/EmptyState.vue'
const api = useApi()
@@ -58,89 +62,147 @@ onMounted(() => {
</script>
<template>
<div class="p-6 space-y-6 bg-neutral-950 min-h-screen">
<!-- Header -->
<div class="flex items-center gap-3">
<CreditCard class="w-5 h-5 text-oxide-500" />
<div>
<h1 class="text-2xl font-bold text-neutral-100">Subscriptions</h1>
<p class="text-sm text-neutral-400 mt-0.5">Module subscription overview and subscriber details.</p>
<div class="pasub">
<!-- Page head -->
<div class="pasub__head">
<div class="pasub__head-id">
<div class="pasub__chip">
<Icon name="credit-card" :size="20" :stroke-width="2" />
</div>
<div>
<div class="t-eyebrow">Platform admin</div>
<h1 class="pasub__title">Subscriptions</h1>
</div>
</div>
</div>
<!-- Summary Cards -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Total Subscribers -->
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
<div class="flex items-center gap-3 mb-3">
<div class="p-2 rounded-lg bg-oxide-500/10">
<Users class="w-4 h-4 text-oxide-400" />
</div>
<p class="text-sm text-neutral-400">Total Subscribers</p>
</div>
<div v-if="isLoading" class="h-8 w-16 bg-neutral-800 rounded animate-pulse" />
<p v-else class="text-3xl font-bold text-neutral-100">{{ safeLocaleString(totalSubscribers) }}</p>
</div>
<!-- Total MRR -->
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
<div class="flex items-center gap-3 mb-3">
<div class="p-2 rounded-lg bg-green-500/10">
<DollarSign class="w-4 h-4 text-green-400" />
</div>
<p class="text-sm text-neutral-400">Total MRR</p>
</div>
<div v-if="isLoading" class="h-8 w-24 bg-neutral-800 rounded animate-pulse" />
<p v-else class="text-3xl font-bold text-neutral-100">{{ safeCurrency(totalMrr, '$') }}</p>
</div>
<!-- Per-Module Cards -->
<div
<!-- KPI row -->
<div class="pasub__kpis">
<StatCard
icon="users"
label="Total subscribers"
:value="isLoading ? '—' : safeLocaleString(totalSubscribers)"
/>
<StatCard
icon="dollar-sign"
label="Total MRR"
:value="isLoading ? '—' : safeCurrency(totalMrr, '$')"
/>
<StatCard
v-for="mod in moduleBreakdown"
:key="mod.name"
class="bg-neutral-900 border border-neutral-800 rounded-lg p-5"
>
<div class="flex items-center gap-3 mb-3">
<div class="p-2 rounded-lg bg-oxide-500/10">
<Package class="w-4 h-4 text-oxide-400" />
</div>
<p class="text-sm text-neutral-400 truncate">{{ mod.name }}</p>
</div>
<p class="text-3xl font-bold text-neutral-100">{{ mod.count }}</p>
<p class="text-xs text-neutral-500 mt-1">subscribers</p>
</div>
icon="package"
:label="mod.name"
:value="String(mod.count)"
note="subscribers"
/>
</div>
<!-- Module breakdown summary -->
<Panel v-if="moduleBreakdown.length > 0" title="Module breakdown" subtitle="Subscribers per module">
<div class="pasub__mods">
<div
v-for="mod in moduleBreakdown"
:key="mod.name"
class="pasub__mod-row"
>
<div class="pasub__mod-icon">
<Icon name="package" :size="14" :stroke-width="2" />
</div>
<span class="pasub__mod-name">{{ mod.name }}</span>
<Badge tone="neutral" :mono="true">{{ mod.count }}</Badge>
</div>
</div>
</Panel>
<!-- Table -->
<div class="bg-neutral-900 border border-neutral-800 rounded-lg overflow-hidden">
<table class="w-full">
<Panel :flush-body="true" title="All subscriptions" :subtitle="subscriptions.length + ' total'">
<table class="pasub__table">
<thead>
<tr class="border-b border-neutral-800 text-left">
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Owner Email</th>
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Module Name</th>
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">License ID</th>
<tr class="pasub__thead-row">
<th class="pasub__th">Owner email</th>
<th class="pasub__th">Module name</th>
<th class="pasub__th">License ID</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral-800">
<tr v-if="subscriptions.length === 0 && !isLoading">
<td colspan="3" class="px-4 py-12 text-center text-neutral-500 text-sm">
No subscriptions found.
</td>
</tr>
<tbody>
<tr v-if="isLoading">
<td colspan="3" class="px-4 py-12 text-center text-neutral-500 text-sm">Loading subscriptions...</td>
<td colspan="3" class="pasub__td-empty">Loading subscriptions...</td>
</tr>
<tr v-else-if="subscriptions.length === 0">
<td colspan="3" class="pasub__td-es">
<EmptyState
icon="credit-card"
title="No subscriptions found"
description="Module subscriptions will appear here once customers subscribe."
/>
</td>
</tr>
<tr
v-for="(sub, idx) in subscriptions"
:key="`${sub.license_id}-${sub.module_name}-${idx}`"
class="hover:bg-neutral-800/50 transition-colors"
class="pasub__tr"
>
<td class="px-4 py-3 text-sm text-neutral-100">{{ sub.owner_email }}</td>
<td class="px-4 py-3 text-sm text-neutral-400">{{ sub.module_name }}</td>
<td class="px-4 py-3 text-sm text-neutral-400 font-mono">{{ sub.license_id }}</td>
<td class="pasub__td">{{ sub.owner_email }}</td>
<td class="pasub__td pasub__td--secondary">{{ sub.module_name }}</td>
<td class="pasub__td pasub__td--mono">{{ sub.license_id }}</td>
</tr>
</tbody>
</table>
</div>
</Panel>
</div>
</template>
<style scoped>
.pasub { max-width: 1000px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px; }
/* Page head */
.pasub__head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
.pasub__head-id { display: flex; align-items: center; gap: 12px; }
.pasub__chip {
width: 40px; height: 40px; flex: none; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
color: var(--accent); background: var(--accent-soft);
box-shadow: inset 0 0 0 1px var(--accent-border);
}
.pasub__title {
font-size: var(--text-2xl); font-weight: 700; letter-spacing: -0.02em;
color: var(--text-primary); margin-top: 3px;
}
/* KPI row */
.pasub__kpis { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 13px; }
/* Module breakdown */
.pasub__mods { display: flex; flex-direction: column; gap: 2px; }
.pasub__mod-row {
display: flex; align-items: center; gap: 10px;
padding: 9px 6px; border-radius: var(--radius-md);
transition: var(--transition-colors);
}
.pasub__mod-row:hover { background: var(--surface-hover); }
.pasub__mod-icon {
width: 26px; height: 26px; flex: none; border-radius: var(--radius-sm);
display: flex; align-items: center; justify-content: center;
color: var(--accent-text); background: var(--accent-soft);
}
.pasub__mod-name { flex: 1; font-size: var(--text-sm); color: var(--text-primary); }
/* Table */
.pasub__table { width: 100%; border-collapse: collapse; }
.pasub__thead-row { border-bottom: 1px solid var(--border-subtle); }
.pasub__th {
padding: 10px 16px; text-align: left;
font-size: var(--text-xs); font-weight: 600; color: var(--text-tertiary);
text-transform: uppercase; letter-spacing: var(--tracking-wider);
white-space: nowrap;
}
.pasub__tr { border-bottom: 1px solid var(--border-subtle); transition: var(--transition-colors); }
.pasub__tr:last-child { border-bottom: 0; }
.pasub__tr:hover { background: var(--surface-hover); }
.pasub__td { padding: 11px 16px; font-size: var(--text-sm); color: var(--text-primary); }
.pasub__td--secondary { color: var(--text-secondary); }
.pasub__td--mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--text-xs); color: var(--text-secondary); }
.pasub__td-empty { padding: 12px 16px; text-align: center; font-size: var(--text-sm); color: var(--text-tertiary); }
.pasub__td-es { padding: 0; }
</style>