Port modals to HTML dialog

Places that used alert() now use a custom dialog. Also the change username modal has been remade.

Also styles some elements. These aren't final, more just to actually style them.

I would like to use jsx or osmething instead of stuffing these in the html, especially since it would allow us to single-source-of-truth i18n (removing our second-source-of-truth in the i18n module that statically replace stuff) in a lot more places. and make other code a lot less iffy.

we do NOT need react or some shit if we do that. just nano-jsx, or something even simpler is more than good enough, and should keep us in our "100 kb smaller" track
This commit is contained in:
modeco80 2024-03-25 10:12:01 -04:00
parent b5256da3a5
commit e3f0ec56a2
10 changed files with 356 additions and 170 deletions

View File

@ -1,4 +1,4 @@
dist
*.md
*.html
#*.html
*.css

View File

@ -1,10 +1,19 @@
// TODO: Maybe we *should* use CSS variables, so the user can pick any theme
// and maybe put in hooks for that.. but for now this is fine!
// (this really should be called _style-dark @ this point..)
$root-bg-color: rgb(32, 32, 32);
$root-fg-color: rgb(180, 180, 180);
/** Cards **/
// Elements
$input-bg-color: rgb(51, 51, 51);
// Resist the urge to make these radioactive. I made them kinda grungy
// on purpose, and it seems to be okay.
$button-bg-color: rgb(51, 107, 145);
$button-red-bg-color: rgb(159, 51, 51);
$button-green-bg-color: rgb(61, 151, 43);
// Cards
$card-background-color: rgb(48, 48, 48);
$card-border-color: rgb(58, 58, 58);
$card-box-shadow-color: rgb(0, 0, 0);
@ -13,3 +22,10 @@ $card-image-background-color: #ffffff10;
$link-color: rgb(100, 140, 200);
$link-visited-color: rgb(160, 140, 200);
// Root mixin
.root {
background-color: $root-bg-color;
color: $root-fg-color;
font-family: 'Segoe UI', Tahoma, Cantarell, sans-serif;
}

View File

@ -1,3 +1,4 @@
// module for cards
@import 'colors-dark';
.cards {

44
src/css/dialog.scss Normal file
View File

@ -0,0 +1,44 @@
// for our new <dialog> based modals
@import 'colors-dark';
/* a bit of a "lazy" curve. should probably be in a shared file */
@keyframes dialogFadeIn {
0% {
opacity: 0;
}
35% {
opacity: 0.75;
}
100% {
opacity: 1;
}
}
// don't really know how i can animate "out",
// at least without hacks, but oh well
//
// this will also probably be snappier than the mess that is
// the bootstrap modals..
dialog::backdrop {
animation: dialogFadeIn 0.3s;
background-color: rgba(0, 0, 0, 0.65);
}
dialog {
@extend .root;
color: inherit;
background-color: $root-fg-color $card-background-color;
border: none;
border-radius: 4px;
box-shadow: 0px 0px 8px 0px $root-fg-color $card-box-shadow-color;
}
:modal {
animation: dialogFadeIn 0.2s;
}
.dialog-alignright {
float: right;
}

58
src/css/elements.scss Normal file
View File

@ -0,0 +1,58 @@
// Module for element styling
// THIS IS NOT DONE!
@import 'colors-dark';
.button-default {
@extend .root;
background-color: $button-bg-color !important;
border-radius: 6px;
border-width: 0;
color: #ffffff !important;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: 500;
line-height: 20px;
list-style: none;
margin: 0;
padding: 10px 12px;
text-align: center;
transition: all 200ms;
vertical-align: baseline;
white-space: nowrap;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
button {
@extend .button-default;
}
.button-red {
background-color: $button-red-bg-color !important;
}
.button-green {
background-color: $button-green-bg-color !important;
}
input[type='text'] {
padding: 0.1em;
height: 1.75em;
//position: relative;
outline: none;
border: 1px solid rgba(0, 0, 0, 0.15);
background-color: $input-bg-color;
font-size: 16px;
color: #ffffff;
}

View File

@ -2,13 +2,14 @@
@import 'colors-dark';
// modules
@import 'elements';
@import 'cards';
html,
body {
background-color: $root-bg-color;
color: $root-fg-color;
font-family: 'Segoe UI', Tahoma, Cantarell, sans-serif;
@extend .root;
margin: unset;
}
@ -331,32 +332,8 @@ a[data-cvm-node='vm0b0t'] {
}
}
// for our new <dialog> based modals
// This should probably be put into a more uniform file
/* a bit of a "lazy" curve */
@keyframes dialogFadeIn {
0% {
opacity: 0;
}
35% {
opacity: 0.75;
}
100% {
opacity: 1;
}
// shitty bootstrap polyfill
.d-none {
display: none;
}
// don't really know how i can animate "out",
// at least without hacks, but oh well
//
// this will also probably be snappier than the mess that is
// the bootstrap modals..
dialog::backdrop {
animation: dialogFadeIn 0.7s;
}
:modal {
animation: dialogFadeIn 0.4s;
}

View File

@ -1,25 +1,49 @@
<!DOCTYPE HTML>
<!doctype html>
<html prefix="og: https://ogp.me/ns#">
<head>
<title>CollabVM</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8"/>
<link href="../css/main.scss" rel="stylesheet"/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf-8" />
<link href="../css/main.scss" rel="stylesheet" />
<script src="https://kit.fontawesome.com/7add23c1ae.js" crossorigin="anonymous"></script>
<link rel="icon" href="../assets/favicon.ico">
<meta name="description" content="A website that lets you take turns controlling online virtual machines with complete strangers!"/>
<link rel="icon" href="../assets/favicon.ico" />
<meta name="description" content="A website that lets you take turns controlling online virtual machines with complete strangers!" />
<!-- Opengraph shit -->
<meta property="og:type" content="website"/>
<meta property="og:title" content="CollabVM"/>
<meta property="og:url" content="https://computernewb.com/collab-vm/"/>
<meta property="og:description" content="A website that lets you take turns controlling online virtual machines with complete strangers!"/>
<meta property="og:site_name" content="Computernewb"/>
<meta property="og:image" content="https://computernewb.com/collab-vm/desktop.png"/>
<meta property="og:type" content="website" />
<meta property="og:title" content="CollabVM" />
<meta property="og:url" content="https://computernewb.com/collab-vm/" />
<meta property="og:description" content="A website that lets you take turns controlling online virtual machines with complete strangers!" />
<meta property="og:site_name" content="Computernewb" />
<meta property="og:image" content="https://computernewb.com/collab-vm/desktop.png" />
</head>
<body class="main">
<!-- Needed for the new modal API, this is where all the modals mount to
I know I could probably create it dynamically but I just want it to work OK-->
<div id="modalRoot">
<!-- TEMP! these will be created in js or jsx -->
<dialog id="changeUsernameDialog">
<form method="dialog">
<label>Enter a new username, or leave blank to assign yourself a guest username.</label><br />
<section>
<input id="usernameInput" type="text" placeholder="A username, or leave blank." />
</section>
<div class="dialog-alignright">
<button id="okButton" value="" class="button-green">OK</button>
<button id="cancelButton" class="button-red">Cancel</button>
</div>
</form>
</dialog>
<dialog id="alertDialog">
<form method="dialog">
<label id="alertMessage"></label>
<div class="dialog-alignright">
<button id="okButton" value="" class="button-green">OK</button>
</div>
</form>
</dialog>
</div>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
@ -55,14 +79,14 @@
</div>
</div>
</nav>
<ul class="cards" id="vmlist">
</ul>
<ul class="cards" id="vmlist"></ul>
<div class="container-fluid" id="vmview">
<div id="vmDisplay"></div>
<p id="turnstatus" class="text-light"></p>
<div id="voteResetPanel" class="bg-dark text-light" style="display:none;">
<span id="voteResetHeaderText"></span><br/>
<button class="btn btn-success" id="voteYesBtn"><i class="fa-solid fa-check"></i> <span id="voteYesBtnText"></span><span class="badge bg-secondary" id="voteYesLabel"></span></button> <button class="btn btn-danger" id="voteNoBtn"><i class="fa-solid fa-ban"></i> <span id="voteNoBtnText"></span><span class="badge bg-secondary" id="voteNoLabel"></span></button><br/>
<div id="voteResetPanel" class="bg-dark text-light" style="display: none">
<span id="voteResetHeaderText"></span><br />
<button class="btn btn-success" id="voteYesBtn"><i class="fa-solid fa-check"></i> <span id="voteYesBtnText"></span><span class="badge bg-secondary" id="voteYesLabel"></span></button>
<button class="btn btn-danger" id="voteNoBtn"><i class="fa-solid fa-ban"></i> <span id="voteNoBtnText"></span><span class="badge bg-secondary" id="voteNoLabel"></span></button><br />
<span id="voteTimeText"></span>
<div id="forceVotePanel">
<button class="btn btn-info" id="forceVoteYesBtn"><i class="fa-solid fa-check"></i> <span id="passVoteBtnText"></span></button>
@ -113,16 +137,14 @@
<div class="col-md-8">
<div class="table-responsive chat-table" id="chatListDiv">
<table class="table table-hover table-dark table-borderless">
<tbody id="chatList">
</tbody>
<tbody id="chatList"></tbody>
</table>
</div>
<div class="input-group">
<span class="input-group-text bg-dark text-light" id="username">Username</span>
<input type="text" class="form-control bg-dark text-light" id="chat-input"/>
<input type="text" class="form-control bg-dark text-light" id="chat-input" />
<div class="input-group-text bg-dark text-light" id="xssCheckboxContainer">
<input class="form-check-input" type="checkbox" value="" id="xssCheckbox"/>
<input class="form-check-input" type="checkbox" value="" id="xssCheckbox" />
<label class="form-check-label" for="xssCheckbox">XSS</label>
</div>
<button class="btn btn-primary text-light" type="button" id="sendChatBtn"><i class="fa-solid fa-paper-plane"></i></button>

View File

@ -11,6 +11,8 @@ import VoteStatus from './protocol/VoteStatus.js';
import MuteState from './protocol/MuteState.js';
import { I18nStringKey, TheI18n } from './i18n.js';
import { Format } from './format.js';
import { ChangeUsername_Modal } from './modals/change_username_modal.js';
import { Alert_Modal } from './modals/alert_modal.js';
// Elements
const w = window as any;
@ -325,13 +327,13 @@ async function openVM(vm: VM): Promise<void> {
// TODO: i18n these
switch (status) {
case 'taken':
alert(TheI18n.GetString(I18nStringKey.kError_UsernameTaken));
Alert_Modal(TheI18n.GetString(I18nStringKey.kError_UsernameTaken));
break;
case 'invalid':
alert(TheI18n.GetString(I18nStringKey.kError_UsernameInvalid));
Alert_Modal(TheI18n.GetString(I18nStringKey.kError_UsernameInvalid));
break;
case 'blacklisted':
alert(TheI18n.GetString(I18nStringKey.kError_UsernameBlacklisted));
Alert_Modal(TheI18n.GetString(I18nStringKey.kError_UsernameBlacklisted));
break;
}
});
@ -339,11 +341,11 @@ async function openVM(vm: VM): Promise<void> {
VM!.on('turn', (status) => turnUpdate(status));
VM!.on('vote', (status: VoteStatus) => voteUpdate(status));
VM!.on('voteend', () => voteEnd());
VM!.on('votecd', (voteCooldown) => window.alert(TheI18n.GetString(I18nStringKey.kVM_VoteCooldownTimer, voteCooldown)));
VM!.on('votecd', (voteCooldown) => window.Alert_Modal(TheI18n.GetString(I18nStringKey.kVM_VoteCooldownTimer, voteCooldown)));
VM!.on('login', (rank: Rank, perms: Permissions) => onLogin(rank, perms));
VM!.on('close', () => {
if (!expectedClose) alert(TheI18n.GetString(I18nStringKey.kError_UnexpectedDisconnection));
if (!expectedClose) Alert_Modal(TheI18n.GetString(I18nStringKey.kError_UnexpectedDisconnection));
closeVM();
});
@ -415,7 +417,7 @@ async function openHash() {
try {
if (v !== undefined) await openVM(v);
} catch (e) {
alert((e as Error).message);
Alert_Modal((e as Error).message);
}
}
@ -631,8 +633,13 @@ elements.sendChatBtn.addEventListener('click', sendChat);
elements.chatinput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendChat();
});
elements.changeUsernameBtn.addEventListener('click', () => {
let newname = prompt(TheI18n.GetString(I18nStringKey.kVMPrompts_EnterNewUsernamePrompt), w.username);
elements.changeUsernameBtn.addEventListener('click', async () => {
let newname = await ChangeUsername_Modal(w.username);
// cancelled
if(newname == null)
return;
if (newname === w.username) return;
VM?.rename(newname);
});
@ -748,7 +755,7 @@ function userModOptions(user: { user: User; element: HTMLTableRowElement }) {
if (perms.grabip)
addUserDropdownItem(ul, 'Get IP', async () => {
let ip = await VM!.getip(user.user.username);
alert(ip);
Alert_Modal(ip);
});
tr.appendChild(ul);
}

View File

@ -0,0 +1,9 @@
const elements = {
alertDialog: document.querySelector('#alertDialog') as HTMLDialogElement
};
export function Alert_Modal(text: string) {
const alertMessage = elements.alertDialog.querySelector('#alertMessage') as HTMLLabelElement;
alertMessage.innerText = text;
elements.alertDialog.showModal();
}

View File

@ -0,0 +1,52 @@
const elements = {
changeUsernameDialog: document.querySelector('#changeUsernameDialog') as HTMLDialogElement
};
// returns a promise which will resolve with "null" for canceled,
// and a username for a username
export async function ChangeUsername_Modal(lastUsername: string): Promise<string | null> {
return new Promise((res, rej) => {
const usernameInput = elements.changeUsernameDialog.querySelector('#usernameInput') as HTMLInputElement;
const cancelButton = elements.changeUsernameDialog.querySelector('#cancelButton') as HTMLButtonElement;
const okButton = elements.changeUsernameDialog.querySelector('#okButton') as HTMLButtonElement;
usernameInput.addEventListener('change', (e) => {
okButton.value = usernameInput.value;
});
function handleDialog() {
resetBox();
res(elements.changeUsernameDialog.returnValue);
}
function handleDialogCancel() {
resetBox();
res(null);
}
function resetBox() {
usernameInput.value = '';
// remove event listener s you google..
elements.changeUsernameDialog.removeEventListener('close', handleDialog);
elements.changeUsernameDialog.removeEventListener('close', handleDialogCancel);
}
elements.changeUsernameDialog.addEventListener('cancel', (e) => {
handleDialogCancel();
});
okButton.addEventListener('click', (ev) => {
elements.changeUsernameDialog.addEventListener('close', handleDialog);
});
cancelButton.addEventListener('click', (ev) => {
elements.changeUsernameDialog.addEventListener('close', handleDialogCancel);
});
// show the modal!
usernameInput.value = lastUsername;
okButton.value = lastUsername;
elements.changeUsernameDialog.showModal();
});
}