Skip to content

Instantly share code, notes, and snippets.

@alexanderankin
Created May 12, 2024 18:48
Show Gist options
  • Save alexanderankin/abaf028a45ea453d44c15f6826825b65 to your computer and use it in GitHub Desktop.
Save alexanderankin/abaf028a45ea453d44c15f6826825b65 to your computer and use it in GitHub Desktop.
mtg counters
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
@alexanderankin
Copy link
Author

dialog::backdrop {
  background: rgba(120, 12, 12, 0.51);
}

@alexanderankin
Copy link
Author

bun i @uidotdev/usehooks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment