Compare commits

...

87 Commits

Author SHA1 Message Date
Viktor Rådberg
354c0dbbb2 bump 2024-01-20 11:11:03 +01:00
Viktor Rådberg
3770d13beb fix some styling 2024-01-20 10:56:53 +01:00
Viktor Rådberg
13733242a2 bump 2024-01-14 14:39:20 +01:00
Viktor Rådberg
81f3891b20 add better pwa support 2024-01-14 14:38:56 +01:00
Viktor Rådberg
e153de9093 Release 0.5.51 2024-01-14 13:42:11 +01:00
Viktor Rådberg
07775f85d2 fix start menu style 2024-01-14 13:41:51 +01:00
Viktor Rådberg
10039175a1 bump 2024-01-14 13:14:53 +01:00
Viktor Rådberg
bcf2a0a840 new colors 2024-01-14 13:14:33 +01:00
Viktor Rådberg
d25da5d97b fix styling 2024-01-14 12:31:57 +01:00
Viktor Rådberg
f5a80e573e cache 2024-01-14 10:41:14 +01:00
Viktor Rådberg
1f36264e39 update package json 2024-01-14 10:40:48 +01:00
Viktor Rådberg
d615cfd3ba reset game styling 2024-01-14 10:38:28 +01:00
Viktor Rådberg
4453b12ce6 bump 2024-01-13 20:32:10 +01:00
Viktor Rådberg
d601a820f8 remove log 2024-01-13 20:32:00 +01:00
Viktor Rådberg
0455f43794 test 2024-01-13 20:26:07 +01:00
Viktor Rådberg
f94103fe51 fix 2024-01-13 20:20:50 +01:00
Viktor Rådberg
c36668b933 fix 2024-01-13 20:20:20 +01:00
Viktor Rådberg
f9d0346300 bump 2024-01-13 20:18:50 +01:00
Viktor Rådberg
2f3ee74c74 test 2024-01-13 20:15:43 +01:00
Viktor Rådberg
f8f0788b97 bump 2024-01-13 19:53:02 +01:00
Viktor Rådberg
bfe25eacb7 fix lint 2024-01-13 19:44:27 +01:00
Viktor Rådberg
7b0965c0dd test release 2024-01-13 19:40:36 +01:00
Viktor Rådberg
e55ea6a83a final test 2024-01-13 19:32:16 +01:00
Viktor Rådberg
481196de9b test 2024-01-13 19:30:42 +01:00
Viktor Rådberg
a136dbd3f9 test 2024-01-13 19:28:50 +01:00
Viktor Rådberg
8d23349dac test 2024-01-13 19:27:25 +01:00
Viktor Rådberg
a7caa46156 test 2024-01-13 19:22:31 +01:00
Viktor Rådberg
39cd3faae2 test 2024-01-13 19:19:30 +01:00
Viktor Rådberg
bdaa8e602f test 2024-01-13 19:18:45 +01:00
Viktor Rådberg
26490103a9 Merge pull request #30 from Vikeo/develop
test pr
2024-01-13 19:14:03 +01:00
Viktor Rådberg
56b07784d5 fix 2024-01-13 19:13:34 +01:00
Viktor Rådberg
4544c689a5 test 2024-01-13 19:12:11 +01:00
Viktor Rådberg
8a7a4b4127 test 2024-01-13 19:11:49 +01:00
Viktor Rådberg
391e654779 test 2024-01-13 18:58:07 +01:00
Viktor Rådberg
f79a0d3e7e test 2024-01-13 18:55:41 +01:00
Viktor Rådberg
0664e340a0 test 2024-01-13 18:49:06 +01:00
Viktor Rådberg
dcb98aeac6 test 2024-01-13 18:48:11 +01:00
Viktor Rådberg
89b62ddac4 release 2024-01-13 18:37:12 +01:00
Viktor Rådberg
c704e3c7f4 release workflow 2024-01-13 18:26:34 +01:00
Viktor Rådberg
69a71e2d6e version check 2024-01-13 18:17:24 +01:00
Viktor Rådberg
18945204bf test 2024-01-13 18:03:59 +01:00
Viktor Rådberg
495e731636 tet 2024-01-13 17:57:03 +01:00
Viktor Rådberg
67b231f0d4 more logs 2024-01-13 17:12:31 +01:00
Viktor Rådberg
9d42fb1635 check version 2024-01-13 17:07:03 +01:00
Viktor Rådberg
38ad046344 test 2024-01-13 15:39:56 +01:00
Viktor Rådberg
bc87f073af test 2024-01-13 15:35:42 +01:00
Viktor Rådberg
da46c25944 test version update 2024-01-13 15:34:24 +01:00
Viktor Rådberg
104f54f5b7 Merge pull request #29 from Vikeo/develop
Release 0.5.0
2024-01-13 15:13:52 +01:00
Viktor Rådberg
101a055694 bump version 2024-01-13 15:09:58 +01:00
Viktor Rådberg
38e4cb8e8c dvh 2024-01-13 15:02:33 +01:00
Viktor Rådberg
4ecb83060d fix webkit highlight 2024-01-13 14:55:39 +01:00
Viktor Rådberg
4f231ba6f4 finish up tailwind 2024-01-13 14:50:36 +01:00
Viktor Rådberg
3cd982c643 wip tailwind 2024-01-07 19:02:07 +01:00
Viktor Rådberg
1013914cdf commander damage tailwind 2024-01-07 00:15:37 +01:00
Viktor Rådberg
db85fc2102 fix some tailwind 2024-01-06 19:45:25 +01:00
Viktor Rådberg
2b0d8102d8 fix version 2024-01-06 01:14:30 +01:00
Viktor Rådberg
35e0224066 fix backdrop 2024-01-06 00:47:21 +01:00
Viktor Rådberg
1fa433a38f remove styled components global styles 2024-01-06 00:45:35 +01:00
Viktor Rådberg
26821273d7 Merge branch 'develop' 2023-12-27 21:39:59 +01:00
Viktor Rådberg
7f19214624 reload window 2023-12-27 21:39:50 +01:00
Viktor Rådberg
8b2cd43a96 Merge pull request #28 from Vikeo/develop
New release
2023-12-27 21:31:40 +01:00
Viktor Rådberg
23e18f8f41 fix styling 2023-12-27 21:29:28 +01:00
Viktor Rådberg
23b844c47e fix 2023-12-27 21:11:44 +01:00
Viktor Rådberg
6ade1998f6 fix 2023-12-27 21:08:43 +01:00
Viktor Rådberg
cc98a1b84a test 2023-12-27 21:05:27 +01:00
Viktor Rådberg
2ca6b91d09 test 2023-12-27 21:03:22 +01:00
Viktor Rådberg
00bda4fb68 test 2023-12-27 20:54:22 +01:00
Viktor Rådberg
d09d992535 test 2023-12-27 20:52:34 +01:00
Viktor Rådberg
e96e4f3aa9 test 2023-12-27 20:50:57 +01:00
Viktor Rådberg
cb132360a9 test 2023-12-27 20:48:29 +01:00
Viktor Rådberg
66b0892461 revert 2023-12-27 20:42:57 +01:00
Viktor Rådberg
fdab09d598 fix 2023-12-27 20:41:30 +01:00
Viktor Rådberg
ec030e7076 remove local storage when initial settings is not correct 2023-12-27 20:39:13 +01:00
Viktor Rådberg
9812c6737c add zod validation 2023-12-27 20:35:10 +01:00
Viktor Rådberg
e8528f46ae remove console logs 2023-12-27 19:57:16 +01:00
Viktor Rådberg
4ff7f67484 Merge pull request #27 from Vikeo/tailwind-wip
Tailwind wip
2023-12-27 19:56:38 +01:00
Viktor Rådberg
bc97e459cd Merge pull request #26 from Vikeo/develop
style fixes
2023-12-24 14:48:14 +01:00
Viktor Rådberg
866dca8e41 Merge pull request #25 from Vikeo/develop
Change start menu header from h2 to h1
2023-10-01 19:01:12 +02:00
Viktor Rådberg
5859bb5a49 Merge pull request #24 from Vikeo/develop
Release 0.4.0
2023-09-28 14:34:29 +02:00
Viktor Rådberg
20fb2153b3 Merge pull request #23 from Vikeo/develop
Just another small release
2023-09-26 17:31:25 +02:00
Viktor Rådberg
75038212c5 Merge pull request #22 from Vikeo/develop
Release 0.3.0
2023-09-22 14:39:31 +02:00
Viktor Rådberg
b712fb6e03 Merge pull request #21 from Vikeo/develop
add button to go back to start if render fails
2023-09-18 22:32:46 +02:00
Viktor Rådberg
3d27335fd0 Merge pull request #20 from Vikeo/develop
Add reset game button
2023-09-18 18:29:44 +02:00
Viktor Rådberg
18b53669d2 Merge pull request #19 from Vikeo/develop
New Release
2023-09-18 14:01:27 +02:00
Viktor Rådberg
28954eb948 Merge pull request #18 from Vikeo/develop
yarn instead of bun
2023-09-17 19:23:32 +02:00
Viktor Rådberg
3c59d5d05b Merge pull request #17 from Vikeo/develop
New release
2023-09-17 19:20:10 +02:00
Viktor Rådberg
22b58c74d6 Update start page text (#16)
* fix z index of settings and lose button

* update start page promt
2023-09-02 08:32:14 +02:00
38 changed files with 3210 additions and 1663 deletions

View File

@@ -1,8 +1,8 @@
robots.txt,1693082171694,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2 index.html,1705225256081,6ef0d7e2de82bf64addbb9294fb28845fd06daaa544b010a47422c12ae3ad97f
manifest.json,1693082171694,91ce94afb71f33a477f5d8d48c3f98bd7de422279c74f17b6500eec72003ac1a robots.txt,1705225255906,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
assets/index-5265c558.css,1693082171837,08c4451946bbdf520fe337edb365417a8bbf91914c018b83866723ef52d57b43 manifest.json,1705225255906,91ce94afb71f33a477f5d8d48c3f98bd7de422279c74f17b6500eec72003ac1a
index.html,1693082171837,09e1919fbaaa3a0bf08f43eb46c29136d62a7747b41f8b5d0f4a7ed23337c344 assets/index-08359bdb.css,1705225256081,d2766260d28230d960d75362810713efaddf40687205e697432b52869f162af7
logo192.png,1693082171693,4309255bccbdbb341b5ab88708677e3d43b9e171d2666528ff932295a8257e4e logo192.png,1705225255905,3b0fcf91fe2128f493de0bce2f6e2d35520a4260a04e05b8d855181359b3d3fe
favicon.ico,1693082171692,48d8c1b9714dbc9bcb012d9c9f04112d229f20e6c889bda588ac159f973e6a8d favicon.ico,1705225255905,75661e6187b524767554b4f28ec09a64bc72b0bb102a0b453aaead88519d9ed3
logo512.png,1693082171694,92c7c05dc98170596d04f48e5e60eaae9535f409bcaeff129fd98fef8aba9f4e logo512.png,1705225255906,cf49739c9e6890bbfcd4157f299dde425df60759b7320ae9188d7ab9dc51e8ca
assets/index-5023e89e.js,1693082171838,8a6177168e95e1ca90e5ad8774252a8a02a9a78765bd329b7deae729c01aedf3 assets/index-20658f4b.js,1705225256081,742f2c10740beea3a23f269aa6266b3c288d1fd9c7e20b6829034e8a898bf1e1

View File

@@ -1,20 +0,0 @@
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on merge
'on':
push:
branches:
- main
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
- run: bun install && bun run build && bun run lint
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_LIFE_TRINKET }}'
channelId: live
projectId: life-trinket

58
.github/workflows/firebase-release.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Deploy to Firebase Hosting
'on':
push:
tags:
- '*'
jobs:
build_and_deploy:
runs-on: ubuntu-latest
env:
REPO_READ_ACCESS_TOKEN: ${{ secrets.REPO_READ_ACCESS_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup bun
uses: oven-sh/setup-bun@v1
- name: Build, lint, and deploy
run: |
bun install
bun run build
bun run lint
- name: Deploy to Firebase Hosting
uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_LIFE_TRINKET }}'
channelId: live
projectId: life-trinket
release:
needs: build_and_deploy
runs-on: ubuntu-latest
env:
working-directory: ${{ github.workspace }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: get version
id: version
uses: notiz-dev/github-action-json-property@v0.2.0
with:
path: 'package.json'
prop_path: 'version'
- name: Create Release Note
id: create_release_note
run: echo "Release Note for version ${{ steps.version.outputs.prop }}" > release_note.txt
- name: Create Release
uses: ncipollo/release-action@v1.13.0
with:
bodyFile: release_note.txt
commit: ${{ github.sha }}
tag: '${{ steps.version.outputs.prop }}'
token: ${{ secrets.RELEASE_TOKEN }}

12
CHANGELOG.md Normal file
View File

@@ -0,0 +1,12 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.0] - 2024-01-13
### Changed
- Styling with Tailwind CSS instead of styled-components

BIN
bun.lockb

Binary file not shown.

1
env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare const APP_VERSION: string;

View File

@@ -1,7 +1,7 @@
{ {
"name": "life-trinket", "name": "life-trinket",
"private": true, "private": true,
"version": "0.4.0", "version": "0.6.0",
"type": "commonjs", "type": "commonjs",
"engines": { "engines": {
"node": ">=18", "node": ">=18",
@@ -13,7 +13,7 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview", "preview": "vite preview",
"generate-icons": "npx @svgr/cli src/Icons/svgs", "generate-icons": "npx @svgr/cli src/Icons/svgs",
"deploy": "bun build && firebase deploy --only hosting" "deploy": "bun run build && firebase deploy --only hosting"
}, },
"dependencies": { "dependencies": {
"@mui/material": "^5.13.6", "@mui/material": "^5.13.6",
@@ -22,7 +22,8 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-screen-wake-lock": "^3.0.2", "react-screen-wake-lock": "^3.0.2",
"styled-components": "^6.0.7" "react-twc": "^1.3.0",
"zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
@@ -42,8 +43,9 @@
"install": "^0.13.0", "install": "^0.13.0",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"prettier": "2.8.8", "prettier": "2.8.8",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.1",
"typescript": "^5.0.2", "typescript": "^5.3.3",
"vite": "^4.4.5" "vite": "^5.0.12",
"vite-plugin-pwa": "^0.17.4"
} }
} }

View File

@@ -1,24 +1,12 @@
import { createGlobalStyle } from 'styled-components';
import { ThemeProvider } from '@mui/material'; import { ThemeProvider } from '@mui/material';
import { LifeTrinket } from './Components/LifeTrinket'; import { LifeTrinket } from './Components/LifeTrinket';
import { theme } from './Data/theme'; import { theme } from './Data/theme';
import { GlobalSettingsProvider } from './Providers/GlobalSettingsProvider'; import { GlobalSettingsProvider } from './Providers/GlobalSettingsProvider';
import { PlayersProvider } from './Providers/PlayersProvider'; import { PlayersProvider } from './Providers/PlayersProvider';
const GlobalStyles = createGlobalStyle`
html,
body {
background-color: ${theme.palette.background.default};
}
#root {
touch-action: manipulation;
}
`;
const App = () => { const App = () => {
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<GlobalStyles />
<PlayersProvider> <PlayersProvider>
<GlobalSettingsProvider> <GlobalSettingsProvider>
<LifeTrinket /> <LifeTrinket />

View File

@@ -1,110 +1,49 @@
import styled from 'styled-components';
import { css } from 'styled-components';
import { Player, Rotation } from '../../Types/Player';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { OutlinedText } from '../Misc/OutlinedText'; import { TwcComponentProps, twc } from 'react-twc';
import { decrementTimeoutMs } from '../../Data/constants'; import { decrementTimeoutMs } from '../../Data/constants';
import { usePlayers } from '../../Hooks/usePlayers'; import { usePlayers } from '../../Hooks/usePlayers';
import { Player, Rotation } from '../../Types/Player';
import { OutlinedText } from '../Misc/OutlinedText';
const CommanderDamageContainer = styled.div<{ export type RotationDivProps = TwcComponentProps<'div'> & {
$rotation: number; $rotation?: number;
}>` };
display: flex;
flex-direction: row;
flex-grow: 1;
width: 100%;
${(props) => { export type RotationSpanProps = TwcComponentProps<'span'> & {
if ( $rotation?: number;
props.$rotation === Rotation.SideFlipped || };
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column;
`;
}
}}
`;
const CommanderDamageButton = styled.button<{ export type RotationButtonProps = TwcComponentProps<'button'> & {
$backgroundColor?: string; $rotation?: number;
$rotation: number; };
}>`
display: flex;
flex-grow: 1;
border: none;
height: 10vmin;
width: 50%;
outline: none;
cursor: pointer;
background-color: ${(props) => props.$backgroundColor || 'antiquewhite'};
margin: 0;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
padding: 0;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
width: 6vmax;
height: auto;
`;
}
}}
`;
const CommanderDamageTextContainer = styled.div<{ const CommanderDamageContainer = twc.div<RotationDivProps>((props) => [
$rotation: number; 'flex flex-grow',
}>` props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
position: relative; ? 'flex-col'
top: 50%; : 'flex-row',
left: 50%; ]);
transform: translate(-50%, -50%);
font-variant-numeric: tabular-nums;
pointer-events: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
${(props) => { const CommanderDamageButton = twc.button<RotationButtonProps>((props) => [
if ( 'flex flex-grow border-none outline-none cursor-pointer m-0 p-0 webkit-user-select-none',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
props.$rotation === Rotation.Side ? 'w-[6vmax] h-auto'
) { : 'h-[10vmin] w-1/2',
return css` ]);
rotate: 270deg;
`;
}
}}
`;
const PartnerDamageSeperator = styled.div<{ const CommanderDamageTextContainer = twc.div<RotationDivProps>((props) => [
$rotation: number; 'relative top-1/2 left-1/2 tabular-nums pointer-events-none select-none webkit-user-select-none',
}>` props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
width: 1px; ? 'rotate-[270deg]'
background-color: rgba(0, 0, 0, 1); : '',
]);
${(props) => { const PartnerDamageSeperator = twc.div<RotationDivProps>((props) => [
if ( 'bg-black',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
props.$rotation === Rotation.Side ? 'w-full h-px'
) { : 'w-px',
return css` ]);
width: auto;
height: 1px;
`;
}
}}
`;
type CommanderDamageButtonComponentProps = { type CommanderDamageButtonComponentProps = {
player: Player; player: Player;
@@ -214,8 +153,8 @@ export const CommanderDamage = ({
onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault(); e.preventDefault();
}} }}
$backgroundColor={opponent.color}
aria-label={`Commander damage. Player ${player.index}, opponent ${opponent.index}`} aria-label={`Commander damage. Player ${player.index}, opponent ${opponent.index}`}
style={{ background: opponent.color }}
> >
<CommanderDamageTextContainer $rotation={player.settings.rotation}> <CommanderDamageTextContainer $rotation={player.settings.rotation}>
<OutlinedText <OutlinedText
@@ -248,8 +187,8 @@ export const CommanderDamage = ({
) => { ) => {
e.preventDefault(); e.preventDefault();
}} }}
$backgroundColor={opponent.color}
aria-label={`Partner Commander damage. Player ${player.index}, opponent ${opponent.index}`} aria-label={`Partner Commander damage. Player ${player.index}, opponent ${opponent.index}`}
style={{ background: opponent.color }}
> >
<CommanderDamageTextContainer $rotation={player.settings.rotation}> <CommanderDamageTextContainer $rotation={player.settings.rotation}>
<OutlinedText <OutlinedText

View File

@@ -1,60 +1,45 @@
import { ReactNode, useRef, useState } from 'react'; import { ReactNode, useRef, useState } from 'react';
import styled from 'styled-components'; import { twc } from 'react-twc';
import { css } from 'styled-components';
import { decrementTimeoutMs } from '../../Data/constants'; import { decrementTimeoutMs } from '../../Data/constants';
import { CounterType, Rotation } from '../../Types/Player'; import { CounterType, Rotation } from '../../Types/Player';
import { OutlinedText } from '../Misc/OutlinedText'; import { OutlinedText } from '../Misc/OutlinedText';
import { RotationDivProps } from './CommanderDamage';
const ExtraCounterContainer = styled.div` const ExtraCounterContainer = twc.div`
display: flex; flex
justify-content: center; justify-center
align-items: center; items-center
pointer-events: all; pointer-events-all
flex-grow: 1; flex-grow
`; `;
export const StyledExtraCounterButton = styled.button` const ExtraCounterButton = twc.button`
display: flex; flex
justify-content: center; justify-center
align-items: center; items-center
position: relative; relative
flex-grow: 1; flex-grow
border: none; border-none
outline: none; outline-none
cursor: pointer; cursor-pointer
background-color: transparent; bg-transparent
user-select: none; select-none
-webkit-touch-callout: none; h-full
-webkit-tap-highlight-color: transparent; webkit-user-select-none
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
height: 100%;
`;
const IconContainer = styled.div<{
$rotation: number;
}>`
width: auto;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: -90deg;
`; `;
}
}}
`;
const TextContainer = styled.div` const IconContainer = twc.div<RotationDivProps>((props) => [
position: absolute; 'w-auto',
translate: -50%; props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
top: 50%; ? 'rotate-[-90deg]'
left: 50%; : '',
`; ]);
const TextContainer = twc.div`
absolute
top-1/2
left-1/2
`;
type ExtraCounterProps = { type ExtraCounterProps = {
Icon: ReactNode; Icon: ReactNode;
@@ -118,7 +103,7 @@ const ExtraCounter = ({
return ( return (
<ExtraCounterContainer> <ExtraCounterContainer>
<StyledExtraCounterButton <ExtraCounterButton
onPointerDown={handleDownInput} onPointerDown={handleDownInput}
onPointerUp={handleUpInput} onPointerUp={handleUpInput}
onPointerLeave={handleLeaveInput} onPointerLeave={handleLeaveInput}
@@ -139,7 +124,7 @@ const ExtraCounter = ({
</OutlinedText> </OutlinedText>
</TextContainer> </TextContainer>
</IconContainer> </IconContainer>
</StyledExtraCounterButton> </ExtraCounterButton>
</ExtraCounterContainer> </ExtraCounterContainer>
); );
}; };

View File

@@ -1,64 +1,42 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import styled from 'styled-components'; import { TwcComponentProps, twc } from 'react-twc';
import { css } from 'styled-components';
import { lifeLongPressMultiplier } from '../../Data/constants'; import { lifeLongPressMultiplier } from '../../Data/constants';
import { Rotation } from '../../Types/Player'; import { Rotation } from '../../Types/Player';
export const StyledLifeCounterButton = styled.button` type RotationButtonProps = TwcComponentProps<'div'> & {
width: 100%;
height: 100%;
color: rgba(0, 0, 0, 0.4);
font-weight: 600;
background-color: transparent;
border: none;
outline: none;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
`;
const TextContainer = styled.div<{
$align?: string; $align?: string;
$rotation: number; $rotation?: number;
}>` };
position: relative;
${(props) => { const LifeCounterButtonTwc = twc.button`
if ( h-full
props.$rotation === Rotation.SideFlipped || w-full
props.$rotation === Rotation.Side flex
) { text-lifeCounter-text
if (props.$align === 'right') { font-semibold
return css` bg-transparent
rotate: -90deg; border-none
bottom: 25%; outline-none
top: auto; cursor-pointer
justify-center
items-center
select-none
webkit-user-select-none
`; `;
}
return css`
rotate: -90deg;
top: 25%;
`;
}
if (props.$align === 'right') { const TextContainer = twc.div<RotationButtonProps>((props) => [
return css` 'relative',
left: 25%; props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
`; ? props.$align === 'right'
} ? '-rotate-90 bottom-1/4 top-auto'
return css` : '-rotate-90 top-1/4'
right: 25%; : 'top-auto',
`; props.$rotation === Rotation.Flipped || props.$rotation === Rotation.Normal
}} ? props.$align === 'right'
`; ? 'left-1/4'
: 'right-1/4'
: '',
]);
type LifeCounterButtonProps = { type LifeCounterButtonProps = {
lifeTotal: number; lifeTotal: number;
@@ -113,7 +91,7 @@ const LifeCounterButton = ({
: '12vmin'; : '12vmin';
return ( return (
<StyledLifeCounterButton <LifeCounterButtonTwc
onPointerDown={handleDownInput} onPointerDown={handleDownInput}
onPointerUp={handleUpInput} onPointerUp={handleUpInput}
onPointerLeave={handleLeaveInput} onPointerLeave={handleLeaveInput}
@@ -129,7 +107,7 @@ const LifeCounterButton = ({
> >
{operation === 'add' ? '\u002B' : '\u2212'} {operation === 'add' ? '\u002B' : '\u2212'}
</TextContainer> </TextContainer>
</StyledLifeCounterButton> </LifeCounterButtonTwc>
); );
}; };

View File

@@ -1,43 +1,17 @@
import styled, { css } from 'styled-components'; import { twc } from 'react-twc';
import { Skull } from '../../Icons/generated'; import { Skull } from '../../Icons/generated';
import { Rotation } from '../../Types/Player'; import { Rotation } from '../../Types/Player';
import { RotationDivProps } from './CommanderDamage';
export const LoseButton = styled.button<{ $rotation: Rotation }>` const LoseButton = twc.div<RotationDivProps>((props) => [
position: absolute; 'absolute flex-grow border-none outline-none cursor-pointer bg-interface-loseButton-background rounded-lg select-none z-[1] webkit-user-select-none',
flex-grow: 1;
border: none;
outline: none;
cursor: pointer;
top: 25%;
right: 15%;
background-color: #43434380;
border-radius: 8px;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
user-select: none;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
z-index: 1;
${(props) => { props.$rotation === Rotation.SideFlipped
if (props.$rotation === Rotation.SideFlipped) { ? `right-auto top-[15%] left-[27%]`
return css` : props.$rotation === Rotation.Side
right: auto; ? `right-auto top-[15%] left-[27%]`
top: 15%; : 'right-[15%] top-1/4',
left: 27%; ]);
rotate: ${props.$rotation}deg;
`;
} else if (props.$rotation === Rotation.Side) {
return css`
right: auto;
top: 15%;
left: 27%;
rotate: ${props.$rotation - 180}deg;
`;
}
}}
`;
type LoseButtonProps = { type LoseButtonProps = {
onClick: () => void; onClick: () => void;
@@ -45,8 +19,20 @@ type LoseButtonProps = {
}; };
export const LoseGameButton = ({ rotation, onClick }: LoseButtonProps) => { export const LoseGameButton = ({ rotation, onClick }: LoseButtonProps) => {
const calcRotation =
rotation === Rotation.SideFlipped
? rotation
: rotation === Rotation.Side
? rotation - 180
: rotation;
return ( return (
<LoseButton $rotation={rotation} onClick={onClick} aria-label={`Lose Game`}> <LoseButton
$rotation={rotation}
onClick={onClick}
aria-label={`Lose Game`}
style={{ rotate: `${calcRotation}deg` }}
>
<Skull size="5vmin" color="black" opacity={0.5} /> <Skull size="5vmin" color="black" opacity={0.5} />
</LoseButton> </LoseButton>
); );

View File

@@ -1,37 +1,14 @@
import styled from 'styled-components'; import { twc } from 'react-twc';
import { css } from 'styled-components';
import { Rotation } from '../../Types/Player';
import { Cog } from '../../Icons/generated'; import { Cog } from '../../Icons/generated';
import { Rotation } from '../../Types/Player';
import { RotationButtonProps } from './CommanderDamage';
export const StyledSettingsButton = styled.button<{ $rotation: Rotation }>` const SettingsButtonTwc = twc.button<RotationButtonProps>((props) => [
position: absolute; 'absolute flex-grow border-none outline-none cursor-pointer bg-transparent z-[1] select-none webkit-user-select-none',
flex-grow: 1; props.$rotation === Rotation.Side || props.$rotation === Rotation.SideFlipped
border: none; ? `right-auto top-[1vmax] left-[27%]`
outline: none; : 'top-1/4 right-[1vmax]',
cursor: pointer; ]);
top: 25%;
right: 1vmax;
background-color: transparent;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
z-index: 1;
${(props) => {
if (
props.$rotation === Rotation.Side ||
props.$rotation === Rotation.SideFlipped
) {
return css`
right: auto;
top: 1vmax;
left: 27%;
`;
}
}}
`;
type SettingsButtonProps = { type SettingsButtonProps = {
onClick: () => void; onClick: () => void;
@@ -40,13 +17,13 @@ type SettingsButtonProps = {
const SettingsButton = ({ onClick, rotation }: SettingsButtonProps) => { const SettingsButton = ({ onClick, rotation }: SettingsButtonProps) => {
return ( return (
<StyledSettingsButton <SettingsButtonTwc
onClick={onClick} onClick={onClick}
$rotation={rotation} $rotation={rotation}
aria-label={`Settings`} aria-label={`Settings`}
> >
<Cog size="5vmin" color="black" opacity="0.3" /> <Cog size="5vmin" color="black" opacity="0.3" />
</StyledSettingsButton> </SettingsButtonTwc>
); );
}; };

View File

@@ -1,27 +1,13 @@
import { twc } from 'react-twc';
import { Player, Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import styled from 'styled-components'; import { CommanderDamage, RotationDivProps } from '../Buttons/CommanderDamage';
import { css } from 'styled-components';
import { CommanderDamage } from '../Buttons/CommanderDamage';
const CommanderDamageGrid = styled.div<{ $rotation: number }>` const CommanderDamageGrid = twc.div<RotationDivProps>((props) => [
display: flex; 'flex flex-grow',
flex-direction: row; props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
flex-grow: 1; ? 'flex-col h-full w-auto'
width: 100%; : 'flex-row w-full',
]);
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column;
height: 100%;
width: auto;
`;
}
}}
`;
type CommanderDamageBarProps = { type CommanderDamageBarProps = {
opponents: Player[]; opponents: Player[];

View File

@@ -1,59 +0,0 @@
import styled from 'styled-components';
import { usePlayers } from '../../Hooks/usePlayers';
import LifeCounter from '../LifeCounter/LifeCounter';
export const CountersWrapper = styled.div`
width: 100%;
height: 100%;
background-color: black;
`;
export const CountersGrid = styled.div<{ $gridTemplateAreas: string }>`
display: grid;
gap: 4px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
height: 100%;
grid-template-areas: ${({ $gridTemplateAreas }) => $gridTemplateAreas};
`;
export const GridItemContainer = styled.div<{
$gridArea: string;
}>`
display: flex;
justify-content: center;
align-items: center;
grid-area: ${(props) => props.$gridArea};
`;
type CountersProps = {
gridAreas: string;
};
const Counters = ({ gridAreas }: CountersProps) => {
const { players } = usePlayers();
return (
<CountersWrapper>
<CountersGrid $gridTemplateAreas={gridAreas}>
{players.map((player) => {
return (
<GridItemContainer
key={player.index}
$gridArea={`player${player.index}`}
>
<LifeCounter
player={player}
opponents={players.filter(
(opponent) => opponent.index !== player.index
)}
/>
</GridItemContainer>
);
})}
</CountersGrid>
</CountersWrapper>
);
};
export default Counters;

View File

@@ -1,8 +1,5 @@
import { CounterType, Player } from '../../Types/Player'; import { twc } from 'react-twc';
import ExtraCounter from '../Buttons/ExtraCounter'; import { usePlayers } from '../../Hooks/usePlayers';
import styled from 'styled-components';
import { css } from 'styled-components';
import { Rotation } from '../../Types/Player';
import { import {
CommanderTax, CommanderTax,
Energy, Energy,
@@ -10,49 +7,23 @@ import {
PartnerTax, PartnerTax,
Poison, Poison,
} from '../../Icons/generated'; } from '../../Icons/generated';
import { usePlayers } from '../../Hooks/usePlayers'; import { CounterType, Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
import ExtraCounter from '../Buttons/ExtraCounter';
const Container = styled.div<{ $rotation: Rotation }>` const Container = twc.div<RotationDivProps>((props) => [
width: 100%; 'flex',
height: 20vmin; props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
display: flex; ? 'h-full w-[8vmax]'
: 'h-[20vmin] w-full',
]);
${(props) => { export const ExtraCountersGrid = twc.div<RotationDivProps>((props) => [
if ( 'flex absolute flex-row flex-grow pointer-events-none',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
props.$rotation === Rotation.Side ? 'flex-col-reverse h-full w-auto bottom-auto'
) { : 'w-full bottom-0',
return css` ]);
height: 100%;
width: 9.3vmax;
`;
}
}}
`;
export const ExtraCountersGrid = styled.div<{ $rotation: Rotation }>`
display: flex;
position: absolute;
width: 100%;
flex-direction: row;
flex-grow: 1;
bottom: 0;
pointer-events: none;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: column-reverse;
height: 100%;
width: auto;
bottom: auto;
`;
}
}}
`;
type ExtraCountersBarProps = { type ExtraCountersBarProps = {
player: Player; player: Player;

View File

@@ -1,115 +1,43 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import styled, { css, keyframes } from 'styled-components'; import { twc } from 'react-twc';
import { Player, Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import {
RotationDivProps,
RotationSpanProps,
} from '../Buttons/CommanderDamage';
import LifeCounterButton from '../Buttons/LifeCounterButton'; import LifeCounterButton from '../Buttons/LifeCounterButton';
import { OutlinedText } from '../Misc/OutlinedText'; import { OutlinedText } from '../Misc/OutlinedText';
const LifeCountainer = styled.div<{ const LifeCountainer = twc.div<RotationDivProps>((props) => [
$rotation: Rotation; 'flex flex-grow relative w-full h-full justify-between items-center',
}>` props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
position: relative; ? 'flex-col-reverse'
display: flex; : 'flex-row',
flex-direction: row; ]);
flex-grow: 1;
width: 100%;
height: 100%;
justify-content: space-between;
align-items: center;
${(props) => { const LifeCounterTextContainer = twc.div<RotationDivProps>((props) => [
if ( 'absolute m-0 p-0 pointer-events-none select-none webkit-user-select-none',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
props.$rotation === Rotation.Side ? 'w-full h-2/3'
) { : 'w-2/3 h-full',
return css` ]);
flex-direction: column-reverse;
`; const TextWrapper = twc.div`
} flex
}} absolute
justify-center
items-center
w-full
h-full
z-[-1]
`; `;
const LifeCounterTextContainer = styled.div<{ const RecentDifference = twc.div<RotationSpanProps>((props) => [
$rotation: Rotation; 'absolute min-w-[20vmin] drop-shadow-none text-center bg-interface-recentDifference-background tabular-nums rounded-full p-[6px 12px] text-[8vmin] text-interface-recentDifference-text animate-fadeOut',
}>` props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
position: absolute; ? 'top-1/3 translate-x-1/4 translate-y-1/2 rotate-[270deg]'
width: 60%; : 'top-1/4 left-[50%] -translate-x-1/2',
height: 100%; ]);
margin: 0;
padding: 0;
pointer-events: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
user-select: none;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
width: 100%;
height: 60%;
`;
}
}}
`;
const TextWrapper = styled.div`
display: flex;
position: absolute;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
z-index: -1;
`;
const fadeOut = keyframes`
0% {
opacity: 1;
}
33% {
opacity: 0.6;
}
100% {
opacity: 0;
}
`;
export const RecentDifference = styled.span<{ $rotation: Rotation }>`
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 15vmin;
text-shadow: none;
text-align: center;
background-color: rgba(255, 255, 255, 0.6);
font-variant-numeric: tabular-nums;
border-radius: 10vmin;
padding: 5px 10px;
font-size: 8vmin;
color: #333333;
animation: ${fadeOut} 3s 1s ease-out forwards;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
top: 27%;
left: 30%;
transform: translate(-50%, -50%);
rotate: 270deg;
`;
}
}}
`;
type HealthProps = { type HealthProps = {
player: Player; player: Player;
@@ -151,7 +79,6 @@ const Health = ({
const textContainer = textContainerRef.current; const textContainer = textContainerRef.current;
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
const calcFontSize = calculateFontSize(textContainer); const calcFontSize = calculateFontSize(textContainer);
console.log(calcFontSize);
setFontSize(calcFontSize); setFontSize(calcFontSize);
}); });

View File

@@ -1,137 +1,36 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import styled, { css, keyframes } from 'styled-components'; import { twc } from 'react-twc';
import { theme } from '../../Data/theme'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { Player, Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
import { LoseGameButton } from '../Buttons/LoseButton'; import { LoseGameButton } from '../Buttons/LoseButton';
import SettingsButton from '../Buttons/SettingsButton'; import SettingsButton from '../Buttons/SettingsButton';
import CommanderDamageBar from '../Counters/CommanderDamageBar'; import CommanderDamageBar from '../Counters/CommanderDamageBar';
import ExtraCountersBar from '../Counters/ExtraCountersBar'; import ExtraCountersBar from '../Counters/ExtraCountersBar';
import PlayerMenu from '../Player/PlayerMenu'; import PlayerMenu from '../Player/PlayerMenu';
import Health from './Health'; import Health from './Health';
import { usePlayers } from '../../Hooks/usePlayers';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
const LifeCounterContentWrapper = styled.div<{ const LifeCounterContentWrapper = twc.div`
$backgroundColor: string; relative flex flex-grow flex-col items-center w-full h-full overflow-hidden`;
}>`
position: relative;
display: flex;
flex-grow: 1;
flex-direction: column;
align-items: center;
height: 100%;
width: 100%;
background-color: ${(props) => props.$backgroundColor || 'antiquewhite'};
@media (orientation: landscape) {
max-width: 100vmax;
max-height: 100vmin;
}
overflow: hidden; const LifeCounterWrapper = twc.div<RotationDivProps>((props) => [
`; 'relative flex items-center w-full h-full z-[1]',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `flex-row`
: `flex-col`,
]);
const LifeCounterWrapper = styled.div<{ const StartingPlayerNoticeWrapper = twc.div`z-[1] flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none bg-primary-main`;
$rotation: Rotation;
}>`
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
z-index: 1; const PlayerLostWrapper = twc.div<RotationDivProps>((props) => [
'z-[1] flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none bg-lifeCounter-lostWrapper',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `rotate-[${props.$rotation - 90}deg]`
: '',
]);
${(props) => { const DynamicText = twc.div`text-[8vmin] whitespace-nowrap`;
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
flex-direction: row;
rotate: ${props.$rotation - 90}deg;
`;
}
return css`
flex-direction: column;
rotate: ${props.$rotation}deg;
`;
}}
`;
const PlayerNoticeWrapper = styled.div<{
$rotation: Rotation;
$backgroundColor: string;
}>`
z-index: 1;
display: flex;
position: absolute;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
background: ${(props) => props.$backgroundColor};
pointer-events: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
user-select: none;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: ${props.$rotation - 90}deg;
`;
}
}}
`;
const DynamicText = styled.div<{ $rotation: Rotation }>`
font-size: 8vmin;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: ${props.$rotation - 180}deg;
`;
}
}}
`;
const fadeOut = keyframes`
0% {
opacity: 1;
}
33% {
opacity: 0.6;
}
100% {
opacity: 0;
}
`;
export const RecentDifference = styled.span`
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
text-shadow: none;
background-color: rgba(255, 255, 255, 0.6);
font-variant-numeric: tabular-nums;
border-radius: 50%;
padding: 5px 10px;
font-size: 8vmin;
color: #333333;
animation: ${fadeOut} 3s 1s ease-out forwards;
`;
const hasCommanderDamageReached21 = (player: Player) => { const hasCommanderDamageReached21 = (player: Player) => {
const commanderDamageTotals = player.commanderDamage.map( const commanderDamageTotals = player.commanderDamage.map(
@@ -208,27 +107,42 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
updatePlayer(updatedPlayer); updatePlayer(updatedPlayer);
}; };
const calcRotation =
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? player.settings.rotation - 90
: player.settings.rotation;
const calcTextRotation =
player.settings.rotation === Rotation.SideFlipped ||
player.settings.rotation === Rotation.Side
? player.settings.rotation - 180
: player.settings.rotation;
return ( return (
<LifeCounterContentWrapper $backgroundColor={player.color}> <LifeCounterContentWrapper style={{ background: player.color }}>
<LifeCounterWrapper $rotation={player.settings.rotation}> <LifeCounterWrapper
$rotation={player.settings.rotation}
style={{ rotate: `${calcRotation}deg` }}
>
{settings.showStartingPlayer && {settings.showStartingPlayer &&
player.isStartingPlayer && player.isStartingPlayer &&
player.showStartingPlayer && ( player.showStartingPlayer && (
<PlayerNoticeWrapper <StartingPlayerNoticeWrapper
$rotation={player.settings.rotation} style={{ rotate: `${calcRotation}deg` }}
$backgroundColor={theme.palette.primary.main} >
<DynamicText
style={{
rotate: `${calcTextRotation}deg`,
}}
> >
<DynamicText $rotation={player.settings.rotation}>
You start! You start!
</DynamicText> </DynamicText>
</PlayerNoticeWrapper> </StartingPlayerNoticeWrapper>
)} )}
{player.hasLost && ( {player.hasLost && (
<PlayerNoticeWrapper <PlayerLostWrapper $rotation={player.settings.rotation} />
$rotation={player.settings.rotation}
$backgroundColor={'#00000070'}
/>
)} )}
<CommanderDamageBar <CommanderDamageBar
opponents={opponents} opponents={opponents}
@@ -256,11 +170,10 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
handleLifeChange={handleLifeChange} handleLifeChange={handleLifeChange}
/> />
<ExtraCountersBar player={player} /> <ExtraCountersBar player={player} />
</LifeCounterWrapper>
{showPlayerMenu && ( {showPlayerMenu && (
<PlayerMenu player={player} setShowPlayerMenu={setShowPlayerMenu} /> <PlayerMenu player={player} setShowPlayerMenu={setShowPlayerMenu} />
)} )}
</LifeCounterWrapper>
</LifeCounterContentWrapper> </LifeCounterContentWrapper>
); );
}; };

View File

@@ -1,47 +1,37 @@
import styled from 'styled-components'; import { twc } from 'react-twc';
import { useGlobalSettings } from '../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../Hooks/useGlobalSettings';
import StartMenu from './Views/StartMenu/StartMenu';
import { Play } from './Views/Play'; import { Play } from './Views/Play';
import StartMenu from './Views/StartMenu/StartMenu';
const StartWrapper = styled.div` const StartWrapper = twc.div`max-w-fit max-h-fit`;
max-width: fit-content;
max-height: fit-content;
`;
const PlayWrapper = styled.div` const PlayWrapper = twc.div`relative z-0 max-w-fit max-h-fit portrait:rotate-90`;
position: relative;
z-index: 0;
max-width: fit-content;
max-height: fit-content;
@media (orientation: portrait) {
rotate: 90deg;
}
`;
const EmergencyResetButton = styled.button` const EmergencyResetButton = () => {
width: 100vmax; const { goToStart } = useGlobalSettings();
height: 100vmin;
font-size: 4vmax; const EmergencyResetButton = twc.button`w-[100dvmax] h-[100dvmin] absolute top-0 z-[-1] bg-background-default`;
position: absolute; const Paragraph = twc.p`text-[4vmax] text-text-secondary`;
top: 0;
z-index: -1; return (
background-color: #4e6815; <EmergencyResetButton onClick={goToStart}>
`; <Paragraph>If you can see this, something is wrong.</Paragraph>
<Paragraph>Press screen to go to start.</Paragraph>
<br />
<Paragraph>If the issue persists, please inform me.</Paragraph>
</EmergencyResetButton>
);
};
export const LifeTrinket = () => { export const LifeTrinket = () => {
const { showPlay, goToStart, initialGameSettings } = useGlobalSettings(); const { showPlay, initialGameSettings } = useGlobalSettings();
return ( return (
<> <>
{showPlay && initialGameSettings ? ( {showPlay && initialGameSettings ? (
<PlayWrapper> <PlayWrapper>
<Play /> <Play />
<EmergencyResetButton onClick={goToStart}> <EmergencyResetButton />
<p>If you can see this, something is wrong.</p>
<p>Press screen to go to start.</p>
<br />
<p>If the issue persists, please inform me.</p>
</EmergencyResetButton>
</PlayWrapper> </PlayWrapper>
) : ( ) : (
<StartWrapper> <StartWrapper>

View File

@@ -1,21 +1,9 @@
import { Modal } from '@mui/material'; import { Modal } from '@mui/material';
import { theme } from '../../Data/theme'; import { twc } from 'react-twc';
import styled from 'styled-components'; import { Separator } from './Separator';
import { Paragraph } from './TextComponents';
export const ModalWrapper = styled.div` export const ModalWrapper = twc.div`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[85vh] bg-background-default p-4 overflow-scroll rounded-2xl border-none text-text-primary w-[95vw] max-w-[548px]`;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80vw;
height: 85vh;
background-color: ${theme.palette.background.default};
padding: 1rem;
overflow: scroll;
border-radius: 1rem;
color: ${theme.palette.text.primary};
border: none;
`;
type InfoModalProps = { type InfoModalProps = {
isOpen: boolean; isOpen: boolean;
@@ -24,29 +12,44 @@ type InfoModalProps = {
export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => { export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
return ( return (
<Modal open={isOpen} onClose={closeModal}> <Modal
open={isOpen}
onClose={closeModal}
style={{ display: 'flex', justifyContent: 'center' }}
>
<>
<div className="flex relative w-full max-w-[548px]">
<button
onClick={closeModal}
className="flex absolute top-10 right-0 z-10 w-10 h-10 text-common-white bg-primary-main items-center justify-center rounded-full border-solid border-primary-dark border-2"
>
X
</button>
</div>
<ModalWrapper> <ModalWrapper>
<div> <div>
<h2 style={{ textAlign: 'center' }}>📋 Usage Guide</h2> <h2 className="text-2xl text-center mb-4">📋 Usage Guide</h2>
<p> <Separator height="1px" />
<Paragraph className="my-4">
There are some controls that you might not know about, so here's a There are some controls that you might not know about, so here's a
short list of them. short list of them.
</p> </Paragraph>
<h3 className="text-lg font-bold mb-2">Life counter</h3>
<h3>Life counter</h3> <ul className="list-disc ml-6 mb-4">
<ul>
<li> <li>
<strong>Tap</strong> on a player's + or - button to add or <strong>Tap</strong> on a player's + or - button to add or
subtract <strong>1 life</strong>. subtract <strong>1 life</strong>.
</li> </li>
<li> <li>
<strong>Long press</strong> on a player's + or - button to add or <strong>Long press</strong> on a player's + or - button to add
subtract <strong>10 life</strong>. or subtract <strong>10 life</strong>.
</li> </li>
</ul> </ul>
<h3>Commander damage and other counters</h3> <h3 className="text-lg font-bold mb-2">
<ul> Commander damage and other counters
</h3>
<ul className="list-disc ml-6 mb-4">
<li> <li>
<strong>Tap</strong> on the counter to add{' '} <strong>Tap</strong> on the counter to add{' '}
<strong>1 counter</strong>. <strong>1 counter</strong>.
@@ -57,33 +60,21 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
</li> </li>
</ul> </ul>
<h3>Other</h3> <h3 className="text-lg font-bold mb-2">Other</h3>
<p> <Paragraph className="mb-4">
When a player is <strong>at or below 0 life</strong>, has taken{' '} When a player is <strong>at or below 0 life</strong>, has taken{' '}
<strong>21 or more Commander Damage</strong> or has{' '} <strong>21 or more Commander Damage</strong> or has{' '}
<strong>10 or more poison counters</strong>, a button with a skull <strong>10 or more poison counters</strong>, a button with a skull
will appear on that player's card. will appear on that player's card. Tapping it will dim the
</p> player's card.
<p> </Paragraph>
Tap on the button to mark that player as lost, dimming their player
card.
</p>
</div> </div>
<br /> <div className="text-center mt-4">
<div
style={{
textAlign: 'center',
marginTop: '1rem',
}}
>
Visit my Visit my
<a <a
href="https://github.com/Vikeo/LifeTrinket" href="https://github.com/Vikeo/LifeTrinket"
target="_blank" target="_blank"
style={{ className="text-text-secondary underline"
textDecoration: 'none',
color: theme.palette.primary.light,
}}
> >
{' '} {' '}
GitHub{' '} GitHub{' '}
@@ -91,6 +82,7 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
for more info about this web app. for more info about this web app.
</div> </div>
</ModalWrapper> </ModalWrapper>
</>
</Modal> </Modal>
); );
}; };

View File

@@ -1,58 +1,30 @@
import styled, { css } from 'styled-components';
import { theme } from '../../Data/theme';
import { Rotation } from '../../Types/Player'; import { Rotation } from '../../Types/Player';
const Container = styled.div` import { twc } from 'react-twc';
display: flex; //TODO Create provider for this
position: relative; import tailwindConfig from './../../../tailwind.config';
width: 100%; import resolveConfig from 'tailwindcss/resolveConfig';
height: 100%;
align-items: center;
justify-content: center;
`;
const CenteredText = styled.div<{ const fullConfig = resolveConfig(tailwindConfig);
strokeWidth?: string;
strokeColor?: string;
fillColor?: string;
fontSize?: string;
fontWeight?: string;
$rotation?: Rotation;
}>`
position: absolute;
font-weight: ${(props) => props.fontWeight || ''};
font-variant-numeric: tabular-nums;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
color: ${(props) => props.fillColor || theme.palette.common.black}; const Container = twc.div`
font-size: ${(props) => props.fontSize || '6vmin'}; flex
-webkit-text-stroke: ${(props) => props.strokeWidth || '1vmin'}${(props) => props.strokeColor || theme.palette.common.white}; relative
-webkit-text-fill-color: ${(props) => w-full
props.fillColor || theme.palette.common.black}; h-full
items-center
${(props) => { justify-center
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
rotate: 270deg;
`; `;
}
}}
`;
const CenteredTextOutline = styled.span` const CenteredText = twc.div`absolute select-none text-common-black text-[6vmin] stroke-common-white
position: absolute; webkit-user-select-none tabular-nums`;
left: 0;
-webkit-text-stroke: 0; const CenteredTextOutline = twc.span`
pointer-events: none; absolute
`; left-0
stroke-none
pointer-events-none
`;
type OutlinedTextProps = { type OutlinedTextProps = {
children?: React.ReactNode; children?: React.ReactNode;
@@ -73,18 +45,33 @@ export const OutlinedText: React.FC<OutlinedTextProps> = ({
fillColor, fillColor,
rotation, rotation,
}) => { }) => {
const calcRotation =
rotation === Rotation.Side
? rotation - 180
: rotation === Rotation.SideFlipped
? rotation
: 0;
return ( return (
<Container> <Container>
<CenteredText <CenteredText
fontSize={fontSize} style={{
fontWeight={fontWeight} fontSize,
strokeWidth={strokeWidth} fontWeight,
strokeColor={strokeColor} strokeWidth: strokeWidth || '1vmin',
fillColor={fillColor} color: fillColor || fullConfig.theme.colors.common.black,
$rotation={rotation} WebkitTextStroke: `${strokeWidth || '1vmin'} ${
strokeColor || fullConfig.theme.colors.common.white
}`,
WebkitTextFillColor:
fillColor || fullConfig.theme.colors.common.black,
rotate: `${calcRotation}deg`,
}}
> >
{children} {children}
<CenteredTextOutline aria-hidden>{children}</CenteredTextOutline> <CenteredTextOutline aria-hidden style={{ WebkitTextStroke: 0 }}>
{children}
</CenteredTextOutline>
</CenteredText> </CenteredText>
</Container> </Container>
); );

View File

@@ -1,13 +1,3 @@
import styled from 'styled-components';
import { Spacer } from './Spacer';
const SeparatorContainer = styled.div<{ width?: string; height?: string }>`
width: ${(props) => props.width};
height: ${(props) => props.height};
background-color: #00000025;
border-radius: 50px;
`;
export const Separator = ({ export const Separator = ({
width = '100%', width = '100%',
height = '100%', height = '100%',
@@ -16,10 +6,9 @@ export const Separator = ({
height?: string; height?: string;
}) => { }) => {
return ( return (
<> <div
<Spacer height="0.5rem" /> className={`bg-common-white bg-opacity-30 rounded-full mt-2 mb-2`}
<SeparatorContainer width={width} height={height} /> style={{ width, height }}
<Spacer height="0.5rem" /> />
</>
); );
}; };

View File

@@ -1,38 +1,18 @@
import { Button, FormLabel, Modal, Switch } from '@mui/material'; import { Button, FormLabel, Modal, Switch } from '@mui/material';
import { ModalWrapper } from './InfoModal'; import { twc } from 'react-twc';
import styled from 'styled-components';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { theme } from '../../Data/theme'; import { ModalWrapper } from './InfoModal';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { Paragraph } from './TextComponents'; import { Paragraph } from './TextComponents';
import { useEffect, useState } from 'react';
const SettingContainer = styled.div` const SettingContainer = twc.div`w-full flex flex-col`;
width: 100%;
display: flex;
flex-direction: column;
`;
const ToggleContainer = styled.div` const ToggleContainer = twc.div`flex flex-row justify-between items-center`;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
`;
const Container = styled.div` const Container = twc.div`flex flex-col items-center w-full`;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
`;
const Description = styled.p` const Description = twc.p`mr-16 text-xs text-left text-text-secondary`;
margin-top: -0.25rem;
margin-right: 3.5rem;
font-size: 0.8rem;
text-align: left;
color: ${theme.palette.text.secondary};
`;
type SettingsModalProps = { type SettingsModalProps = {
isOpen: boolean; isOpen: boolean;
@@ -41,12 +21,65 @@ type SettingsModalProps = {
export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => { export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
const { settings, setSettings, isPWA } = useGlobalSettings(); const { settings, setSettings, isPWA } = useGlobalSettings();
const [isLatestVersion, setIsLatestVersion] = useState(false);
const [newVersion, setNewVersion] = useState<string | undefined>(undefined);
useEffect(() => {
if (!isOpen) {
return;
}
async function checkIfLatestVersion() {
try {
const result = await fetch(
'https://api.github.com/repos/Vikeo/LifeTrinket/releases/latest',
{
headers: {
/* @ts-expect-error is defined in vite.config.ts*/
Authorization: `Bearer ${REPO_READ_ACCESS_TOKEN}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
}
);
const data = await result.json();
if (!data.name) {
setNewVersion(undefined);
setIsLatestVersion(false);
return;
}
setNewVersion(data.name);
/* @ts-expect-error is defined in vite.config.ts*/
if (data.name === APP_VERSION) {
setIsLatestVersion(true);
return;
}
setIsLatestVersion(false);
} catch (error) {
console.error('error getting latest version string', error);
}
}
checkIfLatestVersion();
}, [isOpen]);
return ( return (
<Modal open={isOpen} onClose={closeModal}> <Modal open={isOpen} onClose={closeModal}>
<>
<div className="flex relative w-full max-w-[548px]">
<button
onClick={closeModal}
className="flex absolute top-10 right-0 z-10 w-10 h-10 text-common-white bg-primary-main items-center justify-center rounded-full border-solid border-primary-dark border-2"
>
X
</button>
</div>
<ModalWrapper> <ModalWrapper>
<Container> <Container>
<h2 style={{ textAlign: 'center' }}> Settings </h2> <h2 className="text-center text-2xl mb-2"> Settings </h2>
<Separator height="1px" />
<SettingContainer> <SettingContainer>
<ToggleContainer> <ToggleContainer>
<FormLabel>Show Start Player</FormLabel> <FormLabel>Show Start Player</FormLabel>
@@ -71,13 +104,16 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
<Switch <Switch
checked={settings.keepAwake} checked={settings.keepAwake}
onChange={() => { onChange={() => {
setSettings({ ...settings, keepAwake: !settings.keepAwake }); setSettings({
...settings,
keepAwake: !settings.keepAwake,
});
}} }}
/> />
</ToggleContainer> </ToggleContainer>
<Description> <Description>
Will prevent device from going to sleep while this app is open if Will prevent device from going to sleep while this app is open
this is enabled. if this is enabled.
</Description> </Description>
</SettingContainer> </SettingContainer>
<SettingContainer> <SettingContainer>
@@ -100,7 +136,7 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</SettingContainer> </SettingContainer>
{!isPWA && ( {!isPWA && (
<> <>
<Separator height="2px" /> <Separator height="1px" />
<SettingContainer> <SettingContainer>
<ToggleContainer> <ToggleContainer>
<Paragraph> <Paragraph>
@@ -110,24 +146,50 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
normal app! normal app!
</Paragraph> </Paragraph>
</ToggleContainer> </ToggleContainer>
<Description> <Description className="mt-1">
If you do, this app will work offline and the toolbar will be If you do, this app will work offline and the toolbar will
automatically hidden. be automatically hidden.
</Description> </Description>
</SettingContainer> </SettingContainer>
</> </>
)} )}
<Separator height="2px" /> <Separator height="1px" />
<SettingContainer> <SettingContainer>
<Paragraph>Version: 0.4.0</Paragraph> <Paragraph>
{/* @ts-expect-error is defined in vite.config.ts*/}
Current version: {APP_VERSION}{' '}
{isLatestVersion && (
<span className="text-sm text-text-secondary">(latest)</span>
)}
</Paragraph>
{!isLatestVersion && newVersion && (
<Paragraph className="text-text-secondary text-lg text-center">
New version ({newVersion}) is available!{' '}
</Paragraph>
)}
</SettingContainer> </SettingContainer>
<Separator height="2px" /> {!isLatestVersion && newVersion && (
<Button
variant="contained"
style={{ marginTop: '0.25rem', marginBottom: '0.25rem' }}
onClick={() => window?.location?.reload()}
>
<span>Update</span>
<span className="text-xs">&nbsp;(reload app)</span>
</Button>
)}
<Separator height="1px" />
<Button variant="contained" onClick={closeModal}> <Button
variant="contained"
onClick={closeModal}
style={{ marginTop: '0.25rem' }}
>
Save and Close Save and Close
</Button> </Button>
</Container> </Container>
</ModalWrapper> </ModalWrapper>
</>
</Modal> </Modal>
); );
}; };

View File

@@ -1,6 +0,0 @@
import styled from 'styled-components';
export const Spacer = styled.div<{ width?: string; height?: string }>`
width: ${(props) => props.width};
height: ${(props) => props.height};
`;

View File

@@ -1,43 +1,30 @@
import { Button, Drawer } from '@mui/material'; import { Button, Drawer } from '@mui/material';
import { useState } from 'react'; import { useState } from 'react';
import styled from 'styled-components';
import { theme } from '../../Data/theme';
import { BuyMeCoffee, KoFi } from '../../Icons/generated/Support'; import { BuyMeCoffee, KoFi } from '../../Icons/generated/Support';
import { Paragraph } from './TextComponents'; import { Paragraph } from './TextComponents';
import LittleGuy from '../../Icons/generated/LittleGuy'; import LittleGuy from '../../Icons/generated/LittleGuy';
import { useAnalytics } from '../../Hooks/useAnalytics'; import { useAnalytics } from '../../Hooks/useAnalytics';
import { twc } from 'react-twc';
// import { ButtonBase } from '@mui/material'; const SupportContainer = twc.div`flex flex-col items-center justify-center gap-4 mt-4 mb-4`;
const SupportContainer = styled.div` const SupportButton = twc.button`
display: flex; flex
flex-direction: column; flex-row
align-items: center; items-center
justify-content: center; justify-left
gap: 1rem; border-none
margin: 16px 0; cursor-pointer
`; bg-primary-main
rounded-md
const SupportButton = styled.button` w-10/12
display: flex; mx-4
flex-direction: row; px-4
align-items: center; py-2
justify-content: center; transition-colors duration-200 ease-in-out
border: none; shadow-[1px_2px_4px_0px_rgba(0,0,0,0.3)]
background-color: transparent; hover:bg-primary-dark
cursor: pointer; `;
padding: 0;
margin: 0;
background-color: ${theme.palette.primary.main};
border-radius: 4px;
margin: 0 1rem;
padding: 0 1rem;
transition: background-color 0.2s ease-in-out;
box-shadow: 1px 2px 4px 0px rgba(0, 0, 0, 0.3);
&:hover {
background-color: ${theme.palette.primary.dark};
}
`;
export const SupportMe = () => { export const SupportMe = () => {
const analytics = useAnalytics(); const analytics = useAnalytics();
@@ -87,13 +74,7 @@ export const SupportMe = () => {
<LittleGuy <LittleGuy
height={'4rem'} height={'4rem'}
width={'2.5rem'} width={'2.5rem'}
style={{ className="pointer-events-none absolute top-10 right-0 text-text-primary"
pointerEvents: 'none',
position: 'absolute',
top: '2.5rem',
right: '0',
color: theme.palette.text.primary,
}}
/> />
<Drawer <Drawer
@@ -104,22 +85,12 @@ export const SupportMe = () => {
> >
<SupportContainer> <SupportContainer>
<SupportButton onClick={handleOpenBuyMeCoffee}> <SupportButton onClick={handleOpenBuyMeCoffee}>
<BuyMeCoffee <BuyMeCoffee height="1.5rem" width="1.5rem" className="mr-2" />
height={'1.5rem'} <Paragraph className="text-xs">Buy him a tea</Paragraph>
width={'1.5rem'}
style={{ marginRight: '0.5rem' }}
/>
<Paragraph style={{ fontSize: '0.7rem' }}>Buy him a tea</Paragraph>
</SupportButton> </SupportButton>
<SupportButton onClick={handleOpenKoFi}> <SupportButton onClick={handleOpenKoFi}>
<KoFi <KoFi height="1.5rem" width="1.5rem" className="mr-2" />
height={'1.5rem'} <Paragraph className="text-xs">Buy him a ko-fi</Paragraph>
width={'1.5rem'}
style={{ marginRight: '0.5rem' }}
/>
<Paragraph style={{ fontSize: '0.7rem' }}>
Buy him a ko-fi
</Paragraph>
</SupportButton> </SupportButton>
</SupportContainer> </SupportContainer>
</Drawer> </Drawer>

View File

@@ -1,11 +1,6 @@
import styled from 'styled-components'; import { twc } from 'react-twc';
import { theme } from '../../Data/theme';
export const Paragraph = styled.p` export const Paragraph = twc.p`text-text-primary;`;
color: ${theme.palette.text.primary};
`;
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
export const H1 = styled.h1` export const H1 = twc.h1`text-text-primary;`;
color: ${theme.palette.text.primary};
`;

View File

@@ -1,5 +1,6 @@
import LifeCounter from '../LifeCounter/LifeCounter'; import LifeCounter from '../LifeCounter/LifeCounter';
import { Player as PlayerType } from '../../Types/Player'; import { Player as PlayerType } from '../../Types/Player';
import { twc } from 'react-twc';
const getGridArea = (player: PlayerType) => { const getGridArea = (player: PlayerType) => {
switch (player.index) { switch (player.index) {
@@ -20,9 +21,11 @@ const getGridArea = (player: PlayerType) => {
} }
}; };
const PlayerWrapper = twc.div`w-full h-full bg-black`;
export const Player = (players: PlayerType[], gridClasses: string) => { export const Player = (players: PlayerType[], gridClasses: string) => {
return ( return (
<div className="w-full h-full bg-black"> <PlayerWrapper>
<div className={`grid w-full h-full gap-1 box-border ${gridClasses} `}> <div className={`grid w-full h-full gap-1 box-border ${gridClasses} `}>
{players.map((player) => { {players.map((player) => {
const gridArea = getGridArea(player); const gridArea = getGridArea(player);
@@ -42,6 +45,6 @@ export const Player = (players: PlayerType[], gridClasses: string) => {
); );
})} })}
</div> </div>
</div> </PlayerWrapper>
); );
}; };

View File

@@ -1,168 +1,98 @@
import { Button, Checkbox } from '@mui/material'; import { Button, Checkbox } from '@mui/material';
import styled, { css } from 'styled-components'; import { useRef } from 'react';
import { Player, Rotation } from '../../Types/Player'; import { twc } from 'react-twc';
import { theme } from '../../Data/theme'; import { theme } from '../../Data/theme';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers'; import { usePlayers } from '../../Hooks/usePlayers';
import { useSafeRotate } from '../../Hooks/useSafeRotate';
import { import {
PartnerTax, Cross,
Poison,
Energy, Energy,
Experience,
Exit, Exit,
Experience,
FullscreenOff, FullscreenOff,
FullscreenOn, FullscreenOn,
Cross, PartnerTax,
Poison,
ResetGame, ResetGame,
} from '../../Icons/generated'; } from '../../Icons/generated';
import { useRef } from 'react'; import { Player, Rotation } from '../../Types/Player';
import { Spacer } from '../Misc/Spacer'; import {
import { useSafeRotate } from '../../Hooks/useSafeRotate'; RotationButtonProps,
RotationDivProps,
} from '../Buttons/CommanderDamage';
const SettingsContainer = styled.div<{ const CheckboxContainer = twc.div``;
$rotation: Rotation;
}>`
display: flex;
flex-direction: row;
flex-wrap: wrap;
height: 100%;
width: 100%;
${(props) => { const PlayerMenuWrapper = twc.div`
if ( flex
props.$rotation === Rotation.SideFlipped || flex-col
absolute
w-full
h-full
bg-background-settings
items-center
justify-center
z-[2]
webkit-user-select-none
`;
const BetterRowContainer = twc.div`
flex
flex-col
flex-grow
w-full
h-full
justify-end
items-stretch
`;
const TogglesSection = twc.div`
flex
relative
flex-row
gap-2
justify-evenly
`;
const ButtonsSections = twc.div`
flex
max-w-full
gap-4
justify-between
p-[3%]
items-center
`;
const ColorPicker = twc.input`
absolute
top-[5%]
left-[5%]
h-[8vmax]
w-[8vmax]
border-none
outline-none
cursor-pointer
bg-transparent
user-select-none
text-common-white
`;
const SettingsContainer = twc.div<RotationDivProps>((props) => [
'flex flex-wrap h-full w-full',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col'
: 'flex-row',
]);
const CloseButton = twc.button<RotationButtonProps>((props) => [
'absolute border-none outline-none cursor-pointer bg-transparent z-[99]',
props.$rotation === Rotation.Side props.$rotation === Rotation.Side
) { ? `top-[5%] right-auto left-[5%]`
return css` : props.$rotation === Rotation.SideFlipped
flex-direction: column-reverse; ? 'top-auto left-auto bottom-[5%] right-[5%]'
height: 100%; : 'top-[15%] right-[5%]',
width: 100%; ]);
`;
}
}}
${(props) => {
if (props.$rotation === Rotation.Side) {
return css`
rotate: ${props.$rotation - 180}deg;
`;
} else if (props.$rotation === Rotation.SideFlipped) {
return css`
rotate: ${props.$rotation - 180}deg;
`;
}
}}
`;
const BetterRowContainer = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
width: 100%;
height: 100%;
justify-content: end;
align-items: stretch;
`;
const TogglesSection = styled.div`
display: flex;
position: relative;
flex-direction: row;
gap: 0.5rem;
justify-content: space-evenly;
`;
const ButtonsSections = styled.div`
display: flex;
max-width: 100%;
gap: 1rem;
justify-content: space-between;
padding: 3% 3%;
align-items: center;
`;
const ColorPicker = styled.input`
position: absolute;
top: 5%;
left: 5%;
height: 8vmax;
width: 8vmax;
border: none;
outline: none;
cursor: pointer;
background-color: transparent;
user-select: none;
color: #ffffff;
`;
const CheckboxContainer = styled.div<{ $rotation: Rotation }>`
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return css`
/* rotate: ${props.$rotation - 180}deg; */
`;
}
}}
`;
const PlayerMenuWrapper = styled.div<{
$rotation: Rotation;
}>`
display: flex;
flex-direction: column;
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(20, 20, 20, 0.9);
align-items: center;
justify-content: center;
z-index: 2;
${(props) => {
if (
props.$rotation === Rotation.SideFlipped ||
props.$rotation === Rotation.Side
) {
return;
}
return css`
rotate: ${props.$rotation}deg;
`;
}};
`;
const CloseButton = styled.div<{
$rotation: Rotation;
}>`
position: absolute;
top: 15%;
right: 5%;
z-index: 9999;
border: none;
outline: none;
cursor: pointer;
background-color: transparent;
${(props) => {
if (props.$rotation === Rotation.Side) {
return css`
rotate: ${props.$rotation - 180}deg;
top: 5%;
right: auto;
left: 5%;
`;
} else if (props.$rotation === Rotation.SideFlipped) {
return css`
rotate: ${props.$rotation - 180}deg;
top: auto;
left: auto;
bottom: 5%;
right: 5%;
`;
}
}}
`;
type PlayerMenuProps = { type PlayerMenuProps = {
player: Player; player: Player;
@@ -214,9 +144,31 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
const extraCountersSize = isSide ? '8vmin' : '4vmax'; const extraCountersSize = isSide ? '8vmin' : '4vmax';
const closeButtonSize = isSide ? '6vmin' : '3vmax'; const closeButtonSize = isSide ? '6vmin' : '3vmax';
const calcRotation =
player.settings.rotation === Rotation.Side
? `${player.settings.rotation - 180}deg`
: player.settings.rotation === Rotation.SideFlipped
? `${player.settings.rotation - 180}deg`
: '';
return ( return (
<PlayerMenuWrapper $rotation={player.settings.rotation}> <PlayerMenuWrapper
<CloseButton $rotation={player.settings.rotation}> //TODO: Fix hacky solution to rotation for SideFlipped
style={{
rotate:
player.settings.rotation === Rotation.SideFlipped ? '180deg' : '',
}}
>
<CloseButton
$rotation={player.settings.rotation}
style={{
rotate:
player.settings.rotation === Rotation.Side ||
player.settings.rotation === Rotation.SideFlipped
? `${player.settings.rotation - 180}deg`
: '',
}}
>
<Button <Button
variant="text" variant="text"
onClick={handleOnClick} onClick={handleOnClick}
@@ -230,8 +182,12 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
<Cross size={closeButtonSize} /> <Cross size={closeButtonSize} />
</Button> </Button>
</CloseButton> </CloseButton>
<SettingsContainer <SettingsContainer
$rotation={player.settings.rotation} $rotation={player.settings.rotation}
style={{
rotate: calcRotation,
}}
ref={settingsContainerRef} ref={settingsContainerRef}
> >
<ColorPicker <ColorPicker
@@ -244,7 +200,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
<BetterRowContainer> <BetterRowContainer>
<TogglesSection> <TogglesSection>
{player.settings.useCommanderDamage && ( {player.settings.useCommanderDamage && (
<CheckboxContainer $rotation={player.settings.rotation}> <CheckboxContainer>
<Checkbox <Checkbox
name="usePartner" name="usePartner"
checked={player.settings.usePartner} checked={player.settings.usePartner}
@@ -272,7 +228,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
</CheckboxContainer> </CheckboxContainer>
)} )}
<CheckboxContainer $rotation={player.settings.rotation}> <CheckboxContainer>
<Checkbox <Checkbox
name="usePoison" name="usePoison"
checked={player.settings.usePoison} checked={player.settings.usePoison}
@@ -299,7 +255,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
/> />
</CheckboxContainer> </CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}> <CheckboxContainer>
<Checkbox <Checkbox
name="useEnergy" name="useEnergy"
checked={player.settings.useEnergy} checked={player.settings.useEnergy}
@@ -326,7 +282,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
/> />
</CheckboxContainer> </CheckboxContainer>
<CheckboxContainer $rotation={player.settings.rotation}> <CheckboxContainer>
<Checkbox <Checkbox
name="useExperience" name="useExperience"
checked={player.settings.useExperience} checked={player.settings.useExperience}
@@ -353,8 +309,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
/> />
</CheckboxContainer> </CheckboxContainer>
</TogglesSection> </TogglesSection>
<Spacer height="1rem" /> <ButtonsSections className="mt-4">
<ButtonsSections>
<Button <Button
variant="text" variant="text"
style={{ style={{
@@ -366,7 +321,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
> >
<Exit size={iconSize} style={{ rotate: '180deg' }} /> <Exit size={iconSize} style={{ rotate: '180deg' }} />
</Button> </Button>
<CheckboxContainer $rotation={player.settings.rotation}> <CheckboxContainer>
<Checkbox <Checkbox
name="fullscreen" name="fullscreen"
checked={document.fullscreenElement ? true : false} checked={document.fullscreenElement ? true : false}
@@ -418,18 +373,11 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
</BetterRowContainer> </BetterRowContainer>
<dialog <dialog
ref={dialogRef} ref={dialogRef}
style={{ className="z-[9999] min-h-2/4 bg-background-default text-text-primary rounded-2xl border-none absolute top-[10%]"
zIndex: 9999,
background: theme.palette.background.default,
color: theme.palette.text.primary,
borderRadius: '1rem',
border: 'none',
position: 'absolute',
top: '10%',
}}
> >
<h1>Reset Game?</h1> <div className="h-full flex flex-col p-4 gap-2">
<div style={{ display: 'flex', justifyContent: 'space-evenly' }}> <h1 className="text-center">Reset Game?</h1>
<div className="flex justify-evenly gap-4">
<Button <Button
variant="contained" variant="contained"
onClick={() => dialogRef.current?.close()} onClick={() => dialogRef.current?.close()}
@@ -446,6 +394,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
Yes Yes
</Button> </Button>
</div> </div>
</div>
</dialog> </dialog>
</SettingsContainer> </SettingsContainer>
</PlayerMenuWrapper> </PlayerMenuWrapper>

View File

@@ -1,16 +1,10 @@
import styled from 'styled-components';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers'; import { usePlayers } from '../../Hooks/usePlayers';
import { Orientation } from '../../Types/Settings'; import { Orientation } from '../../Types/Settings';
import { Player } from '../Player/Player'; import { Player } from '../Player/Player';
import { twc } from 'react-twc';
const MainWrapper = styled.div` const MainWrapper = twc.div`w-[100dvmax] h-[100dvmin] overflow-hidden`;
width: 100vmax;
height: 100vmin;
width: 100dvmax;
height: 100dvmin;
overflow: hidden;
`;
export const Play = () => { export const Play = () => {
const { players } = usePlayers(); const { players } = usePlayers();

View File

@@ -1,6 +1,5 @@
import { FormControlLabel, Radio, RadioGroup } from '@mui/material'; import { FormControlLabel, Radio, RadioGroup } from '@mui/material';
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import { theme } from '../../../Data/theme'; import { theme } from '../../../Data/theme';
import { import {
FivePlayers, FivePlayers,
@@ -15,14 +14,11 @@ import {
TwoPlayersSameSide, TwoPlayersSameSide,
} from '../../../Icons/generated/Layouts'; } from '../../../Icons/generated/Layouts';
import { twc } from 'react-twc';
import OnePlayerLandscape from '../../../Icons/generated/Layouts/OnePlayerLandscape'; import OnePlayerLandscape from '../../../Icons/generated/Layouts/OnePlayerLandscape';
import { Orientation } from '../../../Types/Settings'; import { Orientation } from '../../../Types/Settings';
const LayoutWrapper = styled.div` const LayoutWrapper = twc.div`flex flex-row justify-center items-center self-center w-full`;
flex-direction: row;
display: flex;
justify-content: space-evenly;
`;
type LayoutOptionsProps = { type LayoutOptionsProps = {
numberOfPlayers: number; numberOfPlayers: number;
@@ -35,14 +31,16 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
selectedOrientation, selectedOrientation,
onChange, onChange,
}) => { }) => {
const iconHeight = '30vmin'; const iconWidth = '21vmin';
const iconWidth = '20vmin'; const iconHeight = '40vmin';
const iconMaxWidth = '124px';
const iconMaxHeight = '196px';
const renderLayoutOptions = () => { const renderLayoutOptions = () => {
switch (numberOfPlayers) { switch (numberOfPlayers) {
case 1: case 1:
return ( return (
<> <div>
<FormControlLabel <FormControlLabel
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
@@ -62,6 +60,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
TouchRippleProps={{ style: { display: 'none' } }} TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/> />
} }
label="" label=""
@@ -85,11 +84,12 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
/> />
} }
TouchRippleProps={{ style: { display: 'none' } }} TouchRippleProps={{ style: { display: 'none' } }}
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
/> />
} }
label="" label=""
/> />
</> </div>
); );
case 2: case 2:
return ( return (
@@ -98,6 +98,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersSameSide <TwoPlayersSameSide
height={iconHeight} height={iconHeight}
@@ -121,6 +122,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Portrait} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersOppositePortrait <TwoPlayersOppositePortrait
height={iconHeight} height={iconHeight}
@@ -144,6 +146,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.OppositeLandscape} value={Orientation.OppositeLandscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<TwoPlayersOppositeLandscape <TwoPlayersOppositeLandscape
height={iconHeight} height={iconHeight}
@@ -172,6 +175,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<ThreePlayers <ThreePlayers
height={iconHeight} height={iconHeight}
@@ -195,6 +199,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Portrait} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<ThreePlayersSide <ThreePlayersSide
height={iconHeight} height={iconHeight}
@@ -224,6 +229,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FourPlayers <FourPlayers
height={iconHeight} height={iconHeight}
@@ -247,6 +253,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Portrait} value={Orientation.Portrait}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FourPlayersSide <FourPlayersSide
height={iconHeight} height={iconHeight}
@@ -276,6 +283,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<FivePlayers <FivePlayers
height={iconHeight} height={iconHeight}
@@ -328,6 +336,7 @@ export const LayoutOptions: React.FC<LayoutOptionsProps> = ({
value={Orientation.Landscape} value={Orientation.Landscape}
control={ control={
<Radio <Radio
style={{ maxWidth: iconMaxWidth, maxHeight: iconMaxHeight }}
icon={ icon={
<SixPlayers <SixPlayers
height={iconHeight} height={iconHeight}

View File

@@ -1,50 +1,32 @@
import { Button, FormControl, FormLabel, Switch } from '@mui/material'; import { Button, FormControl, FormLabel, Switch } from '@mui/material';
import Slider from '@mui/material/Slider'; import Slider from '@mui/material/Slider';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import styled from 'styled-components'; import { twc } from 'react-twc';
import { createInitialPlayers } from '../../../Data/getInitialPlayers'; import { createInitialPlayers } from '../../../Data/getInitialPlayers';
import { theme } from '../../../Data/theme'; import { theme } from '../../../Data/theme';
import { useAnalytics } from '../../../Hooks/useAnalytics'; import { useAnalytics } from '../../../Hooks/useAnalytics';
import { useGlobalSettings } from '../../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../../Hooks/useGlobalSettings';
import { usePlayers } from '../../../Hooks/usePlayers'; import { usePlayers } from '../../../Hooks/usePlayers';
import { Cog, Info } from '../../../Icons/generated'; import { Cog, Info } from '../../../Icons/generated';
import { InitialGameSettings, Orientation } from '../../../Types/Settings'; import {
GameFormat,
InitialGameSettings,
Orientation,
} from '../../../Types/Settings';
import { InfoModal } from '../../Misc/InfoModal'; import { InfoModal } from '../../Misc/InfoModal';
import { SettingsModal } from '../../Misc/SettingsModal'; import { SettingsModal } from '../../Misc/SettingsModal';
import { Spacer } from '../../Misc/Spacer';
import { SupportMe } from '../../Misc/SupportMe'; import { SupportMe } from '../../Misc/SupportMe';
import { H1, Paragraph } from '../../Misc/TextComponents';
import { LayoutOptions } from './LayoutOptions'; import { LayoutOptions } from './LayoutOptions';
const MainWrapper = styled.div` const MainWrapper = twc.div`w-[100dvw] h-fit pb-14 overflow-hidden items-center flex flex-col`;
width: 100dvw;
height: fit-content;
padding-bottom: 58px;
overflow: hidden;
align-items: center;
display: flex;
flex-direction: column;
`;
const StartButtonFooter = styled.div` const StartButtonFooter = twc.div`w-full max-w-[548px] fixed bottom-4 z-1 items-center flex flex-col px-4`;
position: fixed;
bottom: 1rem;
translate: -50%, -50%;
z-index: 1;
`;
const ToggleButtonsWrapper = styled.div` const SliderWrapper = twc.div`mx-8`;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`;
const ToggleContainer = styled.div` const ToggleButtonsWrapper = twc.div`flex flex-row justify-between items-center`;
display: flex;
flex-direction: column; const ToggleContainer = twc.div`flex flex-col items-center`;
align-items: center;
`;
const playerMarks = [ const playerMarks = [
{ {
@@ -118,7 +100,7 @@ const Start = () => {
startingLifeTotal: 40, startingLifeTotal: 40,
useCommanderDamage: true, useCommanderDamage: true,
orientation: Orientation.Portrait, orientation: Orientation.Portrait,
gameFormat: 'commander', gameFormat: GameFormat.Commander,
} }
); );
@@ -190,9 +172,14 @@ const Start = () => {
<SupportMe /> <SupportMe />
<H1>Life Trinket</H1> <h1 className="text-3xl block font-bold mt-6 mb-5 text-text-primary">
<FormControl focused={false} style={{ width: '80vw' }}> Life Trinket
</h1>
<div className="overflow-hidden items-center flex flex-col max-w-[548px] w-full mb-8 px-4">
<FormControl focused={false} style={{ width: '100%' }}>
<FormLabel>Number of Players</FormLabel> <FormLabel>Number of Players</FormLabel>
<SliderWrapper>
<Slider <Slider
title="Number of Players" title="Number of Players"
max={6} max={6}
@@ -210,8 +197,10 @@ const Start = () => {
}); });
}} }}
/> />
<Spacer height="0.7rem" /> </SliderWrapper>
<FormLabel>Starting Health</FormLabel>
<FormLabel className="mt-[0.7rem]">Starting Health</FormLabel>
<SliderWrapper>
<Slider <Slider
title="Starting Health" title="Starting Health"
max={60} max={60}
@@ -229,9 +218,9 @@ const Start = () => {
}) })
} }
/> />
<Spacer height="1rem" /> </SliderWrapper>
<ToggleButtonsWrapper> <ToggleButtonsWrapper className="mt-4">
<ToggleContainer> <ToggleContainer>
<FormLabel>Commander</FormLabel> <FormLabel>Commander</FormLabel>
<Switch <Switch
@@ -289,7 +278,6 @@ const Start = () => {
numberOfPlayers={playerOptions.numberOfPlayers} numberOfPlayers={playerOptions.numberOfPlayers}
selectedOrientation={playerOptions.orientation} selectedOrientation={playerOptions.orientation}
onChange={(orientation) => { onChange={(orientation) => {
console.log('orientation', { orientation });
setPlayerOptions({ setPlayerOptions({
...playerOptions, ...playerOptions,
orientation, orientation,
@@ -297,23 +285,21 @@ const Start = () => {
}} }}
/> />
</FormControl> </FormControl>
{!isPWA && ( {!isPWA && (
<Paragraph <p className="text-center text-xs text-text-primary w-11/12 mt-4">
style={{ textAlign: 'center', maxWidth: '75%', fontSize: '0.7rem' }}
>
If you're on iOS, this page works better if you{' '} If you're on iOS, this page works better if you{' '}
<strong>hide the toolbar</strong> or{' '} <strong>hide the toolbar</strong> or{' '}
<strong>add the app to your home screen</strong>. <strong>add the app to your home screen</strong>.
</Paragraph> </p>
)} )}
</div>
<StartButtonFooter> <StartButtonFooter>
<Button <Button
size="large" size="large"
variant="contained" variant="contained"
onClick={doStartGame} onClick={doStartGame}
style={{ width: '90dvw' }} fullWidth
> >
START GAME START GAME
</Button> </Button>

View File

@@ -1,34 +1,28 @@
import { createTheme } from '@mui/material'; import { createTheme } from '@mui/material';
import resolveConfig from 'tailwindcss/resolveConfig';
import tailwindConfig from '../../tailwind.config';
//TODO Create provider for this
const fullConfig = resolveConfig(tailwindConfig);
const { primary, secondary, background, text, action, common } =
fullConfig.theme.colors;
export const theme = createTheme({ export const theme = createTheme({
palette: { palette: {
primary: { primary,
main: '#7F9172', secondary,
}, background,
secondary: { text,
main: '#5E714C', action,
}, common,
background: {
default: '#495E35',
},
text: {
primary: '#F5F5F5',
secondary: '#b3b39b',
},
action: {
disabled: '#5E714C',
},
common: {
white: '#F9FFE3',
black: '#000000',
},
}, },
components: { components: {
MuiFormLabel: { MuiFormLabel: {
styleOverrides: { styleOverrides: {
root: { root: {
fontSize: '1rem', fontSize: '1rem',
color: '#F5F5F5', color: text.primary,
}, },
}, },
}, },
@@ -36,12 +30,12 @@ export const theme = createTheme({
styleOverrides: { styleOverrides: {
markLabel: { markLabel: {
fontSize: '1rem', fontSize: '1rem',
color: '#F5F5F5', color: text.primary,
}, },
valueLabel: { valueLabel: {
display: 'none', display: 'none',
color: '#F5F5F5', color: text.primary,
background: '#5E714C', background: secondary.main,
}, },
track: { track: {
height: '0.7rem', height: '0.7rem',
@@ -77,7 +71,7 @@ export const theme = createTheme({
styleOverrides: { styleOverrides: {
paper: { paper: {
top: '1rem', top: '1rem',
background: '#495E35', background: background.default,
height: 'auto', height: 'auto',
borderRadius: '8px', borderRadius: '8px',
}, },
@@ -86,7 +80,7 @@ export const theme = createTheme({
MuiBackdrop: { MuiBackdrop: {
styleOverrides: { styleOverrides: {
root: { root: {
backgroundColor: 'rgba(0, 0, 0, 0.3)', backgroundColor: background.backdrop,
}, },
}, },
}, },
@@ -100,7 +94,7 @@ export const theme = createTheme({
MuiSwitch: { MuiSwitch: {
styleOverrides: { styleOverrides: {
colorPrimary: { colorPrimary: {
color: '#5E714C', color: action.disabled,
}, },
}, },
}, },

View File

@@ -5,7 +5,11 @@ import {
GlobalSettingsContextType, GlobalSettingsContextType,
} from '../Contexts/GlobalSettingsContext'; } from '../Contexts/GlobalSettingsContext';
import { useAnalytics } from '../Hooks/useAnalytics'; import { useAnalytics } from '../Hooks/useAnalytics';
import { InitialGameSettings, Orientation, Settings } from '../Types/Settings'; import {
InitialGameSettings,
InitialGameSettingsSchema,
Settings,
} from '../Types/Settings';
export const GlobalSettingsProvider = ({ export const GlobalSettingsProvider = ({
children, children,
@@ -22,34 +26,47 @@ export const GlobalSettingsProvider = ({
savedShowPlay ? savedShowPlay === 'true' : false savedShowPlay ? savedShowPlay === 'true' : false
); );
const [initialGameSettings, setInitialSettings] = const [initialGameSettings, setInitialGameSettings] =
useState<InitialGameSettings | null>( useState<InitialGameSettings | null>(
savedGameSettings ? JSON.parse(savedGameSettings) : null savedGameSettings ? JSON.parse(savedGameSettings) : null
); );
const setInitialGameSettings = (initialGameSettings: InitialGameSettings) => {
const defaultSettings: InitialGameSettings = {
numberOfPlayers: 4,
startingLifeTotal: 40,
useCommanderDamage: true,
orientation: Orientation.Landscape,
gameFormat: 'commander',
};
setInitialSettings({ ...defaultSettings, ...initialGameSettings });
};
const [settings, setSettings] = useState<Settings>( const [settings, setSettings] = useState<Settings>(
savedSettings savedSettings
? JSON.parse(savedSettings) ? JSON.parse(savedSettings)
: { goFullscreenOnStart: true, keepAwake: true, showStartingPlayer: true } : { goFullscreenOnStart: true, keepAwake: true, showStartingPlayer: true }
); );
const removeLocalStorage = async () => {
localStorage.removeItem('initialGameSettings');
localStorage.removeItem('players');
localStorage.removeItem('playing');
localStorage.removeItem('showPlay');
setShowPlay(false);
};
useEffect(() => { useEffect(() => {
if (savedGameSettings && JSON.parse(savedGameSettings).gridAreas) {
removeLocalStorage();
window.location.reload();
return;
}
//parse existing game settings with zod schema
const parsedInitialGameSettings =
InitialGameSettingsSchema.safeParse(initialGameSettings);
if (!parsedInitialGameSettings.success) {
removeLocalStorage();
window.location.reload();
return;
}
localStorage.setItem( localStorage.setItem(
'initialGameSettings', 'initialGameSettings',
JSON.stringify(initialGameSettings) JSON.stringify(initialGameSettings)
); );
}, [initialGameSettings]); }, [initialGameSettings, savedGameSettings]);
useEffect(() => { useEffect(() => {
localStorage.setItem('settings', JSON.stringify(settings)); localStorage.setItem('settings', JSON.stringify(settings));
@@ -78,14 +95,6 @@ export const GlobalSettingsProvider = ({
request(); request();
} }
const removeLocalStorage = async () => {
localStorage.removeItem('initialGameSettings');
localStorage.removeItem('players');
localStorage.removeItem('playing');
localStorage.removeItem('showPlay');
setShowPlay(localStorage.getItem('showPlay') === 'true' ?? false);
};
const ctxValue = useMemo((): GlobalSettingsContextType => { const ctxValue = useMemo((): GlobalSettingsContextType => {
const goToStart = async () => { const goToStart = async () => {
const currentPlayers = localStorage.getItem('players'); const currentPlayers = localStorage.getItem('players');
@@ -100,7 +109,6 @@ export const GlobalSettingsProvider = ({
}; };
const toggleWakeLock = async () => { const toggleWakeLock = async () => {
console.log('on press', active);
if (active) { if (active) {
setSettings({ ...settings, keepAwake: false }); setSettings({ ...settings, keepAwake: false });
release(); release();

View File

@@ -1,9 +1,17 @@
import { z } from 'zod';
export enum Orientation { export enum Orientation {
OppositeLandscape = 'opposite-landscape', OppositeLandscape = 'opposite-landscape',
Landscape = 'landscape', Landscape = 'landscape',
Portrait = 'portrait', Portrait = 'portrait',
} }
export enum GameFormat {
Commander = 'commander',
Standard = 'standard',
TwoHeadedGiant = 'two-headed-giant',
}
export type Settings = { export type Settings = {
keepAwake: boolean; keepAwake: boolean;
showStartingPlayer: boolean; showStartingPlayer: boolean;
@@ -18,4 +26,10 @@ export type InitialGameSettings = {
orientation: Orientation; orientation: Orientation;
}; };
type GameFormat = 'commander' | 'standard' | 'two-headed-giant'; export const InitialGameSettingsSchema = z.object({
startingLifeTotal: z.number().min(1).max(200).default(20),
useCommanderDamage: z.boolean().default(false),
gameFormat: z.nativeEnum(GameFormat).optional(),
numberOfPlayers: z.number().min(1).max(6).default(2),
orientation: z.nativeEnum(Orientation).default(Orientation.Landscape),
});

View File

@@ -2,7 +2,9 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html,
body { body {
background-color: theme('colors.background.default');
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
@@ -10,8 +12,25 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
#root {
touch-action: manipulation;
}
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; monospace;
} }
@layer utilities {
.pointer-events-all {
pointer-events: all;
}
.webkit-user-select-none {
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
-moz-user-select: -moz-none;
-webkit-user-select: none;
-ms-user-select: none;
}
}

View File

@@ -1,9 +1,14 @@
//@ts-expect-error - tailwindcss-grid-areas does not have typescript support
import tailwindcssGridAreas from '@savvywombat/tailwindcss-grid-areas'; import tailwindcssGridAreas from '@savvywombat/tailwindcss-grid-areas';
import type { Config } from 'tailwindcss';
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: { theme: {
screens: {
modalSm: '548px',
},
extend: { extend: {
gridTemplateAreas: { gridTemplateAreas: {
onePlayerLandscape: ['player0 player0'], onePlayerLandscape: ['player0 player0'],
@@ -35,7 +40,63 @@ export default {
'player0 player4 player4 player4 player4 player5 player5 player5 player5 player3', 'player0 player4 player4 player4 player4 player5 player5 player5 player5 player3',
], ],
}, },
colors: {
primary: {
main: '#3E7D78',
dark: '#2D5F5B',
},
secondary: {
main: '#284F4C',
dark: '#1B3B38',
},
background: {
default: '#08253B',
backdrop: 'rgba(0, 0, 0, 0.3)',
settings: 'rgba(20, 20, 0, 0.9)',
},
text: {
primary: '#F5F5F5',
secondary: '#76A6A5',
},
action: {
disabled: '#234A47',
},
common: {
white: '#F9FFE3',
black: '#000000',
},
lifeCounter: {
text: 'rgba(0, 0, 0, 0.4)',
lostWrapper: '#00000070',
},
interface: {
loseButton: {
background: '#43434380',
},
recentDifference: {
background: 'rgba(255, 255, 255, 0.6);',
text: '#333333',
},
},
},
keyframes: {
fadeOut: {
'0%': {
opacity: '1',
},
'33%': {
opacity: '0.6',
},
'100%': {
opacity: '0',
},
},
},
animation: {
fadeOut: 'fadeOut 3s 1s ease-out forwards',
},
}, },
}, },
plugins: [tailwindcssGridAreas], plugins: [tailwindcssGridAreas],
}; } satisfies Config;
// #98FF98

View File

@@ -1,13 +1,27 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc'; import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
clientsClaim: true,
skipWaiting: true,
},
}),
],
build: { build: {
minify: 'esbuild', minify: 'esbuild',
rollupOptions: { rollupOptions: {
external: ['babel-plugin-macros'], external: ['babel-plugin-macros'],
}, },
}, },
define: {
APP_VERSION: JSON.stringify(process.env.npm_package_version),
REPO_READ_ACCESS_TOKEN: JSON.stringify(process.env.REPO_READ_ACCESS_TOKEN),
},
}); });

2340
yarn.lock

File diff suppressed because it is too large Load Diff