Add Hitchart and Event schedule to game homepage
Some checks failed
Build / build (push) Has been cancelled

This commit is contained in:
Trenton Zimmer
2025-12-17 17:01:15 -05:00
parent 3da000cfcf
commit 08da5d5da7
13 changed files with 515 additions and 45 deletions

View File

@@ -1,4 +1,4 @@
VITE_APP_VERSION="3.0.38"
VITE_APP_VERSION="3.0.39"
VITE_API_URL="http://localhost: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.38"
VITE_APP_VERSION="3.0.39"
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

@@ -37,5 +37,6 @@
"3.0.35": ["- (Bugfix) Fix cursor hover state on news card and profile card", "- (Minor) Update DDR game options", "- (Minor) Update IIDX event settings", "- (Minor) Add IIDX 32 basic QPro settings"],
"3.0.36": ["- (Major) Finish more game metadata (DRS, Museca, Nostalgia, pop'n, jubeat)"],
"3.0.37": ["- (Major) Finish more game metadata (RB, SDVX, FTT)", "- (Bugfix) Fix personal record button showing for games without scores."],
"3.0.38": ["- (Major) Finish IIDX base metadata", "- (Minor) Add new greeting"]
"3.0.38": ["- (Major) Finish IIDX base metadata", "- (Minor) Add new greeting"],
"3.0.39": ["- (Major) Add Hitchart and Event schedule to game homepage", "- (Bugfix) Fix breaking jubeat Copius title"]
}

View File

@@ -0,0 +1,25 @@
<script setup>
import { defineProps } from "vue";
defineProps({
musicData: {
type: Object,
required: true,
},
gameId: {
type: String,
required: true,
},
});
</script>
<template>
<div class="flex mt-3">
<a
class="bg-slate-700 p-2 rounded-xl hover:text-slate-300"
:href="`/games/${gameId}/song/${musicData.id}`"
>
{{ musicData.name }} - {{ musicData.artist }}
</a>
</div>
</template>

View File

@@ -7,7 +7,24 @@ import { exportJSON } from "@/helpers/contentExport";
const $route = useRoute();
const props = defineProps(["headers", "items", "hasAvatar"]);
const props = defineProps({
headers: {
type: Array,
required: true,
},
items: {
type: Array,
required: true,
},
hasAvatar: {
type: Boolean,
default: false,
},
rowsPerPage: {
type: Number,
default: 25,
},
});
const emits = defineEmits(["row-clicked"]);
const handleRowClick = (item) => {
@@ -32,6 +49,7 @@ function downloadJSON() {
body-text-direction="left"
:prevent-context-menu-row="false"
class="pt-2"
:rows-per-page="rowsPerPage"
@click-row="handleRowClick"
>
<template #loading>

View File

@@ -14,6 +14,10 @@ import { ReflecBeatOptions } from "@/constants/gameOptions/reflecOptions";
import { SDVXOptions } from "@/constants/gameOptions/sdvxOptions";
import { WinningElevenOptions } from "@/constants/gameOptions/weOptions";
import { SilentScopeOptions } from "@/constants/gameOptions/silentScopeOptions";
import { DDREvents } from "@/constants/scheduledEvents/ddrEvents";
import { IIDXEvents } from "@/constants/scheduledEvents/iidxEvents";
import { JubeatEvents } from "@/constants/scheduledEvents/jubeatEvents";
const ASSET_PATH = import.meta.env.VITE_ASSET_PATH;
export class GameConstants {
@@ -408,7 +412,6 @@ export const gameData = [
cardBG: null,
noRivals: true,
noScores: true,
noRecords: true,
skip: true,
useUnicode: true,
gameOptions: BishiBashiOptions,
@@ -425,7 +428,6 @@ export const gameData = [
skip: true,
noRivals: true,
noScores: true,
noRecords: true,
},
{
id: GameConstants.DANCE_EVOLUTION,
@@ -434,7 +436,6 @@ export const gameData = [
cardBG: null,
noRivals: true,
noScores: true,
noRecords: true,
skip: true,
useUnicode: true,
},
@@ -484,6 +485,7 @@ export const gameData = [
icon: null,
cardBG: null,
gameOptions: DDROptions,
scheduledEvents: DDREvents,
useActiveRival: true,
videoTable: [
VersionConstants.DDR_ACE,
@@ -648,7 +650,6 @@ export const gameData = [
assetId: "ddr",
noRivals: true,
noScores: true,
noRecords: true,
gameOptions: DDRClassOptions,
versions: [
{
@@ -956,7 +957,6 @@ export const gameData = [
skip: true,
noRivals: true,
noScores: true,
noRecords: true,
versions: [
{
id: VersionConstants.EEMALL,
@@ -975,7 +975,6 @@ export const gameData = [
cardBG: null,
noRivals: true,
noScores: true,
noRecords: true,
useUnicode: true,
versions: [
{
@@ -1239,6 +1238,7 @@ export const gameData = [
cardBG: `${ASSET_PATH}/card/iidx.webp`,
assetId: "iidx",
gameOptions: IIDXOptions,
scheduledEvents: IIDXEvents,
splitRivals: true,
maxLength: 6,
videoTable: [
@@ -1473,7 +1473,6 @@ export const gameData = [
gameOptions: IIDXClassOptions,
noRivals: true,
noScores: true,
noRecords: true,
playerHeaders: [
{ text: "SP DAN", value: "sgrade", width: 100 },
{ text: "DP DAN", value: "dgrade", width: 100 },
@@ -1574,6 +1573,7 @@ export const gameData = [
name: "jubeat",
icon: null,
cardBG: null,
scheduledEvents: JubeatEvents,
scoreHeaders: [
{ text: "Grade", value: "data.rank", width: 80 },
{ text: "Combos", value: "data.combo", width: 80 },
@@ -1699,7 +1699,6 @@ export const gameData = [
cardBG: null,
noRivals: true,
noScores: true,
noRecords: true,
skip: true,
playerHeaders: [
{ text: "Girlfriend", value: "gf", sortable: true, width: 100 },
@@ -1724,7 +1723,6 @@ export const gameData = [
skip: true,
noRivals: true,
noScores: true,
noRecords: true,
},
{
id: GameConstants.MUSECA,
@@ -1812,7 +1810,6 @@ export const gameData = [
skip: true,
noRivals: true,
noScores: true,
noRecords: true,
},
{
id: GameConstants.OTOCA,
@@ -1822,7 +1819,6 @@ export const gameData = [
skip: true,
noRivals: true,
noScores: true,
noRecords: true,
},
{
id: GameConstants.PASELI_CHARGE_MACHINE,
@@ -1830,7 +1826,6 @@ export const gameData = [
skip: true,
noRivals: true,
noScores: true,
noRecords: true,
gameOptions: PaseliChargeOptions,
},
{
@@ -1842,7 +1837,6 @@ export const gameData = [
skip: true,
noRivals: true,
noScores: true,
noRecords: true,
},
{
id: GameConstants.POPN_MUSIC,
@@ -2031,7 +2025,6 @@ export const gameData = [
skip: true,
noRivals: true,
noScores: true,
noRecords: true,
},
{
id: GameConstants.REFLEC_BEAT,
@@ -2125,7 +2118,6 @@ export const gameData = [
skip: true,
noRivals: true,
noScores: true,
noRecords: true,
},
{
id: GameConstants.SDVX,
@@ -2211,7 +2203,6 @@ export const gameData = [
skip: true,
noRivals: true,
noScores: true,
noRecords: true,
useUnicode: true,
gameOptions: SilentScopeOptions,
},
@@ -2221,7 +2212,6 @@ export const gameData = [
skip: false,
noRivals: true,
noScores: false,
noRecords: true,
useUnicode: true,
chartTable: {
0: "Game Mode",
@@ -2236,7 +2226,6 @@ export const gameData = [
skip: false,
noRivals: true,
noScores: true,
noRecords: true,
gameOptions: WinningElevenOptions,
versions: [
{

View File

@@ -0,0 +1,9 @@
export const DDREvents = {
15: [
{
id: "yuniver_charts",
name: "YuniverHills Songs",
duration: "weekly",
},
],
};

View File

@@ -0,0 +1,108 @@
export const IIDXEvents = {
20: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
],
21: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
],
22: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
],
23: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
],
24: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
],
25: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
],
26: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
],
27: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
],
28: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
],
29: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
],
30: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
{
id: "weekly",
name: "Weekly Chart",
duration: "weekly",
},
],
31: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
{
id: "weekly",
name: "Weekly Chart",
duration: "weekly",
},
],
32: [
{
id: "dailies",
name: "Daily Music",
duration: "daily",
},
{
id: "weekly",
name: "Weekly Chart",
duration: "weekly",
},
],
};

View File

@@ -0,0 +1,63 @@
export const JubeatEvents = {
8: [
{
id: "fc_challenge",
name: "FULLCOMBO Challenge",
duration: "daily",
},
],
9: [
{
id: "fc_challenge",
name: "FULLCOMBO Challenge",
duration: "daily",
},
],
10: [
{
id: "fc_challenge",
name: "FULLCOMBO Challenge",
duration: "daily",
},
{
id: "league",
name: "League Course",
duration: "weekly",
},
],
11: [
{
id: "fc_challenge",
name: "FULLCOMBO Challenge",
duration: "daily",
},
],
12: [
{
id: "fc_challenge",
name: "FULLCOMBO Challenge",
duration: "daily",
},
],
13: [
{
id: "fc_challenge",
name: "FULLCOMBO Challenge",
duration: "daily",
},
],
14: [
{
id: "fc_challenge",
name: "FULLCOMBO Challenge",
duration: "daily",
},
],
15: [
{
id: "fc_challenge",
name: "FULLCOMBO Challenge",
duration: "daily",
},
],
};

View File

@@ -20,7 +20,6 @@ export function hydrateScoreData(thisGame, data) {
songData.charts[chartId] = chart;
}
}
console.log(songData);
return songData;
}

View File

@@ -1,14 +1,12 @@
import { useMainStore } from "@/stores/main";
const mainStore = useMainStore();
export async function APIGetAllProfiles(game, version = null) {
export async function APIGetGame(game, version = null) {
try {
const data = await mainStore.callApi(
`/game/${game}/profiles?version=${version}`,
);
const data = await mainStore.callApi(`/game/${game}?version=${version}`);
return data.data;
} catch (error) {
console.log("Error fetching profiles:", error);
console.log("Error fetching game:", error);
throw error;
}
}

View File

@@ -1,7 +1,11 @@
<script setup>
import { reactive, ref, onMounted, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { PhUsersThree } from "@phosphor-icons/vue";
import {
PhUsersThree,
PhCalendarDots,
PhArrowLineUpRight,
} from "@phosphor-icons/vue";
import SectionMain from "@/components/SectionMain.vue";
import GameHeader from "@/components/Cards/GameHeader.vue";
import SectionTitleLine from "@/components/SectionTitleLine.vue";
@@ -10,18 +14,31 @@ import CardBox from "@/components/CardBox.vue";
import FormControl from "@/components/FormControl.vue";
import ProfileCard from "@/components/Cards/ProfileCard.vue";
import GeneralTable from "@/components/GeneralTable.vue";
import PillTag from "@/components/PillTag.vue";
import { APIGetProfile, APIGetAllProfiles } from "@/stores/api/profile";
import { APIGetProfile, APIGetGame } from "@/stores/api/profile";
import { APIGetMusicData } from "@/stores/api/music";
import { getGameInfo } from "@/constants";
import { dashCode } from "@/constants/userData";
import { getIIDXDan } from "@/constants/danClass";
import { formatSortableDate } from "@/constants/date";
import { useMainStore } from "@/stores/main";
import SongCardSmall from "@/components/Cards/SongCardSmall.vue";
const mainStore = useMainStore();
const $route = useRoute();
const $router = useRouter();
var gameID = null;
var thisGame = null;
const myProfile = ref(null);
const profiles = ref([]);
const hitchartData = ref(null);
const timeSensitiveData = ref([]);
const musicIds = ref([]);
const musicData = ref(null);
const versionEvents = ref(null);
const versionForm = reactive({
currentVersion: null,
});
@@ -29,7 +46,13 @@ const versionForm = reactive({
watch(
() => versionForm.currentVersion,
() => {
mainStore.continueLoad = true;
loadGame(versionForm.currentVersion);
loadProfile();
musicIds.value = [];
mainStore.continueLoad = false;
mainStore.isLoading = false;
mainStore.loadingPool.length = 0;
},
);
@@ -45,24 +68,36 @@ if (thisGame == null) {
});
}
const myProfile = ref(null);
const profiles = ref([]);
onMounted(async () => {
try {
const data = await APIGetAllProfiles(gameID);
profiles.value = formatProfiles(data);
} catch (error) {
console.error("Failed to fetch profile data:", error);
}
loadProfile();
mainStore.continueLoad = true;
await loadProfile();
await loadGame();
mainStore.continueLoad = false;
mainStore.isLoading = false;
mainStore.loadingPool.length = 0;
});
if (!thisGame.versions) {
versionForm.currentVersion = 1;
}
async function loadGame(version) {
try {
const data = await APIGetGame(gameID, version);
profiles.value = formatProfiles(data.profiles);
hitchartData.value = data.hitchart;
timeSensitiveData.value = data.scheduledEvents;
getVersionEvents();
musicIds.value = getHitchartIds(data.hitchart).concat(
getEventIds(data.scheduledEvents),
);
await getMusicData();
hitchartData.value = formatHitchart(data.hitchart);
} catch (error) {
console.error("Failed to fetch profile data:", error);
}
}
async function loadProfile() {
try {
myProfile.value = null;
@@ -164,6 +199,109 @@ const navigateToProfile = (item) => {
const userID = item.userId;
$router.push(`/games/${gameID}/profiles/${userID}`);
};
const navigateToSong = (item) => {
const songId = item.songid;
$router.push(`/games/${gameID}/song/${songId}`);
};
function getVersionEvents() {
if (!thisGame.scheduledEvents) {
return null;
}
versionEvents.value =
thisGame.scheduledEvents[versionForm.currentVersion] ?? null;
return versionEvents.value;
}
async function getMusicData() {
if (musicIds.value.length) {
const data = await APIGetMusicData(
gameID,
versionForm.currentVersion,
musicIds.value,
true,
);
musicData.value = data;
}
}
function getMusic(id) {
if (!musicData.value) {
return null;
}
return musicData.value.find((x) => x.id == id);
}
function getEventSetting(eventId) {
const setting = timeSensitiveData.value.find((x) => x.id == eventId);
return setting;
}
const hitchartHeaders = [
{ text: "Song", value: "name", width: 140, sortable: true },
{ text: "Artist", value: "artist", width: 140, sortable: true },
{ text: "Plays", value: "plays", width: 100, sortable: true },
];
function getHitchartIds(data) {
var hitchartIds = [];
for (const dataId in data) {
const songid = data[dataId][0];
hitchartIds.push(songid);
}
return hitchartIds;
}
function getEventIds(eventData) {
if (versionEvents.value == null) {
return [];
}
var eventIds = [];
for (const event of versionEvents.value) {
const setting = eventData.find((x) => x.id == event.id);
if (setting?.data?.music) {
if (Array.isArray(setting?.data.music)) {
for (const music of setting.data.music) {
eventIds.push(music);
}
} else {
eventIds.push(setting?.data.music);
}
}
if (setting?.data?.today) {
eventIds.push(setting?.data.today);
}
if (setting?.data?.whim) {
eventIds.push(setting?.data.whim);
}
}
return eventIds;
}
function formatHitchart(data) {
var formattedData = [];
for (const dataId in data) {
const songid = data[dataId][0];
const plays = data[dataId][1];
const song = getMusic(songid);
formattedData.push({
songid: songid,
plays: plays,
name: song?.name,
artist: song?.artist,
});
}
return formattedData;
}
</script>
<template>
@@ -195,7 +333,129 @@ const navigateToProfile = (item) => {
</div>
</GameHeader>
<SectionTitleLine :icon="PhUsersThree" title="All Players" main />
<div
v-if="versionEvents || !thisGame.noScores"
class="grid lg:grid-cols-2 items-stretch w-full gap-6 mb-6"
>
<div
v-if="!thisGame.noScores && hitchartData"
:class="{
'md:col-span-2': !versionEvents,
}"
>
<SectionTitleLine
:icon="PhArrowLineUpRight"
color="text-yellow-400"
title="Hitchart"
main
/>
<CardBox has-table>
<GeneralTable
:headers="hitchartHeaders"
:items="hitchartData"
:rows-per-page="5"
@row-clicked="navigateToSong"
/>
</CardBox>
</div>
<div v-if="versionEvents != null">
<SectionTitleLine
:icon="PhCalendarDots"
color="text-emerald-400"
title="Scheduled Events"
main
/>
<CardBox>
<div v-if="musicData" class="grid gap-6">
<template v-for="event of versionEvents" :key="event.id">
<template v-if="getEventSetting(event.id)">
<div v-if="event.id">
<PillTag color="info" :label="event.duration" />
<h1 class="text-xl font-bold">
{{ event.name }}
</h1>
<div class="text-md font-light flex gap-2">
<h1>
Started
<span class="font-normal">
{{
formatSortableDate(
getEventSetting(event.id)?.start_time,
)
}}
</span>
</h1>
<span>|</span>
<h1>
Expires
<span class="font-normal">
{{
formatSortableDate(
getEventSetting(event.id)?.end_time,
)
}}
</span>
</h1>
</div>
<template
v-if="
Array.isArray(getEventSetting(event.id)?.data?.music) &&
musicData
"
>
<div class="flex gap-6 text-lg mt-3">
<SongCardSmall
v-for="musicId of getEventSetting(event.id).data
.music"
:game-id="gameID"
:music-data="getMusic(musicId)"
/>
</div>
</template>
<template
v-else-if="getEventSetting(event.id)?.data?.music"
>
<div class="flex mt-3">
<SongCardSmall
:game-id="gameID"
:music-data="
getMusic(getEventSetting(event.id)?.data?.music)
"
/>
</div>
</template>
<template v-if="getEventSetting(event.id)?.data?.today">
<SongCardSmall
:game-id="gameID"
:music-data="
getMusic(getEventSetting(event.id)?.data?.today)
"
/>
</template>
<template v-if="getEventSetting(event.id)?.data?.whim">
<SongCardSmall
:game-id="gameID"
:music-data="
getMusic(getEventSetting(event.id)?.data?.whim)
"
/>
</template>
</div>
</template>
</template>
</div>
</CardBox>
</div>
</div>
<SectionTitleLine
:icon="PhUsersThree"
color="text-blue-400"
title="All Players"
main
/>
<CardBox has-table>
<div
class="bg-white dark:bg-slate-900/95 rounded-2xl lg:flex lg:justify-between"

View File

@@ -15,7 +15,7 @@ import FormField from "@/components/FormField.vue";
import {
APIGetProfile,
APIGetAllProfiles,
APIGetGame,
APIGetLinks,
APIUpdateProfile,
APIPutLink,
@@ -97,8 +97,8 @@ async function loadLinks() {
async function loadAllProfiles() {
try {
const data = await APIGetAllProfiles(gameID, versionForm.currentVersion);
allProfiles.value = data;
const data = await APIGetGame(gameID, versionForm.currentVersion);
allProfiles.value = data.profiles;
} catch (error) {
console.error("Failed to fetch profile data:", error);
}