diff --git a/src/components/adventure-page-layout.js b/src/components/adventure-page-layout.js index 44523ce..3fa6f3a 100644 --- a/src/components/adventure-page-layout.js +++ b/src/components/adventure-page-layout.js @@ -1,23 +1,21 @@ import React from 'react'; import { graphql } from 'gatsby'; -import { - Box, - Card, - Divider, - List, - ListItem, - ListItemText, - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TablePagination, - TableRow, - TableSortLabel, - Typography, -} from '@mui/material'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Divider from '@mui/material/Divider'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TablePagination from '@mui/material/TablePagination'; +import TableRow from '@mui/material/TableRow'; +import TableSortLabel from '@mui/material/TableSortLabel'; +import Typography from '@mui/material/Typography'; import { Link, ListItemButton } from 'gatsby-theme-material-ui'; import { visuallyHidden } from '@mui/utils'; import { Feed, LocationOn, EmojiPeople } from '@mui/icons-material/'; @@ -29,7 +27,7 @@ import { function AdventurePageLayout({ data }) { return ( - + 0) { + parsedSessionData.sort((a, b) => (a.initiative < b.initiative ? 1 : -1)); + sessionStorage.setItem(ssKey, JSON.stringify(parsedSessionData)); + } + // PARSED data: + const [arenaSessionStorage, setArenaSessionStorage] = React.useState(parsedSessionData); + + // Drawer State: + const [arenaDrawerOpen, setArenaDrawerOpen] = React.useState(false); + + // For manual entry, Player Name: + const [playerName, setPlayerName] = React.useState(''); + + // Create ListItems + const [combatantListItems, setCombatantListItems] = React.useState([]); + + // State for new player name validation. + const [error, setError] = React.useState(false); + // Skull input adornment. + const deathAdornment = ( + + + + + + ); + // Keyboard shortcut for Drawer. + React.useEffect(() => { + const ctrlC = (e) => { + if (e.ctrlKey && e.key === 'c') { + setArenaDrawerOpen(!arenaDrawerOpen); + } + }; + window.addEventListener('keyup', ctrlC); + return () => window.removeEventListener('keyup', ctrlC); + }, [arenaDrawerOpen]); + // Turn indicator. + const [turnIndex, setTurnIndex] = React.useState(0); + const handleTurnClick = (direction) => { + if (direction === 'next') { + if (turnIndex === (arenaSessionStorage.length - 1)) { + setTurnIndex(0); + return; + } + setTurnIndex(turnIndex + 1); + return; + } + if (direction === 'previous') { + if (turnIndex === 0) { + setTurnIndex(arenaSessionStorage.length - 1); + return; + } + setTurnIndex(turnIndex - 1); + } + }; + // Submit new initiative by making a shallow copy of data, manipulating it, and then + // submiting the new data. + const initiativeSubmit = (value, index) => { + const arenaCopy = [...arenaSessionStorage]; + const arenaCopyItem = { ...arenaCopy[index] }; + arenaCopyItem.initiative = Number.isNaN(Number(parseInt(value, 10))) + ? 0 : Number(parseInt(value, 10)); + arenaCopy[index] = arenaCopyItem; + setArenaSessionStorage(arenaCopy); + }; + // Submit new HP value like above. + const hpSubmit = (value, current, index) => { + const arenaCopy = [...arenaSessionStorage]; + const arenaCopyItem = { ...arenaCopy[index] }; + console.log(`${arenaCopyItem.name} ${value.includes('+') ? 'receives' : 'loses'} ${value} HP`); + const hpSanitized = Number.isNaN(Number(parseInt(value, 10))) ? 0 : Number(parseInt(value, 10)); + arenaCopyItem.hp = current + hpSanitized; + arenaCopy[index] = arenaCopyItem; + setArenaSessionStorage(arenaCopy); + }; + + // Use this to check for duplicates. + const duplicatesArray = []; + + const combatantListMaker = (array) => array + // Sort by initiative and then by name. + .sort( + (a, b) => ( + (a.initiative < b.initiative) ? 1 : (a.initiative === b.initiative) ? ( + (a.name.toUpperCase() > b.name.toUpperCase()) ? 1 : -1 + ) : -1 + ), + ) + .map((item, index) => { + duplicatesArray.push(item.name); + const dupes = (name) => { + let count = 0; + duplicatesArray.forEach((element, i) => { + if (duplicatesArray[i] === name) { + count += 1; + } + }); + return count; + }; + return ( + + {index === turnIndex && } + {/* Initiative value */} + event.target.select()} + onKeyDown={(event) => { + if (event.key === 'Enter') { + initiativeSubmit(event.target.value, index); + } + }} + sx={{ '& input': { width: '18px' } }} + /> + {/* Combatant's name */} + 1 && ( + + # + {dupes(item.name)} + + )} + primaryTypographyProps={{ variant: 'body2' }} + secondaryTypographyProps={{ component: 'span', sx: { ml: 1, lineHeight: '1.75' } }} + sx={{ display: 'flex', flexDirection: 'row', maxWidth: '15rem' }} + /> + {/* HP value */} + 0 ? '' : deathAdornment, inputMode: 'numeric', pattern: '[0-9.+-]*' }} + defaultValue={item.hp} + onFocus={(event) => event.target.select()} + onKeyDown={(event) => { + if (event.key === 'Enter') { + hpSubmit(event.target.value, item.hp, index); + } + }} + sx={{ '& .gmcm-InputBase-adornedEnd': { pr: 1 }, '& input': { width: '28px' } }} + /> + + { + setArenaSessionStorage( + [...arenaSessionStorage].filter((x) => x !== arenaSessionStorage[index]), + ); + }} + > + + + + + ); + }); + + // IF arenaSessionStorage value is altered, reset the Session Storage data: + React.useEffect(() => { + console.log('arenaSessionStorage useEffect'); + console.log(`turnIndex: ${turnIndex}`); + // Resort the data and recreate the list. + setCombatantListItems(combatantListMaker(arenaSessionStorage)); + // Stringify and resend the data to Session Storage. + sessionStorage.setItem(ssKey, JSON.stringify(arenaSessionStorage)); + }, [arenaSessionStorage, turnIndex]); + + // Clear button stuff. + const options = ['Clear monsters', 'Clear players', 'Clear all combatants']; + const anchorRef = React.useRef(null); + const combatantListRef = React.useRef(null); + const [selectedIndex, setSelectedIndex] = React.useState(1); + const [clearOpen, setClearOpen] = React.useState(false); + const handleClearClick = () => { + const clicked = options[selectedIndex]; + const { children } = combatantListRef.current; + const found = []; + const finder = (type) => { + [...children].forEach((item, index) => { + if (item.classList.contains(`gmcm-Combatant-${type}`)) { + found.push(index); + } + }); + }; + const remover = () => { + let arenaCopy = [...arenaSessionStorage]; + arenaCopy = arenaCopy.filter((value, index) => found.indexOf(index) === -1); + setArenaSessionStorage(arenaCopy); + }; + if (clicked === 'Clear all combatants') { + sessionStorage.clear(); + setArenaSessionStorage([]); + } else { + if (clicked === 'Clear monsters') { + finder('monster'); + } + if (clicked === 'Clear players') { + finder('player'); + } + remover(); + } + }; + const handleToggle = () => { + setClearOpen((prevOpen) => !prevOpen); + }; + const handleClearClose = (event) => { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + setClearOpen(false); + }; + const handleMenuItemClick = (event, index) => { + setSelectedIndex(index); + setClearOpen(false); + }; + + return { + arenaSessionStorage, + setArenaSessionStorage, + arenaDrawerOpen, + setArenaDrawerOpen, + arenaRender: ( + <> + setArenaDrawerOpen(false)}> + {/* Add player form */} + { + event.preventDefault(); + if (playerName === '') { + setError(true); + return; + } + setArenaSessionStorage( + [...arenaSessionStorage, { + name: playerName, initiative: 0, hp: 0, type: 'player', + }], + ); + setPlayerName(''); + }} + + > + + + + ), + }} + onChange={(event) => { + setError(false); + setPlayerName( + event.target.value.charAt(0).toUpperCase() + event.target.value.slice(1), + ); + }} + /> + + {/* Turn advancement */} + + + Turn Direction + + + {/* Combatant List */} + {combatantListItems.length > 0 && ( + + + li + li': { mt: 1 } }}>{combatantListItems} + + + )} + {/* Clear Button Group */} + + + + + + {({ TransitionProps, placement }) => ( + + + + + {options.map((option, index) => ( + handleMenuItemClick(event, index)} + > + {option} + + ))} + + + + + )} + + {/* Hotkey notice */} + + Press CTRL+C to toggle the Combat Drawer. + + + setArenaDrawerOpen(true)}> + + + + + + ), + }; +} + +export default useArena; diff --git a/src/components/layout.js b/src/components/layout.js index 85f52ff..1fa69fb 100644 --- a/src/components/layout.js +++ b/src/components/layout.js @@ -15,7 +15,11 @@ import GmcmBlackBridgeIcon from '../images/black-bridge.svg'; const HeaderContainer = (props) => ; function Layout({ - children, hideNavigation, title, navDirection, + children, + hideNavigation, + title, + navDirection, + arenaRender = undefined, }) { return ( <> @@ -34,6 +38,7 @@ function Layout({ '& thead tr': { backgroundColor: 'primary.main', }, + // for '& .SnackbarContainer-root': { gap: 2, }, @@ -44,7 +49,6 @@ function Layout({ sx={{ backgroundColor: 'primary.main', minHeight: '4.25rem', - // display: 'block', position: 'static', }} > @@ -87,7 +91,10 @@ function Layout({ - + + {arenaRender} {title && title !== 'Home' && ( {FOOTER_COPY} {SITE_AUTHOR} + {', '} + Legal diff --git a/src/components/location-page-layout.js b/src/components/location-page-layout.js index c215a2a..73629cb 100644 --- a/src/components/location-page-layout.js +++ b/src/components/location-page-layout.js @@ -1,21 +1,19 @@ import React, { useEffect, useState } from 'react'; import { graphql } from 'gatsby'; import { GatsbyImage, getImage } from 'gatsby-plugin-image'; -import { - Alert, - Box, - ButtonGroup, - Card, - CardContent, - CardHeader, - Divider, - List, - ListItem, - Paper, - Stack, - SvgIcon, - Tooltip, -} from '@mui/material'; +import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Divider from '@mui/material/Divider'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import Paper from '@mui/material/Paper'; +import Stack from '@mui/material/Stack'; +import SvgIcon from '@mui/material/SvgIcon'; +import Tooltip from '@mui/material/Tooltip'; import { MDXRenderer } from 'gatsby-plugin-mdx'; import { Link, @@ -60,7 +58,7 @@ function LocationPageLayout({ data, location }) { const parentAdventureTitle = location.state ? location.state.parentAdventureTitle : ''; const parentAdventureSlug = location.state ? location.state.parentAdventureSlug : ''; return ( - + {parentAdventureTitle && ( {NAVIGATION_DATA.map((item) => ( - +