Compare commits

...

10 Commits

Author SHA1 Message Date
Viktor Rådberg
89e1eaff4e bump 2024-01-27 16:25:40 +01:00
Viktor Rådberg
0f4e896342 Merge pull request #31 from Vikeo/swipable-settings
Swipable settings
2024-01-27 16:23:54 +01:00
Viktor Rådberg
dc1d5fe01d tsc 2024-01-27 16:20:09 +01:00
Viktor Rådberg
41e73d2c0c swipe 2024-01-27 11:05:54 +01:00
Viktor Rådberg
724dcf086c is side 2024-01-27 09:32:00 +01:00
Viktor Rådberg
51f9c4d20e initial test 2024-01-26 21:24:40 +01:00
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
18 changed files with 2425 additions and 412 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,7 +1,7 @@
{
"name": "life-trinket",
"private": true,
"version": "0.5.51",
"version": "0.6.1",
"type": "commonjs",
"engines": {
"node": ">=18",
@@ -22,6 +22,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-screen-wake-lock": "^3.0.2",
"react-swipeable": "^7.0.1",
"react-twc": "^1.3.0",
"zod": "^3.22.4"
},
@@ -43,8 +44,9 @@
"install": "^0.13.0",
"postcss": "^8.4.32",
"prettier": "2.8.8",
"tailwindcss": "^3.4.0",
"typescript": "^5.0.2",
"vite": "^4.4.5"
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-plugin-pwa": "^0.17.4"
}
}

View File

@@ -66,10 +66,6 @@ export const CommanderDamage = ({
const [timeoutFinished, setTimeoutFinished] = useState(false);
const [hasPressedDown, setHasPressedDown] = useState(false);
const isSide =
player.settings.rotation === Rotation.Side ||
player.settings.rotation === Rotation.SideFlipped;
const handleCommanderDamageChange = (
index: number,
increment: number,
@@ -132,9 +128,9 @@ export const CommanderDamage = ({
};
const opponentIndex = opponent.index;
const fontSize = isSide ? '4vmax' : '7vmin';
const fontSize = player.isSide ? '4vmax' : '7vmin';
const fontWeight = 'bold';
const strokeWidth = isSide ? '0.4vmax' : '0.7vmin';
const strokeWidth = player.isSide ? '0.4vmax' : '0.7vmin';
return (
<CommanderDamageContainer

View File

@@ -47,6 +47,7 @@ type ExtraCounterProps = {
type: CounterType;
setCounterTotal: (updatedCounterTotal: number, type: CounterType) => void;
rotation: number;
isSide: boolean;
playerIndex: number;
};
@@ -56,15 +57,13 @@ const ExtraCounter = ({
setCounterTotal,
type,
rotation,
isSide,
playerIndex,
}: ExtraCounterProps) => {
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false);
const [hasPressedDown, setHasPressedDown] = useState(false);
const isSide =
rotation === Rotation.Side || rotation === Rotation.SideFlipped;
const handleCountChange = (increment: number) => {
if (!counterTotal) {
setCounterTotal(increment, type);

View File

@@ -4,13 +4,11 @@ import { Rotation } from '../../Types/Player';
import { RotationDivProps } from './CommanderDamage';
const LoseButton = twc.div<RotationDivProps>((props) => [
'absolute flex-grow border-none outline-none cursor-pointer bg-interface-loseButton-background rounded-lg select-none z-[1] webkit-user-select-none',
'absolute flex-grow border-none outline-none cursor-pointer bg-interface-loseButton-background rounded-lg select-none z-[1] webkit-user-select-none py-2 px-4 ',
props.$rotation === Rotation.SideFlipped
? `right-auto top-[15%] left-[27%]`
: props.$rotation === Rotation.Side
? `right-auto top-[15%] left-[27%]`
: 'right-[15%] top-1/4',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `left-[19%]`
: 'top-[21%]',
]);
type LoseButtonProps = {
@@ -24,6 +22,8 @@ export const LoseGameButton = ({ rotation, onClick }: LoseButtonProps) => {
? rotation
: rotation === Rotation.Side
? rotation - 180
: rotation === Rotation.Flipped
? rotation - 180
: rotation;
return (
@@ -33,7 +33,7 @@ export const LoseGameButton = ({ rotation, onClick }: LoseButtonProps) => {
aria-label={`Lose Game`}
style={{ rotate: `${calcRotation}deg` }}
>
<Skull size="5vmin" color="black" opacity={0.5} />
<Skull size="8vmin" color="black" opacity={0.5} />
</LoseButton>
);
};

View File

@@ -100,6 +100,7 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
(counter) => counter.type === 'commanderTax'
)?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>
@@ -114,6 +115,7 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
(counter) => counter.type === 'partnerTax'
)?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>
@@ -127,6 +129,7 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
player.extraCounters?.find((counter) => counter.type === 'poison')
?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>
@@ -140,6 +143,7 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
player.extraCounters?.find((counter) => counter.type === 'energy')
?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>
@@ -154,6 +158,7 @@ const ExtraCountersBar = ({ player }: ExtraCountersBarProps) => {
(counter) => counter.type === 'experience'
)?.value
}
isSide={player.isSide}
setCounterTotal={handleCounterChange}
playerIndex={player.index}
/>

View File

@@ -8,7 +8,7 @@ import {
import LifeCounterButton from '../Buttons/LifeCounterButton';
import { OutlinedText } from '../Misc/OutlinedText';
const LifeCountainer = twc.div<RotationDivProps>((props) => [
const LifeContainer = twc.div<RotationDivProps>((props) => [
'flex flex-grow relative w-full h-full justify-between items-center',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? 'flex-col-reverse'
@@ -49,7 +49,6 @@ type HealthProps = {
const Health = ({
player,
rotation,
handleLifeChange,
differenceKey,
recentDifference,
@@ -99,12 +98,13 @@ const Health = ({
}, [textContainerRef]);
const calculateFontSize = (container: HTMLDivElement) => {
const isSide =
rotation === Rotation.SideFlipped || rotation === Rotation.Side;
const widthRatio = player.isSide
? container.clientHeight
: container.clientWidth;
const widthRatio = isSide ? container.clientHeight : container.clientWidth;
const heightRatio = isSide ? container.clientWidth : container.clientHeight;
const heightRatio = player.isSide
? container.clientWidth
: container.clientHeight;
const minRatio = Math.min(widthRatio, heightRatio);
@@ -116,7 +116,7 @@ const Health = ({
};
return (
<LifeCountainer $rotation={player.settings.rotation}>
<LifeContainer $rotation={player.settings.rotation}>
<LifeCounterButton
lifeTotal={player.lifeTotal}
setLifeTotal={handleLifeChange}
@@ -154,7 +154,7 @@ const Health = ({
operation="add"
increment={1}
/>
</LifeCountainer>
</LifeContainer>
);
};

View File

@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react';
import { useSwipeable } from 'react-swipeable';
import { twc } from 'react-twc';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
import { LoseGameButton } from '../Buttons/LoseButton';
import SettingsButton from '../Buttons/SettingsButton';
import CommanderDamageBar from '../Counters/CommanderDamageBar';
import ExtraCountersBar from '../Counters/ExtraCountersBar';
import PlayerMenu from '../Player/PlayerMenu';
@@ -24,7 +24,7 @@ const LifeCounterWrapper = twc.div<RotationDivProps>((props) => [
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`;
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',
'z-[1] flex absolute w-full h-full justify-center items-center pointer-events-none select-none webkit-user-select-none bg-lifeCounter-lostWrapper opacity-75',
props.$rotation === Rotation.SideFlipped || props.$rotation === Rotation.Side
? `rotate-[${props.$rotation - 90}deg]`
: '',
@@ -71,14 +71,51 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
const [showPlayerMenu, setShowPlayerMenu] = useState(false);
const [recentDifference, setRecentDifference] = useState(0);
const [differenceKey, setDifferenceKey] = useState(Date.now());
const [isLandscape, setIsLandscape] = useState(false);
const calcRot = player.isSide
? player.settings.rotation - 180
: player.settings.rotation;
const rotationAngle = isLandscape ? calcRot : calcRot + 90;
const handlers = useSwipeable({
trackMouse: true,
onSwipedDown: () => {
console.log(`User DOWN Swiped on player ${player.index}`);
setShowPlayerMenu(true);
},
onSwipedUp: () => {
console.log(`User UP Swiped on player ${player.index}`);
setShowPlayerMenu(false);
},
swipeDuration: 500,
onSwiping: (eventData) => console.log(eventData),
rotationAngle,
});
useEffect(() => {
const timer = setTimeout(() => {
setRecentDifference(0);
}, 3_000);
return () => clearTimeout(timer);
}, [recentDifference]);
const resizeObserver = new ResizeObserver(() => {
if (document.body.clientWidth > document.body.clientHeight)
setIsLandscape(true);
else setIsLandscape(false);
return;
});
resizeObserver.observe(document.body);
return () => {
clearTimeout(timer);
// Cleanup: disconnect the ResizeObserver when the component unmounts.
resizeObserver.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [recentDifference, document.body.clientHeight, document.body.clientWidth]);
useEffect(() => {
if (player.showStartingPlayer) {
@@ -124,6 +161,7 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
<LifeCounterWrapper
$rotation={player.settings.rotation}
style={{ rotate: `${calcRotation}deg` }}
{...handlers}
>
{settings.showStartingPlayer &&
player.isStartingPlayer &&
@@ -150,12 +188,6 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
key={player.index}
handleLifeChange={handleLifeChange}
/>
<SettingsButton
onClick={() => {
setShowPlayerMenu(!showPlayerMenu);
}}
rotation={player.settings.rotation}
/>
{playerCanLose(player) && (
<LoseGameButton
rotation={player.settings.rotation}
@@ -170,9 +202,12 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
handleLifeChange={handleLifeChange}
/>
<ExtraCountersBar player={player} />
{showPlayerMenu && (
<PlayerMenu player={player} setShowPlayerMenu={setShowPlayerMenu} />
)}
<PlayerMenu
isShown={showPlayerMenu}
player={player}
setShowPlayerMenu={setShowPlayerMenu}
/>
</LifeCounterWrapper>
</LifeCounterContentWrapper>
);

View File

@@ -3,7 +3,7 @@ import { twc } from 'react-twc';
import { Separator } from './Separator';
import { Paragraph } from './TextComponents';
export const ModalWrapper = twc.div`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[80vw] h-[85vh] bg-background-default p-4 overflow-scroll rounded-2xl border-none text-text-primary`;
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]`;
type InfoModalProps = {
isOpen: boolean;
@@ -12,7 +12,20 @@ type InfoModalProps = {
export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
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>
<div>
<h2 className="text-2xl text-center mb-4">📋 Usage Guide</h2>
@@ -28,8 +41,8 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
subtract <strong>1 life</strong>.
</li>
<li>
<strong>Long press</strong> on a player's + or - button to add or
subtract <strong>10 life</strong>.
<strong>Long press</strong> on a player's + or - button to add
or subtract <strong>10 life</strong>.
</li>
</ul>
@@ -52,8 +65,8 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
When a player is <strong>at or below 0 life</strong>, has taken{' '}
<strong>21 or more Commander Damage</strong> or has{' '}
<strong>10 or more poison counters</strong>, a button with a skull
will appear on that player's card. Tapping it will dim the player's
card.
will appear on that player's card. Tapping it will dim the
player's card.
</Paragraph>
</div>
<div className="text-center mt-4">
@@ -69,6 +82,7 @@ export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
for more info about this web app.
</div>
</ModalWrapper>
</>
</Modal>
);
};

View File

@@ -67,6 +67,15 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
return (
<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>
<Container>
<h2 className="text-center text-2xl mb-2"> Settings </h2>
@@ -95,13 +104,16 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
<Switch
checked={settings.keepAwake}
onChange={() => {
setSettings({ ...settings, keepAwake: !settings.keepAwake });
setSettings({
...settings,
keepAwake: !settings.keepAwake,
});
}}
/>
</ToggleContainer>
<Description>
Will prevent device from going to sleep while this app is open if
this is enabled.
Will prevent device from going to sleep while this app is open
if this is enabled.
</Description>
</SettingContainer>
<SettingContainer>
@@ -135,8 +147,8 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</Paragraph>
</ToggleContainer>
<Description className="mt-1">
If you do, this app will work offline and the toolbar will be
automatically hidden.
If you do, this app will work offline and the toolbar will
be automatically hidden.
</Description>
</SettingContainer>
</>
@@ -177,6 +189,7 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</Button>
</Container>
</ModalWrapper>
</>
</Modal>
);
};

View File

@@ -6,7 +6,6 @@ import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { usePlayers } from '../../Hooks/usePlayers';
import { useSafeRotate } from '../../Hooks/useSafeRotate';
import {
Cross,
Energy,
Exit,
Experience,
@@ -17,10 +16,7 @@ import {
ResetGame,
} from '../../Icons/generated';
import { Player, Rotation } from '../../Types/Player';
import {
RotationButtonProps,
RotationDivProps,
} from '../Buttons/CommanderDamage';
import { RotationDivProps } from '../Buttons/CommanderDamage';
const CheckboxContainer = twc.div``;
@@ -35,6 +31,7 @@ const PlayerMenuWrapper = twc.div`
justify-center
z-[2]
webkit-user-select-none
transition-all
`;
const BetterRowContainer = twc.div`
@@ -85,21 +82,17 @@ const SettingsContainer = twc.div<RotationDivProps>((props) => [
: 'flex-row',
]);
const CloseButton = twc.button<RotationButtonProps>((props) => [
'absolute border-none outline-none cursor-pointer bg-transparent z-[99]',
props.$rotation === Rotation.Side
? `top-[5%] right-auto left-[5%]`
: props.$rotation === Rotation.SideFlipped
? 'top-auto left-auto bottom-[5%] right-[5%]'
: 'top-[15%] right-[5%]',
]);
type PlayerMenuProps = {
player: Player;
setShowPlayerMenu: (showPlayerMenu: boolean) => void;
isShown: boolean;
};
const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
const PlayerMenu = ({
player,
setShowPlayerMenu,
isShown,
}: PlayerMenuProps) => {
const settingsContainerRef = useRef<HTMLDivElement | null>(null);
const dialogRef = useRef<HTMLDialogElement | null>(null);
@@ -108,9 +101,6 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
containerRef: settingsContainerRef,
});
const handleOnClick = () => {
setShowPlayerMenu(false);
};
const { fullscreen, wakeLock, goToStart } = useGlobalSettings();
const { updatePlayer, resetCurrentGame } = usePlayers();
@@ -142,7 +132,6 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
const buttonFontSize = isSide ? '1.5vmax' : '3vmin';
const iconSize = isSide ? '6vmin' : '3vmax';
const extraCountersSize = isSide ? '8vmin' : '4vmax';
const closeButtonSize = isSide ? '6vmin' : '3vmax';
const calcRotation =
player.settings.rotation === Rotation.Side
@@ -156,33 +145,10 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
//TODO: Fix hacky solution to rotation for SideFlipped
style={{
rotate:
player.settings.rotation === Rotation.SideFlipped ? '180deg' : '',
player.settings.rotation === Rotation.SideFlipped ? `180deg` : '',
translate: isShown ? '' : player.isSide ? `-100%` : `0 -100%`,
}}
>
<CloseButton
$rotation={player.settings.rotation}
style={{
rotate:
player.settings.rotation === Rotation.Side ||
player.settings.rotation === Rotation.SideFlipped
? `${player.settings.rotation - 180}deg`
: '',
}}
>
<Button
variant="text"
onClick={handleOnClick}
style={{
margin: 0,
padding: 0,
height: closeButtonSize,
width: closeButtonSize,
}}
>
<Cross size={closeButtonSize} />
</Button>
</CloseButton>
<SettingsContainer
$rotation={player.settings.rotation}
style={{
@@ -373,7 +339,7 @@ const PlayerMenu = ({ player, setShowPlayerMenu }: PlayerMenuProps) => {
</BetterRowContainer>
<dialog
ref={dialogRef}
className="z-[9999] min-h-2/4 bg-background-default text-text-primary rounded-2xl border-none absolute top-[10%]"
className="z-[9999] min-h-2/4 bg-background-default text-text-primary rounded-2xl border-none absolute bottom-[20%]"
>
<div className="h-full flex flex-col p-4 gap-2">
<h1 className="text-center">Reset Game?</h1>

View File

@@ -176,7 +176,7 @@ const Start = () => {
Life Trinket
</h1>
<div className="overflow-hidden items-center flex flex-col max-w-[548px] mb-8 px-4">
<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>
<SliderWrapper>

View File

@@ -229,6 +229,7 @@ export const createInitialPlayers = ({
extraCounters: [],
commanderDamage,
hasLost: false,
isSide: rotation === Rotation.Side || rotation === Rotation.SideFlipped,
};
players.push(player);

View File

@@ -0,0 +1,53 @@
import { useEffect, useState } from 'react';
export interface OrientationState {
angle: number;
type: string;
}
const defaultState: OrientationState = {
angle: 0,
type: 'landscape-primary',
};
export default function useOrientation(
initialState: OrientationState = defaultState
) {
const [state, setState] = useState(initialState);
const [isLandscape, setIsLandscape] = useState(false);
useEffect(() => {
const screen = window.screen;
let mounted = true;
const onChange = () => {
if (mounted) {
const { orientation } = screen;
console.log(orientation);
if (orientation) {
const { angle, type } = orientation;
setState({ angle, type });
if (type.includes('landscape')) {
setIsLandscape(true);
} else if (type.includes('portrait')) {
setIsLandscape(false);
}
} else if (window.orientation !== undefined) {
setState({
angle:
typeof window.orientation === 'number' ? window.orientation : 0,
type: '',
});
}
}
};
onChange();
return () => {
mounted = false;
};
}, [isLandscape]);
return { state, isLandscape };
}

View File

@@ -8,6 +8,7 @@ export type Player = {
isStartingPlayer: boolean;
showStartingPlayer: boolean;
hasLost: boolean;
isSide: boolean;
};
export type PlayerSettings = {

View File

@@ -6,6 +6,9 @@ import type { Config } from 'tailwindcss';
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
screens: {
modalSm: '548px',
},
extend: {
gridTemplateAreas: {
onePlayerLandscape: ['player0 player0'],
@@ -53,10 +56,10 @@ export default {
},
text: {
primary: '#F5F5F5',
secondary: '#b3b39b',
secondary: '#76A6A5',
},
action: {
disabled: '#5E714C',
disabled: '#234A47',
},
common: {
white: '#F9FFE3',
@@ -64,7 +67,7 @@ export default {
},
lifeCounter: {
text: 'rgba(0, 0, 0, 0.4)',
lostWrapper: '#00000070',
lostWrapper: '#000000',
},
interface: {
loseButton: {

View File

@@ -1,9 +1,19 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
clientsClaim: true,
skipWaiting: true,
},
}),
],
build: {
minify: 'esbuild',
rollupOptions: {

2231
yarn.lock

File diff suppressed because it is too large Load Diff