arcade paseli charge support, fiz table bug
Some checks failed
Build / build (push) Has been cancelled

This commit is contained in:
Trenton Zimmer 2025-10-04 20:25:50 -04:00
parent f2001b4126
commit 7bbc2d2c77
6 changed files with 354 additions and 96 deletions

View File

@ -1,75 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.png">
<link rel="apple-touch-startup-image" href="/favicon.png">
<meta name="apple-mobile-web-app-title" content="PhaseII">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="manifest" href="/data-sources/standalone.json" />
<title>PhaseWeb 3</title>
<style>
body {
background-color: #1f2937;
}
/* @media (prefers-color-scheme: dark) {
body {
background-color: #1f2937;
}
} */
</style>
<script>
var pattern = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a'];
var current = 0;
var keyHandler = function (event) {
if (pattern.indexOf(event.key) < 0 || event.key !== pattern[current]) {
current = 0;
return;
}
current++;
if (pattern.length === current) {
current = 0;
document.getElementById('audio').play();
window.alert('+30 lives.');
}
};
document.addEventListener('keydown', keyHandler, false);
</script>
</head>
<body class="scroll-smooth">
<noscript>
<strong>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- Particles -->
<!-- <div id="particles-js"></div>
<script src="/assets/particles.js"></script> -->
<!-- <script src="/assets/network.js"></script> -->
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<!-- Bats! -->
<!-- <script src="https://code.jquery.com/jquery.js"></script>
<script src="/assets/halloween-bats.js"></script>
<script type="text/javascript">
window.halloweenBats = $.halloweenBats({
image: "/assets/bats.png",
});
</script> -->
<audio id="audio" src="/assets/sounds/konami.mp3" class="hidden">
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.png">
<link rel="apple-touch-startup-image" href="/favicon.png">
<meta name="apple-mobile-web-app-title" content="PhaseII">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="manifest" href="/data-sources/standalone.json" />
<title>PhaseWeb 3</title>
<style>
body {
background-color: #1f2937;
}
/* @media (prefers-color-scheme: dark) {
body {
background-color: #1f2937;
}
} */
</style>
<script>
var pattern = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a'];
var current = 0;
var keyHandler = function (event) {
if (pattern.indexOf(event.key) < 0 || event.key !== pattern[current]) {
current = 0;
return;
}
current++;
if (pattern.length === current) {
current = 0;
document.getElementById('audio').play();
window.alert('+30 lives.');
}
};
document.addEventListener('keydown', keyHandler, false);
</script>
</head>
<body class="scroll-smooth">
<noscript>
<strong>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- Particles -->
<!-- <div id="particles-js"></div>
<script src="/assets/particles.js"></script> -->
<!-- <script src="/assets/network.js"></script> -->
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<!-- Bats! -->
<script src="https://code.jquery.com/jquery.js"></script>
<script src="/assets/halloween-bats.js"></script>
<audio id="audio" src="/assets/sounds/konami.mp3" class="hidden">
</body>
</html>

View File

@ -27,5 +27,6 @@
"3.0.25-oMini": ["- (Major ++) Move from Material Design Icons to Phosphor Icons. Icons are currently set to their initial picks, but may change (suggest a change!)", "- (Major) Add initial icon animations, start work on icon color standards", "- (Optimization) Improve random greeting selection (SoftieTechCat) ", "- (Minor) Fix ESlint config"],
"3.0.26": ["- (Minor) Game player table is more sortable", "- (Minor) Greetings now support custom styling", "- (Minor) Added the first styled greeting"],
"3.0.27": ["- (Major) Profile data exporting added.", "- (Minor) More work for rivals."],
"3.0.28": ["- (Major) Full support for rivals across applicable games", "- (Major) Move from Vue Hash routing to standard routing", "- (Bugfix) Fix issue when customizing on iOS"]
"3.0.28": ["- (Major) Full support for rivals across applicable games", "- (Major) Move from Vue Hash routing to standard routing", "- (Bugfix) Fix issue when customizing on iOS"],
"3.0.29": ["- (Major) Finish arcade PASELI support", "- (Minor) Clean up arcade page, add button for opening owner", "- (Bugfix) Fix table upper curved edges"]
}

View File

@ -31,6 +31,7 @@ function downloadJSON() {
header-text-direction="left"
body-text-direction="left"
:prevent-context-menu-row="false"
class="pt-2"
@click-row="handleRowClick"
>
<template #loading>

View File

@ -80,6 +80,20 @@ export async function APIGetArcadePASELI(arcadeId) {
}
}
export async function APICreditUserPASELI(arcadeId, userId, cardId, credit) {
try {
const data = await mainStore.callApi(`/arcade/${arcadeId}/paseli`, "POST", {
userId: userId,
cardId: cardId,
credit: credit,
});
return data.data;
} catch (error) {
console.log("Error fetching arcade PASELI data:", error);
throw error;
}
}
export async function APIStartTakeover(PCBID) {
try {
const data = await mainStore.callApi(`/arcade/takeover?PCBID=${PCBID}`);

View File

@ -19,6 +19,7 @@ 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 BaseButtons from "@/components/BaseButtons.vue";
import BaseDivider from "@/components/BaseDivider.vue";
import CardBoxWidget from "@/components/CardBoxWidget.vue";
import UserAvatar from "@/components/UserAvatar.vue";
@ -237,6 +238,10 @@ async function adminDeleteArcade() {
}
}
}
const navigateToProfile = (userID) => {
$router.push(`/profiles/${userID}`);
};
</script>
<template>
@ -373,11 +378,18 @@ async function adminDeleteArcade() {
{{ owner.username }}
</h1>
</div>
<BaseButton
color="danger"
label="Remove"
@click="removeManager(owner.id)"
/>
<BaseButtons>
<BaseButton
label="Open Profile"
color="info"
@click="navigateToProfile(owner.id)"
/>
<BaseButton
color="danger"
label="Remove"
@click="removeManager(owner.id)"
/>
</BaseButtons>
</div>
</CardBox>
</div>

View File

@ -1,19 +1,35 @@
<script setup>
import { ref, onMounted } from "vue";
import { ref, onMounted, computed } from "vue";
import { useRoute } from "vue-router";
import { PhCashRegister, PhCurrencyJpy } from "@phosphor-icons/vue";
import {
PhCashRegister,
PhCreditCard,
PhCurrencyJpy,
PhPlusCircle,
} from "@phosphor-icons/vue";
import SectionMain from "@/components/SectionMain.vue";
import GeneralTable from "@/components/GeneralTable.vue";
import CardBox from "@/components/CardBox.vue";
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
import SectionTitleLine from "@/components/SectionTitleLine.vue";
import ArcadeCard from "@/components/ArcadeCard.vue";
import { APIGetArcade, APIGetArcadePASELI } from "@/stores/api/arcade";
import PillTag from "@/components/PillTag.vue";
import FormControl from "@/components/FormControl.vue";
import FormField from "@/components/FormField.vue";
import {
APIGetArcade,
APIGetArcadePASELI,
APICreditUserPASELI,
} from "@/stores/api/arcade";
import BaseButton from "@/components/BaseButton.vue";
import CardBoxWidget from "@/components/CardBoxWidget.vue";
const arcadeData = ref({});
const paseliData = ref({});
const loading = ref(true);
const newBalance = ref({});
const $route = useRoute();
const arcadeId = parseInt($route.params.id);
@ -32,6 +48,7 @@ async function loadPASELI() {
paseliData.value = null;
const data = await APIGetArcadePASELI(arcadeId);
paseliData.value = data;
paseliData.value.transactions?.reverse();
loading.value = false;
} catch (error) {
console.error("Failed to fetch arcade PASELI:", error);
@ -91,6 +108,106 @@ function filterTransactions(transactions) {
return filteredTransactions;
}
function cardInput(event) {
var input = event.target.value;
input = input.replace(/[^A-Za-z0-9]/g, "");
// Format the input to XXXX-XXXX-XXXX-XXXX
input = input
.toUpperCase()
.replace(/(.{4})/g, "$1-")
.replace(/-$/, "");
newBalance.value.cardId = input;
}
function numInput(event) {
var input = event.target.value;
input = input.replace(/[^0-9]/g, "");
newBalance.value.credit = input;
}
const paseliNetWorth = computed(() => {
if (!paseliData.value?.balances) return 0;
return paseliData.value.balances.reduce(
(sum, entry) => sum + (entry.balance || 0),
0,
);
});
const paseliStats = computed(() => {
if (!paseliData.value?.balances?.length) return {};
const balances = paseliData.value.balances.filter((b) => b.balance > 0);
const total = balances.reduce((sum, b) => sum + b.balance, 0);
const avg = total / balances.length;
const top = balances.reduce(
(a, b) => (b.balance > a.balance ? b : a),
balances[0],
);
return {
total: total,
avg: avg,
count: balances.length,
topPlayer: top.username || "(anonymous)",
topBalance: top.balance,
};
});
const paseliTransactions = computed(() => {
if (!paseliData.value?.transactions)
return { totalDelta: 0, count: 0, avgDelta: 0, last: null };
const tx = paseliData.value.transactions;
const totalDelta = tx.reduce((sum, t) => sum + (t.data?.delta || 0), 0);
const avgDelta = totalDelta / (tx.length || 1);
const lastTx = tx[tx.length - 1] || null;
return {
totalDelta,
count: tx.length,
avgDelta,
last: lastTx,
};
});
const existingBalances = computed(() => {
var balances = [];
for (const player of paseliData.value.balances) {
balances.push({
id: player.userId,
label: player.username == "" ? "Unclaimed Account" : player.username,
});
}
return balances;
});
async function creditCard() {
if (!newBalance.value.cardId && !newBalance.value.userId) {
return;
}
if (!newBalance.value.credit) {
return;
}
loading.value = true;
await APICreditUserPASELI(
arcadeId,
newBalance.value.userId,
newBalance.value.cardId,
newBalance.value.credit,
);
newBalance.value.userId = null;
newBalance.value.cardId = null;
newBalance.value.credit = null;
await loadPASELI();
}
</script>
<template>
@ -98,29 +215,147 @@ function filterTransactions(transactions) {
<SectionMain>
<template v-if="!loading && arcadeData">
<ArcadeCard class="mb-6" :arcade="arcadeData" :use-small="true" />
<SectionTitleLine :icon="PhCurrencyJpy" title="PASELI Stats" main />
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 mb-6">
<CardBoxWidget
v-if="paseliNetWorth"
:number="paseliNetWorth"
label="Total PASELI Net Worth"
:icon="PhCurrencyJpy"
/>
<CardBoxWidget
v-if="paseliTransactions.count"
:number="paseliTransactions.count"
label="Recent Transactions"
:icon="PhCashRegister"
/>
<CardBoxWidget
v-if="paseliTransactions.totalDelta"
:number="paseliTransactions.totalDelta"
label="Recent Net Flow"
:icon="PhCurrencyJpy"
/>
<CardBoxWidget
v-if="paseliStats.avg"
:number="paseliStats.avg"
label="Average Player Balance"
:icon="PhCurrencyJpy"
/>
<CardBoxWidget
v-if="paseliStats.topBalance"
:number="paseliStats.topBalance"
:label="`Top Player: ${paseliStats.topPlayer}`"
:icon="PhCurrencyJpy"
/>
</div>
<SectionTitleLine
:icon="PhCurrencyJpy"
title="Player PASELI Balances"
main
/>
<CardBox has-table>
<div
class="bg-white dark:bg-slate-900/95 rounded-2xl lg:flex lg:justify-between"
>
<div class="w-full">
<GeneralTable
:headers="balanceHeaders"
:items="paseliData.balances"
<div class="grid md:grid-cols-2 gap-6 mb-6">
<div class="grid gap-6">
<CardBox is-form>
<PillTag
label="Update balance"
color="success"
class="mb-4"
:icon="PhPlusCircle"
/>
</div>
</div>
</CardBox>
<FormField
label="Card ID"
help="The ACCESS CODE provided by the game."
>
<FormControl
v-model="newBalance.cardId"
:icon="PhCreditCard"
name="cardId"
type="card"
placeholder="XXXX-XXXX-XXXX-XXXX"
:minlength="19"
:maxlength="19"
required
@input="cardInput"
/>
</FormField>
<FormField label="Credit Amount">
<FormControl
v-model="newBalance.credit"
:icon="PhCurrencyJpy"
name="credit"
required
:maxlength="7"
inputmode="numeric"
@input="numInput"
/>
</FormField>
<BaseButton
label="Credit Account"
color="success"
@click="creditCard"
/>
</CardBox>
<CardBox is-form>
<PillTag
label="Credit existing account"
color="info"
class="mb-4"
:icon="PhPlusCircle"
/>
<FormField label="Select Player">
<FormControl
v-model="newBalance.userId"
:options="existingBalances"
required
/>
</FormField>
<FormField label="Credit Amount">
<FormControl
v-model="newBalance.credit"
:icon="PhCurrencyJpy"
name="credit"
required
:maxlength="7"
inputmode="numeric"
@input="numInput"
/>
</FormField>
<BaseButton
label="Credit Selected Player"
color="info"
@click="creditCard"
/>
</CardBox>
</div>
<div>
<CardBox has-table>
<div
class="bg-white dark:bg-slate-900/95 rounded-2xl lg:flex lg:justify-between"
>
<div class="w-full">
<GeneralTable
:headers="balanceHeaders"
:items="paseliData.balances"
/>
</div>
</div>
</CardBox>
</div>
</div>
<SectionTitleLine
:icon="PhCashRegister"
title="PASELI Transaction History"
main
class="pt-6"
/>
<CardBox has-table>
<div