Add new cards, add new admin arcade features

This commit is contained in:
Trenton Zimmer
2025-07-09 16:47:10 -04:00
parent 4632b9cd19
commit abe3d146c0
14 changed files with 270 additions and 69 deletions

View File

@@ -1,4 +1,4 @@
VITE_APP_VERSION="3.0.17"
VITE_APP_VERSION="3.0.18"
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.17"
VITE_APP_VERSION="3.0.18"
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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -16,5 +16,6 @@
"3.0.14": ["- (Minor) Fix footer buttons on tables, add generic export table button for every table."],
"3.0.15": ["- (Minor) Add beta support for Pinky Crush, clean up gameDB a little.", "- (Optimization) Convert dashboard to a more modular backend.", "- (Bugfix) Fix warnings on auth pages."],
"3.0.16": ["- (Major) Add multiple new admin features for network management.", "- (Optimization) Optimize backend for arcade operations.", "- (Bugfix) Fix possible backend issues with better type enforcing.", "- (Bugfix) Fix event data and game data for IIDX Pinky Crush.", "- (Minor) Add assets for Nostalgia."],
"3.0.17": ["- (Major) Add initial public profile support.", "- (Minor) Lay groundwork for public profile page", "- (Optimization) Clean up admin pages", "- (Bugfix) Clean up random 500 errors."]
"3.0.17": ["- (Major) Add initial public profile support.", "- (Minor) Lay groundwork for public profile page", "- (Optimization) Clean up admin pages", "- (Bugfix) Clean up random 500 errors."],
"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."]
}

View File

@@ -1,13 +1,14 @@
<script setup>
import { mdiDownload, mdiChevronLeft, mdiChevronRight } from "@mdi/js";
import BaseButton from "@/components/BaseButton.vue";
import UserAvatar from "@/components/UserAvatar.vue";
import DownloadJS from "downloadjs";
import { useRoute } from "vue-router";
const $route = useRoute();
// eslint-disable-next-line vue/require-prop-types
const props = defineProps(["headers", "items"]);
const props = defineProps(["headers", "items", "hasAvatar"]);
const emits = defineEmits(["row-clicked"]);
const handleRowClick = (item) => {
@@ -78,6 +79,23 @@ function downloadJSON() {
/>
</div>
</template>
<template v-if="props.hasAvatar" #item-username="{ username, data }">
<div class="username-wrapper flex items-center gap-2">
<div class="w-6">
<UserAvatar
:avatar="
data?.discord
? `https://cdn.discordapp.com/avatars/${data?.discord?.id}/${data?.discord?.avatar}`
: null
"
/>
</div>
<span>{{ username }}</span>
</div>
</template>
<slot />
</EasyDataTable>
<div class="p-2 w-full flex md:justify-end">
<BaseButton

View File

@@ -7,6 +7,8 @@ export class ProfileCustomizations {
{ id: "gradient", label: "Pink/Purple Gradient" },
{ id: "pride", label: "Pride Flag" },
{ id: "trans", label: "Trans Flag" },
{ id: "nonbinary", label: "Non-Binary Flag" },
{ id: "lesbian", label: "Lesbian Flag" },
{ id: "loveplus", label: "LovePlus" },
{ id: "paint", label: "i eat paint" },
];
@@ -29,6 +31,7 @@ export class ProfileCustomizations {
{ id: "pride", label: "Pride Flag" },
{ id: "trans", label: "Trans Flag" },
{ id: "nonbinary", label: "Non-Binary Flag" },
{ id: "lesbian", label: "Lesbian Flag" },
{ id: "leaves", label: "Leaves" },
];
}

View File

@@ -84,6 +84,38 @@ export async function APIAdminUpdateArcade(arcadeId, newArcade) {
}
}
export async function APIAdminAddArcadeOwner(arcadeId, ownerId) {
try {
const data = await mainStore.callApi(
`/admin/arcade/${arcadeId}/owner`,
"PUT",
{ ownerId: ownerId }
);
return data;
} catch (error) {
console.log("Error updating arcade owner:", error);
throw error;
}
}
export async function APIAdminRemoveArcadeOwner(arcadeId, ownerId) {
try {
const confirmed = window.confirm("Are you really?");
if (confirmed) {
const data = await mainStore.callApi(
`/admin/arcade/${arcadeId}/owner`,
"DELETE",
{ ownerId: ownerId }
);
return data;
}
return {};
} catch (error) {
console.log("Error removing arcade owner:", error);
throw error;
}
}
export async function APIAdminMaintenancePeriods() {
try {
const data = await mainStore.callApi(`/admin/maint`);
@@ -124,9 +156,9 @@ export async function APIAdminCreateClient(newClient) {
}
}
export async function APIAdminUsers() {
export async function APIAdminUsers(noData = false) {
try {
const data = await mainStore.callApi(`/admin/users`);
const data = await mainStore.callApi(`/admin/users?noData=${noData}`);
return data.data;
} catch (error) {
console.log("Error loading users:", error);

View File

@@ -35,16 +35,19 @@ const headers = [
text: "Owners",
value: "owners",
width: 150,
sortable: true,
},
{
text: "PASELI Enabled",
value: "data.paseli_enabled",
width: 50,
sortable: true,
},
{
text: "Infinite PASELI",
value: "data.paseli_infinite",
width: 50,
sortable: true,
},
{
text: "Beta Enabled",

View File

@@ -19,7 +19,7 @@ const headers = [
{
text: "User ID",
value: "id",
width: 50,
width: 20,
sortable: true,
},
{
@@ -173,6 +173,7 @@ function filterUsers() {
<GeneralTable
:headers="headers"
:items="userData"
:has-avatar="true"
@row-clicked="openUser"
/>
</div>

View File

@@ -1,6 +1,6 @@
<script setup>
import { useRoute } from "vue-router";
import { ref, onMounted } from "vue";
import { ref, onMounted, watch } from "vue";
import {
mdiStore,
mdiStoreCog,
@@ -21,14 +21,22 @@ import FormControl from "@/components/FormControl.vue";
import PillTag from "@/components/PillTag.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import BaseButton from "@/components/BaseButton.vue";
import BaseDivider from "@/components/BaseDivider.vue";
import CardBoxWidget from "@/components/CardBoxWidget.vue";
import UserAvatar from "@/components/UserAvatar.vue";
import { getNestedValue, setNestedValue } from "@/constants/values";
import {
APIGetArcade,
APIUpdateArcade,
APIGetArcadeVPN,
} from "@/stores/api/arcade";
import { checkArcadeName, APIAdminUpdateArcade } from "@/stores/api/admin";
import {
checkArcadeName,
APIAdminUpdateArcade,
APIAdminUsers,
APIAdminAddArcadeOwner,
APIAdminRemoveArcadeOwner,
} from "@/stores/api/admin";
import { useMainStore } from "@/stores/main";
const mainStore = useMainStore();
@@ -38,6 +46,8 @@ const arcadeData = ref(null);
const loading = ref(true);
const arcadeCheck = ref(null);
const newArcade = ref(null);
const newManager = ref(null);
const userData = ref(null);
const $route = useRoute();
const arcadeId = parseInt($route.params.id);
@@ -89,6 +99,36 @@ async function loadArcade() {
}
}
async function loadUsers() {
if (mainStore.userAdmin) {
try {
const data = await APIAdminUsers(true);
const filteredData = [];
for (const user of data) {
if (user.username != undefined) {
filteredData.push({
id: user.id,
label: user.username,
});
}
}
filteredData.sort((a, b) => a.label.localeCompare(b.label));
userData.value = filteredData;
} catch (error) {
console.error("Failed to fetch admin user data:", error);
}
}
}
watch(
() => mainStore.userAdmin,
() => {
loadUsers();
}
);
async function updateArcade() {
const response = await APIUpdateArcade(arcadeId, optionForm.value);
@@ -99,6 +139,7 @@ async function updateArcade() {
onMounted(() => {
loadArcade();
loadUsers();
});
function formatName(inputString, replaceWith = "NA_") {
@@ -156,6 +197,31 @@ async function adminUpdateArcade() {
await loadArcade();
}
}
async function addManager() {
if (newManager.value == null) {
return;
}
const response = await APIAdminAddArcadeOwner(arcadeId, newManager.value);
if (response.status != "error") {
newManager.value = null;
loadArcade();
}
}
async function removeManager(ownerId) {
if (!ownerId) {
return;
}
const response = await APIAdminRemoveArcadeOwner(arcadeId, ownerId);
if (response.status != "error") {
loadArcade();
}
}
</script>
<template>
@@ -170,67 +236,136 @@ async function adminUpdateArcade() {
title="Arcade Administration"
main
/>
<CardBox is-form class="mb-6" @submit.prevent="adminUpdateArcade">
<PillTag
color="info"
label="General Information"
:icon="mdiInformationOutline"
class="mb-2"
/>
<FormField label="Arcade Name">
<FormControl
v-model="newArcade.name"
:input-value="newArcade.name"
name="arcadeName"
required
/>
</FormField>
<div class="mb-4 flex gap-2 items-stretch">
<BaseButton
color="info"
label="Check Name"
@click="checkName()"
/>
<BaseIcon
v-if="arcadeCheck == true"
:path="mdiCheckOutline"
color="text-green-400"
size="25"
/>
<BaseIcon
v-else-if="arcadeCheck == false"
:path="mdiCloseOutline"
color="text-red-400"
size="25"
/>
</div>
<FormField label="Description">
<FormControl
v-model="newArcade.description"
name="description"
required
/>
</FormField>
<FormField label="Beta Enabled">
<FormCheckRadio
v-model="newArcade.data.is_beta"
name="beta"
:model-value="newArcade.data.is_beta"
:input-value="newArcade.data.is_beta"
type="switch"
/>
</FormField>
<div
v-if="JSON.stringify(arcadeData) !== JSON.stringify(newArcade)"
class="space-x-2 mt-6"
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<CardBox
is-form
class="lg:mb-6"
@submit.prevent="adminUpdateArcade"
>
<BaseButton color="success" label="Save" type="submit" />
<BaseButton color="danger" label="Revert" @click="loadArcade()" />
</div>
</CardBox>
<PillTag
color="info"
label="General Information"
:icon="mdiInformationOutline"
class="mb-2"
/>
<FormField label="Arcade Name">
<FormControl
v-model="newArcade.name"
:input-value="newArcade.name"
name="arcadeName"
required
/>
</FormField>
<div class="mb-4 flex gap-2 items-stretch">
<BaseButton
color="info"
label="Check Name"
@click="checkName()"
/>
<BaseIcon
v-if="arcadeCheck == true"
:path="mdiCheckOutline"
color="text-green-400"
size="25"
/>
<BaseIcon
v-else-if="arcadeCheck == false"
:path="mdiCloseOutline"
color="text-red-400"
size="25"
/>
</div>
<FormField label="Description">
<FormControl
v-model="newArcade.description"
name="description"
required
/>
</FormField>
<FormField label="Beta Enabled">
<FormCheckRadio
v-model="newArcade.data.is_beta"
name="beta"
:model-value="newArcade.data.is_beta"
:input-value="newArcade.data.is_beta"
type="switch"
/>
</FormField>
<div
v-if="JSON.stringify(arcadeData) !== JSON.stringify(newArcade)"
class="space-x-2 mt-6"
>
<BaseButton color="success" label="Save" type="submit" />
<BaseButton
color="danger"
label="Revert"
@click="loadArcade()"
/>
</div>
</CardBox>
<CardBox class="mb-6">
<div class="mb-4">
<PillTag
color="info"
label="Add Manager"
:icon="mdiInformationOutline"
class="mb-2"
/>
<form class="h-full" @submit.prevent="addManager">
<FormField label="Username">
<FormControl
v-model="newManager"
name="username"
:options="userData"
required
/>
</FormField>
<BaseButton
color="success"
type="submit"
label="Add Manager"
/>
</form>
</div>
<BaseDivider />
<div class="mb-4">
<PillTag color="warning" label="Managers" class="mb-4" />
<div class="grid gap-4">
<CardBox
v-for="owner of arcadeData.owners"
:key="owner"
color-prop="bg-slate-800 dark:bg-slate-800"
>
<div class="flex justify-between items-center">
<div class="m-[-5px] md:flex items-center gap-2">
<div class="w-8 md:w-12">
<UserAvatar
:username="owner.username"
:avatar="owner.avatar"
/>
</div>
<h1 class="text-lg lg:text-xl font-bold">
{{ owner.username }}
</h1>
</div>
<BaseButton
color="danger"
label="Remove"
@click="removeManager(owner.id)"
/>
</div>
</CardBox>
</div>
</div>
</CardBox>
</div>
</template>
<SectionTitleLine :icon="mdiStoreCog" title="Arcade Management" main />

View File

@@ -42,11 +42,13 @@ const headers = [
{
text: "PCBID",
value: "pcbId",
sortable: true,
width: 200,
},
{
text: "Name",
value: "description",
sortable: true,
width: 200,
},
{
@@ -55,6 +57,12 @@ const headers = [
sortable: true,
width: 120,
},
{
text: "Arcade Machine",
value: "data.cabinet",
sortable: true,
width: 120,
},
{
text: "OTA",
value: "ota",