Finish up more admin features, clean up code and layouts
Some checks are pending
Build / build (push) Waiting to run
@@ -1,4 +1,4 @@
|
||||
VITE_APP_VERSION="3.0.15"
|
||||
VITE_APP_VERSION="3.0.16"
|
||||
VITE_API_URL="http://10.5.7.5:8000/"
|
||||
VITE_API_KEY="your_api_key_should_be_here"
|
||||
VITE_ASSET_PATH="/assets"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
VITE_APP_VERSION="3.0.15"
|
||||
VITE_APP_VERSION="3.0.16"
|
||||
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"
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 221 KiB |
BIN
public/assets/games/nost/card/1.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
public/assets/games/nost/card/2.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/assets/games/nost/card/3.webp
Normal file
|
After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 22 KiB |
@@ -14,5 +14,6 @@
|
||||
"3.0.12": ["- (Feature) Add new game event settings to SILENT SCOPE: BONE EATER and beatmaniaIIDX EPOLIS."],
|
||||
"3.0.13": ["- (Feature) Add new game event settings to beatmaniaIIDX EPOLIS."],
|
||||
"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.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."]
|
||||
}
|
||||
@@ -423,14 +423,9 @@ export const IIDXOptions = {
|
||||
type: "Array",
|
||||
options: [
|
||||
{ id: 0, label: "No Event" },
|
||||
{ id: 1, label: "MY POLIS DESIGNER (MAP 1)" },
|
||||
{ id: 2, label: "MY POLIS DESIGNER (MAP 2)" },
|
||||
{ id: 3, label: "MY POLIS DESIGNER (MAP 3)" },
|
||||
{ id: 4, label: "MY POLIS DESIGNER (MAP FINAL)" },
|
||||
{ id: 5, label: "EPOLIS RESTORATION (SYSTEM A)" },
|
||||
{ id: 6, label: "EPOLIS RESTORATION (SYSTEM B)" },
|
||||
{ id: 7, label: "EPOLIS RESTORATION (SYSTEM C)" },
|
||||
{ id: 8, label: "EPOLIS RESTORATION (SYSTEM D + X)" },
|
||||
{ id: 1, label: "Pinky Jump UP! (MAP 1)" },
|
||||
{ id: 2, label: "Pinky Jump UP! (MAP 2)" },
|
||||
{ id: 3, label: "Pinky Jump UP! (MAP 3)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1407,7 +1407,7 @@ export const gameData = [
|
||||
},
|
||||
{
|
||||
id: VersionConstants.IIDX_PINKY_CRUSH,
|
||||
label: "PINKY CRUSH",
|
||||
label: "Pinky Crush",
|
||||
maxRivals: 5,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -42,6 +42,20 @@ export async function checkPCBID(PCBID) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function APIAdminCreateMachine(arcadeId, newMachine) {
|
||||
try {
|
||||
const data = await mainStore.callApi(
|
||||
`/admin/arcade/${arcadeId}/machine`,
|
||||
"POST",
|
||||
newMachine
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log("Error adding machine:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function APIOnboardArcade(newArcade) {
|
||||
try {
|
||||
const data = await mainStore.callApi(
|
||||
@@ -56,6 +70,20 @@ export async function APIOnboardArcade(newArcade) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function APIAdminUpdateArcade(arcadeId, newArcade) {
|
||||
try {
|
||||
const data = await mainStore.callApi(
|
||||
`/admin/arcade/${arcadeId}`,
|
||||
"POST",
|
||||
newArcade
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log("Error updating arcade:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function APIAdminMaintenancePeriods() {
|
||||
try {
|
||||
const data = await mainStore.callApi(`/admin/maint`);
|
||||
@@ -95,3 +123,33 @@ export async function APIAdminCreateClient(newClient) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function APIAdminUsers() {
|
||||
try {
|
||||
const data = await mainStore.callApi(`/admin/users`);
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log("Error loading users:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function APIAdminNews() {
|
||||
try {
|
||||
const data = await mainStore.callApi(`/admin/news`);
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log("Error loading news:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function APIAdminCreateNews(newNews) {
|
||||
try {
|
||||
const data = await mainStore.callApi(`/admin/news`, "POST", newNews);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log("Error posting news:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,10 @@ function filterArcades() {
|
||||
if (filterForm.filter) {
|
||||
return arcadeData.value.filter(
|
||||
(arcade) =>
|
||||
arcade.id
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(filterForm.filter.toLowerCase()) ||
|
||||
arcade.name.toLowerCase().includes(filterForm.filter.toLowerCase()) ||
|
||||
arcade.description
|
||||
.toLowerCase()
|
||||
@@ -112,30 +116,6 @@ function filterArcades() {
|
||||
<p class="text-sm text-gray-400">Enter an arcade to manage it</p>
|
||||
</CardBox>
|
||||
|
||||
<SectionTitleLine
|
||||
:icon="mdiStoreOutline"
|
||||
title="All Arcades"
|
||||
color="text-blue-400"
|
||||
main
|
||||
/>
|
||||
|
||||
<CardBox has-table class="mb-6">
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900/95 rounded-2xl lg:flex lg:justify-between"
|
||||
>
|
||||
<div class="w-full">
|
||||
<GeneralTable
|
||||
:headers="headers"
|
||||
:items="arcadeData"
|
||||
@row-clicked="openArcade"
|
||||
/>
|
||||
<h1 class="mx-4 mt-1 mb-2 text-md lg:text-lg">
|
||||
Click row to open arcade
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<SectionTitleLine
|
||||
:icon="mdiStoreEditOutline"
|
||||
title="Search Arcades"
|
||||
@@ -145,7 +125,7 @@ function filterArcades() {
|
||||
<CardBox class="mb-6">
|
||||
<FormField
|
||||
label="Search"
|
||||
help="Search by name, description, or owner."
|
||||
help="Search by ID, name, description, or owner."
|
||||
class="w-full md:w-1/3"
|
||||
>
|
||||
<FormControl
|
||||
@@ -167,9 +147,16 @@ function filterArcades() {
|
||||
{{ arcade.description }}
|
||||
</h2>
|
||||
<h2 class="text-md">Managed by {{ arcade.owners }}</h2>
|
||||
<div
|
||||
class="bg-slate-900 p-2 rounded-md mt-2 w-12 max-w-14 text-center"
|
||||
>
|
||||
<h2 class="text-lg font-mono text-pink-700">
|
||||
{{ arcade.id }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex align-middle mt-2 md:mt-0">
|
||||
<div class="flex align-middle mt-2 md:mt-0 max-h-12">
|
||||
<BaseButton
|
||||
label="Open Arcade"
|
||||
color="info"
|
||||
@@ -181,6 +168,29 @@ function filterArcades() {
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<SectionTitleLine
|
||||
:icon="mdiStoreOutline"
|
||||
title="All Arcades"
|
||||
color="text-blue-400"
|
||||
main
|
||||
/>
|
||||
<CardBox has-table class="mb-6">
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900/95 rounded-2xl lg:flex lg:justify-between"
|
||||
>
|
||||
<div class="w-full">
|
||||
<GeneralTable
|
||||
:headers="headers"
|
||||
:items="arcadeData"
|
||||
@row-clicked="openArcade"
|
||||
/>
|
||||
<h1 class="mx-4 mt-1 mb-2 text-md lg:text-lg">
|
||||
Click row to open arcade
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<SectionTitleLine
|
||||
:icon="mdiStorePlusOutline"
|
||||
title="Create Arcade"
|
||||
|
||||
@@ -1,30 +1,147 @@
|
||||
<script setup>
|
||||
import { mdiFlagCheckered, mdiSecurity } from "@mdi/js";
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { mdiNewspaper, mdiNewspaperPlus } from "@mdi/js";
|
||||
import SectionMain from "@/components/SectionMain.vue";
|
||||
import CardBox from "@/components/CardBox.vue";
|
||||
import CardBoxWidget from "@/components/CardBoxWidget.vue";
|
||||
import GeneralTable from "@/components/GeneralTable.vue";
|
||||
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
|
||||
import SectionTitleLine from "@/components/SectionTitleLine.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
import FormControl from "@/components/FormControl.vue";
|
||||
|
||||
import { APIAdminNews, APIAdminCreateNews } from "@/stores/api/admin";
|
||||
|
||||
const $router = useRouter();
|
||||
const newsData = ref([]);
|
||||
const headers = [
|
||||
{
|
||||
text: "Title",
|
||||
value: "title",
|
||||
width: 150,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
text: "Body",
|
||||
value: "body",
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
text: "Image URL",
|
||||
value: "data.img",
|
||||
width: 300,
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(async () => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const data = await APIAdminNews();
|
||||
var formattedItems = [];
|
||||
for (var item of data) {
|
||||
if (item.timestamp) {
|
||||
const date = new Date(item.timestamp * 1000);
|
||||
item.timestamp = date.toLocaleString();
|
||||
}
|
||||
|
||||
formattedItems.push(item);
|
||||
}
|
||||
|
||||
newsData.value = formattedItems;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch news data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const openNews = (item) => {
|
||||
const arcadeId = item.id;
|
||||
$router.push(`/news/${arcadeId}`);
|
||||
};
|
||||
|
||||
const initNews = {
|
||||
title: null,
|
||||
body: null,
|
||||
data: { img: null },
|
||||
};
|
||||
|
||||
const newNews = reactive({
|
||||
...initNews,
|
||||
});
|
||||
|
||||
async function createNews() {
|
||||
try {
|
||||
const data = await APIAdminCreateNews(newNews);
|
||||
newsData.value = data;
|
||||
} catch (error) {
|
||||
console.error("Failed to post news:", error);
|
||||
}
|
||||
|
||||
Object.assign(newNews, initNews);
|
||||
loadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutAuthenticated>
|
||||
<SectionMain>
|
||||
<CardBox class="mb-6 p-1">
|
||||
<h1 class="text-3xl">News Management</h1>
|
||||
<p class="text-sm text-gray-400">Enter an post to read it</p>
|
||||
</CardBox>
|
||||
|
||||
<SectionTitleLine
|
||||
:icon="mdiSecurity"
|
||||
title="Network Administration"
|
||||
color="text-red-600"
|
||||
:icon="mdiNewspaperPlus"
|
||||
title="Create News"
|
||||
color="text-emerald-600"
|
||||
main
|
||||
/>
|
||||
<CardBox is-form class="row-span-1 mb-6" @submit.prevent="createNews()">
|
||||
<FormField label="Title" help="News headline.">
|
||||
<FormControl v-model="newNews.title" required />
|
||||
</FormField>
|
||||
<FormField label="Body" help="News body.">
|
||||
<FormControl v-model="newNews.body" required type="textarea" />
|
||||
</FormField>
|
||||
<FormField label="Image URL" help="News image URL.">
|
||||
<FormControl v-model="newNews.data.img" required />
|
||||
</FormField>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 mb-6">
|
||||
<CardBoxWidget :number="1" label="User Account(s)" />
|
||||
<CardBoxWidget :number="2" label="Registered Arcades" />
|
||||
<CardBoxWidget :number="3" label="Published Scores" />
|
||||
</div>
|
||||
<div>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="success"
|
||||
label="Create"
|
||||
:small="false"
|
||||
/>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<SectionTitleLine :icon="mdiFlagCheckered" title="Recent Errors" />
|
||||
<CardBox> </CardBox>
|
||||
<SectionTitleLine
|
||||
:icon="mdiNewspaper"
|
||||
title="All News"
|
||||
color="text-blue-400"
|
||||
main
|
||||
/>
|
||||
<CardBox has-table class="mb-6">
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900/95 rounded-2xl lg:flex lg:justify-between"
|
||||
>
|
||||
<div class="w-full">
|
||||
<GeneralTable
|
||||
:headers="headers"
|
||||
:items="newsData"
|
||||
@row-clicked="openNews"
|
||||
/>
|
||||
<h1 class="mx-4 mt-1 mb-2 text-md lg:text-lg">
|
||||
Click row to open arcade
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</LayoutAuthenticated>
|
||||
</template>
|
||||
|
||||
@@ -1,30 +1,183 @@
|
||||
<script setup>
|
||||
import { mdiFlagCheckered, mdiSecurity } from "@mdi/js";
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { mdiAccountBadgeOutline } from "@mdi/js";
|
||||
import SectionMain from "@/components/SectionMain.vue";
|
||||
import CardBox from "@/components/CardBox.vue";
|
||||
import CardBoxWidget from "@/components/CardBoxWidget.vue";
|
||||
import GeneralTable from "@/components/GeneralTable.vue";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
import FormControl from "@/components/FormControl.vue";
|
||||
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
|
||||
import SectionTitleLine from "@/components/SectionTitleLine.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
|
||||
import { APIAdminUsers } from "@/stores/api/admin";
|
||||
|
||||
const $router = useRouter();
|
||||
const userData = ref([]);
|
||||
const headers = [
|
||||
{
|
||||
text: "User ID",
|
||||
value: "id",
|
||||
width: 50,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
text: "Username",
|
||||
value: "username",
|
||||
width: 150,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
text: "Admin",
|
||||
value: "admin",
|
||||
width: 120,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
text: "Banned",
|
||||
value: "banned",
|
||||
width: 120,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
text: "Discord Linked",
|
||||
value: "data.discord.linked",
|
||||
width: 120,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
text: "Tachi Linked",
|
||||
value: "data.tachi.linked",
|
||||
width: 120,
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(async () => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const data = await APIAdminUsers();
|
||||
var formattedItems = [];
|
||||
for (var item of data) {
|
||||
if (!item.username) {
|
||||
item.username = "Unclaimed Account";
|
||||
}
|
||||
|
||||
formattedItems.push(item);
|
||||
}
|
||||
|
||||
userData.value = formattedItems;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const openUser = (item) => {
|
||||
const userId = item.id;
|
||||
$router.push(`/user/${userId}`);
|
||||
};
|
||||
|
||||
const filterForm = reactive({
|
||||
filter: null,
|
||||
});
|
||||
|
||||
function filterUsers() {
|
||||
if (filterForm.filter) {
|
||||
return userData.value
|
||||
.filter(
|
||||
(user) =>
|
||||
user.id
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(filterForm.filter.toLowerCase()) ||
|
||||
user.username.toLowerCase().includes(filterForm.filter.toLowerCase())
|
||||
)
|
||||
.slice(0, 25); // Cap results to 25
|
||||
}
|
||||
return [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutAuthenticated>
|
||||
<SectionMain>
|
||||
<CardBox class="mb-6 p-1">
|
||||
<h1 class="text-3xl">User Management</h1>
|
||||
<p class="text-sm text-gray-400">Click a row to open User</p>
|
||||
</CardBox>
|
||||
|
||||
<CardBox class="mb-6">
|
||||
<FormField
|
||||
label="Search"
|
||||
help="Search by ID or username."
|
||||
class="w-full md:w-1/3"
|
||||
>
|
||||
<FormControl
|
||||
v-model="filterForm.filter"
|
||||
:model-value="filterForm.filter"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div
|
||||
v-for="user of filterUsers()"
|
||||
:key="user.id"
|
||||
class="bg-slate-800 p-4 rounded-xl"
|
||||
>
|
||||
<div class="md:flex w-full place-content-between">
|
||||
<div>
|
||||
<h1 class="text-lg md:text-xl">{{ user.username }}</h1>
|
||||
<h2 class="text-md md:text-lg">
|
||||
User {{ user.banned ? "is" : "isn't" }} banned.
|
||||
</h2>
|
||||
<h2 class="text-md">
|
||||
Discord
|
||||
{{ user.data?.discord?.linked ? "is" : "isn't" }} linked.
|
||||
</h2>
|
||||
<div
|
||||
class="bg-slate-900 p-2 rounded-md mt-2 w-14 max-w-16 text-center"
|
||||
>
|
||||
<h2 class="text-lg font-mono text-pink-700">
|
||||
{{ user.id }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex align-middle mt-2 md:mt-0 max-h-16">
|
||||
<BaseButton
|
||||
label="Open User"
|
||||
color="info"
|
||||
@click="openUser(user)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<SectionTitleLine
|
||||
:icon="mdiSecurity"
|
||||
title="Network Administration"
|
||||
color="text-red-600"
|
||||
:icon="mdiAccountBadgeOutline"
|
||||
title="All Users"
|
||||
color="text-blue-400"
|
||||
main
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 mb-6">
|
||||
<CardBoxWidget :number="1" label="User Account(s)" />
|
||||
<CardBoxWidget :number="2" label="Registered Arcades" />
|
||||
<CardBoxWidget :number="3" label="Published Scores" />
|
||||
</div>
|
||||
|
||||
<SectionTitleLine :icon="mdiFlagCheckered" title="Recent Errors" />
|
||||
<CardBox> </CardBox>
|
||||
<CardBox has-table class="mb-6">
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900/95 rounded-2xl lg:flex lg:justify-between"
|
||||
>
|
||||
<div class="w-full">
|
||||
<GeneralTable
|
||||
:headers="headers"
|
||||
:items="userData"
|
||||
@row-clicked="openUser(user)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</LayoutAuthenticated>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
<script setup>
|
||||
import { useRoute } from "vue-router";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { mdiStore, mdiStoreCog, mdiCogOutline } from "@mdi/js";
|
||||
import {
|
||||
mdiStore,
|
||||
mdiStoreCog,
|
||||
mdiCogOutline,
|
||||
mdiShieldEditOutline,
|
||||
mdiCheckOutline,
|
||||
mdiCloseOutline,
|
||||
mdiInformationOutline,
|
||||
} from "@mdi/js";
|
||||
import SectionMain from "@/components/SectionMain.vue";
|
||||
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
|
||||
import SectionTitleLine from "@/components/SectionTitleLine.vue";
|
||||
@@ -9,7 +17,9 @@ import ArcadeCard from "@/components/ArcadeCard.vue";
|
||||
import CardBox from "@/components/CardBox.vue";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
import FormCheckRadio from "@/components/FormCheckRadio.vue";
|
||||
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 CardBoxWidget from "@/components/CardBoxWidget.vue";
|
||||
import { getNestedValue, setNestedValue } from "@/constants/values";
|
||||
@@ -18,11 +28,16 @@ import {
|
||||
APIUpdateArcade,
|
||||
APIGetArcadeVPN,
|
||||
} from "@/stores/api/arcade";
|
||||
import { checkArcadeName, APIAdminUpdateArcade } from "@/stores/api/admin";
|
||||
import { useMainStore } from "@/stores/main";
|
||||
|
||||
const mainStore = useMainStore();
|
||||
const optionForm = ref(null);
|
||||
const bareForm = ref(null);
|
||||
const arcadeData = ref(null);
|
||||
const loading = ref(true);
|
||||
const arcadeCheck = ref(null);
|
||||
const newArcade = ref(null);
|
||||
|
||||
const $route = useRoute();
|
||||
const arcadeId = parseInt($route.params.id);
|
||||
@@ -61,6 +76,7 @@ async function loadArcade() {
|
||||
bareForm.value = {};
|
||||
const data = await APIGetArcade(arcadeId);
|
||||
arcadeData.value = data;
|
||||
newArcade.value = JSON.parse(JSON.stringify(data));
|
||||
|
||||
for (const setting of arcadeOptions) {
|
||||
const value = getNestedValue(arcadeData.value.data, setting.id);
|
||||
@@ -119,6 +135,27 @@ async function exportVPN() {
|
||||
console.log("Failed to fetch arcade data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkName() {
|
||||
if (newArcade.value.name) {
|
||||
const data = await checkArcadeName(newArcade.value.name);
|
||||
if (data.unused != undefined) {
|
||||
arcadeCheck.value = data.unused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function adminUpdateArcade() {
|
||||
const response = await APIAdminUpdateArcade(arcadeId, {
|
||||
name: newArcade.value.name,
|
||||
description: newArcade.value.description,
|
||||
beta: newArcade.value.data.is_beta,
|
||||
});
|
||||
|
||||
if (response.status != "error") {
|
||||
await loadArcade();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -127,12 +164,76 @@ async function exportVPN() {
|
||||
<template v-if="!loading">
|
||||
<ArcadeCard class="mb-6" :arcade="arcadeData" />
|
||||
|
||||
<SectionTitleLine
|
||||
:icon="mdiStoreCog"
|
||||
title="Arcade Administration"
|
||||
main
|
||||
/>
|
||||
<template v-if="mainStore.userAdmin">
|
||||
<SectionTitleLine
|
||||
:icon="mdiShieldEditOutline"
|
||||
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"
|
||||
>
|
||||
<BaseButton color="success" label="Save" type="submit" />
|
||||
<BaseButton color="danger" label="Revert" @click="loadArcade()" />
|
||||
</div>
|
||||
</CardBox>
|
||||
</template>
|
||||
|
||||
<SectionTitleLine :icon="mdiStoreCog" title="Arcade Management" main />
|
||||
<CardBox is-form class="mb-6" @submit.prevent="updateArcade">
|
||||
<PillTag
|
||||
color="info"
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { mdiGamepad } from "@mdi/js";
|
||||
import { mdiGamepad, mdiDatabasePlus } from "@mdi/js";
|
||||
import { useMainStore } from "@/stores/main";
|
||||
import SectionMain from "@/components/SectionMain.vue";
|
||||
import CardBox from "@/components/CardBox.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
import FormControl from "@/components/FormControl.vue";
|
||||
import FormCheckRadio from "@/components/FormCheckRadio.vue";
|
||||
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
|
||||
import SectionTitleLine from "@/components/SectionTitleLine.vue";
|
||||
import ArcadeCard from "@/components/ArcadeCard.vue";
|
||||
import GeneralTable from "@/components/GeneralTable.vue";
|
||||
import { generatePCBID } from "@/constants/pcbid.js";
|
||||
import { APIGetArcade } from "@/stores/api/arcade";
|
||||
import { checkPCBID, APIAdminCreateMachine } from "@/stores/api/admin";
|
||||
|
||||
const mainStore = useMainStore();
|
||||
const arcadeData = ref({});
|
||||
const loading = ref(true);
|
||||
|
||||
@@ -18,7 +26,6 @@ const arcadeId = parseInt($route.params.id);
|
||||
|
||||
async function loadArcade() {
|
||||
try {
|
||||
arcadeData.value = null;
|
||||
const data = await APIGetArcade(arcadeId);
|
||||
arcadeData.value = data;
|
||||
loading.value = false;
|
||||
@@ -66,6 +73,42 @@ const copyToClipboard = (text) => {
|
||||
alert("Failed to copy to clipboard!");
|
||||
});
|
||||
};
|
||||
|
||||
const initMachine = {
|
||||
name: "",
|
||||
cabinet: false,
|
||||
ota: false,
|
||||
generatePCBID: true,
|
||||
PCBID: "",
|
||||
};
|
||||
|
||||
const newMachine = reactive({
|
||||
...initMachine,
|
||||
});
|
||||
|
||||
async function addMachine() {
|
||||
if (newMachine.generatePCBID) {
|
||||
newMachine.PCBID = generatePCBID();
|
||||
}
|
||||
|
||||
while (!(await checkPCBID(newMachine.PCBID))) {
|
||||
if (newMachine.generatePCBID) {
|
||||
newMachine.PCBID = generatePCBID();
|
||||
} else {
|
||||
window.alert("PCBID already in use!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
APIAdminCreateMachine(arcadeData.value.id, {
|
||||
name: newMachine.name,
|
||||
cabinet: newMachine.cabinet,
|
||||
ota: newMachine.ota,
|
||||
PCBID: newMachine.PCBID.toUpperCase(),
|
||||
});
|
||||
Object.assign(newMachine, initMachine);
|
||||
loadArcade();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -73,8 +116,68 @@ const copyToClipboard = (text) => {
|
||||
<SectionMain>
|
||||
<template v-if="!loading">
|
||||
<ArcadeCard class="mb-6" :arcade="arcadeData" :use-small="true" />
|
||||
<SectionTitleLine :icon="mdiGamepad" title="Machines" main />
|
||||
|
||||
<template v-if="mainStore.userAdmin">
|
||||
<SectionTitleLine :icon="mdiDatabasePlus" title="Add Machine" main />
|
||||
<CardBox is-form class="mb-6">
|
||||
<form class="h-full" @submit.prevent="addMachine()">
|
||||
<FormField
|
||||
label="Generate PCBID"
|
||||
help="Generate a PCBID rather than supply one."
|
||||
>
|
||||
<FormCheckRadio
|
||||
v-model="newMachine.generatePCBID"
|
||||
:input-value="newMachine.generatePCBID"
|
||||
name="generatePCBID"
|
||||
type="switch"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField v-if="!newMachine.generatePCBID" label="PCBID">
|
||||
<FormControl
|
||||
v-model="newMachine.PCBID"
|
||||
name="pcbid"
|
||||
required
|
||||
minlength="20"
|
||||
maxlength="20"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Name">
|
||||
<FormControl v-model="newMachine.name" name="name" required />
|
||||
</FormField>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 w-full gap-2 md:gap-6 md:flex md:place-content-stretch pb-4 md:pb-0"
|
||||
>
|
||||
<FormField
|
||||
label="Arcade Cabinet"
|
||||
help="If this is a real cabinet, enable this."
|
||||
>
|
||||
<FormCheckRadio
|
||||
v-model="newMachine.cabinet"
|
||||
:input-value="newMachine.cabinet"
|
||||
name="cabinet"
|
||||
type="switch"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Enable Updates"
|
||||
help="Enables or disables OTA updates."
|
||||
>
|
||||
<FormCheckRadio
|
||||
v-model="newMachine.ota"
|
||||
:input-value="newMachine.ota"
|
||||
name="ota"
|
||||
type="switch"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<BaseButton color="success" type="submit" label="Add Machine" />
|
||||
</form>
|
||||
</CardBox>
|
||||
</template>
|
||||
|
||||
<SectionTitleLine :icon="mdiGamepad" title="Machines" main />
|
||||
<CardBox has-table>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900/95 rounded-2xl lg:flex lg:justify-between"
|
||||
|
||||