Add profile customization support.
Some checks failed
Build / build (push) Has been cancelled

This commit is contained in:
Trenton Zimmer 2025-03-17 17:21:40 -04:00
parent 6552ed5208
commit a200f27a17
11 changed files with 193 additions and 62 deletions

View File

@ -1,6 +1,8 @@
<script setup>
import { computed } from "vue";
const ASSET_PATH = import.meta.env.VITE_ASSET_PATH;
const props = defineProps({
username: {
type: String,
@ -10,6 +12,10 @@ const props = defineProps({
type: String,
default: null,
},
border: {
type: String,
default: null,
},
});
const avatar = computed(
@ -29,12 +35,13 @@ const username = computed(() => props.username);
height="200"
class="w-full h-auto bg-gray-100 dark:bg-slate-800 rounded-full overflow-hidden drop-shadow-lg"
/>
<!-- <img
:src="`${ASSET_PATH}/border/pride.webp`"
<img
v-if="border"
:src="`${ASSET_PATH}/border/${border}.webp`"
alt="border"
class="absolute inset-0 w-full h-full overflow-hidden scale-[1.02]"
style="object-fit: fill"
/> -->
/>
</div>
<slot />
</div>

View File

@ -6,7 +6,11 @@ const mainStore = useMainStore();
</script>
<template>
<UserAvatar :username="mainStore.userName" :avatar="mainStore.userAvatar">
<UserAvatar
:username="mainStore.userName"
:avatar="mainStore.userAvatar"
:border="mainStore.userCustomize?.border ?? null"
>
<slot />
</UserAvatar>
</template>

View File

@ -33,25 +33,19 @@ const mainStore = useMainStore();
const greeting = GetRandomMessage();
// var last = {
// game: "beatmaniaIIDX",
// version: "22 PENDUAL",
// arcade: "Ho-House",
// };
function getCardStyle(game) {
if (game !== null) {
return `
background-image: url('${ASSET_PATH}/card/${game}.webp');
background-size: cover;
background-repeat: no-repeat;
`;
}
function getCardStyle() {
return `
background-image: url('${ASSET_PATH}/card/${
mainStore.userCustomize?.card ?? "time"
}.webp');
background-size: cover;
background-repeat: no-repeat;
`;
}
</script>
<template>
<CardBox :style="getCardStyle(mainStore.userCardStyle)">
<CardBox :style="getCardStyle()">
<BaseLevel
type="justify-around lg:justify-center md:space-x-4 lg:space-x-0"
class="bg-white dark:bg-slate-900/90 rounded-2xl p-3"
@ -59,17 +53,19 @@ function getCardStyle(game) {
<UserAvatarCurrentUser class="w-28 md:w-32 lg:w-44 lg:mx-12" />
<div class="space-y-3 text-center md:text-left lg:mx-12">
<div class="space-y-2 md:space-y-0">
<h1 v-if="!useSmall" class="text-2xl md:text-xl lg:text-2xl">
<h1
v-if="!useSmall && !mainStore.userCustomize.disableGreeting"
class="text-2xl md:text-xl lg:text-2xl"
>
{{ greeting.header[0] }}<b>{{ mainStore.userName }} </b
>{{ greeting.header[1] }}
</h1>
<h1 v-if="useSmall" class="text-3xl md:text-4xl">
<h1
v-if="useSmall || mainStore.userCustomize.disableGreeting"
class="text-3xl md:text-4xl"
>
<b>{{ mainStore.userName }}</b>
</h1>
<p v-if="useSmall && !evenSmaller" class="text-md md:text-lg">
<!-- Last seen playing <b>{{ last.game }} {{ last.version }}</b> at
<b>{{ last.arcade }}</b> -->
</p>
</div>
<div
v-if="!evenSmaller"
@ -92,7 +88,10 @@ function getCardStyle(game) {
/> -->
</div>
</div>
<div v-if="!useSmall" class="text-center md:text-right">
<div
v-if="!useSmall && !mainStore.userCustomize.disableGreeting"
class="text-center md:text-right"
>
<p class="text-xl md:text-lg lg:text-2lg">{{ greeting.comment }}</p>
<p class="text-xs italic">Greeting by: {{ greeting.author }}</p>
</div>

View File

@ -0,0 +1,33 @@
import { gameData } from "@/constants";
export class ProfileCustomizations {
static baseCardList = [
{ id: "time", label: "Default" },
{ id: "carpet", label: "Arcade Carpet" },
{ id: "gradient", label: "Pink/Purple Gradient" },
{ id: "pride", label: "Pride Flag" },
{ id: "trans", label: "Trans Flag" },
{ id: "loveplus", label: "LovePlus" },
];
static get cardList() {
const gameCards = gameData
.filter((game) => !game.skip)
.map((game) => ({
id: game.assetId ? game.assetId : game.id,
label: game.name,
}));
return [...this.baseCardList, ...gameCards];
}
static borderList = [
{ id: "", label: "None" },
{ id: "test", label: "Basic" },
{ id: "swirl", label: "Swirl" },
{ id: "pride", label: "Pride Flag" },
{ id: "trans", label: "Trans Flag" },
{ id: "nonbinary", label: "Non-Binary Flag" },
{ id: "leaves", label: "Leaves" },
];
}

View File

@ -602,6 +602,7 @@ export const gameData = [
name: "Classic DDR",
icon: `${ASSET_PATH}/icon/ddr.webp`,
cardBG: `${ASSET_PATH}/card/ddr.webp`,
assetId: "ddr",
noRivals: true,
noScores: true,
noRecords: true,
@ -654,6 +655,7 @@ export const gameData = [
name: "DDR OmniMIX",
icon: `${ASSET_PATH}/icon/ddr.webp`,
cardBG: `${ASSET_PATH}/card/ddr.webp`,
assetId: "ddr",
videoTable: [VersionConstants.DDR_A20_PLUS],
scoreHeaders: [
{ text: "Combos", value: "data.combo" },
@ -1165,6 +1167,7 @@ export const gameData = [
shortName: "IIDX",
icon: `${ASSET_PATH}/icon/iidx.webp`,
cardBG: `${ASSET_PATH}/card/iidx.webp`,
assetId: "iidx",
gameOptions: IIDXOptions,
videoTable: [
VersionConstants.IIDX_TRICORO,
@ -1364,6 +1367,7 @@ export const gameData = [
name: "Classic IIDX",
icon: `${ASSET_PATH}/icon/iidx.webp`,
cardBG: `${ASSET_PATH}/card/iidx.webp`,
assetId: "iidx",
noRivals: true,
noScores: true,
noRecords: true,
@ -1678,6 +1682,7 @@ export const gameData = [
name: "Hello! Pop'n Music",
icon: `${ASSET_PATH}/icon/popn.webp`,
cardBG: `${ASSET_PATH}/card/popn.webp`,
assetId: "popn",
skip: true,
noRivals: true,
noScores: true,
@ -1689,6 +1694,7 @@ export const gameData = [
shortName: "pop'n",
icon: `${ASSET_PATH}/icon/popn.webp`,
cardBG: `${ASSET_PATH}/card/popn.webp`,
assetId: "popn",
useUnicode: true,
maxLength: 6,
gameOptions: PopnMusicOptions,
@ -1861,6 +1867,7 @@ export const gameData = [
name: "Road Fighters 3D",
icon: `${ASSET_PATH}/icon/rf.webp`,
cardBG: `${ASSET_PATH}/card/rf.webp`,
assetId: "rf",
skip: true,
noRivals: true,
noScores: true,
@ -1923,6 +1930,7 @@ export const gameData = [
name: "World Soccer",
icon: `${ASSET_PATH}/icon/we.webp`,
cardBG: `${ASSET_PATH}/card/we.webp`,
assetId: "we",
skip: false,
noRivals: true,
noScores: true,

View File

@ -1,6 +1,7 @@
import {
mdiAccount,
mdiLogout,
mdiBrushVariant,
mdiServerNetwork,
mdiCardAccountDetailsOutline,
mdiAccountArrowLeftOutline,
@ -15,6 +16,11 @@ export default [
label: "Settings",
to: "/profile",
},
{
icon: mdiBrushVariant,
label: "Customize",
to: "/profile/customize",
},
{
icon: mdiServerNetwork,
label: "Integrations",

View File

@ -50,14 +50,14 @@ const routes = [
name: "profile",
component: () => import("@/views/Profile/ProfileView.vue"),
},
// {
// meta: {
// title: "Customizations",
// },
// path: "/profile/customize",
// name: "profile_customizations",
// component: () => import("@/views/Profile/CustomizeView.vue"),
// },
{
meta: {
title: "Customizations",
},
path: "/profile/customize",
name: "profile_customizations",
component: () => import("@/views/Profile/CustomizeView.vue"),
},
{
meta: {
title: "Integrations",

View File

@ -180,3 +180,15 @@ export async function APIIntegrateWith(service, code) {
throw error;
}
}
export async function APIUserCustomize(customize) {
try {
const data = await mainStore.callApi(`/user/customize`, "POST", {
customize: customize,
});
return data;
} catch (error) {
console.log(`Error saving user customize!`, error);
throw error;
}
}

View File

@ -15,6 +15,7 @@ export const useMainStore = defineStore("main", {
userProfiles: [],
userArcades: [],
profiles: {},
userCustomize: {},
/* Field focus with ctrl+k (to register only once) */
isFieldFocusRegistered: false,
@ -65,6 +66,9 @@ export const useMainStore = defineStore("main", {
if (payload.arcades) {
this.userArcades = payload.arcades;
}
if (payload.customize) {
this.userCustomize = payload.customize;
}
},
fetch(sampleDataKey) {
@ -250,6 +254,7 @@ export const useMainStore = defineStore("main", {
cardStyle: "time",
profiles: user.profiles,
arcades: user.arcades,
customize: user.data?.customize,
});
this.userLoaded = true;
return true;

View File

@ -1,18 +1,69 @@
<script setup>
import { useMainStore } from "@/stores/main";
import { useRouter } from "vue-router";
import { ref, watch } from "vue";
import { mdiAccountTieHat } from "@mdi/js";
import SectionMain from "@/components/SectionMain.vue";
import CardBox from "@/components/CardBox.vue";
import BaseButton from "@/components/BaseButton.vue";
import UserCard from "@/components/UserCard.vue";
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
import SectionTitleLine from "@/components/SectionTitleLine.vue";
import FormField from "@/components/FormField.vue";
import FormControl from "@/components/FormControl.vue";
import FormCheckRadio from "@/components/FormCheckRadio.vue";
import { ProfileCustomizations } from "@/constants/customizations";
import { APIUserCustomize } from "@/stores/api/account";
const $router = useRouter();
const mainStore = useMainStore();
const userCustomize = ref(mainStore.userCustomize);
var originalData = JSON.parse(JSON.stringify(mainStore.userCustomize));
watch(
() => mainStore.userCustomize,
(newValue) => {
userCustomize.value = newValue;
originalData = JSON.parse(JSON.stringify(newValue));
}
);
async function updateCustomize() {
var data = null;
try {
data = await APIUserCustomize(userCustomize.value);
} catch (error) {
console.error("Failed to update customize:", error);
}
if (data?.status === "success") {
mainStore.userLoaded = false;
$router.go();
}
}
function userChanged() {
console.log(originalData);
console.log(userCustomize.value);
if (JSON.stringify(originalData) !== JSON.stringify(userCustomize.value)) {
return true;
} else {
return false;
}
}
async function revert() {
mainStore.userLoaded = false;
$router.go();
}
</script>
<template>
<LayoutAuthenticated>
<SectionMain>
<UserCard class="mb-6" use-small even-smaller />
<UserCard class="mb-6" />
<SectionTitleLine
:icon="mdiAccountTieHat"
title="Profile Customizations"
@ -21,39 +72,44 @@ import FormControl from "@/components/FormControl.vue";
<div>
<CardBox>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<h2 class="text-lg mb-2">Avatar Source</h2>
<form @submit.prevent="updateCustomize">
<FormField label="Profile Background">
<FormControl
:options="['Discord', 'Gravatar', 'Kamaitachi']"
model-value="Gravatar"
name="border"
v-model="userCustomize.card"
:options="ProfileCustomizations.cardList"
:model-value="userCustomize.card ?? 'time'"
name="card"
/>
</div>
<div>
<h2 class="text-lg mb-2">Avatar Border</h2>
<FormControl
:options="['Trans Flag', 'Pride Flag']"
model-value="Trans Flag"
name="border"
/>
</div>
<div>
<h2 class="text-lg mb-2">Profile Background</h2>
<FormControl
:options="['Trans Flag', 'Pride Flag']"
model-value="Trans Flag"
name="border"
/>
</div>
</div>
</FormField>
<FormField label="Profile Border">
<FormControl
v-model="userCustomize.border"
:options="ProfileCustomizations.borderList"
:model-value="userCustomize.border ?? ''"
name="border"
/>
</FormField>
<FormField label="Disable Greeting">
<FormCheckRadio
v-model="userCustomize.disableGreeting"
type="switch"
:input-value="userCustomize.disableGreeting ?? false"
name="disableGreeting"
/>
</FormField>
<template #footer>
<div class="space-x-2">
<BaseButton type="submit" color="success" label="Save" />
<BaseButton type="submit" color="danger" label="Revert" />
<BaseButton
v-if="userChanged()"
color="danger"
label="Revert"
@click="revert"
/>
</div>
</template>
</form>
</CardBox>
</div>
</SectionMain>

View File

@ -61,6 +61,7 @@ async function submitProfile() {
const response = await mainStore.putUser(profileForm);
if (response.status == "success") {
profileLoading.value = false;
mainStore.userLoaded = false;
await mainStore.loadUser();
$router.go();
}