Finish public profile page, clean up dashboard cards
Some checks failed
Build / build (push) Has been cancelled

This commit is contained in:
Trenton Zimmer 2025-07-14 14:16:10 -04:00
parent 091dfe548e
commit 6e609f72bb
10 changed files with 289 additions and 24 deletions

View File

@ -1,4 +1,4 @@
VITE_APP_VERSION="3.0.21"
VITE_APP_VERSION="3.0.22"
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.21"
VITE_APP_VERSION="3.0.22"
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

@ -20,5 +20,6 @@
"3.0.18": ["- (Admin) Adds the ability to add and remove managers from an arcade.", "- (Minor) Lay groundwork for avatars in tables.", "- (Minor) Add new profile customizations."],
"3.0.19": ["- (Admin) Finish admin arcade and machine pages.", "- (Minor) Add new greetings."],
"3.0.20": ["- (Major) Add read state to network news.", "- (Minor) Add new greetings.", "- (Minor) Add information to linked services", "- (Minor) Add session info and button to remove all sessions", "- (Minor) Add text gradient animation backend, needs ported to text once finished."],
"3.0.21": ["- (Admin) Finish admin machine/arcade pages", "- (Major) Finish initial public profile page", "- (Minor) Change table right-click behavior", "- (Admin) Finish admin News pages", "- (Admin) Add search for user via card ID"]
"3.0.21": ["- (Admin) Finish admin machine/arcade pages", "- (Major) Finish initial public profile page", "- (Minor) Change table right-click behavior", "- (Admin) Finish admin News pages", "- (Admin) Add search for user via card ID"],
"3.0.22": ["- (Major) Finish first implementation of the public profile page", "- (Minor) Clean up dashboard game stat box", "- (Minor) Load profile name in game stat box"]
}

View File

@ -3,7 +3,6 @@ import { computed } from "vue";
import { useRouter } from "vue-router";
import CardBoxComponentBody from "@/components/CardBoxComponentBody.vue";
import BaseLevel from "@/components/BaseLevel.vue";
import PillTag from "@/components/PillTag.vue";
import GameIcon from "@/components/GameIcon.vue";
import { getGameInfo } from "@/constants";
@ -17,6 +16,11 @@ const props = defineProps({
type: [String, Number],
required: true,
},
userId: {
type: String,
required: false,
default: null,
},
profileName: {
type: String,
required: true,
@ -34,15 +38,15 @@ const props = defineProps({
const tag = computed(() => {
if (props.type === "plays") {
return {
type: "success",
type: "text-emerald-600",
};
} else if (props.type === "scores") {
return {
type: "danger",
type: "text-red-500",
};
} else if (props.type === "ranking") {
return {
type: "warning",
type: "text-amber-400",
};
}
@ -53,7 +57,11 @@ const tag = computed(() => {
function loadGamePage() {
if (!props.disableLocalClick) {
router.push(`/games/${props.game}`);
if (!props.userId) {
router.push(`/games/${props.game}`);
} else {
router.push(`/games/${props.game}/profiles/${props.userId}`);
}
}
}
@ -82,30 +90,34 @@ const cardStyle = `
class="md:mr-6 scale-110 md:scale-100"
:path="selectedGame.icon"
/>
<div class="text-center space-y-1 md:text-left md:mr-6">
<h2 class="text-xl sr-only sm:not-sr-only">
{{ selectedGame.name }}
</h2>
<h2 class="text-xl font-semibold not-sr-only sm:sr-only -my-2">
<div class="text-center space-y-1 md:text-left md:mr-6 w-full">
<h2 class="text-xl font-semibold not-sr-only sm:sr-only">
{{
selectedGame.shortName
? selectedGame.shortName
: selectedGame.name
}}
</h2>
<div class="grid">
<h2 class="text-xl sr-only sm:not-sr-only">
{{ selectedGame.name }}
</h2>
<hr class="my-1 text-gray-500" />
<div>
<p class="text-sm md:text-md text-gray-300">
{{ profileName }}
</p>
</div>
</div>
<div
class="flex space-x-2 justify-center md:justify-start pt-2 md:pt-0"
>
<PillTag :color="tag.type" :label="type" small />
<h4 class="text-lg">{{ value }}</h4>
<h4 class="text-lg drop-shadow-xl">
{{ value }} <span :class="tag.type">{{ type }}</span>
</h4>
</div>
</div>
</BaseLevel>
<div class="text-center md:text-right">
<p class="text-sm text-gray-400">
{{ profileName }}
</p>
</div>
</BaseLevel>
</CardBoxComponentBody>
</div>

View File

@ -16,6 +16,7 @@ import {
mdiCheckDecagramOutline,
mdiLinkBoxVariantOutline,
mdiAccountCheck,
mdiGavel,
} from "@mdi/js";
import BaseLevel from "@/components/BaseLevel.vue";
import UserAvatar from "@/components/UserAvatar.vue";
@ -64,6 +65,7 @@ const cardData = ref({
userAvatar: "",
userAdmin: false,
userPublic: false,
userBanned: false,
discordRoles: null,
userCustomize: null,
});
@ -75,6 +77,7 @@ if (props.overrideProfile) {
userName: overrideProfile.name,
userAvatar: overrideProfile.avatar,
userAdmin: overrideProfile.admin,
userBanned: overrideProfile.banned,
userPublic: overrideProfile.public,
discordRoles: overrideProfile.discordRoles,
userCustomize: overrideProfile.customize,
@ -87,6 +90,7 @@ if (props.overrideProfile) {
userAvatar: mainStore.userAvatar,
userAdmin: mainStore.userAdmin,
userPublic: mainStore.userPublic,
userBanned: mainStore.userBanned,
discordRoles: mainStore.discordRoles,
userCustomize: mainStore.userCustomize,
};
@ -150,6 +154,13 @@ function getCardStyle() {
:icon="mdiSecurity"
small
/>
<PillTag
v-if="cardData.userBanned"
label="Banned"
color="danger"
:icon="mdiGavel"
small
/>
<PillTag
v-if="cardData.userId < 300"
label="Veteran"

View File

@ -20,6 +20,7 @@ export async function APIGetUser(userId) {
customize: userData.data?.customize,
userScoreStats: userData.scoreStats,
public: userData.public,
banned: userData.banned,
};
return data;
} catch (error) {

View File

@ -223,6 +223,43 @@ export async function APIAdminUsers(noData = false) {
}
}
export async function APIAdminPutUser(userId, newUser) {
try {
const data = await mainStore.callApi(
`/admin/user/${userId}`,
"POST",
newUser
);
return data;
} catch (error) {
console.log("Error updating user:", error);
throw error;
}
}
export async function APIAdminUpdatePassword(
userId,
newPassword,
confirmPassword
) {
const mainStore = useMainStore();
try {
const data = await mainStore.callApi(
`/admin/user/${userId}/updatePassword`,
"POST",
{
newPassword: newPassword,
confirmPassword: confirmPassword,
}
);
return data;
} catch (error) {
console.log("Error updating password:", error);
throw error;
}
}
export async function APIAdminUserFromCardId(cardId) {
try {
const data = await mainStore.callApi(`/admin/user/card/${cardId}`);

View File

@ -22,6 +22,7 @@ export const useMainStore = defineStore("main", {
userCustomize: {},
userScoreStats: {},
userPublic: false,
userBanned: false,
/* Field focus with ctrl+k (to register only once) */
isFieldFocusRegistered: false,
@ -63,6 +64,9 @@ export const useMainStore = defineStore("main", {
if (payload.public) {
this.userPublic = payload.public;
}
if (payload.banned) {
this.userBanned = payload.banned;
}
if (payload.data) {
this.userData = payload.data;
}

View File

@ -270,7 +270,7 @@ const cardBoxes = ref([
:key="profile.game"
:game="profile.game"
:value="profile.data.total_plays"
profile-name=""
:profile-name="profile?.username"
type="plays"
/>
</div>

View File

@ -1,6 +1,6 @@
<script setup>
import { computed, ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { computed, ref, reactive, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
mdiAccount,
mdiGamepad,
@ -9,6 +9,10 @@ import {
mdiGamepadOutline,
mdiFire,
mdiTrendingUp,
mdiShieldEditOutline,
mdiInformationOutline,
mdiMail,
mdiAsterisk,
} from "@mdi/js";
import SectionMain from "@/components/SectionMain.vue";
import CardBox from "@/components/CardBox.vue";
@ -18,23 +22,35 @@ import UserCard from "@/components/UserCard.vue";
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
import SectionTitleLine from "@/components/SectionTitleLine.vue";
import LineChart from "@/components/Charts/LineChart.vue";
import PillTag from "@/components/PillTag.vue";
import FormField from "@/components/FormField.vue";
import FormControl from "@/components/FormControl.vue";
import FormCheckRadio from "@/components/FormCheckRadio.vue";
import BaseButton from "@/components/BaseButton.vue";
import { getGameInfo } from "@/constants";
import { generateChartData } from "@/components/Charts/chart.config";
import { useMainStore } from "@/stores/main";
import { APIGetUser } from "@/stores/api/account";
import { APIAdminPutUser, APIAdminUpdatePassword } from "@/stores/api/admin";
const mainStore = useMainStore();
const $route = useRoute();
const $router = useRouter();
const reqUserId = $route.params.id;
const newUser = ref(null);
const userProfile = ref(null);
const userProfiles = ref(null);
const userScoreStats = ref(null);
async function loadUser() {
try {
userProfile.value = null;
var data = await APIGetUser(reqUserId);
if (!data.name) {
data.name = "Unclaimed Account";
}
userProfile.value = data;
newUser.value = JSON.parse(JSON.stringify(data));
} catch (error) {
console.error("Failed to fetch user profile data:", error);
}
@ -222,6 +238,45 @@ const cardBoxes = ref([
number: todayAttempts,
},
]);
function pinInput(event) {
event.target.value = event.target.value.replace(/\D/g, "");
}
async function adminUpdateUser() {
const response = await APIAdminPutUser(reqUserId, newUser.value);
if (response.status !== "success") {
window.alert("Failed to update user!");
} else {
loadUser();
}
}
const passwordForm = reactive({
newPassword: "",
confirmPassword: "",
});
async function adminSubmitPassword() {
const response = await APIAdminUpdatePassword(
reqUserId,
passwordForm.newPassword,
passwordForm.confirmPassword
);
if (response.status == "success") {
alert("Password changed.");
passwordForm.newPassword = null;
passwordForm.confirmPassword = null;
await loadUser();
} else {
alert("Failed to update password. Check that both passwords match.");
}
}
const openArcade = (item) => {
const arcadeId = item.id;
$router.push(`/arcade/${arcadeId}`);
};
</script>
<template>
@ -235,6 +290,149 @@ const cardBoxes = ref([
/>
<UserCard class="mb-6" :override-profile="userProfile" use-small />
<template v-if="mainStore.userAdmin">
<SectionTitleLine
:icon="mdiShieldEditOutline"
title="User Administration"
main
/>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<CardBox is-form class="" @submit.prevent="adminUpdateUser">
<PillTag
color="info"
label="General Information"
:icon="mdiInformationOutline"
class="mb-2"
/>
<div class="grid md:grid-cols-2 gap-x-4 mb-6">
<FormField label="Username">
<FormControl
v-model="newUser.name"
:input-value="newUser.name"
name="username"
required
/>
</FormField>
<FormField
label="E-mail"
help="Used for password resetting and 2FA"
>
<FormControl
v-model="newUser.email"
:icon="mdiMail"
type="email"
name="email"
required
/>
</FormField>
<FormField label="PIN" help="Used when logging into a game">
<FormControl
v-model="newUser.pin"
:icon="mdiAsterisk"
type="password"
name="pin"
:minlength="4"
:maxlength="4"
inputmode="numeric"
pattern="\d{4}"
@input="pinInput"
/>
</FormField>
</div>
<FormField
label="Public Profile"
help="Show this profile publicly. If disabled, only game profiles and scores will be visible."
>
<FormCheckRadio
v-model="newUser.public"
name="public"
:model-value="newUser.public"
:input-value="newUser.public"
type="switch"
/>
</FormField>
<FormField
label="Ban Profile"
help="Ban this profile. Locks out all arcades that this user manages."
>
<FormCheckRadio
v-model="newUser.banned"
name="banned"
:model-value="newUser.banned"
:input-value="newUser.banned"
type="switch"
/>
</FormField>
<div
v-if="JSON.stringify(userProfile) !== JSON.stringify(newUser)"
class="space-x-2 mt-6 mb-4"
>
<BaseButton color="success" label="Save" type="submit" />
<BaseButton
color="warning"
label="Revert"
@click="loadUser()"
/>
</div>
</CardBox>
<CardBox is-form @submit.prevent="adminSubmitPassword()">
<PillTag color="info" label="Change Password" class="mb-2" />
<FormField label="New Password">
<FormControl
v-model="passwordForm.newPassword"
:icon="mdiAsterisk"
name="newPassword"
type="password"
required
/>
</FormField>
<FormField label="Confirm Password">
<FormControl
v-model="passwordForm.confirmPassword"
:icon="mdiAsterisk"
name="confirmPassword"
type="password"
required
/>
</FormField>
<template #footer>
<BaseButton type="submit" color="success" label="Update" />
</template>
</CardBox>
<CardBox v-if="userProfile?.arcades?.length" class="mb-6">
<PillTag color="info" label="Arcades" class="mb-2" />
<div class="grid gap-4 md:grid-cols-2">
<div
v-for="arcade of userProfile.arcades"
:key="arcade.id"
class="bg-slate-800 p-4 rounded-xl"
>
<div class="md:flex w-full place-content-between">
<div>
<h1 class="text-md md:text-lg">{{ arcade.name }}</h1>
</div>
<div class="flex align-middle mt-2 md:mt-0 max-h-12">
<BaseButton
label="Open Arcade"
color="info"
@click="openArcade(arcade)"
/>
</div>
</div>
</div>
</div>
</CardBox>
</div>
</template>
<SectionTitleLine
:icon="mdiChartTimelineVariant"
title="Quick Stats"
@ -264,7 +462,8 @@ const cardBoxes = ref([
:key="profile.game"
:game="profile.game"
:value="profile.data.total_plays"
profile-name=""
:user-id="reqUserId"
:profile-name="profile?.username"
type="plays"
/>
</div>