571 lines
18 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Welcome to Firebase Hosting</title>
<!-- update the version number as needed -->
<script defer src="/__/firebase/9.12.1/firebase-app-compat.js"></script>
<!-- include only the Firebase features as you need -->
<script defer src="/__/firebase/9.12.1/firebase-auth-compat.js"></script>
<script defer src="/__/firebase/9.12.1/firebase-database-compat.js"></script>
<script defer src="/__/firebase/9.12.1/firebase-firestore-compat.js"></script>
<script defer src="/__/firebase/9.12.1/firebase-functions-compat.js"></script>
<script defer src="/__/firebase/9.12.1/firebase-messaging-compat.js"></script>
<script defer src="/__/firebase/9.12.1/firebase-storage-compat.js"></script>
<script defer src="/__/firebase/9.12.1/firebase-analytics-compat.js"></script>
<script defer src="/__/firebase/9.12.1/firebase-remote-config-compat.js"></script>
<script defer src="/__/firebase/9.12.1/firebase-performance-compat.js"></script>
<!-- Include tmi.js Twitch IRC bot for chat integration -->
<script defer src="https://github.com/tmijs/tmi.js/releases/download/v1.8.5/tmi.js"></script>
<!--
initialize the SDK after all desired features are loaded, set useEmulator to false
to avoid connecting the SDK to running emulators.
-->
<script defer src="/__/firebase/init.js?useEmulator=true"></script>
<style media="screen">
:root {
--ruleColor: #999;
--titleColor: #333;
--textColor: #444;
--infoColor: #666;
--vote1: #444;
--vote2: #888;
--vote3: #ccc;
--vote4: #eee;
}
h1, h2, h3, h4 {
color: var(--titleColor);
}
#prompt {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 8px 16px;
text-align: center;
font-size: 14px;
z-index: 100;
height: 35%;
display: flex;
flex-direction: row;
background-color: #ffffff;
/* margin-bottom: 5%; */ /* TODO: Remove */
/*
background: #039be5;
color: white;
*/
}
/* Twitch chat */
#chat {
display: flex;
width: 20%;
margin: 8px;
flex-direction: column;
/* background: #ffa100; */
}
#chattitle {
align-self: center;
margin-bottom: 1px;
margin-top: 10px;
}
#chatcontent {
display: flex;
flex-direction: column;
flex: 1;
padding: 8px 4px;
list-style: none;
align-items: flex-start;
margin-top: 1px;
/*-ms-overflow-style: none; /* IE and Edge */
/*scrollbar-width: none; /* Firefox */
}
/*
#chatcontent::--webkit-scrollbar {
display: none;
visibility: hidden;
}
*/
#chatentry {
display: flex;
flex-direction: row;
align-items: center;
margin: 1px;
}
#chatusername {
font-weight: bold;
flex: 1;
margin-right: auto;
margin-bottom: auto;
}
#chatmessage {
margin-left: 8px;
flex: 5;
}
hr.solid {
border-top: 3px solid var(--ruleColor);
border-radius: 5px;
margin-left: 5px;
margin-right: 5px;
}
/* Main vote info section */
#voteinfo {
display: flex;
width: 80%;
flex-direction: column;
align-items: flex-start;
padding: 8px 16px;
margin: 8px;
/* background: #123123; */
}
#novote {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
width: 80%;
padding: 8px 16px;
margin: 8px;
}
#voteprompt {
margin-bottom: 1px;
margin-top: 2px;
}
#votes {
list-style: none;
display: flex;
flex-direction: row;
align-items: center;
padding: 0px;
width: 100%;
height: 35px;
margin: 5px 0px;
overflow: hidden;
/* background-color: #ffa100; */
}
#voteextrainfo {
height: 100%;
align-self: stretch;
margin: 0px;
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: stretch;
align-content: stretch;
}
#votedetails {
display: flex;
flex-direction: column;
width: 80%;
margin-right: 8px;
/* background-color: #ffa100; */
}
#votehelp {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-left: 8px;
width: 20%;
/* background-color: #ffa100; */
}
.posttitle {
align-self: flex-start;
margin-bottom: 1px;
margin-top: 5px;
}
#postdescription {
font-size: 16px;
text-align: start;
color: var(--textColor);
}
#tipbox {
display: flex;
flex-direction: row;
align-items: baseline;
margin-top: auto;
justify-content: flex-start;
width: 100%;
}
.verticalrule {
border: 1px solid var(--ruleColor);
width: 0px;
height: auto;
outline: none;
}
#optsandinfo {
display: flex;
flex-direction: column;
justify-content: space-evenly;
margin-top: auto;
width: 100%;
}
#votes > li {
padding: 8px 0px;
-webkit-transition-property: width;
-moz-transition-property: width;
-o-transition-property: width;
transition-property: width;
-webkit-transition-timing-function: ease-in-out;
-moz-transition-timing-function: ease-in-out;
-o-transition-timing-function: ease-in-out;
transition-timing-function: ease-in-out;
white-space: nowrap;
overflow-x: hidden;
}
#voteopts {
flex-wrap: wrap;
display: flex;
list-style: none;
align-items: stretch;
justify-content: space-evenly;
padding: 0px;
}
#voteopts > li {
flex: 1 0 35%;
display: flex;
align-items: center;
justify-content: center;
margin: 1%;
border-color: black;
border-style: solid;
border-width: 2px;
}
#voteopts > li > strong {
width: auto;
margin: 5px 0px;
padding: 2px 0px;
color: var(--titleColor);
}
/* General styles */
.outlinebox {
outline-color: black;
outline-style: solid;
outline-width: 2px;
}
.hidden {
display: none !important;
}
</style>
</head>
<body>
<!--
<div id="message">
<h2>Welcome</h2>
<h1>Firebase Hosting Setup Complete</h1>
<p>You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!</p>
<a target="_blank" href="https://firebase.google.com/docs/hosting/">Open Hosting Documentation</a>
</div>
<p id="load">Firebase SDK Loading&hellip;</p>
-->
<div id="prompt" class="outlinebox">
<div id="chat" class="outlinebox">
<h2 id="chattitle">JURY NOTES</h2>
<hr class="solid">
<ul id="chatcontent"></ul>
</div>
<div id="voteinfo" class="outlinebox">
<h1 id="voteprompt">Is OP the asshole?</h1>
<ul id="votes" class="outlinebox">
<!--<li style="width: 60%; background-color: var(--vote1);">/Yes (60%)</li>
<li style="width: 40%; background-color: var(--vote4);">/No (40%)</li>-->
</ul>
<div id="voteextrainfo">
<div id="votedetails">
<h2 class="posttitle">Post title</h2>
<p id="postdescription">My friends and I want to watch movies that our traumatized friend may find triggering - Would I be the asshole to just not invite her anymore?</p>
<div id="tipbox">
<h3 style="padding-right: 5px; margin-bottom: 0px;">TIP:</h3>
<p style="margin-bottom: 0px;">Get transcript link with the /source command in chat!</p>
</div>
</div>
<hr class="verticalrule">
<div id="votehelp">
<h3 class="posttitle">VOTE NOW!</h3>
<p style="text-align: start; color: var(--infoColor);">Vote by typing one of the following options in chat.</p>
<div id="optsandinfo">
<ul id="voteopts">
<!--
<li><strong>/Yes</strong></li>
<li><strong>/No</strong></li>
-->
</ul>
<p style="margin: 0px; align-self: center; color: var(--infoColor);">23 votes registered</p>
</div>
</div>
</div>
</div>
<div id="novote" class="outlinebox hidden">
<h1>There is currently no vote going on!</h1>
</div>
</div>
<script>
// Settings
const voteUpdateInterval = 1000;
const maxBarAnimateTime = 2; // Seconds
const optionsField = "options";
const activeField = "active";
function runTwitchAuth(token, secret, id, callback) {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 200) {
const authObj = JSON.parse(xhr.responseText);
if (authObj && authObj.access_token) {
callback(authObj.access_token);
} else {
// Probably my fault?
console.error("Error getting access token");
}
} else {
// Definitely not my fault
console.log("Error: " + xhr.status);
}
};
xhr.open("POST", "https://id.twitch.tv/oauth2/token");
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(`grant_type=refresh_token&refresh_token=${token}&client_secret=${secret}&client_id=${id}`);
}
function makeChatEntry(username, message) {
const entry = document.createElement("li");
entry.id = "chatentry";
const user = document.createElement("div");
const msg = document.createElement("div");
user.id = "chatusername";
user.innerText = username + ":";
msg.id = "chatmessage";
msg.innerText = message;
entry.appendChild(user);
entry.appendChild(msg);
return entry;
}
const urlParams = new URLSearchParams(window.location.search);
const username = urlParams.get('username');
const token = urlParams.get('token');
const secret = urlParams.get('secret');
const id = urlParams.get('id');
const channel = urlParams.get('channel');
let activeVoteData = undefined; // Holds current vote info
document.addEventListener('DOMContentLoaded', function() {
const chatWindow = document.getElementById("chatcontent");
const activeVote = firebase.functions().httpsCallable('activeVote');
const castVote = firebase.functions().httpsCallable('castVote');
const { Client } = tmi;
runTwitchAuth(token, secret, id, (auth) => {
const tmiClient = new Client({
identity: {
username,
password: `oauth:${auth}`
},
channels: [
channel
]
});
tmiClient.on('message', (channel, userstate, msg, self) => {
if (self) return; // Ignore messages from the bot
// Remove whitespace from chat message
const commandName = msg.trim();
console.log(channel, userstate, msg, self);
const resolvedData = activeVoteData
const username = userstate.username;
if (resolvedData && username) {
let voted = false;
resolvedData[optionsField].forEach((option, index) => {
if (commandName === option) {
castVote({voteId: resolvedData.voteId, voter: username, voteIndex: index});
voted = true;
}
});
if (!voted) chatWindow.appendChild(makeChatEntry(username, msg))
}
});
tmiClient.on('connected', (address, port) => {
console.log(`* Connected to ${address}:${port}`);
});
tmiClient.connect();
});
//const loadEl = document.querySelector('#load');
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
// // The Firebase SDK is initialized and available here!
//
// firebase.auth().onAuthStateChanged(user => { });
// firebase.database().ref('/path/to/ref').on('value', snapshot => { });
// firebase.firestore().doc('/foo/bar').get().then(() => { });
// firebase.functions().httpsCallable('yourFunction')().then(() => { });
// firebase.messaging().requestPermission().then(() => { });
// firebase.storage().ref('/path/to/ref').getDownloadURL().then(() => { });
// firebase.analytics(); // call to activate
// firebase.analytics().logEvent('tutorial_completed');
// firebase.performance(); // call to activate
//
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
function updateVoteUI(fullRefresh) {
if (activeVoteData === undefined) {
document.getElementById("voteinfo").classList.add("hidden");
document.getElementById("novote").classList.remove("hidden");
return;
}
document.getElementById("novote").classList.add("hidden");
document.getElementById("voteinfo").classList.remove("hidden");
if (fullRefresh) {
document.getElementById("voteprompt").innerText = activeVoteData.prompt;
document.getElementById("postdescription").innerText = activeVoteData.description;
}
let totalVotes = 0;
for (let i = 0; i < activeVoteData.votes.length; i++) {
totalVotes += activeVoteData.votes[i];
}
const voteOpts = document.getElementById("voteopts");
const votes = document.getElementById("votes");
if (fullRefresh) {
// Remove all existing vote options
while (voteOpts.firstChild) {
voteOpts.removeChild(voteOpts.firstChild);
}
for (let i = 0; i < activeVoteData.options.length; i++) {
const newOpt = document.createElement("li");
const newOptText = document.createElement("strong");
newOptText.innerText = activeVoteData.options[i];
newOpt.appendChild(newOptText);
voteOpts.appendChild(newOpt);
}
while (votes.firstChild) {
votes.removeChild(votes.firstChild);
}
for (let i = 0; i < activeVoteData.options.length; i++) {
const newVote = document.createElement("li");
newVote.style.width = totalVotes == 0 ? `${100/activeVoteData.options.length}%` : `${(activeVoteData.votes[i] / totalVotes * 100)}%`;
newVote.style.backgroundColor = `var(--vote${i + 1})`;
newVote.innerHTML = activeVoteData.options[i] + " (" + (totalVotes == 0 ? `${+(100/activeVoteData.options.length).toFixed(2)}` : +(activeVoteData.votes[i] / totalVotes * 100).toFixed(2)) + "%)";
votes.appendChild(newVote);
}
} else {
const voteOptsChildren = voteOpts.children;
for (let i = 0; i < voteOptsChildren.length; i++) {
const voteOpt = voteOptsChildren[i];
const voteOptText = voteOpt.children[0];
voteOptText.innerText = activeVoteData.options[i];
}
// Update votes list
const votesChildren = votes.children;
let maxTime = totalVotes == 0 ? 0 : maxBarAnimateTime;
for (let i = 0; i < votesChildren.length; i++) {
const changePC = Math.abs(
votesChildren[i]
.style
.width
.substring(0, votesChildren[i].style.width.length - 1) -
(totalVotes == 0 ?
1 / activeVoteData.options.length :
activeVoteData.votes[i] / totalVotes
) * 100
);
if (changePC > maxTime) {
maxTime = changePC;
}
}
maxTime /= 100;
console.log(maxTime);
for (let i = 0; i < votesChildren.length; i++) {
const vote = votesChildren[i];
const changePC = totalVotes == 0 ? 1 / activeVoteData.options.length : activeVoteData.votes[i] / totalVotes;
const change = changePC * 100;
vote.innerHTML = activeVoteData.options[i] + " (" + (+change.toFixed(2)) + "%)";
vote.style.width = change + "%";
vote.style.transitionDuration = (maxBarAnimateTime * maxTime) + "s";
}
}
document.getElementById("optsandinfo").children[1].innerText = totalVotes + " votes registered";
}
function updateVote() {
activeVote().then((data) => {
if (data) {
if (!data.data) {
activeVoteData = undefined;
updateVoteUI(true);
} else if (!activeVoteData || activeVoteData.voteId !== data.data.voteId) {
activeVoteData = data.data;
console.log("Full update");
updateVoteUI(true);
} else {
activeVoteData = data.data;
console.log("Partial update");
updateVoteUI(false);
}
}
});
}
updateVoteUI(true);
updateVote();
firebase.firestore().collection('state').onSnapshot(updateVote);
});
</script>
</body>
</html>