Use Readonly props and optional chaining/nullish coalescing
Mark component props as Readonly<> across 15 component files and simplify edit-player-character field access with optional chaining and nullish coalescing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { type RefObject, useDeferredValue, useState } from "react";
|
import React, { type RefObject, useDeferredValue, useState } from "react";
|
||||||
import type { SearchResult } from "../hooks/use-bestiary.js";
|
import type { SearchResult } from "../hooks/use-bestiary.js";
|
||||||
import { cn } from "../lib/utils.js";
|
import { cn } from "../lib/utils.js";
|
||||||
import { D20Icon } from "./d20-icon.js";
|
import { D20Icon } from "./d20-icon.js";
|
||||||
@@ -62,7 +62,7 @@ function AddModeSuggestions({
|
|||||||
onConfirmQueued,
|
onConfirmQueued,
|
||||||
onAddFromPlayerCharacter,
|
onAddFromPlayerCharacter,
|
||||||
onClear,
|
onClear,
|
||||||
}: {
|
}: Readonly<{
|
||||||
nameInput: string;
|
nameInput: string;
|
||||||
suggestions: SearchResult[];
|
suggestions: SearchResult[];
|
||||||
pcMatches: PlayerCharacter[];
|
pcMatches: PlayerCharacter[];
|
||||||
@@ -75,7 +75,7 @@ function AddModeSuggestions({
|
|||||||
onSetQueued: (q: QueuedCreature | null) => void;
|
onSetQueued: (q: QueuedCreature | null) => void;
|
||||||
onConfirmQueued: () => void;
|
onConfirmQueued: () => void;
|
||||||
onAddFromPlayerCharacter?: (pc: PlayerCharacter) => void;
|
onAddFromPlayerCharacter?: (pc: PlayerCharacter) => void;
|
||||||
}) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-full z-50 mb-1 w-full max-w-xs rounded-md border border-border bg-card shadow-lg">
|
<div className="absolute bottom-full z-50 mb-1 w-full max-w-xs rounded-md border border-border bg-card shadow-lg">
|
||||||
<button
|
<button
|
||||||
@@ -263,7 +263,7 @@ export function ActionBar({
|
|||||||
rollAllInitiativeDisabled,
|
rollAllInitiativeDisabled,
|
||||||
onOpenSourceManager,
|
onOpenSourceManager,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
}: ActionBarProps) {
|
}: Readonly<ActionBarProps>) {
|
||||||
const [nameInput, setNameInput] = useState("");
|
const [nameInput, setNameInput] = useState("");
|
||||||
const [suggestions, setSuggestions] = useState<SearchResult[]>([]);
|
const [suggestions, setSuggestions] = useState<SearchResult[]>([]);
|
||||||
const [pcMatches, setPcMatches] = useState<PlayerCharacter[]>([]);
|
const [pcMatches, setPcMatches] = useState<PlayerCharacter[]>([]);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function BulkImportPrompt({
|
|||||||
importState,
|
importState,
|
||||||
onStartImport,
|
onStartImport,
|
||||||
onDone,
|
onDone,
|
||||||
}: BulkImportPromptProps) {
|
}: Readonly<BulkImportPromptProps>) {
|
||||||
const [baseUrl, setBaseUrl] = useState(DEFAULT_BASE_URL);
|
const [baseUrl, setBaseUrl] = useState(DEFAULT_BASE_URL);
|
||||||
const baseUrlId = useId();
|
const baseUrlId = useId();
|
||||||
const totalSources = getAllSourceCodes().length;
|
const totalSources = getAllSourceCodes().length;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function BulkImportToasts({
|
|||||||
state,
|
state,
|
||||||
visible,
|
visible,
|
||||||
onReset,
|
onReset,
|
||||||
}: BulkImportToastsProps) {
|
}: Readonly<BulkImportToastsProps>) {
|
||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
|
|
||||||
if (state.status === "loading") {
|
if (state.status === "loading") {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface ColorPaletteProps {
|
|||||||
|
|
||||||
const COLORS = [...VALID_PLAYER_COLORS] as string[];
|
const COLORS = [...VALID_PLAYER_COLORS] as string[];
|
||||||
|
|
||||||
export function ColorPalette({ value, onChange }: ColorPaletteProps) {
|
export function ColorPalette({ value, onChange }: Readonly<ColorPaletteProps>) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{COLORS.map((color) => (
|
{COLORS.map((color) => (
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ function EditableName({
|
|||||||
onRename,
|
onRename,
|
||||||
onShowStatBlock,
|
onShowStatBlock,
|
||||||
color,
|
color,
|
||||||
}: {
|
}: Readonly<{
|
||||||
name: string;
|
name: string;
|
||||||
combatantId: CombatantId;
|
combatantId: CombatantId;
|
||||||
onRename: (id: CombatantId, newName: string) => void;
|
onRename: (id: CombatantId, newName: string) => void;
|
||||||
onShowStatBlock?: () => void;
|
onShowStatBlock?: () => void;
|
||||||
color?: string;
|
color?: string;
|
||||||
}) {
|
}>) {
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [draft, setDraft] = useState(name);
|
const [draft, setDraft] = useState(name);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -154,10 +154,10 @@ function EditableName({
|
|||||||
function MaxHpDisplay({
|
function MaxHpDisplay({
|
||||||
maxHp,
|
maxHp,
|
||||||
onCommit,
|
onCommit,
|
||||||
}: {
|
}: Readonly<{
|
||||||
maxHp: number | undefined;
|
maxHp: number | undefined;
|
||||||
onCommit: (value: number | undefined) => void;
|
onCommit: (value: number | undefined) => void;
|
||||||
}) {
|
}>) {
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [draft, setDraft] = useState(maxHp?.toString() ?? "");
|
const [draft, setDraft] = useState(maxHp?.toString() ?? "");
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -215,12 +215,12 @@ function ClickableHp({
|
|||||||
maxHp,
|
maxHp,
|
||||||
onAdjust,
|
onAdjust,
|
||||||
dimmed,
|
dimmed,
|
||||||
}: {
|
}: Readonly<{
|
||||||
currentHp: number | undefined;
|
currentHp: number | undefined;
|
||||||
maxHp: number | undefined;
|
maxHp: number | undefined;
|
||||||
onAdjust: (delta: number) => void;
|
onAdjust: (delta: number) => void;
|
||||||
dimmed?: boolean;
|
dimmed?: boolean;
|
||||||
}) {
|
}>) {
|
||||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
const status = deriveHpStatus(currentHp, maxHp);
|
const status = deriveHpStatus(currentHp, maxHp);
|
||||||
|
|
||||||
@@ -265,10 +265,10 @@ function ClickableHp({
|
|||||||
function AcDisplay({
|
function AcDisplay({
|
||||||
ac,
|
ac,
|
||||||
onCommit,
|
onCommit,
|
||||||
}: {
|
}: Readonly<{
|
||||||
ac: number | undefined;
|
ac: number | undefined;
|
||||||
onCommit: (value: number | undefined) => void;
|
onCommit: (value: number | undefined) => void;
|
||||||
}) {
|
}>) {
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [draft, setDraft] = useState(ac?.toString() ?? "");
|
const [draft, setDraft] = useState(ac?.toString() ?? "");
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -319,13 +319,13 @@ function InitiativeDisplay({
|
|||||||
dimmed,
|
dimmed,
|
||||||
onSetInitiative,
|
onSetInitiative,
|
||||||
onRollInitiative,
|
onRollInitiative,
|
||||||
}: {
|
}: Readonly<{
|
||||||
initiative: number | undefined;
|
initiative: number | undefined;
|
||||||
combatantId: CombatantId;
|
combatantId: CombatantId;
|
||||||
dimmed: boolean;
|
dimmed: boolean;
|
||||||
onSetInitiative: (id: CombatantId, value: number | undefined) => void;
|
onSetInitiative: (id: CombatantId, value: number | undefined) => void;
|
||||||
onRollInitiative?: (id: CombatantId) => void;
|
onRollInitiative?: (id: CombatantId) => void;
|
||||||
}) {
|
}>) {
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [draft, setDraft] = useState(initiative?.toString() ?? "");
|
const [draft, setDraft] = useState(initiative?.toString() ?? "");
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function ConditionPicker({
|
|||||||
activeConditions,
|
activeConditions,
|
||||||
onToggle,
|
onToggle,
|
||||||
onClose,
|
onClose,
|
||||||
}: ConditionPickerProps) {
|
}: Readonly<ConditionPickerProps>) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [flipped, setFlipped] = useState(false);
|
const [flipped, setFlipped] = useState(false);
|
||||||
const [maxHeight, setMaxHeight] = useState<number | undefined>(undefined);
|
const [maxHeight, setMaxHeight] = useState<number | undefined>(undefined);
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function ConditionTags({
|
|||||||
conditions,
|
conditions,
|
||||||
onRemove,
|
onRemove,
|
||||||
onOpenPicker,
|
onOpenPicker,
|
||||||
}: ConditionTagsProps) {
|
}: Readonly<ConditionTagsProps>) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-0.5">
|
<div className="flex flex-wrap items-center gap-0.5">
|
||||||
{conditions?.map((condId) => {
|
{conditions?.map((condId) => {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function CreatePlayerModal({
|
|||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
playerCharacter,
|
playerCharacter,
|
||||||
}: CreatePlayerModalProps) {
|
}: Readonly<CreatePlayerModalProps>) {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [ac, setAc] = useState("10");
|
const [ac, setAc] = useState("10");
|
||||||
const [maxHp, setMaxHp] = useState("10");
|
const [maxHp, setMaxHp] = useState("10");
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface IconGridProps {
|
|||||||
|
|
||||||
const ICONS = [...VALID_PLAYER_ICONS] as PlayerIcon[];
|
const ICONS = [...VALID_PLAYER_ICONS] as PlayerIcon[];
|
||||||
|
|
||||||
export function IconGrid({ value, onChange }: IconGridProps) {
|
export function IconGrid({ value, onChange }: Readonly<IconGridProps>) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{ICONS.map((iconId) => {
|
{ICONS.map((iconId) => {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function PlayerManagement({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onCreate,
|
onCreate,
|
||||||
}: PlayerManagementProps) {
|
}: Readonly<PlayerManagementProps>) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function SourceFetchPrompt({
|
|||||||
fetchAndCacheSource,
|
fetchAndCacheSource,
|
||||||
onSourceLoaded,
|
onSourceLoaded,
|
||||||
onUploadSource,
|
onUploadSource,
|
||||||
}: SourceFetchPromptProps) {
|
}: Readonly<SourceFetchPromptProps>) {
|
||||||
const [url, setUrl] = useState(() => getDefaultFetchUrl(sourceCode));
|
const [url, setUrl] = useState(() => getDefaultFetchUrl(sourceCode));
|
||||||
const [status, setStatus] = useState<"idle" | "fetching" | "error">("idle");
|
const [status, setStatus] = useState<"idle" | "fetching" | "error">("idle");
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ interface SourceManagerProps {
|
|||||||
onCacheCleared: () => void;
|
onCacheCleared: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SourceManager({ onCacheCleared }: SourceManagerProps) {
|
export function SourceManager({
|
||||||
|
onCacheCleared,
|
||||||
|
}: Readonly<SourceManagerProps>) {
|
||||||
const [sources, setSources] = useState<CachedSourceInfo[]>([]);
|
const [sources, setSources] = useState<CachedSourceInfo[]>([]);
|
||||||
const [optimisticSources, applyOptimistic] = useOptimistic(
|
const [optimisticSources, applyOptimistic] = useOptimistic(
|
||||||
sources,
|
sources,
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ function CollapsedTab({
|
|||||||
creatureName,
|
creatureName,
|
||||||
side,
|
side,
|
||||||
onToggleCollapse,
|
onToggleCollapse,
|
||||||
}: {
|
}: Readonly<{
|
||||||
creatureName: string;
|
creatureName: string;
|
||||||
side: "left" | "right";
|
side: "left" | "right";
|
||||||
onToggleCollapse: () => void;
|
onToggleCollapse: () => void;
|
||||||
}) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -73,13 +73,13 @@ function PanelHeader({
|
|||||||
onToggleCollapse,
|
onToggleCollapse,
|
||||||
onPin,
|
onPin,
|
||||||
onUnpin,
|
onUnpin,
|
||||||
}: {
|
}: Readonly<{
|
||||||
panelRole: "browse" | "pinned";
|
panelRole: "browse" | "pinned";
|
||||||
showPinButton: boolean;
|
showPinButton: boolean;
|
||||||
onToggleCollapse: () => void;
|
onToggleCollapse: () => void;
|
||||||
onPin: () => void;
|
onPin: () => void;
|
||||||
onUnpin: () => void;
|
onUnpin: () => void;
|
||||||
}) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between border-border border-b px-4 py-2">
|
<div className="flex items-center justify-between border-border border-b px-4 py-2">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -133,7 +133,7 @@ function DesktopPanel({
|
|||||||
onPin,
|
onPin,
|
||||||
onUnpin,
|
onUnpin,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: Readonly<{
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
side: "left" | "right";
|
side: "left" | "right";
|
||||||
creatureName: string;
|
creatureName: string;
|
||||||
@@ -143,7 +143,7 @@ function DesktopPanel({
|
|||||||
onPin: () => void;
|
onPin: () => void;
|
||||||
onUnpin: () => void;
|
onUnpin: () => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}>) {
|
||||||
const sideClasses = side === "left" ? "left-0 border-r" : "right-0 border-l";
|
const sideClasses = side === "left" ? "left-0 border-r" : "right-0 border-l";
|
||||||
const collapsedTranslate =
|
const collapsedTranslate =
|
||||||
side === "right"
|
side === "right"
|
||||||
@@ -179,10 +179,10 @@ function DesktopPanel({
|
|||||||
function MobileDrawer({
|
function MobileDrawer({
|
||||||
onDismiss,
|
onDismiss,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: Readonly<{
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}>) {
|
||||||
const { offsetX, isSwiping, handlers } = useSwipeToDismiss(onDismiss);
|
const { offsetX, isSwiping, handlers } = useSwipeToDismiss(onDismiss);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -239,7 +239,7 @@ export function StatBlockPanel({
|
|||||||
onStartBulkImport,
|
onStartBulkImport,
|
||||||
onBulkImportDone,
|
onBulkImportDone,
|
||||||
sourceManagerMode,
|
sourceManagerMode,
|
||||||
}: StatBlockPanelProps) {
|
}: Readonly<StatBlockPanelProps>) {
|
||||||
const [isDesktop, setIsDesktop] = useState(
|
const [isDesktop, setIsDesktop] = useState(
|
||||||
() => globalThis.matchMedia("(min-width: 1024px)").matches,
|
() => globalThis.matchMedia("(min-width: 1024px)").matches,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ function abilityMod(score: number): string {
|
|||||||
function PropertyLine({
|
function PropertyLine({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
}: {
|
}: Readonly<{
|
||||||
label: string;
|
label: string;
|
||||||
value: string | undefined;
|
value: string | undefined;
|
||||||
}) {
|
}>) {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
return (
|
return (
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
@@ -34,7 +34,7 @@ function SectionDivider() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatBlock({ creature }: StatBlockProps) {
|
export function StatBlock({ creature }: Readonly<StatBlockProps>) {
|
||||||
const abilities = [
|
const abilities = [
|
||||||
{ label: "STR", score: creature.abilities.str },
|
{ label: "STR", score: creature.abilities.str },
|
||||||
{ label: "DEX", score: creature.abilities.dex },
|
{ label: "DEX", score: creature.abilities.dex },
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function TurnNavigation({
|
|||||||
onAdvanceTurn,
|
onAdvanceTurn,
|
||||||
onRetreatTurn,
|
onRetreatTurn,
|
||||||
onClearEncounter,
|
onClearEncounter,
|
||||||
}: TurnNavigationProps) {
|
}: Readonly<TurnNavigationProps>) {
|
||||||
const hasCombatants = encounter.combatants.length > 0;
|
const hasCombatants = encounter.combatants.length > 0;
|
||||||
const isAtStart = encounter.roundNumber === 1 && encounter.activeIndex === 0;
|
const isAtStart = encounter.roundNumber === 1 && encounter.activeIndex === 0;
|
||||||
const activeCombatant = encounter.combatants[encounter.activeIndex];
|
const activeCombatant = encounter.combatants[encounter.activeIndex];
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ interface EditFields {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateFields(fields: EditFields): DomainError | null {
|
function validateFields(fields: EditFields): DomainError | null {
|
||||||
if (fields.name !== undefined && fields.name.trim() === "") {
|
if (fields.name?.trim() === "") {
|
||||||
return {
|
return {
|
||||||
kind: "domain-error",
|
kind: "domain-error",
|
||||||
code: "invalid-name",
|
code: "invalid-name",
|
||||||
@@ -81,9 +81,9 @@ function applyFields(
|
|||||||
): PlayerCharacter {
|
): PlayerCharacter {
|
||||||
return {
|
return {
|
||||||
id: existing.id,
|
id: existing.id,
|
||||||
name: fields.name === undefined ? existing.name : fields.name.trim(),
|
name: fields.name?.trim() ?? existing.name,
|
||||||
ac: fields.ac === undefined ? existing.ac : fields.ac,
|
ac: fields.ac ?? existing.ac,
|
||||||
maxHp: fields.maxHp === undefined ? existing.maxHp : fields.maxHp,
|
maxHp: fields.maxHp ?? existing.maxHp,
|
||||||
color:
|
color:
|
||||||
fields.color === undefined
|
fields.color === undefined
|
||||||
? existing.color
|
? existing.color
|
||||||
|
|||||||
Reference in New Issue
Block a user