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
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
// Cosmetic + role tweaks for the portal shell. Persisted in localStorage so
|
||||
// reloads stay coherent during prototyping. Applied to <html> as data-*
|
||||
// attributes; tokens.css picks them up via selector overrides.
|
||||
//
|
||||
// `role` is the most important tweak — it switches which sidebar nav + which
|
||||
// pages are visible (end user / customer admin / partner admin). For real use
|
||||
// the role would come from Authentik group claims; the tweak lets us preview
|
||||
// all three views without standing up three orgs.
|
||||
|
||||
export type ThemeMode = 'dark' | 'light'
|
||||
export type Density = 'comfy' | 'compact'
|
||||
export type Accent = 'signal' | 'cobalt' | 'coral' | 'moss'
|
||||
export type PortalRole = 'end-user' | 'customer-admin' | 'partner-admin'
|
||||
|
||||
interface TweakState {
|
||||
theme: ThemeMode
|
||||
density: Density
|
||||
accent: Accent
|
||||
role: PortalRole
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'dezky-portal-tweaks'
|
||||
|
||||
const DEFAULTS: TweakState = {
|
||||
theme: 'light',
|
||||
density: 'comfy',
|
||||
accent: 'signal',
|
||||
role: 'customer-admin',
|
||||
}
|
||||
|
||||
const state = ref<TweakState>({ ...DEFAULTS })
|
||||
const hydrated = ref(false)
|
||||
|
||||
function apply() {
|
||||
if (!import.meta.client) return
|
||||
const root = document.documentElement
|
||||
root.setAttribute('data-theme', state.value.theme)
|
||||
root.setAttribute('data-density', state.value.density)
|
||||
root.setAttribute('data-accent', state.value.accent)
|
||||
root.setAttribute('data-role', state.value.role)
|
||||
}
|
||||
|
||||
function persist() {
|
||||
if (!import.meta.client) return
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.value))
|
||||
} catch {
|
||||
// localStorage can throw in private mode; tweaks are cosmetic so swallow.
|
||||
}
|
||||
}
|
||||
|
||||
function hydrate() {
|
||||
if (!import.meta.client || hydrated.value) return
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as Partial<TweakState>
|
||||
state.value = {
|
||||
theme: parsed.theme ?? DEFAULTS.theme,
|
||||
density: parsed.density ?? DEFAULTS.density,
|
||||
accent: parsed.accent ?? DEFAULTS.accent,
|
||||
role: parsed.role ?? DEFAULTS.role,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore corrupt JSON
|
||||
}
|
||||
apply()
|
||||
hydrated.value = true
|
||||
}
|
||||
|
||||
export const usePortalTweaks = () => {
|
||||
if (import.meta.client) hydrate()
|
||||
|
||||
function set<K extends keyof TweakState>(key: K, value: TweakState[K]) {
|
||||
state.value = { ...state.value, [key]: value }
|
||||
apply()
|
||||
persist()
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
setTheme: (v: ThemeMode) => set('theme', v),
|
||||
setDensity: (v: Density) => set('density', v),
|
||||
setAccent: (v: Accent) => set('accent', v),
|
||||
setRole: (v: PortalRole) => set('role', v),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user