Files
dezky/apps/portal/pages/admin/storage.vue
T
Ronni Baslund 0bd4e5498e feat: portal redesign, pricing catalog, partner-staff invites
- portal: new admin/ and partner/ surfaces with full component library
  (AppLauncher, Avatar, Badge, Card, Modal, Tabs, etc.), composables,
  layouts, partner-routing middleware, and supporting server APIs
- pricing: Price schema/module with operator CRUD, pricing.vue catalog UI,
  Subscription extended with cycle/currency/perSeatAmount/seats snapshots
  for stable MRR aggregation
- partner staff: User.partnerId, invite-partner-user DTO and flow,
  /partners/:slug/users endpoints, InvitePartnerUserModal, shared
  dezky-partner-staff Authentik group
- /me: partner-aware endpoint returning user + partner context so portal
  can route between end-user and partner-admin surfaces
- tenant: seats field for portfolio displays and future MRR calculations
- operator: pricing page, signed-out page, useMe/useToast composables,
  ToastStack
2026-05-28 20:00:33 +02:00

109 lines
3.7 KiB
Vue

<script setup lang="ts">
// Strict port of project/platform-app.jsx `StorageScreen` (lines 970-1020).
// Two-card 1.4fr/1fr layout: aggregate + top users on the left, type breakdown
// on the right. No tabs in the source — just two cards.
import { sampleUsersFlat } from '~/data/workspace'
const topUsers = computed(() =>
[...sampleUsersFlat].slice(0, 5).sort((a, b) => b.storage - a.storage),
)
const typeBreakdown: Array<[string, number, string]> = [
['Documents', 42, 'var(--text)'],
['Images', 24, 'var(--info)'],
['Video', 18, 'var(--warn)'],
['Archives', 9, 'var(--ok)'],
['Other', 7, 'var(--text-mute)'],
]
</script>
<template>
<div>
<PageHeader
eyebrow="Drev"
title="Storage"
subtitle="Aggregate file storage across your workspace, by user and type."
/>
<div class="content">
<Card>
<div class="card-head">
<Eyebrow>Aggregate</Eyebrow>
<div class="card-title">1.4 TB used</div>
<div class="card-sub">64% of 2.2 TB allocated · Business plan</div>
</div>
<div class="progress" style="height: 10px;">
<span style="width: 64%" />
</div>
<div class="progress-legend">
<span>1.4 TB used</span>
<span>820 GB free</span>
</div>
<div class="top-block">
<Eyebrow>Top users</Eyebrow>
<div class="top-list">
<div v-for="u in topUsers" :key="u.id" class="top-row">
<div class="user-cell">
<Avatar :name="u.name" :size="22" />
<span>{{ u.name }}</span>
</div>
<div class="progress thin"><span :style="{ width: Math.min(100, (u.storage / 50) * 100) + '%' }" /></div>
<Mono>{{ u.storage }} GB</Mono>
</div>
</div>
</div>
</Card>
<Card>
<div class="card-head">
<Eyebrow>By type</Eyebrow>
<div class="card-title">What's taking space</div>
</div>
<div class="types">
<div v-for="[n, p, c] in typeBreakdown" :key="n">
<div class="type-head">
<span>{{ n }}</span>
<span class="pct">{{ p }}%</span>
</div>
<div class="progress thinner"><span :style="{ width: p + '%', background: c }" /></div>
</div>
</div>
</Card>
</div>
</div>
</template>
<style scoped>
.content { padding: 24px 40px 64px 40px; display: grid; grid-template-columns: 1.4fr 1fr; gap: 16px; max-width: 1200px; }
.card-head { margin-bottom: 16px; }
.card-title { font-family: var(--font-display); font-weight: 600; font-size: 18px; letter-spacing: -0.01em; margin-top: 4px; }
.card-sub { font-size: 13px; color: var(--text-mute); margin-top: 4px; }
.progress { background: var(--bg); border-radius: 999px; overflow: hidden; }
.progress.thin { height: 6px; }
.progress.thinner { height: 5px; }
.progress span { display: block; height: 100%; background: var(--text); }
.progress-legend {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-mute);
}
.top-block { margin-top: 32px; }
.top-list { margin-top: 12px; display: flex; flex-direction: column; gap: 10px; }
.top-row { display: grid; grid-template-columns: 180px 1fr 60px; gap: 12px; align-items: center; }
.user-cell { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.top-row > .mono, .top-row :deep(.mono) { font-family: var(--font-mono); font-size: 11px; text-align: right; }
.types { display: flex; flex-direction: column; gap: 12px; }
.type-head { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 4px; }
.pct { font-family: var(--font-mono); color: var(--text-mute); }
</style>