Finish admin news and arcade,

This commit is contained in:
Trenton Zimmer 2025-07-13 14:13:27 -04:00
parent 998be4f087
commit 091dfe548e
9 changed files with 353 additions and 118 deletions

View File

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

@ -19,5 +19,6 @@
"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."],
"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.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"]
}

View File

@ -32,6 +32,7 @@ function downloadJSON() {
table-class-name="customize-table"
header-text-direction="left"
body-text-direction="left"
:prevent-context-menu-row="false"
@click-row="handleRowClick"
>
<template #loading>
@ -89,6 +90,7 @@ function downloadJSON() {
? `https://cdn.discordapp.com/avatars/${data?.discord?.id}/${data?.discord?.avatar}`
: null
"
username=""
/>
</div>
<span>{{ username }}</span>

View File

@ -42,6 +42,16 @@ export async function checkPCBID(PCBID) {
}
}
export async function APIAdminMachineFromPCBID(PCBID) {
try {
const data = await mainStore.callApi(`/admin/machine/${PCBID}`);
return data.data;
} catch (error) {
console.log("Error testing machine:", error);
throw error;
}
}
export async function APIAdminCreateMachine(arcadeId, newMachine) {
try {
const data = await mainStore.callApi(
@ -213,6 +223,16 @@ export async function APIAdminUsers(noData = false) {
}
}
export async function APIAdminUserFromCardId(cardId) {
try {
const data = await mainStore.callApi(`/admin/user/card/${cardId}`);
return data.data;
} catch (error) {
console.log("Error getting user:", error);
throw error;
}
}
export async function APIAdminNews() {
try {
const data = await mainStore.callApi(`/admin/news`);
@ -232,3 +252,27 @@ export async function APIAdminCreateNews(newNews) {
throw error;
}
}
export async function APIAdminUpdateNews(newsId, newNews) {
try {
const data = await mainStore.callApi(
`/admin/news/${newsId}`,
"POST",
newNews
);
return data;
} catch (error) {
console.log("Error posting news:", error);
throw error;
}
}
export async function APIAdminDeleteNews(newsId) {
try {
const data = await mainStore.callApi(`/admin/news/${newsId}`, "DELETE");
return data;
} catch (error) {
console.log("Error posting news:", error);
throw error;
}
}

View File

@ -12,10 +12,11 @@ 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 PillTag from "@/components/PillTag.vue";
import FormField from "@/components/FormField.vue";
import FormControl from "@/components/FormControl.vue";
import { APIAdminArcades } from "@/stores/api/admin";
import { APIAdminArcades, APIAdminMachineFromPCBID } from "@/stores/api/admin";
const $router = useRouter();
const arcadeData = ref([]);
@ -109,6 +110,18 @@ function filterArcades() {
);
}
}
const PCBIDForm = ref(null);
async function findMachine() {
const machine = await APIAdminMachineFromPCBID(PCBIDForm.value);
if (!machine) {
window.alert("Couldn't find machine.");
return;
} else {
openArcade({ id: machine?.arcadeId });
}
}
</script>
<template>
@ -125,51 +138,73 @@ function filterArcades() {
color="text-amber-600"
main
/>
<CardBox class="mb-6">
<FormField
label="Search"
help="Search by ID, name, description, or owner."
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="arcade of filterArcades()"
:key="arcade.id"
class="bg-slate-800 p-4 rounded-xl"
<div class="grid md:grid-cols-2 gap-6">
<CardBox class="mb-6">
<PillTag color="info" label="Search" class="mb-2" />
<FormField
label="Search"
help="Search by ID, name, description, or owner."
class="w-full"
>
<div class="md:flex w-full place-content-between">
<div>
<h1 class="text-lg md:text-xl">{{ arcade.name }}</h1>
<h2 class="text-md md:text-lg">
{{ 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>
<FormControl
v-model="filterForm.filter"
:model-value="filterForm.filter"
/>
</FormField>
<div class="flex align-middle mt-2 md:mt-0 max-h-12">
<BaseButton
label="Open Arcade"
color="info"
@click="openArcade(arcade)"
/>
<div class="grid gap-4">
<div
v-for="arcade of filterArcades()"
:key="arcade.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">{{ arcade.name }}</h1>
<h2 class="text-md md:text-lg">
{{ 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 max-h-12">
<BaseButton
label="Open Arcade"
color="info"
@click="openArcade(arcade)"
/>
</div>
</div>
</div>
</div>
</div>
</CardBox>
</CardBox>
<CardBox class="mb-6" is-form @submit.prevent="findMachine">
<PillTag color="warning" label="From PCBID" class="mb-2" />
<FormField
label="PCBID"
help="20 character ID for a machine."
class="w-full"
>
<FormControl
v-model="PCBIDForm"
name="PCBID"
required
:model-value="PCBIDForm"
:minlength="20"
:maxlength="20"
/>
</FormField>
<BaseButton color="success" type="submit" label="Open Arcade" />
</CardBox>
</div>
<SectionTitleLine
:icon="mdiStoreOutline"

View File

@ -1,20 +1,35 @@
<script setup>
import { ref, reactive, onMounted } from "vue";
import { useRouter } from "vue-router";
import { mdiNewspaper, mdiNewspaperPlus } from "@mdi/js";
import {
mdiNewspaper,
mdiNewspaperVariantMultipleOutline,
mdiNewspaperMinus,
mdiNewspaperPlus,
} from "@mdi/js";
import SectionMain from "@/components/SectionMain.vue";
import CardBox from "@/components/CardBox.vue";
import GeneralTable from "@/components/GeneralTable.vue";
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
import SectionTitleLine from "@/components/SectionTitleLine.vue";
import BaseDivider from "@/components/BaseDivider.vue";
import BaseButton from "@/components/BaseButton.vue";
import PillTag from "@/components/PillTag.vue";
import FormField from "@/components/FormField.vue";
import FormControl from "@/components/FormControl.vue";
import { APIAdminNews, APIAdminCreateNews } from "@/stores/api/admin";
import {
APIAdminNews,
APIAdminCreateNews,
APIAdminUpdateNews,
APIAdminDeleteNews,
} from "@/stores/api/admin";
import BaseButtons from "@/components/BaseButtons.vue";
const $router = useRouter();
const newsData = ref([]);
const selectedNewsId = ref(null);
const selectedNews = ref(null);
const headers = [
{
text: "Title",
@ -83,6 +98,42 @@ async function createNews() {
Object.assign(newNews, initNews);
loadData();
}
async function updateNews() {
if (!selectedNews.value) {
return;
}
await APIAdminUpdateNews(selectedNewsId.value, selectedNews.value);
selectedNews.value = null;
selectedNewsId.value = null;
loadData();
}
async function deleteNews() {
if (!selectedNews.value) {
return;
}
await APIAdminDeleteNews(selectedNews.value.id);
selectedNews.value = null;
selectedNewsId.value = null;
loadData();
}
function formatNewsSelect(newsData) {
var formattedNews = [];
for (const news of newsData) {
formattedNews.push({ id: news.id, label: news.title });
}
return formattedNews;
}
function getSelectedNews(newsId) {
if (!newsId || !newsData.value) return null;
selectedNews.value = newsData.value?.find((news) => news.id === newsId);
return true;
}
</script>
<template>
@ -94,34 +145,95 @@ async function createNews() {
</CardBox>
<SectionTitleLine
:icon="mdiNewspaperPlus"
title="Create News"
:icon="mdiNewspaper"
title="Post Management"
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>
<BaseButton
type="submit"
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<CardBox is-form class="row-span-1 mb-6" @submit.prevent="createNews()">
<PillTag
color="success"
label="Create"
:small="false"
label="Create Post"
:icon="mdiNewspaperPlus"
class="mb-2"
/>
</div>
</CardBox>
<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>
<BaseButton
type="submit"
color="success"
label="Create"
:small="false"
/>
</div>
</CardBox>
<CardBox is-form class="row-span-1 mb-6">
<PillTag
color="warning"
label="Edit Post"
:icon="mdiNewspaperMinus"
class="mb-2"
/>
<FormField label="Post">
<FormControl
v-model="selectedNewsId"
name="news"
:options="formatNewsSelect(newsData)"
required
/>
</FormField>
<BaseDivider />
<template v-if="getSelectedNews(selectedNewsId)">
<FormField label="Title" help="News headline.">
<FormControl v-model="selectedNews.title" required />
</FormField>
<FormField label="Body" help="News body.">
<FormControl
v-model="selectedNews.body"
required
type="textarea"
/>
</FormField>
<FormField label="Image URL" help="News image URL.">
<FormControl v-model="selectedNews.data.img" required />
</FormField>
<BaseButtons>
<BaseButton
color="info"
label="Update"
:small="false"
:icon="mdiNewspaper"
@click="updateNews()"
/>
<BaseButton
color="danger"
label="Delete Post"
:small="false"
:icon="mdiNewspaperMinus"
@click="deleteNews()"
/>
</BaseButtons>
</template>
</CardBox>
</div>
<SectionTitleLine
:icon="mdiNewspaper"
:icon="mdiNewspaperVariantMultipleOutline"
title="All News"
color="text-blue-400"
main

View File

@ -7,14 +7,17 @@ import CardBox from "@/components/CardBox.vue";
import GeneralTable from "@/components/GeneralTable.vue";
import FormField from "@/components/FormField.vue";
import FormControl from "@/components/FormControl.vue";
import PillTag from "@/components/PillTag.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";
import { APIAdminUsers, APIAdminUserFromCardId } from "@/stores/api/admin";
const $router = useRouter();
const userData = ref([]);
const cardIdForm = ref(null);
const headers = [
{
text: "User ID",
@ -28,6 +31,12 @@ const headers = [
width: 150,
sortable: true,
},
{
text: "Public",
value: "public",
width: 120,
sortable: true,
},
{
text: "Admin",
value: "admin",
@ -100,6 +109,16 @@ function filterUsers() {
}
return [];
}
async function findUser() {
const user = await APIAdminUserFromCardId(cardIdForm.value);
if (!user) {
window.alert("Couldn't find user.");
return;
} else {
openUser({ id: user?.id });
}
}
</script>
<template>
@ -110,54 +129,76 @@ function filterUsers() {
<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="grid md:grid-cols-2 gap-6">
<CardBox class="mb-6">
<PillTag color="info" label="Search" class="mb-2" />
<FormField
label="Search"
help="Search by ID or username."
class="w-full"
>
<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>
<FormControl
v-model="filterForm.filter"
:model-value="filterForm.filter"
/>
</FormField>
<div class="flex align-middle mt-2 md:mt-0 max-h-16">
<BaseButton
label="Open User"
color="info"
@click="openUser(user)"
/>
<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>
</div>
</CardBox>
</CardBox>
<CardBox class="mb-6" is-form @submit.prevent="findUser">
<PillTag color="warning" label="From CardID" class="mb-2" />
<FormField
label="Card ID"
help="16 character ID for a user's card."
class="w-full"
>
<FormControl
v-model="cardIdForm"
name="Card ID"
required
:model-value="cardIdForm"
:minlength="16"
:maxlength="16"
/>
</FormField>
<BaseButton color="success" type="submit" label="Open User" />
</CardBox>
</div>
<SectionTitleLine
:icon="mdiAccountBadgeOutline"

View File

@ -19,15 +19,17 @@ import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
import SectionTitleLine from "@/components/SectionTitleLine.vue";
import LineChart from "@/components/Charts/LineChart.vue";
import { getGameInfo } from "@/constants";
import { generateChartData } from "@/components/Charts/chart.config";
import { APIGetUser } from "@/stores/api/account";
const $route = useRoute();
const reqUserId = $route.params.id;
const userProfile = ref({});
const userProfile = ref(null);
const userProfiles = ref(null);
const userScoreStats = ref(null);
async function loadUser() {
try {
userProfile.value = {};
var data = await APIGetUser(reqUserId);
if (!data.name) {
data.name = "Unclaimed Account";
@ -39,13 +41,11 @@ async function loadUser() {
}
onMounted(async () => {
loadUser();
console.log(userProfile.value);
await loadUser();
userProfiles.value = userProfile.value.profiles;
userScoreStats.value = userProfile.value.userScoreStats;
});
const userProfiles = computed(() => userProfile.value.userProfiles);
const userScoreStats = computed(() => userProfile.value.userScoreStats);
const cumulativePlays = computed(() => {
return userProfiles.value?.reduce(
(total, user) => total + user.data.total_plays,
@ -227,7 +227,7 @@ const cardBoxes = ref([
<template>
<LayoutAuthenticated>
<SectionMain>
<template v-if="userProfile != null">
<template v-if="userProfile !== null">
<SectionTitleLine
:icon="mdiAccount"
:title="`${userProfile.name}'s Profile`"