diff --git a/package.json b/package.json index a2fe5b6b..57062bc6 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@floating-ui/react": "^0.26.23", + "@mdi/js": "^7.4.47", + "@mdi/react": "^1.6.1", "@mui/base": "5.0.0-beta.4", "@playwright/test": "^1.35.0", "@prisma/client": "^4.15.0", diff --git a/src/components/box/GridBox.tsx b/src/components/box/GridBox.tsx index 89992b87..8e12dc39 100644 --- a/src/components/box/GridBox.tsx +++ b/src/components/box/GridBox.tsx @@ -21,15 +21,16 @@ export function GridBox({ columnGap, rowGap, gridTemplateColumns, + gap, ...rest }: PropsWithChildren) { return ( diff --git a/src/components/form/containers/FormSection.tsx b/src/components/form/containers/FormSection.tsx index fb4029ba..ca973ca2 100644 --- a/src/components/form/containers/FormSection.tsx +++ b/src/components/form/containers/FormSection.tsx @@ -1,6 +1,12 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { PropsWithChildren, useContext, useEffect, useState } from 'react'; +import { + CSSProperties, + PropsWithChildren, + useContext, + useEffect, + useState, +} from 'react'; import { RpgIcons } from '~/constants/icons'; import { Theme } from '~/constants/theme'; @@ -16,14 +22,19 @@ import { CollapseButton } from '../../buttons/CollapseButton'; import { RpgIcon } from '../../icons/RpgIcon'; import { Text } from '../../Text'; -interface FormSectionProps { +type BorderProperties = { + borderless?: boolean; + borderColor?: Color; + borderStyle?: CSSProperties['borderStyle']; +}; + +type FormSectionProps = { title: string; children: React.ReactNode | React.ReactNode[]; columns?: GridBoxProps['columns']; isCollapsible?: boolean; className?: string; visibilityTitle?: string; - borderless?: boolean; gridTemplateColumns?: GridBoxProps['gridTemplateColumns']; defaultExpanded?: boolean; icon?: RpgIcons; @@ -32,7 +43,7 @@ interface FormSectionProps { titleColor?: Color; onToggleOpen?: (nextOpenState: boolean) => void; linkId?: string; -} +} & BorderProperties; const TitleBox = styled(FlexBox)` position: relative; @@ -56,23 +67,42 @@ const Section = styled(FlexBox)` height: 100%; `; -const createCollapsibleStyles = (theme: Theme, borderless?: boolean) => css` - border-color: ${borderless ? 'transparent' : theme.colors.textAccent}; - border-width: ${borderless ? 0 : theme.borderWidth[1]}; - border-style: solid; -`; +const createCollapsibleStyles = ( + theme: Theme, + borderProperties?: BorderProperties +) => { + const { + borderless, + borderColor: pBorderColor, + borderStyle: pBorderStyle, + } = borderProperties || {}; + + const borderColor = theme.colors[pBorderColor || 'textAccent']; + const borderStyle = pBorderStyle || 'solid'; -const Line = styled(Box)` + return css` + border-color: ${borderless ? 'transparent' : borderColor}; + border-width: ${borderless ? 0 : theme.borderWidth[1]}; + border-style: ${borderStyle}; + `; +}; + +const Line = styled(Box)<{ borderProperties?: BorderProperties }>` height: 0; width: 100%; - ${({ theme }) => createCollapsibleStyles(theme)}; + ${({ theme, borderProperties }) => + createCollapsibleStyles(theme, borderProperties)}; border-bottom-width: 0; border-left-width: 0; border-right-width: 0; `; -const Container = styled(GridBox)<{ isOpen?: boolean; borderless?: boolean }>` - ${({ theme, borderless }) => createCollapsibleStyles(theme, borderless)}; +const Container = styled(GridBox)<{ + isOpen?: boolean; + borderProperties?: BorderProperties; +}>` + ${({ theme, borderProperties }) => + createCollapsibleStyles(theme, borderProperties)}; border-top-width: 0; height: 100%; visibility: ${({ isOpen }) => (isOpen ? 'visible' : 'collapse')}; @@ -125,6 +155,8 @@ export function FormSection({ titleColor, onToggleOpen, linkId, + borderColor, + borderStyle, }: FormSectionProps) { const { getSectionVisibilityInfo, setSectionVisibilityInfo } = useContext(VisibilityContext); @@ -152,10 +184,23 @@ export function FormSection({ }, [initIsExpanded]); // END - SECTION COLLAPSED STATUS - END + const borderProperties = + borderless || borderColor || borderStyle + ? { + borderless, + borderColor, + borderStyle, + } + : undefined; + return (
- + - {!borderless && } + {!borderless && } + // Focus trap on dialog adds two observer elements, but if those get caught in + // a flexbox with a gap or something, they shift the layout. This box makes sure + // that this whole button and dialog get treated as one element +
setIsConfirmModalOpen(false) }} confirm={{ onClick: onDelete, label: 'Delete', severity: 'danger' }} @@ -50,6 +53,6 @@ export function DeleteButton({ characterId, playerId }: DeleteButtonProps) { setIsConfirmModalOpen(true)}> - +
); } diff --git a/src/components/formNav/FormNavBaseButtons.tsx b/src/components/formNav/FormNavBaseButtons.tsx index 31cb0c5b..d778fdf5 100644 --- a/src/components/formNav/FormNavBaseButtons.tsx +++ b/src/components/formNav/FormNavBaseButtons.tsx @@ -1,4 +1,7 @@ import { useUser } from '@auth0/nextjs-auth0'; +import { useTheme } from '@emotion/react'; +import { mdiStarBoxMultiple } from '@mdi/js'; +import Icon from '@mdi/react'; import { useRouter } from 'next/router'; import { useContext } from 'react'; @@ -13,21 +16,29 @@ import { IconButton } from '../buttons/IconButton'; import { Pencil } from '../icons/Pencil'; import { SaveButton } from './SaveButton'; -interface NavButtonsProps { +export interface QuickAccessProps { + showQuickAccess: boolean; + setShowQuickAccess: (show: boolean) => void; +} + +type NavButtonProps = { isMyCharacter: boolean; rulebookName: RulebookType; characterName: string; -} + quickAccess?: QuickAccessProps; +}; export function FormNavBaseButtons({ isMyCharacter, rulebookName, characterName, -}: NavButtonsProps) { + quickAccess, +}: NavButtonProps) { const { user } = useUser(); const { isEditMode, setIsEditMode } = useContext(EditContext); const { query } = useRouter(); const isNew = useIsNewCharacter(); + const theme = useTheme(); return ( <> @@ -63,6 +74,27 @@ export function FormNavBaseButtons({ titleId="edit-pencil-icon" /> + {quickAccess && ( + + quickAccess.setShowQuickAccess(!quickAccess.showQuickAccess) + } + > + + + )} ); } diff --git a/src/components/meta/Layout.tsx b/src/components/meta/Layout.tsx index dd200ec9..ad9b9fda 100644 --- a/src/components/meta/Layout.tsx +++ b/src/components/meta/Layout.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { NavContext } from '~/logic/contexts/navContext'; import { useBreakpointsAtLeast } from '~/logic/hooks/useBreakpoints'; +import { pxToRem } from '~/logic/utils/styles/pxToRem'; import { FlexBox } from '../box/FlexBox'; import { DropdownMenuProps } from '../dropdowns/DropdownMenu'; @@ -19,9 +20,9 @@ const PageWrapper = styled(FlexBox)` max-width: ${({ theme }) => theme.breakpointValues.lg}px; width: 100%; height: 100%; - padding-top: ${({ theme }) => theme.spacing[80]}; + padding-top: ${pxToRem(120)}; ${({ theme }) => theme.breakpoints.xs} { - padding-top: ${({ theme }) => theme.spacing[128]}; + padding-top: ${pxToRem(140)}; } `; diff --git a/src/components/nav/NavBar.tsx b/src/components/nav/NavBar.tsx index 79a087f4..51b9d226 100644 --- a/src/components/nav/NavBar.tsx +++ b/src/components/nav/NavBar.tsx @@ -16,7 +16,6 @@ import { LogoAscii } from '../ascii/LogoAscii'; import { Box } from '../box/Box'; import { FlexBox } from '../box/FlexBox'; import { GridBox } from '../box/GridBox'; -import { Divider } from '../divider/Divider'; import { DropdownMenuProps } from '../dropdowns/DropdownMenu'; import { ProfileDropdown } from '../dropdowns/ProfileDropdown'; import { RpgIcon } from '../icons/RpgIcon'; @@ -77,12 +76,6 @@ const Portal = styled.div<{ flexGap: Spacing }>` const UserName = styled(Text)` white-space: nowrap; `; - -const VertDivider = styled(Divider)` - align-self: stretch; - height: unset; -`; - interface NavBarProps { title: string; setIconPortalNode: (node: HTMLDivElement) => void; @@ -97,15 +90,20 @@ export function NavBar({ dropdownMenuItems, }: NavBarProps) { const isXxs = useBreakpointsLessThan('xs'); - const atLeastMd = useBreakpointsAtLeast('md'); const flexGap = isXxs ? 8 : 16; const { user } = useUser(); const userName = getNameFromUser(user as StrictSessionUser); + const atLeastMd = useBreakpointsAtLeast('md'); return ( - + - + @@ -119,36 +117,35 @@ export function NavBar({ )} - {userName && atLeastMd && ( - <> - - + - - - - - - {userName} - - - - + + + + + {userName} + + + )} + + + ); } diff --git a/src/components/rulebookSpecific/sotww/CharacterSheet.tsx b/src/components/rulebookSpecific/sotww/CharacterSheet.tsx index 15f98a4c..ac006b4f 100644 --- a/src/components/rulebookSpecific/sotww/CharacterSheet.tsx +++ b/src/components/rulebookSpecific/sotww/CharacterSheet.tsx @@ -1,8 +1,9 @@ import { useUser } from '@auth0/nextjs-auth0'; import styled from '@emotion/styled'; import { useRouter } from 'next/router'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { FlexBox } from '~/components/box/FlexBox'; import { GridBox } from '~/components/box/GridBox'; import { Form as FormComponent } from '~/components/form/Form'; import { TabPanel } from '~/components/tabs/TabPanel'; @@ -35,6 +36,7 @@ import { MagicTraditionInputs } from './inputs/MagicInputs/MagicTraditionInputs' import { PathInputs } from './inputs/PathInputs/PathInputs'; import { PhysicalTraitsInputs } from './inputs/PhysicalTraitsInputs'; import { WeaponInputs } from './inputs/WeaponInputs'; +import { QuickAccess } from './QuickAccess'; const SotwwCharacterSheet = styled(FormComponent)` padding-bottom: ${({ theme }) => theme.spacing[48]}; @@ -77,6 +79,8 @@ const sharedGapProps = { }; export function CharacterSheet({ character }: SotwwCharacterSheetProps) { + const [showQuickAccess, setShowQuickAccess] = useState(false); + const { user } = useUser(); const { isEditMode, @@ -105,70 +109,87 @@ export function CharacterSheet({ character }: SotwwCharacterSheetProps) { onSubmit={() => undefined} > - - - router.replace({ - query: { - ...router.query, - tab: tabLabels[index].label.toLowerCase(), - }, - }) - } - > - {/* Description */} - - - - - - - - {/* Stats */} - - - - - - - - - {/* Abilities */} - - - - - - - {/* Combat */} - - + + + {showQuickAccess && } + + router.replace( + { + query: { + ...router.query, + tab: tabLabels[index].label.toLowerCase(), + }, + }, + undefined, + { scroll: !showQuickAccess } + ) + } + > + {/* Description */} + + + + + + + + {/* Stats */} + - - + + + + + + + {/* Abilities */} + + + + + + + {/* Combat */} + + + + + + + + + + + + {/* Magic */} + + + + + + + {/* Inventory */} + + + + - - - - - - {/* Magic */} - - - - - - - {/* Inventory */} - - - - - - - + + + diff --git a/src/components/rulebookSpecific/sotww/FormNav.tsx b/src/components/rulebookSpecific/sotww/FormNav.tsx index 8e57dcbb..a9aa654f 100644 --- a/src/components/rulebookSpecific/sotww/FormNav.tsx +++ b/src/components/rulebookSpecific/sotww/FormNav.tsx @@ -6,7 +6,10 @@ import { useFormContext } from 'react-hook-form'; import { FlexBox } from '~/components/box/FlexBox'; import { BaseButton } from '~/components/buttons/BaseButton'; -import { FormNavBaseButtons } from '~/components/formNav/FormNavBaseButtons'; +import { + FormNavBaseButtons, + QuickAccessProps, +} from '~/components/formNav/FormNavBaseButtons'; import { Text } from '~/components/Text'; import { NavContext } from '~/logic/contexts/navContext'; import { useBreakpointsAtLeast } from '~/logic/hooks/useBreakpoints'; @@ -16,6 +19,7 @@ import { SotwwCharacterData } from '~/typings/sotww/characterData'; interface FormNavProps { isMyCharacter: boolean; + quickAccess?: QuickAccessProps; } interface CharacterHeaderProps { @@ -83,7 +87,7 @@ function CharacterHeader({ headerPortalNode, name }: CharacterHeaderProps) { ); } -export function FormNav({ isMyCharacter }: FormNavProps) { +export function FormNav({ isMyCharacter, quickAccess }: FormNavProps) { const { watch } = useFormContext(); const name = watch('name'); @@ -103,6 +107,7 @@ export function FormNav({ isMyCharacter }: FormNavProps) { , iconPortalNode diff --git a/src/components/rulebookSpecific/sotww/QuickAccess.tsx b/src/components/rulebookSpecific/sotww/QuickAccess.tsx new file mode 100644 index 00000000..919ffe4e --- /dev/null +++ b/src/components/rulebookSpecific/sotww/QuickAccess.tsx @@ -0,0 +1,84 @@ +import styled from '@emotion/styled'; +import { trim } from 'lodash'; +import { useContext } from 'react'; +import { useFormContext } from 'react-hook-form'; + +import { FlexBox } from '~/components/box/FlexBox'; +import { GridBox } from '~/components/box/GridBox'; +import { FormSection } from '~/components/form/containers/FormSection'; +import { Text } from '~/components/Text'; +import { SotwwCharacterData } from '~/typings/sotww/characterData'; + +import { DefenseContext } from './DefenseProvider'; + +const LongEffect = styled(Text)` + white-space: pre-line; +`; + +export function QuickAccess() { + const { watch } = useFormContext(); + const { totalDefense } = useContext(DefenseContext); + + const equippedWeapon = watch('weapons').find((w) => w.weapon_equipped); + const weaponText = equippedWeapon + ? `${equippedWeapon.weapon_name} ${equippedWeapon.weapon_damage}` + : 'Unarmed 1d6'; + const equippedArmor = watch('armors').filter((a) => a.armor_equipped); + const equippedArmorText = equippedArmor.map((a) => a.armor_name).join(', '); + const damage = watch('damage'); + const health = watch('health_current'); + const conditions = watch('conditions'); + const boonBane = watch('boons_and_banes'); + + return ( + + + + + Damage:{' '} + + {damage}/{health} + + + + Weapon: {weaponText} + + + Defense:{' '} + + {totalDefense} ({equippedArmorText || 'Natural Defense'}) + + + + + + {trim(boonBane) && ( + + Boons/Banes: + + {boonBane} + + + )} + {trim(conditions) && ( + + Conditions: + + {conditions} + + + )} + + + + ); +} diff --git a/src/components/rulebookSpecific/sotww/inputs/ArmorInputs/ArmorInputItem.tsx b/src/components/rulebookSpecific/sotww/inputs/ArmorInputs/ArmorInputItem.tsx index e112de31..d7b7fa4d 100644 --- a/src/components/rulebookSpecific/sotww/inputs/ArmorInputs/ArmorInputItem.tsx +++ b/src/components/rulebookSpecific/sotww/inputs/ArmorInputs/ArmorInputItem.tsx @@ -28,7 +28,8 @@ export function ArmorInputItem({ onDelete, }: ArmorInputItemProps) { const { isEditMode } = useContext(EditContext); - const { recalculateDefense, highestArmorId } = useContext(DefenseContext); + const { recalculateDefense, highestArmorId, totalDefense } = + useContext(DefenseContext); const { watch, register, setValue } = useFormContext(); const armorIdFieldName = createArmorFieldName('armor_id', index); @@ -70,7 +71,7 @@ export function ArmorInputItem({ }; if (highestArmorId) { - if (armorId !== highestArmorId) { + if (armorId !== highestArmorId && armorScore < totalDefense) { relevantArmorBonus = { type: 'bonus', value: armorBonus, diff --git a/src/pages/api/characters/[id]/index.ts b/src/pages/api/characters/[id]/index.ts index 273c58e9..d4755976 100644 --- a/src/pages/api/characters/[id]/index.ts +++ b/src/pages/api/characters/[id]/index.ts @@ -121,12 +121,6 @@ const handleRequest: NextApiHandler = async (req, res) => { default: returnErrorResponse(res, new Error(ErrorTypes.SomethingWentWrong)); } - - if (method === 'PATCH') { - await patchCharacter(req, res); - } else { - await getCharacter(req, res); - } }; export default handleRequest; diff --git a/yarn.lock b/yarn.lock index fdd400f1..598e3118 100644 --- a/yarn.lock +++ b/yarn.lock @@ -877,6 +877,18 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@mdi/js@^7.4.47": + version "7.4.47" + resolved "https://registry.yarnpkg.com/@mdi/js/-/js-7.4.47.tgz#7d8a4edc9631bffeed80d1ec784f9beae559a76a" + integrity sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ== + +"@mdi/react@^1.6.1": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@mdi/react/-/react-1.6.1.tgz#624313593ae8065d2a09878ca81beb3e4b676b03" + integrity sha512-4qZeDcluDFGFTWkHs86VOlHkm6gnKaMql13/gpIcUQ8kzxHgpj31NuCkD8abECVfbULJ3shc7Yt4HJ6Wu6SN4w== + dependencies: + prop-types "^15.7.2" + "@mui/base@5.0.0-beta.4": version "5.0.0-beta.4" resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.4.tgz#e3f4f4a056b88ab357194a245e223177ce35e0b0"