Add additional data to the dashboard, including today stats and score stats. Add score stats to chart.

This commit is contained in:
Trenton Zimmer
2025-05-27 14:11:36 -04:00
parent b11ae9a28c
commit 55f19fa846
6 changed files with 183 additions and 15 deletions

View File

@@ -1,4 +1,4 @@
VITE_APP_VERSION="3.0.8-shrimplinks"
VITE_APP_VERSION="3.0.9"
VITE_API_URL="http://10.5.7.5:8000/"
VITE_API_KEY="your_api_key_should_be_here"
VITE_ASSET_PATH="/assets"

View File

@@ -1,4 +1,4 @@
VITE_APP_VERSION="3.0.8-shrimplinks"
VITE_APP_VERSION="3.0.9"
VITE_API_URL="https://restfulsleep.phaseii.network"
VITE_API_KEY="your_api_key_should_be_here"
VITE_ASSET_PATH="https://cdn.phaseii.network/file/PhaseII/web-assets"

View File

@@ -7,5 +7,6 @@
"3.0.6": ["- (Major) Replace non-standard game cards with a standardized and new header card. May break some layouts so PLEASE REPORT BUGS!", "- (Feature) Create song detail page, not yet completed or reactive.", "- (Bugfix) Fix issue where deleteSession fails if the session has already been deleted.", "- (Minor) Edit ring color on red buttons", "- (Admin) Add Arcades admin page", "- (Bugfix) Fix incorrect prop types for Notes Radar"],
"3.0.7": ["- (Major) Song data page now loads data. This data is quite broken for a few games so EXPECT BUGS!", "- (Optimization) Add in table data for games, still missing some data but the basics are complete now."],
"3.0.8": ["- (Minor) Added game event options for Classic IIDX versions"],
"3.0.8-shrimplinks": ["- (Bugfix) Fix broken session."]
"3.0.8-shrimplinks": ["- (Bugfix) Fix broken session."],
"3.0.9": ["- (Minor) Add additional data to the dashboard, including today stats and score stats. Add score stats to chart."]
}

View File

@@ -27,6 +27,48 @@ const datasetObject = (color, points) => {
};
};
const datasetRecordObject = (color, points) => {
return {
fill: false,
borderColor: chartColors.default[color],
borderWidth: 2,
borderDash: [],
borderDashOffset: 0.0,
pointBackgroundColor: chartColors.default[color],
pointBorderColor: "rgba(255,255,255,0)",
pointHoverBackgroundColor: chartColors.default[color],
pointBorderWidth: 20,
pointHoverRadius: 4,
pointHoverBorderWidth: 15,
pointRadius: 4,
data: points,
tension: 0.5,
cubicInterpolationMode: "default",
label: "Records",
};
};
const datasetAttemptObject = (color, points) => {
return {
fill: false,
borderColor: chartColors.default[color],
borderWidth: 2,
borderDash: [],
borderDashOffset: 0.0,
pointBackgroundColor: chartColors.default[color],
pointBorderColor: "rgba(255,255,255,0)",
pointHoverBackgroundColor: chartColors.default[color],
pointBorderWidth: 20,
pointHoverRadius: 4,
pointHoverBorderWidth: 15,
pointRadius: 4,
data: points,
tension: 0.5,
cubicInterpolationMode: "default",
label: "Attempts",
};
};
const parseArcadeHistory = (users) => {
const groupByDay = (timestamps) => {
const dayMap = {};
@@ -67,7 +109,7 @@ const parseArcadeHistory = (users) => {
return { labels: last14Days, data: playCounts };
};
export const generateChartData = (users) => {
export const generateChartData = (users, scoreStats = null) => {
if (!Array.isArray(users) || users.length === 0) {
return {
labels: [],
@@ -76,9 +118,39 @@ export const generateChartData = (users) => {
}
const { labels, data } = parseArcadeHistory(users);
const baseLabels = labels;
const groupByDay = (timestamps) => {
const dayMap = {};
timestamps.forEach((timestamp) => {
const day = new Date(timestamp * 1000).toISOString().split("T")[0];
dayMap[day] = (dayMap[day] || 0) + 1;
});
return dayMap;
};
const recordsData = [];
const attemptsData = [];
if (scoreStats?.records && scoreStats?.attempts) {
const recordTimestamps = scoreStats.records.map((r) => r.timestamp);
const attemptTimestamps = scoreStats.attempts.map((a) => a.timestamp);
const recordCountsByDay = groupByDay(recordTimestamps);
const attemptCountsByDay = groupByDay(attemptTimestamps);
baseLabels.forEach((day) => {
recordsData.push(recordCountsByDay[day] || 0);
attemptsData.push(attemptCountsByDay[day] || 0);
});
}
return {
labels,
datasets: [datasetObject("info", data)],
labels: baseLabels,
datasets: [
datasetObject("info", data),
datasetRecordObject("primary", recordsData),
datasetAttemptObject("danger", attemptsData),
],
};
};

View File

@@ -20,6 +20,7 @@ export const useMainStore = defineStore("main", {
userArcades: [],
profiles: {},
userCustomize: {},
userScoreStats: {},
/* Field focus with ctrl+k (to register only once) */
isFieldFocusRegistered: false,
@@ -76,6 +77,9 @@ export const useMainStore = defineStore("main", {
if (payload.customize) {
this.userCustomize = payload.customize;
}
if (payload.userScoreStats) {
this.userScoreStats = payload.userScoreStats;
}
},
fetch(sampleDataKey) {
@@ -268,6 +272,7 @@ export const useMainStore = defineStore("main", {
profiles: user.profiles,
arcades: user.arcades,
customize: user.data?.customize,
userScoreStats: user.scoreStats,
});
this.userLoaded = true;
return true;

View File

@@ -1,5 +1,5 @@
<script setup>
import { computed, ref, onMounted, watch } from "vue";
import { computed, ref, onMounted } from "vue";
import {
mdiGamepad,
mdiNewspaperVariant,
@@ -41,13 +41,8 @@ function humanReadableTime(timestamp) {
return date.toLocaleString();
}
const userProfiles = ref(mainStore.userProfiles);
watch(
() => mainStore.userProfiles,
(newValue) => {
userProfiles.value = newValue;
}
);
const userProfiles = computed(() => mainStore.userProfiles);
const userScoreStats = computed(() => mainStore.userScoreStats);
const cumulativePlays = computed(() => {
return userProfiles.value.reduce(
@@ -104,6 +99,60 @@ function filterUserProfiles(userProfiles) {
return filteredProfiles;
}
const today = new Date().toISOString().split("T")[0];
const totalAttempts = computed(() => {
return userScoreStats.value?.attempts?.length || 0;
});
const totalRecords = computed(() => {
return userScoreStats.value?.records?.length || 0;
});
const todayAttempts = computed(() => {
return (
userScoreStats.value?.attempts?.filter((a) => {
const attemptDate = new Date(a.timestamp * 1000)
.toISOString()
.split("T")[0];
return attemptDate === today;
}).length || 0
);
});
const todayRecords = computed(() => {
return (
userScoreStats.value?.records?.filter((r) => {
const recordDate = new Date(r.timestamp * 1000)
.toISOString()
.split("T")[0];
return recordDate === today;
}).length || 0
);
});
const todayPlays = computed(() => {
const todayStr = new Date().toISOString().split("T")[0];
let total = 0;
userProfiles.value.forEach((profile) => {
const arcadeHistory = profile.data?.arcade_history || {};
Object.values(arcadeHistory).forEach((machines) => {
Object.values(machines).forEach((timestamps) => {
timestamps.forEach((ts) => {
const dateStr = new Date(ts * 1000).toISOString().split("T")[0];
if (dateStr === todayStr) {
total++;
}
});
});
});
});
return total;
});
</script>
<template>
@@ -152,6 +201,14 @@ function filterUserProfiles(userProfiles) {
suffix="games"
icon-color="text-sky-300"
/>
<CardBoxWidget
v-if="todayPlays"
:icon="mdiGamepadOutline"
:number="todayPlays"
label="Plays Today"
suffix="games"
icon-color="text-sky-300"
/>
<CardBoxWidget
:icon="mdiFire"
:number="longestStreak"
@@ -159,6 +216,36 @@ function filterUserProfiles(userProfiles) {
suffix="plays"
icon-color="text-red-600"
/>
<CardBoxWidget
:icon="mdiGamepadOutline"
:number="totalRecords"
label="Total Records"
suffix="records"
icon-color="text-sky-300"
/>
<CardBoxWidget
:icon="mdiGamepadOutline"
:number="totalAttempts"
label="Total Attempts"
suffix="attempts"
icon-color="text-sky-300"
/>
<CardBoxWidget
v-if="todayRecords"
:icon="mdiGamepadOutline"
:number="todayRecords"
label="Records Today"
suffix="records"
icon-color="text-sky-300"
/>
<CardBoxWidget
v-if="todayAttempts"
:icon="mdiGamepadOutline"
:number="todayAttempts"
label="Attempts Today"
suffix="attempts"
icon-color="text-sky-300"
/>
</div>
<SectionTitleLine :icon="mdiGamepad" title="Showcase" main />
@@ -178,7 +265,10 @@ function filterUserProfiles(userProfiles) {
<SectionTitleLine :icon="mdiTrendingUp" title="Play Trends" main />
<CardBox class="mb-6">
<div v-if="userProfiles">
<LineChart :data="generateChartData(userProfiles)" class="h-96" />
<LineChart
:data="generateChartData(userProfiles, userScoreStats)"
class="h-96"
/>
</div>
</CardBox>
</SectionMain>