feat(admin): add AdminAlertsPanel component
This commit is contained in:
parent
c8ac730791
commit
ba74bfd929
1 changed files with 173 additions and 0 deletions
173
app/components/admin/AdminAlertsPanel.vue
Normal file
173
app/components/admin/AdminAlertsPanel.vue
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
<!-- app/components/admin/AdminAlertsPanel.vue -->
|
||||
<template>
|
||||
<div v-if="visibleAlerts.length" class="alerts-panel">
|
||||
<div class="section-label">Needs Attention</div>
|
||||
<div
|
||||
v-for="alert in visibleAlerts"
|
||||
:key="alert.type"
|
||||
class="alert-row"
|
||||
:class="`severity-${alert.severity}`"
|
||||
>
|
||||
<div class="alert-head">
|
||||
<div>
|
||||
<span class="alert-title">{{ alert.title }}</span>
|
||||
<span class="alert-count">{{ alert.count }}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="dismiss-btn"
|
||||
:disabled="dismissing[alert.type]"
|
||||
@click="dismissAlert(alert)"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
<ul v-if="alert.items.length" class="alert-items">
|
||||
<li v-for="(item, idx) in displayItems(alert)" :key="item.id || idx">
|
||||
<NuxtLink v-if="item.href" :to="item.href">{{ item.label }}</NuxtLink>
|
||||
<span v-else>{{ item.label }}</span>
|
||||
<span v-if="item.sublabel" class="alert-item-sub">— {{ item.sublabel }}</span>
|
||||
</li>
|
||||
<li v-if="alert.items.length > maxItems" class="alert-more">
|
||||
and {{ alert.items.length - maxItems }} more
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const maxItems = 5
|
||||
const dismissing = reactive({})
|
||||
|
||||
const { data, refresh } = await useFetch('/api/admin/alerts', { default: () => ({ alerts: [] }) })
|
||||
|
||||
const visibleAlerts = computed(() => data.value?.alerts || [])
|
||||
|
||||
function displayItems(alert) {
|
||||
return alert.items.slice(0, maxItems)
|
||||
}
|
||||
|
||||
async function dismissAlert(alert) {
|
||||
dismissing[alert.type] = true
|
||||
try {
|
||||
await $fetch('/api/admin/alerts/dismiss', {
|
||||
method: 'POST',
|
||||
body: { alertType: alert.type, signature: alert.signature }
|
||||
})
|
||||
// Optimistic local removal
|
||||
data.value = {
|
||||
alerts: visibleAlerts.value.filter((a) => a.type !== alert.type)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to dismiss alert', err)
|
||||
// On error, refetch to get authoritative state
|
||||
await refresh()
|
||||
} finally {
|
||||
dismissing[alert.type] = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alerts-panel {
|
||||
border-bottom: 1px dashed var(--border);
|
||||
padding: 24px 28px;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-faint);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.alert-row {
|
||||
border: 1px dashed var(--border);
|
||||
border-left-width: 3px;
|
||||
border-left-style: solid;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.alert-row.severity-critical {
|
||||
border-left-color: var(--ember);
|
||||
}
|
||||
|
||||
.alert-row.severity-attention {
|
||||
border-left-color: var(--candle);
|
||||
}
|
||||
|
||||
.alert-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 13px;
|
||||
color: var(--text-bright);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.alert-count {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.dismiss-btn {
|
||||
background: none;
|
||||
border: 1px dashed var(--border);
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.dismiss-btn:hover:not(:disabled) {
|
||||
border-color: var(--candle);
|
||||
color: var(--candle);
|
||||
}
|
||||
|
||||
.dismiss-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.alert-items {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
.alert-items li {
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.alert-items a {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.alert-items a:hover {
|
||||
color: var(--candle);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.alert-item-sub {
|
||||
color: var(--text-faint);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.alert-more {
|
||||
color: var(--text-faint);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue