Open Source commit

This commit is contained in:
gabe farrell 2022-06-26 01:22:08 -04:00
commit 7ad332ad02
36 changed files with 22430 additions and 0 deletions

23
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
frontend/README.md Normal file
View file

@ -0,0 +1,24 @@
# massflip
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
frontend/babel.config.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

19
frontend/jsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

20137
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

46
frontend/package.json Normal file
View file

@ -0,0 +1,46 @@
{
"name": "massflip",
"version": "0.0.2",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"dotenv": "^16.0.1",
"pinia": "^2.0.0-rc.10",
"vue": "^3.2.13",
"vue-gtag": "^2.0.1"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Massflip</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Concert+One&family=Fredoka+One&display=swap" rel="stylesheet">
</head>
<body style="background-color: #000814;">
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

71
frontend/src/App.vue Normal file
View file

@ -0,0 +1,71 @@
<template>
<div id="Page">
<MassFlip />
<PageFooter />
</div>
</template>
<script setup>
import MassFlip from './components/MassFlip.vue'
import PageFooter from './components/PageFooter.vue'
import { userStore } from './store.js'
import { WSSend } from './main.js'
import { onMounted } from 'vue'
onMounted(() => {
let cookieStr = document.cookie
function cookiesToObj(str) {
str = str.split('; ');
var result = {};
for (var i = 0; i < str.length; i++) {
var cur = str[i].split('=');
result[cur[0]] = cur[1];
}
return result;
}
let jar = cookiesToObj(cookieStr)
if ("session" in jar) {
let id = jar["session"]
// handle logged in user
let req = new XMLHttpRequest
req.open("POST", "/api/login/bysession")
req.send(JSON.stringify({
session: id
}))
req.onreadystatechange = () => {
if (req.readyState == XMLHttpRequest.DONE) {
let usr = JSON.parse(req.responseText)
if ("error" in usr) {
document.cookie = "session=; Max-Age=-99999999"
console.log(usr["error"])
return
}
userStore().updateUser(usr)
let msg = JSON.stringify({
type: "bind",
username: usr.username
})
WSSend(msg)
}
}
}
})
</script>
<style>
#Page {
height: 100%;
width: 100%;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
display: flex;
flex-direction: column;
}
body {
background-color: #000814;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -0,0 +1,58 @@
<template>
<div id="AboutBG">
<div id="close" @click="$emit('display', 'game')">X</div>
<div id="About">
<p>
<span class="logo">Massflip</span> was created by MNRVA as a way to learn about frontend
frameworks, websockets, APIs, and databases. The goal for <span class="logo">Massflip</span>
is to eventually become an open source coin-flip-game web server, with integration in twitch
streams, discord bots, etc., and also to be a project to put on my resume :)
<br>
<br>
If you enjoy this site, share it with your friends! This game thrives on the community chatting
and betting against each other, so more players are always welcome.
</p>
</div>
</div>
</template>
<script setup>
import { defineEmits } from 'vue'
defineEmits(['display'])
</script>
<style scoped>
#close {
color: #ccccd1;
font-size: 26px;
position: absolute;
top: 30px;
right: 50px;
font-family: 'Fredoka One';
}
#close:hover {
cursor: pointer;
color: #888;
}
#About {
width: 40%;
padding: 30px;
margin: auto;
margin-top: 5em;
border-radius: 30px;
background-color: rgba(0, 30, 61, 1);
}
p {
font-family: 'Concert One', cursive;
font-size: 18px;
color: #f3f9f8;
}
.logo {
font-family: 'Fredoka One', cursive;
font-size: 20px;
background-image: linear-gradient(0deg, #efbe08, #dc2f91);
background-size: 100%;
background-clip: text;
color: transparent;
}
</style>

View file

@ -0,0 +1,254 @@
<template>
<div id="GameWindow">
<div id="Timer">
<div id="timeUntilFlip">{{ clock }}</div>
<div id="until">{{ until }}</div>
</div>
<div id="BetGraph">
<div class="pie animate pie-base" :style="tailsStyle"></div>
<div class="pie animate start-no-round pie-overlap" :style="headsStyle"></div>
<div class="data-overlap" id="data">
<div id="headsInfo" class="percent heads">{{ headsPercent }}%</div>
<div id="headsPool" class="pool heads"> {{ headsPool }}gp</div>
<div id="tailsInfo" class="percent tails">{{ tailsPercent }}%</div>
<div id="tailsPool" class="pool tails">{{ tailsPool }}gp</div>
</div>
</div>
<div id="lower" v-show="userStore().username != ''">
<BetInput />
<div id="gpCount">
Your GP:
<div id="gp">
{{ userStore().points }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { userStore } from '../store.js'
import BetInput from './BetInput.vue'
import { WS } from '../main.js'
import { onMounted,ref,reactive } from 'vue'
const User = userStore()
const clock = ref('0:00s')
const until = ref('until next flip')
const headsPercent = ref(0)
const tailsPercent = ref(0)
const headsPool = ref(0)
const tailsPool = ref(0)
const headsStyle = reactive({
"--p":50,
"--c":"#efbe08", //unfocused #7a661b
"--b":"30px"
})
const tailsStyle = reactive({
"--p":100,
"--c":"#dc2f91", //unfocused #6b214b
"--b":"30px"
})
User.$subscribe(() => {
if (User.bet == 'heads') {
tailsStyle['--c'] = '#6b214b'
} else if (User.bet == 'tails') {
headsStyle['--c'] = '#7a661b'
} else {
headsStyle['--c'] = '#efbe08'
tailsStyle['--c'] = '#dc2f91'
}
})
onMounted(() => {
function updatePool(wsMsg) {
let hP = Math.floor((wsMsg.headspool / (wsMsg.headspool + wsMsg.tailspool)) * 100)
if (isNaN(hP)) {
headsPercent.value = 0
headsStyle['--p'] = 50
} else {
headsPercent.value = hP
headsStyle['--p'] = headsPercent.value
}
if (isNaN(hP) || wsMsg.tailspool == 0) {
tailsPercent.value = 0
} else {
tailsPercent.value = (100 - hP)
}
headsPool.value = wsMsg.headspool
tailsPool.value = wsMsg.tailspool
}
WS.addEventListener("message", function (evt) {
let wsMsg = JSON.parse(evt.data)
if (wsMsg.type == "pool") {
updatePool(wsMsg)
} else if (wsMsg.type == "win") {
userStore().addPoints(wsMsg.value)
} else if (wsMsg.type == "tick") {
let time = wsMsg.clock
let timeString = (Math.floor(time/60)).toString() + ":" + ((time%60)>9?"":"0") + (time%60).toString() + "s"
clock.value = timeString
until.value = 'until next flip'
updatePool(wsMsg)
} else if (wsMsg.type == "flip") {
clock.value = wsMsg.value
until.value = ''
userStore().setBet("")
} else if (wsMsg.type == "hasbet") {
if (wsMsg.value == true) {
userStore().bet = wsMsg.bet
}
}
})
})
</script>
<style scoped>
#GameWindow {
display: flex;
align-items: center;
flex-direction: column;
margin: auto;
justify-content: space-around;
height: 90%;
}
#BetGraph {
height: 350px;
width: 350px;
background-color: #000814;
border-radius: 175px;
user-select: none;
margin: none;
padding: 0;
}
#lower {
height: 200px;
}
@property --p{
syntax: '<number>';
inherits: true;
initial-value: 0;
}
.pie {
--p:50; /* the percentage */
--b:30px; /* the thickness */
--c:#444; /* the color */
--w:350px; /* the size*/
width:var(--w);
aspect-ratio:1/1;
position:fixed;
display:inline-grid;
place-content:center;
font-size:25px;
font-weight:bold;
font-family:sans-serif;
margin: 0px;
transition: all, .5s;
}
.pie:before,
.pie:after {
content:"";
position:absolute;
border-radius:50%;
}
.pie:before {
inset:0;
background:
radial-gradient(farthest-side,var(--c) 98%,#0000) top/var(--b) var(--b) no-repeat,
conic-gradient(var(--c) calc(var(--p)*1%),#0000 0);
-webkit-mask:radial-gradient(farthest-side,#0000 calc(99% - var(--b)),#000 calc(100% - var(--b)));
mask:radial-gradient(farthest-side,#0000 calc(99% - var(--b)),#000 calc(100% - var(--b)));
}
.pie:after {
inset:calc(50% - var(--b)/2);
background:var(--c);
transform:rotate(calc(var(--p)*3.6deg - 90deg)) translate(calc(var(--w)/2 - 50%));
}
.animate {
animation:p 1s .5s both;
}
.start-no-round:before {
inset:0;
background:
radial-gradient(farthest-side,var(--c) 98%,#0000) top/var(--b) var(--b) no-repeat,
conic-gradient(var(--c) calc(var(--p)*1%),#0000 0);
-webkit-mask:radial-gradient(farthest-side,#0000 calc(99% - var(--b)),#000 calc(100% - var(--b)));
mask:radial-gradient(farthest-side,#0000 calc(99% - var(--b)),#000 calc(100% - var(--b)));
}
.start-no-round:after {
inset:calc(50% - var(--b)/2);
background:var(--c);
transform:rotate(calc(var(--p)*3.6deg - 90deg)) translate(calc(var(--w)/2 - 50%));
}
@keyframes p{
from{--p:0}
}
.pie-base {
position: absolute;
}
.pie-overlap {
position:relative;
}
.data-overlap {
position:relative;
bottom:305px;
left: 50px;
background-color: #001d3d;
width: 250px;
height: 250px;
border-radius: 125px;
}
#data-bg {
height: 150px;
background-color: #001d3d;
}
.percent {
font-family: 'Fredoka One', cursive;
font-size: 48px;
margin: 0px;
}
.pool {
font-family: 'Fredoka One', cursive;
font-size: 15px;
margin-top: 5px;
}
.heads {
color:#efbe08;
}
.tails {
color:#dc2f91;
}
.pool.heads {
padding-bottom: 2em;
}
.heads.percent {
padding-top: .5em;
}
#Timer {
font-family: 'Fredoka One', cursive;
font-size: 56px;
}
#until {
font-size: 18px;
padding-bottom: 2em;
height: 20px;
}
#gpCount {
font-family: 'Concert One', cursive;
font-size: 24px;
}
#gp {
font-size: 36px;
background-image: linear-gradient(0deg, #efbe08, #dc2f91);
background-size: 100%;
background-clip: text;
color: transparent;
}
</style>

View file

@ -0,0 +1,108 @@
<template>
<div id="BetInput">
<input id="betAmount" :class="{ badInput: hasError}" :disabled="userStore().bet != ''" v-model="bet" type="number" placeholder="Place a bet:"/>
<div id="buttons">
<button id="betHeads" :class="{ disable: userStore().bet != ''}" @click="submitBet('heads')" type="button">Heads</button>
<button id="betTails" :class="{ disable: userStore().bet != ''}" @click="submitBet('tails')" type="button">Tails</button>
</div>
<div id="err"> {{ error }}</div>
</div>
</template>
<script setup>
import { userStore } from '../store.js'
import { WSSend } from '../main.js'
import { ref } from 'vue'
const bet = ref()
const hasError = ref(false)
const error = ref('')
function submitBet(HorT){
if (userStore().bet != '') {
return
}
if (bet.value <= 0) {
hasError.value = true
error.value = "Error: bet must be greater than 0"
return
}
if (userStore().points - bet.value < 0) {
hasError.value = true
error.value = "Error: you cannot bet more gp than you have"
return
}
error.value = ""
hasError.value = false
let wsMsg = {
type: "bet",
username: userStore().username.toLowerCase(),
bet: HorT,
amount: bet.value,
}
WSSend(JSON.stringify(wsMsg));
userStore().subtractPoints(wsMsg.amount)
bet.value = ""
userStore().setBet(HorT)
}
</script>
<style scoped>
#BetInput {
display: flex;
align-items: center;
flex-direction: column;
padding-top: 2em;
max-width: 150px;
}
input {
height: 25px;
width: 100%;
border-radius: 30px;
border: 1px solid white;
color:#000814;
font-size: 12px;
font-weight: bold;
padding-left: 10px;
}
input:focus {
outline: none;
}
#buttons {
display: flex;
justify-content: space-around;
gap: 10px;
}
button {
background: none;
border: none;
color: white;
padding: 10px;
cursor: pointer;
font-family: 'Concert One', cursive;
font-size: 22px;
}
#betHeads {
color: #efbe08;
}
#betTails {
color: #dc2f91;
}
#betHeads:hover {
color: #7a661b;
}
#betTails:hover {
color:#6b214b;
}
#err {
color:red;
}
#betHeads.disable {
color: #7a661b;
cursor: default;
}
#betTails.disable {
color:#6b214b;
cursor: default;
}
</style>

View file

@ -0,0 +1,163 @@
<template>
<div id="ChatBoxContainer">
<div id="ChatWindow">
</div>
<div id="ChatInputContainer">
<div v-if="userStore().username != ''">
<iframe name="cca" style="display:none;"></iframe>
<form action="#" target="cca" autocomplete="off">
<input id="ChatInput" v-model="chat" type="text" :placeholder="ChatString" maxlength="336"/>
<button id="submit" @click="sendChat">Send</button>
</form>
</div>
<p v-show="userStore().username == ''">You need to be logged in to chat</p>
</div>
</div>
</template>
<script setup>
import { userStore } from '../store.js'
import { Queue } from '../main.js'
import { WS,WSSend } from '../main.js'
import { ref,onMounted, computed } from 'vue'
const chatQueue = new Queue()
const ChatString = computed(() => {
return `Chat as ${userStore().username}:`
})
const chat = ref('')
const ChatColors = {
green: "limegreen",
yellow: "gold",
cyan: "cyan",
red: "firebrick",
pink: "fuchsia",
violet: "violet",
orange: "orange",
}
const _CHAT_MAX_HISTORY = 75;
onMounted(() => {
let log = document.getElementById("ChatWindow")
function appendLog(item) {
var doScroll = log.scrollTop > log.scrollHeight - log.clientHeight - 1;
log.appendChild(item);
if (doScroll) {
log.scrollTop = log.scrollHeight - log.clientHeight;
}
}
WS.addEventListener("message", function (evt) {
let wsMsg = JSON.parse(evt.data)
if (wsMsg.type == "chat") {
chatQueue.enqueue(wsMsg)
if (chatQueue.length >= _CHAT_MAX_HISTORY) {
chatQueue.dequeue()
}
log.innerHTML = ""
for (let message of Object.values(chatQueue.elements)) {
var item = document.createElement("div")
let fromUser = document.createElement("span")
fromUser.style = `color: ${message.color};`
fromUser.innerText = message.username
item.appendChild(fromUser)
let chatScore = document.createElement("span")
chatScore.innerText = `(${message.points})`
chatScore.style = `color: ${message.color};font-family: 'Helvetica';font-size: 12px;`
item.appendChild(chatScore)
let chatMsg = document.createTextNode(`: ${message.message}`)
item.appendChild(chatMsg)
appendLog(item)
}
}
})
})
function sendChat() {
if (chat.value.startsWith("/color")) {
let newColor = chat.value.split(" ")[1]
if (newColor in ChatColors) {
userStore().$patch({color:ChatColors[newColor]})
chat.value = ''
let req = new XMLHttpRequest()
req.open("PUT", "/api/chatcolor")
req.send(JSON.stringify({
username: userStore().username,
color: ChatColors[newColor],
}))
return
} else if (newColor == 'list') {
// put a chat message that shows all the colors
chat.value = ''
return
}
}
let wsSend = {
type: "chat",
username: userStore().username,
color: userStore().color,
message: chat.value,
points: userStore().points
}
WSSend(JSON.stringify(wsSend))
chat.value = ''
}
</script>
<style scoped>
#ChatBoxContainer {
height: 70vh;
width: 420px;
max-width: 50%;
margin: 2vh;
border-radius: 30px;
background-color: #000814;
text-align: left;
font-family: 'Concert One', cursive;
}
#ChatInputContainer p {
text-align: center;
}
#ChatWindow {
box-sizing: border-box;
height: 92%;
padding: 20px;
margin: 1vh;
margin-bottom: 5px;
overflow-y: scroll;
overflow-x: hidden;
scrollbar-color: #001d3d;
scrollbar-width: thin;
font-size: 16px;
}
#ChatWindow::-webkit-scrollbar {
background-color: #000814;
}
#ChatInput {
height: 20px;
width: 75%;
margin-left: 15px;
border-radius: 30px;
border: 1px solid white;
color:#000814;
font-size: 12px;
font-weight: bold;
padding-left: 10px;
}
#submit {
background-color: #000814;
color: #f3f9f8;
margin-left: 10px;
outline: none;
border: none;
font-weight: bold;
}
#submit:hover {
cursor: pointer;
color:grey;
}
#ChatInput:focus {
outline: none !important;
}
</style>

View file

@ -0,0 +1,18 @@
<template>
<div id="GameWindow">
<BetBox />
<ChatBox />
</div>
</template>
<script setup>
import BetBox from './BetBox.vue'
import ChatBox from './ChatBox.vue'
</script>
<style scoped>
#GameWindow {
display: flex;
justify-content: space-around;
}
</style>

View file

@ -0,0 +1,60 @@
<template>
<div id="HowToBG">
<div id="close" @click="$emit('display', 'game')">X</div>
<div id="HowTo">
<p>
The idea of <span class="logo">Massflip</span> is simple: place a bet on a coinflip.
Every player starts with the same amount of GP - the game's currency. The goal is to win
as much GP as possible without going bankrupt.
<br>
The game's clock will tick
down to each coin flip, and in that time you can enter a bet into the Betting input,
and click on either heads or tails to place your bet. If you win, you get a payout based on
how much GP was bet against you, and how much you contributed to the winning side's GP.
<br>
Compete with your friends to see how much GP you can win without going bankrupt!
</p>
</div>
</div>
</template>
<script setup>
import { defineEmits } from 'vue'
defineEmits(['display'])
</script>
<style scoped>
#close {
color: #ccccd1;
font-size: 26px;
position: absolute;
top: 30px;
right: 50px;
font-family: 'Fredoka One';
}
#close:hover {
cursor: pointer;
color: #888;
}
#HowTo {
width: 40%;
padding: 30px;
margin: auto;
margin-top: 5em;
border-radius: 30px;
background-color: rgba(0, 30, 61, 1);
}
p {
font-family: 'Concert One', cursive;
font-size: 18px;
color: #f3f9f8;
}
.logo {
font-family: 'Fredoka One', cursive;
font-size: 20px;
background-image: linear-gradient(0deg, #efbe08, #dc2f91);
background-size: 100%;
background-clip: text;
color: transparent;
}
</style>

View file

@ -0,0 +1,201 @@
<template>
<div id="LoginBackground">
<div id="close" @click="$emit('display', 'game')">X</div>
<div id="LoginContainer">
<form action="none">
<label>Username</label>
<input type="text" :class="{ badInput: unHasError }" v-model="form.username" required="yes" maxlength="24" />
<label>Password</label>
<input type="password" :class="{ badInput: pHasError }" v-model="form.password" required="yes" maxlength="255">
<label v-show="tCreate">Confirm Password</label>
<input type="password" :class="{ badInput: pHasError }" v-model="form.confirm" required="yes" id="confPass" v-show="tCreate">
<div id="midRow">
<div>
<input type="checkbox" v-model="form.remember" id="remember">
<label id="rememberLabel">Remember me</label>
</div>
<div id="noAcc" @click="toggleCreate">Don't have an account?</div>
</div>
<button class="submit" v-show="!tCreate" @click="login">Login</button>
<button class="submit" v-show="tCreate" @click="createAccount">Create</button>
</form>
<p id="serverResponse"> {{ serverResponse }}</p>
</div>
</div>
</template>
<script setup>
import { defineEmits, ref, reactive } from 'vue'
defineEmits(['display'])
const form = reactive({
username: '',
password: '',
confirm: '',
remember: false
})
const tCreate = ref(false)
const unHasError = ref(false)
const pHasError = ref(false)
const serverResponse = ref('')
function toggleCreate() {
tCreate.value = !tCreate.value
}
function loginFieldsReady() {
let ret = true
let UnameReg = /^[a-zA-Z0-9-_]{3,24}$/
if (form.username.search(UnameReg) === -1) {
unHasError.value = true
serverResponse.value = "Username must be 3-24 characters, and only contain letters, numbers, - and _"
ret = false
} else {
unHasError.value = false
}
if (form.password.length < 8) {
pHasError.value = true
serverResponse.value = "Password must be at least 8 characters"
ret = false
} else if (form.password.length > 255){
pHasError.value = true
serverResponse.value = "Password must be no larger than 255 characters"
ret = false
} else {
pHasError.value = false
}
return ret
}
async function createAccount(e) {
e.preventDefault()
if (!loginFieldsReady()) {
return
}
if (form.password != form.confirm) {
pHasError.value = true
serverResponse.value = "passwords do not match"
return
}
let req = new XMLHttpRequest()
req.open("POST", "/api/createaccount")
req.withCredentials = true
req.send(JSON.stringify({
username: form.username,
password: form.password,
remember: form.remember
}))
req.onreadystatechange = () => {
if (req.readyState == XMLHttpRequest.DONE) {
let resp = JSON.parse(req.response)
let err = resp["error"]
if (err == undefined) {
location.reload()
} else {
document.getElementById("serverResponse").innerText = `Error: ${err}`;
}
}
}
}
function login(e) {
e.preventDefault()
if (!loginFieldsReady()) {
return
}
let req = new XMLHttpRequest()
req.open("POST", "/api/login")
req.withCredentials = true
req.send(JSON.stringify({
username: form.username,
password: form.password,
remember: form.remember
}))
req.onreadystatechange = () => {
if (req.readyState == XMLHttpRequest.DONE) {
let resp = JSON.parse(req.response)
let err = resp["error"]
if (err == undefined) {
location.reload()
} else {
serverResponse.value = `Error: ${err}`;
}
}
}
}
</script>
<style scoped>
#close {
color: #ccccd1;
font-size: 26px;
position: absolute;
top: 30px;
right: 50px;
font-family: 'Fredoka One';
}
#close:hover {
cursor: pointer;
color: #888;
}
#LoginContainer {
width: 40%;
padding: 30px;
margin: auto;
margin-top: 5em;
border-radius: 30px;
background-color: rgba(0, 30, 61, 1);
}
#noAcc {
font-family: 'Concert One', cursive;
color:#417fc2;
font-size: 14px;
}
#noAcc:hover {
cursor: pointer;
color:#2b4d72;
}
#rememberLabel {
padding-left: 3px;
}
#midRow {
width: 100%;
display: flex;
justify-content: space-around;
align-items:flex-end;
}
form {
display: flex;
flex-flow: column;
align-items: flex-start;
}
form label {
font-family: 'Concert One', cursive;
padding-left: 1em;
}
input[type=text], input[type=password] {
width: 100%;
padding: 6px;
margin: 8px 0;
border-radius: 15px;
font-family: 'Concert One', cursive;
font-size: 16px;
}
.submit {
margin-top: 1em;
border: none;
width: 30%;
background: none;
color: #f3f9f8;
font-family: 'Fredoka One', cursive;
font-size: 24px;
align-self: flex-end;
}
.submit:hover {
cursor: pointer;
color: #979b9a;
}
#serverResponse {
font-family: 'Times New Roman', Times, serif;
color: rgb(212, 97, 117);
}
.badInput {
border: 2px solid red;
}
</style>

View file

@ -0,0 +1,53 @@
<template>
<div id="Content">
<PageHeader @display="displayHandle"/>
<div id="Container">
<GameWindow />
<LoginForm @display="displayHandle" class="dullBG" v-show="display.content == 'login'"/>
<AboutPage @display="displayHandle" class="dullBG" v-show="display.content == 'about'"/>
<HowToPlay @display="displayHandle" class="dullBG" v-show="display.content == 'howTo'"/>
</div>
</div>
</template>
<script setup>
import PageHeader from './PageHeader.vue'
import LoginForm from './LoginForm.vue'
import GameWindow from './GameWindow.vue'
import AboutPage from './AboutPage.vue'
import HowToPlay from './HowToPlay.vue'
import { reactive } from 'vue'
const display = reactive({ content: 'game' })
function displayHandle(c) {
if (display.content == c) {
display.content = 'game'
} else {
display.content = c
}
}
</script>
<style scoped>
#Container {
width: 1000px;
max-width: 100%;
height: 75vh;
border-radius: 30px;
margin: auto;
color: #f3f9f8;
background-color: #001d3d;
position: relative;
}
.dullBG {
height: 100%;
width: 100%;
position: absolute;
top: 0px;
left: 0px;
border-radius: 30px;
background-color: rgba(0, 8, 20,0.7);
}
</style>

View file

@ -0,0 +1,13 @@
<template>
<p>Massflip v0.0.3 - Copyright 2022 MNRVA</p>
</template>
<script setup>
</script>
<style scoped>
p {
font-family: 'Concert One', cursive;
color: #001d3d;
}
</style>

View file

@ -0,0 +1,67 @@
<template>
<div id="HeaderContainer">
<div id="header">Massflip<span id="beta">alpha</span></div>
<nav>
<p @click="$emit('display', 'howTo')">How to play</p>
<p @click="$emit('display', 'about')">About</p>
<p v-if="userStore().username == ''" @click="$emit('display', 'login')" id="showLoginNav">Login</p>
<p v-if="userStore().username != ''" @click="logout()">Logout</p>
</nav>
</div>
</template>
<script setup>
import { userStore } from '../store.js'
import { defineEmits } from 'vue'
defineEmits(['display'])
function logout() {
let req = new XMLHttpRequest()
req.open("POST", "/api/logout")
req.send(JSON.stringify({
username: userStore().username
}))
req.onreadystatechange = () => {
if (req.readyState == XMLHttpRequest.DONE) {
document.cookie = "session=; Max-Age=-99999999"
location.reload()
}
}
}
</script>
<style scoped>
#HeaderContainer {
color: #f3f9f8;
display: flex;
height: 125px;
flex-direction: column;
align-items: center;
user-select: none;
font-family: 'Fredoka One', cursive;
font-size: 48px;
margin: 0;
margin-top: 10px;
}
#header {
display: flex;
}
#beta {
font-size: 24px;
background-image: linear-gradient(0deg, #efbe08 60%, #dc2f91);
background-size: 100%;
background-clip: text;
color: transparent;
}
nav {
display: flex;
align-items: center;
font-family: 'Concert One', cursive;
width: 400px;
justify-content: space-between;
font-size: 18px;
}
nav p:hover {
cursor: pointer;
color: #bbb;
}
</style>

69
frontend/src/main.js Normal file
View file

@ -0,0 +1,69 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import VueGtag from "vue-gtag";
// switch these in production
export const WS = new WebSocket("wss://" + "massflip.mnrva.dev" + "/ws")
//export const WS = new WebSocket("ws://" + "127.0.0.1:8000" + "/ws")
WS.onclose = function() {
alert("WebSocket connection closed.")
}
WS.onerror = function() {
alert("WebSocket connection error.")
}
export function WSSend(msg){
WSWait(WS, function(){
WS.send(msg);
});
}
function WSWait(socket, callback){
setTimeout(
function () {
if (socket.readyState === WebSocket.OPEN) {
if (callback != null){
callback();
}
} else {
WSWait(socket, callback);
}
}, 5);
}
export class Queue {
constructor() {
this.elements = {};
this.head = 0;
this.tail = 0;
}
enqueue(element) {
this.elements[this.tail] = element;
this.tail++;
}
dequeue() {
const item = this.elements[this.head];
delete this.elements[this.head];
this.head++;
return item;
}
peek() {
return this.elements[this.head];
}
get length() {
return this.tail - this.head;
}
get isEmpty() {
return this.length === 0;
}
}
const pinia = createPinia()
var app = createApp(App)
app.use(pinia)
app.use(VueGtag, {config: { id: "G-C3WQH98SZB" }})
app.mount('#app')

61
frontend/src/store.js Normal file
View file

@ -0,0 +1,61 @@
import { defineStore } from 'pinia'
// useStore could be anything like useUser, useCart
// the first argument is a unique id of the store across your application
export const userStore = defineStore({
id:'main',
state: () => {
return {
username: '',
color: '',
points: 0,
resets: 0,
bet: "",
}
},
getters: {
getUser() {
return {
username: this.username,
color: this.color,
points: this.points,
resets: this.resets,
}
},
ready() {
if (this.username != '') {
return true
} else {
return false
}
},
getBet() {
return this.bet
},
getUsername() {
return this.username
}
},
actions: {
updateUser(usr) {
this.$state = {
username: usr.username,
color: usr.color,
points: usr.points,
resets: usr.resets,
}
},
addPoints(amt) {
this.$patch({points: this.points + amt})
},
subtractPoints(amt) {
this.$patch({points: this.points - amt})
},
toggle() {
this.$patch({t:!this.t})
},
setBet(bet) {
this.$patch({bet: bet})
}
}
})

17
frontend/vue.config.js Normal file
View file

@ -0,0 +1,17 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})
module.exports = {
devServer: {
host: "localhost",
proxy: {
"/": {
target: "http://localhost:8000",
secure: false,
ws: true
}
}
}
};