mirror of
https://github.com/gabehf/massflip.git
synced 2026-03-08 23:18:13 -07:00
Open Source commit
This commit is contained in:
commit
7ad332ad02
36 changed files with 22430 additions and 0 deletions
23
frontend/.gitignore
vendored
Normal file
23
frontend/.gitignore
vendored
Normal 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
24
frontend/README.md
Normal 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
5
frontend/babel.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
19
frontend/jsconfig.json
Normal file
19
frontend/jsconfig.json
Normal 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
20137
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
46
frontend/package.json
Normal file
46
frontend/package.json
Normal 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
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
20
frontend/public/index.html
Normal file
20
frontend/public/index.html
Normal 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
71
frontend/src/App.vue
Normal 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>
|
||||
BIN
frontend/src/assets/favicon.ico
Normal file
BIN
frontend/src/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
frontend/src/assets/favicon.png
Normal file
BIN
frontend/src/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/src/assets/logo.png
Normal file
BIN
frontend/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
58
frontend/src/components/AboutPage.vue
Normal file
58
frontend/src/components/AboutPage.vue
Normal 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>
|
||||
254
frontend/src/components/BetBox.vue
Normal file
254
frontend/src/components/BetBox.vue
Normal 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>
|
||||
108
frontend/src/components/BetInput.vue
Normal file
108
frontend/src/components/BetInput.vue
Normal 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>
|
||||
163
frontend/src/components/ChatBox.vue
Normal file
163
frontend/src/components/ChatBox.vue
Normal 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>
|
||||
18
frontend/src/components/GameWindow.vue
Normal file
18
frontend/src/components/GameWindow.vue
Normal 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>
|
||||
60
frontend/src/components/HowToPlay.vue
Normal file
60
frontend/src/components/HowToPlay.vue
Normal 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>
|
||||
201
frontend/src/components/LoginForm.vue
Normal file
201
frontend/src/components/LoginForm.vue
Normal 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>
|
||||
53
frontend/src/components/MassFlip.vue
Normal file
53
frontend/src/components/MassFlip.vue
Normal 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>
|
||||
13
frontend/src/components/PageFooter.vue
Normal file
13
frontend/src/components/PageFooter.vue
Normal 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>
|
||||
67
frontend/src/components/PageHeader.vue
Normal file
67
frontend/src/components/PageHeader.vue
Normal 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
69
frontend/src/main.js
Normal 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
61
frontend/src/store.js
Normal 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
17
frontend/vue.config.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue