diff --git a/package.json b/package.json index ff782ee..cd30e2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "6.1.4", + "version": "7.0.0", "description": "React frontend for the Cards 110", "author": "Daithi Hearn", "license": "MIT", @@ -10,10 +10,11 @@ "@coreui/coreui": "^4.2.6", "@coreui/icons": "^3.0.0", "@coreui/react": "^4.6.0", - "@emotion/react": "^11.10.6", - "@emotion/styled": "^11.10.6", + "@emotion/react": "^11.11.0", + "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.11.11", "@mui/material": "^5.11.11", + "@mui/x-data-grid": "^6.4.0", "@popperjs/core": "^2.5.4", "@reduxjs/toolkit": "^1.9.3", "@types/jest": "^29.4.0", @@ -27,7 +28,6 @@ "chart.js": "^4.2.1", "core-js": "^3.29.0", "crypto-js": "4.1.1", - "flag-icon-css": "^4.1.7", "font-awesome": "^4.7.0", "heic2any": "^0.0.3", "jwt-decode": "^3.1.2", @@ -39,16 +39,13 @@ "react-block-ui": "^1.3.3", "react-chartjs-2": "5", "react-confetti": "^6.1.0", - "react-data-table-component": "7.5.3", "react-dom": "18.2.0", "react-redux": "^8.0.5", "react-router": "^6.8.2", "react-router-config": "^5.1.1", "react-router-dom": "^6.8.2", - "react-simple-pull-to-refresh": "^1.3.3", "react-stomp-hooks": "2.1.0", "react-viewer": "^3.2.2", - "reactstrap": "9.1.6", "semantic-ui-css": "^2.4.1", "simple-line-icons": "^2.5.5", "styled-components": "^5.3.8", diff --git a/public/assets/img/dummy-dark.png b/public/assets/img/dummy-dark.png new file mode 100644 index 0000000..f8841cd Binary files /dev/null and b/public/assets/img/dummy-dark.png differ diff --git a/src/assets/img/dummy.png b/public/assets/img/dummy-light.png similarity index 100% rename from src/assets/img/dummy.png rename to public/assets/img/dummy-light.png diff --git a/public/assets/img/mycards-dark.png b/public/assets/img/mycards-dark.png new file mode 100644 index 0000000..d0d335a Binary files /dev/null and b/public/assets/img/mycards-dark.png differ diff --git a/src/assets/img/mycards.png b/public/assets/img/mycards-light.png similarity index 100% rename from src/assets/img/mycards.png rename to public/assets/img/mycards-light.png diff --git a/public/cards/originals/carpet.jpg b/public/cards/originals/carpet.jpg deleted file mode 100644 index 576a113..0000000 Binary files a/public/cards/originals/carpet.jpg and /dev/null differ diff --git a/public/cards/originals/carpet2.jpg b/public/cards/originals/carpet2.jpg deleted file mode 100644 index 12f1be9..0000000 Binary files a/public/cards/originals/carpet2.jpg and /dev/null differ diff --git a/public/cards/originals/carpet3.jpg b/public/cards/originals/carpet3.jpg deleted file mode 100644 index 2c1400d..0000000 Binary files a/public/cards/originals/carpet3.jpg and /dev/null differ diff --git a/public/cards/originals/carpet4.jpg b/public/cards/originals/carpet4.jpg deleted file mode 100644 index 4f1293a..0000000 Binary files a/public/cards/originals/carpet4.jpg and /dev/null differ diff --git a/public/cards/thumbnails/blank_card_outline_dark.png b/public/cards/thumbnails/blank_card_outline_dark.png new file mode 100644 index 0000000..95575a6 Binary files /dev/null and b/public/cards/thumbnails/blank_card_outline_dark.png differ diff --git a/public/cards/thumbnails/blank_card_outline.png b/public/cards/thumbnails/blank_card_outline_light.png similarity index 100% rename from public/cards/thumbnails/blank_card_outline.png rename to public/cards/thumbnails/blank_card_outline_light.png diff --git a/public/manifest.json b/public/manifest.json index e4015ed..4298657 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "short_name": "Cards 110", "name": "Cards 110", - "version": "6.1.4", + "version": "7.0.0", "icons": [ { "src": "./assets/favicon.png", diff --git a/src/App.tsx b/src/App.tsx index 6887acd..bfeaead 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,6 +30,13 @@ import Home from "./pages/Home/Home" import Game from "./pages/Game/Game" import Layout from "./pages/Layout/Layout" import ErrorPage from "./pages/Error/Error" +import { lightTheme, darkTheme } from "Themes" +import { + createTheme, + CssBaseline, + ThemeProvider, + useMediaQuery, +} from "@mui/material" const AUTHO_DOMAIN = process.env.REACT_APP_AUTH0_DOMAIN as string const AUTH0_CLIENT_ID = process.env.REACT_APP_AUTH0_CLIENT_ID as string @@ -46,18 +53,31 @@ const router = createBrowserRouter( ) const App = () => { + const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)") + + const theme = React.useMemo( + () => + createTheme({ + ...(prefersDarkMode ? darkTheme : lightTheme), + }), + [prefersDarkMode], + ) + return ( - - - - - - - - + + + + + + + + + + + ) } export default App diff --git a/src/Themes.ts b/src/Themes.ts new file mode 100644 index 0000000..a3d0d3b --- /dev/null +++ b/src/Themes.ts @@ -0,0 +1,189 @@ +import { ThemeOptions } from "@mui/material" + +export const darkTheme: ThemeOptions = { + components: { + MuiModal: { + styleOverrides: { + root: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + }, + }, + MuiCssBaseline: { + styleOverrides: { + ".carpet": { + background: + "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='37' height='37' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(60)'%3E%3Crect width='100%25' height='100%25' fill='rgba(34, 84, 61,1)'/%3E%3Ccircle cx='20' cy='20' r='1' fill='%23f6e05e'/%3E%3Ccircle cx='30' cy='20' r='1' fill='%23d69e2e'/%3E%3Ccircle cx='20' cy='30' r='1' fill='%23d69e2e'/%3E%3Ccircle cx='10' cy='20' r='1' fill='%23d69e2e'/%3E%3Ccircle cx='20' cy='10' r='1' fill='%23d69e2e'/%3E%3Ccircle cx='30' cy='20' r='1' fill='%23d69e2e'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E \")", + position: "fixed", + width: "100%", + height: "100%", + overflow: "overlay", + }, + ".dummy": { + backgroundImage: "url('/assets/img/dummy-light.png')", + }, + ".cards-background": { + backgroundImage: "url('/assets/img/mycards-light.png')", + }, + }, + }, + }, + palette: { + mode: "dark", + primary: { + main: "#595959", // dark grey (instead of soft black) + }, + secondary: { + main: "#F8F8F8", // soft white (instead of soft black) + }, + error: { + main: "#FF7F7F", // soft red + }, + warning: { + main: "#7F7FFF", // soft blue + }, + info: { + main: "#7FFF7F", // soft green + }, + background: { + default: "#303030", // dark grey for contrast to dark grey primary + paper: "#424242", // even darker grey + }, + text: { + primary: "#F8F8F8", // soft white (instead of soft black) + secondary: "#F3F3F3", // softer white + }, + }, + typography: { + fontFamily: "sans-serif", + h1: { + fontSize: "2rem", + fontWeight: "bold", + }, + h2: { + fontSize: "1.5rem", + fontWeight: "bold", + }, + h3: { + fontSize: "1.25rem", + fontWeight: "bold", + }, + h4: { + fontSize: "1rem", + fontWeight: "bold", + }, + h5: { + fontSize: "0.875rem", + fontWeight: "bold", + }, + body1: { + fontSize: "1rem", + lineHeight: 1.5, + }, + body2: { + fontSize: "0.875rem", + lineHeight: 1.5, + }, + button: { + fontSize: "0.875rem", + fontWeight: "bold", + textTransform: "none", + }, + }, +} + +export const lightTheme: ThemeOptions = { + components: { + MuiModal: { + styleOverrides: { + root: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + }, + }, + MuiCssBaseline: { + styleOverrides: { + ".carpet": { + background: + "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='37' height='37' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(60)'%3E%3Crect width='100%25' height='100%25' fill='rgba(178, 245, 234,1)'/%3E%3Ccircle cx='20' cy='20' r='1' fill='rgba(45, 55, 72,1)'/%3E%3Ccircle cx='30' cy='20' r='1' fill='rgba(34, 84, 61,1)'/%3E%3Ccircle cx='20' cy='30' r='1' fill='rgba(34, 84, 61,1)'/%3E%3Ccircle cx='10' cy='20' r='1' fill='rgba(34, 84, 61,1)'/%3E%3Ccircle cx='20' cy='10' r='1' fill='rgba(34, 84, 61,1)'/%3E%3Ccircle cx='30' cy='20' r='1' fill='rgba(34, 84, 61,1)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E \")", + position: "fixed", + width: "100%", + height: "100%", + overflow: "overlay", + }, + ".dummy": { + backgroundImage: "url('/assets/img/dummy-dark.png')", + }, + ".cards-background": { + backgroundImage: "url('/assets/img/mycards-dark.png')", + }, + }, + }, + }, + palette: { + mode: "light", + primary: { + main: "#F8F8F8", // soft white + }, + secondary: { + main: "#595959", // soft black + }, + error: { + main: "#FF7F7F", // soft red + }, + warning: { + main: "#7F7FFF", // soft blue + }, + info: { + main: "#7FFF7F", // soft green + }, + background: { + default: "#F0F0F0", // soft grey for slight contrast to soft white + paper: "#F8F8F8", // soft white + }, + text: { + primary: "#595959", // soft black + secondary: "#5E5E5E", // softer black + }, + }, + typography: { + fontFamily: "sans-serif", + h1: { + fontSize: "2rem", + fontWeight: "bold", + }, + h2: { + fontSize: "1.5rem", + fontWeight: "bold", + }, + h3: { + fontSize: "1.25rem", + fontWeight: "bold", + }, + h4: { + fontSize: "1rem", + fontWeight: "bold", + }, + h5: { + fontSize: "0.875rem", + fontWeight: "bold", + }, + body1: { + fontSize: "1rem", + lineHeight: 1.5, + }, + body2: { + fontSize: "0.875rem", + lineHeight: 1.5, + }, + button: { + fontSize: "0.875rem", + fontWeight: "bold", + textTransform: "none", + }, + }, +} diff --git a/src/assets/img/brand/footer.png b/src/assets/img/brand/footer.png deleted file mode 100644 index 992dd56..0000000 Binary files a/src/assets/img/brand/footer.png and /dev/null differ diff --git a/src/assets/img/brand/logo.png b/src/assets/img/brand/logo.png deleted file mode 100644 index 992dd56..0000000 Binary files a/src/assets/img/brand/logo.png and /dev/null differ diff --git a/src/assets/img/cloth.jpg b/src/assets/img/cloth.jpg deleted file mode 100644 index c5def34..0000000 Binary files a/src/assets/img/cloth.jpg and /dev/null differ diff --git a/src/assets/img/wood.jpg b/src/assets/img/wood.jpg deleted file mode 100644 index 025dc28..0000000 Binary files a/src/assets/img/wood.jpg and /dev/null differ diff --git a/src/caches/GameSlice.ts b/src/caches/GameSlice.ts index c4af716..47795bb 100644 --- a/src/caches/GameSlice.ts +++ b/src/caches/GameSlice.ts @@ -10,6 +10,7 @@ const initialState: GameState = { isMyGo: false, iamGoer: false, iamDealer: false, + iamAdmin: false, cards: [], status: GameStatus.NONE, players: [], @@ -94,6 +95,7 @@ export const getMaxCall = createSelector(getGame, game => game.maxCall || 0) export const getIsMyGo = createSelector(getGame, game => game.isMyGo) export const getIamGoer = createSelector(getGame, game => game.iamGoer) export const getIamDealer = createSelector(getGame, game => game.iamDealer) +export const getIamAdmin = createSelector(getGame, game => game.iamAdmin) export const getIHavePlayed = createSelector(getGame, game => { const myPosition = game.players.findIndex(p => p.id === game.me?.id) const currentPlayerPosition = game.players.findIndex( diff --git a/src/components/Avatar/ProfilePictureEditor.tsx b/src/components/Avatar/ProfilePictureEditor.tsx index 220835d..e7333d7 100644 --- a/src/components/Avatar/ProfilePictureEditor.tsx +++ b/src/components/Avatar/ProfilePictureEditor.tsx @@ -3,16 +3,18 @@ import React, { useCallback, useRef, useState } from "react" import heic2any from "heic2any" import { useSnackbar } from "notistack" import { - Col, - Modal, - ModalBody, - ModalFooter, + Dialog, + DialogTitle, + DialogContent, + DialogActions, Button, - FormGroup, - FormText, + FormControl, + FormHelperText, Input, - ButtonGroup, -} from "reactstrap" + InputLabel, + Slider, + Grid, +} from "@mui/material" import { useAppDispatch, useAppSelector } from "caches/hooks" import { getMyProfile } from "caches/MyProfileSlice" import ProfileService from "services/ProfileService" @@ -83,9 +85,10 @@ const ProfilePictureEditor: React.FC = ({ show, callback }) => { } return ( - - - + + Edit Profile Picture + + {selectedImage && ( = ({ show, callback }) => { rotate={0} /> )} - - - - - - Please choose your new avatar - - - - - - - - - - - - - - - - - + + + New Avatar + + + Please choose your new avatar + + + + + + + updateScale(value as number) + } + min={1} + max={2} + step={0.01} + defaultValue={1.2} + /> + + + + + + + + + ) } diff --git a/src/components/Game/Actions/ActionsWrapper.tsx b/src/components/Game/Actions/ActionsWrapper.tsx index f394218..7cfabfa 100644 --- a/src/components/Game/Actions/ActionsWrapper.tsx +++ b/src/components/Game/Actions/ActionsWrapper.tsx @@ -1,4 +1,4 @@ -import { ButtonGroup, CardBody } from "reactstrap" +import { ButtonGroup as MuiButtonGroup, CardContent } from "@mui/material" import { getIsRoundBuying, getIsRoundCalled, @@ -18,14 +18,14 @@ const ActionsWrapper = () => { const isCalled = useAppSelector(getIsRoundCalled) return ( - - + + {isCalling && } {isBuying && } {isCalled && } {isPlaying && } - - + + ) } diff --git a/src/components/Game/Actions/Buying.tsx b/src/components/Game/Actions/Buying.tsx index 4e53c21..bb1c2b4 100644 --- a/src/components/Game/Actions/Buying.tsx +++ b/src/components/Game/Actions/Buying.tsx @@ -1,5 +1,3 @@ -import { Button } from "reactstrap" - import { useCallback, useEffect, useState } from "react" import GameService from "services/GameService" @@ -21,9 +19,10 @@ import { riskOfMistakeBuyingCards } from "utils/GameUtils" import ThrowCardsWarningModal from "./ThrowCardsWarningModal" import { SelectableCard } from "model/Cards" import parseError from "utils/ErrorUtils" +import { Button } from "@mui/material" const WaitingForRoundToStart = () => ( - ) diff --git a/src/components/Game/Actions/Calling.tsx b/src/components/Game/Actions/Calling.tsx index 746a566..f5d4146 100644 --- a/src/components/Game/Actions/Calling.tsx +++ b/src/components/Game/Actions/Calling.tsx @@ -1,5 +1,3 @@ -import { Button } from "reactstrap" - import GameService from "services/GameService" import { useAppDispatch, useAppSelector } from "caches/hooks" import { @@ -14,6 +12,7 @@ import { import { useCallback, useMemo } from "react" import { useSnackbar } from "notistack" import parseError from "utils/ErrorUtils" +import { Button } from "@mui/material" const Calling = () => { const dispatch = useAppDispatch() @@ -114,7 +113,7 @@ const Calling = () => { diff --git a/src/components/Game/Actions/PlayCard.tsx b/src/components/Game/Actions/PlayCard.tsx index bcbfaca..b9bff40 100644 --- a/src/components/Game/Actions/PlayCard.tsx +++ b/src/components/Game/Actions/PlayCard.tsx @@ -1,5 +1,3 @@ -import { Button } from "reactstrap" - import { useCallback, useMemo } from "react" import GameService from "services/GameService" @@ -10,9 +8,10 @@ import { getGameId, getIsMyGo, getRound } from "caches/GameSlice" import { BLANK_CARD } from "model/Cards" import parseError from "utils/ErrorUtils" import { RoundStatus } from "model/Round" +import { Button } from "@mui/material" const WaitingForYourTurn = () => ( - ) diff --git a/src/components/Game/Actions/SelectSuit.tsx b/src/components/Game/Actions/SelectSuit.tsx index d98a752..4b5ed6b 100644 --- a/src/components/Game/Actions/SelectSuit.tsx +++ b/src/components/Game/Actions/SelectSuit.tsx @@ -1,5 +1,3 @@ -import { Button } from "reactstrap" - import { useCallback, useState } from "react" import GameService from "services/GameService" @@ -12,9 +10,10 @@ import { removeAllFromHand } from "utils/GameUtils" import ThrowCardsWarningModal from "./ThrowCardsWarningModal" import { SelectableCard } from "model/Cards" import parseError from "utils/ErrorUtils" +import { Button } from "@mui/material" const WaitingForSuit = () => ( - ) diff --git a/src/components/Game/Actions/ThrowCardsWarningModal.tsx b/src/components/Game/Actions/ThrowCardsWarningModal.tsx index 9c5c517..9f40a87 100644 --- a/src/components/Game/Actions/ThrowCardsWarningModal.tsx +++ b/src/components/Game/Actions/ThrowCardsWarningModal.tsx @@ -1,15 +1,14 @@ import React, { useCallback, useMemo } from "react" import { - Modal, - ModalBody, - ModalHeader, + Dialog, + DialogTitle, + DialogContent, Button, ButtonGroup, - CardImg, - CardBody, - CardGroup, - Card as CardComponent, -} from "reactstrap" + Card, + CardMedia, + CardContent, +} from "@mui/material" import { getGameId } from "caches/GameSlice" import { useAppSelector } from "caches/hooks" import { getMyCardsWithoutBlanks, getSelectedCards } from "caches/MyCardsSlice" @@ -43,54 +42,50 @@ const ThrowCardsWarningModal: React.FC = ({ if (!gameId) throw Error("GameId not set") continueCallback(gameId, selectedCards, suit) }, [gameId, selectedCards]) + return ( - cancelCallback()} - isOpen={modalVisible}> - - cancelCallback()} open={modalVisible}> + + Are you sure you want to throw these cards away? - - - - - - {cardsToBeThrown.map(card => ( - {card.name} - ))} - + + + + + {cardsToBeThrown.map(card => ( + {card.name} + ))} + - - - - - - - - - - + + + + + + + + + ) } diff --git a/src/components/Game/GameHeader.tsx b/src/components/Game/GameHeader.tsx deleted file mode 100644 index 94966ef..0000000 --- a/src/components/Game/GameHeader.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useMemo } from "react" - -import { CardImg } from "reactstrap" -import { getGamePlayers, getRound } from "caches/GameSlice" -import { useAppSelector } from "caches/hooks" -import { getPlayerProfiles } from "caches/PlayerProfilesSlice" -import { Player, PlayerProfile } from "model/Player" - -interface PlayerAndProfile { - player?: Player - profile?: PlayerProfile -} - -const GameHeader = () => { - const round = useAppSelector(getRound) - const players = useAppSelector(getGamePlayers) - const playerProfiles = useAppSelector(getPlayerProfiles) - - const goer = useMemo(() => { - if (round && round.goerId) { - const profile = playerProfiles.find(p => p.id === round.goerId) - const player = players.find(p => p.id === round.goerId) - - return { - profile, - player, - } - } - return {} - }, [round, players, playerProfiles]) - - return ( - <> - {goer.profile && goer.player && ( -
- - - {goer.player.call && ( - - )} - - {round && round.suit && ( - - )} -
- )} - - ) -} - -export default GameHeader diff --git a/src/components/Game/GameOver.tsx b/src/components/Game/GameOver.tsx index 25689dd..94dd53d 100644 --- a/src/components/Game/GameOver.tsx +++ b/src/components/Game/GameOver.tsx @@ -1,26 +1,28 @@ -import { Card, CardBody, CardGroup, Container, CardHeader } from "reactstrap" +import { Card, CardContent, CardHeader, Grid } from "@mui/material" import Leaderboard from "components/Leaderboard/Leaderboard" import Confetti from "react-confetti" const GameOver = () => { return ( - - - - Game Over - - - - - - - + + + + + + + + + + + ) } diff --git a/src/components/Game/GameWrapper.tsx b/src/components/Game/GameWrapper.tsx index d3129fb..6463239 100644 --- a/src/components/Game/GameWrapper.tsx +++ b/src/components/Game/GameWrapper.tsx @@ -1,4 +1,3 @@ -import { Card, CardGroup } from "reactstrap" import MyCards from "./MyCards" import PlayersAndCards from "./PlayersAndCards" @@ -7,21 +6,34 @@ import WebsocketManager from "./WebsocketManager" import { useAppSelector } from "caches/hooks" import { getIamSpectator } from "caches/GameSlice" import ActionsWrapper from "./Actions/ActionsWrapper" +import { Card, styled } from "@mui/material" + +const GameCard = styled(Card)(({ theme }) => ({ + backgroundColor: "transparent", + borderStyle: "solid", + borderWidth: "5px", + borderRadius: "10px", + borderColor: + theme.palette.mode === "dark" + ? theme.palette.grey[300] + : theme.palette.grey[800], + padding: "2px", +})) const GameWrapper = () => { const iamSpectator = useAppSelector(getIamSpectator) return ( - + <> - + {!iamSpectator ? : null} {!iamSpectator ? : null} - - + + ) } diff --git a/src/components/Game/MyCards.tsx b/src/components/Game/MyCards.tsx index 5724e69..54cd210 100644 --- a/src/components/Game/MyCards.tsx +++ b/src/components/Game/MyCards.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useMemo } from "react" -import { CardImg, CardBody } from "reactstrap" import { DragDropContext, Draggable, @@ -7,7 +6,6 @@ import { DropResult, } from "react-beautiful-dnd" import { BLANK_CARD, SelectableCard } from "model/Cards" - import { RoundStatus } from "model/Round" import { getIamGoer, getIsRoundCalled, getRound } from "caches/GameSlice" import { useAppDispatch, useAppSelector } from "caches/hooks" @@ -23,6 +21,7 @@ import { toggleAutoPlay, clearAutoPlay, } from "caches/AutoPlaySlice" +import { CardContent, CardMedia } from "@mui/material" const EMPTY_HAND = [ { ...BLANK_CARD, selected: false }, @@ -122,7 +121,7 @@ const MyCards: React.FC = () => { if (cardsSelectable && card.name !== BLANK_CARD.name) { if (card.name === autoPlayCard) { - classes += " cardAutoPlayed" + classes += " auto-played" } else if (!card.selected) { classes += " cardNotSelected" } @@ -135,12 +134,12 @@ const MyCards: React.FC = () => { return (
- + {provided => (
@@ -163,7 +162,8 @@ const MyCards: React.FC = () => { ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}> - handleSelectCard( @@ -171,7 +171,7 @@ const MyCards: React.FC = () => { event, ) } - src={`/cards/thumbnails/${card.name}.png`} + image={`/cards/thumbnails/${card.name}.png`} className={getStyleForCard( card, )} @@ -186,14 +186,14 @@ const MyCards: React.FC = () => { )} - + - + {provided => (
@@ -219,7 +219,8 @@ const MyCards: React.FC = () => { ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}> - handleSelectCard( @@ -227,7 +228,7 @@ const MyCards: React.FC = () => { event, ) } - src={`/cards/thumbnails/${card.name}.png`} + image={`/cards/thumbnails/${card.name}.png`} className={getStyleForCard( card, )} @@ -242,7 +243,7 @@ const MyCards: React.FC = () => { )} - +
) } diff --git a/src/components/Game/PlayerCard.tsx b/src/components/Game/PlayerCard.tsx index 39ce2d4..b020e44 100644 --- a/src/components/Game/PlayerCard.tsx +++ b/src/components/Game/PlayerCard.tsx @@ -1,33 +1,162 @@ -import { useCallback, useMemo, useState } from "react" +import React, { useCallback, useEffect, useMemo, useState } from "react" import { - Col, - CardImgOverlay, - CardText, - CardImg, - Card, - CardSubtitle, - Modal, - ModalBody, - CardGroup, - CardHeader, - CardBody, -} from "reactstrap" + Grid, + CardMedia, + Typography, + Card as MuiCard, + CardContent, + Dialog, + DialogContent, + Stack, + Box, + useTheme, +} from "@mui/material" +import { styled } from "@mui/system" import { getGamePlayers, getRound } from "caches/GameSlice" import { useAppSelector } from "caches/hooks" import { getPlayerProfiles } from "caches/PlayerProfilesSlice" import { BLANK_CARD } from "model/Cards" import { PlayedCard } from "model/Game" - import { Player } from "model/Player" import Leaderboard from "components/Leaderboard/Leaderboard" +import { Suit } from "model/Suit" interface PlayerRowI { player: Player className?: string } + +const Card = styled(MuiCard)({ + background: "transparent", +}) + +const BlankCard: React.FC<{ name: string }> = ({ name }) => { + const theme = useTheme() + + return ( + + ) +} + +enum ChipType { + Dealer = "DEALER.png", + Call10 = "call_10.png", + Call15 = "call_15.png", + Call20 = "call_20.png", + Call25 = "call_25.png", + CallJink = "call_jink.png", + Clubs = "CLUBS_ICON.svg", + Diamonds = "DIAMONDS_ICON.svg", + Hearts = "HEARTS_ICON.svg", + Spades = "SPADES_ICON.svg", +} + +const SuitChip: React.FC<{ player: Player }> = ({ player }) => { + const round = useAppSelector(getRound) + + const isGoer: boolean = useMemo( + () => !!round && round.goerId === player.id, + [round, player], + ) + + const suitChip = useMemo(() => { + if (!round || !round.suit) return undefined + switch (round.suit) { + case Suit.CLUBS: + return ChipType.Clubs + case Suit.DIAMONDS: + return ChipType.Diamonds + case Suit.HEARTS: + return ChipType.Hearts + case Suit.SPADES: + return ChipType.Spades + default: + return undefined + } + }, [round]) + + if (!isGoer || !suitChip) return null + return ( + + + + ) +} + +const CallChip: React.FC<{ + call: number +}> = ({ call }) => { + const round = useAppSelector(getRound) + + const callChip = useMemo(() => { + if (!round || round.suit) return undefined + switch (call) { + case 10: + return ChipType.Call10 + case 15: + return ChipType.Call15 + case 20: + return ChipType.Call20 + case 25: + return ChipType.Call25 + case 30: + return ChipType.CallJink + default: + return undefined + } + }, [round, call]) + + if (!callChip) return null + return ( + + + + ) +} + +const DealerChip: React.FC<{ player: Player }> = ({ player }) => { + const round = useAppSelector(getRound) + + const isDealer: boolean = useMemo( + () => !!round && !round.suit && round.dealerId === player.id, + [round, player], + ) + + if (!isDealer) return null + return ( + + + + ) +} + const PlayerCard: React.FC = ({ player, className }) => { + const theme = useTheme() const round = useAppSelector(getRound) - const players = useAppSelector(getGamePlayers) const playerProfiles = useAppSelector(getPlayerProfiles) const [modalLeaderboard, updateModalLeaderboard] = useState(false) @@ -55,11 +184,6 @@ const PlayerCard: React.FC = ({ player, className }) => { [round, player], ) - const isDealer: boolean = useMemo( - () => !!round && !round.suit && round.dealerId === player.id, - [round, player], - ) - const scoreClassName = useMemo(() => { if (player.score < -30) { return "bg-dark text-light" @@ -79,120 +203,62 @@ const PlayerCard: React.FC = ({ player, className }) => { if (!profile) return null return ( - - - + + - - + + {player.score} - - + +
- + {playedCard ? ( - ) : ( - <> - - - {isDealer ? ( - - - - ) : null} - - {round && !round.suit ? ( - - {player.call === 10 ? ( - - ) : null} - {player.call === 15 ? ( - - ) : null} - {player.call === 20 ? ( - - ) : null} - {player.call === 25 ? ( - - ) : null} - {player.call === 30 && players.length === 2 ? ( - - ) : null} - {player.call === 30 && players.length > 2 ? ( - - ) : null} - - ) : null} - + + + + + + + + )} - - - - - Leaderboard - - - - - - - - + + + + + + + + ) } diff --git a/src/components/Game/PlayersAndCards.tsx b/src/components/Game/PlayersAndCards.tsx index 9d97fa3..610c410 100644 --- a/src/components/Game/PlayersAndCards.tsx +++ b/src/components/Game/PlayersAndCards.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react" -import { Row, CardBody, Container } from "reactstrap" +import { Grid, CardContent, Container } from "@mui/material" import { getGamePlayers } from "caches/GameSlice" import { useAppSelector } from "caches/hooks" import { Player } from "model/Player" @@ -20,28 +20,24 @@ const PlayersAndCards = () => { ) return ( - + - + {sortedPlayers.map((player, index) => ( - + + + ))} - + - + ) } diff --git a/src/components/GameStats/GameStats.tsx b/src/components/GameStats/GameStats.tsx index aba1676..3985fb8 100644 --- a/src/components/GameStats/GameStats.tsx +++ b/src/components/GameStats/GameStats.tsx @@ -1,4 +1,11 @@ -import { Card, CardHeader, CardBody, CardGroup, Input, Label } from "reactstrap" +import { + Card as MuiCard, + CardContent, + CardHeader, + Grid, + Switch, + FormControlLabel, +} from "@mui/material" import { useCallback, useEffect, useState } from "react" import { useAppSelector } from "caches/hooks" import { getMyProfile } from "caches/MyProfileSlice" @@ -20,32 +27,33 @@ const GameStats = () => { ) return ( - - - Stats - + + + + {myProfile.isAdmin ? ( ) : null} - - + + {player ? ( ) : null} - - - - + + + } + label="Last 3 months" + /> + + ) } diff --git a/src/components/GameStats/PlayerSwitcher.tsx b/src/components/GameStats/PlayerSwitcher.tsx index 1086c0d..b6b78c7 100644 --- a/src/components/GameStats/PlayerSwitcher.tsx +++ b/src/components/GameStats/PlayerSwitcher.tsx @@ -1,10 +1,12 @@ import { useCallback, useEffect, useMemo, useState } from "react" import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownToggle, -} from "reactstrap" + IconButton, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + Avatar, +} from "@mui/material" import { useAppSelector } from "caches/hooks" import { getMyProfile } from "caches/MyProfileSlice" import { getPlayerProfiles } from "caches/PlayerProfilesSlice" @@ -18,13 +20,16 @@ interface Props { const PlayerSwitcher: React.FC = ({ onChange }) => { const myProfile = useAppSelector(getMyProfile) const players = useAppSelector(getPlayerProfiles) - const [showDropdown, setShowDropdown] = useState(false) + const [anchorEl, setAnchorEl] = useState(null) const [currentPlayer, setCurrentPlayer] = useState() - const toggleDropdown = useCallback( - () => setShowDropdown(!showDropdown), - [showDropdown], - ) + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } const sortedPlayers = useMemo( () => @@ -47,29 +52,45 @@ const PlayerSwitcher: React.FC = ({ onChange }) => { }, [currentPlayer]) return ( - - - + + setShowDropdown(true)} + className="clickable" /> - - + + {sortedPlayers.map(p => { return ( - setCurrentPlayer(p)}> - {FormatName(p.name)} - + onClick={() => { + setCurrentPlayer(p) + handleClose() + }}> + + + + + ) })} - - + + ) } diff --git a/src/components/Header/NavBar.tsx b/src/components/Header/NavBar.tsx index 08632a2..189e145 100644 --- a/src/components/Header/NavBar.tsx +++ b/src/components/Header/NavBar.tsx @@ -3,44 +3,78 @@ import { useNavigate } from "react-router-dom" import { useAuth0 } from "@auth0/auth0-react" -import { useAppSelector } from "caches/hooks" -import { getMyProfile } from "caches/MyProfileSlice" +import { useAppDispatch, useAppSelector } from "caches/hooks" import ProfilePictureEditor from "components/Avatar/ProfilePictureEditor" -import GameHeader from "components/Game/GameHeader" import { + AppBar, + Box, + Button, Card, - CardBody, - CardGroup, + CardContent, CardHeader, - Col, - Container, - Dropdown, - DropdownItem, - DropdownMenu, - DropdownToggle, - Modal, - ModalBody, - Row, -} from "reactstrap" -import { getIsGameActive } from "caches/GameSlice" + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, + IconButton, + Menu, + MenuItem, + Toolbar, +} from "@mui/material" +import { getIsGameActive, getIamAdmin } from "caches/GameSlice" import Leaderboard from "components/Leaderboard/Leaderboard" -import MenuButton from "assets/icons/Menu.svg" +import MenuButton from "@mui/icons-material/Menu" +import HomeButton from "@mui/icons-material/Home" +import GameService from "services/GameService" +import { useSnackbar } from "notistack" +import parseError from "utils/ErrorUtils" const NavBar = () => { const { logout } = useAuth0() const navigate = useNavigate() + const dispatch = useAppDispatch() + const { enqueueSnackbar } = useSnackbar() + const [modalDeleteGameOpen, updateModalDeleteGameOpen] = useState(false) const isGameActive = useAppSelector(getIsGameActive) + const gameId = useAppSelector(state => state.game.id) + const iamAdmin = useAppSelector(getIamAdmin) const [showEditAvatar, setShowEditAvatar] = useState(false) - const [showDropdown, setShowDropdown] = useState(false) + const [anchorEl, setAnchorEl] = useState(null) const [modalLeaderboard, updateModalLeaderboard] = useState(false) - const toggleDropdown = useCallback( - () => setShowDropdown(!showDropdown), - [showDropdown], - ) + const deleteGame = useCallback(() => { + if (!gameId) { + return + } + dispatch(GameService.deleteGame(gameId)) + .then(() => { + enqueueSnackbar("Game deleted") + navigate("/") + }) + .catch((e: Error) => + enqueueSnackbar(parseError(e), { variant: "error" }), + ) + }, [dispatch, enqueueSnackbar, navigate, gameId]) + + const showDeleteGameModal = () => { + updateModalDeleteGameOpen(true) + } + + const handleCloseDeleteGameModal = useCallback(() => { + updateModalDeleteGameOpen(false) + }, [updateModalDeleteGameOpen]) + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } const toggleLeaderboardModal = useCallback( () => updateModalLeaderboard(!modalLeaderboard), @@ -54,86 +88,100 @@ const NavBar = () => { const navigateHome = () => navigate("/") - const myProfile = useAppSelector(getMyProfile) - const signOut = () => logout() - if (!myProfile) { - return null - } - return ( - + + + + + + + + + + + + You are about to Delete a game + + + Are you sure you want to delete this game? + + + + + + + + ) } diff --git a/src/components/Leaderboard/DoublesLeaderboard.tsx b/src/components/Leaderboard/DoublesLeaderboard.tsx index e80761b..7422fd0 100644 --- a/src/components/Leaderboard/DoublesLeaderboard.tsx +++ b/src/components/Leaderboard/DoublesLeaderboard.tsx @@ -1,12 +1,18 @@ import React, { useCallback, useMemo } from "react" -import DataTable, { TableColumn } from "react-data-table-component" -import TrophyImage from "assets/icons/trophy.png" +import VictoryIcon from "@mui/icons-material/EmojiEventsTwoTone" import { useAppSelector } from "caches/hooks" -import { getGamePlayers, getRound, getIsGameFinished } from "caches/GameSlice" +import { getGamePlayers, getIsGameActive, getRound } from "caches/GameSlice" import { getPlayerProfiles } from "caches/PlayerProfilesSlice" import { compareScore, compareTeamIds } from "utils/PlayerUtils" import { Player } from "model/Player" -import { customStyles } from "components/Tables/CustomStyles" +import { + List, + ListItem, + ListItemIcon, + Avatar, + ListItemText, + ListItemSecondaryAction, +} from "@mui/material" interface LeaderBoardPlayer { cardsBought?: number @@ -28,7 +34,7 @@ const DoublesLeaderboard = () => { const round = useAppSelector(getRound) const players = useAppSelector(getGamePlayers) const playerProfiles = useAppSelector(getPlayerProfiles) - const isGameFinished = useAppSelector(getIsGameFinished) + const isGameActive = useAppSelector(getIsGameActive) const previousHand = useMemo(() => { if (round) return round.completedHands[round.completedHands.length - 1] @@ -93,120 +99,40 @@ const DoublesLeaderboard = () => { return items.sort(compareScore) }, [players]) - const columns: TableColumn[] = [ - { - cell: row => ( - <> -
- Image Preview - - {!isGameFinished && !!row.player1.previousCard ? ( -
- {previousHand ? ( - {row.player1.previousCard} - ) : null} -
- ) : null} - - {!isGameFinished ? ( -
- {!!row.player1.cardsBought - ? `Bought: ${row.player1.cardsBought}` - : ``} -
- ) : null} -
- - ), - }, - { - cell: row => ( -
- Image Preview - - {!isGameFinished && previousHand ? ( -
- {previousHand ? ( - {row.player2.previousCard} - ) : null} -
- ) : null} - - {!isGameFinished ? ( -
- {!!row.player2.cardsBought - ? `Bought: ${row.player2.cardsBought}` - : ``} -
- ) : null} -
- ), - }, - { - name: "Score", - selector: row => row.score, - sortable: true, - center: true, - }, - { - name: "Rings", - selector: row => row.rings, - sortable: true, - center: true, - }, - { - cell: row => ( -
- {row.winner ? ( - - ) : null} -
- ), - center: true, - omit: !isGameFinished, - }, - ] - if (!playerProfiles || playerProfiles.length === 0) { return null } return ( - + + {leaderboardData.map((item, index) => ( + + + {item.player1.name} + + + {item.player2.name} + + + + + {item.winner && } + + + ))} + ) } diff --git a/src/components/Leaderboard/SinglesLeaderboard.tsx b/src/components/Leaderboard/SinglesLeaderboard.tsx index e8a5c66..181e95b 100644 --- a/src/components/Leaderboard/SinglesLeaderboard.tsx +++ b/src/components/Leaderboard/SinglesLeaderboard.tsx @@ -1,12 +1,16 @@ import React, { useCallback, useMemo } from "react" -import DataTable, { TableColumn } from "react-data-table-component" -import TrophyImage from "assets/icons/trophy.png" -import { getGame } from "caches/GameSlice" +import VictoryIcon from "@mui/icons-material/EmojiEventsTwoTone" +import { getGame, getIsGameActive } from "caches/GameSlice" import { useAppSelector } from "caches/hooks" import { getPlayerProfiles } from "caches/PlayerProfilesSlice" -import { GameStatus } from "model/Game" import { Player } from "model/Player" -import { customStyles } from "components/Tables/CustomStyles" +import { + List, + ListItem, + ListItemIcon, + ListItemText, + ListItemSecondaryAction, +} from "@mui/material" interface LeaderboardItem { previousCard?: string @@ -20,6 +24,7 @@ interface LeaderboardItem { const SinglesLeaderboard = () => { const game = useAppSelector(getGame) + const isGameActive = useAppSelector(getIsGameActive) const playerProfiles = useAppSelector(getPlayerProfiles) const previousHand = useMemo(() => { @@ -29,8 +34,6 @@ const SinglesLeaderboard = () => { ] }, [game]) - const gameOver = useMemo(() => game.status === GameStatus.FINISHED, [game]) - const getProfile = useCallback( (player: Player) => playerProfiles.find(p => p.id === player.id, [playerProfiles]), @@ -59,61 +62,9 @@ const SinglesLeaderboard = () => { winner: player.winner, }) }) - return leaderboardData + return leaderboardData.sort((a, b) => b.score - a.score) }, [playerProfiles, game, getProfile, previousHand]) - const columns: TableColumn[] = useMemo( - () => [ - { - cell: row => ( - {row.name} - ), - }, - { - cell: row => ( - {row.previousCard} - ), - center: true, - omit: gameOver || !previousHand, - }, - { - name: "Score", - selector: row => row.score, - sortable: true, - center: true, - }, - { - name: "Rings", - selector: row => row.rings, - sortable: true, - center: true, - }, - { - name: "Bought", - selector: row => row.cardsBought, - sortable: true, - center: true, - omit: gameOver, - }, - { - cell: row => ( -
- {row.winner ? ( - - ) : null} -
- ), - center: true, - omit: !gameOver, - }, - ], - [gameOver, previousHand], - ) - if ( !game || !game.status || @@ -125,16 +76,37 @@ const SinglesLeaderboard = () => { return ( - + + {leaderboardData.map((item, index) => ( + + + {item.name} + + + + + + {item.winner ? ( + + ) : isGameActive && item.previousCard ? ( + {item.previousCard} + ) : null} + + + ))} + ) } diff --git a/src/components/MyGames/MyGames.tsx b/src/components/MyGames/MyGames.tsx index 1c01a13..34afa93 100644 --- a/src/components/MyGames/MyGames.tsx +++ b/src/components/MyGames/MyGames.tsx @@ -1,82 +1,51 @@ -import React, { useCallback, useState } from "react" +import React, { useMemo } from "react" -import RemoveImage from "assets/icons/remove.png" -import { Link } from "react-router-dom" +import { useNavigate } from "react-router-dom" -import GameService from "services/GameService" - -import DataTable, { TableColumn } from "react-data-table-component" -import TrophyImage from "assets/icons/trophy.png" +import PlayIcon from "@mui/icons-material/PlayCircleFilled" +import VisibilityIcon from "@mui/icons-material/Visibility" +import VictoryIcon from "@mui/icons-material/EmojiEventsTwoTone" +import FailureIcon from "@mui/icons-material/MoneyOffTwoTone" import { - Modal, - ModalBody, - ModalHeader, - ModalFooter, - Button, Card, - CardBody, - CardGroup, + CardContent, CardHeader, -} from "reactstrap" + Grid, + List, + ListItem, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, +} from "@mui/material" + import moment from "moment" -import { useAppDispatch, useAppSelector } from "caches/hooks" +import { useAppSelector } from "caches/hooks" import { getMyGames } from "caches/MyGamesSlice" import { getMyProfile } from "caches/MyProfileSlice" -import { useSnackbar } from "notistack" import { Game, GameStatus } from "model/Game" -import { customStyles } from "components/Tables/CustomStyles" -import parseError from "utils/ErrorUtils" +import { Player } from "model/Player" + +const sortByDate = (a: Game, b: Game) => { + return moment(b.timestamp).diff(moment(a.timestamp)) +} +// Filters out the dummy player and returns the number of players in the game +const getNumberOfPlayers = (players: Player[]) => { + return players.filter(player => player.id !== "dummy").length +} const MyGames = () => { - const dispatch = useAppDispatch() - const { enqueueSnackbar } = useSnackbar() + const navigateTo = useNavigate() const myGames = useAppSelector(getMyGames) const myProfile = useAppSelector(getMyProfile) - const [modalDeleteGameOpen, updateModalDeleteGameOpen] = useState(false) - const [deleteGameId, updateDeleteGameId] = useState("") - - const deleteGame = () => { - dispatch(GameService.deleteGame(deleteGameId)) - .then(() => { - enqueueSnackbar("Game deleted") - handleCloseDeleteGameModal() - }) - .catch((e: Error) => - enqueueSnackbar(parseError(e), { variant: "error" }), - ) - } + const last10Games = useMemo(() => { + return [...myGames].sort(sortByDate).slice(0, 7) + }, [myGames]) - const showDeleteGameModal = (id: string) => { - updateModalDeleteGameOpen(true) - updateDeleteGameId(id) - } - - const handleCloseDeleteGameModal = useCallback(() => { - updateModalDeleteGameOpen(false) - updateDeleteGameId("") - }, [updateDeleteGameId, updateModalDeleteGameOpen]) - - const parsePlayButtonLabel = (game: Game, playerId: string) => { - if (game.status === GameStatus.ACTIVE) { - if (isInGame(game, playerId)) { - return "Play" - } - return "Spectate" - } - return "Result" - } - - const parsePlayButtonColour = (game: Game, playerId: string) => { - if (game.status === GameStatus.ACTIVE) { - if (isInGame(game, playerId)) { - return "success" - } - return "primary" - } - return "secondary" + const isGameActive = (game: Game) => { + return game.status === GameStatus.ACTIVE } const isWinner = (game: Game, playerId: string) => { @@ -85,105 +54,63 @@ const MyGames = () => { return !!player && player.winner } - const isInGame = (game: Game, playerId: string) => { - return game.players.some(e => e.id === playerId) - } + const isLoser = (game: Game, playerId: string) => { + if (game.status === GameStatus.ACTIVE) return false + const player = game.players.find(e => e.id === playerId) - const columns: TableColumn[] = [ - { - cell: row => ( - - - - ), - center: true, - }, - { name: "Name", selector: row => row.name, sortable: true }, - { - name: "Date", - selector: row => row.timestamp, - format: row => moment(row.timestamp).format("llll"), - sortable: true, - }, - { - cell: row => ( -
- {isWinner(row, myProfile.id!) ? ( - - ) : null} -
- ), - center: true, - }, - - { - cell: row => ( - - ), - center: true, - omit: !myProfile.isAdmin, - }, - ] + return !!player && !player.winner + } return ( - - - Games - - - - - - - You are about to Delete a game - - - Are you sure you want to delete this game? - - - - - - - - + + + + + + + {last10Games.map(row => ( + + + navigateTo(`/game/${row.id}`) + } + sx={{ + cursor: "pointer", + }}> + {isGameActive(row) ? ( + + ) : ( + + )} + + + + + + {isWinner(row, myProfile.id!) && ( + + )} + {isLoser(row, myProfile.id!) && ( + + )} + + + ))} + + + + + ) } diff --git a/src/components/StartNewGame/StartNewGame.tsx b/src/components/StartNewGame/StartNewGame.tsx index 9f4054d..784ef87 100644 --- a/src/components/StartNewGame/StartNewGame.tsx +++ b/src/components/StartNewGame/StartNewGame.tsx @@ -1,33 +1,35 @@ import React, { useCallback, useState } from "react" import GameService from "services/GameService" -import DataTable, { TableColumn } from "react-data-table-component" import { getPlayerProfiles } from "caches/PlayerProfilesSlice" import { Button, ButtonGroup, - Form, - FormGroup, - Input, Card, - CardBody, - CardGroup, + CardContent, CardHeader, - Label, -} from "reactstrap" - + FormControl, + Grid, + InputLabel, + OutlinedInput, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + useTheme, +} from "@mui/material" import { useAppDispatch, useAppSelector } from "caches/hooks" import { PlayerProfile } from "model/Player" import { useSnackbar } from "notistack" -import { customStyles } from "components/Tables/CustomStyles" import parseError from "utils/ErrorUtils" -import moment from "moment" import { FormatName } from "utils/FormattingUtils" import WinPercentageGraph from "components/GameStats/WinPercentageGraph" -import { Divider } from "@mui/material" const StartNewGame = () => { + const theme = useTheme() const dispatch = useAppDispatch() const { enqueueSnackbar } = useSnackbar() @@ -39,10 +41,16 @@ const StartNewGame = () => { ) const togglePlayer = useCallback( - (rows: { selectedRows: React.SetStateAction }) => { - updateSelectedPlayers(rows.selectedRows) + (player: PlayerProfile) => { + if (selectedPlayers.includes(player)) { + updateSelectedPlayers( + selectedPlayers.filter(p => p.id !== player.id), + ) + } else { + updateSelectedPlayers([...selectedPlayers, player]) + } }, - [], + [selectedPlayers], ) const startGame = useCallback( @@ -87,112 +95,124 @@ const StartNewGame = () => { [], ) - const columns: TableColumn[] = [ - { - name: "Player", - selector: row => row.name, - cell: (row: PlayerProfile) => ( -
- Image Preview - - - {FormatName(row.name)} - -
- ), - format: row => FormatName(row.name), - sortable: true, - }, - { - name: "Last 3 months", - cell: (pp: PlayerProfile) => ( - - ), - center: true, - }, - { - name: "All Time", - cell: (pp: PlayerProfile) => ( - - ), - center: true, - }, - { - name: "Last Access", - id: "lastAccess", - selector: row => row.lastAccess, - format: row => moment(row.lastAccess).format("lll"), - sortable: true, - omit: true, - }, - ] - return ( - - - Start a new game - - -
- - - - - - -
- -
-
-
-
+ + + + + + +
+ + + + + Game Name + + + + + + + + + + +
+ + + + + Player + Last 3 months + All Time + + + + {allPlayers.map(player => ( + + togglePlayer(player) + } + selected={selectedPlayers.includes( + player, + )} + sx={{ + cursor: "pointer", + "&.Mui-selected, &.Mui-selected:hover": + { + backgroundColor: + theme.palette + .action + .selected, + }, + }}> + +
+ Image Preview + + + {FormatName( + player.name, + )} + + +
+
+ + + + + + +
+ ))} +
+
+
+
+
+
+
+
) } diff --git a/src/model/Game.ts b/src/model/Game.ts index 681dc48..6ff143f 100644 --- a/src/model/Game.ts +++ b/src/model/Game.ts @@ -32,6 +32,7 @@ export interface GameState { isMyGo: boolean iamGoer: boolean iamDealer: boolean + iamAdmin: boolean cards: string[] status: GameStatus round?: Round diff --git a/src/pages/Game/Game.tsx b/src/pages/Game/Game.tsx index d5ae465..f452485 100644 --- a/src/pages/Game/Game.tsx +++ b/src/pages/Game/Game.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useEffect } from "react" import GameWrapper from "components/Game/GameWrapper" import GameOver from "components/Game/GameOver" import GameService from "services/GameService" -import PullToRefresh from "react-simple-pull-to-refresh" import { withAuthenticationRequired } from "@auth0/auth0-react" @@ -13,7 +12,6 @@ import { useSnackbar } from "notistack" import { getIamSpectator, getIsGameActive, resetGame } from "caches/GameSlice" import { clearAutoPlay } from "caches/AutoPlaySlice" import { clearMyCards } from "caches/MyCardsSlice" -import RefreshingData from "components/icons/RefreshingData" import parseError from "utils/ErrorUtils" const Game = () => { @@ -50,17 +48,13 @@ const Game = () => { }, []) return ( - }> -
-
-
- {isGameActive ? : } -
+
+
+
+ {isGameActive ? : }
- +
) } diff --git a/src/pages/Game/_game.scss b/src/pages/Game/_game.scss index fea7cf3..5d4ca67 100644 --- a/src/pages/Game/_game.scss +++ b/src/pages/Game/_game.scss @@ -1,7 +1,3 @@ -.body { - font-family: $fontFamily; -} - .card { --cui-card-bg: ; border: 0; @@ -12,51 +8,44 @@ filter: brightness(70%); } -.cardAutoPlayed { +.auto-played { filter: drop-shadow(16px 16px 20px red); } -.myCards { - border-style: solid; - border-width: 5px; - border-radius: 10px; - padding: 2px; - background-image: url("/assets/img/mycards.png"); - background-size: contain; +.highlight-player-light { + filter: drop-shadow(16px 16px 20px black); +} + +.highlight-player-dark { + filter: drop-shadow(16px 16px 20px white); } -.dummy { - background-image: url("/assets/img/dummy.png"); +.dull-player { + filter: grayscale(25%); + opacity: 0.7; } -.gameContainer { +.my-cards { border-style: solid; border-width: 5px; border-radius: 10px; padding: 2px; + background-size: contain; } - -// .carpet { -// background-image: url("/assets/img/carpet2.jpg"); -// } - -.wood { - background-image: url("/assets/img/wood.jpg"); - background-color: brown; -} - .overlay-chip { margin-top: 5rem; } -.player-score { - font-weight: bolder; - font-size: 1.5em; - color: white; +.overlay-suit-chip { + margin-top: 3rem; +} + +.overlay-dealer-chip { + margin-top: 2rem; } .score-text { - width: 2.5em; + width: 4em; display: inline-block; border-radius: 16%; } @@ -66,8 +55,18 @@ padding-bottom: 0.25em; } -.overlay-dealer-chip { - margin-top: 2rem; +.no-shadow { + box-shadow: none !important; +} + +.thumbnail-chips { + max-width: 70px; + max-height: 70px; +} + +.thumbnail-suit { + max-width: 45px; + max-height: 60px; } .player-column { @@ -98,18 +97,35 @@ padding-right: 0.2rem; } -.cardAreaHeader { - color: rgb(0, 0, 0); - font-weight: bold; - text-align: center; - width: 100%; +.image-team1-filter { + border: solid 4px red; } -.cardAreaHeaderContainer { - color: rgb(0, 0, 0); - background-color: #ced2d8; - padding-top: 0.25em; - padding-bottom: 0.25em; +.image-team2-filter { + border: solid 4px yellow; +} + +.image-team3-filter { + border: solid 4px blue; +} + +.clickable:hover { + cursor: pointer; +} + +.thumbnail_size { + max-width: 100px; + max-height: 153px; +} + +.thumbnail_size_small { + max-width: 70px; + max-height: 100px; +} + +.thumbnail_size_extra_small { + max-width: 45px; + max-height: 45px; } .leaderboard-button { @@ -132,8 +148,7 @@ } .buttonArea { - min-height: 84px; - text-align: center; + min-height: 75px; } .gameModalBody { @@ -164,12 +179,6 @@ } } -@media (max-width: 768px) { - .overlay-chip { - margin-top: 5rem; - } -} - @media (max-width: 420px) { .card-img-overlay { padding-left: 0; @@ -183,6 +192,10 @@ margin-top: 1rem; } + .overlay-suit-chip { + margin-top: 0.5rem; + } + .overlay-dealer-chip { margin-top: 0.5rem; } diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 3474b34..46ec7b3 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -1,21 +1,17 @@ import React, { useEffect } from "react" -import { Card, CardGroup, CardHeader } from "reactstrap" - +import { Card, CardHeader, Grid } from "@mui/material" import StartNewGame from "components/StartNewGame/StartNewGame" import MyGames from "components/MyGames/MyGames" import GameStats from "components/GameStats/GameStats" import { withAuthenticationRequired } from "@auth0/auth0-react" -import PullToRefresh from "react-simple-pull-to-refresh" import { useAppDispatch, useAppSelector } from "caches/hooks" import { getMyProfile } from "caches/MyProfileSlice" import GameService from "services/GameService" import { useSnackbar } from "notistack" import StatsService from "services/StatsService" -import { Divider } from "@mui/material" import parseError from "utils/ErrorUtils" -import RefreshingData from "components/icons/RefreshingData" const Home = () => { const dispatch = useAppDispatch() @@ -40,47 +36,35 @@ const Home = () => { }, []) return ( - }> -
-
-
- {!myProfile.isPlayer && !myProfile.isAdmin ? ( - - - - You are successfully logged in but don't - yet have any access permissions. Please - contact Daithi to get access. - +
+
+
+ {!myProfile.isPlayer && !myProfile.isAdmin ? ( + + + + - - ) : ( -
- {myProfile.isPlayer ? ( - <> - - - - - - ) : null} - {myProfile.isPlayer && !myProfile.isAdmin ? ( - <> - - - - - - ) : null} - {myProfile.isAdmin ? : null} -
- )} -
+ + + ) : ( +
+ {myProfile.isPlayer ? ( + + + + ) : null} + {myProfile.isPlayer && !myProfile.isAdmin ? ( + + + + ) : null} + {myProfile.isAdmin ? : null} +
+ )}
- +
) } diff --git a/src/pages/Home/_home.scss b/src/pages/Home/_home.scss index 8532082..faad98b 100644 --- a/src/pages/Home/_home.scss +++ b/src/pages/Home/_home.scss @@ -1,23 +1,3 @@ -.body { - font-family: $fontFamily; -} -.app_body { - width: auto; - flex: 1; - position: relative; - font-family: $fontFamily; -} - -.data-card { - background-color: $white; - opacity: 0.95; -} - -.navBarInner { - max-width: 800px; - min-height: 45px; -} - .overflow-hidden { overflow: hidden; } @@ -32,49 +12,7 @@ } .game_container { - font-size: 16px; - line-height: 24px; - padding: 100px 20px 100px 20px; - - &_headerText { - height: 40px; - width: 92px; - color: #000000; - font-size: $fontSizeH2; - line-height: 40px; - font-weight: $fontWeightBold; - font-family: $fontFamily; - margin: 0 auto 24px auto; - } - - &_subtext { - min-height: 44px; - max-width: 293px; - color: #0f0f0f; - font-family: $fontFamily; - font-size: $fontSizeBody; - line-height: 22px; - text-align: center; - margin: 0 auto 34px auto; - } - - &_text { - height: auto; - max-width: 304px; - color: $black; - font-family: $fontFamily; - font-size: 16px; - line-height: 20px; - text-align: center; - margin: 40px auto 20px auto; - - &_link { - color: $blue; - font-weight: $fontWeightBold; - font-family: $fontFamily; - cursor: pointer; - } - } + padding: 10px 10px 10px 10px; } .player-avatar { @@ -99,246 +37,10 @@ border-radius: 50%; } -.image-team1-filter { - border: solid 4px red; - filter: saturate(2); -} - -.image-team2-filter { - border: solid 4px yellow; - filter: sepia(100%); -} - -.image-team3-filter { - border: solid 4px blue; - // filter: invert(100%); -} - -.clickable:hover { - cursor: pointer; -} - -.thumbnail_size { - max-width: 100px; - max-height: 153px; -} - -.thumbnail_size_small { - max-width: 70px; - max-height: 100px; -} - -.thumbnail_size_extra_small { - max-width: 45px; - max-height: 45px; -} - -.thumbnail_chips { - max-width: 70px; - max-height: 70px; -} - -.background_white { - border-radius: 5px; - background-color: white; -} - .transparent { opacity: 0.6; } -.diplay_image_size { - max-width: 512px; - max-height: 512px; -} - -.dropdown-menu-quizzes { - // position: absolute; - /* top: 100%; */ - /* left: 0; */ - z-index: 1000; - /* display: none; */ - // float: left; - min-width: 10rem; - padding: 0 0; - margin: 0.125rem 0 0; - font-size: 0.875rem; - color: #23282c; - text-align: left; - list-style: none; - background-color: white; - background-clip: padding-box; - border: 1px solid #c8ced3; - border-radius: 0.25rem; -} - -.home-tile_wrap { - max-width: 1140px; - overflow: overlay; - margin-left: auto; - margin-right: auto; - padding: 20px 40px 0 40px; - font-size: $fontSizeH2; - font-family: $fontFamily; - color: $blackShade2; - font-weight: $fontWeightBlack; - line-height: 62px; - pointer-events: none; - - &_sub { - font-size: $fontSizeBody; - line-height: 22px; - font-weight: $fontWeightRegular; - font-family: $fontFamily; - pointer-events: none; - } -} - -.tile_wrap { - max-width: 1140px; - overflow: overlay; - margin-left: auto; - margin-right: auto; - padding: 0 40px 0 40px; -} - -.card-product_Stats { - margin: 35px 0 10px 0; - overflow: hidden; - min-height: 340px; - background: white; - display: flex; - align-items: center; - border-radius: 12px; - box-shadow: 1px 4px 11px 2px rgba(0, 0, 0, 0.06); - - .rwd-table { - min-width: 300px; - max-width: 100%; - margin-bottom: 10px; - } - - .TableBody { - border: 1px solid $greyshade4; - } - - .rwd-table tr:first-child { - background: $white; - color: #808080; - } - - .rwd-table th { - display: none; - } - - .rwd-table td { - display: block; - } - - .rwd-table td:first-child { - margin-top: 0.5em; - } - - .rwd-table td:last-child { - margin-bottom: 0.5em; - } - - .rwd-table td:before { - content: attr(data-th) ": "; - font-weight: $fontWeightBold; - width: 120px; - display: inline-block; - color: $blackShade2; - } - - .rwd-table th, - .rwd-table td { - text-align: left; - border-right: 1px solid $greyshade4; - } - .rwd-table th:last-child, - .rwd-table td:last-child { - border-right: none; - } - - .rwd-table { - color: #333; - overflow-y: scroll; - } - - .rwd-table tr { - border-color: #bfbfbf; - } - - .rwd-table th, - .rwd-table td { - padding: 0.5em 1em; - } - @media screen and (max-width: 800px) { - .rwd-table tr:nth-child(2) { - border-top: 1px solid $greyshade4 !important ; - } - - .rwd-table tr { - border-right: none; - border-bottom: 1px solid $greyshade4; - } - } - @media screen and (min-width: 800px) { - .game_wrap { - max-width: 790px; - } - .rwd-table tr:hover:not(:first-child) { - background-color: #d8e7f3; - } - .rwd-table td:before { - display: none; - } - .rwd-table th, - .rwd-table td { - display: table-cell; - padding: 0.25em 0.5em; - } - .rwd-table th:first-child, - .rwd-table td:first-child { - padding-left: 0; - } - - .rwd-table th, - .rwd-table td { - padding: 1em !important; - } - } - - &_textContent { - margin: 0px 20px 20px 80px; - } - - &_Content { - font-size: $fontSizeH1; - line-height: 68px; - color: #000000; - font-weight: $fontWeightBold; - font-family: $fontFamily; - margin-top: 10px; - margin-bottom: 15px; - text-align: left; - max-width: 600px; - pointer-events: none; - } - - &_subContent { - font-size: $fontSizeBody; - color: black; - font-weight: 300; - font-family: $fontFamily; - margin-top: 14px; - margin-bottom: 28px; - text-align: left; - pointer-events: none; - } -} - .diplay_image_size { max-width: 1100px; max-height: 1024px; @@ -386,25 +88,6 @@ .tile_wrap { padding: 0 20px 0 20px; } - - .card-product_Stats { - margin: 35px 0; - height: auto; - display: block; - margin-left: auto; - margin-right: auto; - display: flex; - flex-direction: column-reverse; - - &_textContent { - margin: 0px 30px 10px 30px; - } - - &_Content { - margin-top: 28px; - font-size: 30px; - } - } } @media only screen and (max-width: 900px) { @@ -486,11 +169,6 @@ max-width: 460px; } - .card-product_StatsHome { - &_Content { - font-size: 38px !important ; - } - } } @media only screen and (max-width: 420px) { @@ -503,11 +181,6 @@ max-width: 320px; } - .card-product_StatsHome { - &_Content { - font-size: 38px !important ; - } - } } @media only screen and (max-width: 385px) { diff --git a/src/pages/Layout/Layout.tsx b/src/pages/Layout/Layout.tsx index 0688a44..b97957c 100644 --- a/src/pages/Layout/Layout.tsx +++ b/src/pages/Layout/Layout.tsx @@ -5,6 +5,7 @@ import { getAccessToken } from "caches/MyProfileSlice" import DefaultHeader from "components/Header/Header" import { useEffect } from "react" import Loading from "components/icons/Loading" +import { Box } from "@mui/material" const AUTH0_AUDIENCE = process.env.REACT_APP_AUTH0_AUDIENCE as string const AUTH0_SCOPE = process.env.REACT_APP_AUTH0_SCOPE as string @@ -25,7 +26,9 @@ const Layout = () => { }, [isLoading, isAuthenticated]) return ( -
+
@@ -43,7 +46,7 @@ const Layout = () => {
-
+ ) } diff --git a/src/scss/_custom.scss b/src/scss/_custom.scss index f894f07..2b2fe79 100644 --- a/src/scss/_custom.scss +++ b/src/scss/_custom.scss @@ -4,13 +4,6 @@ body { // Here you can add other styles .main_content { - background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='40' height='40' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(135)'%3E%3Crect width='100%25' height='100%25' fill='rgba(47, 133, 90,1)'/%3E%3Ccircle cx='40' cy='20' r='1' fill='%231a202c'/%3E%3Ccircle cx='0' cy='20' r='1' fill='%231a202c'/%3E%3Ccircle cx='20' cy='40' r='1' fill='%231a202c'/%3E%3Ccircle cx='20' cy='0' r='1' fill='%231a202c'/%3E%3Ccircle cx='38' cy='2' r='1' fill='%23ecc94b'/%3E%3Ccircle cx='2' cy='38' r='1' fill='%23ecc94b'/%3E%3Ccircle cx='38' cy='38' r='1' fill='%23ecc94b'/%3E%3Ccircle cx='2' cy='2' r='1' fill='%23ecc94b'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E "); - background-color: #2f855a; - position: fixed; - width: 100%; - height: 100%; - overflow: overlay; - &_mainBody { width: 100%; }