Add game option saving, clean up api calls
Some checks are pending
Build / build (push) Waiting to run

This commit is contained in:
Trenton Zimmer
2024-10-14 18:26:28 -04:00
parent 64157dd2f2
commit 248b6f9d89
12 changed files with 10152 additions and 10131 deletions

View File

@@ -17,11 +17,11 @@ const props = defineProps({
default: null,
},
maxlength: {
type: String,
type: Number,
default: null,
},
minlength: {
type: String,
type: Number,
default: null,
},
max: {

View File

@@ -27,7 +27,7 @@ const username = computed(() => props.username);
:alt="username"
width="200"
height="200"
class="w-full h-auto bg-gray-100 dark:bg-slate-800 rounded-full overflow-hidden"
class="w-full h-auto bg-gray-100 dark:bg-slate-800 rounded-full overflow-hidden drop-shadow-lg"
/>
<!-- <img
src="/assets/border/pride.webp"

View File

@@ -1649,6 +1649,8 @@ export const gameData = [
shortName: "pop'n",
icon: "/assets/icon/popn.webp",
cardBG: "/assets/card/popn.webp",
useUnicode: true,
maxLength: 6,
scoreHeaders: [
{ text: "Combos", value: "combo" },
{ text: "Halo", value: "halo" },
@@ -1779,6 +1781,7 @@ export const gameData = [
name: "ReflecBeat",
icon: null,
cardBG: null,
useUnicode: true,
scoreHeaders: [
{ text: "Combos", value: "combo" },
{ text: "Misses", value: "misses" },
@@ -1813,9 +1816,9 @@ export const gameData = [
},
{
id: GameConstants.ROAD_FIGHTERS,
name: "Road Fighters",
icon: null,
cardBG: null,
name: "Road Fighters 3D",
icon: "/assets/icon/rf.webp",
cardBG: "/assets/card/rf.webp",
skip: true,
noRivals: true,
noScores: true,
@@ -1832,8 +1835,9 @@ export const gameData = [
{ text: "Halo", value: "halo" },
],
playerHeaders: [
{ text: "Skill Level", value: "skillLevel", sortable: true, width: 100 },
{ text: "VOLFORCE", value: "vf", sortable: true, width: 100 },
{ text: "Skill Level", value: "skill_level", sortable: true, width: 100 },
{ text: "BLOCK", value: "block", sortable: false, width: 100 },
{ text: "PACKET", value: "packet", sortable: false, width: 100 },
],
versions: [
{
@@ -1871,16 +1875,7 @@ export const gameData = [
noRivals: true,
noScores: false,
noRecords: true,
},
{
id: GameConstants.ROAD_FIGHTERS,
name: "Road Fighters 3D",
icon: "/assets/icon/rf.webp",
cardBG: "/assets/card/rf.webp",
skip: true,
noRivals: true,
noScores: true,
noRecords: true,
useUnicode: true,
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,8 @@ export function getGameOptions(game, version) {
name: "Username",
help: "Set your username for this profile",
type: "String",
maxLength: 8,
maxLength: game.maxLength ?? 8,
useUnicode: game.useUnicode ?? false,
},
];

50
src/stores/api/profile.js Normal file
View File

@@ -0,0 +1,50 @@
import { useMainStore } from "@/stores/main";
const mainStore = useMainStore();
export async function APIGetAllProfiles(game) {
try {
const data = await mainStore.callApi(`/game/${game}/profiles`);
return data.data;
} catch (error) {
console.log("Error fetching profiles:", error);
throw error;
}
}
export async function APIGetProfile(game, version, userId = null) {
if (!userId) {
while (!mainStore.userId) {
await new Promise((resolve) => setTimeout(resolve, 200));
}
userId = mainStore.userId;
}
try {
const data = await mainStore.callApi(
`/profile/${game}?version=${version}&userId=${userId}`
);
return data.data;
} catch (error) {
console.log("Error fetching profile:", error);
throw error;
}
}
export async function APIUpdateProfile(game, version, newProfile) {
try {
while (!mainStore.userId) {
await new Promise((resolve) => setTimeout(resolve, 200));
}
const userId = mainStore.userId;
const data = await mainStore.callApi(
`/profile/${game}?version=${version}&userId=${userId}`,
"POST",
newProfile
);
return data;
} catch (error) {
console.log("Error updating profile:", error);
throw error;
}
}

View File

@@ -283,35 +283,6 @@ export const useMainStore = defineStore("main", {
}
},
async getGameProfiles(game) {
try {
const data = await this.callApi(`/game/${game}/profiles`);
return data.data;
} catch (error) {
console.log("Error fetching profiles:", error);
throw error;
}
},
async getUserProfile(game, version, userId = null) {
if (!userId) {
while (!this.userId) {
await new Promise((resolve) => setTimeout(resolve, 200));
}
userId = this.userId;
}
try {
const data = await this.callApi(
`/profile/${game}?version=${version}&userId=${userId}`
);
return data.data;
} catch (error) {
console.log("Error fetching profile:", error);
throw error;
}
},
async getMusicData(game, version, songIds = null, oneChart = false) {
try {
const data = await this.callApi(

View File

@@ -1,7 +1,6 @@
<script setup>
import { reactive, onMounted, watch, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useMainStore } from "@/stores/main";
import { mdiAccountTieHat, mdiBackburger } from "@mdi/js";
import SectionMain from "@/components/SectionMain.vue";
import CardBox from "@/components/CardBox.vue";
@@ -15,13 +14,14 @@ import FormControl from "@/components/FormControl.vue";
import EmblemCardBox from "@/components/Cards/EmblemCardBox.vue";
import QproCardBox from "@/components/Cards/QproCardBox.vue";
import PillTag from "@/components/PillTag.vue";
import { APIGetProfile, APIUpdateProfile } from "@/stores/api/profile";
import { getGameInfo } from "@/constants";
import { getVideoSource, getCardStyle } from "@/constants/sources";
import { getGameOptions } from "@/constants/options";
const $route = useRoute();
const $router = useRouter();
const mainStore = useMainStore();
var gameID = null;
var thisGame = null;
@@ -53,6 +53,7 @@ if (!thisGame.versions) {
}
const optionForm = ref(null);
const bareForm = ref(null);
const myProfile = ref(null);
onMounted(() => {
@@ -72,13 +73,20 @@ function filterVersions(haveVersions) {
async function loadProfile() {
try {
myProfile.value = null;
optionForm.value = null;
const data = await mainStore.getUserProfile(
gameID,
versionForm.currentVersion
);
optionForm.value = {};
bareForm.value = {};
const data = await APIGetProfile(gameID, versionForm.currentVersion);
myProfile.value = data;
optionForm.value = data;
// Deep clone nested values from myProfile to optionForm using paths
for (const setting of getGameOptions(
thisGame,
versionForm.currentVersion
)) {
const value = getNestedValue(myProfile.value, setting.id);
setNestedValue(optionForm.value, setting.id, value);
setNestedValue(bareForm.value, setting.id, value);
}
if (data && !versionForm.currentVersion) {
versionForm.currentVersion = data.versions[data.versions.length - 1];
@@ -91,6 +99,75 @@ async function loadProfile() {
function getNestedValue(obj, path) {
return path.split(".").reduce((acc, part) => acc && acc[part], obj);
}
function setNestedValue(obj, path, value) {
const keys = path.split(".");
const lastKey = keys.pop();
const nestedObj = keys.reduce((acc, key) => (acc[key] = acc[key] || {}), obj);
nestedObj[lastKey] = value;
}
function transformNonUnicode(value, maxLength) {
const allowedCharsRegex = /^[0-9A-Z!?#$&*-. ]*$/;
const transformedValue = value.toUpperCase().slice(0, maxLength);
return (
allowedCharsRegex.test(transformedValue) ? transformedValue : ""
).toUpperCase();
}
function transformUnicode(value, maxLength) {
let transformedValue = "";
for (
let i = 0;
i < value.length && transformedValue.length < maxLength;
i++
) {
let c = value.charCodeAt(i);
if (c >= 0x30 && c <= 0x39) {
// '0' to '9'
c = 0xff10 + (c - 0x30);
} else if (c >= 0x41 && c <= 0x5a) {
// 'A' to 'Z'
c = 0xff21 + (c - 0x41);
} else if (c >= 0x61 && c <= 0x7a) {
// 'a' to 'z'
c = 0xff41 + (c - 0x61);
} else if (c === 0x40) {
// '@'
c = 0xff20;
} else if (c === 0x2c) {
// ','
c = 0xff0c;
} else if (c === 0x2e) {
// '.'
c = 0xff0e;
} else if (c === 0x5f) {
// '_'
c = 0xff3f;
}
transformedValue += String.fromCharCode(c);
}
const allowedCharsRegex =
/^[\uFF20-\uFF3A\uFF41-\uFF5A\uFF10-\uFF19\uFF0C\uFF0E\uFF3F\u3041-\u308D\u308F\u3092\u3093\u30A1-\u30ED\u30EF\u30F2\u30F3\u30FC]*$/;
return (
allowedCharsRegex.test(transformedValue) ? transformedValue : ""
).toUpperCase();
}
async function updateProfile() {
const response = await APIUpdateProfile(
thisGame.id,
versionForm.currentVersion,
optionForm.value
);
if (response.status != "error") {
await loadProfile();
}
}
</script>
<template>
@@ -151,58 +228,87 @@ function getNestedValue(obj, path) {
</div>
</div>
<div v-if="versionForm.currentVersion && myProfile && optionForm">
<CardBox>
<form>
<div>
<PillTag color="info" label="General" class="mb-2" />
<FormField
v-for="setting of getGameOptions(
thisGame,
versionForm.currentVersion
)"
:key="setting.id"
:label="setting.name"
:help="setting.help"
>
<FormControl
v-if="setting.type == 'String'"
:name="setting.id"
:model-value="getNestedValue(optionForm, setting.id)"
:maxlength="setting.maxLength ?? 15"
/>
<div v-if="versionForm.currentVersion && myProfile">
<CardBox is-form>
<form @submit.prevent="updateProfile()">
<PillTag color="info" label="General" class="mb-2" />
<FormField
v-for="setting of getGameOptions(
thisGame,
versionForm.currentVersion
)"
:key="setting.id"
:label="setting.name"
:help="setting.help"
>
<FormControl
v-if="setting.type == 'String'"
:model-value="getNestedValue(optionForm, setting.id) ?? ``"
:name="setting.id"
:maxlength="setting.maxLength ?? 15"
@update:model-value="
(value) =>
setNestedValue(
optionForm,
setting.id,
setting.useUnicode
? transformUnicode(value, setting.maxLength)
: transformNonUnicode(value, setting.maxLength)
)
"
/>
<FormControl
v-if="setting.type == 'Number'"
:name="setting.id"
:model-value="getNestedValue(optionForm, setting.id)"
type="number"
onkeypress="return event.charCode >= 48 && event.charCode <= 57"
min="0"
max="999"
/>
<FormControl
v-if="setting.type == 'Number'"
:model-value="getNestedValue(optionForm, setting.id) ?? 0"
:name="setting.id"
type="number"
onkeypress="return event.charCode >= 48 && event.charCode <= 57"
min="0"
max="999"
@update:model-value="
(value) =>
setNestedValue(optionForm, setting.id, Number(value))
"
/>
<FormControl
v-if="setting.type == 'Array'"
:options="setting.options"
:name="setting.id"
:model-value="getNestedValue(optionForm, setting.id)"
:selected="getNestedValue(optionForm, setting.id)"
/>
<FormControl
v-if="setting.type == 'Array'"
:model-value="getNestedValue(optionForm, setting.id) ?? 0"
:options="setting.options"
:name="setting.id"
:selected="getNestedValue(myProfile, setting.id) ?? 0"
@update:model-value="
(value) =>
setNestedValue(optionForm, setting.id, Number(value))
"
/>
<FormCheckRadio
v-if="setting.type == 'Boolean'"
:name="setting.id"
:input-value="Boolean(getNestedValue(optionForm, setting.id))"
:model-value="Boolean(getNestedValue(optionForm, setting.id))"
type="switch"
/>
</FormField>
</div>
<FormCheckRadio
v-if="setting.type == 'Boolean'"
:name="setting.id"
:model-value="
Boolean(getNestedValue(optionForm, setting.id) ?? 0)
"
:input-value="true"
type="switch"
@update:model-value="
(value) =>
setNestedValue(optionForm, setting.id, Number(value) ?? 0)
"
/>
</FormField>
<div class="space-x-2 mt-6">
<BaseButton type="submit" color="success" label="Save" />
<BaseButton type="submit" color="danger" label="Revert" />
<div
v-if="JSON.stringify(optionForm) !== JSON.stringify(bareForm)"
class="space-x-2 mt-6"
>
<BaseButton color="success" label="Save" type="submit" />
<BaseButton
color="danger"
label="Revert"
@click="loadProfile()"
/>
</div>
</form>
</CardBox>

View File

@@ -6,7 +6,6 @@ import {
mdiPlaylistMusicOutline,
mdiFormatListText,
} from "@mdi/js";
import { useMainStore } from "@/stores/main";
import SectionMain from "@/components/SectionMain.vue";
import BaseButton from "@/components/BaseButton.vue";
import SectionTitleLine from "@/components/SectionTitleLine.vue";
@@ -16,6 +15,8 @@ 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 { APIGetProfile, APIGetAllProfiles } from "@/stores/api/profile";
import { getGameInfo } from "@/constants";
import { getVideoSource, getCardStyle } from "@/constants/sources";
import { dashCode } from "@/constants/userData";
@@ -23,7 +24,6 @@ import { getIIDXDan } from "@/constants/danClass";
const $route = useRoute();
const $router = useRouter();
const mainStore = useMainStore();
var gameID = null;
var thisGame = null;
@@ -55,7 +55,7 @@ const profiles = ref([]);
onMounted(async () => {
try {
const data = await mainStore.getGameProfiles(gameID);
const data = await APIGetAllProfiles(gameID);
profiles.value = formatProfiles(data);
} catch (error) {
console.error("Failed to fetch profile data:", error);
@@ -71,10 +71,7 @@ if (!thisGame.versions) {
async function loadProfile() {
try {
myProfile.value = null;
const data = await mainStore.getUserProfile(
gameID,
versionForm.currentVersion
);
const data = await APIGetProfile(gameID, versionForm.currentVersion);
myProfile.value = data;
if (data && !versionForm.currentVersion) {

View File

@@ -9,6 +9,8 @@ import SectionTitleLine from "@/components/SectionTitleLine.vue";
import GeneralTable from "@/components/GeneralTable.vue";
import CardBox from "@/components/CardBox.vue";
import BaseButton from "@/components/BaseButton.vue";
import { APIGetProfile } from "@/stores/api/profile";
import { getGameInfo } from "@/constants";
const $route = useRoute();
@@ -67,7 +69,7 @@ onMounted(async () => {
async function loadProfile() {
try {
myProfile.value = null;
const data = await mainStore.getUserProfile(gameID, null, profileUserId);
const data = await APIGetProfile(gameID, null, profileUserId);
myProfile.value = data;
} catch (error) {
console.error("Failed to fetch user profile data:", error);

View File

@@ -1,7 +1,6 @@
<script setup>
import { reactive, ref, onMounted, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useMainStore } from "@/stores/main";
import {
mdiAccountOutline,
mdiBackburger,
@@ -20,13 +19,14 @@ import SectionTitleLine from "@/components/SectionTitleLine.vue";
import FormControl from "@/components/FormControl.vue";
import PillTag from "@/components/PillTag.vue";
import JubilityTable from "@/components/Tables/JubilityTable.vue";
import { APIGetProfile } from "@/stores/api/profile";
import { getGameInfo } from "@/constants";
import { getIIDXDan } from "@/constants/danClass.js";
import { getGitadoraColor, getJubilityColor } from "@/constants/skillColor";
const $route = useRoute();
const $router = useRouter();
const mainStore = useMainStore();
var gameID = null;
var thisGame = null;
var profileUserId = null;
@@ -68,7 +68,7 @@ if (!thisGame.versions) {
async function loadProfile() {
try {
myProfile.value = null;
const data = await mainStore.getUserProfile(
const data = await APIGetProfile(
gameID,
versionForm.currentVersion,
profileUserId

View File

@@ -1,41 +1,26 @@
<script setup>
import { computed, ref, onMounted, watch } from "vue";
import {
// mdiReload,
// mdiChartBellCurveCumulative,
mdiGamepad,
// mdiTestTube,
mdiNewspaperVariant,
mdiChartTimelineVariant,
} from "@mdi/js";
import UserCard from "@/components/UserCard.vue";
import * as chartConfig from "@/components/Charts/chart.config.js";
// import LineChart from "@/components/Charts/LineChart.vue";
import SectionMain from "@/components/SectionMain.vue";
//import CardBox from "@/components/CardBox.vue";
import CardBoxWidget from "@/components/CardBoxWidget.vue";
// import BaseButton from "@/components/BaseButton.vue";
import CardBoxGameStat from "@/components/CardBoxGameStat.vue";
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
import SectionTitleLine from "@/components/SectionTitleLine.vue";
// import PillTag from "@/components/PillTag.vue";
// Public beta news data
import { useMainStore } from "@/stores/main";
const mainStore = useMainStore();
import CardBoxNews from "@/components/Cards/CardBoxNews.vue";
import CardBoxComponentEmpty from "@/components/CardBoxComponentEmpty.vue";
import { getGameInfo } from "@/constants";
import { useMainStore } from "@/stores/main";
const mainStore = useMainStore();
var newsData = ref([]);
const chartData = ref(null);
const fillChartData = () => {
chartData.value = chartConfig.sampleChartData();
};
onMounted(async () => {
fillChartData();
try {
const data = await mainStore.fetchAllNews();
newsData.value = data;
@@ -49,23 +34,6 @@ function humanReadableTime(timestamp) {
return date.toLocaleString();
}
// const setGoals = [
// {
// game: "DanceDance Revolution",
// type: "Rank",
// goal: "Top 10 Ranking",
// status: "#10 of 132",
// deadline: "3 Weeks",
// },
// {
// game: "pop'n music",
// type: "Plays",
// goal: "100 Plays",
// status: "2 Plays Since Creation",
// deadline: "1 Week",
// },
// ];
const userProfiles = ref(mainStore.userProfiles);
watch(
() => mainStore.userProfiles,
@@ -81,12 +49,6 @@ const cumulativePlays = computed(() => {
);
});
// const sortedUserProfiles = computed(() => {
// return [...userProfiles.value].sort(
// (a, b) => b.data.last_play_timestamp - a.data.last_play_timestamp
// );
// });
function filterUserProfiles(userProfiles) {
var filteredProfiles = [];
for (const profile of userProfiles) {
@@ -109,7 +71,6 @@ function filterUserProfiles(userProfiles) {
<SectionMain>
<UserCard class="mb-6 mt-2 shadow-xl" />
<!-- For public beta, we'll load the news here. -->
<SectionTitleLine :icon="mdiNewspaperVariant" title="Network News" main />
<div
@@ -129,15 +90,6 @@ function filterUserProfiles(userProfiles) {
</div>
<CardBoxComponentEmpty v-if="!newsData || !newsData.length" />
<!-- <div class="my-6">
<NotificationBar color="info">
You have unread news!
<template #right>
<a href="#/news" class="text-blue-300 hover:underline">View now</a>
</template>
</NotificationBar>
</div> -->
<SectionTitleLine
:icon="mdiChartTimelineVariant"
title="Quick Stats"
@@ -159,45 +111,10 @@ function filterUserProfiles(userProfiles) {
:key="profile.game"
:game="profile.game"
:value="profile.data.total_plays"
profile-name=" "
profile-name=""
type="plays"
/>
</div>
<!-- <SectionTitleLine :icon="mdiFlagCheckered" title="Active Goals" main />
<div class="mb-6">
<CardBox has-table>
<TableGoals :goals="setGoals" />
</CardBox>
</div> -->
<!-- <SectionTitleLine
:icon="mdiChartBellCurveCumulative"
title="Play Trends"
main
>
<BaseButton
:icon="mdiReload"
color="whiteDark"
@click="fillChartData"
/>
</SectionTitleLine>
<CardBox class="mb-6">
<PillTag
label="Scores (7-Day Period)"
color="info"
:icon="mdiTestTube"
/>
<div v-if="chartData">
<line-chart :data="chartData" class="h-96" />
</div>
</CardBox> -->
<!-- <SectionTitleLine :icon="mdiAccountMultipleOutline" title="Rivals" main />
<CardBox has-table>
<TableRivalsFull />
</CardBox> -->
</SectionMain>
</LayoutAuthenticated>
</template>