Created
May 12, 2024 18:48
-
-
Save alexanderankin/abaf028a45ea453d44c15f6826825b65 to your computer and use it in GitHub Desktop.
mtg counters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { CSSProperties, useEffect, useRef, useState } from 'react' | |
import './App.css' | |
import { useLocalStorage } from '@uidotdev/usehooks'; | |
function App() { | |
// noinspection JSUnusedLocalSymbols | |
// @ts-ignore | |
const [count, setCount] = useState(0) | |
return ( | |
// <div className='vw-100'> | |
// <div style={{width: '100vh', transform: 'rotate(90deg)'}}> | |
<div className='vw-100 vh-100 p-3'> | |
<h1>MTG Counters</h1> | |
<Counters /> | |
</div> | |
) | |
} | |
interface CounterShape { | |
health: number, | |
commanderDamage: number[] | |
} | |
const defaultCounters: CounterShape = { | |
health: 20, | |
commanderDamage: [0, 0, 0, 0], | |
} | |
function newCounters(i: number = 4) { | |
return Array(i).fill(null).map(() => ({ | |
...defaultCounters, | |
commanderDamage: [...defaultCounters.commanderDamage] | |
})) | |
} | |
const RESET_STYLE: CSSProperties = { position: 'relative', zIndex: 10, left: 10, top: 10 }; | |
function Counters() { | |
const [counters, saveCounters] = useLocalStorage<CounterShape[]>("counters", newCounters(4)); | |
function counterAdapter(key: number) { | |
return { | |
value: counters[key], | |
// setter: (callback: () => Object) => { | |
setter: (callback: (cc: CounterShape) => CounterShape) => { | |
saveCounters(cc => { | |
cc[key] = callback(cc[key]) | |
return { ...cc }; | |
}); | |
} | |
} | |
} | |
function reset() { | |
saveCounters(newCounters(4)); | |
} | |
return <> | |
<div className='position-absolute'> | |
<button className='btn btn-small btn-outline-danger' style={RESET_STYLE} onClick={reset}>Reset</button> | |
</div> | |
<div className='card' style={{ height: '80vh' }}> | |
<div className='card-body' style={{ minHeight: '100%' }}> | |
{/* <div style={{transform: 'rotate(90deg)'}}> */} | |
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}> | |
{/* grid of four */} | |
{Array(4).fill(null).map((_, i) => { | |
let style = i in [0, 1] ? { transform: 'rotate(180deg)' } : {} | |
// let style = {} | |
return <Counter style={style} key={i} reactKey={i} adapter={counterAdapter(i)} /> | |
})} | |
</div> | |
</div> | |
</div> | |
</>; | |
} | |
interface CounterProps { | |
reactKey: number, | |
style?: CSSProperties, | |
adapter: { | |
setter: (callback: (cc: CounterShape) => CounterShape) => void; | |
value: CounterShape | |
} | |
} | |
function Counter({ adapter, style: propStyle, reactKey }: CounterProps) { | |
const [modal, setModal] = useState(false) | |
const [delta, setDelta] = useState(0) | |
let style = propStyle || {} | |
let setCount = adapter.setter | |
let inc = () => { | |
setDelta(d => d + 1); | |
setCount(stats => ({ ...stats, health: stats.health + 1 })); | |
}; | |
let dec = () => { | |
setDelta(d => d - 1); | |
setCount(stats => ({ ...stats, health: stats.health - 1 })); | |
}; | |
let modalRef = useRef<HTMLDialogElement>(null) | |
useEffect(() => { | |
if (modal) | |
modalRef.current?.showModal(); | |
else | |
modalRef.current?.hidePopover(); | |
}) | |
// html dialog interacts with escape key | |
// we need to override that to remain usable after Esc key press | |
useEffect(() => { | |
function onEscape(e: KeyboardEvent) { | |
if (e.key === "Escape") { | |
setModal(false); | |
} | |
} | |
document.addEventListener("keydown", onEscape, false); | |
return () => document.removeEventListener("keydown", onEscape); | |
}) | |
useEffect(() => { | |
let timer = setTimeout(() => { | |
setDelta(0) | |
}, 5000); | |
return () => clearTimeout(timer) | |
}) | |
return <> | |
{!modal ? null : <> | |
{/* pass style to flip in direction of player */} | |
<dialog ref={modalRef} style={{ ...style, minWidth: 400, minHeight: 250 }}> | |
<p>Commander damage</p> | |
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}> | |
{Array(4).fill(null).map((_, i) => { | |
return <InnerCounter style={{}} key={i} ourKey={reactKey} otherKey={i} adapter={adapter} /> | |
})} | |
{/* <InnerCounter style={style} key={0} reactKey={0} adapter={adapter} /> */} | |
</div> | |
<button onClick={() => setModal(false)}>OK</button> | |
</dialog> | |
</>} | |
<div className='card' style={{ ...style, display: 'flex', flex: 1, flexBasis: '50%', flexDirection: 'row' }}> | |
<div className='card-body p-0' style={{ display: 'grid', gridTemplate: '1fr / 1fr', placeItems: 'center' }}> | |
<div style={{ gridColumn: '1/1', gridRow: '1/1', zIndex: 1, height: '100%', width: '100%' }}> | |
<button className={'btn'} style={{ height: '100%', width: '50%' }} onClick={dec}>-</button> | |
<button className={'btn'} style={{ height: '100%', width: '50%' }} onClick={inc}>+</button> | |
</div> | |
<div style={{ gridColumn: '1/1', gridRow: '1/1', zIndex: 0, height: '40vh' }}> | |
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', flexDirection: 'column' }}> | |
<div style={{flex: '0 1'}}><h1>{adapter.value.health}</h1></div> | |
<div style={{flex: '0 1'}}><span>{delta === 0 ? ' ' : delta}</span></div> | |
</div> | |
</div> | |
<button | |
className={'btn btn-outline-secondary'} | |
style={{ | |
height: 50, | |
width: 100, | |
backgroundColor: 'gray', | |
zIndex: 2, | |
position: 'absolute', | |
bottom: 10, | |
color: 'white' | |
}} | |
onClick={() => setModal(true)} | |
> | |
{adapter.value.commanderDamage.map((cd, i) => i === reactKey ? 'me' : cd).join(',')} | |
</button> | |
</div> | |
</div> | |
</>; | |
} | |
interface InnerCounterProps { | |
style: CSSProperties, | |
ourKey: number, | |
otherKey: number, | |
adapter: { setter: (callback: (cc: CounterShape) => CounterShape) => void; value: CounterShape } | |
} | |
function InnerCounter({ style, ourKey, otherKey, adapter }: InnerCounterProps) { | |
// const [clicked, setClicked] = useState(0) | |
let inc = () => { | |
adapter.setter(stats => { | |
stats.health -= 1; | |
stats.commanderDamage[otherKey] += 1; | |
return { ...stats, commanderDamage: [...stats.commanderDamage] }; | |
}) | |
}; | |
let dec = () => { | |
adapter.setter(stats => { | |
stats.health += 1; | |
stats.commanderDamage[otherKey] -= 1; | |
return { ...stats, commanderDamage: [...stats.commanderDamage] }; | |
}) | |
}; | |
let cardStyle: CSSProperties = { | |
...style, | |
display: 'flex', flex: 1, flexBasis: '50%', flexDirection: 'row', height: 90 | |
}; | |
return <> | |
<div className='card' style={cardStyle}> | |
<div className='card-body p-0 h-100' style={{ display: 'grid', gridTemplate: '1fr / 1fr', placeItems: 'center' }}> | |
<div style={{ gridColumn: '1/1', gridRow: '1/1', zIndex: 1, height: '100%', width: '100%' }}> | |
{otherKey === ourKey ? null : <> | |
<button className={'btn'} style={{ height: '100%', width: '50%' }} onClick={dec}>-</button> | |
<button className={'btn'} style={{ height: '100%', width: '50%' }} onClick={inc}>+</button> | |
</>} | |
</div> | |
<div style={{ gridColumn: '1/1', gridRow: '1/1', zIndex: 0, height: '80%' }}> | |
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', flexDirection: 'column' }}> | |
<h1>{ourKey === otherKey ? 'me' : adapter.value.commanderDamage[otherKey]}</h1> | |
</div> | |
</div> | |
</div> | |
</div> | |
</>; | |
} | |
export default App |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
bun i @uidotdev/usehooks