From 399d19deab390f5ddef2e520e2a1990515f0f530 Mon Sep 17 00:00:00 2001 From: Isaac Priestley Date: Tue, 27 Jan 2026 14:06:05 -0500 Subject: [PATCH 1/2] Add expiry UI for character effects - Add end_sequence and end_shot fields to CharacterEffect type - Add expiry dropdowns to AddEffectModal for setting when effects expire - Display expiry info in CharacterEffectsDisplay popover --- .../encounters/effects/AddEffectModal.tsx | 82 ++++++++++++++++++- .../effects/CharacterEffectsDisplay.tsx | 24 ++++++ src/types/resources.ts | 2 + 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/components/encounters/effects/AddEffectModal.tsx b/src/components/encounters/effects/AddEffectModal.tsx index 6b4c2219..3d48fb59 100644 --- a/src/components/encounters/effects/AddEffectModal.tsx +++ b/src/components/encounters/effects/AddEffectModal.tsx @@ -11,6 +11,7 @@ import { Stack, Box, IconButton, + Typography, } from "@mui/material" import CircleIcon from "@mui/icons-material/Circle" import { useState } from "react" @@ -32,6 +33,12 @@ const severityOptions: { value: Severity; label: string }[] = [ { value: "success", label: "Success (Green)" }, ] +// Generate shot options (18 down to 0) +const shotOptions = Array.from({ length: 19 }, (_, i) => 18 - i) + +// Generate sequence options (1 to 10) +const sequenceOptions = Array.from({ length: 10 }, (_, i) => i + 1) + export default function AddEffectModal({ open, onClose, @@ -49,6 +56,8 @@ export default function AddEffectModal({ change: "", character_id: character.id, shot_id: character.shot_id, + end_sequence: null, + end_shot: null, }) // Define available action values based on character type @@ -72,10 +81,19 @@ export default function AddEffectModal({ const handleChange = (field: keyof CharacterEffect) => (event: React.ChangeEvent) => { - setEffect(prev => ({ - ...prev, - [field]: event.target.value, - })) + const value = event.target.value + // Handle numeric fields that can be null + if (field === "end_sequence" || field === "end_shot") { + setEffect(prev => ({ + ...prev, + [field]: value === "" ? null : Number(value), + })) + } else { + setEffect(prev => ({ + ...prev, + [field]: value, + })) + } } const handleSubmit = async () => { @@ -113,6 +131,8 @@ export default function AddEffectModal({ change: "", character_id: character.id, shot_id: character.shot_id, + end_sequence: null, + end_shot: null, }) onClose() } @@ -212,6 +232,60 @@ export default function AddEffectModal({ /> + + + + Expiry (Optional) + + + Leave empty for permanent effect. Effect expires when the shot + counter passes the specified shot. + + + { + const val = e.target.value + setEffect(prev => ({ + ...prev, + end_sequence: val === "" ? null : Number(val), + })) + }} + fullWidth + > + None + {sequenceOptions.map(seq => ( + + Sequence {seq} + + ))} + + { + const val = e.target.value + setEffect(prev => ({ + ...prev, + end_shot: val === "" ? null : Number(val), + })) + }} + fullWidth + > + None + {shotOptions.map(shot => ( + + Shot {shot} + + ))} + + + diff --git a/src/components/encounters/effects/CharacterEffectsDisplay.tsx b/src/components/encounters/effects/CharacterEffectsDisplay.tsx index 564c047d..66fda048 100644 --- a/src/components/encounters/effects/CharacterEffectsDisplay.tsx +++ b/src/components/encounters/effects/CharacterEffectsDisplay.tsx @@ -145,6 +145,21 @@ export default function CharacterEffectsDisplay({ return `${actionValueLabel(effect)} = ${effect.change}` } + const formatExpiry = (effect: CharacterEffect) => { + if (effect.end_sequence == null && effect.end_shot == null) { + return null + } + + const parts: string[] = [] + if (effect.end_sequence != null) { + parts.push(`Seq ${effect.end_sequence}`) + } + if (effect.end_shot != null) { + parts.push(`Shot ${effect.end_shot}`) + } + return `Expires: ${parts.join(", ")}` + } + const severityOrder: Severity[] = ["error", "warning", "info", "success"] // Cleanup timeouts on unmount @@ -251,6 +266,15 @@ export default function CharacterEffectsDisplay({ )} + {formatExpiry(effect) && ( + + {formatExpiry(effect)} + + )} {isGamemaster && ( Date: Tue, 27 Jan 2026 14:22:55 -0500 Subject: [PATCH 2/2] Address PR review comments - Replace dropdown selectors with numeric inputs for expiry fields - Use handleChange for consistent onChange logic - Cache formatExpiry result to avoid calling twice --- .../encounters/effects/AddEffectModal.tsx | 48 ++------- .../effects/CharacterEffectsDisplay.tsx | 97 ++++++++++--------- 2 files changed, 60 insertions(+), 85 deletions(-) diff --git a/src/components/encounters/effects/AddEffectModal.tsx b/src/components/encounters/effects/AddEffectModal.tsx index 3d48fb59..59a936fd 100644 --- a/src/components/encounters/effects/AddEffectModal.tsx +++ b/src/components/encounters/effects/AddEffectModal.tsx @@ -33,12 +33,6 @@ const severityOptions: { value: Severity; label: string }[] = [ { value: "success", label: "Success (Green)" }, ] -// Generate shot options (18 down to 0) -const shotOptions = Array.from({ length: 19 }, (_, i) => 18 - i) - -// Generate sequence options (1 to 10) -const sequenceOptions = Array.from({ length: 10 }, (_, i) => i + 1) - export default function AddEffectModal({ open, onClose, @@ -243,47 +237,25 @@ export default function AddEffectModal({ { - const val = e.target.value - setEffect(prev => ({ - ...prev, - end_sequence: val === "" ? null : Number(val), - })) - }} + onChange={handleChange("end_sequence")} fullWidth - > - None - {sequenceOptions.map(seq => ( - - Sequence {seq} - - ))} - + inputProps={{ min: 1 }} + helperText="Sequence number (1, 2, 3...)" + /> { - const val = e.target.value - setEffect(prev => ({ - ...prev, - end_shot: val === "" ? null : Number(val), - })) - }} + onChange={handleChange("end_shot")} fullWidth - > - None - {shotOptions.map(shot => ( - - Shot {shot} - - ))} - + inputProps={{ min: 0, max: 30 }} + helperText="Shot value (0-30)" + /> diff --git a/src/components/encounters/effects/CharacterEffectsDisplay.tsx b/src/components/encounters/effects/CharacterEffectsDisplay.tsx index 66fda048..dc765816 100644 --- a/src/components/encounters/effects/CharacterEffectsDisplay.tsx +++ b/src/components/encounters/effects/CharacterEffectsDisplay.tsx @@ -234,59 +234,62 @@ export default function CharacterEffectsDisplay({ onMouseEnter={handlePopoverMouseEnter} onMouseLeave={handlePopoverClose} > - {groupedEffects[openSeverity].map((effect, index) => ( - - - - - {effect.name} - {effect.action_value && effect.change && ( + {groupedEffects[openSeverity].map((effect, index) => { + const expiry = formatExpiry(effect) + return ( + + + + + {effect.name} + {effect.action_value && effect.change && ( + + ({formatChange(effect)}) + + )} + + {expiry && ( - ({formatChange(effect)}) + {expiry} )} - - {formatExpiry(effect) && ( - + {isGamemaster && ( + handleDeleteEffect(effect)} + sx={{ padding: 0.25 }} > - {formatExpiry(effect)} - + + )} - - {isGamemaster && ( - handleDeleteEffect(effect)} - sx={{ padding: 0.25 }} - > - - - )} - - ))} + + ) + })} )}