Compare commits
16 Commits
feature/ca
...
mauster
| Author | SHA1 | Date | |
|---|---|---|---|
| 233e7a9c8e | |||
| 7123bc4529 | |||
| 3294b1adf9 | |||
| 355dabf439 | |||
| 8997a570fa | |||
| e30e2d5d60 | |||
|
|
6ba1523853 | ||
|
|
4ae1538552 | ||
| 3b53b8997d | |||
|
|
b738263726 | ||
|
|
d5df884ae8 | ||
|
|
4959e197bb | ||
|
|
d907a56483 | ||
|
|
d547db430c | ||
|
|
e78d0d46f7 | ||
| ff9c9c6a84 |
@@ -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
|
||||||
@@ -36,18 +42,13 @@ jobs:
|
|||||||
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
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Create .env File
|
|
||||||
run: |
|
|
||||||
touch .env
|
|
||||||
echo "REACT_APP_API_URL=https://mau-mau.nl/api" > .env
|
|
||||||
echo "REACT_APP_WEBSOCKET_URL=wss://mau-mau.nl/api" >> .env
|
|
||||||
|
|
||||||
- name: Set up Docker BuildX
|
- name: Set up Docker BuildX
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
# Create a production build of the app
|
# Create a production build of the app
|
||||||
FROM node:16.9.1 as build
|
FROM node:16.9.1 as build
|
||||||
|
WORKDIR /var/www/mau-mau.nl
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN echo "REACT_APP_API_URL=https://api.mau-mau.nl" >> .env
|
||||||
|
RUN echo "REACT_APP_WEBSOCKET_URL=wss://api.mau-mau.nl" >> .env
|
||||||
|
|
||||||
RUN npm install --silent
|
RUN npm install --silent
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
RUN npm install -g serve
|
RUN npm install -g serve
|
||||||
|
|||||||
82
package-lock.json
generated
82
package-lock.json
generated
@@ -15,6 +15,8 @@
|
|||||||
"@types/node": "^16.18.14",
|
"@types/node": "^16.18.14",
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"bootstrap-icons": "^1.11.3",
|
||||||
|
"framer-motion": "^11.2.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-query": "^3.39.3",
|
"react-query": "^3.39.3",
|
||||||
@@ -1808,16 +1810,21 @@
|
|||||||
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
|
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.21.0",
|
"version": "7.24.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
|
||||||
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
|
"integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.13.11"
|
"regenerator-runtime": "^0.14.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime/node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||||
|
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.20.7",
|
"version": "7.20.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
|
||||||
@@ -5274,6 +5281,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
||||||
},
|
},
|
||||||
|
"node_modules/bootstrap-icons": {
|
||||||
|
"version": "1.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz",
|
||||||
|
"integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/twbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@@ -8149,6 +8171,30 @@
|
|||||||
"url": "https://www.patreon.com/infusion"
|
"url": "https://www.patreon.com/infusion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "11.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.4.tgz",
|
||||||
|
"integrity": "sha512-D+EXd0lspaZijv3BJhAcSsyGz+gnvoEdnf+QWkPZdhoFzbeX/2skrH9XSVFb0osgUnCajW8x1frjhLuKwa/Reg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fresh": {
|
"node_modules/fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
@@ -18369,11 +18415,18 @@
|
|||||||
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
|
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
|
||||||
},
|
},
|
||||||
"@babel/runtime": {
|
"@babel/runtime": {
|
||||||
"version": "7.21.0",
|
"version": "7.24.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
|
||||||
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
|
"integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"regenerator-runtime": "^0.13.11"
|
"regenerator-runtime": "^0.14.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"regenerator-runtime": {
|
||||||
|
"version": "0.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||||
|
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/template": {
|
"@babel/template": {
|
||||||
@@ -20871,6 +20924,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
||||||
},
|
},
|
||||||
|
"bootstrap-icons": {
|
||||||
|
"version": "1.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz",
|
||||||
|
"integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww=="
|
||||||
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@@ -22961,6 +23019,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
||||||
"integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA=="
|
"integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA=="
|
||||||
},
|
},
|
||||||
|
"framer-motion": {
|
||||||
|
"version": "11.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.4.tgz",
|
||||||
|
"integrity": "sha512-D+EXd0lspaZijv3BJhAcSsyGz+gnvoEdnf+QWkPZdhoFzbeX/2skrH9XSVFb0osgUnCajW8x1frjhLuKwa/Reg==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"fresh": {
|
"fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"@types/node": "^16.18.14",
|
"@types/node": "^16.18.14",
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"bootstrap-icons": "^1.11.3",
|
||||||
|
"framer-motion": "^11.2.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-query": "^3.39.3",
|
"react-query": "^3.39.3",
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import '../src/styles/App.scss';
|
import '../src/styles/App.scss';
|
||||||
import Router from "./config/Router";
|
import Router from "./config/Router";
|
||||||
import {useTheme} from "./utils/contexts/ThemeContext";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
const {isDarkMode} = useTheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`app ${isDarkMode ? 'dark' : 'light'}`}>
|
<div className={"app"}>
|
||||||
<Router/>
|
<Router/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
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;
|
||||||
@@ -1,13 +1,26 @@
|
|||||||
import React, {FunctionComponent} from "react";
|
import React, {ButtonHTMLAttributes, DetailedHTMLProps, FunctionComponent} from "react";
|
||||||
|
|
||||||
|
interface ButtonProps extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
|
||||||
|
|
||||||
interface Props {
|
|
||||||
className?: string
|
|
||||||
onClick?: () => void
|
|
||||||
children?: React.ReactNode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GHButton: FunctionComponent<Props> = ({className, onClick, children}) => {
|
export const NoButton: FunctionComponent<ButtonProps> = (props) => {
|
||||||
|
|
||||||
|
const {className, ...rest} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`gh-button ${className}`} onClick={onClick}>{children}</span>
|
<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}` : "");
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import React, {FunctionComponent} from "react";
|
import React, {FunctionComponent} from "react";
|
||||||
|
import {NoButton} from "./Button";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cardString: string;
|
cardString: string;
|
||||||
handleClick?: (cardString: string) => void;
|
handleClick?: (cardString: string) => void;
|
||||||
isHidden?: boolean;
|
isHidden?: boolean;
|
||||||
isClickable?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Card: FunctionComponent<Props> = ({cardString, handleClick, isHidden, isClickable}) => {
|
const Card: FunctionComponent<Props> = ({cardString, handleClick, isHidden}) => {
|
||||||
const cardType = cardString.split(' ')[0].toLowerCase();
|
const cardType = cardString.split(' ')[0].toLowerCase();
|
||||||
const cardValue = cardString.split(' ')[1].toLowerCase();
|
const cardValue = cardString.split(' ')[1].toLowerCase();
|
||||||
|
|
||||||
@@ -30,9 +30,17 @@ const Card: FunctionComponent<Props> = ({cardString, handleClick, isHidden, isCl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (handleClick) {
|
||||||
|
return (
|
||||||
|
<NoButton className={"card"} onClick={handleCardClick}>
|
||||||
|
<img className="card__texture" src={cardSource()} alt={cardName}/>
|
||||||
|
</NoButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`card ${isClickable && 'card-clickable'}`}>
|
<div className={"card"}>
|
||||||
<img className="card__texture" src={cardSource()} alt={cardName} onClick={handleCardClick}/>
|
<img className="card__texture" src={cardSource()} alt={cardName}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
@@ -15,12 +15,10 @@ const Deck: FunctionComponent<Props> = ({currentCard, actionOnClick}) => {
|
|||||||
return (
|
return (
|
||||||
<div className="deck-container">
|
<div className="deck-container">
|
||||||
<div className="used-cards">
|
<div className="used-cards">
|
||||||
<p></p>
|
|
||||||
<Card cardString={currentCard}/>
|
<Card cardString={currentCard}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="deck">
|
<div className="deck">
|
||||||
<p></p>
|
<Card cardString={"AA BB"} handleClick={handleClick} isHidden/>
|
||||||
<Card cardString={"AA BB"} handleClick={handleClick} isHidden={true}/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import React, {FunctionComponent} from "react";
|
|
||||||
import Deck from "./Deck";
|
|
||||||
import Hand from "./Hand";
|
|
||||||
import {GHButton} from "./Button";
|
|
||||||
|
|
||||||
export interface GameState {
|
|
||||||
PlayerName: string;
|
|
||||||
CurrentState: string;
|
|
||||||
Hand: string[];
|
|
||||||
CurrentCard: string;
|
|
||||||
CurrentPlayer: string;
|
|
||||||
Players: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
gameState: GameState
|
|
||||||
handleCardSend: (cardString: string) => void;
|
|
||||||
handleDraw: () => void;
|
|
||||||
handleChoice: (choice: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CHOICES = ['SPADES', 'HEARTS', 'DIAMONDS', 'CLUBS'];
|
|
||||||
|
|
||||||
const Game: FunctionComponent<Props> = ({gameState, handleCardSend, handleDraw, handleChoice}) => {
|
|
||||||
return (
|
|
||||||
<div className="game">
|
|
||||||
{
|
|
||||||
gameState.CurrentCard &&
|
|
||||||
<Deck currentCard={gameState.CurrentCard} actionOnClick={handleDraw}/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
gameState.Hand &&
|
|
||||||
<Hand hand={gameState.Hand} actionOnClick={handleCardSend}/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
gameState.CurrentState === 'CHOOSE' &&
|
|
||||||
CHOICES.map(choice => <GHButton key={choice} onClick={() => handleChoice(choice)}>{choice}</GHButton>)
|
|
||||||
}
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
gameState?.Players &&
|
|
||||||
gameState.Players.map((player) => {
|
|
||||||
const isCurrentPlayer = player === gameState.CurrentPlayer;
|
|
||||||
const isMe = player === gameState.PlayerName;
|
|
||||||
return <li key={player} style={{fontWeight: isCurrentPlayer ? 'bold' : 'normal'}}>
|
|
||||||
{player} {isMe && '(You)'}
|
|
||||||
</li>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Game;
|
|
||||||
@@ -3,25 +3,24 @@ import Card from "./Card";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
hand: string[];
|
hand: string[];
|
||||||
actionOnClick: (cardString: string) => void;
|
actionOnClick?: (cardString: string) => void;
|
||||||
isHidden?: boolean;
|
isHidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Hand: FunctionComponent<Props> = ({hand, actionOnClick, isHidden}) => {
|
const Hand: FunctionComponent<Props> = ({hand, actionOnClick, isHidden}) => {
|
||||||
|
|
||||||
const isMyHand = !isHidden;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hand">
|
<ul className="hand">
|
||||||
{
|
{
|
||||||
hand.map((card, index) => {
|
hand.map((card, index) => {
|
||||||
return (
|
return (
|
||||||
<Card key={index} cardString={card} handleClick={actionOnClick} isHidden={!isMyHand}
|
<li key={card} className={"hand__item"}>
|
||||||
isClickable={isMyHand}/>
|
<Card cardString={card} handleClick={actionOnClick} isHidden={isHidden}/>
|
||||||
|
</li>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</ul>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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;
|
||||||
@@ -2,6 +2,7 @@ import React from "react";
|
|||||||
import useTitle from "../../utils/hooks/TitleHook";
|
import useTitle from "../../utils/hooks/TitleHook";
|
||||||
import Card from "../components/Card";
|
import Card from "../components/Card";
|
||||||
import {useNavigate} from "react-router";
|
import {useNavigate} from "react-router";
|
||||||
|
import {NoButton} from "../components/Button";
|
||||||
|
|
||||||
const ROOM_URL = `${process.env.REACT_APP_API_URL}/room`;
|
const ROOM_URL = `${process.env.REACT_APP_API_URL}/room`;
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ const MainLobby = () => {
|
|||||||
useTitle('Mau-Mau Lobby');
|
useTitle('Mau-Mau Lobby');
|
||||||
|
|
||||||
const navigateTo = useNavigate();
|
const navigateTo = useNavigate();
|
||||||
|
const playerName = localStorage.getItem('playerName') ?? "";
|
||||||
|
|
||||||
const handleCreateRoom = () => {
|
const handleCreateRoom = () => {
|
||||||
fetch(ROOM_URL, {
|
fetch(ROOM_URL, {
|
||||||
@@ -19,18 +21,28 @@ const MainLobby = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changePlayerName = (name: string) => {
|
||||||
|
if (!name) return;
|
||||||
|
localStorage.setItem('playerName', name);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"main-lobby"}>
|
<div className={"main-lobby"}>
|
||||||
<h1 className={"main-lobby__title mau"}>Mau-Mau</h1>
|
<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"}>
|
<div className={"main-lobby__container"}>
|
||||||
<div className={"main-lobby__container-button clickable"} onClick={handleCreateRoom}>
|
<NoButton className={"main-lobby__container-button"} onClick={handleCreateRoom}>
|
||||||
<h2 className={"mau"}>Host Game</h2>
|
<h2 className={"mau"}>Host Game</h2>
|
||||||
<Card cardString={'SPADES ACE'} isClickable/>
|
<Card cardString={'SPADES ACE'}/>
|
||||||
</div>
|
</NoButton>
|
||||||
<div className={"main-lobby__container-button clickable"} onClick={() => navigateTo('/rooms')}>
|
<NoButton className={"main-lobby__container-button"} onClick={() => navigateTo('/rooms')}>
|
||||||
<h2 className={"mau"}>Join Game</h2>
|
<h2 className={"mau"}>Join Game</h2>
|
||||||
<Card cardString={'SPADES ACE'} isHidden isClickable/>
|
<Card cardString={'SPADES ACE'} isHidden/>
|
||||||
</div>
|
</NoButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import useWebSocket from "react-use-websocket";
|
import useWebSocket from "react-use-websocket";
|
||||||
import React from "react";
|
import React, {useEffect} from "react";
|
||||||
import {useNavigate, useParams} from "react-router";
|
import {useNavigate, useParams} from "react-router";
|
||||||
import {GHButton} from "../components/Button";
|
|
||||||
import useTitle from "../../utils/hooks/TitleHook";
|
import useTitle from "../../utils/hooks/TitleHook";
|
||||||
import Game, {GameState} from "../components/Game";
|
import Game, {GameState} from "../views/Game/Game";
|
||||||
|
import Lobby from "../views/Lobby/Lobby";
|
||||||
|
import Chat from "../components/Chat";
|
||||||
|
import {Button} from "../components/Button";
|
||||||
|
|
||||||
interface ChatMessage {
|
export interface Connection {
|
||||||
PlayerName: string;
|
Id: string;
|
||||||
Message: string;
|
Name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SocketMessage {
|
export interface Player extends Connection {
|
||||||
|
CardsLeft: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SocketMessage {
|
||||||
Type: string;
|
Type: string;
|
||||||
Payload: any;
|
Data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Room = () => {
|
const Room = () => {
|
||||||
@@ -23,34 +29,75 @@ const Room = () => {
|
|||||||
|
|
||||||
const {roomId} = useParams();
|
const {roomId} = useParams();
|
||||||
|
|
||||||
const WS_URL = `${process.env.REACT_APP_WEBSOCKET_URL}/room/${roomId}`;
|
const playerName = localStorage.getItem('playerName') ?? "";
|
||||||
|
const WS_URL = `${process.env.REACT_APP_WEBSOCKET_URL}/room/${roomId}/${playerName}`;
|
||||||
|
|
||||||
const [gameState, setGameState] = React.useState<GameState>({
|
const [gameState, setGameState] = React.useState<GameState | undefined>(undefined);
|
||||||
PlayerName: '',
|
const [winner, setWinner] = React.useState<string | undefined>(undefined);
|
||||||
CurrentState: '',
|
const [playerList, setPlayerList] = React.useState<Connection[]>([]);
|
||||||
Hand: [],
|
|
||||||
CurrentCard: '',
|
const chatRef = React.useRef<HTMLOListElement>(null);
|
||||||
CurrentPlayer: '',
|
|
||||||
Players: []
|
const addChatMessage = (message: { Sender: string, Message: string }) => {
|
||||||
});
|
if (!chatRef.current) return;
|
||||||
const [chatMessages, setChatMessages] = React.useState<ChatMessage[]>([]);
|
|
||||||
const [chatInput, setChatInput] = React.useState<string>('');
|
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, {
|
const websocket = useWebSocket(WS_URL, {
|
||||||
onOpen: () => {
|
onOpen: () => {
|
||||||
console.log('WebSocket connection established.');
|
console.log('WebSocket connection established.');
|
||||||
},
|
},
|
||||||
onMessage: (event) => {
|
onMessage: (messageEvent) => {
|
||||||
const data = JSON.parse(event.data);
|
const data: SocketMessage = JSON.parse(messageEvent.data);
|
||||||
const payload = JSON.parse(data.Payload);
|
const payload = JSON.parse(data.Data);
|
||||||
if (data.Type === 'GAME') setGameState(payload);
|
switch (data.Type) {
|
||||||
if (data.Type === 'CHAT') setChatMessages(prev => [...prev, payload]);
|
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 = () => {
|
const handleLeaveRoom = () => {
|
||||||
const socket = websocket.getWebSocket();
|
|
||||||
if (socket) socket.close();
|
|
||||||
navigateTo('/');
|
navigateTo('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,58 +105,51 @@ const Room = () => {
|
|||||||
websocket.sendMessage(JSON.stringify(message));
|
websocket.sendMessage(JSON.stringify(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCardSend = (card: string) => {
|
const handleLobbyAction = (action: string) => {
|
||||||
handleSend({
|
handleSend({Type: "LOBBY", Data: JSON.stringify(action)});
|
||||||
Type: "GAME",
|
|
||||||
Payload: JSON.stringify({
|
|
||||||
Action: "PLAYCARD",
|
|
||||||
Data: JSON.stringify({
|
|
||||||
CardType: card.split(' ')[0],
|
|
||||||
CardValue: card.split(' ')[1]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChoice = (choice: string) => {
|
const handleKick = (playerId: string) => {
|
||||||
handleSend({
|
handleSend({Type: "KICK", Data: playerId});
|
||||||
Type: "GAME",
|
|
||||||
Payload: JSON.stringify({
|
|
||||||
Action: "CHOOSE",
|
|
||||||
Data: choice
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDraw = () => {
|
const handleChatMessage = (chatMessage: string) => {
|
||||||
handleSend({
|
|
||||||
Type: "GAME",
|
|
||||||
Payload: JSON.stringify({
|
|
||||||
Action: "DRAW",
|
|
||||||
Data: ""
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChat = (message: string) => {
|
|
||||||
handleSend({
|
handleSend({
|
||||||
Type: "CHAT",
|
Type: "CHAT",
|
||||||
Payload: message
|
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 (
|
return (
|
||||||
<div>
|
<div className={"room"}>
|
||||||
<h1>Room {roomId}</h1>
|
<aside className={"room-aside"}>
|
||||||
<GHButton onClick={handleLeaveRoom}>Leave Room</GHButton>
|
<header className={"aside-header"}>
|
||||||
<Game gameState={gameState} handleCardSend={handleCardSend} handleDraw={handleDraw} handleChoice={handleChoice}/>
|
<hgroup className={"aside-header__titles"}>
|
||||||
<input type="text" placeholder={"Chat"} value={chatInput} onChange={(e) => setChatInput(e.target.value)} />
|
<h1>Mau-Mau</h1>
|
||||||
<button onClick={() => handleChat(chatInput)}>Send</button>
|
<h2>Room {roomId?.split("-")[0]}</h2>
|
||||||
<ul>
|
</hgroup>
|
||||||
{chatMessages.map((message, index) => (
|
<Button onClick={handleLeaveRoom} className={"icon-start"}>
|
||||||
<li key={index}>{message.PlayerName}: {message.Message}</li>
|
<i className="bi bi-arrow-bar-left"></i>
|
||||||
))}
|
Leave Room
|
||||||
</ul>
|
</Button>
|
||||||
|
</header>
|
||||||
|
<Chat chatRef={chatRef} handleSend={handleChatMessage}/>
|
||||||
|
</aside>
|
||||||
|
{
|
||||||
|
gameState ?
|
||||||
|
<Game gameState={gameState} handleGameAction={handleSend}/>
|
||||||
|
:
|
||||||
|
<Lobby playerList={playerList} winner={winner} handleLobbyAction={handleLobbyAction} onKickClick={handleKick}/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import useTitle from "../../utils/hooks/TitleHook";
|
import useTitle from "../../utils/hooks/TitleHook";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {useNavigate} from "react-router";
|
import {useNavigate} from "react-router";
|
||||||
|
import {NoButton} from "../components/Button";
|
||||||
|
|
||||||
const ROOM_URL = `${process.env.REACT_APP_API_URL}/room`;
|
const ROOM_URL = `${process.env.REACT_APP_API_URL}/room`;
|
||||||
|
|
||||||
@@ -11,6 +12,13 @@ const Rooms = () => {
|
|||||||
|
|
||||||
const [rooms, setRooms] = React.useState<string[]>([]);
|
const [rooms, setRooms] = React.useState<string[]>([]);
|
||||||
|
|
||||||
|
const handleRoomClick = (room: string) => {
|
||||||
|
if (!localStorage.getItem('playerName')){
|
||||||
|
localStorage.setItem("playerName", "Mau");
|
||||||
|
}
|
||||||
|
navigateTo(`/room/${room}`);
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
fetch(ROOM_URL)
|
fetch(ROOM_URL)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
@@ -19,12 +27,13 @@ const Rooms = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Rooms</h1>
|
<h1 className={"mau"}>Rooms</h1>
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
rooms.map((room, index) => {
|
rooms.map((room) => {
|
||||||
return <li key={index} className={"clickable"}
|
return <li key={room}>
|
||||||
onClick={() => navigateTo(`/room/${room}`)}>{room}</li>
|
<NoButton onClick={() => handleRoomClick(room)}>{room}</NoButton>
|
||||||
|
</li>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
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;
|
||||||
@@ -7,9 +7,35 @@
|
|||||||
src: local('Mau'), url(../assets/fonts/OrientalCatsLight.otf) format('opentype');
|
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 {
|
.app {
|
||||||
width: 100vw;
|
padding: 1rem;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mau {
|
.mau {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
@import "button";
|
@import "button";
|
||||||
@import "card";
|
@import "card";
|
||||||
|
@import "chat";
|
||||||
|
@import "card-type-choice-modal";
|
||||||
|
@import "deck";
|
||||||
|
@import "game";
|
||||||
|
@import "game-lobby";
|
||||||
@import "hand";
|
@import "hand";
|
||||||
@import "deck";
|
@import "modal";
|
||||||
|
@import "room-header";
|
||||||
@@ -1,56 +1,46 @@
|
|||||||
.gh-button {
|
.icon-end {
|
||||||
appearance: none;
|
display: grid;
|
||||||
background-color: #FAFBFC;
|
grid-template-columns: 1fr auto;
|
||||||
border: 1px solid rgba(27, 31, 35, 0.15);
|
}
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: rgba(27, 31, 35, 0.04) 0 1px 0, rgba(255, 255, 255, 0.25) 0 1px 0 inset;
|
.icon-start {
|
||||||
box-sizing: border-box;
|
display: grid;
|
||||||
color: #24292E;
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-button {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
}
|
||||||
font-family: -apple-system, system-ui, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 20px;
|
|
||||||
list-style: none;
|
|
||||||
padding: 6px 16px;
|
|
||||||
position: relative;
|
|
||||||
transition: background-color 0.2s cubic-bezier(0.3, 0, 0.5, 1);
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
touch-action: manipulation;
|
|
||||||
vertical-align: middle;
|
|
||||||
white-space: nowrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
|
|
||||||
:hover {
|
.button {
|
||||||
background-color: #F3F4F6;
|
background-color: #2e2e2e;
|
||||||
text-decoration: none;
|
border: none;
|
||||||
transition-duration: 0.1s;
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #3e3e3e;
|
||||||
}
|
}
|
||||||
|
|
||||||
:disabled {
|
&-success {
|
||||||
background-color: #FAFBFC;
|
background-color: #276b00;
|
||||||
border-color: rgba(27, 31, 35, 0.15);
|
|
||||||
color: #959DA5;
|
&:hover {
|
||||||
cursor: default;
|
background-color: #3e8e00;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:active {
|
&-danger {
|
||||||
background-color: #EDEFF2;
|
background-color: #6b0000;
|
||||||
box-shadow: rgba(225, 228, 232, 0.2) 0 1px 0 inset;
|
|
||||||
transition: none 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
:focus {
|
&:hover {
|
||||||
outline: 1px transparent;
|
background-color: #8e0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,6 @@
|
|||||||
width: 100px;
|
width: 100px;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
|
|
||||||
&-clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__texture {
|
&__texture {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
.hand {
|
.hand {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
max-width: 100%;
|
||||||
//justify-content: center;
|
flex-flow: row nowrap !important;
|
||||||
flex-flow: nowrap !important;
|
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
gap: 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
@import "mainlobby";
|
@import "mainlobby";
|
||||||
|
@import "room";
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
.main-lobby {
|
.main-lobby {
|
||||||
width: 100vw;
|
|
||||||
height: 75vh;
|
height: 75vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
&__container {
|
&__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
@@ -19,6 +24,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
font-size: 5rem;
|
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,2 +1 @@
|
|||||||
@import "pointer";
|
@import "pointer";
|
||||||
@import "theme";
|
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
.clickable {
|
.mouse-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mouse-default {
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.dark {
|
|
||||||
background-color: #333;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.light {
|
|
||||||
background-color: #fff;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
1
src/styles/vendor/_vendor.scss
vendored
1
src/styles/vendor/_vendor.scss
vendored
@@ -1 +1,2 @@
|
|||||||
|
@import "bootstrap";
|
||||||
@import "normalize";
|
@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";
|
||||||
Reference in New Issue
Block a user