depot/web/barf/frontend/index.html

1483 lines
42 KiB
HTML

<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<link rel="stylesheet" href="/static/fonts/stylesheet.css" />
<title>BARF | Data Collection</title>
<script type="application/json" id="load-data">{{.}}</script>
<style>
html, body, #page {
height: 100%;
}
body {
background-color: #003399;
font-family: Tahoma;
user-select: none;
}
.network-transaction {
cursor: wait;
}
* {
padding: 0;
margin: 0;
font-smooth: none;
text-rendering: geometricPrecision;
-webkit-font-smoothing: none;
-moz-osx-font-smoothing: none;
}
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24
}
#page {
display: grid;
grid-template-columns: 20% 80%;
grid-template-rows: 3rem 2px auto 2px 3rem;
grid-template-areas:
"header header"
"header-divider header-divider"
"sidebar main"
"footer-divider footer-divider"
"footer footer";
}
#header {
grid-area: header;
font-size: 2rem;
line-height: 3rem;
padding-left: 1rem;
color: white;
font-family: "Franklin Gothic";
}
.xp {
color: #ed773e;
vertical-align: top;
font-size: 0.7em;
position: relative;
top: -0.4em;
}
#header-divider {
grid-area: header-divider;
background: rgb(83,120,213);
background: linear-gradient(90deg, rgba(83,120,213,1) 0%, rgba(197,222,241,1) 20%, rgba(83,120,213,1) 100%);
}
#progress {
grid-area: sidebar;
background: rgb(83,120,213);
background: radial-gradient(circle at 100%, #6F90E3 0%, rgba(83,120,213,1) 80%);
}
#main {
grid-area: main;
background: rgb(83,120,213);
background: radial-gradient(circle at 90% 90%, #6F90E2 0%, rgba(83,120,213,1) 20%);
}
#footer-divider {
grid-area: footer-divider;
background: #c17c3e;
background: linear-gradient(90deg, #003399 0%, #c17c3e 20%, #003399 100%);
}
#footer {
grid-area: footer;
}
.pips {
margin-left: auto;
margin-top: 0.9rem;
height: 12px;
width: 12rem;
margin-right: 0;
}
.pip {
box-sizing: border-box;
display: inline-block;
width: 1rem;
height: 1rem;
margin-right: 1rem;
border-radius: 0.1rem;
background: rgb(0,127,0);
background: radial-gradient(circle, rgba(0,127,0,1) 10%, rgba(0,51,153,1) 90%);
}
@keyframes pip {
0%, 40%, 100% {opacity: 0;}
20% {opacity: 1;}
}
.pip::after {
display: block;
content: "";
box-sizing: border-box;
border: 0.1rem solid white;
width: 1rem;
height: 1rem;
background: rgb(149,223,150);
background: radial-gradient(circle at 20% 20%, rgba(149,223,150,1) 0%, rgba(38,217,38,1) 100%);
opacity: 0;
animation: pip 3s linear infinite;
}
.pip-one::after { animation-delay: 0; }
.pip-two::after { animation-delay: 0.6s; }
.pip-three::after { animation-delay: 1.2s; }
.pip-four::after { animation-delay: 1.8s; }
.pip-five::after { animation-delay: 2.4s; }
body.all-done .now-safe {
opacity: 1;
}
.now-safe {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 900;
background: black;
color: orange;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
font-weight: bold;
font-family: Arial;
font-size: xx-large;
padding: 2rem;
opacity: 0;
transition: opacity 0.2s ease-in-out;
pointer-events: none;
}
.blurb {
margin: 2.5rem;
color: white;
font-size: 175%;
}
.blurb-text {
margin-top: 3rem;
}
.blurb-head {
filter: drop-shadow(0.05em 0.1em 0 #00256d);
}
.progress-list, .will-complete {
padding: 2rem 4rem;
list-style-type: none;
font-size: 1.6rem;
color: white;
}
.progress-list li {
margin-top: 1rem;
padding-left: 1.3em;
position: relative;
}
.progress-list .dot {
border: 0.15em solid #245493;
}
.progress-done {
font-weight: bold;
color: white;
}
.progress-done .dot::after {
display: block;
content: "";
width: 70%;
height: 70%;
border-radius: 100%;
margin-left: 13.6%;
margin-top: 13.6%;
background: rgb(100,213,88);
background: radial-gradient(circle at 40% 40%, rgba(100,213,88,1) 0%, rgba(26,165,24,1) 100%);
}
.progress-current {
font-weight: bold;
color: orange;
}
.progress-current .dot {
background: rgb(217,183,144);
background: linear-gradient(137deg, rgba(217,183,144,1) 0%, rgba(225,148,77,1) 100%);
}
.progress-current .dot::before {
display: block;
content: "";
width: 70%;
height: 70%;
border-radius: 100%;
margin-left: 13.6%;
margin-top: 13.6%;
background: white;
}
.progress-list .dot {
position: absolute;
left: 0em;
top: 0.12em;
display: inline-block;
width: 0.7em;
height: 0.7em;
background-color: white;
border-radius: 100%;
}
.hide {
display: none;
}
.will-complete {
font-size: 1rem;
font-weight: bold;
}
.eta {
padding-left: 1.8em;
}
.dialog {
position: absolute;
right: 2rem;
top: 5rem;
width: min(90%, 45rem);
background-color: #d8d0cc;
box-shadow: 0.1rem 0.1rem 0 #8F8E8E,
0.1rem 0 0 #8F8E8E,
0.1rem -0.1rem 0 #efe7ed,
-0.1rem -0.1rem 0 #efe7ed,
-0.1rem 0.1rem 0 #efe7ed,
0.2rem 0.2rem 0 #474647,
0.2rem 0 0 #474647,
0rem 0.2rem 0 #474647,
0.1rem -0.2rem 0 #f5effb,
0.2rem -0.2rem 0 #474647,
-0.2rem -0.2rem 0 #f5effb,
-0.2rem 0.2rem 0 #f5effb;
}
.dialog-titlebar {
font-weight: bold;
color: white;
padding: 0.1rem 0;
background: rgb(20,37,89);
background: linear-gradient(137deg, rgba(20,37,89,1) 0%, rgba(166,193,228,1) 100%);
}
.dialog-headerbar {
position: relative;
height: 5.2rem;
background-color: white;
}
.dialog-headerbar-header {
font-size: medium;
padding-left: 2rem;
padding-top: 1rem;
}
.dialog-headerbar-blurb {
padding-left: 4rem;
}
.dialog-headerbar-icon {
position: absolute;
top: 0.4rem;
right: 0.4rem;
width: 4.4rem;
height: 4.4rem;
font-size: 4.4rem;
background: #03003B;
color: white;
}
.dialog-body {
min-height: 34rem;
position: relative;
display: grid;
grid-template-columns: 5rem 4.4rem 1.6rem auto 2rem;
grid-template-rows: 2rem auto 1rem min-content 1rem;
grid-template-areas:
". . . . ."
". icon . body ."
". . . . ."
". buttons buttons buttons ."
". . . . .";
}
.dialog-body-icon {
width: 4.4rem;
height: 4.4rem;
grid-area: icon;
float: left;
background-size: cover;
background-repeat: no-repeat;
}
.dialog-icon-sound {
background-image: url(/static/soundicon.png);
}
.dialog-icon-agent {
background-image: url(/static/agenticon.png);
}
.dialog-icon-email {
background-image: url(/static/emailicon.png);
}
.dialog-icon-error {
background-image: url(/static/erroricon.png);
}
.dialog-icon-calendar {
background-image: url(/static/calendaricon.png);
}
.dialog-icon-filmstrip {
background-image: url(/static/filmstripicon.png);
}
.dialog-icon-network {
background-image: url(/static/networkicon.png);
}
.dialog-icon-speech {
background-image: url(/static/speechicon.png);
}
.dialog-actual-body {
grid-area: body;
margin-top: -0.5rem;
}
.dialog-bottom-buttons {
grid-area: buttons;
display: flex;
justify-content: end;
margin-right: 10rem;
}
.dialog-actual-body p {
margin: 0.5rem 0;
}
.dialog-message-box {
right: 30%;
top: 30%;
}
.dialog-message-box .dialog-body {
min-height: initial;
grid-template-columns: 2rem 4.4rem 1.6rem auto 2rem;
grid-template-rows: 1rem auto 1rem;
grid-template-areas:
". . . . ."
". icon . body ."
". . . . .";
text-align: center;
}
.dialog-message-box .dialog-actual-body {
margin-top: 0.5rem;
}
.dialog-btn {
box-sizing: border-box;
font-family: Tahoma;
padding: 0.2em 1.8em;
background-color: #d8d0cc;
border-top: 0.1rem solid #ffffff;
border-left: 0.1rem solid #ffffff;
border-bottom: 0.1rem solid #474647;
border-right: 0.1rem solid #474647;
}
.dialog-btn:active {
border-top: 0.1rem solid #000000;
border-left: 0.1rem solid #000000;
border-bottom: 0.1rem solid #000000;
border-right: 0.1rem solid #000000;
}
.dialog input[type="text"], .dialog input[type="email"], .dialog textarea {
border-bottom: 0.1rem solid #ffffff;
border-right: 0.1rem solid #ffffff;
border-top: 0.1rem solid #474647;
border-left: 0.1rem solid #474647;
}
.dialog textarea {
width: 100%;
min-height: 6rem;
resize: vertical;
font-family: Tahoma;
}
.if-on-mobile {
display: none;
}
.checkbox-it {
appearance: none;
position: relative;
margin-left: 0.5em;
margin-right: 0.5em;
}
.checkbox-it::before {
display: inline-block;
content: "";
width: 0.7em;
height: 0.7em;
background-color: white;
background-size: cover;
box-shadow: 0.05rem 0.05rem 0 #d8d0cc,
0.05rem 0rem 0 #d8d0cc,
0rem 0.05rem 0 #d8d0cc,
-0.05rem -0.05rem 0 #474647,
-0.05rem 0rem 0 #474647,
0.05rem -0.05rem 0 #474647,
-0.1rem -0.1rem 0 #8F8E8E,
0rem -0.1rem 0 #8F8E8E,
-0.1rem 0rem 0 #8F8E8E,
0.1rem 0.1rem 0 #ffffff,
-0.1rem 0.1rem 0 #ffffff;
}
.checkbox-it.active::before {
background-color: #d8d0cc;
}
.checkbox-it:checked::before {
background-image: url(/static/check.svg);
}
.dialog-rule {
margin: 0.5rem 0;
}
.dialog-line {
display: flex;
margin: 1rem 0;
}
.dialog-line > span {
width: 10rem;
}
.dialog-line > input {
flex-grow: 1;
}
.dialog-availability-line {
display: flex;
flex-direction: row;
justify-content: space-between;
}
#assistant > video {
position: fixed;
bottom: 4rem;
left: 1rem;
width: 124px;
height: 93px;
pointer-events: none;
}
#assistant-bubble {
position: fixed;
bottom: calc(4rem + 40px);
left: calc(1rem + 124px);
background-color: #F8FCC8;
color: black;
padding: 0.2em;
border: 1px solid black;
border-radius: 0.4rem;
font-size: smaller;
max-width: 20rem;
}
#assistant-bubble::after {
display: inline-block;
content: "";
width: 0;
height: 0;
border-width: 4px;
border-style: solid;
border-color: transparent;
border-right-color: #f8fcc8;
border-top-color: #f8fcc8;
position: absolute;
bottom: 6px;
right: 100%;
}
#assistant-bubble::before {
display: inline-block;
content: "";
width: 0;
height: 0;
border-width: 5px;
border-style: solid;
border-color: transparent;
border-right-color: black;
border-top-color: black;
position: absolute;
bottom: 5px;
right: 100%;
}
#clickshield {
display: none;
}
.network-transaction #clickshield {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 99999;
cursor: wait;
}
@media (max-width: 1000px) {
#page {
grid-template-columns: 100%;
grid-template-rows: 3rem 2px auto 2px 3rem;
grid-template-areas:
"header"
"header-divider"
"main"
"footer-divider"
"footer";
}
#progress {
display: none;
}
.blurb {
font-size: 80%;
}
.if-on-mobile {
display: revert;
}
.dialog {
position: absolute;
right: 1rem !important;
top: 5rem !important;
}
.dialog-headerbar-header {
font-size: medium;
padding-left: 1rem;
padding-top: 0.5rem;
}
.dialog-headerbar-blurb {
padding-left: 2rem;
padding-right: 1rem;
}
.dialog:not(.dialog-message-box) .dialog-body {
min-height: initial;
display: grid;
grid-template-columns: auto;
grid-template-rows: auto 1rem auto;
grid-template-areas:
"body"
"."
"buttons";
margin: 1rem;
}
.dialog-headerbar-icon, .dialog:not(.dialog-message-box) .dialog-body-icon {
display: none;
}
.dialog-bottom-buttons {
margin-right: 0;
}
#assistant, #assistant-bubble {
display: none;
}
}
@media (max-width: 1450px) {
.progress-list, .will-complete {
padding: 2rem 2rem;
}
.progress-list {
font-size: 1.1rem;
}
}
</style>
</head>
<body>
<div id="clickshield"></div>
<div id="page">
<div id="header">
BARF<span class="xp">xp</span>
</div>
<div id="header-divider"></div>
<div id="progress">
<ol class="progress-list">
<li class="progress-done"><div class="dot"></div> Introduction</li>
<li class="progress-current"><div class="dot"></div> Collecting information</li>
<li class="progress-not-done"><div class="dot"></div> Availability</li>
<li class="progress-not-done"><div class="dot"></div> Proposal</li>
<li class="progress-not-done"><div class="dot"></div> Accommodation and travel</li>
<li class="progress-not-done"><div class="dot"></div> Finishing up</li>
</ol>
<p class="will-complete">Setup will complete in approximately:<br><span class="eta">3 minutes</span></p>
</div>
<div id="main">
<div class="blurb">
<h2 class="blurb-head">Experience the ultimate in Birthday Shenanigans</h2>
<p class="blurb-text">Building on the success of Birthday® 2022, BARF XP Professional provides the latest technologies to help plan another birthday party, while keeping your data safe from unauthorized access.</p>
<p class="blurb-text">BARF Assistant helps keep you informed and entertained while filling in boring information. Built-in animations keep you aggravated and annoyed throughout the process.</p>
<p class="blurb-text">If you need to encrypt your data, BARF XP Professional uses the latest in Transport Layer Security version 1.3, which has been improved to be better than the previous version in some way.</p>
</div>
</div>
<div id="footer-divider"></div>
<div id="footer">
<div class="pips">
<div class="pip pip-one"></div>
<div class="pip pip-two"></div>
<div class="pip pip-three"></div>
<div class="pip pip-four"></div>
<div class="pip pip-five"></div>
</div>
</div>
</div>
<div class="dialogs">
<div class="dialog dialog-message-box hide">
<div class="dialog-titlebar">BARF XP Professional Edition</div>
<div class="dialog-body">
<div class="dialog-body-icon dialog-icon-error"></div>
<div class="dialog-actual-body">
<p>Feck, lorem ipsum dolor sit amet. Quis custodies ipsos lorem ipsum dolor sit amet.</p>
</div>
</div>
</div>
<div class="dialog dialog-introduction hide">
<div class="dialog-titlebar">BARF XP Professional Edition</div>
<div class="dialog-headerbar">
<h2 class="dialog-headerbar-header">Introduction</h2>
<p class="dialog-headerbar-blurb">You can customize various aspects of BARF before we get started.</p>
<div class="dialog-headerbar-icon material-symbols-outlined">interactive_space</div>
</div>
<div class="dialog-body">
<div class="dialog-body-icon dialog-icon-sound"></div>
<div class="dialog-actual-body">
<p class="if-on-mobile"><strong>You seem to be on a mobile device, or with a window that I arbitrarily decided was 'too small'.</strong> You <em>will</em> be better off using this application from a desktop or laptop, for the full BARF™ experience.</p>
<hr class="if-on-mobile dialog-rule">
<p>You can choose whether to have audio turned on, for the real BARF™ eXPerience. I won't force you to, but it'll be slightly funnier. Probably.</p>
<div class="audio"><label><input type="checkbox" name="audio-enabled" data-dataset-name="audioEnabled" value="on" class="audio-toggle checkbox-it"> Enable audio</label></div>
<hr class="dialog-rule">
<p>Note that the URL you were given is unique to you, so please don't share it.</p>
</div>
<div class="dialog-bottom-buttons">
<button class="dialog-btn dialog-next">Next &gt;</button>
</div>
</div>
</div>
<div class="dialog dialog-collecting-info hide">
<div class="dialog-titlebar">BARF XP Professional Edition</div>
<div class="dialog-headerbar">
<h2 class="dialog-headerbar-header">Collecting information</h2>
<p class="dialog-headerbar-blurb">Let's learn more about who you are.</p>
<div class="dialog-headerbar-icon material-symbols-outlined">interactive_space</div>
</div>
<div class="dialog-body">
<div class="dialog-body-icon dialog-icon-agent">
</div>
<div class="dialog-actual-body">
<p>Welcome to BARF, the Birthday Activities Registration Form.</p>
<p>Your responses will be saved as we go through, so you should be able to reload the page and resume from where you left off if something goes wrong.</p>
<p>Some user data is required, which will be deleted after the event:</p>
<hr class="dialog-rule">
<form>
<p>BARF has suggested a name. If you wish to be called something else, you can correct it now.</p>
<label class="dialog-line"><span>Name:</span><input type="text" id="field-name" data-dataset-name="name" data-tts="What should I call you?" required></label>
<hr class="dialog-rule">
<p>To get in contact before and during the event, Discord will be used.</p>
<br>
<p>Provide a Discord username.</p>
<label class="dialog-line"><span>Discord Username:</span><input type="text" id="field-discord-username" data-dataset-name="discordUsername" data-tts="Alright ${name}, what's your Discord username?" required></label>
</form>
</div>
<div class="dialog-bottom-buttons">
<button class="dialog-btn dialog-prev">&lt; Prev</button>
<button class="dialog-btn dialog-next">Next &gt;</button>
</div>
</div>
</div>
<div class="dialog dialog-collecting-info-2 hide">
<div class="dialog-titlebar">BARF XP Professional Edition</div>
<div class="dialog-headerbar">
<h2 class="dialog-headerbar-header">Collecting information</h2>
<p class="dialog-headerbar-blurb">Let's learn more about who you are.</p>
<div class="dialog-headerbar-icon material-symbols-outlined">interactive_space</div>
</div>
<div class="dialog-body">
<div class="dialog-body-icon dialog-icon-email">
</div>
<div class="dialog-actual-body">
<form>
<p>Would you like to receive calendar invites to the events you're interested in?</p>
<label><input type="checkbox" id="field-receive-email" class="checkbox-it" data-dataset-name="receiveEmail"> Send me calendar invites</label>
<hr class="dialog-rule">
<p>If you want calendar invites, an email address must be provided.</p>
<label class="dialog-line"><span>Email:</span><input type="email" data-dataset-name="email" disabled data-tts="OK, to send you calendar invites, I'll need your email address. What is it?" id="field-email"></label>
</form>
</div>
<div class="dialog-bottom-buttons">
<button class="dialog-btn dialog-prev">&lt; Prev</button>
<button class="dialog-btn dialog-next">Next &gt;</button>
</div>
</div>
</div>
<div class="dialog dialog-availability hide">
<div class="dialog-titlebar">BARF XP Professional Edition</div>
<div class="dialog-headerbar">
<h2 class="dialog-headerbar-header">Availability</h2>
<p class="dialog-headerbar-blurb">Check your calendar to ascertain your availability.</p>
<div class="dialog-headerbar-icon material-symbols-outlined">interactive_space</div>
</div>
<div class="dialog-body">
<div class="dialog-body-icon dialog-icon-calendar">
</div>
<div class="dialog-actual-body">
<form>
<p>The following weekends are proposed - the main festivities will be on the Saturday until late.</p>
<hr class="dialog-rule">
<p>Saturday 31st August/Sunday 1st September</p>
<p class="dialog-availability-line">
<label><input type="radio" name="sat-31-august" data-dataset-name="dateSat31August" value="yes" required> Yes</label>
<label><input type="radio" name="sat-31-august" data-dataset-name="dateSat31August" value="no"> No</label>
<label><input type="radio" name="sat-31-august" data-dataset-name="dateSat31August" value="maybe"> If Necessary</label>
</p>
<hr class="dialog-rule">
<p>Saturday 7th September/Sunday 8th September</p>
<p class="dialog-availability-line">
<label><input type="radio" name="sat-7th-september" data-dataset-name="dateSat7September" value="yes" required> Yes</label>
<label><input type="radio" name="sat-7th-september" data-dataset-name="dateSat7September" value="no"> No</label>
<label><input type="radio" name="sat-7th-september" data-dataset-name="dateSat7September" value="maybe"> If Necessary</label>
</p>
</form>
</div>
<div class="dialog-bottom-buttons">
<button class="dialog-btn dialog-prev">&lt; Prev</button>
<button class="dialog-btn dialog-next">Next &gt;</button>
</div>
</div>
</div>
<div class="dialog dialog-proposal hide">
<div class="dialog-titlebar">BARF XP Professional Edition</div>
<div class="dialog-headerbar">
<h2 class="dialog-headerbar-header">Proposal</h2>
<p class="dialog-headerbar-blurb">Select the activities of interest.</p>
<div class="dialog-headerbar-icon material-symbols-outlined">interactive_space</div>
</div>
<div class="dialog-body">
<div class="dialog-body-icon dialog-icon-filmstrip">
</div>
<div class="dialog-actual-body">
<form>
<p>The festivities will likely include the following activities. Select the ones you would be interested to attend:</p>
<hr class="dialog-rule">
<p>Escape Room (2pmish-4:30pmish)</p>
<p class="dialog-availability-line">
<label><input type="radio" name="escape-room" data-dataset-name="activityEscapeRoom" data-tts="Ah, an escape room connoisseur are you?" value="yes" required> Yes</label>
<label><input type="radio" name="escape-room" data-dataset-name="activityEscapeRoom" value="no"> No</label>
<label><input type="radio" name="escape-room" data-dataset-name="activityEscapeRoom" value="maybe"> Maybe</label>
</p>
<hr class="dialog-rule">
<p>Pub/Socialising (4:30pmish-7pmish)</p>
<p class="dialog-availability-line">
<label><input type="radio" name="pub" data-dataset-name="activityPub" data-tts="Socialising at the pub should be fun. I wish I could drink, but I am Clippy." value="yes" required> Yes</label>
<label><input type="radio" name="pub" data-dataset-name="activityPub" value="no"> No</label>
<label><input type="radio" name="pub" data-dataset-name="activityPub" value="maybe"> Maybe</label>
</p>
<hr class="dialog-rule">
<p>Mean Girls: The Musical (7:30pm-10pm)</p>
<p class="dialog-availability-line">
<label><input type="radio" name="mean-girls" data-dataset-name="activityMeanGirlsTheMusical" data-tts="I hear that mitochondria is the powerhouse of the cell." value="yes" required> Yes</label>
<label><input type="radio" name="mean-girls" data-dataset-name="activityMeanGirlsTheMusical" value="no"> No</label>
<label><input type="radio" name="mean-girls" data-dataset-name="activityMeanGirlsTheMusical" value="maybe"> Maybe</label>
</p>
<hr class="dialog-rule">
<p>Karaoke (10:30pm-12:30am)</p>
<p class="dialog-availability-line">
<label><input type="radio" name="karaoke" data-dataset-name="activityKaraoke" data-tts="I can't really sing very well, so I'll give this a pass." value="yes" required> Yes</label>
<label><input type="radio" name="karaoke" data-dataset-name="activityKaraoke" value="no"> No</label>
<label><input type="radio" name="karaoke" data-dataset-name="activityKaraoke" value="maybe"> Maybe</label>
</p>
</form>
</div>
<div class="dialog-bottom-buttons">
<button class="dialog-btn dialog-prev">&lt; Prev</button>
<button class="dialog-btn dialog-next">Next &gt;</button>
</div>
</div>
</div>
<div class="dialog dialog-accommodation hide">
<div class="dialog-titlebar">BARF XP Professional Edition</div>
<div class="dialog-headerbar">
<h2 class="dialog-headerbar-header">Accommodation &amp; Travel</h2>
<p class="dialog-headerbar-blurb">Room and board, or board to a room.</p>
<div class="dialog-headerbar-icon material-symbols-outlined">interactive_space</div>
</div>
<div class="dialog-body">
<div class="dialog-body-icon dialog-icon-network">
</div>
<div class="dialog-actual-body">
<form>
<p>Will you need a hotel room on the Saturday night?</p>
<label><input type="checkbox" name="hotel" data-dataset-name="accommodationRequired"> Yes, book me a hotel room</label>
<hr class="dialog-rule">
<p>Do you need help covering travel costs into London?</p>
<label><input type="checkbox" name="hotel" data-dataset-name="travelCosts"> Yes, please help with my travel costs</label>
</form>
</div>
<div class="dialog-bottom-buttons">
<button class="dialog-btn dialog-prev">&lt; Prev</button>
<button class="dialog-btn dialog-next">Next &gt;</button>
</div>
</div>
</div>
<div class="dialog dialog-finishing-up hide">
<div class="dialog-titlebar">BARF XP Professional Edition</div>
<div class="dialog-headerbar">
<h2 class="dialog-headerbar-header">Final Comments</h2>
<p class="dialog-headerbar-blurb">Free text to express yourself in. Go nuts.</p>
<div class="dialog-headerbar-icon material-symbols-outlined">interactive_space</div>
</div>
<div class="dialog-body">
<div class="dialog-body-icon dialog-icon-speech">
</div>
<div class="dialog-actual-body">
<form>
<p>Anything else that needs mentioning?</p>
<textarea data-dataset-name="misc"></textarea>
</form>
</div>
<div class="dialog-bottom-buttons">
<button class="dialog-btn dialog-prev">&lt; Prev</button>
<button class="dialog-btn dialog-next">Next &gt;</button>
</div>
</div>
</div>
</div>
<div id="assistant"></div>
<div id="assistant-bubble" class="hide">Hello, I am the barf barf barf. Hello, I am the barf barf barf. Hello, I am the barf barf barf. Hello, I am the barf barf barf. Hello, I am the barf barf barf.</div>
<audio id="text-to-speecher"></audio>
<div class="now-safe">
<p>It's now safe to close this tab.</p>
</div>
<script type="module">
let TTS_ENABLED = false;
const PHASES = [{
name: "Introduction",
eta: "3 minutes",
dialogs: [{
id: 'introduction',
}],
}, {
name: "Collecting information",
eta: "3 minutes",
dialogs: [{
introTTS: "Hello, and welcome to the Birthday Activities Registration Form, or BARF. I'm Clippy and I'm helping Luke organise everything! Your responses will be saved as we go through, so you should be able to reload the page and resume from where you left off.\n\nIn order to get started, I need a few pieces of information from you, which will be deleted after the event.",
id: 'collecting-info',
}, {
introTTS: "${name}, would you like to receive calendar invites to the events you're interested in?",
id: 'collecting-info-2'
}],
}, {
name: "Availability",
eta: "3 minutes",
dialogs: [{
introTTS: "OK ${name} - time to check your calendar. What's your availability for these weekends?",
introAnimation: "headphones",
id: 'availability',
}],
}, {
name: "Proposal",
eta: "2 minutes",
dialogs: [{
introTTS: "Here's the general proposal for the schedule on the Saturday. What sort of things are you interested in, ${name}?",
introAnimation: "sleepy",
id: 'proposal',
}],
}, {
name: "Accommodation and travel",
eta: "1 minute",
dialogs: [{
introTTS: "Awesome! Last question: do you need a hotel room, or help covering travel (or any other) costs to get to London?",
introAnimation: "tapscreen",
id: 'accommodation',
}],
}, {
name: "Finishing up",
eta: null,
dialogs: [{
introTTS: "I lied. This is the real last question. Do you have anything else you might want to mention, like dietary requirements, assorted thanks, arthropod facts, or the entire Bee Movie script?",
introAnimation: "writing",
id: 'finishing-up',
}],
}];
let CURRENT_PHASE = 0;
class BARF {
constructor() {
this.textToSpeechEnabledSound = new Audio("/static/audioenabled.wav");
this.currentlySpeaking = null;
this.dataset = {
currentPhase: 0,
currentPhaseDialog: 0,
audioEnabled: false,
name: "",
discordUsername: "",
receiveEmail: false,
email: "",
};
this.prevDialog = null;
this.assistantQueue = [];
this.assistantShown = false;
this.assistantRunning = false;
this.currentAssistantActivity = null;
}
assistantPlay(video, text, callback) {
if (text !== null) {
this.stopRunningTTS();
}
this.assistantQueue.push({
what: 'playVideo',
video: video,
text: text,
callback: callback,
});
this.maybeRunAssistant();
}
maybeRunAssistant() {
if (!this.assistantRunning) {
this.runAssistant();
}
}
setNetworkTransaction(state) {
if (state) {
document.body.classList.add('network-transaction');
} else {
document.body.classList.remove('network-transaction');
}
for (const el of document.body.querySelectorAll('input, textarea, button')) {
if (state) {
if (!el.dataset.previouslyDisabled) el.dataset.previouslyDisabled = el.disabled;
el.disabled = state;
} else {
el.disabled = el.dataset.previouslyDisabled === 'true';
delete el.dataset.previouslyDisabled;
}
}
}
runAssistant() {
if (this.assistantQueue.length === 0) {
this.assistantRunning = false;
return;
}
this.assistantRunning = true;
const assistantActuallyRendering = window.getComputedStyle(document.querySelector('#assistant')).display !== 'none';
if (!this.assistantShown) {
if (assistantActuallyRendering) {
this.setNetworkTransaction(true);
this.assistantQueue.splice(0, 0, {
what: 'playVideo',
video: 'intro',
callback: () => {
this.setNetworkTransaction(false);
this.assistantShown = true;
},
});
} else {
this.assistantShown = true;
}
}
const firstThing = this.assistantQueue.shift();
if (!firstThing) {
this.assistantRunning = false;
return;
}
const pumpLoop = () => {
if (firstThing.callback) {
firstThing.callback();
}
this.currentAssistantActivity = null;
this.runAssistant();
};
this.currentAssistantActivity = firstThing;
switch (firstThing.what) {
case 'playVideo':
let promises = [];
if (firstThing.text !== null) {
promises.push(new Promise((resolve) => {
this.assistantDoTTS(firstThing.text, resolve);
}));
}
if (assistantActuallyRendering) {
promises.push(new Promise((resolve) => {
this.assistantDoVideo(firstThing.video, firstThing.videoBackground, resolve);
}));
}
Promise.allSettled(promises).finally(pumpLoop);
break;
case 'doTTS':
this.assistantDoTTS(firstThing.text, pumpLoop);
break;
}
}
assistantDoVideo(video, asBackground, callback) {
const url = `/static/clipit/${video}.webm`;
const videoEl = document.createElement('video');
videoEl.src = url;
videoEl.muted = true;
videoEl.autoplay = true;
const assistantEl = document.querySelector('#assistant');
assistantEl.innerHTML = '';
assistantEl.appendChild(videoEl);
if (asBackground) {
callback();
} else {
videoEl.addEventListener('ended', callback);
}
}
assistantDoTTS(text, callback) {
if (!this.dataset.audioEnabled) callback();
const replacedText = text.replace(/[$][{]([^}]+)[}]/g, (match, p1) => {
return this.dataset[p1];
});
const bubbleEl = document.querySelector('#assistant-bubble');
bubbleEl.innerText = replacedText;
bubbleEl.classList.remove('hide');
const audio = new Audio(`/sam?text=${encodeURIComponent(replacedText)}`);
audio.play();
this.currentlySpeaking = audio;
audio.addEventListener('ended', callback);
}
stopRunningTTS() {
if (this.currentlySpeaking !== null) {
this.currentlySpeaking.pause();
}
this.currentlySpeaking = null;
this.assistantQueue = this.assistantQueue.filter((el) => el.what !== 'doTTS' && !el.text);
if (this.currentAssistantActivity && (this.currentAssistantActivity.what === 'doTTS' || this.currentAssistantActivity.text)) {
this.runAssistant();
}
}
assistantDismiss() {
const bubbleEl = document.querySelector('#assistant-bubble');
bubbleEl.classList.add('hide');
return new Promise((resolve) => {
this.assistantPlay('goodbye', null, () => {
const assistantEl = document.querySelector('#assistant');
assistantEl.innerHTML = '';
resolve();
});
});
}
finishUpDone() {
this.stopRunningTTS();
Promise.all([this.assistantDismiss(), this.doSave()]).then(() => {
document.body.classList.add('all-done');
}, (err) => {
this.panic(`Saving for the final time failed: ${err}`);
});
}
renderPhase() {
// Phase list
const progressListEl = document.createElement('ol');
progressListEl.classList.add('progress-list');
for (let i = 0; i < PHASES.length; i++) {
let phaseClass = "";
if (i < this.dataset.currentPhase) {
phaseClass = "progress-done";
} else if (i == this.dataset.currentPhase) {
phaseClass = "progress-current";
} else {
phaseClass = "progress-not-done";
}
const phaseEl = document.createElement('li');
phaseEl.classList.add(phaseClass);
const dotEl = document.createElement('div');
dotEl.classList.add('dot');
phaseEl.appendChild(dotEl);
phaseEl.appendChild(document.createTextNode(` ${PHASES[i].name}`));
progressListEl.appendChild(phaseEl);
}
const curProgressListEl = document.querySelector('.progress-list');
curProgressListEl.parentNode.replaceChild(progressListEl, curProgressListEl);
// ETA
const curWillCompleteEl = document.querySelector('.will-complete');
const curETAEl = curWillCompleteEl.querySelector('.eta');
const currentPhase = PHASES[this.dataset.currentPhase];
if (!currentPhase || currentPhase.eta == null) {
curWillCompleteEl.classList.add('hide');
} else {
curETAEl.innerText = currentPhase.eta;
curWillCompleteEl.classList.remove('hide');
}
}
currentDialog() {
const phase = PHASES[this.dataset.currentPhase];
if (!phase || !phase.dialogs) {
return null;
}
const dialog = phase.dialogs[this.dataset.currentPhaseDialog];
if (!dialog) {
return null;
}
return document.querySelector(`.dialog-${dialog.id}`);
}
doSave() {
this.setNetworkTransaction(true);
return fetch(window.location, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(this.dataset),
}).finally(() => {
this.setNetworkTransaction(false);
});
}
advanceBy(n) {
let prevDialog = this.currentDialog();
if (prevDialog === null) {
prevDialog = this.prevDialog;
}
const prevPhase = PHASES[this.dataset.currentPhase];
const prevDialogObj = prevPhase.dialogs[this.dataset.currentPhaseDialog];
while (n > 0) {
let newPhase = PHASES[this.dataset.currentPhase];
this.dataset.currentPhaseDialog += n;
if (this.dataset.currentPhaseDialog < newPhase.dialogs.length) {
break;
}
n = this.dataset.currentPhaseDialog - newPhase.dialogs.length;
this.dataset.currentPhase++;
this.dataset.currentPhaseDialog = 0;
}
while (n < 0) {
let newPhase = PHASES[this.dataset.currentPhase];
this.dataset.currentPhaseDialog += n;
if (this.dataset.currentPhaseDialog >= 0) {
break;
}
n = this.dataset.currentPhaseDialog+1;
this.dataset.currentPhase--;
this.dataset.currentPhaseDialog = PHASES[this.dataset.currentPhase].dialogs.length-1;
}
const thisPhase = PHASES[this.dataset.currentPhase];
this.hideDialogs();
this.renderPhase();
if (this.dataset.currentPhase === PHASES.length) {
this.finishUpDone(); // finishUp will save itself while playing the outro animation
return;
}
this.doSave().then(() => {
this.showDialogForCurrentPhase();
const nowDialog = this.currentDialog();
if (nowDialog !== null) {
this.prevDialog = nowDialog;
}
if (prevDialog !== null && nowDialog !== null) {
nowDialog.style.left = prevDialog.style.left;
nowDialog.style.top = prevDialog.style.top;
nowDialog.style.right = prevDialog.style.right;
nowDialog.style.bottom = prevDialog.style.bottom;
}
if (thisPhase.dialogs) {
const thisDialogObj = thisPhase.dialogs[this.dataset.currentPhaseDialog];
if (thisDialogObj != prevDialogObj) {
if (thisDialogObj.introAnimation) {
let tts = thisDialogObj.introTTS;
if (!tts) tts = null;
this.assistantPlay(thisDialogObj.introAnimation, tts);
} else if (thisDialogObj.introTTS) {
this.sayIt(thisDialogObj.introTTS);
}
}
}
}, (err) => {
this.panic('Something went wrong saving your form responses: ' + err);
});
}
checkFormValidity() {
const dialogEl = this.currentDialog();
if (!dialogEl) return true;
const formEls = dialogEl.querySelectorAll('form');
for (const formEl of formEls) {
if (!formEl.reportValidity()) return false;
}
return true;
}
nextClicked() {
if (this.checkFormValidity()) this.advanceBy(1);
}
prevClicked() {
if (this.checkFormValidity()) this.advanceBy(-1);
}
dialogMouseDown(ev, dialogEl, titlebarEl) {
ev.preventDefault();
let data = {};
data.lastX = ev.clientX;
data.lastY = ev.clientY;
data.totalDeltaX = 0;
data.totalDeltaY = 0;
data.mouseUpListener = (ev) => this.dialogMouseUp(ev, dialogEl, titlebarEl, data);
data.mouseMoveListener = (ev) => this.dialogMouseMove(ev, dialogEl, titlebarEl, data);
document.addEventListener('mouseup', data.mouseUpListener);
document.addEventListener('mousemove', data.mouseMoveListener);
}
dialogMouseUp(ev, dialogEl, titlebarEl, data) {
document.removeEventListener('mouseup', data.mouseUpListener);
document.removeEventListener('mousemove', data.mouseMoveListener);
const compStyle = window.getComputedStyle(dialogEl);
const leftPx = parseInt(compStyle.getPropertyValue("left"), 10);
const topPx = parseInt(compStyle.getPropertyValue("top"), 10);
dialogEl.style.left = `${leftPx - data.totalDeltaX}px`;
dialogEl.style.top = `${topPx - data.totalDeltaY}px`;
dialogEl.style.right = 'initial';
dialogEl.style.bottom = 'initial';
dialogEl.style.transform = null;
}
dialogMouseMove(ev, dialogEl, titlebarEl, data) {
ev.preventDefault();
const deltaX = data.lastX - ev.clientX;
const deltaY = data.lastY - ev.clientY;
data.lastX = ev.clientX;
data.lastY = ev.clientY;
data.totalDeltaX += deltaX;
data.totalDeltaY += deltaY;
dialogEl.style.transform = `translate(${-data.totalDeltaX}px, ${-data.totalDeltaY}px)`;
}
makeDialogsDraggable() {
const dialogEls = document.querySelectorAll('.dialog');
for (const dialogEl of dialogEls) {
const titlebarEl = dialogEl.querySelector('.dialog-titlebar');
titlebarEl.addEventListener('mousedown', (ev) => this.dialogMouseDown(ev, dialogEl, titlebarEl));
}
}
audioToggleClicked(audioToggleEl, audioToggleEls) {
if (audioToggleEl.value === 'on' && audioToggleEl.checked) {
this.dataset.audioEnabled = true;
} else {
this.dataset.audioEnabled = false;
}
for (const audioToggleEl of audioToggleEls) {
if (audioToggleEl.value === 'on') {
audioToggleEl.checked = this.dataset.audioEnabled;
} else {
audioToggleEl.checked = !this.dataset.audioEnabled;
}
}
if (this.dataset.audioEnabled) {
this.textToSpeechEnabledSound.currentTime = 0;
this.textToSpeechEnabledSound.play();
this.currentlySpeaking = this.textToSpeechEnabledSound;
} else {
this.stopRunningTTS();
}
}
sayIt(text) {
this.stopRunningTTS();
if (!text) return;
this.assistantQueue.push({
what: 'doTTS',
text: text,
});
this.maybeRunAssistant();
}
bindAudioToggles() {
const audioToggleEls = document.querySelectorAll('.audio-toggle');
for (const audioToggleEl of audioToggleEls) {
audioToggleEl.addEventListener('click', () => this.audioToggleClicked(audioToggleEl, audioToggleEls));
}
}
bindDialogButtons() {
for (const nextEl of document.querySelectorAll('.dialog-next')) {
nextEl.addEventListener('click', () => {
this.nextClicked();
});
}
for (const prevEl of document.querySelectorAll('.dialog-prev')) {
prevEl.addEventListener('click', () => {
this.prevClicked();
});
}
}
hideDialogs() {
for (const dialogEl of document.querySelectorAll('.dialog')) {
dialogEl.classList.add('hide');
}
}
showDialogForCurrentPhase() {
this.hideDialogs();
const dialogEl = this.currentDialog();
if (dialogEl !== null) {
dialogEl.classList.remove('hide');
}
}
bindActive() {
const inputEls = document.querySelectorAll('.checkbox-it');
for (const inputEl of inputEls) {
let wrapperEl = inputEl;
if (inputEl.parentElement.nodeName === 'LABEL') {
wrapperEl = inputEl.parentElement;
}
wrapperEl.addEventListener('mousedown', () => {
inputEl.classList.add('active');
});
wrapperEl.addEventListener('mouseup', () => {
inputEl.classList.remove('active');
});
if (inputEl.hasAttribute('id')) {
const labelEl = document.body.querySelector(`label[for="${inputEl.id}"]`);
if (labelEl !== null) {
labelEl.addEventListener('mousedown', () => {
inputEl.classList.add('active');
});
labelEl.addEventListener('mouseup', () => {
inputEl.classList.remove('active');
});
}
}
}
}
updateFieldsFromDataset() {
for (const key of Object.keys(this.dataset)) {
const els = document.body.querySelectorAll(`[data-dataset-name="${key}"]`);
const value = this.dataset[key];
for (const el of els) {
if (el.nodeName === "INPUT") {
switch (el.type) {
case 'checkbox':
el.checked = value === true;
break;
case 'radio':
el.checked = el.value === value;
break;
case 'text':
case 'email':
default:
el.value = value;
break;
}
} else if (el.nodeName === "TEXTAREA") {
el.value = value;
}
}
}
this.updateDependentFields();
}
updateDependentFields() {
// Email field based on "send me calendar invites"
if (!this.dataset.receiveEmail) {
this.dataset.email = '';
}
for (const emailEl of document.querySelectorAll(`[data-dataset-name="email"]`)) {
emailEl.disabled = !this.dataset.receiveEmail;
emailEl.required = this.dataset.receiveEmail;
}
}
bindFields() {
const els = document.body.querySelectorAll(`[data-dataset-name]`);
for (const el of els) {
el.addEventListener('input', () => {
let value = null;
switch (el.type) {
case 'checkbox':
value = el.checked;
break;
case 'radio':
if (el.checked) {
value = el.value;
} else {
return;
}
break;
case 'text':
case 'email':
default:
value = el.value;
break;
}
console.log('Updated dataset:', el.dataset.datasetName, value);
this.dataset[el.dataset.datasetName] = value;
this.updateDependentFields();
});
}
const ttsEls = document.body.querySelectorAll(`[data-tts]`);
for (const ttsEl of ttsEls) {
ttsEl.addEventListener('focus', () => {
this.sayIt(ttsEl.dataset.tts);
});
}
}
restoreDataset(newDataset) {
return new Promise((resolve, reject) => {
this.dataset = {...this.dataset, ...newDataset};
this.updateFieldsFromDataset();
resolve();
});
}
panic(message) {
this.hideDialogs();
const messageBoxEl = document.querySelector('.dialog.dialog-message-box');
messageBoxEl.querySelector('.dialog-actual-body > p').innerText = message;
messageBoxEl.classList.remove('hide');
}
doLoad() {
const dataset = JSON.parse(document.querySelector('#load-data').innerText);
console.log('Recovered dataset', dataset);
this.restoreDataset(dataset).then(() => {
this.renderPhase();
this.showDialogForCurrentPhase();
}, (err) => {
console.error(err);
this.panic("Something went wrong trying to restore data from the server. Oops.");
});
}
startup() {
this.bindAudioToggles();
this.bindDialogButtons();
this.makeDialogsDraggable();
this.bindActive();
this.bindFields();
this.doLoad();
}
}
window.BARF = new BARF();
window.BARF.startup();
</script>
</body>
</html>