0bd4e5498e
- 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
109 lines
3.7 KiB
Vue
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>
|