Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 233e7a9c8e | |||
| 7123bc4529 | |||
| 3294b1adf9 | |||
| 355dabf439 | |||
| 8997a570fa | |||
| 3b53b8997d | |||
| ff9c9c6a84 |
@@ -1,3 +1,5 @@
|
|||||||
**/node_modules
|
.env.example
|
||||||
**/dist
|
.env
|
||||||
.env.local
|
.idea
|
||||||
|
README.md
|
||||||
|
.gitea
|
||||||
1
.env.example
Normal file
1
.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
REACT_APP_API_URL="mau-mau.nl"
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/* eslint-env node */
|
|
||||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
'extends': [
|
|
||||||
'plugin:vue/vue3-essential',
|
|
||||||
'eslint:recommended',
|
|
||||||
'@vue/eslint-config-typescript',
|
|
||||||
'@vue/eslint-config-prettier/skip-formatting'
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
name: Build Mau & Deploy Mau
|
name: Build Mau & Deploy Mau
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'mauster'
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: mau
|
group: mau
|
||||||
@@ -22,7 +28,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 16
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
@@ -30,9 +36,13 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: npm test -- --watch=false
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
|
if: gitea.ref == 'refs/heads/mauster'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|||||||
47
.gitignore
vendored
47
.gitignore
vendored
@@ -1,30 +1,27 @@
|
|||||||
# Logs
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
logs
|
|
||||||
*.log
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.development
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
.DS_Store
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
coverage
|
|
||||||
*.local
|
|
||||||
|
|
||||||
/cypress/videos/
|
|
||||||
/cypress/screenshots/
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
.idea
|
||||||
*.suo
|
.env
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
*.tsbuildinfo
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/prettierrc",
|
|
||||||
"semi": false,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 100,
|
|
||||||
"trailingComma": "none"
|
|
||||||
}
|
|
||||||
31
Dockerfile
31
Dockerfile
@@ -1,18 +1,15 @@
|
|||||||
# build stage
|
# Create a production build of the app
|
||||||
FROM node:lts-alpine AS build-stage
|
FROM node:16.9.1 as build
|
||||||
ARG API_ENDPOINT="https://api.mau-mau.nl"
|
WORKDIR /var/www/mau-mau.nl
|
||||||
ARG WEBSOCKET_ENDPOINT="wss://api.mau-mau.nl"
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm install
|
|
||||||
COPY . .
|
|
||||||
RUN echo "VITE_API_ENDPOINT=${API_ENDPOINT}" > .env
|
|
||||||
RUN echo "VITE_WEBSOCKET_ENDPOINT=${WEBSOCKET_ENDPOINT}" >> .env
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# production stage
|
COPY . .
|
||||||
FROM nginx:stable-alpine AS production-stage
|
|
||||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
RUN echo "REACT_APP_API_URL=https://api.mau-mau.nl" >> .env
|
||||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
|
RUN echo "REACT_APP_WEBSOCKET_URL=wss://api.mau-mau.nl" >> .env
|
||||||
EXPOSE 80
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
RUN npm install --silent
|
||||||
|
RUN npm run build
|
||||||
|
RUN npm install -g serve
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD serve -l 3000 -s build
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -1,32 +1 @@
|
|||||||
# MauMau-Client-Vue
|
Mau
|
||||||
|
|
||||||
## Env variables
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_API_ENDPOINT=http://{url}:{port}
|
|
||||||
VITE_WEBSOCKET_ENDPOINT=ws://{url}:{port}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Setup
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compile and Hot-Reload for Development
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type-Check, Compile and Minify for Production
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lint with [ESLint](https://eslint.org/)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|||||||
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
version: '3.9'
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: 'MauMau-Client'
|
||||||
|
ports:
|
||||||
|
- '80:80'
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- MauMau
|
||||||
|
labels:
|
||||||
|
traefik.enable: "true"
|
||||||
|
traefik.http.routers.maumau.entrypoints: "websecure"
|
||||||
|
traefik.http.routers.maumau.rule: "Host(`mau-mau.nl`)"
|
||||||
|
traefik.http.routers.maumau.tls: "true"
|
||||||
|
traefik.http.routers.maumau.tls.certresolver: "lets-encrypt"
|
||||||
|
traefik.docker.network: "traefik"
|
||||||
|
networks:
|
||||||
|
MauMau:
|
||||||
|
driver: bridge
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
11
index.html
11
index.html
@@ -1,11 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link rel="shortcut icon" href="/favicon.ico">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Mau Mau!</title>
|
|
||||||
</head>
|
|
||||||
<body id="app"></body>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</html>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
location / {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html index.htm;
|
|
||||||
try_files $uri $uri/ /index.html =404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29918
package-lock.json
generated
29918
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
80
package.json
80
package.json
@@ -1,44 +1,50 @@
|
|||||||
{
|
{
|
||||||
"name": "maumau-client",
|
"name": "maumau-client",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"build-only": "vite build",
|
|
||||||
"type-check": "vue-tsc --build --force",
|
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
|
||||||
"format": "prettier --write src/"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/svg": "^7.4.47",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"pinia": "^2.1.7",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"vue": "^3.4.21",
|
"@types/jest": "^27.5.2",
|
||||||
"vue-confetti-explosion": "^1.0.2",
|
"@types/node": "^16.18.14",
|
||||||
"vue-router": "^4.3.0",
|
"@types/react": "^18.0.28",
|
||||||
"vue-toast-notification": "^3.1.2"
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"bootstrap-icons": "^1.11.3",
|
||||||
|
"framer-motion": "^11.2.4",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-query": "^3.39.3",
|
||||||
|
"react-router": "^6.9.0",
|
||||||
|
"react-router-dom": "^6.9.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"react-use-websocket": "^4.3.1",
|
||||||
|
"sass": "^1.59.3",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"scripts": {
|
||||||
"@rushstack/eslint-patch": "^1.8.0",
|
"start": "react-scripts start",
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"build": "react-scripts build",
|
||||||
"@types/node": "^20.12.5",
|
"test": "react-scripts test",
|
||||||
"@types/uuid": "^9.0.8",
|
"eject": "react-scripts eject"
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
},
|
||||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
"eslintConfig": {
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"extends": [
|
||||||
"@vue/eslint-config-typescript": "^13.0.0",
|
"react-app",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"react-app/jest"
|
||||||
"eslint": "^8.57.0",
|
]
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
},
|
||||||
"npm-run-all2": "^6.1.2",
|
"browserslist": {
|
||||||
"prettier": "^3.2.5",
|
"production": [
|
||||||
"sass": "^1.77.5",
|
">0.2%",
|
||||||
"typescript": "~5.4.0",
|
"not dead",
|
||||||
"vite": "^5.2.8",
|
"not op_mini all"
|
||||||
"vite-plugin-vue-devtools": "^7.0.25",
|
],
|
||||||
"vue-tsc": "^2.0.11"
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
public/index.html
Normal file
17
public/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta name="description" content="Mau-Mau!"/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<title>Mau-Mau</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
public/logo192.png
Normal file
BIN
public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
25
public/manifest.json
Normal file
25
public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "Mau Mau",
|
||||||
|
"name": "Mau Mau",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
9
src/App.test.tsx
Normal file
9
src/App.test.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
test('Home screen contains \'MauMau\'', () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/Mau-Mau/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
14
src/App.tsx
Normal file
14
src/App.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '../src/styles/App.scss';
|
||||||
|
import Router from "./config/Router";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"app"}>
|
||||||
|
<Router/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
31
src/App.vue
31
src/App.vue
@@ -1,31 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { RouterView } from 'vue-router'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<RouterView />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import "./scss/variables";
|
|
||||||
@import "./scss/layout";
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
@extend .container-full;
|
|
||||||
|
|
||||||
background-color: $background-color;
|
|
||||||
color: $background-text-color;
|
|
||||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
@extend .container-full;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Binary file not shown.
BIN
src/assets/fonts/OrientalCatsLight.otf
Normal file
BIN
src/assets/fonts/OrientalCatsLight.otf
Normal file
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 276 B |
@@ -1,85 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import StylessButton from "@/components/button/StylessButton.vue";
|
|
||||||
import {nextTick, reactive, ref} from "vue";
|
|
||||||
import {useScroll, watchArray} from "@vueuse/core";
|
|
||||||
|
|
||||||
export interface ChatMessage {
|
|
||||||
Id: string;
|
|
||||||
Sender: string;
|
|
||||||
Message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
chats: ChatMessage[]
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: "send", message: string): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const chatRef = ref<HTMLElement>();
|
|
||||||
const scroll = useScroll(chatRef);
|
|
||||||
|
|
||||||
const state = reactive<{
|
|
||||||
chatInput: string;
|
|
||||||
}>({
|
|
||||||
chatInput: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
watchArray(props.chats, async () => {
|
|
||||||
await nextTick();
|
|
||||||
if (scroll.arrivedState.bottom) {
|
|
||||||
chatRef.value?.children[chatRef.value.children.length - 1]?.scrollIntoView();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const sendChat = () => {
|
|
||||||
emits("send", state.chatInput);
|
|
||||||
state.chatInput = "";
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ol ref="chatRef" class="chat-list">
|
|
||||||
<li v-for="chat in chats" :key="chat.Id">
|
|
||||||
<strong>{{ chat.Sender }}</strong>: {{ chat.Message }}
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<form @submit.prevent="sendChat" class="chat-form">
|
|
||||||
<label>
|
|
||||||
Chat:
|
|
||||||
<input v-model="state.chatInput" type="text" autocomplete="off"/>
|
|
||||||
</label>
|
|
||||||
<StylessButton type="submit">Send</StylessButton>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.chat {
|
|
||||||
&-list {
|
|
||||||
list-style-type: none;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-y: scroll;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-form {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
background-color: #2E2E2E;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
|
|
||||||
input {
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
to?: string,
|
|
||||||
show?: boolean,
|
|
||||||
}>(), {
|
|
||||||
to: "body",
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Teleport v-if="show" :to="to">
|
|
||||||
<Transition>
|
|
||||||
<dialog v-if="show" class="modal">
|
|
||||||
<slot />
|
|
||||||
</dialog>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/layout";
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
@extend .container-fill, .place-center;
|
|
||||||
|
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-enter-active,
|
|
||||||
.v-leave-active {
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-enter-from,
|
|
||||||
.v-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: "click"): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
emits("click");
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<button class="danger-button" @click="onClick">
|
|
||||||
<slot />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/variables";
|
|
||||||
|
|
||||||
.danger-button {
|
|
||||||
background: $danger-color;
|
|
||||||
color: $surface-text-color;
|
|
||||||
border: none;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
vertical-align: baseline;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten($danger-color, 10%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: "click"): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
emits("click");
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<button class="primary-button" @click="onClick">
|
|
||||||
<slot />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/variables";
|
|
||||||
|
|
||||||
.primary-button {
|
|
||||||
background: $surface-color;
|
|
||||||
color: $surface-text-color;
|
|
||||||
border: none;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
vertical-align: baseline;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten($surface-color, 10%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{
|
|
||||||
disabled?: boolean
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: "click"): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
emits('click');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<button class="styless-button" :disabled="disabled" @click="onClick">
|
|
||||||
<slot />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.styless-button {
|
|
||||||
background: none;
|
|
||||||
color: inherit;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
font: inherit;
|
|
||||||
vertical-align: baseline;
|
|
||||||
outline: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {reactive} from "vue";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
initialValue?: string
|
|
||||||
placeholder?: string
|
|
||||||
clearOnSubmit?: boolean
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: "input", value: string): void
|
|
||||||
(event: "submit", value: string): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const state = reactive<{
|
|
||||||
value: string,
|
|
||||||
}>({
|
|
||||||
value: props.initialValue ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
const onInput = () => {
|
|
||||||
emit('input', state.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = () => {
|
|
||||||
emit('submit', state.value);
|
|
||||||
if (props.clearOnSubmit) state.value = '';
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<label class="input">
|
|
||||||
<input v-model="state.value" v-on:keydown.prevent.enter="onSubmit" @input="onInput"
|
|
||||||
type="text" :placeholder="placeholder" autocomplete="off"/>
|
|
||||||
<slot name="buttonEnd" @click="onSubmit"/>
|
|
||||||
</label>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.input {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
background-color: #2E2E2E;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
|
|
||||||
input {
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
outline: none;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="backdrop">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/layout.scss";
|
|
||||||
|
|
||||||
.backdrop {
|
|
||||||
@extend .container-fill;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="center">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.center {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import StylessButton from "@/components/button/StylessButton.vue";
|
|
||||||
import type {ICard} from "@/types/card.type";
|
|
||||||
import {computed} from "vue";
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
card?: ICard,
|
|
||||||
clickable?: boolean,
|
|
||||||
hoverable?: boolean
|
|
||||||
}>(), {
|
|
||||||
clickable: p => p.card !== undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: 'click', card?: ICard): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const imageUrl = computed(() => {
|
|
||||||
if (!props.card) {
|
|
||||||
return new URL("@/assets/cards/back.png", import.meta.url).href;
|
|
||||||
}
|
|
||||||
|
|
||||||
// I hate this with a passion, but vite doesn't want to resolve dynamic paths >:(
|
|
||||||
switch (props.card.type) {
|
|
||||||
case "SPADES":
|
|
||||||
switch (props.card.value) {
|
|
||||||
case "TWO":
|
|
||||||
return new URL("@/assets/cards/spades_two.png", import.meta.url).href;
|
|
||||||
case "THREE":
|
|
||||||
return new URL("@/assets/cards/spades_three.png", import.meta.url).href;
|
|
||||||
case "FOUR":
|
|
||||||
return new URL("@/assets/cards/spades_four.png", import.meta.url).href;
|
|
||||||
case "FIVE":
|
|
||||||
return new URL("@/assets/cards/spades_five.png", import.meta.url).href;
|
|
||||||
case "SIX":
|
|
||||||
return new URL("@/assets/cards/spades_six.png", import.meta.url).href;
|
|
||||||
case "SEVEN":
|
|
||||||
return new URL("@/assets/cards/spades_seven.png", import.meta.url).href;
|
|
||||||
case "EIGHT":
|
|
||||||
return new URL("@/assets/cards/spades_eight.png", import.meta.url).href;
|
|
||||||
case "NINE":
|
|
||||||
return new URL("@/assets/cards/spades_nine.png", import.meta.url).href;
|
|
||||||
case "TEN":
|
|
||||||
return new URL("@/assets/cards/spades_ten.png", import.meta.url).href;
|
|
||||||
case "JACK":
|
|
||||||
return new URL("@/assets/cards/spades_jack.png", import.meta.url).href;
|
|
||||||
case "QUEEN":
|
|
||||||
return new URL("@/assets/cards/spades_queen.png", import.meta.url).href;
|
|
||||||
case "KING":
|
|
||||||
return new URL("@/assets/cards/spades_king.png", import.meta.url).href;
|
|
||||||
case "ACE":
|
|
||||||
return new URL("@/assets/cards/spades_ace.png", import.meta.url).href;
|
|
||||||
default:
|
|
||||||
return new URL("@/assets/cards/back.png", import.meta.url).href;
|
|
||||||
}
|
|
||||||
case "HEARTS":
|
|
||||||
switch (props.card.value) {
|
|
||||||
case "TWO":
|
|
||||||
return new URL("@/assets/cards/hearts_two.png", import.meta.url).href;
|
|
||||||
case "THREE":
|
|
||||||
return new URL("@/assets/cards/hearts_three.png", import.meta.url).href;
|
|
||||||
case "FOUR":
|
|
||||||
return new URL("@/assets/cards/hearts_four.png", import.meta.url).href;
|
|
||||||
case "FIVE":
|
|
||||||
return new URL("@/assets/cards/hearts_five.png", import.meta.url).href;
|
|
||||||
case "SIX":
|
|
||||||
return new URL("@/assets/cards/hearts_six.png", import.meta.url).href;
|
|
||||||
case "SEVEN":
|
|
||||||
return new URL("@/assets/cards/hearts_seven.png", import.meta.url).href;
|
|
||||||
case "EIGHT":
|
|
||||||
return new URL("@/assets/cards/hearts_eight.png", import.meta.url).href;
|
|
||||||
case "NINE":
|
|
||||||
return new URL("@/assets/cards/hearts_nine.png", import.meta.url).href;
|
|
||||||
case "TEN":
|
|
||||||
return new URL("@/assets/cards/hearts_ten.png", import.meta.url).href;
|
|
||||||
case "JACK":
|
|
||||||
return new URL("@/assets/cards/hearts_jack.png", import.meta.url).href;
|
|
||||||
case "QUEEN":
|
|
||||||
return new URL("@/assets/cards/hearts_queen.png", import.meta.url).href;
|
|
||||||
case "KING":
|
|
||||||
return new URL("@/assets/cards/hearts_king.png", import.meta.url).href;
|
|
||||||
case "ACE":
|
|
||||||
return new URL("@/assets/cards/hearts_ace.png", import.meta.url).href;
|
|
||||||
default:
|
|
||||||
return new URL("@/assets/cards/back.png", import.meta.url).href;
|
|
||||||
}
|
|
||||||
case "DIAMONDS":
|
|
||||||
switch (props.card.value) {
|
|
||||||
case "TWO":
|
|
||||||
return new URL("@/assets/cards/diamonds_two.png", import.meta.url).href;
|
|
||||||
case "THREE":
|
|
||||||
return new URL("@/assets/cards/diamonds_three.png", import.meta.url).href;
|
|
||||||
case "FOUR":
|
|
||||||
return new URL("@/assets/cards/diamonds_four.png", import.meta.url).href;
|
|
||||||
case "FIVE":
|
|
||||||
return new URL("@/assets/cards/diamonds_five.png", import.meta.url).href;
|
|
||||||
case "SIX":
|
|
||||||
return new URL("@/assets/cards/diamonds_six.png", import.meta.url).href;
|
|
||||||
case "SEVEN":
|
|
||||||
return new URL("@/assets/cards/diamonds_seven.png", import.meta.url).href;
|
|
||||||
case "EIGHT":
|
|
||||||
return new URL("@/assets/cards/diamonds_eight.png", import.meta.url).href;
|
|
||||||
case "NINE":
|
|
||||||
return new URL("@/assets/cards/diamonds_nine.png", import.meta.url).href;
|
|
||||||
case "TEN":
|
|
||||||
return new URL("@/assets/cards/diamonds_ten.png", import.meta.url).href;
|
|
||||||
case "JACK":
|
|
||||||
return new URL("@/assets/cards/diamonds_jack.png", import.meta.url).href;
|
|
||||||
case "QUEEN":
|
|
||||||
return new URL("@/assets/cards/diamonds_queen.png", import.meta.url).href;
|
|
||||||
case "KING":
|
|
||||||
return new URL("@/assets/cards/diamonds_king.png", import.meta.url).href;
|
|
||||||
case "ACE":
|
|
||||||
return new URL("@/assets/cards/diamonds_ace.png", import.meta.url).href;
|
|
||||||
default:
|
|
||||||
return new URL("@/assets/cards/back.png", import.meta.url).href;
|
|
||||||
}
|
|
||||||
case "CLUBS":
|
|
||||||
switch (props.card.value) {
|
|
||||||
case "TWO":
|
|
||||||
return new URL("@/assets/cards/clubs_two.png", import.meta.url).href;
|
|
||||||
case "THREE":
|
|
||||||
return new URL("@/assets/cards/clubs_three.png", import.meta.url).href;
|
|
||||||
case "FOUR":
|
|
||||||
return new URL("@/assets/cards/clubs_four.png", import.meta.url).href;
|
|
||||||
case "FIVE":
|
|
||||||
return new URL("@/assets/cards/clubs_five.png", import.meta.url).href;
|
|
||||||
case "SIX":
|
|
||||||
return new URL("@/assets/cards/clubs_six.png", import.meta.url).href;
|
|
||||||
case "SEVEN":
|
|
||||||
return new URL("@/assets/cards/clubs_seven.png", import.meta.url).href;
|
|
||||||
case "EIGHT":
|
|
||||||
return new URL("@/assets/cards/clubs_eight.png", import.meta.url).href;
|
|
||||||
case "NINE":
|
|
||||||
return new URL("@/assets/cards/clubs_nine.png", import.meta.url).href;
|
|
||||||
case "TEN":
|
|
||||||
return new URL("@/assets/cards/clubs_ten.png", import.meta.url).href;
|
|
||||||
case "JACK":
|
|
||||||
return new URL("@/assets/cards/clubs_jack.png", import.meta.url).href;
|
|
||||||
case "QUEEN":
|
|
||||||
return new URL("@/assets/cards/clubs_queen.png", import.meta.url).href;
|
|
||||||
case "KING":
|
|
||||||
return new URL("@/assets/cards/clubs_king.png", import.meta.url).href;
|
|
||||||
case "ACE":
|
|
||||||
return new URL("@/assets/cards/clubs_ace.png", import.meta.url).href;
|
|
||||||
default:
|
|
||||||
return new URL("@/assets/cards/back.png", import.meta.url).href;
|
|
||||||
}
|
|
||||||
case "JOKER":
|
|
||||||
switch (props.card.value) {
|
|
||||||
case "RED":
|
|
||||||
return new URL("@/assets/cards/joker_red.png", import.meta.url).href;
|
|
||||||
case "BLACK":
|
|
||||||
return new URL("@/assets/cards/joker_black.png", import.meta.url).href;
|
|
||||||
default:
|
|
||||||
return new URL("@/assets/cards/back.png", import.meta.url).href;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return new URL("@/assets/cards/back.png", import.meta.url).href;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
emits('click', props.card);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<StylessButton class="card" :class="hoverable ? 'card__hover' : ''" :disabled="!clickable" @click="onClick">
|
|
||||||
<img class="card__texture" :src="imageUrl" :alt="card?.type + ' ' + card?.value"/>
|
|
||||||
</StylessButton>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.card {
|
|
||||||
width: 100px;
|
|
||||||
height: 150px;
|
|
||||||
min-width: 100px;
|
|
||||||
|
|
||||||
&__texture {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__hover {
|
|
||||||
img {
|
|
||||||
transition: margin-top 250ms ease-in-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
margin-top: -1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import Backdrop from "@/components/layout/Backdrop.vue";
|
|
||||||
import {CardType} from "@/types/card.type";
|
|
||||||
import SpadesImage from "@mdi/svg/svg/cards-spade.svg";
|
|
||||||
import HeartsImage from "@mdi/svg/svg/cards-heart.svg";
|
|
||||||
import DiamondsImage from "@mdi/svg/svg/cards-diamond.svg";
|
|
||||||
import ClubsImage from "@mdi/svg/svg/cards-club.svg";
|
|
||||||
import {computed} from "vue";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
currentType: CardType;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const currentTypeImage = computed(() => {
|
|
||||||
switch (props.currentType) {
|
|
||||||
case CardType.SPADES:
|
|
||||||
return SpadesImage;
|
|
||||||
case CardType.HEARTS:
|
|
||||||
return HeartsImage;
|
|
||||||
case CardType.DIAMONDS:
|
|
||||||
return DiamondsImage;
|
|
||||||
case CardType.CLUBS:
|
|
||||||
return ClubsImage;
|
|
||||||
default:
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Backdrop>
|
|
||||||
<div class="current-type-backdrop">
|
|
||||||
<Transition v-if="currentTypeImage">
|
|
||||||
<img :key="currentType" :src="currentTypeImage" :alt="currentType">
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</Backdrop>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/layout.scss";
|
|
||||||
|
|
||||||
.current-type-backdrop {
|
|
||||||
@extend .container-fill, .place-center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-enter-active,
|
|
||||||
.v-leave-active {
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-enter-from,
|
|
||||||
.v-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type {ICard} from "@/types/card.type";
|
|
||||||
import Card from "@/components/maumau/game/Card.vue";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
currentCard?: ICard,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: 'click'): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const onNewCardClick = () => {
|
|
||||||
emits('click');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="deck">
|
|
||||||
<Card :card="currentCard" :clickable="false" />
|
|
||||||
<Card class="deck-new-cards" @click="onNewCardClick" clickable hoverable />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.deck {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
&-new-cards {
|
|
||||||
background-image: url("@/assets/cards/back.png");
|
|
||||||
background-size: 100px 150px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import Hand from "@/components/maumau/game/Hand.vue";
|
|
||||||
import Deck from "@/components/maumau/game/Deck.vue";
|
|
||||||
import type {useGame} from "@/composables/game.composable";
|
|
||||||
import {ref, watch} from "vue";
|
|
||||||
import CurrentTypeBackdrop from "@/components/maumau/game/CurrentTypeBackdrop.vue";
|
|
||||||
import PlayerList from "@/components/maumau/game/PlayerList.vue";
|
|
||||||
import TypeChoiceModal from "@/components/maumau/game/TypeChoiceModal.vue";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
game: ReturnType<typeof useGame>,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const gameState = ref(props.game.state.value);
|
|
||||||
|
|
||||||
const setTurnTitle = () => {
|
|
||||||
if (gameState.value.CurrentPlayer.Id === gameState.value.Me.Id){
|
|
||||||
document.title = "Mau - Your turn!"
|
|
||||||
} else {
|
|
||||||
document.title = "Mau - " + gameState.value.CurrentPlayer.Name + "'s turn!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTurnTitle();
|
|
||||||
|
|
||||||
watch(() => props.game.state.value, (newState) => {
|
|
||||||
gameState.value = newState;
|
|
||||||
|
|
||||||
setTurnTitle();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="game">
|
|
||||||
<div class="game-container">
|
|
||||||
<PlayerList :current="gameState.CurrentPlayer" :players="gameState.Players" :me="gameState.Me" />
|
|
||||||
<Deck :currentCard="gameState.CurrentCard" @click="game.onGrabCard" />
|
|
||||||
<Hand :cards="gameState.Hand" @click="game.onPlayCard" />
|
|
||||||
</div>
|
|
||||||
<TypeChoiceModal :show="gameState.MyState === 'CHOOSE'" @choice="game.onCardTypeChoice" />
|
|
||||||
<CurrentTypeBackdrop :currentType="gameState.NextAllowedCardType ?? gameState.CurrentCard.type" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/layout";
|
|
||||||
|
|
||||||
.game {
|
|
||||||
@extend .container-fill;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&-container {
|
|
||||||
@extend .container-fill-height;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: repeat(3, 1fr);
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import type {ICard} from "@/types/card.type";
|
|
||||||
import Card from "@/components/maumau/game/Card.vue";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
cards?: ICard[],
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: 'click', card: ICard): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const onCardClick = (card?: ICard) => {
|
|
||||||
if (!card) return;
|
|
||||||
emits('click', card);
|
|
||||||
}
|
|
||||||
|
|
||||||
const keepItemInPlace = (el: Element) => {
|
|
||||||
const htmlElement = el as HTMLElement;
|
|
||||||
const rect = htmlElement.getBoundingClientRect();
|
|
||||||
htmlElement.style.left = `${rect.left}px`;
|
|
||||||
htmlElement.style.top = `${rect.top}px`;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ul class="hand">
|
|
||||||
<TransitionGroup @beforeLeave="keepItemInPlace">
|
|
||||||
<li v-for="card in cards" :key="card.id">
|
|
||||||
<Card :card="card" @click="onCardClick" hoverable/>
|
|
||||||
</li>
|
|
||||||
</TransitionGroup>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/layout";
|
|
||||||
|
|
||||||
.hand {
|
|
||||||
@extend .container-fill-height;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
flex-flow: row nowrap !important;
|
|
||||||
overflow-x: scroll;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-enter-from, .v-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-25%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-leave-active {
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-enter-active,
|
|
||||||
.v-leave-active,
|
|
||||||
.v-move {
|
|
||||||
transition: all 0.5s ease-out;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type {Player} from "@/composables/game.composable";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
players: Player[],
|
|
||||||
me: Player,
|
|
||||||
current: Player,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const isTurn = (player: Player) => player.Id === props.current.Id;
|
|
||||||
const isMe = (player: Player) => player.Id === props.me.Id;
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ol class="player-list">
|
|
||||||
<li v-for="player in players" :key="player.Id" class="player-list__info" :class="isTurn(player) ? 'current-turn' : ''">
|
|
||||||
<h3>{{ player.Name }} {{ isMe(player) ? "(You)" : "" }}</h3>
|
|
||||||
<p>{{ player.CardsLeft }} cards left</p>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/layout";
|
|
||||||
|
|
||||||
.player-list {
|
|
||||||
@extend .container-fill-width;
|
|
||||||
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
&__info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
place-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-turn {
|
|
||||||
border: 1px solid red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {CardType} from "@/types/card.type";
|
|
||||||
import Modal from "@/components/Modal.vue";
|
|
||||||
import StylessButton from "@/components/button/StylessButton.vue";
|
|
||||||
|
|
||||||
interface Choice {
|
|
||||||
type: CardType,
|
|
||||||
char: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
show?: boolean
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: "choice", cardType: CardType): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const choices: Choice[] = [
|
|
||||||
{type: CardType.SPADES, char: '♠'},
|
|
||||||
{type: CardType.HEARTS, char: '♥'},
|
|
||||||
{type: CardType.DIAMONDS, char: '♦'},
|
|
||||||
{type: CardType.CLUBS, char: '♣'}
|
|
||||||
];
|
|
||||||
|
|
||||||
const onTypeChoice = (cardType: CardType) => {
|
|
||||||
emits("choice", cardType);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getReadableName = (cardType: CardType) => {
|
|
||||||
return cardType.charAt(0) + cardType.substring(1).toLowerCase();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Modal :show="show">
|
|
||||||
<div class="choice-modal">
|
|
||||||
<div class="choice-modal__choices">
|
|
||||||
<StylessButton v-for="choice in choices" :key="choice.type" @click="onTypeChoice(choice.type)"
|
|
||||||
:title="getReadableName(choice.type)">
|
|
||||||
{{ choice.char }}
|
|
||||||
</StylessButton>
|
|
||||||
</div>
|
|
||||||
<h3 class="choice-modal__title">Choose a card type</h3>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/variables";
|
|
||||||
|
|
||||||
.choice-modal {
|
|
||||||
background-color: $modal-color;
|
|
||||||
color: $modal-text-color;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
|
|
||||||
&__choices {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type {useGame} from "@/composables/game.composable";
|
|
||||||
import {onMounted} from "vue";
|
|
||||||
import WinnerModal from "@/components/maumau/lobby/WinnerModal.vue";
|
|
||||||
import PrimaryButton from "@/components/button/PrimaryButton.vue";
|
|
||||||
import PlayerList from "@/components/maumau/lobby/PlayerList.vue";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
game: ReturnType<typeof useGame>,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.title = "Mau - Waiting for host to start the game...";
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="lobby">
|
|
||||||
<h1 class="lobby-title">Waiting for host to start the game</h1>
|
|
||||||
|
|
||||||
<PlayerList :players="game.players.value" @kick="game.onKick"/>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Game Settings</h2>
|
|
||||||
<p>Not available yet...</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<PrimaryButton @click="game.startGame()">Start Game</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<WinnerModal :winner="props.game.winner.value" @close="game.clearWinner"/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/layout";
|
|
||||||
|
|
||||||
.lobby {
|
|
||||||
@extend .container-fill-height;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr 1fr auto;
|
|
||||||
grid-gap: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import DangerButton from "@/components/button/DangerButton.vue";
|
|
||||||
import type {Connection} from "@/types/connection.type";
|
|
||||||
import PrimaryButton from "@/components/button/PrimaryButton.vue";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
players: Connection[]
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: "kick", player: Connection): void
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const onKick = (player: Connection) => {
|
|
||||||
emits("kick", player);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section class="player-list">
|
|
||||||
<header class="player-list__header">
|
|
||||||
<h2>Players</h2>
|
|
||||||
<PrimaryButton>Add bot</PrimaryButton>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<ol class="list">
|
|
||||||
<li class="player" v-for="player in players" :key="player.Id">
|
|
||||||
<p>{{ player.Name }}</p>
|
|
||||||
<DangerButton @click="onKick(player)">Kick</DangerButton>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/variables";
|
|
||||||
|
|
||||||
.player-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
list-style: none;
|
|
||||||
padding-left: 0;
|
|
||||||
overflow-y: scroll;
|
|
||||||
|
|
||||||
.player {
|
|
||||||
border-radius: $border-radius;
|
|
||||||
padding: 0.5rem;
|
|
||||||
background-color: $surface-color;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import StylessButton from "@/components/button/StylessButton.vue";
|
|
||||||
import type {Connection} from "@/types/connection.type";
|
|
||||||
import Modal from "@/components/Modal.vue";
|
|
||||||
import ConfettiExplosion from "vue-confetti-explosion";
|
|
||||||
import {useWindowSize} from "@vueuse/core";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
winner?: Connection,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const {width, height} = useWindowSize();
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: "close"): void,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
emits("close");
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Modal :show="winner !== undefined">
|
|
||||||
<div class="winner-modal">
|
|
||||||
<StylessButton class="winner-modal__close" @click="onClose">⨉</StylessButton>
|
|
||||||
<h2 class="winner-modal__title">{{ winner!.Name }} won!</h2>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
<Teleport v-if="winner !== undefined" to="body">
|
|
||||||
<div class="confetti">
|
|
||||||
<ConfettiExplosion :stageHeight="height" :stageWidth="width" :colors="['#FFA500', '#FFA500', '#FFFFFF']"/>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/layout";
|
|
||||||
@import "@/scss/variables";
|
|
||||||
|
|
||||||
.winner-modal {
|
|
||||||
background-color: $modal-color;
|
|
||||||
color: $modal-text-color;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 3rem 4rem;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
font-size: 3rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__close {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.confetti {
|
|
||||||
@extend .container-fill;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
import {useUserName} from "@/stores/userName";
|
|
||||||
import {useWebSocket, WebSocketCloseCodes, type WebSocketMessage} from "@/composables/webSocket";
|
|
||||||
import {ref} from "vue";
|
|
||||||
import {type ChatMessage} from "@/components/Chat.vue";
|
|
||||||
import {useRouter} from "vue-router";
|
|
||||||
import type {Connection} from "@/types/connection.type";
|
|
||||||
import {useToast} from "vue-toast-notification";
|
|
||||||
import {CardType, CardValue, type ICard} from "@/types/card.type";
|
|
||||||
|
|
||||||
export interface Player extends Connection {
|
|
||||||
CardsLeft: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GameState {
|
|
||||||
Me: Player;
|
|
||||||
MyState: string;
|
|
||||||
Hand: ICard[];
|
|
||||||
CurrentCard: ICard;
|
|
||||||
NextAllowedCardType: CardType;
|
|
||||||
CurrentPlayer: Player;
|
|
||||||
Players: Player[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseCard = (data: string): ICard => {
|
|
||||||
const [typeString, valueString] = data.split(" ");
|
|
||||||
return {
|
|
||||||
id: data,
|
|
||||||
type: CardType[typeString as keyof typeof CardType],
|
|
||||||
value: CardValue[valueString as keyof typeof CardValue]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseGameState = (gameData: any): GameState => {
|
|
||||||
const hand: ICard[] = gameData.Hand.map(parseCard);
|
|
||||||
const currentCard = parseCard(gameData.CurrentCard);
|
|
||||||
return {
|
|
||||||
Me: gameData.Me,
|
|
||||||
MyState: gameData.MyState,
|
|
||||||
Hand: hand,
|
|
||||||
CurrentCard: currentCard,
|
|
||||||
NextAllowedCardType: CardType[gameData.NextAllowedCardType as keyof typeof CardType],
|
|
||||||
CurrentPlayer: gameData.CurrentPlayer,
|
|
||||||
Players: gameData.Players
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useGame = (roomId: string) => {
|
|
||||||
const userNameStore = useUserName();
|
|
||||||
const router = useRouter();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
|
|
||||||
const WS_URL = import.meta.env.VITE_WEBSOCKET_ENDPOINT || "__WEBSOCKET_ENDPOINT__";
|
|
||||||
|
|
||||||
const chatMessages = ref<ChatMessage[]>([]);
|
|
||||||
const players = ref<Connection[]>([]);
|
|
||||||
const currentRoomType = ref<string>("LOBBY");
|
|
||||||
const state = ref<GameState>({} as GameState);
|
|
||||||
const winner = ref<Connection | undefined>()
|
|
||||||
|
|
||||||
const webSocket = useWebSocket(WS_URL + "/room/" + roomId + "/" + userNameStore.userName, {
|
|
||||||
onOpen: (_event: Event) => {
|
|
||||||
console.log("Connected to WebSocket");
|
|
||||||
},
|
|
||||||
onMessage: (event: MessageEvent) => {
|
|
||||||
console.log("Message received:", event.data);
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
handleMessage(data);
|
|
||||||
},
|
|
||||||
onClose: (_event: Event) => {
|
|
||||||
router.push("/").then(() => console.log("Disconnected from WebSocket"));
|
|
||||||
},
|
|
||||||
onError: (error: Event) => {
|
|
||||||
toast.error("Something went wrong!")
|
|
||||||
console.error("WebSocket error", error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleMessage = (message: WebSocketMessage) => {
|
|
||||||
const data = JSON.parse(message.Data);
|
|
||||||
switch (message.Type) {
|
|
||||||
case "CHAT":
|
|
||||||
chatMessages.value.push(data);
|
|
||||||
break;
|
|
||||||
case "JOIN":
|
|
||||||
players.value = data.Connections;
|
|
||||||
chatMessages.value.push({
|
|
||||||
Id: Date.now().toString(),
|
|
||||||
Sender: "System",
|
|
||||||
Message: data.NewConnection.Name + " joined!"
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "GAME":
|
|
||||||
currentRoomType.value = "GAME";
|
|
||||||
state.value = parseGameState(data);
|
|
||||||
break;
|
|
||||||
case "END":
|
|
||||||
currentRoomType.value = "LOBBY";
|
|
||||||
winner.value = data.Winner;
|
|
||||||
state.value = {} as GameState;
|
|
||||||
break;
|
|
||||||
case "LEAVE":
|
|
||||||
const playerIndex = players.value.findIndex(player => player.Id === data.Connection.Id);
|
|
||||||
if (playerIndex !== -1) {
|
|
||||||
players.value.splice(playerIndex, 1);
|
|
||||||
chatMessages.value.push({
|
|
||||||
Id: Date.now().toString(),
|
|
||||||
Sender: "System",
|
|
||||||
Message: data.Connection.Name + " left!"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const startGame = () => {
|
|
||||||
webSocket.sendJsonMessage("LOBBY", "START");
|
|
||||||
}
|
|
||||||
|
|
||||||
const onChatSend = (chatMessage: string) => {
|
|
||||||
webSocket.sendJsonMessage("CHAT", chatMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onLeaveGame = async () => {
|
|
||||||
webSocket.close(WebSocketCloseCodes.CLOSE_GOING_AWAY, "User left the room");
|
|
||||||
await router.push("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
const onGrabCard = () => {
|
|
||||||
webSocket.sendJsonMessage("DRAW");
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPlayCard = (card: ICard) => {
|
|
||||||
webSocket.sendJsonMessage("PLAY", {CardType: card.type, CardValue: card.value});
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKick = (player: Connection) => {
|
|
||||||
webSocket.sendJsonMessage("KICK", player.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCardTypeChoice = (cardType: CardType) => {
|
|
||||||
webSocket.sendJsonMessage("CHOOSE", cardType);
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearWinner = () => {
|
|
||||||
winner.value = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
webSocket,
|
|
||||||
currentRoomType,
|
|
||||||
chatMessages,
|
|
||||||
players,
|
|
||||||
state,
|
|
||||||
winner,
|
|
||||||
startGame,
|
|
||||||
onChatSend,
|
|
||||||
onLeaveGame,
|
|
||||||
onGrabCard,
|
|
||||||
onPlayCard,
|
|
||||||
onKick,
|
|
||||||
onCardTypeChoice,
|
|
||||||
clearWinner,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
/**
|
|
||||||
* A list of WebSocket close codes. </br>
|
|
||||||
* Beside this list of codes, a custom code between the range 3000-4999 may be used for private use.
|
|
||||||
* @see https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
|
|
||||||
*/
|
|
||||||
export const WebSocketCloseCodes = {
|
|
||||||
// Successful operation / regular socket shutdown
|
|
||||||
CLOSE_NORMAL: 1000,
|
|
||||||
// Client is leaving (browser tab closing)
|
|
||||||
CLOSE_GOING_AWAY: 1001,
|
|
||||||
// Endpoint received a malformed frame
|
|
||||||
CLOSE_PROTOCOL_ERROR: 1002,
|
|
||||||
// Endpoint received an unsupported frame (e.g. binary-only endpoint received text frame)
|
|
||||||
CLOSE_UNSUPPORTED: 1003,
|
|
||||||
// Expected close status, received none
|
|
||||||
CLOSE_NO_STATUS: 1005,
|
|
||||||
// No close code frame has been received
|
|
||||||
CLOSE_ABNORMAL: 1006,
|
|
||||||
// Endpoint received inconsistent message (e.g. malformed UTF-8)
|
|
||||||
UNSUPPORTED_PAYLOAD: 1007,
|
|
||||||
// Generic code used for situations other than 1003 and 1009
|
|
||||||
POLICY_VIOLATION: 1008,
|
|
||||||
// Endpoint won't process large frame
|
|
||||||
CLOSE_TOO_LARGE: 1009,
|
|
||||||
// Client wanted an extension which server did not negotiate
|
|
||||||
MANDATORY_EXTENSION: 1010,
|
|
||||||
// Internal server error while operating
|
|
||||||
SERVER_ERROR: 1011,
|
|
||||||
// Server/service is restarting
|
|
||||||
SERVICE_RESTART: 1012,
|
|
||||||
// Temporary server condition forced blocking client's request
|
|
||||||
TRY_AGAIN_LATER: 1013,
|
|
||||||
// Server acting as gateway received an invalid response
|
|
||||||
BAD_GATEWAY: 1014,
|
|
||||||
// Transport Layer Security handshake failure
|
|
||||||
TLS_HANDSHAKE_FAIL: 1015
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export interface WebSocketMessage {
|
|
||||||
Type: string;
|
|
||||||
Data: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseWebSocketOptions {
|
|
||||||
onClose?: (event: CloseEvent) => void;
|
|
||||||
onError?: (event: Event) => void;
|
|
||||||
onMessage?: (event: MessageEvent) => void;
|
|
||||||
onOpen?: (event: Event) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useWebSocket = (url: string, options: UseWebSocketOptions) => {
|
|
||||||
const socket = new WebSocket(url);
|
|
||||||
|
|
||||||
const isOpen = () => {
|
|
||||||
return socket.readyState === WebSocket.OPEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.onClose) {
|
|
||||||
socket.onclose = options.onClose;
|
|
||||||
}
|
|
||||||
if (options.onError) {
|
|
||||||
socket.onerror = options.onError;
|
|
||||||
}
|
|
||||||
if (options.onMessage) {
|
|
||||||
socket.onmessage = options.onMessage;
|
|
||||||
}
|
|
||||||
if (options.onOpen) {
|
|
||||||
socket.onopen = options.onOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendMessage = (message: string) => {
|
|
||||||
console.log("Sending message: ", message);
|
|
||||||
socket.send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendJsonMessage = (type: string, data?: any) => {
|
|
||||||
if (data instanceof String || typeof data === "string") {
|
|
||||||
sendMessage(JSON.stringify({Type: type, Data: data}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sendMessage(JSON.stringify({Type: type, Data: JSON.stringify(data)}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = (code?: number, reason?: string) => {
|
|
||||||
if (!isOpen()) return;
|
|
||||||
// TODO: Weird behavior where closing doesnt work with code/reason
|
|
||||||
// socket.close(code, reason);
|
|
||||||
socket.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {socket, isOpen, sendMessage, sendJsonMessage, close};
|
|
||||||
}
|
|
||||||
21
src/config/Router.tsx
Normal file
21
src/config/Router.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {BrowserRouter, Route, Routes} from "react-router-dom";
|
||||||
|
import MainLobby from "../layout/pages/MainLobby";
|
||||||
|
import NotFound from "../layout/pages/NotFound";
|
||||||
|
import Room from "../layout/pages/Room";
|
||||||
|
import Rooms from "../layout/pages/Rooms";
|
||||||
|
|
||||||
|
const Router = () => {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<MainLobby/>}/>
|
||||||
|
<Route path="rooms" element={<Rooms/>}/>
|
||||||
|
<Route path="room/:roomId" element={<Room/>}/>
|
||||||
|
<Route path="*" element={<NotFound/>}/>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Router;
|
||||||
15
src/index.tsx
Normal file
15
src/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import ThemeContextProvider from "./utils/contexts/ThemeContext";
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(
|
||||||
|
document.getElementById('root') as HTMLElement
|
||||||
|
);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ThemeContextProvider>
|
||||||
|
<App/>
|
||||||
|
</ThemeContextProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
38
src/layout/components/AnimatedDiv.tsx
Normal file
38
src/layout/components/AnimatedDiv.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {FunctionComponent} from "react";
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
animation?: any;
|
||||||
|
className?: string;
|
||||||
|
children: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAnimation = {
|
||||||
|
initial: {
|
||||||
|
opacity: 0
|
||||||
|
},
|
||||||
|
animate: {
|
||||||
|
opacity: 1
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
transition: {
|
||||||
|
duration: .1,
|
||||||
|
ease: 'easeInOut'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const AnimatedDiv: FunctionComponent<Props> = ({animation, className, children}) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
variants={animation ?? defaultAnimation}
|
||||||
|
initial={"initial"}
|
||||||
|
animate={"animate"}
|
||||||
|
exit={"exit"}
|
||||||
|
className={className}>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnimatedDiv;
|
||||||
26
src/layout/components/Button.tsx
Normal file
26
src/layout/components/Button.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React, {ButtonHTMLAttributes, DetailedHTMLProps, FunctionComponent} from "react";
|
||||||
|
|
||||||
|
interface ButtonProps extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoButton: FunctionComponent<ButtonProps> = (props) => {
|
||||||
|
|
||||||
|
const {className, ...rest} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={getClassName("no-button", className)} {...rest}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button: FunctionComponent<ButtonProps> = (props) => {
|
||||||
|
const {className, ...rest} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={getClassName("button", className)} {...rest}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getClassName = (templateClass: string, otherClass?: string) => {
|
||||||
|
return templateClass + (otherClass ? ` ${otherClass}` : "");
|
||||||
|
}
|
||||||
48
src/layout/components/Card.tsx
Normal file
48
src/layout/components/Card.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React, {FunctionComponent} from "react";
|
||||||
|
import {NoButton} from "./Button";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cardString: string;
|
||||||
|
handleClick?: (cardString: string) => void;
|
||||||
|
isHidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Card: FunctionComponent<Props> = ({cardString, handleClick, isHidden}) => {
|
||||||
|
const cardType = cardString.split(' ')[0].toLowerCase();
|
||||||
|
const cardValue = cardString.split(' ')[1].toLowerCase();
|
||||||
|
|
||||||
|
const cardSource = (): string => {
|
||||||
|
if (isHidden) return require("../../assets/cards/back.png");
|
||||||
|
if (cardType === "JOKER") {
|
||||||
|
if (cardValue === "RED") {
|
||||||
|
return require("../../assets/cards/joker_red.png")
|
||||||
|
}
|
||||||
|
return require("../../assets/cards/joker_black.png")
|
||||||
|
}
|
||||||
|
return require(`../../assets/cards/${cardType}_${cardValue}.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardName = isHidden ? 'back' : `${cardType} ${cardValue}`;
|
||||||
|
|
||||||
|
const handleCardClick = () => {
|
||||||
|
if (handleClick) {
|
||||||
|
handleClick(cardString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handleClick) {
|
||||||
|
return (
|
||||||
|
<NoButton className={"card"} onClick={handleCardClick}>
|
||||||
|
<img className="card__texture" src={cardSource()} alt={cardName}/>
|
||||||
|
</NoButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"card"}>
|
||||||
|
<img className="card__texture" src={cardSource()} alt={cardName}/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Card;
|
||||||
34
src/layout/components/Chat.tsx
Normal file
34
src/layout/components/Chat.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React, {FormEvent, FunctionComponent, RefObject} from "react";
|
||||||
|
import {NoButton} from "./Button";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
chatRef: RefObject<HTMLOListElement>;
|
||||||
|
handleSend: (message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Chat: FunctionComponent<Props> = ({chatRef, handleSend}) => {
|
||||||
|
|
||||||
|
const handleChat = (form: FormEvent<HTMLFormElement>) => {
|
||||||
|
form.preventDefault();
|
||||||
|
const data = new FormData(form.currentTarget);
|
||||||
|
const chatInput = data.get("chat-input") as string;
|
||||||
|
if (!chatInput) return;
|
||||||
|
handleSend(chatInput);
|
||||||
|
form.currentTarget.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ol className={"chat__list"} ref={chatRef}/>
|
||||||
|
<form className={"chat-form"} onSubmit={handleChat}>
|
||||||
|
<label className={"chat-form__input"}>
|
||||||
|
<input type={"text"} placeholder={"Chat here..."} id={"chat-input"} name={"chat-input"}
|
||||||
|
autoComplete={"off"}/>
|
||||||
|
<NoButton type={"submit"} className={"bi bi-send"}/>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Chat;
|
||||||
27
src/layout/components/Deck.tsx
Normal file
27
src/layout/components/Deck.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React, {FunctionComponent} from "react";
|
||||||
|
import Card from "./Card";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentCard: string;
|
||||||
|
actionOnClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Deck: FunctionComponent<Props> = ({currentCard, actionOnClick}) => {
|
||||||
|
|
||||||
|
const handleClick = (card: string) => {
|
||||||
|
actionOnClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="deck-container">
|
||||||
|
<div className="used-cards">
|
||||||
|
<Card cardString={currentCard}/>
|
||||||
|
</div>
|
||||||
|
<div className="deck">
|
||||||
|
<Card cardString={"AA BB"} handleClick={handleClick} isHidden/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Deck;
|
||||||
27
src/layout/components/Hand.tsx
Normal file
27
src/layout/components/Hand.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React, {FunctionComponent} from 'react';
|
||||||
|
import Card from "./Card";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
hand: string[];
|
||||||
|
actionOnClick?: (cardString: string) => void;
|
||||||
|
isHidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Hand: FunctionComponent<Props> = ({hand, actionOnClick, isHidden}) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="hand">
|
||||||
|
{
|
||||||
|
hand.map((card, index) => {
|
||||||
|
return (
|
||||||
|
<li key={card} className={"hand__item"}>
|
||||||
|
<Card cardString={card} handleClick={actionOnClick} isHidden={isHidden}/>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Hand;
|
||||||
25
src/layout/components/Modal.tsx
Normal file
25
src/layout/components/Modal.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {FunctionComponent} from "react";
|
||||||
|
import {createPortal} from "react-dom";
|
||||||
|
import AnimatedDiv from "./AnimatedDiv";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean
|
||||||
|
children: JSX.Element | JSX.Element[];
|
||||||
|
modalRoot: HTMLElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modal: FunctionComponent<Props> = (props) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
props.isOpen &&
|
||||||
|
createPortal(<AnimatedDiv className={"modal-root"}>
|
||||||
|
<div className={"modal-container"}>{props.children}</div>
|
||||||
|
</AnimatedDiv>, props.modalRoot ?? document.body)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
51
src/layout/pages/MainLobby.tsx
Normal file
51
src/layout/pages/MainLobby.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React from "react";
|
||||||
|
import useTitle from "../../utils/hooks/TitleHook";
|
||||||
|
import Card from "../components/Card";
|
||||||
|
import {useNavigate} from "react-router";
|
||||||
|
import {NoButton} from "../components/Button";
|
||||||
|
|
||||||
|
const ROOM_URL = `${process.env.REACT_APP_API_URL}/room`;
|
||||||
|
|
||||||
|
const MainLobby = () => {
|
||||||
|
|
||||||
|
useTitle('Mau-Mau Lobby');
|
||||||
|
|
||||||
|
const navigateTo = useNavigate();
|
||||||
|
const playerName = localStorage.getItem('playerName') ?? "";
|
||||||
|
|
||||||
|
const handleCreateRoom = () => {
|
||||||
|
fetch(ROOM_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
}).then(res => res.json()).then(room => {
|
||||||
|
navigateTo(`/room/${room}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const changePlayerName = (name: string) => {
|
||||||
|
if (!name) return;
|
||||||
|
localStorage.setItem('playerName', name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"main-lobby"}>
|
||||||
|
<h1 className={"main-lobby__title mau"}>Mau-Mau</h1>
|
||||||
|
<div className={"main-lobby__name mau"}>
|
||||||
|
<label htmlFor={"name-input"}>Enter your name</label>
|
||||||
|
<input id={"name-input"} type="text" placeholder="Enter your name"
|
||||||
|
defaultValue={playerName} onBlur={(e) => changePlayerName(e.target.value)}/>
|
||||||
|
</div>
|
||||||
|
<div className={"main-lobby__container"}>
|
||||||
|
<NoButton className={"main-lobby__container-button"} onClick={handleCreateRoom}>
|
||||||
|
<h2 className={"mau"}>Host Game</h2>
|
||||||
|
<Card cardString={'SPADES ACE'}/>
|
||||||
|
</NoButton>
|
||||||
|
<NoButton className={"main-lobby__container-button"} onClick={() => navigateTo('/rooms')}>
|
||||||
|
<h2 className={"mau"}>Join Game</h2>
|
||||||
|
<Card cardString={'SPADES ACE'} isHidden/>
|
||||||
|
</NoButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MainLobby;
|
||||||
9
src/layout/pages/NotFound.tsx
Normal file
9
src/layout/pages/NotFound.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const NotFound = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
404
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
157
src/layout/pages/Room.tsx
Normal file
157
src/layout/pages/Room.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import useWebSocket from "react-use-websocket";
|
||||||
|
import React, {useEffect} from "react";
|
||||||
|
import {useNavigate, useParams} from "react-router";
|
||||||
|
import useTitle from "../../utils/hooks/TitleHook";
|
||||||
|
import Game, {GameState} from "../views/Game/Game";
|
||||||
|
import Lobby from "../views/Lobby/Lobby";
|
||||||
|
import Chat from "../components/Chat";
|
||||||
|
import {Button} from "../components/Button";
|
||||||
|
|
||||||
|
export interface Connection {
|
||||||
|
Id: string;
|
||||||
|
Name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Player extends Connection {
|
||||||
|
CardsLeft: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SocketMessage {
|
||||||
|
Type: string;
|
||||||
|
Data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Room = () => {
|
||||||
|
|
||||||
|
useTitle('Mau!');
|
||||||
|
|
||||||
|
const navigateTo = useNavigate();
|
||||||
|
|
||||||
|
const {roomId} = useParams();
|
||||||
|
|
||||||
|
const playerName = localStorage.getItem('playerName') ?? "";
|
||||||
|
const WS_URL = `${process.env.REACT_APP_WEBSOCKET_URL}/room/${roomId}/${playerName}`;
|
||||||
|
|
||||||
|
const [gameState, setGameState] = React.useState<GameState | undefined>(undefined);
|
||||||
|
const [winner, setWinner] = React.useState<string | undefined>(undefined);
|
||||||
|
const [playerList, setPlayerList] = React.useState<Connection[]>([]);
|
||||||
|
|
||||||
|
const chatRef = React.useRef<HTMLOListElement>(null);
|
||||||
|
|
||||||
|
const addChatMessage = (message: { Sender: string, Message: string }) => {
|
||||||
|
if (!chatRef.current) return;
|
||||||
|
|
||||||
|
const {scrollTop, scrollHeight, clientHeight} = chatRef.current;
|
||||||
|
|
||||||
|
const newChatElement = document.createElement('li');
|
||||||
|
newChatElement.innerHTML = `<strong>${message.Sender}:</strong> ${message.Message}`;
|
||||||
|
chatRef.current.appendChild(newChatElement);
|
||||||
|
|
||||||
|
const isNearBottom = scrollTop + clientHeight + newChatElement.scrollHeight * 2 >= scrollHeight;
|
||||||
|
if (isNearBottom) {
|
||||||
|
newChatElement.scrollIntoView({behavior: 'smooth'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const websocket = useWebSocket(WS_URL, {
|
||||||
|
onOpen: () => {
|
||||||
|
console.log('WebSocket connection established.');
|
||||||
|
},
|
||||||
|
onMessage: (messageEvent) => {
|
||||||
|
const data: SocketMessage = JSON.parse(messageEvent.data);
|
||||||
|
const payload = JSON.parse(data.Data);
|
||||||
|
switch (data.Type) {
|
||||||
|
case 'GAME':
|
||||||
|
setGameState(payload);
|
||||||
|
break;
|
||||||
|
case 'CHAT':
|
||||||
|
addChatMessage(payload);
|
||||||
|
break;
|
||||||
|
case "END":
|
||||||
|
setWinner(payload.Name)
|
||||||
|
setGameState(undefined);
|
||||||
|
break;
|
||||||
|
case "JOIN":
|
||||||
|
onJoin(payload);
|
||||||
|
break;
|
||||||
|
case "LEAVE":
|
||||||
|
onLeave(payload.Connection);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('Unknown message type:', data.Type);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClose: (closeEvent) => {
|
||||||
|
navigateTo('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onJoin = (payload: { Connections: Connection[], NewConnection: Connection }) => {
|
||||||
|
setPlayerList(payload.Connections);
|
||||||
|
addChatMessage({Sender: "System", Message: payload.NewConnection.Name + " has joined the room."});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLeave = (payload: Connection) => {
|
||||||
|
const connection = payload;
|
||||||
|
setPlayerList(playerList.filter(player => player.Id !== connection.Id));
|
||||||
|
addChatMessage({Sender: "System", Message: connection.Name + " has left the room."});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLeaveRoom = () => {
|
||||||
|
navigateTo('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSend = (message: SocketMessage) => {
|
||||||
|
websocket.sendMessage(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLobbyAction = (action: string) => {
|
||||||
|
handleSend({Type: "LOBBY", Data: JSON.stringify(action)});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKick = (playerId: string) => {
|
||||||
|
handleSend({Type: "KICK", Data: playerId});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChatMessage = (chatMessage: string) => {
|
||||||
|
handleSend({
|
||||||
|
Type: "CHAT",
|
||||||
|
Data: chatMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
const socket = websocket.getWebSocket();
|
||||||
|
if (socket) socket.close();
|
||||||
|
}
|
||||||
|
// this is a cleanup function and should not include any dependencies that would cause it to run more than once
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"room"}>
|
||||||
|
<aside className={"room-aside"}>
|
||||||
|
<header className={"aside-header"}>
|
||||||
|
<hgroup className={"aside-header__titles"}>
|
||||||
|
<h1>Mau-Mau</h1>
|
||||||
|
<h2>Room {roomId?.split("-")[0]}</h2>
|
||||||
|
</hgroup>
|
||||||
|
<Button onClick={handleLeaveRoom} className={"icon-start"}>
|
||||||
|
<i className="bi bi-arrow-bar-left"></i>
|
||||||
|
Leave Room
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
<Chat chatRef={chatRef} handleSend={handleChatMessage}/>
|
||||||
|
</aside>
|
||||||
|
{
|
||||||
|
gameState ?
|
||||||
|
<Game gameState={gameState} handleGameAction={handleSend}/>
|
||||||
|
:
|
||||||
|
<Lobby playerList={playerList} winner={winner} handleLobbyAction={handleLobbyAction} onKickClick={handleKick}/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Room;
|
||||||
44
src/layout/pages/Rooms.tsx
Normal file
44
src/layout/pages/Rooms.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import useTitle from "../../utils/hooks/TitleHook";
|
||||||
|
import React from "react";
|
||||||
|
import {useNavigate} from "react-router";
|
||||||
|
import {NoButton} from "../components/Button";
|
||||||
|
|
||||||
|
const ROOM_URL = `${process.env.REACT_APP_API_URL}/room`;
|
||||||
|
|
||||||
|
const Rooms = () => {
|
||||||
|
|
||||||
|
useTitle('Mau-Mau Rooms');
|
||||||
|
const navigateTo = useNavigate();
|
||||||
|
|
||||||
|
const [rooms, setRooms] = React.useState<string[]>([]);
|
||||||
|
|
||||||
|
const handleRoomClick = (room: string) => {
|
||||||
|
if (!localStorage.getItem('playerName')){
|
||||||
|
localStorage.setItem("playerName", "Mau");
|
||||||
|
}
|
||||||
|
navigateTo(`/room/${room}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
fetch(ROOM_URL)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => setRooms(data));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className={"mau"}>Rooms</h1>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
rooms.map((room) => {
|
||||||
|
return <li key={room}>
|
||||||
|
<NoButton onClick={() => handleRoomClick(room)}>{room}</NoButton>
|
||||||
|
</li>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Rooms;
|
||||||
84
src/layout/views/Game/Game.tsx
Normal file
84
src/layout/views/Game/Game.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import React, {FunctionComponent} from "react";
|
||||||
|
import {Player, SocketMessage} from "../../pages/Room";
|
||||||
|
import Deck from "../../components/Deck";
|
||||||
|
import Hand from "../../components/Hand";
|
||||||
|
import CardTypeChoiceModal from "./components/CardTypeChoiceModal";
|
||||||
|
|
||||||
|
export interface GameState {
|
||||||
|
Me: Player;
|
||||||
|
MyState: string;
|
||||||
|
Hand: string[];
|
||||||
|
CurrentCard: string;
|
||||||
|
NextAllowedCardType?: string
|
||||||
|
CurrentPlayer: Player;
|
||||||
|
Players: Player[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
gameState: GameState
|
||||||
|
handleGameAction: (message: SocketMessage) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Game: FunctionComponent<Props> = ({gameState, handleGameAction}) => {
|
||||||
|
|
||||||
|
const handleChoice = (choice: string) => {
|
||||||
|
handleGameAction({Type: 'CHOOSE', Data: choice});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDraw = () => {
|
||||||
|
handleGameAction({Type: 'DRAW', Data: ""});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCardSend = (cardString: string) => {
|
||||||
|
handleGameAction({
|
||||||
|
Type: "PLAY", Data: JSON.stringify({
|
||||||
|
CardType: cardString.split(' ')[0],
|
||||||
|
CardValue: cardString.split(' ')[1]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main id={"game"} className="game">
|
||||||
|
<ol className={"game__players"}>
|
||||||
|
{
|
||||||
|
gameState?.Me?.Id &&
|
||||||
|
gameState.Players.map((player) => {
|
||||||
|
const isCurrentPlayer = player.Id === gameState.CurrentPlayer.Id;
|
||||||
|
const isMe = player.Id === gameState.Me.Id;
|
||||||
|
|
||||||
|
const style =
|
||||||
|
{
|
||||||
|
fontWeight: isMe ? 'bold' : 'normal',
|
||||||
|
outline: isCurrentPlayer ? '2px solid red' : 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
return <li key={player.Id} style={style}>
|
||||||
|
<div className={"game__players__info"}>
|
||||||
|
<h3>{player.Name} {isMe && '(You)'}</h3>
|
||||||
|
<p>{player.CardsLeft} left</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ol>
|
||||||
|
{
|
||||||
|
gameState.NextAllowedCardType &&
|
||||||
|
<p>Next card type: {gameState.NextAllowedCardType}</p>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
gameState.CurrentCard &&
|
||||||
|
<Deck currentCard={gameState.CurrentCard} actionOnClick={handleDraw}/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
gameState.Hand &&
|
||||||
|
<Hand hand={gameState.Hand} actionOnClick={handleCardSend}/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
<CardTypeChoiceModal isOpen={gameState.MyState === 'CHOOSE'} handleChoice={handleChoice}/>
|
||||||
|
}
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Game;
|
||||||
41
src/layout/views/Game/components/CardTypeChoiceModal.tsx
Normal file
41
src/layout/views/Game/components/CardTypeChoiceModal.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React, {FunctionComponent} from "react";
|
||||||
|
import {NoButton} from "../../../components/Button";
|
||||||
|
import Modal from "../../../components/Modal";
|
||||||
|
|
||||||
|
export interface Choice {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleChoice: (choice: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardTypeChoices: Choice[] = [
|
||||||
|
{name: 'SPADES', icon: "bi-suit-spade-fill"},
|
||||||
|
{name: 'HEARTS', icon: "bi-suit-heart-fill"},
|
||||||
|
{name: 'DIAMONDS', icon: "bi-suit-diamond-fill"},
|
||||||
|
{name: 'CLUBS', icon: "bi-suit-club-fill"}
|
||||||
|
];
|
||||||
|
|
||||||
|
const CardTypeChoiceModal: FunctionComponent<Props> = (props) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={props.isOpen} modalRoot={document.getElementById("game")}>
|
||||||
|
<div className={"card-type-choice-modal"}>
|
||||||
|
<div className={"card-type-choice-modal__choices"}>
|
||||||
|
{
|
||||||
|
cardTypeChoices.map(type =>
|
||||||
|
<NoButton key={type.name} onClick={() => props.handleChoice(type.name)} aria-label={type.name.charAt(0) + type.name.substring(1).toLowerCase()}>
|
||||||
|
<i className={`bi ${type.icon}`}/>
|
||||||
|
</NoButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<h3 className={"card-type-choice-modal__title"}>Choose a card type</h3>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardTypeChoiceModal;
|
||||||
46
src/layout/views/Lobby/Lobby.tsx
Normal file
46
src/layout/views/Lobby/Lobby.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {FunctionComponent} from "react";
|
||||||
|
import {Connection} from "../../pages/Room";
|
||||||
|
import {Button} from "../../components/Button";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
winner: string | undefined;
|
||||||
|
handleLobbyAction: (action: string) => void;
|
||||||
|
playerList: Connection[];
|
||||||
|
onKickClick: (playerId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Lobby: FunctionComponent<Props> = ({winner, handleLobbyAction, playerList, onKickClick}) => {
|
||||||
|
|
||||||
|
const onStartClick = () => {
|
||||||
|
console.log('Start Game');
|
||||||
|
handleLobbyAction("START");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className={"game-lobby"}>
|
||||||
|
<h1>Waiting for host to start the game</h1>
|
||||||
|
<section>
|
||||||
|
<h2>Players</h2>
|
||||||
|
<ol className={"game-lobby__player-list"}>
|
||||||
|
{
|
||||||
|
playerList.map((player) => {
|
||||||
|
return <li key={player.Id}>
|
||||||
|
<p>{player.Name}</p>
|
||||||
|
<Button className={"button-danger"} onClick={() => onKickClick(player.Id)}>Kick</Button>
|
||||||
|
</li>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Game Settings</h2>
|
||||||
|
<i>Not available... yet?</i>
|
||||||
|
</section>
|
||||||
|
<Button className={"button-success game-lobby__start-game"} onClick={onStartClick}>
|
||||||
|
<strong>Start Game</strong>
|
||||||
|
</Button>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Lobby;
|
||||||
1
src/logo.svg
Normal file
1
src/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
17
src/main.ts
17
src/main.ts
@@ -1,17 +0,0 @@
|
|||||||
import {createApp} from 'vue';
|
|
||||||
import {createPinia} from 'pinia';
|
|
||||||
import ToastPlugin from 'vue-toast-notification';
|
|
||||||
import 'vue-toast-notification/dist/theme-bootstrap.css';
|
|
||||||
|
|
||||||
import App from './App.vue';
|
|
||||||
import router from './router';
|
|
||||||
|
|
||||||
const app = createApp(App);
|
|
||||||
|
|
||||||
app.use(createPinia());
|
|
||||||
app.use(router);
|
|
||||||
app.use(ToastPlugin, {
|
|
||||||
position: 'top-right',
|
|
||||||
});
|
|
||||||
|
|
||||||
app.mount('#app');
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import {createRouter, createWebHistory} from 'vue-router'
|
|
||||||
import HomeView from '../views/HomeView.vue'
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
name: 'home',
|
|
||||||
component: HomeView,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/rooms',
|
|
||||||
name: 'rooms',
|
|
||||||
component: () => import('../views/RoomsView.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/rooms/:roomId',
|
|
||||||
name: 'room',
|
|
||||||
component: () => import('../views/RoomView.vue'),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
router.beforeEach((to, from) => {
|
|
||||||
document.title = to.meta?.title as string ?? "MauMau!";
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
@import "./variables.scss";
|
|
||||||
|
|
||||||
.container {
|
|
||||||
&-full {
|
|
||||||
width: 100dvw;
|
|
||||||
height: 100dvh;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-fill {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-fill-width {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-fill-height {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-xs {
|
|
||||||
max-width: $breakpoint-xs;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-sm {
|
|
||||||
max-width: $breakpoint-sm;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-md {
|
|
||||||
max-width: $breakpoint-md;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-lg {
|
|
||||||
max-width: $breakpoint-lg;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-xl {
|
|
||||||
max-width: $breakpoint-xl;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-xxl {
|
|
||||||
max-width: $breakpoint-xxl;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.place {
|
|
||||||
&-center {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-start {
|
|
||||||
display: grid;
|
|
||||||
place-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-end {
|
|
||||||
display: grid;
|
|
||||||
place-items: end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
$background-color: #131313;
|
|
||||||
$background-text-color: #ffffff;
|
|
||||||
|
|
||||||
$surface-color: #2e2e2e;
|
|
||||||
$surface-text-color: #ffffff;
|
|
||||||
|
|
||||||
$modal-color: #9f7d2f;
|
|
||||||
$modal-text-color: #ffffff;
|
|
||||||
|
|
||||||
$danger-color: #6b0000;
|
|
||||||
$success-color: #276b00;
|
|
||||||
|
|
||||||
$breakpoint-xs: 320px;
|
|
||||||
$breakpoint-sm: 640px;
|
|
||||||
$breakpoint-md: 768px;
|
|
||||||
$breakpoint-lg: 1024px;
|
|
||||||
$breakpoint-xl: 1280px;
|
|
||||||
$breakpoint-xxl: 1536px;
|
|
||||||
|
|
||||||
$border-radius: 0.5rem;
|
|
||||||
5
src/setupTests.ts
Normal file
5
src/setupTests.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import {defineStore} from "pinia";
|
|
||||||
import {ref} from "vue";
|
|
||||||
|
|
||||||
export const useUserName = defineStore('username', () => {
|
|
||||||
const userName = ref<string>("Mau");
|
|
||||||
|
|
||||||
const setUserName = (nameParam?: string | null) => {
|
|
||||||
console.log("Setting username to: ", nameParam);
|
|
||||||
userName.value = nameParam || generateUserName();
|
|
||||||
localStorage.setItem("userName", userName.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateUserName = () => {
|
|
||||||
return "Mau-" + Math.random().toString(36).substring(2, 15);
|
|
||||||
}
|
|
||||||
|
|
||||||
setUserName(localStorage.getItem("userName"));
|
|
||||||
|
|
||||||
return { userName, setUserName }
|
|
||||||
})
|
|
||||||
43
src/styles/App.scss
Normal file
43
src/styles/App.scss
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
@import "vendor/vendor";
|
||||||
|
@import "layout/layout";
|
||||||
|
@import "utils/utils";
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Mau";
|
||||||
|
src: local('Mau'), url(../assets/fonts/OrientalCatsLight.otf) format('opentype');
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
height: 100dvh;
|
||||||
|
width: 100dvw;
|
||||||
|
background-color: #131313;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root, .app {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
padding: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mau {
|
||||||
|
font-family: "Mau", serif;
|
||||||
|
}
|
||||||
2
src/styles/layout/_layout.scss
Normal file
2
src/styles/layout/_layout.scss
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@import "pages/pages";
|
||||||
|
@import "components/components";
|
||||||
10
src/styles/layout/components/_components.scss
Normal file
10
src/styles/layout/components/_components.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@import "button";
|
||||||
|
@import "card";
|
||||||
|
@import "chat";
|
||||||
|
@import "card-type-choice-modal";
|
||||||
|
@import "deck";
|
||||||
|
@import "game";
|
||||||
|
@import "game-lobby";
|
||||||
|
@import "hand";
|
||||||
|
@import "modal";
|
||||||
|
@import "room-header";
|
||||||
46
src/styles/layout/components/button.scss
Normal file
46
src/styles/layout/components/button.scss
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
.icon-end {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-start {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-button {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: #2e2e2e;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #3e3e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-success {
|
||||||
|
background-color: #276b00;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #3e8e00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-danger {
|
||||||
|
background-color: #6b0000;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #8e0000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/styles/layout/components/card-type-choice-modal.scss
Normal file
17
src/styles/layout/components/card-type-choice-modal.scss
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.card-type-choice-modal {
|
||||||
|
background-color: #9f7d2f;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
|
||||||
|
&__choices {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/styles/layout/components/card.scss
Normal file
10
src/styles/layout/components/card.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.card {
|
||||||
|
width: 100px;
|
||||||
|
min-width: 100px;
|
||||||
|
|
||||||
|
&__texture {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/styles/layout/components/chat.scss
Normal file
38
src/styles/layout/components/chat.scss
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
.chat {
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
overflow-y: hidden;
|
||||||
|
word-wrap: break-word;
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1rem 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
background-color: #2e2e2e;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
> input {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/styles/layout/components/deck.scss
Normal file
6
src/styles/layout/components/deck.scss
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.deck-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
41
src/styles/layout/components/game-lobby.scss
Normal file
41
src/styles/layout/components/game-lobby.scss
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
.game-lobby {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr 1fr auto;
|
||||||
|
grid-row-gap: 1rem;
|
||||||
|
justify-items: center;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__player-list {
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
min-height: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
height: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background-color: #2e2e2e;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__start-game {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/styles/layout/components/game.scss
Normal file
26
src/styles/layout/components/game.scss
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
.game {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr 1fr 1fr;
|
||||||
|
place-items: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__players {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/styles/layout/components/hand.scss
Normal file
12
src/styles/layout/components/hand.scss
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.hand {
|
||||||
|
display: flex;
|
||||||
|
max-width: 100%;
|
||||||
|
flex-flow: row nowrap !important;
|
||||||
|
overflow-x: scroll;
|
||||||
|
padding: 0.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/styles/layout/components/modal.scss
Normal file
13
src/styles/layout/components/modal.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.modal {
|
||||||
|
&-root {
|
||||||
|
position: absolute;
|
||||||
|
padding: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/styles/layout/components/room-header.scss
Normal file
10
src/styles/layout/components/room-header.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.aside-header {
|
||||||
|
|
||||||
|
&__titles {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/styles/layout/pages/_pages.scss
Normal file
2
src/styles/layout/pages/_pages.scss
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@import "mainlobby";
|
||||||
|
@import "room";
|
||||||
29
src/styles/layout/pages/mainlobby.scss
Normal file
29
src/styles/layout/pages/mainlobby.scss
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.main-lobby {
|
||||||
|
height: 75vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 7.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/styles/layout/pages/room.scss
Normal file
19
src/styles/layout/pages/room.scss
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.room {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-column-gap: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&-aside {
|
||||||
|
display: grid;
|
||||||
|
min-width: 20%;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
overflow: hidden auto;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/styles/utils/_utils.scss
Normal file
1
src/styles/utils/_utils.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import "pointer";
|
||||||
9
src/styles/utils/pointer.scss
Normal file
9
src/styles/utils/pointer.scss
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.mouse-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mouse-default {
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
2
src/styles/vendor/_vendor.scss
vendored
Normal file
2
src/styles/vendor/_vendor.scss
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@import "bootstrap";
|
||||||
|
@import "normalize";
|
||||||
2
src/styles/vendor/bootstrap.scss
vendored
Normal file
2
src/styles/vendor/bootstrap.scss
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
//@import "../../../node_modules/bootstrap/scss/bootstrap.scss";
|
||||||
|
@import "../../../node_modules/bootstrap-icons/font/bootstrap-icons.scss";
|
||||||
352
src/styles/vendor/normalize.scss
vendored
Normal file
352
src/styles/vendor/normalize.scss
vendored
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
|
/* Document
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the line height in all browsers.
|
||||||
|
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the margin in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the `main` element consistently in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in Firefox.
|
||||||
|
* 2. Show the overflow in Edge and IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box; /* 1 */
|
||||||
|
height: 0; /* 1 */
|
||||||
|
overflow: visible; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background on active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the bottom border in Chrome 57-
|
||||||
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: none; /* 1 */
|
||||||
|
text-decoration: underline; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||||
|
* all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the border on images inside links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Change the font styles in all browsers.
|
||||||
|
* 2. Remove the margin in Firefox and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit; /* 1 */
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the overflow in IE.
|
||||||
|
* 1. Show the overflow in Edge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input { /* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||||
|
* 1. Remove the inheritance of text transform in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select { /* 1 */
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner border and padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the focus styles unset by the previous rule.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type="button"]:-moz-focusring,
|
||||||
|
[type="reset"]:-moz-focusring,
|
||||||
|
[type="submit"]:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 0.35em 0.75em 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the text wrapping in Edge and IE.
|
||||||
|
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||||
|
* 3. Remove the padding so developers are not caught out when they zero out
|
||||||
|
* `fieldset` elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
color: inherit; /* 2 */
|
||||||
|
display: table; /* 1 */
|
||||||
|
max-width: 100%; /* 1 */
|
||||||
|
padding: 0; /* 3 */
|
||||||
|
white-space: normal; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the default vertical scrollbar in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in IE 10.
|
||||||
|
* 2. Remove the padding in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="checkbox"],
|
||||||
|
[type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
* 2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
outline-offset: -2px; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
* 2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Misc
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
export enum CardType {
|
|
||||||
SPADES = "SPADES",
|
|
||||||
HEARTS = "HEARTS",
|
|
||||||
DIAMONDS = "DIAMONDS",
|
|
||||||
CLUBS = "CLUBS",
|
|
||||||
JOKER = "JOKER"
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CardValue {
|
|
||||||
TWO = "TWO",
|
|
||||||
THREE = "THREE",
|
|
||||||
FOUR = "FOUR",
|
|
||||||
FIVE = "FIVE",
|
|
||||||
SIX = "SIX",
|
|
||||||
SEVEN = "SEVEN",
|
|
||||||
EIGHT = "EIGHT",
|
|
||||||
NINE = "NINE",
|
|
||||||
TEN = "TEN",
|
|
||||||
JACK = "JACK",
|
|
||||||
QUEEN = "QUEEN",
|
|
||||||
KING = "KING",
|
|
||||||
ACE = "ACE",
|
|
||||||
RED = "RED",
|
|
||||||
BLACK = "BLACK"
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICard {
|
|
||||||
id: string;
|
|
||||||
type: CardType;
|
|
||||||
value: CardValue;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export interface Connection {
|
|
||||||
Id: string;
|
|
||||||
Name: string;
|
|
||||||
}
|
|
||||||
28
src/utils/contexts/ThemeContext.tsx
Normal file
28
src/utils/contexts/ThemeContext.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface IThemeContext {
|
||||||
|
isDarkMode: boolean
|
||||||
|
toggleTheme?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = React.createContext<IThemeContext>({
|
||||||
|
isDarkMode: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useTheme = () => React.useContext(ThemeContext);
|
||||||
|
|
||||||
|
const ThemeContextProvider = ({children}: any) => {
|
||||||
|
const [isDarkMode, setIsDarkMode] = React.useState(true);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setIsDarkMode(!isDarkMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{isDarkMode, toggleTheme}}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeContextProvider;
|
||||||
14
src/utils/hooks/TitleHook.ts
Normal file
14
src/utils/hooks/TitleHook.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const useTitle = (title: string) => {
|
||||||
|
React.useEffect(() => {
|
||||||
|
const prevTitle = document.title;
|
||||||
|
document.title = title;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.title = prevTitle;
|
||||||
|
};
|
||||||
|
}, [title]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTitle;
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import Input from "@/components/form/Input.vue";
|
|
||||||
import {useUserName} from "@/stores/userName";
|
|
||||||
import {useRouter} from "vue-router";
|
|
||||||
import PrimaryButton from "@/components/button/PrimaryButton.vue";
|
|
||||||
|
|
||||||
const userNameStore = useUserName();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const onHostClick = async () => {
|
|
||||||
const appUrl = import.meta.env.VITE_API_ENDPOINT || "__API_ENDPOINT__";
|
|
||||||
fetch(appUrl + "/room", {
|
|
||||||
method: "POST"
|
|
||||||
}).then(response => response.json())
|
|
||||||
.then(room => router.push({name: "room", params: {roomId: room}}))
|
|
||||||
.catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onBrowseClick = () => {
|
|
||||||
router.push({name: "rooms"});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="home">
|
|
||||||
<div class="hero">
|
|
||||||
<h1 class="hero__title">MauMau</h1>
|
|
||||||
<form>
|
|
||||||
<Input :initialValue="userNameStore.userName" placeholder="Username" @input="userNameStore.setUserName"/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="interactions">
|
|
||||||
<div class="interactions-container">
|
|
||||||
<h2 class="interactions-container__title">Join game</h2>
|
|
||||||
<form class="interactions-container__buttons">
|
|
||||||
<Input placeholder="Enter roomcode" autocomplete="off"/>
|
|
||||||
<PrimaryButton type="button" @click="onBrowseClick">Browse rooms</PrimaryButton>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="interactions-container">
|
|
||||||
<h2 class="interactions-container__title">Host game</h2>
|
|
||||||
<div class="interactions-container__buttons">
|
|
||||||
<PrimaryButton @click="onHostClick">Start Hosting</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/variables";
|
|
||||||
@import "@/scss/layout";
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Cattie";
|
|
||||||
src: url(@/assets/fonts/CattieRegular.ttf) format('truetype');
|
|
||||||
}
|
|
||||||
|
|
||||||
.home {
|
|
||||||
@extend .container-fill;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
place-items: center;
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
@extend .container-md;
|
|
||||||
|
|
||||||
height: 50%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-around;
|
|
||||||
|
|
||||||
@media screen and (max-width: $breakpoint-md) {
|
|
||||||
height: 40%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 6rem;
|
|
||||||
font-weight: lighter;
|
|
||||||
font-family: Cattie ,sans-serif;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.interactions {
|
|
||||||
@extend .container-md, .container-fill-height, .place-start;
|
|
||||||
|
|
||||||
height: 50%;
|
|
||||||
display: grid;
|
|
||||||
grid-template: 1fr / repeat(2, 1fr);
|
|
||||||
grid-gap: 2rem;
|
|
||||||
|
|
||||||
@media screen and (max-width: $breakpoint-md) {
|
|
||||||
height: 60%;
|
|
||||||
grid-template: repeat(2, 1fr) / 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-container {
|
|
||||||
@extend .container-fill-width;
|
|
||||||
|
|
||||||
background-color: white;
|
|
||||||
padding: 0 1rem 1rem 1rem;
|
|
||||||
border: 0.5rem black;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow: 1rem 1rem 1rem rgba(255, 255, 255, 0.25);
|
|
||||||
color: black;
|
|
||||||
min-height: 66%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
|
|
||||||
@media screen and (max-width: $breakpoint-md) {
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
font-size: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-around;
|
|
||||||
|
|
||||||
* {
|
|
||||||
@extend .container-fill-width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(input) {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(button) {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import Chat from "@/components/Chat.vue";
|
|
||||||
import Lobby from "@/components/maumau/lobby/Lobby.vue";
|
|
||||||
import Game from "@/components/maumau/game/Game.vue";
|
|
||||||
import {onBeforeUnmount, onMounted} from "vue";
|
|
||||||
import {useRoute} from "vue-router";
|
|
||||||
import {WebSocketCloseCodes} from "@/composables/webSocket";
|
|
||||||
import {useGame} from "@/composables/game.composable";
|
|
||||||
import PrimaryButton from "@/components/button/PrimaryButton.vue";
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const roomId = route.params.roomId as string;
|
|
||||||
|
|
||||||
const game = useGame(roomId);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener("beforeunload", () =>
|
|
||||||
game.webSocket.close(WebSocketCloseCodes.CLOSE_GOING_AWAY, "User left the room"));
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
game.webSocket.close(WebSocketCloseCodes.CLOSE_GOING_AWAY, "User left the room");
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="room">
|
|
||||||
<aside class="sidebar">
|
|
||||||
<div>
|
|
||||||
<h1>Mau-Mau</h1>
|
|
||||||
<h2>Room {{ roomId.split('-')[0] }}</h2>
|
|
||||||
<PrimaryButton @click="game.onLeaveGame()">
|
|
||||||
Leave Room
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
<Chat :chats="game.chatMessages.value" @send="game.onChatSend"/>
|
|
||||||
</aside>
|
|
||||||
<main>
|
|
||||||
<Game v-if="game.currentRoomType.value === 'GAME'" :game="game"/>
|
|
||||||
<Lobby v-if="game.currentRoomType.value === 'LOBBY'" :game="game"/>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/variables";
|
|
||||||
@import "@/scss/layout";
|
|
||||||
|
|
||||||
.room {
|
|
||||||
@extend .container-fill;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
@media screen and (max-width: $breakpoint-md) {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside {
|
|
||||||
display: grid;
|
|
||||||
min-width: 20%;
|
|
||||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
||||||
overflow: hidden auto;
|
|
||||||
|
|
||||||
@media screen and (min-width: $breakpoint-md) {
|
|
||||||
max-width: $breakpoint-xs;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 0.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
@extend .container-fill;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import PrimaryButton from "@/components/button/PrimaryButton.vue";
|
|
||||||
import {onBeforeMount, reactive} from "vue";
|
|
||||||
import StylessButton from "@/components/button/StylessButton.vue";
|
|
||||||
import router from "@/router";
|
|
||||||
|
|
||||||
const state = reactive<{
|
|
||||||
rooms: string[],
|
|
||||||
}>({
|
|
||||||
rooms: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
const onRoomClick = (room: string) => {
|
|
||||||
router.push({name: "room", params: {roomId: room}});
|
|
||||||
}
|
|
||||||
|
|
||||||
const onHomeClick = () => {
|
|
||||||
router.push({name: "home"});
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
|
||||||
const appUrl = import.meta.env.VITE_API_ENDPOINT || "__API_ENDPOINT__";
|
|
||||||
fetch(appUrl + "/room")
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => state.rooms = data);
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="rooms">
|
|
||||||
<aside>
|
|
||||||
<div>
|
|
||||||
<h1>Mau-Mau</h1>
|
|
||||||
<PrimaryButton @click="onHomeClick">
|
|
||||||
Back to home
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<main>
|
|
||||||
<div class="header">
|
|
||||||
<h1>Browse rooms</h1>
|
|
||||||
<PrimaryButton>Or join a random one</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
<ul class="rooms-list">
|
|
||||||
<li v-for="room in state.rooms" :key="room">
|
|
||||||
<StylessButton @click="onRoomClick(room)">{{ room }}</StylessButton>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/scss/variables";
|
|
||||||
@import "@/scss/layout";
|
|
||||||
|
|
||||||
.rooms {
|
|
||||||
@extend .container-fill;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
@media screen and (max-width: $breakpoint-md) {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside {
|
|
||||||
display: grid;
|
|
||||||
min-width: 20%;
|
|
||||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
||||||
overflow: hidden auto;
|
|
||||||
|
|
||||||
@media screen and (min-width: $breakpoint-md) {
|
|
||||||
max-width: $breakpoint-xs;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 0.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
@extend .container-fill;
|
|
||||||
|
|
||||||
.header {
|
|
||||||
padding-top: 0.67em;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
|
||||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
|
||||||
"exclude": ["src/**/__tests__/*"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
||||||
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,26 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"compilerOptions": {
|
||||||
"references": [
|
"target": "es5",
|
||||||
{
|
"lib": [
|
||||||
"path": "./tsconfig.node.json"
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
{
|
"include": [
|
||||||
"path": "./tsconfig.app.json"
|
"src"
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@tsconfig/node20/tsconfig.json",
|
|
||||||
"include": [
|
|
||||||
"vite.config.*",
|
|
||||||
"vitest.config.*",
|
|
||||||
"cypress.config.*",
|
|
||||||
"nightwatch.conf.*",
|
|
||||||
"playwright.config.*"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
||||||
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Bundler",
|
|
||||||
"types": ["node"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
|
||||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
vue(),
|
|
||||||
vueJsx(),
|
|
||||||
VueDevTools(),
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user