stable state

This commit is contained in:
Viktor Rådberg
2023-07-02 23:09:11 +02:00
parent 17dbef5f3d
commit 2e86ad8818
13 changed files with 610 additions and 58 deletions

View File

@@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -1,15 +1,70 @@
import './App.css'; import './App.css';
import Counters from './Components/Counters/Counters'; import Counters from './Components/Counters/Counters';
import styled from 'styled-components'; import styled from 'styled-components';
import { Player } from './Types/Player';
const MainWrapper = styled.div` const MainWrapper = styled.div`
width: 100vw;
height: 100vh;
overflow: hidden;
`; `;
const players: Player[] = [
{
key: 1,
color: "grey",
settings: {
useCommanderDamage: true,
usePartner: true,
useEnergy: true,
useExperience: true,
usePoison: true,
flipped: true,
}
},
{
key: 2,
color: "mintcream",
settings: {
useCommanderDamage: true,
usePartner: false,
useEnergy: true,
useExperience: true,
usePoison: true,
flipped: true,
}
},
{
key: 3,
color: "gold",
settings: {
useCommanderDamage: true,
usePartner: false,
useEnergy: true,
useExperience: true,
usePoison: true,
flipped: false,
}
},
{
key: 4,
color: "aquamarine",
settings: {
useCommanderDamage: true,
usePartner: true,
useEnergy: true,
useExperience: true,
usePoison: true,
flipped: false,
}
},
];
function App() { function App() {
return ( return (
<MainWrapper> <MainWrapper>
<Counters/> <Counters players={players} />
</MainWrapper> </MainWrapper>
); );
} }

View File

@@ -0,0 +1,61 @@
import { useRef, useState } from "react";
import styled from "styled-components";
export const StyledLifeCounterButton = styled.button<{ align?: string }>`
width: 50%;
height: auto;
color: rgba(0, 0, 0, 0.4);
font-size: 4rem;
font-weight: 600;
background-color: transparent;
border: none;
outline: none;
cursor: pointer;
padding: 0 28px;
text-align: ${props => props.align || "center"};
user-select: none;
`;
type AddLifeButtonProps = {
lifeTotal: number;
setLifeTotal: (lifeTotal: number) => void;
};
const AddLifeButton = ({ lifeTotal, setLifeTotal }: AddLifeButtonProps) => {
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false);
const handleLifeChange = (increment: number) => {
setLifeTotal(lifeTotal + increment);
};
const handleDownInput = () => {
setTimeoutFinished(false);
timeoutRef.current = setTimeout(() => {
handleLifeChange(10);
setTimeoutFinished(true);
}, 500)
}
const handleUpInput = () => {
if (!timeoutFinished) {
clearTimeout(timeoutRef.current);
handleLifeChange(1);
}
}
return (
<StyledLifeCounterButton
onPointerDown={handleDownInput}
onPointerUp={handleUpInput}
onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
}}
align="right"
>
&#43;
</StyledLifeCounterButton>
);
};
export default AddLifeButton;

View File

@@ -0,0 +1,174 @@
import { useRef, useState } from "react";
import { Player } from "../../Types/Player";
import styled from "styled-components";
const CommanderDamageGrid = styled.div`
display: flex;
flex-direction: row;
flex-grow: 1;
width: 100%;
`;
const CommanderDamageContainer = styled.div`
display: flex;
flex-direction: row;
flex-grow: 1;
width: 100%;
`;
const CommanderDamageButton = styled.button<{ backgroundColor?: string }>`
display: flex;
flex-grow: 1;
border: none;
height: 10vh;
outline: none;
cursor: pointer;
background-color: ${props => props.backgroundColor || "antiquewhite"};
`;
const CommanderDamageButtonText = styled.p`
position: relative;
margin: auto;
font-size: 1.5rem;
text-align: center;
text-size-adjust: auto;
font-variant-numeric: tabular-nums;
pointer-events: none;
width: 2rem;
user-select: none;
`;
const VerticalSeperator = styled.div`
width: 1px;
background-color: rgba(0, 0, 0, 1);
`;
type CommanderDamageBarProps = {
lifeTotal: number;
setLifeTotal: (lifeTotal: number) => void;
opponents: Player[];
};
const CommanderDamageBar = ({ opponents, lifeTotal, setLifeTotal }: CommanderDamageBarProps) => {
const [commanderDamage, setCommanderDamage] = useState<number[]>(
Array(opponents.length).fill(0)
);
const [partnerCommanderDamage, setPartnerCommanderDamage] = useState<number[]>(
Array(opponents.length).fill(0)
);
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false);
const handleCommanderDamageChange = (index: number, increment: number) => {
const currentCommanderDamage = commanderDamage[index];
if (currentCommanderDamage === 0 && increment === -1) {
return;
}
if (currentCommanderDamage === 21 && increment === 1) {
return;
}
const updatedCommanderDamage = [...commanderDamage];
updatedCommanderDamage[index] += increment;
setCommanderDamage(updatedCommanderDamage);
setLifeTotal(lifeTotal - increment);
};
const handlePartnerCommanderDamageChange = (index: number, increment: number) => {
const currentPartnerCommanderDamage = partnerCommanderDamage[index];
if (currentPartnerCommanderDamage === 0 && increment === -1) {
return;
}
const updatedPartnerCommanderDamage = [...partnerCommanderDamage];
updatedPartnerCommanderDamage[index] += increment;
setPartnerCommanderDamage(updatedPartnerCommanderDamage);
setLifeTotal(lifeTotal - increment);
};
const handleDownInput = (index: number) => {
setTimeoutFinished(false);
timeoutRef.current = setTimeout(() => {
setTimeoutFinished(true);
handleCommanderDamageChange(index, -1);
}, 500)
}
const handleUpInput = (index: number) => {
if (!timeoutFinished) {
clearTimeout(timeoutRef.current);
handleCommanderDamageChange(index, 1);
}
clearTimeout(timeoutRef.current);
}
const handlePartnerDownInput = (index: number) => {
setTimeoutFinished(false);
timeoutRef.current = setTimeout(() => {
setTimeoutFinished(true);
handlePartnerCommanderDamageChange(index, -1);
}, 500)
}
const handlePartnerUpInput = (index: number) => {
if (!timeoutFinished) {
clearTimeout(timeoutRef.current);
handlePartnerCommanderDamageChange(index, 1);
}
}
return (
<CommanderDamageGrid>
{opponents.map((opponent, index) => {
return (
<CommanderDamageContainer>
<CommanderDamageButton
key={index}
onPointerDown={() => handleDownInput(index)}
onPointerUp={() => handleUpInput(index)}
onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
}
}
backgroundColor={opponent.color}
>
<CommanderDamageButtonText>
{commanderDamage[index] > 0 ? commanderDamage[index] : ""}
</CommanderDamageButtonText>
</CommanderDamageButton>
{opponent.settings.usePartner && (
<>
<VerticalSeperator />
<CommanderDamageButton
key={index}
onPointerDown={() => handlePartnerDownInput(index)}
onPointerUp={() => handlePartnerUpInput(index)}
onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
}
}
backgroundColor={opponent.color}
>
<CommanderDamageButtonText>
{partnerCommanderDamage[index] > 0 ? partnerCommanderDamage[index] : ""}
</CommanderDamageButtonText>
</CommanderDamageButton>
</>
)
}
</CommanderDamageContainer>
)
})}
</CommanderDamageGrid>
);
};
export default CommanderDamageBar;

View File

@@ -0,0 +1,56 @@
import { useRef, useState } from "react";
import CommanderTaxIcon from "../../Icons/CommanderTaxIcon";
import styled from "styled-components";
export const StyledCommanderTaxButton = styled.button`
flex-grow: 1;
border: none;
outline: none;
cursor: pointer;
background-color: transparent;
user-select: none;
`;
const CommanderTaxButton = () => {
const [commanderTax, setCommanderTax] = useState(0);
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false);
const handleCommanderTaxChange = (increment: number) => {
setCommanderTax(commanderTax + increment);
};
const handleDownInput = () => {
setTimeoutFinished(false);
timeoutRef.current = setTimeout(() => {
setTimeoutFinished(true);
handleCommanderTaxChange(-1);
}, 500)
}
const handleUpInput = () => {
if (!timeoutFinished) {
clearTimeout(timeoutRef.current);
handleCommanderTaxChange(1);
}
}
return (
<StyledCommanderTaxButton
onPointerDown={handleDownInput}
onPointerUp={handleUpInput}
onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
}
}
>
<CommanderTaxIcon
size="8vh"
text={commanderTax ? commanderTax : undefined}
/>
</StyledCommanderTaxButton>
);
};
export default CommanderTaxButton;

View File

@@ -0,0 +1,56 @@
import { useRef, useState } from "react";
import CommanderTaxIcon from "../../Icons/CommanderTaxIcon";
import styled from "styled-components";
export const StyledCommanderTaxButton = styled.button`
flex-grow: 1;
border: none;
outline: none;
cursor: pointer;
background-color: transparent;
user-select: none;
`;
const PartnerCommanderTaxButton = () => {
const [partnerCommanderTax, setPartnerCommanderTax] = useState(0);
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false);
const handlePartnerCommanderTaxChange = (increment: number) => {
setPartnerCommanderTax(partnerCommanderTax + increment);
};
const handleDownInput = () => {
setTimeoutFinished(false);
timeoutRef.current = setTimeout(() => {
setTimeoutFinished(true);
handlePartnerCommanderTaxChange(-1);
}, 500)
}
const handleUpInput = () => {
if (!timeoutFinished) {
clearTimeout(timeoutRef.current);
handlePartnerCommanderTaxChange(1);
}
}
return (
<StyledCommanderTaxButton
onPointerDown={handleDownInput}
onPointerUp={handleUpInput}
onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
}
}
>
<CommanderTaxIcon
size="8vh"
text={partnerCommanderTax ? partnerCommanderTax : undefined}
/> 2
</StyledCommanderTaxButton>
);
};
export default PartnerCommanderTaxButton;

View File

@@ -0,0 +1,61 @@
import { useRef, useState } from "react";
import styled from "styled-components";
export const StyledLifeCounterButton = styled.button<{ align?: string }>`
width: 50%;
height: auto;
color: rgba(0, 0, 0, 0.4);
font-size: 4rem;
font-weight: 600;
background-color: transparent;
border: none;
outline: none;
cursor: pointer;
padding: 0 28px;
text-align: ${props => props.align || "center"};
user-select: none;
`;
type SubtractLifeButtonProps = {
lifeTotal: number;
setLifeTotal: (lifeTotal: number) => void;
};
const SubtractLifeButton = ({ lifeTotal, setLifeTotal }: SubtractLifeButtonProps) => {
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [timeoutFinished, setTimeoutFinished] = useState(false);
const handleLifeChange = (increment: number) => {
setLifeTotal(lifeTotal + increment);
};
const handleDownInput = () => {
setTimeoutFinished(false);
timeoutRef.current = setTimeout(() => {
handleLifeChange(-10);
setTimeoutFinished(true);
}, 500)
}
const handleUpInput = () => {
if (!timeoutFinished) {
clearTimeout(timeoutRef.current);
handleLifeChange(-1);
}
}
return (
<StyledLifeCounterButton
onPointerDown={handleDownInput}
onPointerUp={handleUpInput}
onContextMenu={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
}}
align="left"
>
&#8722;
</StyledLifeCounterButton>
);
};
export default SubtractLifeButton;

View File

@@ -1,9 +1,9 @@
import styled from "styled-components"; import styled from "styled-components";
export const CountersWrapper = styled.div` export const CountersWrapper = styled.div`
width: 100vw; width: 100%;
height: 100vh; max-height: 100%;
background-color: #4f4f4f; background-color: black;
`; `;
export const CountersGrid = styled.div` export const CountersGrid = styled.div`
@@ -11,16 +11,27 @@ export const CountersGrid = styled.div`
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: 10px;
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */ -moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */ box-sizing: border-box; /* Opera/IE 8+ */
row-gap: 4px;
column-gap: 4px;
`; `;
export const GridItemContainer = styled.div` export const GridItemContainer = styled.div`
display: flex; display: flex;
width: 50%; width: calc(50vw - 2px);
height: 50vh; height: calc(50vh - 2px);
justify-content: center; justify-content: center;
align-items: center; align-items: center;
`; `;
export const GridItemContainerFlipped = styled.div`
display: flex;
width: calc(50vw - 2px);
height: calc(50vh - 2px);
justify-content: center;
align-items: center;
transform: rotate(180deg);
`;

View File

@@ -1,26 +1,36 @@
import * as S from "./Counters.style"; import * as S from "./Counters.style";
import LifeCounter from "../LifeCounter/LifeCounter"; import LifeCounter from "../LifeCounter/LifeCounter";
import { Player } from "../../Types/Player";
type CountersProps = {
players: Player[];
};
const Counters = () => { const Counters = ({ players }: CountersProps) => {
return ( return (
<S.CountersWrapper> <S.CountersWrapper>
<S.CountersGrid> <S.CountersGrid>
<S.GridItemContainer> {players.map((player) => {
<LifeCounter backgroundColor="grey"/> if (player.settings.flipped) {
</S.GridItemContainer> return (
<S.GridItemContainer> <S.GridItemContainerFlipped>
<LifeCounter backgroundColor="pink"/> <LifeCounter backgroundColor={player.color} player={player} opponents={
</S.GridItemContainer> players.filter((opponent) => opponent.key !== player.key)
<S.GridItemContainer> } />
<LifeCounter backgroundColor="white"/> </S.GridItemContainerFlipped>
</S.GridItemContainer> )
<S.GridItemContainer> }
<LifeCounter backgroundColor="lightblue"/> return (
</S.GridItemContainer> <S.GridItemContainer>
</S.CountersGrid> <LifeCounter backgroundColor={player.color} player={player} opponents={
</S.CountersWrapper> players.filter((opponent) => opponent.key !== player.key)
); } />
</S.GridItemContainer>
)
})}
</S.CountersGrid>
</S.CountersWrapper>
);
} }
export default Counters; export default Counters;

View File

@@ -1,36 +1,44 @@
import styled from "styled-components"; import styled from "styled-components";
//LifeCounterWrapper with a background color variable:
export const LifeCounterWrapper = styled.div<{ backgroundColor?: string }>` export const LifeCounterWrapper = styled.div<{ backgroundColor?: string }>`
position: relative;
display: flex; display: flex;
justify-content: center; flex-direction: column;
align-items: center; align-items: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: ${props => props.backgroundColor || "antiquewhite"}; background-color: ${props => props.backgroundColor || "antiquewhite"};
border-radius: 10px;
`; `;
export const LifeCounterButton = styled.button` export const LifeCountainer = styled.div`
display: flex;
flex-direction: row;
flex-grow: 1;
width: 100%; width: 100%;
height: 100%; height: 100%;
font-size: 5rem;
font-weight: bold;
background-color: transparent;
border: none;
outline: none;
cursor: pointer;
`; `;
export const LifeCounterText = styled.p` export const LifeCounterText = styled.p`
font-size: 5rem; position: absolute;
font-weight: bold; top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 30vh;
text-align: center; text-align: center;
text-size-adjust: auto;
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%;
font-variant-numeric: tabular-nums;
pointer-events: none;
user-select: none;
`;
export const ExtraCountersGrid = styled.div`
display: flex;
flex-direction: row;
flex-grow: 1;
width: 100%;
`; `;

View File

@@ -1,20 +1,35 @@
import { useState } from "react"; import { useState } from "react";
import * as S from "./LifeCounter.style"; import * as S from "./LifeCounter.style";
import { Player } from "../../Types/Player";
import CommanderTaxButton from "../Buttons/CommanderTaxButton";
import PartnerCommanderTaxButton from "../Buttons/PartnerCommanderTaxButton copy";
import AddLifeButton from "../Buttons/AddLifeButton";
import SubtractLifeButton from "../Buttons/SubtractLifeButton";
import CommanderDamageBar from "../Buttons/CommanderDamageBar";
type LifeCounterProps = { type LifeCounterProps = {
player: Player;
backgroundColor: string; backgroundColor: string;
} opponents: Player[];
};
const LifeCounter = ({backgroundColor}: LifeCounterProps) => { const LifeCounter = ({ backgroundColor, player, opponents }: LifeCounterProps) => {
const [life, setLife] = useState(40); const [lifeTotal, setLifeTotal] = useState(40);
return ( return (
<S.LifeCounterWrapper backgroundColor={backgroundColor}> <S.LifeCounterWrapper backgroundColor={backgroundColor}>
<S.LifeCounterButton onClick={() => setLife(life - 1)}>-</S.LifeCounterButton> <CommanderDamageBar lifeTotal={lifeTotal} setLifeTotal={setLifeTotal} opponents={opponents} />
<S.LifeCounterText>{life}</S.LifeCounterText> <S.LifeCountainer>
<S.LifeCounterButton onClick={() => setLife(life + 1)}>+</S.LifeCounterButton> <SubtractLifeButton lifeTotal={lifeTotal} setLifeTotal={setLifeTotal}/>
<S.LifeCounterText>{lifeTotal}</S.LifeCounterText>
<AddLifeButton lifeTotal={lifeTotal} setLifeTotal={setLifeTotal} />
</S.LifeCountainer>
<S.ExtraCountersGrid>
<CommanderTaxButton/>
{player.settings.usePartner && <PartnerCommanderTaxButton/>}
</S.ExtraCountersGrid>
</S.LifeCounterWrapper> </S.LifeCounterWrapper>
); );
} };
export default LifeCounter; export default LifeCounter;

View File

@@ -0,0 +1,39 @@
type CommanderTaxIconProps = {
size?: string;
text?: number;
};
const CommanderTaxIcon = ({ size, text }: CommanderTaxIconProps) => {
return (
<div style={{ position: 'relative', display: 'inline-block'}}>
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 325 325" width={size || 'auto'} height={size || 'auto'}>
<title>CommanderTaxIcon</title>
<style>{`.s0 { fill: #000000; fill-opacity: 0.5}`}</style>
<path
id="Lager 1"
className="s0"
d="m162 168c-40.9 0-74-33.1-74-74 0-40.9 33.1-74 74-74 40.9 0 74 33.1 74 74 0 40.9-33.1 74-74 74z"
/>
<path
id="Form 1"
className="s0"
d="m159.9 351.8c-11.4 0.3-22.5 0.7-33.2 1.2-10.6 0.6-20.8 1.3-30.5 1.8-9.6 0.5-18.8 0.6-27.2 0.5-8.4-0.6-16.1-1.6-23-3.4-6.9-2.3-12.9-5.5-18.1-9.5-5-4.7-9.1-10.4-12.2-16.9-3.1-7.1-5.2-15-6.3-23.4-1.1-8.8-1.2-17.9-0.4-27.1 0.8-9.2 2.4-17.7 4.7-25.3 2.2-7.4 5.2-13.9 8.8-19.5 3.6-5.3 7.8-9.9 12.8-13.6 4.8-3.7 10.5-6.9 16.8-9.5 6.4-2.9 13.6-5.4 21.5-7.6 7.9-2.4 16.6-4.6 26-6.5 9.4-2.1 19.4-3.8 29.9-5 10.6-1.3 21.7-2 33-2 11.3 0 22.4 0.7 33 2 10.5 1.2 20.6 2.9 30 5 9.4 1.9 18.2 4.1 26.2 6.5 7.9 2.3 15.2 4.8 21.7 7.6 6.4 2.7 12.2 5.8 17.1 9.5 5.1 3.8 9.4 8.3 12.9 13.7 3.7 5.5 6.7 12.1 8.9 19.5 2.3 7.7 3.8 16.2 4.5 25.3 0.7 9.2 0.4 18.3-0.9 27.1-1.3 8.4-3.6 16.2-6.8 23.3-3.4 6.4-7.8 11.9-13.1 16.4-5.5 3.9-11.8 6.9-19 9.1-7.3 1.6-15.4 2.6-24.1 3-8.8 0.1-18.3-0.1-28.2-0.5-10-0.4-20.5-0.9-31.4-1.3-10.8-0.3-22-0.5-33.4-0.4z"
/>
</svg>
<div
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
fontSize: '8vh',
fontWeight: 'bold',
}}
>
{text}
</div>
</div>
);
};
export default CommanderTaxIcon;

View File

@@ -0,0 +1,15 @@
export type Player = {
key: number;
color: string;
settings: PlayerSettings;
}
type PlayerSettings = {
useCommanderDamage: boolean;
flipped?: boolean;
usePartner?: boolean;
usePoison?: boolean;
useEnergy?: boolean;
useExperience?: boolean;
}