1 Commits

Author SHA1 Message Date
AlecM33
fbf8a9a1ae minor performance improvements per the lighthouse report 2024-11-12 20:20:21 -05:00
18 changed files with 904 additions and 199 deletions

View File

@@ -22,7 +22,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12V17" stroke="#e73333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 12V17" stroke="#e73333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 7H20" stroke="#e73333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 10V18C6 19.6569 7.34315 21 9 21H15C16.6569 21 18 19.6569 18 18V10" stroke="#e73333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V7H9V5Z" stroke="#e73333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 855 B

View File

@@ -10,17 +10,17 @@ export const HTMLFragments = {
<canvas id="canvas"></canvas>
<div id='game-parameters'>
<div>
<img alt='clock' width="20" height="20" src='/images/clock.svg'/>
<img alt='clock' src='/images/clock.svg'/>
<div id='timer-parameters'></div>
</div>
<div>
<img alt='person' width="22" height="20" src='/images/person.svg'/>
<img alt='person' src='/images/person.svg'/>
<div id='game-player-count'></div>
</div>
</div>
</div>
<div>
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' width="25" height="25" src='/images/info.svg'/></button>
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' src='/images/info.svg'/></button>
</div>
</div>
<div>
@@ -84,7 +84,7 @@ export const HTMLFragments = {
</div>
</div>
<div>
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' width="25" height="25" src='/images/info.svg'/></button>
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' src='/images/info.svg'/></button>
</div>
</div>
<div id='game-role' tabindex="0">
@@ -127,7 +127,7 @@ export const HTMLFragments = {
</div>
</div>
<div>
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' width="25" height="25" src='/images/info.svg'/></button>
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' src='/images/info.svg'/></button>
</div>
</div>
<div id='game-people-container'>
@@ -187,7 +187,7 @@ export const HTMLFragments = {
Transfer Mod Powers <img alt='transfer icon' src='/images/shuffle.svg'/>
</button>
<div>
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' width="25" height="25" src='/images/info.svg'/></button>
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' src='/images/info.svg'/></button>
</div>
</div>
<div id="game-players-container">
@@ -229,7 +229,7 @@ export const HTMLFragments = {
<div id='play-pause'> </div>
</div>
<div>
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' width="25" height="25" src='/images/info.svg'/></button>
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' src='/images/info.svg'/></button>
</div>
</div>
<div id='game-role' tabindex="0">
@@ -309,7 +309,7 @@ export const HTMLFragments = {
`<div id='end-of-game-header'>
<h2>&#x1F3C1; The moderator has ended the game. Roles are revealed.</h2>
<div id="end-of-game-buttons">
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' width="25" height="25" src='/images/info.svg'/></button>
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' src='/images/info.svg'/></button>
</div>
</div>
<div id='game-people-container'>
@@ -369,7 +369,7 @@ export const HTMLFragments = {
<img tabindex="0" class="role-include" src="../images/add.svg" title="add one" alt="add one"/>
<img tabindex="0" class="role-info" src="../images/info.svg" title="info" alt="info"/>
<img tabindex="0" class="role-edit" src="../images/pencil.svg" title="edit" alt="edit"/>
<img tabindex="0" class="role-remove" src="../images/delete-2.svg" title="remove" alt="remove"/>
<img tabindex="0" class="role-remove" src="../images/delete.svg" title="remove" alt="remove"/>
</div>`,
DECK_SELECT_ROLE_DEFAULT:
`<div class="role-name"></div>

View File

@@ -15,6 +15,7 @@ th, thead, tr, tt, u, ul, var {
@font-face {
font-family: 'signika-negative';
src: url("../webfonts/SignikaNegative-Light.woff2") format("woff2");
font-display: swap;
}
html {
@@ -68,131 +69,12 @@ textarea {
resize: none;
}
.toast-top {
top: 2rem;
}
.toast-bottom {
bottom: 140px;
}
.toast-success {
background-color: #bef5cb;
border: 3px solid #8ac78a;
}
.toast-warning {
background-color: #fff5b1;
border: 3px solid #c7c28a;
}
.toast-error {
background-color: #f98e9a;
border: 3px solid #c57272;
}
.toast-neutral {
background-color: #e9e9e9;
border: 3px solid #b7b7b7;
}
.toast-dispel-automatically {
animation: fade-in-slide-down-then-exit ease normal forwards;
}
.toast-not-dispelled-automatically {
animation: fade-in-slide-down ease normal forwards;
}
.toast-short {
animation-duration: 3s;
}
.toast-medium {
animation-duration: 5s;
}
.toast-long {
animation-duration: 8s;
}
.toast-plus-one {
color: #1c8a36;
font-weight: bold;
font-size: 20px;
margin-right: 5px;
}
.toast-minus-one {
font-size: 20px;
margin-right: 5px;
color: #e73333;
font-weight: bold;
}
.toast-plus-role-quantity {
color: #1c8a36;
font-weight: bold;
margin-left: 10px;
}
.toast-minus-role-quantity {
color: #e73333;
font-weight: bold;
margin-left: 10px;
}
#footer {
bottom: 0;
width: 100%;
text-align: center;
align-items: center;
display: flex;
justify-content: center;
flex-wrap: wrap;
color: #d7d7d7;
font-size: 14px;
margin-top: 4em;
margin-bottom: 0.5em;
}
#footer a:not([href='https://www.buymeacoffee.com/alecm33']) img {
width: 32px;
}
#footer a[href='https://www.buymeacoffee.com/alecm33'] img {
width: 200px;
}
#footer a {
color: #f7f7f7;
text-decoration: none;
cursor: pointer;
font-family: 'signika-negative', sans-serif;
margin: 0 0.25em;
}
#footer a:hover {
color: gray;
}
#footer div {
display: flex;
}
.teaser {
text-align:center;
color:gray;
margin-bottom:3em;
}
#footer > div, #footer > a {
margin: 0.5em;
}
#footer div:nth-child(2) > a, #footer div:nth-child(2) > p {
margin: 0 5px;
}
label {
color: #d7d7d7;
font-family: 'signika-negative', sans-serif;
@@ -299,6 +181,7 @@ button {
}
#game-parameters img {
height: 20px;
margin-right: 10px;
}

View File

@@ -454,9 +454,9 @@ input[type="number"] {
}
#role-select {
margin: 0.5em 0;
margin: 0.5em 1em 1.5em 0;
overflow-y: auto;
height: 22em;
height: 20em;
}
.default-role, .custom-role, .added-role {
@@ -492,8 +492,8 @@ input[type="number"] {
}
#role-select img, #deck-status-container img {
height: 25px;
margin: 0 10px;
height: 20px;
margin: 0 8px;
cursor: pointer;
padding: 5px;
border-radius: 5px;
@@ -505,7 +505,7 @@ input[type="number"] {
}
#role-select img:nth-child(4) {
height: 25px;
height: 18px;
}
#role-select img:hover, #deck-status-container img:hover {
@@ -683,7 +683,7 @@ input[type="number"] {
@media(max-width: 550px) {
#custom-roles-container, #deck-status-container {
min-width: 90%;
min-width: 85%;
}
h1 {
font-size: 32px;

View File

@@ -182,7 +182,7 @@
}
#timer-parameters {
width: fit-content;
width: 65px;
}
#role-edit-container-background, #timer-edit-container-background {
@@ -268,7 +268,6 @@ h1 {
border-radius: 5px;
padding: 7px;
margin: 0.5em;
text-align: center;
}
#end-of-game-header button {

View File

@@ -21,7 +21,7 @@ button#home-create-button {
}
.framed-phone-screenshot-container {
margin: 0 0 0 0;
margin: 0 0 20px 0;
border: 1px solid #464552;
}
@@ -95,7 +95,7 @@ a button {
#join-button:hover {
background-color: #326243;
border: 3px solid #1c8a36;
border: 2px solid #1c8a36;
}
#join-form div:nth-child(1) {
@@ -194,6 +194,5 @@ label[for="room-code"], label[for="player-name"] {
#about-container h2 {
font-size: 18px;
margin: 0 15px 20px 15px;
}
}

111
client/src/styles/toast.css Normal file
View File

@@ -0,0 +1,111 @@
.toast-top {
top: 2rem;
}
.toast-bottom {
bottom: 140px;
}
.toast-success {
background-color: #bef5cb;
border: 3px solid #8ac78a;
}
.toast-warning {
background-color: #fff5b1;
border: 3px solid #c7c28a;
}
.toast-error {
background-color: #f98e9a;
border: 3px solid #c57272;
}
.toast-neutral {
background-color: #e9e9e9;
border: 3px solid #b7b7b7;
}
.toast-dispel-automatically {
animation: fade-in-slide-down-then-exit ease normal forwards;
}
.toast-not-dispelled-automatically {
animation: fade-in-slide-down ease normal forwards;
}
.toast-short {
animation-duration: 3s;
}
.toast-medium {
animation-duration: 5s;
}
.toast-long {
animation-duration: 8s;
}
.toast-plus-one {
color: #1c8a36;
font-weight: bold;
font-size: 20px;
margin-right: 5px;
}
.toast-minus-one {
font-size: 20px;
margin-right: 5px;
color: #e73333;
font-weight: bold;
}
.toast-plus-role-quantity {
color: #1c8a36;
font-weight: bold;
margin-left: 10px;
}
.toast-minus-role-quantity {
color: #e73333;
font-weight: bold;
margin-left: 10px;
}
@keyframes fade-in-slide-down-then-exit {
0% {
opacity: 0;
transform: translateY(-20px);
}
5% {
opacity: 1;
transform: translateY(0px);
}
95% {
opacity: 1;
transform: translateY(0px);
}
100% {
opacity: 0;
transform: translateY(-20px);
}
}
@keyframes fade-in-slide-down {
0% {
opacity: 0;
transform: translateY(-20px);
}
5% {
opacity: 1;
transform: translateY(0px);
}
95% {
opacity: 1;
transform: translateY(0px);
}
100% {
opacity: 1;
transform: translateY(0px);
}
}

View File

@@ -18,6 +18,8 @@
<link rel="stylesheet" href="/styles/modal.css">
<link rel="stylesheet" href="/styles/hamburgers.css">
<link rel="stylesheet" href="/styles/404.css">
<link rel="preload" href="./styles/toast.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="./styles/toast.css"></noscript>
</head>
<body>
<div id="mobile-menu-background-overlay"></div>

View File

@@ -15,9 +15,13 @@
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" href="./styles/GLOBAL.css">
<link rel="stylesheet" href="./styles/create.css">
<link rel="stylesheet" href="./styles/modal.css">
<link rel="stylesheet" href="./styles/confirmation.css">
<link rel="preload" href="./styles/confirmation.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="./styles/confirmation.css"></noscript>
<link rel="preload" href="./styles/modal.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="./styles/modal.css"></noscript>
<link rel="stylesheet" href="/styles/hamburgers.css">
<link rel="preload" href="./styles/toast.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="./styles/toast.css"></noscript>
</head>
<body>
<div id="mobile-menu-background-overlay"></div>

View File

@@ -14,11 +14,14 @@
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" href="/styles/GLOBAL.css">
<link rel="stylesheet" href="/styles/game.css">
<link rel="stylesheet" href="/styles/create.css">
<link rel="stylesheet" href="/styles/modal.css">
<link rel="stylesheet" href="/styles/confirmation.css">
<link rel="stylesheet" href="/styles/hamburgers.css">
<link rel="preload" href="/webfonts/SignikaNegative-Light.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/styles/toast.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/toast.css"></noscript>
<link rel="preload" href="/styles/create.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/create.css"></noscript>
</head>
<body>
<script src="/dist/game-bundle.js.gz"></script>

View File

@@ -14,10 +14,461 @@
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" href="./styles/GLOBAL.css">
<link rel="stylesheet" href="./styles/home.css">
<link rel="stylesheet" href="/styles/hamburgers.css">
<link rel="stylesheet" href="./styles/confirmation.css">
<link rel="preload" href="./styles/toast.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="./styles/toast.css"></noscript>
<style>
canvas, caption, center, cite, code,
dd, del, dfn, div, dl, dt, em, embed,
fieldset, font, form, h1, h2, h3, h4,
h5, h6, hr, i, iframe, img, ins, kbd,
label, legend, li, menu, object, ol, p,
pre, q, s, samp, small, span, strike,
strong, sub, sup, table, tbody, td, tfoot,
th, thead, tr, tt, u, ul, var {
margin: 0;
padding: 0;
border: 0;
background: transparent;
}
@font-face {
font-family: 'signika-negative';
src: url("../webfonts/SignikaNegative-Light.woff2") format("woff2");
font-display: swap;
}
html {
font-family: 'signika-negative', sans-serif !important;
background-color: #0f0f10;
overflow: auto;
}
body {
display: flex;
flex-direction: column;
align-items: flex-start;
margin: 0 auto;
background-color: #0f0f10;
}
h2 {
color: #e7e7e7;
font-size: 25px;
font-weight: bold;
}
label {
color: #d7d7d7;
font-family: 'signika-negative', sans-serif;
font-size: 20px;
font-weight: normal;
}
input, textarea {
background-color: transparent;
border: 1px solid white;
border-radius: 5px;
color: #d7d7d7;
}
a {
text-decoration: none;
}
textarea, input {
font-family: 'signika-negative', sans-serif;
font-size: 16px;
}
button {
display: flex;
align-items: center;
justify-content: center;
}
.app-button, input[type="submit"] {
font-family: 'signika-negative', sans-serif !important;
padding: 10px;
background-color: #1a7a31;
border-radius: 5px;
color: #e7e7e7;
font-size: 18px;
cursor: pointer;
border: 3px solid transparent;
text-shadow: 0 3px 4px rgb(0 0 0 / 55%);
}
.app-button:active, input[type=submit]:active {
border: 3px solid #21ba45;
}
.submitted {
filter: opacity(0.5);
pointer-events: none;
}
.app-button:hover, input[type="submit"]:hover, #game-link:hover {
background-color: #326243;
border: 3px solid #1a7a31;
}
input {
padding: 10px;
}
.info-message {
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
position: fixed;
z-index: 1000000;
padding: 10px;
border-radius: 5px;
font-family: 'signika-negative', sans-serif;
font-weight: 100;
box-shadow:
0 1px 1px rgba(0,0,0,0.11),
0 2px 2px rgba(0,0,0,0.11),
0 4px 4px rgba(0,0,0,0.11),
0 8px 8px rgba(0,0,0,0.11),
0 16px 16px rgba(0,0,0,0.11),
0 32px 32px rgba(0,0,0,0.11);
left: 0;
right: 0;
width: fit-content;
max-width: 80%;
min-width: 15em;
font-size: 20px;
margin: 0 auto;
}
#navbar {
display: flex;
align-items: center;
padding: 10px 0;
width: 100%;
background-color: #0f0f10;
height: 45px;
z-index: 53000;
}
#desktop-links > a:nth-child(1) {
margin: 0 0.5em;
width: 50px;
}
#mobile-links a:nth-child(1) {
width: 80px;
margin: 0 auto 3em auto !important;
display: flex;
}
.logo {
display: flex;
align-items: center;
}
#navbar img {
width: 100%;
}
#navbar a:not(.logo) {
color: #f7f7f7;
text-decoration: none;
cursor: pointer;
font-family: 'signika-negative', sans-serif;
border-radius: 5px;
padding: 2px 5px;
font-size: 20px;
margin: 1em;
width: fit-content;
}
#navbar a:hover {
color: gray;
}
.overlay {
position: fixed;
background-size: cover;
height: 100%;
opacity: 75%;
background-color: black;
width: 100%;
z-index: 50000;
}
.hidden {
display: none !important;
}
#mobile-links {
display: flex;
flex-direction: column;
margin-top: 3em;
}
.mobile-link {
margin-top: 1em !important;
margin-left: 2em !important;
}
@media(max-width: 1000px) {
#navbar {
display: flex;
padding: 0;
}
#navbar-hamburger {
z-index: 52000;
position: relative;
}
#desktop-menu {
display: none;
}
#mobile-menu {
position: absolute;
top: 0;
left: 0;
height: auto;
padding-bottom: 2em;
width: 100%;
z-index: 51000;
background-color: #1e1e1e;
}
}
@media(min-width: 1001px) {
#navbar-hamburger, #mobile-menu, #mobile-menu-background-overlay {
display: none;
}
.desktop-link {
display: flex;
}
#desktop-links {
display: flex;
align-items: center;
}
}
@media(max-width: 550px) {
h1 {
font-size: 30px;
}
#step-1 div {
font-size: 20px;
}
.info-message {
padding: 5px;
font-size: 16px;
}
}
body {
align-items: center;
}
button#home-create-button {
padding: 20px;
}
#framed-phone-screenshot, #framed-phone-screenshot-2, .framed-phone-screenshot-container {
max-width: 250px;
width: 40vw;
min-width: 175px;
border-radius: 21px;
aspect-ratio: 1522 / 3290;
}
.framed-phone-screenshot-container {
margin: 0 0 20px 0;
border: 1px solid #464552;
}
#about-container {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: center;
width: 100%;
}
#about-container > div {
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
flex-wrap: wrap;
padding: 25px 0;
}
#join-container form {
margin: 0;
}
#about-container h2 {
max-width: 17em;
font-size: 22px;
border-left: 1px solid #bababa;
padding: 15px;
margin: 0 15px;
}
#about-container div:nth-child(2) h2 {
margin-bottom: 2em;
}
#homepage-logos {
display: flex;
align-items: center;
}
#home-page-top-section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
background-color: #0f0f10;
}
form {
display: flex;
flex-wrap: wrap;
margin: 10px 0;
border-radius: 5px;
justify-content: center;
align-items: center;
}
a button {
text-shadow: 0 3px 4px rgb(0 0 0 / 85%);
}
#join-button {
min-width: 6em;
max-height: 3em;
background-color: #1c8a36;
color: #e7e7e7;
font-size: 16px;
}
#join-button:hover {
background-color: #326243;
border: 2px solid #1c8a36;
}
#join-form div:nth-child(1) {
margin-right: 0.5em;
}
h3 {
color: #d7d7d7;
font-weight: normal;
font-size: 20px;
margin-bottom: 1em;
padding: 1em;
max-width: 23em;
font-family: 'signika-negative', sans-serif;
}
img[src='../images/new-logo.png'], #new-logo-container {
max-width: 250px;
width: 25vw;
min-width: 150px;
margin: 1em 0 1em 0;
aspect-ratio: 500 / 641;
}
h3 a {
color: #768df0;
text-decoration: underline;
cursor: pointer;
font-family: 'signika-negative', sans-serif;
width: fit-content;
}
h3 a:hover {
color: gray;
}
form > div {
margin: 15px 0;
}
#create-join {
width: 100%;
max-width: 900px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-evenly;
margin-bottom: 1em;
}
#join-container {
max-width: 90%;
border: 1px solid #55555599;
background: #5555555c;
padding: 0.5em;
border-radius: 5px;
margin: 20px
}
#join-container > label {
font-size: 35px;
font-family: 'signika-negative', sans-serif;
color: #e7e7e7;
filter: drop-shadow(2px 2px 4px black);
}
#room-code {
background-color: #1e1e1e;
}
label[for="room-code"], label[for="player-name"] {
margin-right: 0.5em;
}
@media (min-width: 700px) {
button#home-create-button {
font-size: 30px;
}
}
@media (max-width: 701px) {
button#home-create-button {
font-size: 5vw;
padding: 15px;
}
img[src='../images/new-logo.png'], #new-logo-container {
margin: 1em 0 0 0;
}
#join-container > label {
font-size: 26px;
}
#room-code {
max-width: 9em;
}
#about-container h2 {
font-size: 18px;
}
}
</style>
</head>
<body>
<div id="mobile-menu-background-overlay"></div>
@@ -45,16 +496,16 @@
</div>
<div id="about-container">
<div>
<h2>Join a game and have a role dealt to your device.</h2>
<div class="framed-phone-screenshot-container">
<img id="framed-phone-screenshot" alt="framed phone screenshot" src="../images/framed-phone-screenshot_resized.webp"/>
</div>
<h2>Join a game and have a role dealt to your device.</h2>
</div>
<div>
<h2>Create your own game with default or custom roles.</h2>
<div class="framed-phone-screenshot-container">
<img id="framed-phone-screenshot-2" alt="framed phone screenshot" src="../images/framed-phone-screenshot-2_resized.webp"/>
</div>
<h2>Create your own game with default or custom roles.</h2>
</div>
</div>
<script src="/dist/home-bundle.js.gz"></script>

View File

@@ -37,8 +37,10 @@
</ul>
</div>
<h1 class="how-to-use-header" id="purpose-of-the-app">Purpose</h1>
<div class="how-to-use-section">This app helps a group play Werewolf when meeting virtually, or when it's simply difficult to
play with physical cards. Set up games with any number of roles and deal them to everyone's device. It's flexible, free,
<div class="how-to-use-section">This app helps a group play Werewolf when meeting virtually, or when you don't have
Werewolf cards with you. In general, the app can be very efficient for setting up games of any size and configuration.
Once everyone has joined the room, cards can be quickly dealt and re-dealt, and important information such as
role descriptions, time remaining, and the status of players is conveniently centralized. It's flexible, free,
and doesn't require installing an app or creating an account. If you use this app, I'd love to hear about it. I
am always looking for ways to improve this tool.
</div>
@@ -48,23 +50,27 @@
<br>
<h3>- Step One: Choosing a method of moderation</h3>
<br>
Choose either the <span class="emphasized">Dedicated
Moderator</span> option or the <span class="emphasized">Temporary Moderator</span> option. Dedicated moderators
are not dealt into the game, while temporary moderators are. Both will have moderator powers, but temporary moderators
will not know anyone's role. When they first remove someone from the game, their moderator powers will be transferred to that person,
who will become a dedicated moderator.
You have two options for moderation during the game. If the moderator isn't playing, you can choose the <span class="emphasized">Dedicated
Moderator</span> option. Dedicated Moderators are not dealt into the game. Once they start the game, they will know
everyone's role. During the game, they can kill players, reveal players' roles, transfer their
moderator powers, play/pause the timer (if there is one), end the game (revealing everyone's role), or return the game to the lobby.
<br><br>
Dedicated moderators can transfer their moderator powers to any player that has been eliminated,
or to a spectator. That way, if the current moderator has to leave, or simply does not want to moderate
Similarly, you can also choose the <span class="emphasized">Temporary Moderator</span> option. You are dealt a role,
and you have the same powers as the Dedicated Moderator, except game knowledge - you know the same
information that a regular player does. When you remove the first player from the game (which can be yourself),
they will automatically become the dedicated moderator.
<br><br>
<span class="emphasized">Dedicated Moderators</span> can transfer their moderator powers to a player that is out,
or to a spectator. That way, if the current Dedicated Moderator has to leave, or simply does not want to moderate
anymore, they can easily delegate.
<br><br>
<h3>- Step Two: Build your deck</h3>
<br>
There is a <span class="emphasized">Role Box</span> on this page that includes a list of <span class="emphasized">Default Roles</span> and a list
There is a role box on this page that includes a list of <span class="emphasized">Default Roles</span> and a list
of <span class="emphasized">Custom Roles</span>, which can be displayed by selecting the appropriate button within the box.
If you want to add a certain role to the game, click the green plus, and one copy
of it will be added to the <span class="emphasized">Deck Box</span>, which displays the added roles and the corresponding player count.
Likewise, if you want to remove one copy of a given role, click the red minus on the role
If you want to add a certain role to the game, click the <span class="emphasized">green plus</span> and one copy
of it will be added to the <span class="emphasized">Deck</span> which is the other box displaying a player count.
Likewise, if you want to remove one copy of a given role, click the <span class="emphasized">red minus</span> on the role
in the Deck Box.
<br><br>
Here I add 3 villagers to the game, and then remove them:
@@ -89,13 +95,13 @@
<h3>- In the Lobby</h3>
<br>
In the Lobby, moderators can manage the people in the room and the cards in the game. By clicking
the three vertical dots (AKA the "kebab menu") next to a given player (<span class="emphasized">point A</span>
the <span class="emphasized">three vertical dots (AKA the "kebab menu")</span> next to a given player (<span class="emphasized">point A</span>
in the screenshot below), you have the option to kick that player. You can do the same with a spectator by first
viewing the spectator list (<span class="emphasized">point C</span>) and clicking their kebab menus. By clicking the
"Edit Roles" button (<span class="emphasized">point B</span>), you can change which roles are in the game and the
quantities of those roles. This button will bring up the same module you encountered when you first created the room.
Saving any changes to the roles may affect the player count. If you wish to start the game <span class="emphasized">(point B)</span>, the number
of players in the lobby must equal the number of cards in the game.
Saving any changes to the roles may affect the player count. If you wish to <span class="emphasized">start the game (point B)</span>, the number
of Players in the Lobby must equal the number of cards in the game.
<br><br>
<img alt="moderator view in the lobby" class='tutorial-image-small-portrait' src="../images/tutorial/dedicated-mod-lobby-mobile.webp"/>
<br><br>
@@ -107,15 +113,15 @@
but not the "Reveal" option. Or, if you happen to have a role that reveals but is not immediately removed from the game,
you can use the "Reveal" option but not the "Kill" option. You of course don't have to utilize either of these options.
If you just want to use the app to deal cards, you are free to do that. The moderator also has permission to
play and pause the timer (<span class="emphasized">Point B</span>), and can end the game (revealing everyone's role)
or return the game to the lobby (<span class="emphasized">Point C</span>), where it can be started again with different settings.
play and pause the Timer (<span class="emphasized">Point B</span>), and can end the game (revealing everyone's role)
or return the game to the Lobby (<span class="emphasized">Point C</span>), where it can be started anew with different settings.
<br><br>
<img alt="moderator view during the game" class='tutorial-image-small-portrait' src="../images/tutorial/dedicated-mod-in-progress-mobile.webp"/>
<br><br>
Similarly, the <span class="emphasized">Temporary Moderator</span> view looks like the below image. They have
Similarly, the <span class="emphasized">Temporary Moderator view</span> looks like the below image. They have
much the same abilities as a dedicated moderator, except they don't know role or alignment information and cannot
transfer their powers. Their powers will be transferred automatically to the first person they remove from the game
(which can be themselves!).
(which can be themselves).
<br><br>
<img alt="temporary moderator view during the game" class='tutorial-image-small-portrait' src="../images/tutorial/temp-mod-in-progress-mobile.webp"/>
<br><br>
@@ -130,7 +136,7 @@
<h1 class="how-to-use-header" id="being-a-player">Being a Player</h1>
<div class="how-to-use-section">
This is an example of what a <span class="emphasized">Player</span> is seeing, including the running timer,
their role card, and the player list. You can also edit your name for the room by clicking the pencil next to it.
their role card, and the player list. You can also edit your name for the Room by clicking the pencil next to it.
Below, we flip our role card up and down by double-clicking it, and then we bring up the prompt to edit our name:
<br><br>
<img alt='player-view' class='tutorial-image-small-portrait' src="../images/tutorial/player-view.gif"/>
@@ -138,8 +144,8 @@
Players can view the timer, but only the current moderator can play and pause it. Your role card starts flipped over
- this is useful if you are in-person and don't want someone else accidentally seeing your role as
it is dealt. You can view your role at any time by double-clicking/double-tapping it. Requiring a double-click guards against the possibility
of accidentally flipping your role when tapping other things. Within the player list, you can see who is alive or
dead and who has had their role revealed. There is also a role info button that,
of accidentally flipping your role when tapping other things. Within the Player List, you can see who is alive or
dead and who has had their role revealed. There is also a <span class="emphasized">role info button</span> that,
when pressed, displays all the different roles in the current game, including their descriptions and alignment (Good/Evil).
<br><br>
</div>

View File

@@ -12,11 +12,274 @@
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" href="/styles/GLOBAL.css">
<link rel="stylesheet" href="/styles/join.css">
<link rel="stylesheet" href="/styles/modal.css">
<link rel="stylesheet" href="/styles/hamburgers.css">
<link rel="stylesheet" href="/styles/modal.css">
<link rel="preload" href="/styles/toast.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="./styles/toast.css"></noscript>
<link rel="preload" href="/webfonts/SignikaNegative-Light.woff2" as="font" type="font/woff2" crossorigin>
<style>
canvas, caption, center, cite, code,
dd, del, dfn, div, dl, dt, em, embed,
fieldset, font, form, h1, h2, h3, h4,
h5, h6, hr, i, iframe, img, ins, kbd,
label, legend, li, menu, object, ol, p,
pre, q, s, samp, small, span, strike,
strong, sub, sup, table, tbody, td, tfoot,
th, thead, tr, tt, u, ul, var {
margin: 0;
padding: 0;
border: 0;
background: transparent;
}
@font-face {
font-family: 'signika-negative';
src: url("../webfonts/SignikaNegative-Light.woff2") format("woff2");
font-display: swap;
}
html {
font-family: 'signika-negative', sans-serif !important;
background-color: #0f0f10;
overflow: auto;
}
body {
display: flex;
flex-direction: column;
align-items: flex-start;
margin: 0 auto;
background-color: #0f0f10;
}
label {
color: #d7d7d7;
font-family: 'signika-negative', sans-serif;
font-size: 20px;
font-weight: normal;
}
input, textarea {
background-color: transparent;
border: 1px solid white;
border-radius: 5px;
color: #d7d7d7;
}
a {
text-decoration: none;
}
textarea, input {
font-family: 'signika-negative', sans-serif;
font-size: 16px;
}
button {
display: flex;
align-items: center;
justify-content: center;
}
.app-button, input[type="submit"] {
font-family: 'signika-negative', sans-serif !important;
padding: 10px;
background-color: #1a7a31;
border-radius: 5px;
color: #e7e7e7;
font-size: 18px;
cursor: pointer;
border: 3px solid transparent;
text-shadow: 0 3px 4px rgb(0 0 0 / 55%);
}
.app-button:active, input[type=submit]:active {
border: 3px solid #21ba45;
}
.submitted {
filter: opacity(0.5);
pointer-events: none;
}
.app-button:hover, input[type="submit"]:hover, #game-link:hover {
background-color: #326243;
border: 3px solid #1a7a31;
}
#game-parameters {
font-family: signika-negative, sans-serif;
color: #d7d7d7;
font-size: 20px;
}
#game-parameters > div {
display: flex;
align-items: center;
font-size: 25px;
}
input {
padding: 10px;
}
.info-message {
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
position: fixed;
z-index: 1000000;
padding: 10px;
border-radius: 5px;
font-family: 'signika-negative', sans-serif;
font-weight: 100;
box-shadow:
0 1px 1px rgba(0,0,0,0.11),
0 2px 2px rgba(0,0,0,0.11),
0 4px 4px rgba(0,0,0,0.11),
0 8px 8px rgba(0,0,0,0.11),
0 16px 16px rgba(0,0,0,0.11),
0 32px 32px rgba(0,0,0,0.11);
left: 0;
right: 0;
width: fit-content;
max-width: 80%;
min-width: 15em;
font-size: 20px;
margin: 0 auto;
}
#navbar {
display: flex;
align-items: center;
padding: 10px 0;
width: 100%;
background-color: #0f0f10;
height: 45px;
z-index: 53000;
}
#desktop-links > a:nth-child(1) {
margin: 0 0.5em;
width: 50px;
}
#mobile-links a:nth-child(1) {
width: 80px;
margin: 0 auto 3em auto !important;
display: flex;
}
.logo {
display: flex;
align-items: center;
}
#navbar img {
width: 100%;
}
#navbar a:not(.logo) {
color: #f7f7f7;
text-decoration: none;
cursor: pointer;
font-family: 'signika-negative', sans-serif;
border-radius: 5px;
padding: 2px 5px;
font-size: 20px;
margin: 1em;
width: fit-content;
}
#navbar a:hover {
color: gray;
}
.overlay {
position: fixed;
background-size: cover;
height: 100%;
opacity: 75%;
background-color: black;
width: 100%;
z-index: 50000;
}
.hidden {
display: none !important;
}
#mobile-links {
display: flex;
flex-direction: column;
margin-top: 3em;
}
.mobile-link {
margin-top: 1em !important;
margin-left: 2em !important;
}
@media(max-width: 1000px) {
#navbar {
display: flex;
padding: 0;
}
#navbar-hamburger {
z-index: 52000;
position: relative;
}
#desktop-menu {
display: none;
}
#mobile-menu {
position: absolute;
top: 0;
left: 0;
height: auto;
padding-bottom: 2em;
width: 100%;
z-index: 51000;
background-color: #1e1e1e;
}
}
@media(min-width: 1001px) {
#navbar-hamburger, #mobile-menu, #mobile-menu-background-overlay {
display: none;
}
.desktop-link {
display: flex;
}
#desktop-links {
display: flex;
align-items: center;
}
}
@media(max-width: 550px) {
h1 {
font-size: 30px;
}
#step-1 div {
font-size: 20px;
}
.info-message {
padding: 5px;
font-size: 16px;
}
}
</style>
</head>
<body>
<div id="mobile-menu-background-overlay"></div>

24
package-lock.json generated
View File

@@ -13,7 +13,7 @@
"body-parser": "^1.20.3",
"compression-webpack-plugin": "^10.0.0",
"cors": "^2.8.5",
"express": "^4.21.2",
"express": "^4.21.1",
"express-force-https": "^1.0.0",
"express-rate-limit": "^6.0.1",
"open": "^7.0.3",
@@ -47,7 +47,7 @@
"webpack-remove-debug": "^0.1.0"
},
"engines": {
"node": ">= 20.0.0"
"node": ">= 14.0.0"
}
},
"node_modules/@ampproject/remapping": {
@@ -3934,10 +3934,9 @@
}
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -3958,7 +3957,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -3973,10 +3972,6 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express-force-https": {
@@ -5862,10 +5857,9 @@
"dev": true
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
},
"node_modules/picocolors": {
"version": "1.1.1",

View File

@@ -30,7 +30,7 @@
"body-parser": "^1.20.3",
"compression-webpack-plugin": "^10.0.0",
"cors": "^2.8.5",
"express": "^4.21.2",
"express": "^4.21.1",
"express-force-https": "^1.0.0",
"express-rate-limit": "^6.0.1",
"open": "^7.0.3",

View File

@@ -83,11 +83,9 @@ router.patch('/:code/players', async function (req, res) {
res.status(200).send({ cookie: data, environment: gameManager.environment });
}).catch((data) => {
console.error(data);
res.set('content-type', 'text/plain');
res.status(data.status || 500).send(data.reason);
});
} else {
res.set('content-type', 'text/plain');
res.status(404).send();
}
}

View File

@@ -95,7 +95,7 @@ const ServerBootstrapper = {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; font-src 'self' https://fonts.gstatic.com/; img-src 'self' https://img.buymeacoffee.com;" +
" script-src 'self'; style-src 'self' https://fonts.googleapis.com/ 'nonce-" + nonce + "'; frame-src 'self'"
" script-src 'self' https://cdnjs.buymeacoffee.com; style-src 'self' https://cdnjs.buymeacoffee.com https://fonts.googleapis.com/ 'nonce-" + nonce + "'; frame-src 'self'"
);
next();
});