From 0f935f545338f0755070be48c344de388a1b0e81 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Tue, 14 Jan 2025 10:49:44 +0100 Subject: [PATCH] Add confirm instance details dialog --- static/add_an_instance.mts | 19 +++++--- static/confirm_instance_details.mts | 70 +++++++++++++++++++++++++++++ static/crossroad.css | 40 ++++++++++++++++- static/crossroad.html | 32 +++++++++++++ static/crossroad.mts | 41 +++++++++++++++-- 5 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 static/confirm_instance_details.mts diff --git a/static/add_an_instance.mts b/static/add_an_instance.mts index e305637..5503dc1 100644 --- a/static/add_an_instance.mts +++ b/static/add_an_instance.mts @@ -10,7 +10,14 @@ export function parseHost(host: string): { host: string, secure: boolean } | nul }; } -export function initializeAddInstanceDialog(dialog: HTMLDialogElement): { +export function initializeAddInstanceDialog( + dialog: HTMLDialogElement, + callback: ( + host: string, + secure: boolean, + autoQueryMetadata: boolean, + ) => void +): { showAddInstanceDialog: () => void, hideAddInstanceDialog: () => void, } { @@ -32,12 +39,14 @@ export function initializeAddInstanceDialog(dialog: HTMLDialogElement): { instanceHost.setCustomValidity(""); }); - form.addEventListener("submit", async e => { + const autoQueryMetadata = form.querySelector("#autoQueryMetadata"); + if (!(autoQueryMetadata instanceof HTMLInputElement)) + throw new Error("#autoQueryMetadata isn't an input"); + + form.addEventListener("submit", e => { // A sane browser doesn't allow for submitting the form if the above validation fails const { host, secure } = parseHost(instanceHost.value)!; - console.log( - await fetch(`/api/instance_info/${secure}/${encodeURI(host)}`).then(r => r.json()) - ); + callback(host, secure, autoQueryMetadata.checked); form.reset(); }); diff --git a/static/confirm_instance_details.mts b/static/confirm_instance_details.mts new file mode 100644 index 0000000..0a19609 --- /dev/null +++ b/static/confirm_instance_details.mts @@ -0,0 +1,70 @@ +// This file handles the "Confirm instance details" dialog + +import knownSoftware from "./known_software.mjs"; + +const blankImage = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + +export function initializeInstanceDetailsDialog(dialog: HTMLDialogElement): { + showInstanceDetailsDialog: () => void, + hideInstanceDetailsDialog: () => void, + populateInstanceDetailsDialog: ( + instanceNameValue: string, + instanceSoftwareValue: string, + instanceIconValue: string | null + ) => void, +} { + const showInstanceDetailsDialog = () => dialog.showModal(); + const hideInstanceDetailsDialog = () => dialog.close(); + + const form = dialog.querySelector(".instanceDetailsForm"); + if (!(form instanceof HTMLFormElement)) + throw new Error(".instanceDetailsForm isn't a form"); + + const instanceName = form.querySelector("#instanceName"); + if (!(instanceName instanceof HTMLInputElement)) + throw new Error("#instanceName isn't an input"); + + const instanceSoftware = form.querySelector("#instanceSoftware"); + if (!(instanceSoftware instanceof HTMLSelectElement)) + throw new Error("#instanceSoftware isn't a select"); + + for (const [name, software] of Object.entries(knownSoftware.software)) { + const option = new Option(software.name, name); + instanceSoftware.appendChild(option); + } + + const instanceIcon = form.querySelector("#instanceIcon"); + if (!(instanceIcon instanceof HTMLImageElement)) + throw new Error("#instanceIcon isn't an image"); + + instanceIcon.src = blankImage; + + const populateInstanceDetailsDialog = ( + instanceNameValue: string, + instanceSoftwareValue: string, + instanceIconValue: string | null + ) => { + instanceName.value = instanceNameValue; + instanceSoftware.value = instanceSoftwareValue; + instanceIcon.src = instanceIconValue ?? blankImage; + }; + + form.addEventListener("submit", e => { + form.reset(); + }); + + const closeButton = form.querySelector(".close"); + if (!(closeButton instanceof HTMLButtonElement)) + throw new Error(".close isn't a button"); + + closeButton.addEventListener("click", e => { + instanceIcon.src = blankImage; + hideInstanceDetailsDialog(); + }); + + return { + showInstanceDetailsDialog, + hideInstanceDetailsDialog, + populateInstanceDetailsDialog + }; +} diff --git a/static/crossroad.css b/static/crossroad.css index a390c14..92bcd3c 100644 --- a/static/crossroad.css +++ b/static/crossroad.css @@ -1,6 +1,7 @@ :root { --red: #cb0b0b; --blue: #2081c3; + --transparent-black: #0008; --large: 2em; --medium: 1em; } @@ -25,7 +26,7 @@ dialog { dialog::backdrop { backdrop-filter: blur(5px); - background-color: #0008; + background-color: var(--transparent-black); transition: background-color 0.125s ease-out; @starting-style { @@ -65,6 +66,16 @@ abbr[title] { flex-direction: row; } +.flex-row-reverse { + display: flex; + flex-direction: row-reverse; +} + +.flex-column-reverse { + display: flex; + flex-direction: column-reverse; +} + .half-width { min-width: 50%; } @@ -73,8 +84,12 @@ abbr[title] { min-height: 50%; } +.full-height { + min-height: 100%; +} + .separator-bottom { - border-bottom: solid 1px #0008; + border-bottom: solid 1px var(--transparent-black); } .margin-auto-top { @@ -83,4 +98,25 @@ abbr[title] { .margin-large-bottom { margin-bottom: var(--large); +} + +.square { + aspect-ratio: 1; +} + +.iconContainer { + width: 64px; + height: 64px; + padding: 1px; + border: solid 1px var(--transparent-black); +} + +.icon { + position: relative; + max-width: 64px; + max-height: 64px; + margin: auto; + top: 50%; + left: 50%; + translate: -50% -50%; } \ No newline at end of file diff --git a/static/crossroad.html b/static/crossroad.html index 405032e..7dd8312 100644 --- a/static/crossroad.html +++ b/static/crossroad.html @@ -58,6 +58,38 @@ We do not track or save any requests or data."> + +

Confirm instance details

+
+
+
+ +
+ +

+ +
+ +
+
+
+
+ +
+ + Icon for the selected instance +
+
+
+
+
+
+ + +
+
\ No newline at end of file diff --git a/static/crossroad.mts b/static/crossroad.mts index 00332d4..0585374 100644 --- a/static/crossroad.mts +++ b/static/crossroad.mts @@ -1,4 +1,5 @@ import { initializeAddInstanceDialog } from "./add_an_instance.mjs"; +import { initializeInstanceDetailsDialog } from "./confirm_instance_details.mjs"; import knownSoftware from "./known_software.mjs"; import storageManager from "./storage_manager.mjs"; console.log(knownSoftware); @@ -9,7 +10,41 @@ export function getMainDialog(): HTMLDialogElement { return document.getElementById('mainDialog') as HTMLDialogElement; } -const dialog = document.querySelector("#addInstance"); -if (!(dialog instanceof HTMLDialogElement)) +const detailsDialog = document.querySelector("#instanceDetails"); +if (!(detailsDialog instanceof HTMLDialogElement)) + throw new Error("Couldn't find instanceDetails dialog"); +export const { + showInstanceDetailsDialog, + hideInstanceDetailsDialog, + populateInstanceDetailsDialog +} = initializeInstanceDetailsDialog(detailsDialog); + +const addInstanceDialogCallback = async ( + host: string, + secure: boolean, + autoQueryMetadata: boolean, +) => { + if (!autoQueryMetadata) { + showInstanceDetailsDialog(); + return; + } + const { name, software, iconURL } = + await fetch(`/api/instance_info/${secure}/${encodeURI(host)}`) + .then(r => r.json()); + if ( + typeof name !== "string" + || typeof software !== "string" + || !(typeof iconURL === "string" || iconURL === null) + ) + throw new Error("Invalid API response"); + populateInstanceDetailsDialog(name, software, iconURL as string | null); + showInstanceDetailsDialog(); +} + +const addDialog = document.querySelector("#addInstance"); +if (!(addDialog instanceof HTMLDialogElement)) throw new Error("Couldn't find addInstance dialog"); -export const { showAddInstanceDialog, hideAddInstanceDialog } = initializeAddInstanceDialog(dialog); +export const { + showAddInstanceDialog, + hideAddInstanceDialog +} = initializeAddInstanceDialog(addDialog, addInstanceDialogCallback);