finish 3.1.0-RC

This commit is contained in:
Trenton Zimmer
2026-01-12 15:08:05 -05:00
parent 03f34bf0c3
commit 1347fa5300
19 changed files with 520 additions and 16 deletions

View File

@@ -1,4 +1,4 @@
VITE_APP_VERSION="3.0.45"
VITE_APP_VERSION="3.1.0-RC"
VITE_API_URL="http://localhost: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.45"
VITE_APP_VERSION="3.1.0-RC"
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

@@ -1,6 +1,6 @@
{
"name": "phaseweb3",
"version": "3.0.45",
"version": "3.1.0-RC",
"scripts": {
"dev": "vite --host",
"build": "vite build",

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -44,5 +44,6 @@
"3.0.42-oMini": ["- (Major) Refactor loading state", "- (Minor) Add two new customizations, filled/outlined PillTags and Buttons"],
"3.0.43": ["- (Major) Add pop'n music Jam&Fizz"],
"3.0.44": ["- (Bugfix) Fix broken race state on game page", "- (Major) Add customization options for BaseIcon fill type", "- (Major) Add pop'n music weekly course song"],
"3.0.45": ["- (Bugfix) Fix Timeline not loading", "- (Major) Add a filter to the data in the timeline for private profiles", "- (Game) Add SDVX 6th chart", "- (Bugfix) Fix floats for jubeat difficulties"]
"3.0.45": ["- (Bugfix) Fix Timeline not loading", "- (Major) Add a filter to the data in the timeline for private profiles", "- (Game) Add SDVX 6th chart", "- (Bugfix) Fix floats for jubeat difficulties"],
"3.1.0-RC": ["- (Feature) Add onboarding page", "- (Minor) Add BaseImage component", "- (Minor) Add BaseAccordion component, - (Admin) Finish admin event log page"]
}

View File

@@ -0,0 +1,50 @@
[
{
"label": "Dashboard",
"body": [
"The dashboard is the main landing page of PhaseII.",
"You can view news, quick game stats, and play trend data."
],
"image": "dashboard"
},
{
"label": "Game Showcase",
"body": [
"Cards for the games you've played.",
"Sorted by last play, click them and you'll go to the game!"
],
"image": "showcase"
},
{
"label": "Dropdown Menu",
"body": [
"Here you can find all profile settings and customizations.",
"This collapses on mobile."
],
"image": "dropdown"
},
{
"label": "Sidebar",
"body": [
"Here you can find all other pages in the WebUI.",
"This collapses on mobile."
],
"image": "sidebar"
},
{
"label": "Profile Customization",
"body": [
"Make PhaseII WebUI V3 your own with UI tweaks!",
"This will be expanded in the future."
],
"image": "customize"
},
{
"label": "Service Integrations",
"body": [
"Connect your PhaseII account to outside sources for data linking and special features!",
"Currently, we can link to Kamaitachi and Discord."
],
"image": "integrate"
}
]

View File

@@ -0,0 +1,53 @@
<script setup>
import { ref } from "vue";
import { PhCaretUp } from "@phosphor-icons/vue";
import BaseIcon from "@/components/BaseIcon.vue";
defineProps({
title: {
type: String,
required: true,
},
});
const open = ref(false);
const toggle = () => {
open.value = !open.value;
};
</script>
<template>
<div class="rounded-lg overflow-hidden">
<button
class="w-full flex items-center justify-between px-4 py-3 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 hover:dark:bg-slate-700 transition duration-25"
@click="toggle"
>
<span class="text-lg">
{{ title }}
</span>
<BaseIcon
:icon="PhCaretUp"
class="flex-none transition-transform duration-150 ease-in-out"
:class="[open ? 'rotate-0' : 'rotate-180']"
/>
</button>
<Transition
enter-active-class="transition-position duration-75"
enter-from-class="opacity-50 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition-position duration-75"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-50 -translate-y-2"
>
<div
v-show="open"
class="px-4 py-3 border-t text-left bg-slate-50 dark:bg-slate-700"
>
<slot />
</div>
</Transition>
</div>
</template>

View File

@@ -0,0 +1,34 @@
<script setup>
import AccordionItem from "@/components/AccordionItem.vue";
import BaseImage from "@/components/BaseImage.vue";
const ASSET_PATH = import.meta.env.VITE_ASSET_PATH;
defineProps({
items: {
type: Array,
required: true,
},
imagePath: {
type: String,
required: false,
default: "onboarding",
},
});
</script>
<template>
<div class="space-y-2">
<AccordionItem
v-for="(item, index) in items"
:key="index"
:title="item.label"
class="grid"
>
<p v-for="p of item.body">{{ p }}</p>
<BaseImage
:url="`${ASSET_PATH}/${imagePath}/${item.image}.webp`"
class="mt-2"
/>
</AccordionItem>
</div>
</template>

View File

@@ -0,0 +1,95 @@
<script setup>
import { ref } from "vue";
defineProps({
url: {
type: String,
required: true,
},
size: {
type: Number,
default: 200,
},
});
const cardRef = ref(null);
const shineRef = ref(null);
function handleMouseMove(event) {
const card = cardRef.value;
const shine = shineRef.value;
if (!card || !shine) return;
const rect = card.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const dx = (x - centerX) / centerX;
const dy = (y - centerY) / centerY;
const aspect = rect.width / rect.height;
const maxTilt = 10;
const rotateX = dy * -maxTilt * Math.min(1, aspect);
const rotateY = dx * maxTilt * Math.min(1, 1 / aspect);
card.style.transform = `
rotateX(${rotateX}deg)
rotateY(${rotateY}deg)
scale(1.05)
`;
shine.style.opacity = "1";
shine.style.background = `
radial-gradient(
circle at ${x}px ${y}px,
rgba(255,255,255,0.6),
rgba(255,255,255,0.2),
transparent 65%
)
`;
}
function resetTransform() {
if (cardRef.value) {
cardRef.value.style.transform = "rotateX(0deg) rotateY(0deg) scale(1)";
}
if (shineRef.value) {
shineRef.value.style.opacity = "0";
}
}
</script>
<template>
<div
class="relative inline-block [perspective:900px]"
@mousemove="handleMouseMove"
@mouseleave="resetTransform"
>
<a :href="url" target="_blank" rel="noopener noreferrer">
<div
ref="cardRef"
class="relative will-change-transform transition-transform duration-200 ease-out [transform-style:preserve-3d]"
>
<div
ref="shineRef"
class="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-200 mix-blend-overlay rounded-lg"
/>
<img
:src="url"
class="block rounded-lg shadow-xl"
:style="{
width: `${size}px`,
height: 'auto',
}"
draggable="false"
/>
</div>
</a>
</div>
</template>

View File

@@ -4,6 +4,7 @@ import { ref, onMounted } from "vue";
import CardBox from "@/components/CardBox.vue";
import OverlayLayer from "@/components/OverlayLayer.vue";
import BaseButton from "@/components/BaseButton.vue";
import BaseButtons from "@/components/BaseButtons.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import { PhSparkle } from "@phosphor-icons/vue";
import { APIUserAppUpdate, APIUserCustomize } from "@/stores/api/account";
@@ -111,7 +112,7 @@ async function updateUserData(disable = false) {
</div>
<h2 class="font-mono text-pink-500">Version: {{ appVersion }}</h2>
<div class="flex gap-2">
<BaseButtons>
<BaseButton
label="Close"
color="success"
@@ -122,7 +123,7 @@ async function updateUserData(disable = false) {
color="danger"
@click="updateUserData(true)"
/>
</div>
</BaseButtons>
</div>
</CardBox>
</OverlayLayer>

View File

@@ -0,0 +1,147 @@
<script setup>
import axios from "axios";
import { ref, onMounted } from "vue";
import CardBox from "@/components/CardBox.vue";
import OverlayLayer from "@/components/OverlayLayer.vue";
import BaseButton from "@/components/BaseButton.vue";
import BaseButtons from "@/components/BaseButtons.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import BaseAccordion from "@/components/BaseAccordion.vue";
import { PhHandWaving } from "@phosphor-icons/vue";
import { APIUserOnboard } from "@/stores/api/account";
import { useMainStore } from "@/stores/main.js";
const mainStore = useMainStore();
const onboardingData = ref(null);
const appVersion = import.meta.env.VITE_APP_VERSION;
const activeState = ref(true);
async function loadOnboarding() {
try {
const response = await axios.get(`/data-sources/onboarding.json`);
if (response.data) {
onboardingData.value = response.data;
}
} catch (error) {
console.error("Error loading onboardingData:", error.message);
}
}
onMounted(async () => {
await loadOnboarding();
});
function isActive() {
if (mainStore.userData.onboardingComplete ?? false) {
activeState.value = false;
}
return activeState.value;
}
async function updateUserData() {
const mainStore = useMainStore();
var data = null;
try {
data = await APIUserOnboard(appVersion);
} catch (error) {
console.error("Failed to update user:", error);
}
if (data?.status === "success") {
activeState.value = false;
mainStore.userLoaded = false;
await mainStore.loadUser();
}
}
</script>
<template>
<template v-if="isActive()">
<OverlayLayer v-if="activeState" :transparent="true">
<CardBox
class="shadow-lg max-h-modal w-11/12 md:w-3/5 lg:w-2/5 xl:w-4/12 z-50 text-white/90 max-h-screen overflow-scroll"
>
<div class="grid text-center justify-center grid-cols-1 gap-3">
<div class="place-self-center">
<BaseIcon
:icon="PhHandWaving"
class="text-emerald-600 waving-hand"
w="w-20"
:size="45"
/>
<h1 class="text-xl xl:text-2xl font-bold">
Welcome to PhaseII WebUI V3!
</h1>
<h1 class="text-lg xl:text-xl">
This is the next generation web interface. Let's get started!
</h1>
</div>
<div v-if="mainStore.userData.webVersions" class="space-y-2">
<hr class="border-t border my-4 w-full" />
<h2 class="text-lg xl:text-xl text-left">
Thanks for participating in the beta! People like you make this
software better.
</h2>
</div>
<hr class="border-t border mt-3 w-full" />
<div v-if="onboardingData">
<BaseAccordion :items="onboardingData" />
</div>
<BaseButtons>
<BaseButton
label="Close"
color="success"
@click="updateUserData()"
/>
</BaseButtons>
</div>
</CardBox>
</OverlayLayer>
</template>
<template v-else>
<slot />
</template>
</template>
<style scoped>
.waving-hand {
animation-name: wave-animation;
animation-duration: 2.5s;
animation-iteration-count: infinite;
transform-origin: 70% 70%;
}
@keyframes wave-animation {
0% {
transform: rotate(0deg);
}
15% {
transform: rotate(14deg);
} /* The following five values can be played with to make the waving more or less extreme */
30% {
transform: rotate(-8deg);
}
40% {
transform: rotate(14deg);
}
50% {
transform: rotate(-4deg);
}
60% {
transform: rotate(10deg);
}
70% {
transform: rotate(0deg);
} /* Reset for the last half to pause */
100% {
transform: rotate(0deg);
}
}
</style>

View File

@@ -19,6 +19,7 @@ import { useStyleStore } from "@/stores/style.js";
import LoadingModal from "@/components/Modal/LoadingModal.vue";
// import EmailModal from "@/components/Modal/EmailModal.vue";
import UpdateModal from "@/components/Modal/UpdateModal.vue";
import WelcomeModal from "@/components/Modal/WelcomeModal.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import NavBar from "@/components/NavBar.vue";
import NavBarItemPlain from "@/components/NavBarItemPlain.vue";
@@ -235,10 +236,6 @@ const menuAside = computed(() => {
'overflow-hidden lg:overflow-visible': isAsideMobileExpanded,
}"
>
<template v-if="userLoaded">
<UpdateModal class="transition-opacity duration-300 ease-out" />
<!-- <EmailModal class="transition-opacity duration-300 ease-out" /> -->
</template>
<LoadingModal
:active="loading || saving"
:is-save="saving"
@@ -249,6 +246,12 @@ const menuAside = computed(() => {
'opacity-0': !loading && !saving,
}"
/>
<template v-if="userLoaded">
<WelcomeModal class="transition-opacity duration-300 ease-out">
<UpdateModal class="transition-opacity duration-300 ease-out" />
</WelcomeModal>
<!-- <EmailModal class="transition-opacity duration-300 ease-out" /> -->
</template>
<div
v-if="userLoaded"
:class="[layoutAsidePadding, { 'ml-60 lg:ml-0': isAsideMobileExpanded }]"

View File

@@ -266,6 +266,20 @@ export async function APIUserAppUpdate(version = null, disable = false) {
}
}
export async function APIUserOnboard(version) {
const mainStore = useMainStore();
try {
const data = await mainStore.callApi(`/user/onboard`, "POST", {
version: version,
});
return data;
} catch (error) {
console.log(`Error saving user!`, error);
throw error;
}
}
export async function APIUserReadNews(newsId) {
const mainStore = useMainStore();

View File

@@ -1,6 +1,13 @@
<script setup>
import { ref, onMounted } from "vue";
import { PhFlagCheckered, PhCashRegister, PhSiren } from "@phosphor-icons/vue";
import {
PhFlagCheckered,
PhCashRegister,
PhSiren,
PhGlobeX,
PhExclamationMark,
PhCloudArrowDown,
} from "@phosphor-icons/vue";
import SectionMain from "@/components/SectionMain.vue";
import CardBox from "@/components/CardBox.vue";
import LayoutAuthenticated from "@/layouts/LayoutAuthenticated.vue";
@@ -34,7 +41,7 @@ const eventHeaders = [
width: 120,
},
{
text: "Game",
text: "Model",
value: "data.model",
width: 120,
},
@@ -155,20 +162,56 @@ const unhandledHeaders = [
value: "date",
width: 120,
},
{
text: "PCBID",
value: "data.pcbid",
width: 120,
},
{
text: "Model",
value: "data.model",
width: 120,
},
{
text: "Request",
value: "data.request",
value: "data.requestName",
width: 120,
},
{
text: "Method",
value: "data.method",
width: 120,
},
];
const unauthorizedHeaders = [
{
text: "Timestamp",
value: "date",
width: 120,
},
{
text: "PCBID",
value: "data.pcbid",
width: 120,
},
{
text: "Model",
value: "data.model",
width: 120,
},
];
function formatData(events) {
if (events === undefined) {
return [];
}
return events.map((item) => {
if (item.timestamp) {
item.date = formatSortableDate(item.timestamp);
}
if (item?.data?.service === "xrpc" && item?.data?.request) {
if (item?.data?.request) {
const extracted = extractExceptionData(item.data.request);
item.data.pcbid = extracted.pcbid;
item.data.model = extracted.model;
@@ -217,6 +260,47 @@ function extractExceptionData(xmlString) {
<GeneralTable
:headers="exceptionHeaders"
:items="formatData(auditData?.exception)"
:rows-per-page="2"
/>
</div>
</div>
</CardBox>
<SectionTitleLine
:icon="PhGlobeX"
title="Recent Unhandled Packets"
color="text-amber-300"
main
/>
<CardBox has-table class="mb-4">
<div
class="bg-white dark:bg-slate-900/95 rounded-2xl lg:flex lg:justify-between"
>
<div class="w-full">
<GeneralTable
:headers="unhandledHeaders"
:items="formatData(auditData?.unhandled_packet)"
:rows-per-page="10"
/>
</div>
</div>
</CardBox>
<SectionTitleLine
:icon="PhExclamationMark"
title="Recent Unauthorized PCBIDs"
color="text-yellow-400"
main
/>
<CardBox has-table class="mb-4">
<div
class="bg-white dark:bg-slate-900/95 rounded-2xl lg:flex lg:justify-between"
>
<div class="w-full">
<GeneralTable
:headers="unauthorizedHeaders"
:items="formatData(auditData?.unauthorized_pcbid)"
:rows-per-page="10"
/>
</div>
</div>
@@ -225,7 +309,7 @@ function extractExceptionData(xmlString) {
<SectionTitleLine
:icon="PhFlagCheckered"
title="Recent PCB Events"
color="text-amber-400"
color="text-emerald-400"
main
/>
<CardBox has-table class="mb-4">
@@ -236,6 +320,27 @@ function extractExceptionData(xmlString) {
<GeneralTable
:headers="eventHeaders"
:items="formatData(auditData?.pcbevent)"
:rows-per-page="10"
/>
</div>
</div>
</CardBox>
<SectionTitleLine
:icon="PhCloudArrowDown"
title="Recent Updater Statuses"
color="text-sky-400"
main
/>
<CardBox has-table class="mb-4">
<div
class="bg-white dark:bg-slate-900/95 rounded-2xl lg:flex lg:justify-between"
>
<div class="w-full">
<GeneralTable
:headers="updaterHeaders"
:items="formatData(auditData?.avs_updater)"
:rows-per-page="10"
/>
</div>
</div>
@@ -244,7 +349,7 @@ function extractExceptionData(xmlString) {
<SectionTitleLine
:icon="PhCashRegister"
title="Recent PASELI Transactions"
color="text-emerald-400"
color="text-green-400"
main
/>
<CardBox has-table>
@@ -255,6 +360,7 @@ function extractExceptionData(xmlString) {
<GeneralTable
:headers="transactionHeaders"
:items="formatData(auditData?.paseli_transaction)"
:rows-per-page="10"
/>
</div>
</div>