Add in various stuff i've been working on in the background, i want to get my git changes slimmed down

This commit is contained in:
Trenton Zimmer
2026-04-14 00:14:45 -04:00
parent 93c9732094
commit 0cfb6109ef
19 changed files with 224 additions and 539 deletions

View File

@@ -1,9 +1,11 @@
VITE_APP_VERSION="3.1.2"
VITE_APP_VERSION="3.1.3"
VITE_API_URL="http://localhost:8000/"
VITE_API_KEY="your_api_key_should_be_here"
VITE_ASSET_PATH="/assets"
VITE_CDN_PATH="https://cdn.phaseii.network/file/PhaseII"
VITE_GAME_ASSET_PATH="https://cdn.phaseii.network/file/PhaseII/game-assets"
VITE_DISCORD_OAUTH_URL="https://discord.com/oauth2/authorize?client_id=947985989421395988&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A5173%2F%23%2Fprofile%2Fintegrate%2Fdiscord&scope=identify"
VITE_TACHI_OAUTH_URL="https://kamai.tachi.ac/oauth/request-auth?clientID=CIb72d1ff81fcfdc001ead8bde3a0ae1a7e0663a37"
VITE_DISCORD_URL="https://discord.gg/mNmjpX33cd"
VITE_DOCS_URL="https://docs.phaseii.network"
VITE_DOCS_URL="https://docs.phaseii.network"
VITE_UNITY_URL="http://localhost:5174"

View File

@@ -1,9 +1,11 @@
VITE_APP_VERSION="3.1.2"
VITE_APP_VERSION="3.1.3"
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"
VITE_CDN_PATH="https://cdn.phaseii.network/file/PhaseII"
VITE_GAME_ASSET_PATH="https://cdn.phaseii.network/file/PhaseII/game-assets"
VITE_DISCORD_OAUTH_URL="https://discord.com/oauth2/authorize?client_id=947985989421395988&response_type=code&redirect_uri=https%3A%2F%2Fweb3.phaseii.network%2Fprofile%2Fintegrate%2Fdiscord&scope=identify"
VITE_TACHI_OAUTH_URL="https://kamai.tachi.ac/oauth/request-auth?clientID=CIce4260e1939ceed11c8e48ee857a3aef2a87ba56"
VITE_DISCORD_URL="https://discord.gg/mNmjpX33cd"
VITE_DOCS_URL="https://docs.phaseii.network"
VITE_DOCS_URL="https://docs.phaseii.network"
VITE_UNITY_URL="https://unity.phaseii.network"

View File

@@ -1,6 +1,6 @@
{
"name": "phaseweb3",
"version": "3.1.2",
"version": "3.1.3",
"scripts": {
"dev": "vite --host",
"build": "vite build",

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View File

@@ -48,5 +48,6 @@
"3.1.0-RC": ["- (Feature) Add onboarding page", "- (Minor) Add BaseImage component", "- (Minor) Add BaseAccordion component, - (Admin) Finish admin event log page"],
"3.1.0": ["- (Major) WebUI V3 full release"],
"3.1.1": ["- (Admin) Add more admin stats for users", "- (Minor) Add new greetings"],
"3.1.2": ["- (Major) Add initial code for oAuth pages", "- (Major) Add redirect to target page after login", "- (Minor) Add quick links to navbar", "- (Minor) Add new greetings", "- (Minor) Replace login page background", "- (Minor) Add animation to loading modal", "- (Bugfix) Refactor loading modal", "- (Bugfix) Refactor overlay layer", "- (Bugfix) Optimize main layouts", "- (Bugfix) Fix loading states around site", "- (Bugfix) Optimize news read api calls", "- (Minor) Add simple auth checkers to router, blocking access at webui level"]
"3.1.2": ["- (Major) Add initial code for oAuth pages", "- (Major) Add redirect to target page after login", "- (Minor) Add quick links to navbar", "- (Minor) Add new greetings", "- (Minor) Replace login page background", "- (Minor) Add animation to loading modal", "- (Bugfix) Refactor loading modal", "- (Bugfix) Refactor overlay layer", "- (Bugfix) Optimize main layouts", "- (Bugfix) Fix loading states around site", "- (Bugfix) Optimize news read api calls", "- (Minor) Add simple auth checkers to router, blocking access at webui level"],
"3.1.3": ["- (Major) Add better error popups, add new buttons on errors", "- (Major) Finish initial authorization page for internal apps", "- (Minor) Add more greetings", "- (Bugfix) Fix overlay layer transparency when saving", "- (Bugfix) Fix remaining missing colors"]
}

View File

@@ -24,7 +24,19 @@ watch(isActive, (val) => {
if (!isActive.value) {
showModal.value = false;
}
}, 250);
}, 150);
}
});
watch(errorCode, (val) => {
if (val) {
showModal.value = true;
} else {
setTimeout(() => {
if (!errorCode.value) {
showModal.value = false;
}
}, 150);
}
});
</script>

View File

@@ -13,6 +13,7 @@ export const colorsBgLight = {
info: "bg-blue-700 border-blue-500 text-white",
sakura: "bg-pink-700 border-pink-500 text-white",
gold: "bg-amber-700 border-amber-500 text-white",
slight_danger: "bg-red-600 border-red-500",
};
export const colorsText = {

View File

@@ -3,6 +3,7 @@ import { useRouter } from "vue-router";
import CardBox from "@/components/CardBox.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import BaseButton from "@/components/BaseButton.vue";
import BaseButtons from "@/components/BaseButtons.vue";
import { useMainStore } from "@/stores/main";
import { PhCloudCheck, PhCloudX, PhShrimp } from "@phosphor-icons/vue";
@@ -32,6 +33,18 @@ function hotReload() {
$router.go();
return null;
}
function goBack() {
$router.go(-1);
return null;
}
function closeModal() {
mainStore.errorCode = null;
mainStore.decrementLoading();
mainStore.decrementSaving();
return null;
}
</script>
<template>
@@ -107,15 +120,29 @@ function hotReload() {
<h1 class="text-lg md:text-xl">
Error: <span class="text-red-500">{{ errorCode }}</span>
</h1>
<div>
<BaseButton
type="submit"
color="info"
label="Reload"
:small="false"
@click="hotReload()"
/>
</div>
<BaseButtons>
<div class="flex justify-center w-full">
<BaseButton
color="warning"
label="Go Back"
:small="false"
@click="goBack()"
/>
<BaseButton
type="submit"
color="success"
label="Reload"
:small="false"
@click="hotReload()"
/>
<BaseButton
color="info"
label="Close"
:small="false"
@click="closeModal()"
/>
</div>
</BaseButtons>
</div>
</CardBox>
</template>

View File

@@ -1,7 +1,8 @@
<script setup>
import { computed } from "vue";
import { useStyleStore } from "@/stores/style.js";
defineProps({
const props = defineProps({
zIndex: {
type: String,
default: "z-50",
@@ -27,14 +28,20 @@ const overlayClick = (event) => {
};
const styleStore = useStyleStore();
const transparentClass = computed(() => {
return props.transparent
? "dark:from-gray-700/90 dark:via-gray-900/90 dark:to-gray-700/90"
: "dark:from-gray-700 dark:via-gray-900 dark:to-gray-700";
});
</script>
<template>
<Transition name="overlay-fade">
<div
v-show="active"
:class="[type, zIndex]"
class="fixed inset-0 flex flex-col items-center justify-center bg-linear-to-tr dark:from-gray-700 dark:via-gray-900 dark:to-gray-700 transform"
:class="[type, zIndex, transparentClass]"
class="fixed inset-0 flex flex-col items-center justify-center bg-linear-to-tr transform"
>
<div
class="absolute inset-0"

View File

@@ -12,6 +12,7 @@ export class ProfileCustomizations {
{ id: "loveplus", label: "LovePlus" },
{ id: "paint", label: "i eat paint" },
{ id: "shrimp", label: "Shrimp" },
{ id: "unity", label: "Unity" },
];
static get cardList() {

View File

@@ -0,0 +1,9 @@
export const InternalApps = {
unity: {
id: "unity",
name: "Unity",
image: "https://cdn.phaseii.network/file/PhaseII/web-assets/unity.png",
about:
"Unity is PhaseII's developer platform. It allows you to create and manage applications that can connect to PhaseII accounts and data.",
},
};

View File

@@ -655,5 +655,25 @@
"author": "superaustin7",
"header": "Welcome to jubeat, <username>",
"comment": "THAT WON'T MAKE IT IN TIME FOR THIS SCHEDULE."
},
{
"author": "Anonymous",
"header": "Explore Every Avenue, <username>.",
"comment": ""
},
{
"author": "Anonymous",
"header": "Welcome to PhaseII Web Solutions, <username>.",
"comment": "Now offering a 99% SLA. Excludes pickaxe related incidents."
},
{
"author": "yonokid",
"header": "Something might happen, <username>",
"comment": "If you would please consult the graph."
},
{
"author": "superaustin7",
"header": "THE <username> HAS AWAKENED",
"comment": "BE VERY SCARED"
}
]

View File

@@ -30,6 +30,7 @@ import { gameData } from "@/constants";
import BaseButton from "@/components/BaseButton.vue";
const DISCORD_URL = import.meta.env.VITE_DISCORD_URL;
const DOCS_URL = import.meta.env.VITE_DOCS_URL;
const UNITY_URL = import.meta.env.VITE_UNITY_URL;
const router = useRouter();
const route = useRoute();
@@ -174,7 +175,8 @@ const menuAside = computed(() => {
sideMenu.push({
label: "Developer Portal",
icon: PhCode,
to: "/developer",
href: UNITY_URL,
target: "blank",
});
}

View File

@@ -452,26 +452,6 @@ const routes = [
hotReload: true, // disables Hot Reload
},
},
{
meta: {
requiresAuth: true,
requiresDev: true,
title: "Developer Portal",
},
path: "/developer",
name: "Developer Portal",
component: () => import("@/views/Developer/PortalView.vue"),
},
{
meta: {
requiresAuth: true,
requiresDev: true,
title: "App Registration",
},
path: "/developer/register",
name: "App Registration",
component: () => import("@/views/Developer/RegisterView.vue"),
},
];
const router = createRouter({

View File

@@ -321,3 +321,27 @@ export async function APIDeleteUserSessions() {
throw error;
}
}
export async function APIGetAuthClient(clientId) {
const mainStore = useMainStore();
try {
const data = await mainStore.callApi(`/oauth/client/${clientId}`);
return data?.data;
} catch (error) {
console.log("Error fetching client:", error);
throw error;
}
}
export async function APIPostAuthClient(clientId) {
const mainStore = useMainStore();
try {
const data = await mainStore.callApi(`/oauth/client/${clientId}`, "POST");
return data?.data;
} catch (error) {
console.log("Error posting client:", error);
throw error;
}
}

View File

@@ -131,6 +131,8 @@ export const useMainStore = defineStore("main", {
this.incrementSaving();
}
this.errorCode = null;
const baseHeaders = {
"App-Auth-Key": apiKey,
};
@@ -158,11 +160,12 @@ export const useMainStore = defineStore("main", {
this.errorCode = error.message || "Network Error";
throw error;
} finally {
// Always decrement in finally block
if (method === "GET") {
this.decrementLoading();
} else if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
this.decrementSaving();
if (this.errorCode === null) {
if (method === "GET") {
this.decrementLoading();
} else if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
this.decrementSaving();
}
}
}
},

View File

@@ -1,34 +1,56 @@
<script setup>
import { useRouter } from "vue-router";
import { reactive } from "vue";
import { useMainStore } from "@/stores/main.js";
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import CardBox from "@/components/CardBox.vue";
import BaseButton from "@/components/BaseButton.vue";
import LayoutGuest from "@/layouts/LayoutGuest.vue";
import { APIGetAuthClient, APIPostAuthClient } from "@/stores/api/account.js";
import { InternalApps } from "@/constants/developer/apps.js";
const router = useRouter();
const mainStore = useMainStore();
const route = useRoute();
const clientId = route.query.client_id;
const clientData = ref(null);
const form = reactive({
spinin: false,
onMounted(async () => {
if (clientId) {
await getClient(clientId);
} else {
console.error("No client_id provided in query parameters.");
}
});
const submit = async () => {
const response = await mainStore.createUserSession();
if (response) {
router.push("/");
async function getClient(id) {
try {
const data = await APIGetAuthClient(id);
if (data.internal && InternalApps[id]) {
clientData.value = InternalApps[id];
clientData.value.internal = true;
clientData.value.callbackUrl = data.callbackUrl;
}
} catch (error) {
console.error("Failed to fetch client data:", error);
}
}
const submit = async () => {
const response = await APIPostAuthClient(clientId);
if (!response.code) {
console.error("Authorization failed");
}
window.location.href =
clientData.value.callbackUrl + "?code=" + response.code;
};
const appInfo = {
name: "Edi",
image: "/edi512x512_2.png",
manager: "Lumen",
about:
"Edi is a DDR score tracking app that offers a direct connection to your PhaseII account.",
intents: ["user", "account", "webhook_scores"],
internal: false,
};
// const appInfo = {
// name: "Edi",
// image: "/edi512x512_2.png",
// manager: "Lumen",
// about:
// "Edi is a DDR score tracking app that offers a direct connection to your PhaseII account.",
// intents: ["user", "account", "webhook_scores"],
// internal: false,
// };
</script>
<template>
@@ -36,7 +58,6 @@ const appInfo = {
<div class="flex md:min-h-screen md:items-center md:justify-center">
<CardBox
class="w-full md:w-auto rounded-none md:rounded-xl md:drop-shadow-xl"
:class="form.spinin ? 'animate-spin' : 'animate-none'"
has-table
is-auth
>
@@ -47,10 +68,7 @@ const appInfo = {
<div class="flex flex-col items-center text-wrap">
<img src="/favicon.png" class="rounded-full shadow-lg mb-2" />
<h1 class="text-xl"><samp>PhaseII</samp></h1>
<button
class="text-sm text-gray-700 dark:text-white/75 hover:cursor-pointer"
@click="form.spinin = !form.spinin"
>
<button class="text-sm text-gray-700 dark:text-white/75">
Spinnin' since 2021
</button>
</div>
@@ -60,50 +78,67 @@ const appInfo = {
<div class="md:border-r" />
<form @submit.prevent="submit()">
<div class="flex flex-col items-center text-wrap">
<h1 class="text-lg md:text-xl mb-2">
Integrate with <span class="font-bold">{{ appInfo.name }}</span>
</h1>
<template v-if="!clientData?.internal">
<h1 class="text-lg md:text-xl mb-2">
Integrate with
<span class="font-bold">{{ clientData?.name }}</span>
</h1>
</template>
<template v-else>
<h1 class="text-lg md:text-xl mb-2">
Log in to
<span class="font-bold">{{ clientData?.name }}</span>
</h1>
</template>
<img
:src="appInfo.image"
:src="clientData?.image"
width="75"
class="rounded-full shadow-lg mb-2"
/>
<p class="text-md max-w-md wrap-break-word text-center">
{{ appInfo.about }}
{{ clientData?.about }}
</p>
<hr class="border-r my-2 w-full" />
<h2 class="text-md md:text-lg">
<span class="font-bold">{{ appInfo.name }}</span> will have
access to:
</h2>
<ul class="text-sm md:text-md text-center">
<li v-for="intent of appInfo.intents" :v-key="intent">
- {{ intent }}
</li>
</ul>
<template v-if="!clientData?.internal">
<h2 class="text-md md:text-lg">
<span class="font-bold">{{ clientData?.name }}</span> will
have access to:
</h2>
<ul class="text-sm md:text-md text-center">
<li v-for="intent of clientData?.intents" :v-key="intent">
- {{ intent }}
</li>
</ul>
</template>
</div>
<div class="flex flex-col gap-2 mt-4">
<div class="flex flex-col gap-2 mt-2">
<BaseButton label="Authorize" color="success" type="submit" />
</div>
<hr class="border-t my-4 w-full" />
<div class="flex flex-col items-center">
<p
v-if="!appInfo.internal"
class="text-sm max-w-md wrap-break-word text-center mb-2"
>
Please note that
<span class="font-bold">{{ appInfo.name }}</span> is not a
<samp>PhaseII</samp> provided service, and that any issues
should be directed to the appropriate channels.
</p>
<p class="text-sm max-w-md wrap-break-word text-center">
<span class="font-bold">{{ appInfo.name }}</span> is managed by
<span class="font-bold">{{ appInfo.manager }}</span>
</p>
<template v-if="!clientData?.internal">
<p class="text-sm max-w-md wrap-break-word text-center mb-2">
Please note that
<span class="font-bold">{{ clientData?.name }}</span> is not a
<samp>PhaseII</samp> provided service, and that any issues
should be directed to the appropriate channels.
</p>
<p class="text-sm max-w-md wrap-break-word text-center">
<span class="font-bold">{{ clientData?.name }}</span> is
managed by
<span class="font-bold">{{ clientData?.manager }}</span>
</p>
</template>
<template v-else>
<p class="text-sm max-w-md wrap-break-word text-center">
This is an internal application provided by
<samp>PhaseII</samp>
</p>
</template>
</div>
<hr class="border-t my-4 w-full" />

View File

@@ -1,97 +0,0 @@
<script setup>
import { ref, onMounted, reactive } from "vue";
import { PhCloud, PhCode } from "@phosphor-icons/vue";
import { dashCode } from "@/constants/userData.js";
import SectionMain from "@/components/SectionMain.vue";
import BaseButton from "@/components/BaseButton.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import UserCard from "@/components/UserCard.vue";
import CardBox from "@/components/CardBox.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";
const DOCS_URL = import.meta.env.VITE_DOCS_URL;
const copyToClipboard = (text) => {
navigator.clipboard
.writeText(text)
.then(() => {
alert("Copied to clipboard!");
})
.catch(() => {
alert("Failed to copy to clipboard!");
});
};
const appInfo = {
name: "Edi",
image: "/edi512x512_2.png",
manager: "Lumen",
about:
"Edi is a DDR score tracking app that offers a direct connection to your PhaseII account.",
intents: ["user", "account", "webhook_scores"],
webhooks: ["scores"],
useOAuth: true,
internal: false,
};
</script>
<template>
<LayoutAuthenticated>
<SectionMain>
<UserCard class="mb-6" use-small even-smaller />
<SectionTitleLine :icon="PhCode" title="Developer Portal" main />
<CardBox class="row-span-2 mb-6">
<h1 class="text-xl md:text-2xl">
Welcome to the <samp>PhaseII</samp> Developer Portal!
</h1>
<h2 class="text-lg">You're gonna do great 😄</h2>
<p>
If you're just now getting started, please read the
<a
class="text-blue-400 hover:text-blue-600"
:href="`${DOCS_URL}/developer`"
target="_blank"
>
developer docs
</a>
</p>
<hr class="border-r my-2 w-full" />
<p>Create or manage your applications below</p>
</CardBox>
<SectionTitleLine :icon="PhCloud" title="My Applications" main>
<BaseButton
label="Add Application"
color="success"
to="/developer/register"
/>
</SectionTitleLine>
<CardBox class="row-span-2 mb-6">
<div class="flex">
<div
class="flex flex-col justify-center p-4 rounded-xl bg-slate-700 hover:drop-shadow-xl transition-all"
>
<div class="w-full flex justify-center">
<img
:src="appInfo.image"
width="75"
class="rounded-full shadow-lg mb-2"
/>
</div>
<div class="mb-2 text-center">
<h1 class="text-lg md:text-xl font-bold">
{{ appInfo.name }}
</h1>
<h2 v-if="appInfo.useOAuth">OAuth Enabled</h2>
<h2>{{ appInfo.webhooks?.length || 0 }} Webhook(s)</h2>
</div>
</div>
</div>
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>

View File

@@ -1,344 +0,0 @@
<script setup>
import { reactive } from "vue";
import {
PhCloudArrowUp,
PhFileImage,
PhKey,
PhLinkSimple,
PhStorefront,
PhTextbox,
PhWebhooksLogo,
} from "@phosphor-icons/vue";
import SectionMain from "@/components/SectionMain.vue";
import UserCard from "@/components/UserCard.vue";
import PillTag from "@/components/PillTag.vue";
import CardBox from "@/components/CardBox.vue";
import FormField from "@/components/FormField.vue";
import FormCheckRadio from "@/components/FormCheckRadio.vue";
import FormFilePicker from "@/components/FormFilePicker.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 { useMainStore } from "@/stores/main";
import { applicationIntents, applicationWebhooks } from "@/constants/developer";
const mainStore = useMainStore();
const appForm = reactive({
name: "",
about: "",
image: null,
useOAuth: false,
callbackUri: null,
intents: [],
useWebhooks: false,
webhookList: [],
});
const initWebhook = {
type: "network.news",
name: "",
endpoint: "",
authKey: "",
fields: {},
};
const newWebhook = reactive({
...initWebhook,
});
const sortedArcades = mainStore.userArcades.map((arcade) => ({
label: arcade.name,
id: arcade.id,
}));
function isValidUrl(value, { requireHttps = true } = {}) {
if (!value || typeof value !== "string") return false;
try {
const url = new URL(value);
if (!["http:", "https:"].includes(url.protocol)) return false;
if (requireHttps && url.protocol !== "https:") return false;
return true;
} catch {
return false;
}
}
function isValidAuthKey(key) {
return typeof key === "string" && key.length >= 40;
}
async function addWebhook() {
const tmpWebhook = JSON.parse(
JSON.stringify({
type: newWebhook.type,
name: newWebhook.name,
endpoint: newWebhook.endpoint,
authKey: newWebhook.authKey,
fields: newWebhook.fields,
}),
);
if (!tmpWebhook.name?.trim()) {
return alert("Webhook name is required.");
}
if (!isValidUrl(tmpWebhook.endpoint)) {
return alert("Webhook endpoint must be a valid HTTPS URL.");
}
if (!isValidAuthKey(tmpWebhook.authKey)) {
return alert("Auth key must be at least 40 characters.");
}
const webhookMeta = applicationWebhooks.find((w) => w.id === tmpWebhook.type);
if (webhookMeta?.fields?.includes("arcadeId")) {
if (!tmpWebhook.fields.arcadeId) {
return alert("Arcade selection is required.");
}
} else {
tmpWebhook.fields = {};
}
appForm.webhookList.push(tmpWebhook);
Object.assign(newWebhook, initWebhook);
}
function removeWebhook(authKey) {
appForm.webhookList = appForm.webhookList.filter(
(webhook) => webhook.authKey !== authKey,
);
}
async function requestApp() {
}
</script>
<template>
<LayoutAuthenticated>
<SectionMain>
<UserCard class="mb-6" use-small even-smaller />
<SectionTitleLine
:icon="PhCloudArrowUp"
title="Create an Application"
main
/>
<CardBox class="row-span-2 mb-6">
<PillTag color="info" label="General" class="mb-2" />
<FormField label="Name">
<FormControl
v-model="appForm.name"
:icon="PhTextbox"
name="name"
required
/>
</FormField>
<FormField label="About">
<FormControl
v-model="appForm.about"
name="about"
type="textarea"
required
/>
</FormField>
<FormField label="App Logo">
<FormFilePicker
v-model="appForm.image"
label="Image Upload"
accept="image/*"
:icon="PhFileImage"
/>
</FormField>
</CardBox>
<CardBox class="row-span-2 mb-6">
<PillTag color="warning" label="OAuth2.0" class="mb-2" />
<FormField label="Enable OAuth">
<FormCheckRadio
v-model="appForm.useOAuth"
name="useOAuth"
:model-value="appForm.useOAuth"
:input-value="appForm.useOAuth"
type="switch"
/>
</FormField>
<template v-if="appForm.useOAuth">
<FormField
label="Callback URI"
help="The URL that the user will be redirected to from the authorization page (ex: input of https://mycoolapp.my.domain/callback/url will redirect user to https://address/callback/url?code= )"
>
<FormControl
v-model="appForm.callbackUri"
:icon="PhLinkSimple"
name="callbackUri"
placeholder="https://my.domain/callback/url"
required
/>
</FormField>
<FormField
label="Intents"
help="Select what you'd like your app to do"
>
<template v-for="intent in applicationIntents" :key="intent.id">
<div class="grid md:flex justify-between">
<FormCheckRadio
v-model="appForm.intents"
:name="'intents'"
:label="intent.label"
:input-value="intent.id"
type="checkbox"
/>
<span
class="text-slate-400 hover:text-slate-200 font-thin transition-all delay-0"
>
{{ intent.tip ?? "" }}
</span>
</div>
<hr class="border-r my-3 w-full" />
</template>
</FormField>
</template>
</CardBox>
<CardBox class="row-span-2 mb-6">
<PillTag color="success" label="Webhooks" class="mb-2" />
<FormField
label="Enable Application Webhooks"
help="These are different from user webhooks!"
>
<FormCheckRadio
v-model="appForm.useWebhooks"
name="useWebhooks"
:model-value="appForm.useWebhooks"
:input-value="appForm.useWebhooks"
type="switch"
/>
</FormField>
<template v-if="appForm.useWebhooks">
<div class="grid md:grid-cols-2 gap-6">
<form class="h-full" @submit.prevent="addWebhook()">
<PillTag color="info" label="New Webhook" class="mb-4" />
<FormField
label="Webhook Type"
:help="
applicationWebhooks.find(
(webhook) => webhook.id === newWebhook.type,
)?.tip
"
>
<FormControl
v-model="newWebhook.type"
name="type"
:icon="PhWebhooksLogo"
:options="applicationWebhooks"
required
/>
</FormField>
<FormField label="Name">
<FormControl
v-model="newWebhook.name"
name="name"
:icon="PhTextbox"
required
/>
</FormField>
<FormField
label="API Endpoint"
help="The URL that the Webhook server will contact"
>
<FormControl
v-model="newWebhook.endpoint"
name="name"
placeholder="https://my.domain/endpoint/url"
:icon="PhLinkSimple"
required
/>
</FormField>
<FormField
label="API Auth Key"
help="Sent as headers x-api-key, x-webhook-key, authorization"
>
<FormControl
v-model="newWebhook.authKey"
name="authKey"
:icon="PhKey"
required
:minlength="40"
/>
</FormField>
<template
v-for="field of applicationWebhooks.find(
(webhookData) => webhookData.id === newWebhook.type,
)?.fields"
>
<template v-if="field === 'arcadeId'">
<FormField
label="Arcade"
help="The arcade that this Webhook will track"
>
<FormControl
v-model="newWebhook.fields.arcadeId"
name="arcadeId"
:icon="PhStorefront"
:options="sortedArcades"
required
/>
</FormField>
</template>
</template>
<BaseButton color="success" type="submit" label="Add Webhook" />
</form>
<div>
<PillTag color="warning" label="Queue" class="mb-4" />
<div class="grid gap-4">
<CardBox
v-for="webhook of appForm.webhookList"
:key="webhook.type"
color-prop="bg-slate-800 dark:bg-slate-800"
>
<div class="flex justify-between items-center">
<div class="m-[-5px]">
<PillTag
color="info"
:label="webhook.type"
class="mb-4"
small
/>
<h1 class="text-md font-bold">{{ webhook.name }}</h1>
<samp
class="text-sm font-mono p-1 bg-slate-900 rounded-lg"
>
{{ webhook.endpoint }}
</samp>
<h2
v-for="(value, field) of webhook.fields"
class="text-sm font-mono mt-2"
>
{{ field }}: {{ value }}
</h2>
</div>
<BaseButton
color="danger"
label="Remove"
@click="removeWebhook(webhook.authKey)"
/>
</div>
</CardBox>
</div>
</div>
</div>
</template>
</CardBox>
<BaseButton
:small="false"
label="Submit"
color="success"
@click="console.log(appForm)"
/>
</SectionMain>
</LayoutAuthenticated>
</template>