Initial commit: Aurora Admin plugin
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useDashboardData } from './useDashboardData.js';
|
||||
import { useAnalyticsData } from './useAnalyticsData.js';
|
||||
import RecentContent from './cards/RecentContent.vue';
|
||||
import ScheduledContent from './cards/ScheduledContent.vue';
|
||||
import RecentComments from './cards/RecentComments.vue';
|
||||
import UserAnalytics from './cards/UserAnalytics.vue';
|
||||
import MediaAnalytics from './cards/MediaAnalytics.vue';
|
||||
import ServerHealth from './cards/ServerHealth.vue';
|
||||
import PageViews from './cards/PageViews.vue';
|
||||
import ActiveUsers from './cards/ActiveUsers.vue';
|
||||
import DeviceUsage from './cards/DeviceUsage.vue';
|
||||
import Engagement from './cards/Engagement.vue';
|
||||
import TopList from './cards/TopList.vue';
|
||||
|
||||
const props = defineProps({
|
||||
serverData: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const { data, loading, load } = useDashboardData(props.serverData);
|
||||
const { data: analytics, load: loadAnalytics } = useAnalyticsData(props.serverData);
|
||||
|
||||
// Date range — default last 7 days, with simple presets.
|
||||
const today = new Date();
|
||||
const range = ref([new Date(new Date().setDate(today.getDate() - 7)), new Date()]);
|
||||
const presetsOpen = ref(false);
|
||||
|
||||
const fmtLabel = (d) =>
|
||||
d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
|
||||
const reload = (from, to) => {
|
||||
load(from, to);
|
||||
loadAnalytics(from, to);
|
||||
};
|
||||
|
||||
const applyPreset = (days) => {
|
||||
const to = new Date();
|
||||
const from = new Date();
|
||||
from.setDate(to.getDate() - days);
|
||||
range.value = [from, to];
|
||||
presetsOpen.value = false;
|
||||
reload(from, to);
|
||||
};
|
||||
|
||||
const activeTab = ref('overview');
|
||||
|
||||
onMounted(() => reload(range.value[0], range.value[1]));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="aurora-dashboard">
|
||||
<header class="dash__head">
|
||||
<div>
|
||||
<h1 class="dash__title">Dashboard</h1>
|
||||
<p class="dash__subtitle">Overview of your site activity</p>
|
||||
</div>
|
||||
|
||||
<div class="dash__range">
|
||||
<button type="button" class="dash__range-btn" @click="presetsOpen = !presetsOpen">
|
||||
<span class="dash__range-icon">▦</span>
|
||||
{{ fmtLabel(range[0]) }} — {{ fmtLabel(range[1]) }}
|
||||
</button>
|
||||
<div v-if="presetsOpen" class="dash__presets">
|
||||
<button type="button" @click="applyPreset(7)">Last 7 days</button>
|
||||
<button type="button" @click="applyPreset(30)">Last 30 days</button>
|
||||
<button type="button" @click="applyPreset(90)">Last 90 days</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="dash__tabs">
|
||||
<button
|
||||
type="button"
|
||||
:class="['dash__tab', { 'dash__tab--active': activeTab === 'overview' }]"
|
||||
@click="activeTab = 'overview'"
|
||||
>
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['dash__tab', { 'dash__tab--active': activeTab === 'analytics' }]"
|
||||
@click="activeTab = 'analytics'"
|
||||
>
|
||||
Analytics
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'overview'" class="dash__grid">
|
||||
<div class="dash__cell" style="grid-column: span 4">
|
||||
<RecentContent :data="data ? data.recentContent : {}" />
|
||||
</div>
|
||||
<div class="dash__cell" style="grid-column: span 4">
|
||||
<ScheduledContent :data="data ? data.scheduledContent : {}" />
|
||||
</div>
|
||||
<div class="dash__cell" style="grid-column: span 4">
|
||||
<RecentComments :data="data ? data.recentComments : {}" />
|
||||
</div>
|
||||
<div class="dash__cell" style="grid-column: span 6">
|
||||
<UserAnalytics :data="data ? data.userAnalytics : {}" />
|
||||
</div>
|
||||
<div class="dash__cell" style="grid-column: span 6">
|
||||
<MediaAnalytics :data="data ? data.mediaAnalytics : {}" />
|
||||
</div>
|
||||
<div class="dash__cell" style="grid-column: span 6">
|
||||
<ServerHealth :data="data ? data.serverHealth : {}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="dash__grid">
|
||||
<div class="dash__cell" style="grid-column: span 8">
|
||||
<PageViews :data="analytics ? analytics.pageViews : {}" />
|
||||
</div>
|
||||
<div class="dash__cell" style="grid-column: span 4">
|
||||
<ActiveUsers :count="analytics ? analytics.activeUsers : 0" />
|
||||
</div>
|
||||
<div class="dash__cell" style="grid-column: span 4">
|
||||
<DeviceUsage :items="analytics ? analytics.deviceUsage : []" />
|
||||
</div>
|
||||
<div class="dash__cell" style="grid-column: span 4">
|
||||
<Engagement :bounce="analytics ? analytics.bounce : {}" />
|
||||
</div>
|
||||
<div class="dash__cell" style="grid-column: span 4">
|
||||
<TopList
|
||||
title="Top Countries"
|
||||
subtitle="Where visitors are from"
|
||||
:items="analytics ? analytics.topCountries : []"
|
||||
empty-text="No location data yet."
|
||||
/>
|
||||
</div>
|
||||
<div class="dash__cell" style="grid-column: span 6">
|
||||
<TopList
|
||||
title="Top Pages"
|
||||
subtitle="Most viewed pages"
|
||||
:items="analytics ? analytics.topPages : []"
|
||||
/>
|
||||
</div>
|
||||
<div class="dash__cell" style="grid-column: span 6">
|
||||
<TopList
|
||||
title="Top Referrers"
|
||||
subtitle="Where traffic comes from"
|
||||
:items="analytics ? analytics.topReferrers : []"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.aurora-dashboard {
|
||||
padding: 24px var(--aurora-gutter-r, 44px) 40px 32px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.dash__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.dash__title {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--aurora-text);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.dash__subtitle { margin: 4px 0 0; font-size: 0.875rem; color: var(--aurora-text-muted); }
|
||||
.dash__range { position: relative; }
|
||||
.dash__range-btn {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
background: var(--aurora-card-bg);
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
color: var(--aurora-text);
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dash__range-icon { color: var(--aurora-text-muted); }
|
||||
.dash__presets {
|
||||
position: absolute; right: 0; top: calc(100% + 6px);
|
||||
background: var(--aurora-card-bg);
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
border-radius: 10px;
|
||||
padding: 6px;
|
||||
z-index: 10;
|
||||
min-width: 160px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
|
||||
}
|
||||
.dash__presets button {
|
||||
display: block; width: 100%; text-align: left;
|
||||
background: none; border: none; color: var(--aurora-text);
|
||||
padding: 8px 10px; border-radius: 6px; cursor: pointer; font-size: 0.8125rem;
|
||||
}
|
||||
.dash__presets button:hover { background: var(--aurora-bg-subtle); }
|
||||
.dash__tabs {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
border-radius: 10px;
|
||||
padding: 3px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.dash__tab {
|
||||
border: none; background: none; cursor: pointer;
|
||||
padding: 6px 16px; border-radius: 8px;
|
||||
font-size: 0.8125rem; font-weight: 500;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.dash__tab--active { background: var(--aurora-card-bg); color: var(--aurora-text); }
|
||||
.dash__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
gap: 18px;
|
||||
}
|
||||
.dash__cell { min-width: 0; display: flex; }
|
||||
.dash__cell > * { width: 100%; }
|
||||
.dash__analytics-empty {
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.875rem;
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.dash__cell { grid-column: span 12 !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Shared bits used inside the card slots (head-right badges, footer links).
|
||||
Scoped under .aurora-dashboard so they can't collide with WordPress or
|
||||
leak elsewhere, while still reaching slotted content in child cards. */
|
||||
.aurora-dashboard .dot {
|
||||
display: inline-block;
|
||||
width: 7px; height: 7px;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.aurora-dashboard .dot--accent { background: var(--aurora-accent); }
|
||||
.aurora-dashboard .dot--green { background: #34d399; }
|
||||
.aurora-dashboard .viewall {
|
||||
font-size: 0.75rem;
|
||||
color: var(--aurora-text-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
.aurora-dashboard .viewall:hover { color: var(--aurora-accent); }
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup>
|
||||
import AuroraCard from '../components/AuroraCard.vue';
|
||||
|
||||
defineProps({ count: { type: Number, default: 0 } });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuroraCard title="Active Users" subtitle="Visitors in the last 5 minutes">
|
||||
<div class="au">
|
||||
<span class="au__pulse" />
|
||||
<div class="au__num">{{ count }}</div>
|
||||
<div class="au__label">active now</div>
|
||||
</div>
|
||||
</AuroraCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.au {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
text-align: center; padding: 24px 0; height: 100%;
|
||||
}
|
||||
.au__pulse {
|
||||
width: 10px; height: 10px; border-radius: 50%;
|
||||
background: #34d399; margin-bottom: 14px;
|
||||
box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.6);
|
||||
animation: au-pulse 1.8s infinite;
|
||||
}
|
||||
@keyframes au-pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(52,211,153,0.5); }
|
||||
70% { box-shadow: 0 0 0 12px rgba(52,211,153,0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(52,211,153,0); }
|
||||
}
|
||||
.au__num { font-size: 2.5rem; font-weight: 700; color: var(--aurora-text); line-height: 1; }
|
||||
.au__label { margin-top: 6px; font-size: 0.8125rem; color: var(--aurora-text-muted); }
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import AuroraCard from '../components/AuroraCard.vue';
|
||||
import BarList from '../components/BarList.vue';
|
||||
|
||||
const props = defineProps({ items: { type: Array, default: () => [] } });
|
||||
|
||||
const cap = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
|
||||
const list = computed(() =>
|
||||
props.items.map((d) => ({ label: `${cap(d.device)} · ${d.pct}%`, count: d.count }))
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuroraCard title="Device Usage" subtitle="How visitors browse">
|
||||
<BarList :items="list" empty-text="No visits in this range yet." />
|
||||
</AuroraCard>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import AuroraCard from '../components/AuroraCard.vue';
|
||||
import StatRow from '../components/StatRow.vue';
|
||||
|
||||
defineProps({ bounce: { type: Object, default: () => ({}) } });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuroraCard title="Engagement" subtitle="Bounce rate & time on site">
|
||||
<StatRow
|
||||
:stats="[
|
||||
{ value: (bounce.rate || 0) + '%', label: 'Bounce Rate' },
|
||||
{ value: bounce.avgTime || '0s', label: 'Avg. Time' },
|
||||
{ value: bounce.sessions || 0, label: 'Sessions' },
|
||||
]"
|
||||
/>
|
||||
</AuroraCard>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
import AuroraCard from '../components/AuroraCard.vue';
|
||||
|
||||
defineProps({ data: { type: Object, default: () => ({}) } });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuroraCard title="Media Analytics" subtitle="Media library overview">
|
||||
<div class="ma__top">
|
||||
<div class="ma__big">
|
||||
<div class="ma__big-num">{{ data.totalFiles || 0 }}</div>
|
||||
<div class="ma__big-label">Total Files</div>
|
||||
</div>
|
||||
<div class="ma__big">
|
||||
<div class="ma__big-num">{{ data.totalSize || '0 B' }}</div>
|
||||
<div class="ma__big-label">Total Size</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ma__grid">
|
||||
<div class="ma__box">
|
||||
<div class="ma__box-num">{{ data.recent30d || 0 }}</div>
|
||||
<div class="ma__box-label">Recent (30d)</div>
|
||||
</div>
|
||||
<div class="ma__box">
|
||||
<div class="ma__box-num">{{ data.unused || 0 }}</div>
|
||||
<div class="ma__box-label">Unused</div>
|
||||
</div>
|
||||
<div class="ma__box">
|
||||
<div class="ma__box-num">{{ data.largeFiles || 0 }}</div>
|
||||
<div class="ma__box-label">Large Files</div>
|
||||
</div>
|
||||
<div class="ma__box">
|
||||
<div class="ma__box-num">{{ data.diskUsage || '0 B' }}</div>
|
||||
<div class="ma__box-label">Disk Usage</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<a :href="data.manageUrl" class="viewall">Manage Media ›</a>
|
||||
</template>
|
||||
</AuroraCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ma__top {
|
||||
display: grid; grid-template-columns: 1fr 1fr; text-align: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.ma__big-num { font-size: 1.75rem; font-weight: 700; color: var(--aurora-text); }
|
||||
.ma__big-label { margin-top: 4px; font-size: 0.75rem; color: var(--aurora-text-muted); }
|
||||
.ma__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.ma__box {
|
||||
background: var(--aurora-bg-subtle);
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
.ma__box-num { font-size: 1.25rem; font-weight: 700; color: var(--aurora-text); }
|
||||
.ma__box-label { margin-top: 2px; font-size: 0.6875rem; color: var(--aurora-text-muted); }
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
import AuroraCard from '../components/AuroraCard.vue';
|
||||
import StatRow from '../components/StatRow.vue';
|
||||
import AreaChart from '../components/AreaChart.vue';
|
||||
|
||||
defineProps({ data: { type: Object, default: () => ({}) } });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuroraCard title="Page Views" subtitle="Traffic over the selected range">
|
||||
<StatRow
|
||||
:boxed="false"
|
||||
:stats="[
|
||||
{ value: data.total || 0, label: 'Total Views' },
|
||||
{ value: data.unique || 0, label: 'Unique Visitors' },
|
||||
]"
|
||||
/>
|
||||
<div class="pv__chart">
|
||||
<AreaChart :series="data.series || []" :height="150" />
|
||||
</div>
|
||||
</AuroraCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pv__chart { margin-top: 20px; }
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import AuroraCard from '../components/AuroraCard.vue';
|
||||
import StatRow from '../components/StatRow.vue';
|
||||
|
||||
defineProps({ data: { type: Object, default: () => ({}) } });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuroraCard title="Recent Comments" subtitle="Latest comments and discussions">
|
||||
<template #head-right>
|
||||
<span class="dot dot--accent" /> {{ data.total || 0 }} total
|
||||
</template>
|
||||
|
||||
<StatRow
|
||||
:stats="[
|
||||
{ value: data.approved || 0, label: 'Approved' },
|
||||
{ value: data.pending || 0, label: 'Pending' },
|
||||
{ value: data.spam || 0, label: 'Spam' },
|
||||
]"
|
||||
/>
|
||||
|
||||
<ul class="cm__list">
|
||||
<li v-for="c in data.latest" :key="c.id" class="cm__item">
|
||||
<p class="cm__excerpt">{{ c.excerpt }}</p>
|
||||
<div class="cm__meta">on <span class="cm__post">{{ c.postTitle }}</span> · {{ c.date }}</div>
|
||||
</li>
|
||||
<li v-if="!data.latest || !data.latest.length" class="cm__empty">No comments yet.</li>
|
||||
</ul>
|
||||
|
||||
<template #footer>
|
||||
<a :href="data.listUrl" class="viewall">View all ›</a>
|
||||
</template>
|
||||
</AuroraCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cm__list { list-style: none; margin: 16px 0 0; padding: 0; }
|
||||
.cm__item { padding: 8px 0; }
|
||||
.cm__excerpt { margin: 0; font-size: 0.8125rem; color: var(--aurora-text); }
|
||||
.cm__meta { margin-top: 4px; font-size: 0.75rem; color: var(--aurora-text-muted); }
|
||||
.cm__post { color: var(--aurora-accent); }
|
||||
.cm__empty { font-size: 0.8125rem; color: var(--aurora-text-muted); padding: 12px 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup>
|
||||
import AuroraCard from '../components/AuroraCard.vue';
|
||||
import StatRow from '../components/StatRow.vue';
|
||||
|
||||
const props = defineProps({ data: { type: Object, default: () => ({}) } });
|
||||
|
||||
const statusLabel = (s) =>
|
||||
({ publish: 'Published', draft: 'Draft', future: 'Scheduled' }[s] || s);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuroraCard title="Recent content" subtitle="Latest content activity">
|
||||
<template #head-right>
|
||||
<span class="dot dot--accent" /> {{ data.total || 0 }} total
|
||||
</template>
|
||||
|
||||
<StatRow
|
||||
:stats="[
|
||||
{ value: data.published || 0, label: 'Published' },
|
||||
{ value: data.draft || 0, label: 'Draft' },
|
||||
{ value: data.scheduled || 0, label: 'Scheduled' },
|
||||
]"
|
||||
/>
|
||||
|
||||
<ul class="rc__list">
|
||||
<li v-for="p in data.latest" :key="p.id" class="rc__item">
|
||||
<div class="rc__main">
|
||||
<a :href="p.editUrl" class="rc__title">{{ p.title || '(no title)' }}</a>
|
||||
<div class="rc__meta">{{ p.author }} · {{ p.date }}</div>
|
||||
</div>
|
||||
<span class="rc__badge" :class="`rc__badge--${p.status}`">
|
||||
{{ statusLabel(p.status) }}
|
||||
</span>
|
||||
</li>
|
||||
<li v-if="!data.latest || !data.latest.length" class="rc__empty">No content yet.</li>
|
||||
</ul>
|
||||
|
||||
<template #footer>
|
||||
<a :href="data.listUrl" class="viewall">View all ›</a>
|
||||
</template>
|
||||
</AuroraCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rc__list { list-style: none; margin: 16px 0 0; padding: 0; }
|
||||
.rc__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.rc__title {
|
||||
color: var(--aurora-text);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.rc__title:hover { color: var(--aurora-accent); }
|
||||
.rc__meta { font-size: 0.75rem; color: var(--aurora-text-muted); margin-top: 2px; }
|
||||
.rc__badge {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rc__badge--publish { color: #34d399; }
|
||||
.rc__badge--future { color: var(--aurora-accent); }
|
||||
.rc__badge--draft { color: var(--aurora-text-muted); }
|
||||
.rc__empty { font-size: 0.8125rem; color: var(--aurora-text-muted); padding: 12px 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import AuroraCard from '../components/AuroraCard.vue';
|
||||
|
||||
const props = defineProps({ data: { type: Object, default: () => ({}) } });
|
||||
|
||||
const view = ref(new Date());
|
||||
|
||||
const monthLabel = computed(() =>
|
||||
view.value.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })
|
||||
);
|
||||
|
||||
const weekdays = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
|
||||
|
||||
// Build a calendar grid (Mon-first) for the viewed month.
|
||||
const grid = computed(() => {
|
||||
const year = view.value.getFullYear();
|
||||
const month = view.value.getMonth();
|
||||
const first = new Date(year, month, 1);
|
||||
const startOffset = (first.getDay() + 6) % 7; // Mon = 0
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
const cells = [];
|
||||
for (let i = 0; i < startOffset; i++) cells.push(null);
|
||||
const today = new Date();
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const key = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
||||
const scheduled = props.data.days && props.data.days[key] ? props.data.days[key].length : 0;
|
||||
const isToday =
|
||||
d === today.getDate() && month === today.getMonth() && year === today.getFullYear();
|
||||
cells.push({ d, key, scheduled, isToday });
|
||||
}
|
||||
return cells;
|
||||
});
|
||||
|
||||
const move = (delta) => {
|
||||
const next = new Date(view.value);
|
||||
next.setMonth(next.getMonth() + delta);
|
||||
view.value = next;
|
||||
};
|
||||
|
||||
// Clicking a day: with scheduled content, open the posts list filtered to
|
||||
// that day; otherwise show a transient popup on the dashboard.
|
||||
const toast = ref('');
|
||||
let toastTimer;
|
||||
const onDayClick = (cell) => {
|
||||
if (!cell) return;
|
||||
if (cell.scheduled > 0) {
|
||||
// Relative to /wp-admin/ (the dashboard is at index.php).
|
||||
window.location.href = `edit.php?post_status=future&aurora_day=${cell.key}`;
|
||||
} else {
|
||||
toast.value = 'No scheduled content for this day.';
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => (toast.value = ''), 3000);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuroraCard title="Scheduled Content" :subtitle="monthLabel">
|
||||
<template #head-right>
|
||||
<div class="sc__nav">
|
||||
<button type="button" @click="move(-1)" aria-label="Previous month">‹</button>
|
||||
<button type="button" @click="move(1)" aria-label="Next month">›</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="sc__count">
|
||||
<span class="sc__count-num">{{ data.count || 0 }}</span> Scheduled Content
|
||||
</div>
|
||||
|
||||
<div class="sc__weekdays">
|
||||
<span v-for="(w, i) in weekdays" :key="i">{{ w }}</span>
|
||||
</div>
|
||||
<div class="sc__grid">
|
||||
<div v-for="(cell, i) in grid" :key="i" class="sc__cell">
|
||||
<button
|
||||
v-if="cell"
|
||||
type="button"
|
||||
class="sc__day"
|
||||
:class="{ 'sc__day--has': cell.scheduled > 0, 'sc__day--today': cell.isToday }"
|
||||
:title="cell.scheduled > 0 ? `${cell.scheduled} scheduled` : 'No scheduled content'"
|
||||
@click="onDayClick(cell)"
|
||||
>
|
||||
<span class="sc__date">{{ cell.d }}</span>
|
||||
<span v-if="cell.scheduled > 0" class="sc__count">{{ cell.scheduled }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="sc-toast">
|
||||
<div v-if="toast" class="sc__toast">{{ toast }}</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</AuroraCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sc__nav { display: flex; gap: 4px; }
|
||||
.sc__nav button {
|
||||
width: 26px; height: 26px;
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text-muted);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
.sc__nav button:hover { color: var(--aurora-text); }
|
||||
.sc__count { font-size: 0.875rem; color: var(--aurora-text-muted); margin-bottom: 16px; }
|
||||
.sc__count-num { font-size: 1.5rem; font-weight: 700; color: var(--aurora-text); margin-right: 6px; }
|
||||
.sc__weekdays {
|
||||
display: grid; grid-template-columns: repeat(7, 1fr);
|
||||
font-size: 0.6875rem; color: var(--aurora-text-muted); margin-bottom: 8px;
|
||||
}
|
||||
.sc__weekdays span { text-align: center; }
|
||||
.sc__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
|
||||
.sc__cell { aspect-ratio: 1; }
|
||||
.sc__day {
|
||||
position: relative;
|
||||
width: 100%; height: 100%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.12s ease, background-color 0.12s ease;
|
||||
}
|
||||
.sc__day:hover { border-color: var(--aurora-accent); }
|
||||
.sc__day--has:hover { filter: brightness(1.08); }
|
||||
.sc__date {
|
||||
position: absolute; top: 5px; left: 6px;
|
||||
font-size: 0.7rem; line-height: 1; color: var(--aurora-text-muted);
|
||||
}
|
||||
.sc__count {
|
||||
font-size: 1.05rem; font-weight: 700; color: var(--aurora-text);
|
||||
}
|
||||
.sc__day--has { background: var(--aurora-accent); border-color: var(--aurora-accent); }
|
||||
.sc__day--has .sc__date { color: rgba(255, 255, 255, 0.85); }
|
||||
.sc__day--has .sc__count { color: #fff; }
|
||||
.sc__day--today { border-color: var(--aurora-accent); }
|
||||
.sc__day--today .sc__date { color: var(--aurora-accent); }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Toast is teleported to <body>, so these rules are global (not scoped).
|
||||
They rely on the global --aurora-* theme variables. */
|
||||
.sc__toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100001;
|
||||
background: var(--aurora-card-bg, #1f1f23);
|
||||
color: var(--aurora-text, #f4f4f5);
|
||||
border: 1px solid var(--aurora-border, #2e2e33);
|
||||
border-radius: 10px;
|
||||
padding: 12px 18px;
|
||||
font-size: 0.875rem;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.sc-toast-enter-active,
|
||||
.sc-toast-leave-active { transition: opacity 0.2s ease, transform 0.2s ease; }
|
||||
.sc-toast-enter-from,
|
||||
.sc-toast-leave-to { opacity: 0; transform: translate(-50%, 10px); }
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup>
|
||||
import AuroraCard from '../components/AuroraCard.vue';
|
||||
import StatRow from '../components/StatRow.vue';
|
||||
import ProgressBar from '../components/ProgressBar.vue';
|
||||
|
||||
defineProps({ data: { type: Object, default: () => ({}) } });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuroraCard title="Server Health" subtitle="System performance and updates">
|
||||
<template #head-right>
|
||||
<span class="dot dot--green" /> {{ data.updates || 0 }} updates
|
||||
</template>
|
||||
|
||||
<div class="sh__versions">
|
||||
<div class="sh__ver">
|
||||
<div class="sh__ver-label">‹› WordPress</div>
|
||||
<div class="sh__ver-num">{{ data.wp || '—' }}</div>
|
||||
</div>
|
||||
<div class="sh__ver">
|
||||
<div class="sh__ver-label">⚙ PHP</div>
|
||||
<div class="sh__ver-num">{{ data.php || '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sh__bars">
|
||||
<ProgressBar
|
||||
label="Memory Usage"
|
||||
icon="◍"
|
||||
:pct="data.memory ? data.memory.pct : 0"
|
||||
:detail="data.memory ? `${data.memory.used} / ${data.memory.total}` : ''"
|
||||
/>
|
||||
<ProgressBar
|
||||
label="Disk Space"
|
||||
icon="▤"
|
||||
gradient
|
||||
:pct="data.disk ? data.disk.pct : 0"
|
||||
:detail="data.disk ? `${data.disk.used} / ${data.disk.total}` : ''"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<StatRow
|
||||
:stats="[
|
||||
{ value: data.plugins || 0, label: 'Plugins' },
|
||||
{ value: data.themes || 0, label: 'Themes' },
|
||||
{ value: data.core || 0, label: 'Core' },
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="sh__foot">
|
||||
<span>{{ data.ssl ? '🔒 SSL enabled' : '⚠ SSL disabled' }}</span>
|
||||
<span>🕓 Timezone: {{ data.timezone || 'UTC' }}</span>
|
||||
</div>
|
||||
</AuroraCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sh__versions { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 14px; }
|
||||
.sh__ver {
|
||||
background: var(--aurora-bg-subtle);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.sh__ver-label { font-size: 0.6875rem; color: var(--aurora-text-muted); }
|
||||
.sh__ver-num { font-size: 1rem; font-weight: 600; color: var(--aurora-text); margin-top: 2px; }
|
||||
.sh__bars { display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px; }
|
||||
.sh__foot {
|
||||
display: flex; justify-content: space-between;
|
||||
margin-top: 16px; font-size: 0.6875rem; color: var(--aurora-text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
import AuroraCard from '../components/AuroraCard.vue';
|
||||
import BarList from '../components/BarList.vue';
|
||||
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
subtitle: { type: String, default: '' },
|
||||
items: { type: Array, default: () => [] },
|
||||
emptyText: { type: String, default: 'No data in this range yet.' },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuroraCard :title="title" :subtitle="subtitle">
|
||||
<BarList :items="items" :empty-text="emptyText" />
|
||||
</AuroraCard>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import AuroraCard from '../components/AuroraCard.vue';
|
||||
import StatRow from '../components/StatRow.vue';
|
||||
import AreaChart from '../components/AreaChart.vue';
|
||||
|
||||
defineProps({ data: { type: Object, default: () => ({}) } });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuroraCard title="User Analytics" subtitle="User registration trends">
|
||||
<StatRow
|
||||
:boxed="false"
|
||||
:stats="[
|
||||
{ value: data.total || 0, label: 'Total Users' },
|
||||
{ value: data.inRange || 0, label: 'In Range' },
|
||||
{ value: data.recent7d || 0, label: 'Recent (7d)' },
|
||||
]"
|
||||
/>
|
||||
<div class="ua__chart">
|
||||
<AreaChart :series="data.series || []" :height="150" />
|
||||
</div>
|
||||
</AuroraCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ua__chart { margin-top: 20px; }
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
// series: [{ date, count }]
|
||||
const props = defineProps({
|
||||
series: { type: Array, default: () => [] },
|
||||
height: { type: Number, default: 140 },
|
||||
});
|
||||
|
||||
const W = 600;
|
||||
const PAD = 10;
|
||||
|
||||
const view = computed(() => {
|
||||
const s = props.series;
|
||||
if (!s.length) return null;
|
||||
const max = Math.max(1, ...s.map((d) => d.count));
|
||||
const H = props.height;
|
||||
const innerW = W - PAD * 2;
|
||||
const innerH = H - PAD * 2;
|
||||
const stepX = s.length > 1 ? innerW / (s.length - 1) : 0;
|
||||
const points = s.map((d, i) => {
|
||||
const x = PAD + i * stepX;
|
||||
const y = PAD + innerH - (d.count / max) * innerH;
|
||||
return [x, y];
|
||||
});
|
||||
const line = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ');
|
||||
const area = `${line} L${points[points.length - 1][0].toFixed(1)},${(H - PAD).toFixed(1)} L${points[0][0].toFixed(1)},${(H - PAD).toFixed(1)} Z`;
|
||||
return { points, line, area, H };
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="areachart" v-if="view">
|
||||
<svg :viewBox="`0 0 ${W} ${view.H}`" preserveAspectRatio="none" class="areachart__svg">
|
||||
<defs>
|
||||
<linearGradient id="aurora-area-grad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="var(--aurora-accent)" stop-opacity="0.35" />
|
||||
<stop offset="100%" stop-color="var(--aurora-accent)" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path :d="view.area" fill="url(#aurora-area-grad)" />
|
||||
<path :d="view.line" fill="none" stroke="var(--aurora-accent)" stroke-width="2"
|
||||
vector-effect="non-scaling-stroke" />
|
||||
<circle v-for="(p, i) in view.points" :key="i" :cx="p[0]" :cy="p[1]" r="3"
|
||||
fill="var(--aurora-accent)" vector-effect="non-scaling-stroke" />
|
||||
</svg>
|
||||
<div class="areachart__labels">
|
||||
<span v-for="(d, i) in series" :key="i">{{ d.date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.areachart__svg {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.areachart__labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 6px;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.areachart__labels span {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.areachart__labels span:first-child { text-align: left; }
|
||||
.areachart__labels span:last-child { text-align: right; }
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
subtitle: { type: String, default: '' },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="acard">
|
||||
<header class="acard__head">
|
||||
<div class="acard__titles">
|
||||
<h2 class="acard__title">{{ title }}</h2>
|
||||
<p v-if="subtitle" class="acard__subtitle">{{ subtitle }}</p>
|
||||
</div>
|
||||
<div class="acard__head-right">
|
||||
<slot name="head-right" />
|
||||
</div>
|
||||
</header>
|
||||
<div class="acard__body">
|
||||
<slot />
|
||||
</div>
|
||||
<footer v-if="$slots.footer" class="acard__foot">
|
||||
<slot name="footer" />
|
||||
</footer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.acard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--aurora-card-bg);
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.acard__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.acard__title {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 600;
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.acard__subtitle {
|
||||
margin: 2px 0 0;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.acard__head-right {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.acard__body {
|
||||
flex: 1;
|
||||
}
|
||||
.acard__foot {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
// items: [{ label, count }]
|
||||
const props = defineProps({
|
||||
items: { type: Array, default: () => [] },
|
||||
emptyText: { type: String, default: 'No data yet.' },
|
||||
});
|
||||
|
||||
const max = computed(() => Math.max(1, ...props.items.map((i) => i.count)));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="barlist">
|
||||
<div v-for="(it, i) in items" :key="i" class="barlist__row">
|
||||
<div class="barlist__head">
|
||||
<span class="barlist__label" :title="it.label">{{ it.label }}</span>
|
||||
<span class="barlist__count">{{ it.count }}</span>
|
||||
</div>
|
||||
<div class="barlist__track">
|
||||
<div class="barlist__fill" :style="{ width: (it.count / max) * 100 + '%' }" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!items.length" class="barlist__empty">{{ emptyText }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.barlist__row { margin-bottom: 12px; }
|
||||
.barlist__head {
|
||||
display: flex; justify-content: space-between; gap: 12px; margin-bottom: 5px;
|
||||
}
|
||||
.barlist__label {
|
||||
font-size: 0.8125rem; color: var(--aurora-text);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.barlist__count { font-size: 0.8125rem; color: var(--aurora-text-muted); flex-shrink: 0; }
|
||||
.barlist__track {
|
||||
height: 6px; border-radius: 999px;
|
||||
background: rgba(127,127,127,0.18); overflow: hidden;
|
||||
}
|
||||
.barlist__fill { height: 100%; border-radius: 999px; background: var(--aurora-accent); }
|
||||
.barlist__empty { font-size: 0.8125rem; color: var(--aurora-text-muted); padding: 8px 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
label: { type: String, required: true },
|
||||
pct: { type: Number, default: 0 },
|
||||
detail: { type: String, default: '' },
|
||||
gradient: { type: Boolean, default: false },
|
||||
icon: { type: String, default: '' },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pbar">
|
||||
<div class="pbar__top">
|
||||
<span class="pbar__label">
|
||||
<span v-if="icon" class="pbar__icon">{{ icon }}</span>{{ label }}
|
||||
</span>
|
||||
<span class="pbar__pct">{{ pct }}%</span>
|
||||
</div>
|
||||
<div class="pbar__track">
|
||||
<div
|
||||
class="pbar__fill"
|
||||
:class="{ 'pbar__fill--gradient': gradient }"
|
||||
:style="{ width: Math.min(100, pct) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="detail" class="pbar__detail">{{ detail }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pbar {
|
||||
background: var(--aurora-bg-subtle);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.pbar__top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.pbar__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.pbar__icon { color: var(--aurora-text-muted); }
|
||||
.pbar__pct {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #34d399;
|
||||
}
|
||||
.pbar__track {
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(127, 127, 127, 0.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
.pbar__fill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: var(--aurora-accent);
|
||||
}
|
||||
.pbar__fill--gradient {
|
||||
background: linear-gradient(90deg, #4f7bf5 0%, #a855f7 55%, #ef4444 100%);
|
||||
}
|
||||
.pbar__detail {
|
||||
margin-top: 6px;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup>
|
||||
// stats: [{ value, label }]
|
||||
defineProps({
|
||||
stats: { type: Array, required: true },
|
||||
boxed: { type: Boolean, default: true },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="statrow" :class="{ 'statrow--boxed': boxed }">
|
||||
<div v-for="(s, i) in stats" :key="i" class="statrow__item">
|
||||
<div class="statrow__value">{{ s.value }}</div>
|
||||
<div class="statrow__label">{{ s.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.statrow {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
.statrow--boxed {
|
||||
background: var(--aurora-bg-subtle);
|
||||
border-radius: 14px;
|
||||
padding: 16px 8px;
|
||||
}
|
||||
.statrow__value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--aurora-text);
|
||||
line-height: 1.1;
|
||||
}
|
||||
.statrow__label {
|
||||
margin-top: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/** Fetches aggregated analytics from aurora-admin/v1/analytics for a range. */
|
||||
export function useAnalyticsData(serverData) {
|
||||
const data = ref(null);
|
||||
const loading = ref(false);
|
||||
const error = ref(false);
|
||||
|
||||
const fmt = (d) => {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
};
|
||||
|
||||
const load = async (from, to) => {
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
try {
|
||||
const url = new URL(`${serverData.restUrl}aurora-admin/v1/analytics`);
|
||||
url.searchParams.set('from', fmt(from));
|
||||
url.searchParams.set('to', fmt(to));
|
||||
const res = await fetch(url, { headers: { 'X-WP-Nonce': serverData.restNonce } });
|
||||
if (!res.ok) throw new Error('Request failed');
|
||||
data.value = await res.json();
|
||||
} catch {
|
||||
error.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return { data, loading, error, load };
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* Fetches all dashboard card data from aurora-admin/v1/dashboard for a
|
||||
* given date range. Re-callable when the range changes.
|
||||
*/
|
||||
export function useDashboardData(serverData) {
|
||||
const data = ref(null);
|
||||
const loading = ref(false);
|
||||
const error = ref(false);
|
||||
|
||||
const fmt = (d) => {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
};
|
||||
|
||||
const load = async (from, to) => {
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
try {
|
||||
const url = new URL(`${serverData.restUrl}aurora-admin/v1/dashboard`);
|
||||
url.searchParams.set('from', fmt(from));
|
||||
url.searchParams.set('to', fmt(to));
|
||||
const res = await fetch(url, {
|
||||
headers: { 'X-WP-Nonce': serverData.restNonce },
|
||||
});
|
||||
if (!res.ok) throw new Error('Request failed');
|
||||
data.value = await res.json();
|
||||
} catch {
|
||||
error.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return { data, loading, error, load };
|
||||
}
|
||||
Reference in New Issue
Block a user