From 3e70cc8eee0ca865d496ddb0d5e91732d164110b Mon Sep 17 00:00:00 2001 From: Scott Benton Date: Sun, 17 Mar 2024 16:19:45 -0400 Subject: [PATCH] feat(tracks): Added ability for users to view completed campaign & character tracks and clocks --- CHANGELOG.MD | 1 + .../features/ProgressTrack/ProgressTrack.tsx | 59 +++-- .../ProgressTrack/ProgressTrackList.tsx | 198 ++++++--------- .../features/ProgressTrack/ProgressTracks.tsx | 141 +++++++++++ .../charactersAndCampaigns/Clocks/Clock.tsx | 138 ++++++---- .../Clocks/ClockSection.tsx | 239 ++++++------------ .../charactersAndCampaigns/Clocks/Clocks.tsx | 140 ++++++++++ src/components/shared/SectionHeading.tsx | 16 +- .../components/TracksSection.tsx | 109 +++----- .../CampaignSheetPage/CampaignSheetPage.tsx | 6 +- .../components/CampaignProgressTracks.tsx | 63 +---- .../Tabs/ProgressTrackSection.tsx | 45 +--- .../tracks/campaignTracks.slice.default.ts | 19 +- .../tracks/campaignTracks.slice.ts | 18 +- .../tracks/campaignTracks.slice.type.ts | 18 +- .../tracks/useListenToCampaignTracks.ts | 13 + .../tracks/characterTracks.slice.default.ts | 19 +- .../tracks/characterTracks.slice.ts | 20 +- .../tracks/characterTracks.slice.type.ts | 17 +- .../tracks/useListenToCharacterTracks.ts | 14 + 20 files changed, 731 insertions(+), 562 deletions(-) create mode 100644 src/components/features/ProgressTrack/ProgressTracks.tsx create mode 100644 src/components/features/charactersAndCampaigns/Clocks/Clocks.tsx diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 2aa2957d..641fdbbe 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -12,6 +12,7 @@ - Added ability to add extra experience points past 30 (Ironsworn) - Added the ability to delete Rolls from the log (AnnB) - Added ability for GMs to remove other players and their characters from campaigns +- Added the ability for users to view completed campaign & character tracks and clocks ### Bug Fixes diff --git a/src/components/features/ProgressTrack/ProgressTrack.tsx b/src/components/features/ProgressTrack/ProgressTrack.tsx index 408e5dc5..5d106159 100644 --- a/src/components/features/ProgressTrack/ProgressTrack.tsx +++ b/src/components/features/ProgressTrack/ProgressTrack.tsx @@ -1,9 +1,14 @@ -import { Box, Button, ButtonBase, Link, Typography } from "@mui/material"; +import { Box, Button, ButtonBase, Chip, Link, Typography } from "@mui/material"; import { useEffect, useId, useState } from "react"; import { ProgressTrackTick } from "./ProgressTrackTick"; import MinusIcon from "@mui/icons-material/Remove"; import PlusIcon from "@mui/icons-material/Add"; -import { DIFFICULTY, PROGRESS_TRACKS, TRACK_TYPES } from "types/Track.type"; +import { + DIFFICULTY, + PROGRESS_TRACKS, + TRACK_STATUS, + TRACK_TYPES, +} from "types/Track.type"; import CompleteIcon from "@mui/icons-material/Check"; import DieIcon from "@mui/icons-material/Casino"; import { useConfirm } from "material-ui-confirm"; @@ -32,6 +37,7 @@ const trackMoveIdSystemValues: GameSystemChooser<{ export interface ProgressTracksProps { trackType?: PROGRESS_TRACKS; + status?: TRACK_STATUS; label?: string; difficulty?: DIFFICULTY; description?: string; @@ -41,6 +47,7 @@ export interface ProgressTracksProps { onDelete?: () => void; onEdit?: () => void; hideDifficultyLabel?: boolean; + hideRollButton?: boolean; } const getDifficultyLabel = (difficulty: DIFFICULTY): string => { @@ -79,6 +86,7 @@ export function ProgressTrack(props: ProgressTracksProps) { const { trackType, label, + status, description, difficulty, max, @@ -87,6 +95,7 @@ export function ProgressTrack(props: ProgressTracksProps) { onDelete, onEdit, hideDifficultyLabel, + hideRollButton, } = props; const trackMoveIds = useGameSystemValue(trackMoveIdSystemValues); @@ -176,25 +185,35 @@ export function ProgressTrack(props: ProgressTracksProps) { )} {(label || onEdit) && ( - theme.palette.text.primary} - fontFamily={(theme) => theme.fontFamilyTitle} - > - {label + " "} - {onEdit && ( - + theme.palette.text.primary} + fontFamily={(theme) => theme.fontFamilyTitle} + > + {label + " "} + {onEdit && ( + onEdit()} + > + Edit + + )} + + {status === TRACK_STATUS.COMPLETED && ( + onEdit()} - > - Edit - + size={"small"} + /> )} - + )} {description && ( )} - {trackType && ( + {trackType && !hideRollButton && ( - ) + <> + + toggleShowCompletedTracks(checked) + } + /> + } + label={`Show Completed ${typeLabel}s`} + /> + {!readOnly && ( + + )} + } breakContainer={headingBreakContainer} /> - ({ - px: headingBreakContainer ? 0 : 2, - [theme.breakpoints.up("md")]: { - px: headingBreakContainer ? 0 : 3, - }, - })} - > - {Array.isArray(orderedTrackIds) && orderedTrackIds.length > 0 ? ( - orderedTrackIds.map((trackId, index) => ( - handleUpdateValue(trackId, value)} - onDelete={ - handleDeleteTrack - ? () => { - handleDeleteTrack(trackId); - } - : undefined - } - max={40} - onEdit={ - handleUpdateTrack - ? () => setCurrentlyEditingTrackId(trackId) - : undefined - } - /> - )) - ) : ( - - )} - + + {showCompletedTracks && ( + + )} ); } diff --git a/src/components/features/ProgressTrack/ProgressTracks.tsx b/src/components/features/ProgressTrack/ProgressTracks.tsx new file mode 100644 index 00000000..23287ce6 --- /dev/null +++ b/src/components/features/ProgressTrack/ProgressTracks.tsx @@ -0,0 +1,141 @@ +import { Divider, Stack } from "@mui/material"; +import { useState } from "react"; +import { useStore } from "stores/store"; +import { TRACK_SECTION_PROGRESS_TRACKS, TRACK_STATUS } from "types/Track.type"; +import { ProgressTrack } from "./ProgressTrack"; +import { EmptyState } from "components/shared/EmptyState"; +import { EditOrCreateTrackDialog } from "./EditOrCreateTrackDialog"; + +export interface ProgressTracksProps { + isCampaign?: boolean; + isCompleted?: boolean; + trackType: TRACK_SECTION_PROGRESS_TRACKS; + typeLabel: string; + headingBreakContainer?: boolean; + readOnly?: boolean; +} + +export function ProgressTracks(props: ProgressTracksProps) { + const { + isCampaign, + isCompleted, + trackType, + typeLabel, + headingBreakContainer, + readOnly, + } = props; + + const tracks = useStore((store) => + isCampaign + ? store.campaigns.currentCampaign.tracks.trackMap[ + isCompleted ? TRACK_STATUS.COMPLETED : TRACK_STATUS.ACTIVE + ][trackType] + : store.characters.currentCharacter.tracks.trackMap[ + isCompleted ? TRACK_STATUS.COMPLETED : TRACK_STATUS.ACTIVE + ][trackType] + ); + + const orderedTrackIds = Object.keys(tracks).sort((trackId1, trackId2) => { + const track1 = tracks[trackId1]; + const track2 = tracks[trackId2]; + + return track2.createdDate.getTime() - track1.createdDate.getTime(); + }); + + const [currentlyEditingTrackId, setCurrentlyEditingTrackId] = + useState(); + + const currentlyEditingTrack = + currentlyEditingTrackId && tracks + ? tracks[currentlyEditingTrackId] + : undefined; + + const updateCampaignProgressTrack = useStore( + (store) => store.campaigns.currentCampaign.tracks.updateTrack + ); + const updateCharacterProgressTrack = useStore( + (store) => store.characters.currentCharacter.tracks.updateTrack + ); + + const updateProgressTrack = isCampaign + ? updateCampaignProgressTrack + : updateCharacterProgressTrack; + + const deleteProgressTrack = (trackId: string) => { + updateProgressTrack(trackId, { status: TRACK_STATUS.COMPLETED }).catch( + () => {} + ); + }; + + const updateProgressTrackValue = (trackId: string, value: number) => { + updateProgressTrack(trackId, { value }).catch(() => {}); + }; + + return ( + <> + ({ + px: headingBreakContainer ? 0 : 2, + [theme.breakpoints.up("md")]: { + px: headingBreakContainer ? 0 : 3, + }, + })} + > + {isCompleted && Completed Tracks} + {Array.isArray(orderedTrackIds) && orderedTrackIds.length > 0 ? ( + orderedTrackIds.map((trackId, index) => ( + updateProgressTrackValue(trackId, value) + } + onDelete={ + readOnly || isCompleted + ? undefined + : () => deleteProgressTrack(trackId) + } + onEdit={ + readOnly || isCompleted + ? undefined + : () => setCurrentlyEditingTrackId(trackId) + } + hideRollButton={readOnly || isCompleted} + /> + )) + ) : ( + + )} + + {!readOnly && currentlyEditingTrack && currentlyEditingTrackId && ( + setCurrentlyEditingTrackId(undefined)} + trackType={ + currentlyEditingTrack.type as TRACK_SECTION_PROGRESS_TRACKS + } + trackTypeName={`${typeLabel}`} + initialTrack={currentlyEditingTrack} + handleTrack={(track) => + currentlyEditingTrack + ? updateProgressTrack(currentlyEditingTrackId, track) + : new Promise((res) => res(true)) + } + /> + )} + + ); +} diff --git a/src/components/features/charactersAndCampaigns/Clocks/Clock.tsx b/src/components/features/charactersAndCampaigns/Clocks/Clock.tsx index 9a07f165..5d89c741 100644 --- a/src/components/features/charactersAndCampaigns/Clocks/Clock.tsx +++ b/src/components/features/charactersAndCampaigns/Clocks/Clock.tsx @@ -2,12 +2,13 @@ import { Box, Button, Card, + Chip, Link, MenuItem, TextField, Typography, } from "@mui/material"; -import { Clock as IClock } from "types/Track.type"; +import { Clock as IClock, TRACK_STATUS } from "types/Track.type"; import { ClockCircle } from "./ClockCircle"; import CheckIcon from "@mui/icons-material/Check"; import { useConfirm } from "material-ui-confirm"; @@ -29,10 +30,10 @@ const clockOracleMap = { export interface ClockProps { clock: IClock; - onEdit: () => void; - onValueChange: (value: number) => void; - onSelectedOracleChange: (oracleKey: CLOCK_ORACLES_KEYS) => void; - onComplete: () => void; + onEdit?: () => void; + onValueChange?: (value: number) => void; + onSelectedOracleChange?: (oracleKey: CLOCK_ORACLES_KEYS) => void; + onComplete?: () => void; } export function Clock(props: ClockProps) { @@ -44,45 +45,64 @@ export function Clock(props: ClockProps) { const confirm = useConfirm(); const handleCompleteClick = () => { - confirm({ - title: "Complete Clock", - description: "Are you sure you want to complete this clock?", - confirmationText: "Complete", - confirmationButtonProps: { - variant: "contained", - color: "primary", - }, - }) - .then(() => { - onComplete(); + if (onComplete) { + confirm({ + title: "Complete Clock", + description: "Are you sure you want to complete this clock?", + confirmationText: "Complete", + confirmationButtonProps: { + variant: "contained", + color: "primary", + }, }) - .catch(() => {}); + .then(() => { + onComplete(); + }) + .catch(() => {}); + } }; const handleProgressionRoll = () => { - const result = rollClockProgression( - clock.label, - clockOracleMap[clock.oracleKey ?? CLOCK_ORACLES_KEYS.FIFTY_FIFTY] - ); + if (onValueChange) { + const result = rollClockProgression( + clock.label, + clockOracleMap[clock.oracleKey ?? CLOCK_ORACLES_KEYS.FIFTY_FIFTY] + ); - if (result && clock.value < clock.segments) { - onValueChange(clock.value + 1); + if (result && clock.value < clock.segments) { + onValueChange(clock.value + 1); + } } }; return ( - theme.fontFamilyTitle} variant={"h6"}> - {clock.label} - onEdit()} + + theme.fontFamilyTitle} + variant={"h6"} > - Edit - - + {clock.label} + {onEdit && ( + onEdit()} + > + Edit + + )} + + {clock.status === TRACK_STATUS.COMPLETED && ( + + )} + {clock.description && ( + onSelectedOracleChange && onSelectedOracleChange(evt.target.value as CLOCK_ORACLES_KEYS) } + disabled={!onSelectedOracleChange} fullWidth > @@ -130,32 +152,42 @@ export function Clock(props: ClockProps) { Small Chance - + {onValueChange && ( + + )} { - onValueChange(clock.value >= clock.segments ? 0 : clock.value + 1); - }} + onClick={ + onValueChange + ? () => { + onValueChange( + clock.value >= clock.segments ? 0 : clock.value + 1 + ); + } + : undefined + } /> - + {onComplete && ( + + )} ); } diff --git a/src/components/features/charactersAndCampaigns/Clocks/ClockSection.tsx b/src/components/features/charactersAndCampaigns/Clocks/ClockSection.tsx index f23a5706..914ae628 100644 --- a/src/components/features/charactersAndCampaigns/Clocks/ClockSection.tsx +++ b/src/components/features/charactersAndCampaigns/Clocks/ClockSection.tsx @@ -1,11 +1,10 @@ -import { Button, Stack } from "@mui/material"; +import { Button, Checkbox, FormControlLabel } from "@mui/material"; import { SectionHeading } from "components/shared/SectionHeading"; import { useState } from "react"; import { useStore } from "stores/store"; -import { Clock as IClock, TRACK_STATUS, TRACK_TYPES } from "types/Track.type"; +import { Clock as IClock } from "types/Track.type"; import { ClockDialog } from "./ClockDialog"; -import { EmptyState } from "components/shared/EmptyState"; -import { Clock } from "./Clock"; +import { Clocks } from "./Clocks"; export interface ClockSectionProps { headingBreakContainer?: boolean; @@ -14,6 +13,29 @@ export interface ClockSectionProps { export function ClockSection(props: ClockSectionProps) { const { headingBreakContainer } = props; + const setLoadCompletedCharacterTracks = useStore( + (store) => store.characters.currentCharacter.tracks.setLoadCompletedTracks + ); + const setLoadCompletedCampaignTracks = useStore( + (store) => store.campaigns.currentCampaign.tracks.setLoadCompletedTracks + ); + const [showCompletedCharacterClocks, setShowCompletedCharacterClocks] = + useState(false); + const toggleShowCompletedCharacterClocks = (value: boolean) => { + if (value) { + setLoadCompletedCharacterTracks(); + } + setShowCompletedCharacterClocks(value); + }; + const [showCompletedCampaignClocks, setShowCompletedCampaignClocks] = + useState(false); + const toggleShowCompletedCampaignClocks = (value: boolean) => { + if (value) { + setLoadCompletedCampaignTracks(); + } + setShowCompletedCampaignClocks(value); + }; + const isInCampaign = useStore( (store) => !!store.campaigns.currentCampaign.currentCampaignId ); @@ -26,63 +48,18 @@ export function ClockSection(props: ClockSectionProps) { shared?: boolean; }>({ open: false }); - const [editingClock, setEditingClock] = useState<{ - clock: { id: string; clock: IClock } | undefined; - shared?: boolean; - }>({ clock: undefined }); - - const characterClocks = useStore( - (store) => - store.characters.currentCharacter.tracks.trackMap[TRACK_TYPES.CLOCK] - ); - const addClock = useStore( + const addCharacterClock = useStore( (store) => store.characters.currentCharacter.tracks.addTrack ); - const updateClock = useStore( - (store) => store.characters.currentCharacter.tracks.updateTrack - ); - const orderedCharacterClockIds = characterClocks - ? Object.keys(characterClocks).sort((clockId1, clockId2) => { - const clock1 = characterClocks[clockId1]; - const clock2 = characterClocks[clockId2]; - - return clock2.createdDate.getTime() - clock1.createdDate.getTime(); - }) - : []; - - const campaignClocks = useStore( - (store) => - store.campaigns.currentCampaign.tracks.trackMap[TRACK_TYPES.CLOCK] - ); const addCampaignClock = useStore( (store) => store.campaigns.currentCampaign.tracks.addTrack ); - const updateCampaignClock = useStore( - (store) => store.campaigns.currentCampaign.tracks.updateTrack - ); - const orderedCampaignClockIds = campaignClocks - ? Object.keys(campaignClocks).sort((clockId1, clockId2) => { - const clock1 = campaignClocks[clockId1]; - const clock2 = campaignClocks[clockId2]; - - return clock2.createdDate.getTime() - clock1.createdDate.getTime(); - }) - : []; const handleAddClock = (clock: IClock, shared?: boolean) => { - const addFn = shared ? addCampaignClock : addClock; + const addFn = shared ? addCampaignClock : addCharacterClock; return addFn(clock); }; - const handleEditClock = ( - clockId: string, - clock: IClock, - shared?: boolean - ) => { - const editFn = shared ? updateCampaignClock : updateClock; - return editFn(clockId, clock); - }; - return ( <> {isInCampaign && ( @@ -91,57 +68,35 @@ export function ClockSection(props: ClockSectionProps) { breakContainer={headingBreakContainer} label={"Shared Clocks"} action={ - - } - /> - {orderedCampaignClockIds.length > 0 ? ( - ({ - px: headingBreakContainer ? 0 : 2, - [theme.breakpoints.up("md")]: { - px: headingBreakContainer ? 0 : 3, - }, - })} - > - {orderedCampaignClockIds.map((clockId) => ( - - setEditingClock({ - clock: { - id: clockId, - clock: campaignClocks[clockId], - }, - shared: true, - }) - } - onSelectedOracleChange={(oracleKey) => - updateCampaignClock(clockId, { - oracleKey, - }).catch(() => {}) - } - onComplete={() => - updateCampaignClock(clockId, { - status: TRACK_STATUS.COMPLETED, - }).catch(() => {}) - } - onValueChange={(value) => - updateCampaignClock(clockId, { value }).catch(() => {}) + <> + + toggleShowCompletedCampaignClocks(checked) + } + /> } + label={"Show Completed Clocks"} /> - ))} - - ) : ( - + + + } + /> + + {showCompletedCampaignClocks && ( + )} )} @@ -151,54 +106,31 @@ export function ClockSection(props: ClockSectionProps) { breakContainer={headingBreakContainer} label={"Character Clocks"} action={ - - } - /> - {orderedCharacterClockIds.length > 0 ? ( - ({ - px: headingBreakContainer ? 0 : 2, - [theme.breakpoints.up("md")]: { - px: headingBreakContainer ? 0 : 3, - }, - })} - > - {orderedCharacterClockIds.map((clockId) => ( - - setEditingClock({ - clock: { - id: clockId, - clock: characterClocks[clockId], - }, - }) - } - onSelectedOracleChange={(oracleKey) => - updateClock(clockId, { - oracleKey, - }).catch(() => {}) - } - onComplete={() => - updateClock(clockId, { - status: TRACK_STATUS.COMPLETED, - }).catch(() => {}) - } - onValueChange={(value) => - updateClock(clockId, { value }).catch(() => {}) + <> + + toggleShowCompletedCharacterClocks(checked) + } + /> } + label={"Show Completed Clocks"} /> - ))} - - ) : ( - + + + } + /> + + + {showCompletedCharacterClocks && ( + )} )} @@ -208,21 +140,6 @@ export function ClockSection(props: ClockSectionProps) { shared={addClockDialogOpen.shared} onClock={(clock) => handleAddClock(clock, addClockDialogOpen.shared)} /> - {editingClock.clock && ( - setEditingClock({ clock: undefined })} - shared={editingClock.shared} - onClock={(clock) => - handleEditClock( - editingClock.clock?.id ?? "", - clock, - addClockDialogOpen.shared - ) - } - /> - )} ); } diff --git a/src/components/features/charactersAndCampaigns/Clocks/Clocks.tsx b/src/components/features/charactersAndCampaigns/Clocks/Clocks.tsx new file mode 100644 index 00000000..b9919481 --- /dev/null +++ b/src/components/features/charactersAndCampaigns/Clocks/Clocks.tsx @@ -0,0 +1,140 @@ +import { Divider, Stack } from "@mui/material"; +import { useStore } from "stores/store"; +import { Clock as IClock, TRACK_STATUS, TRACK_TYPES } from "types/Track.type"; +import { Clock } from "./Clock"; +import { useState } from "react"; +import { EmptyState } from "components/shared/EmptyState"; +import { ClockDialog } from "./ClockDialog"; + +export interface ClocksProps { + isCampaignSection?: boolean; + isCompleted?: boolean; + headingBreakContainer?: boolean; +} + +export function Clocks(props: ClocksProps) { + const { isCampaignSection, isCompleted, headingBreakContainer } = props; + + const clocks = useStore((store) => + isCampaignSection + ? store.campaigns.currentCampaign.tracks.trackMap[ + isCompleted ? TRACK_STATUS.COMPLETED : TRACK_STATUS.ACTIVE + ][TRACK_TYPES.CLOCK] + : store.characters.currentCharacter.tracks.trackMap[ + isCompleted ? TRACK_STATUS.COMPLETED : TRACK_STATUS.ACTIVE + ][TRACK_TYPES.CLOCK] + ); + const sortedClockIds = getSortedClockIds(clocks); + + const [editingClock, setEditingClock] = useState<{ + clock: { id: string; clock: IClock } | undefined; + shared?: boolean; + }>({ clock: undefined }); + + const updateCharacterClock = useStore( + (store) => store.characters.currentCharacter.tracks.updateTrack + ); + const updateCampaignClock = useStore( + (store) => store.campaigns.currentCampaign.tracks.updateTrack + ); + + const handleEditClock = ( + clockId: string, + clock: IClock, + shared?: boolean + ) => { + const editFn = shared ? updateCampaignClock : updateCharacterClock; + return editFn(clockId, clock); + }; + + const updateClock = isCampaignSection + ? updateCampaignClock + : updateCharacterClock; + + return ( + <> + {sortedClockIds.length > 0 ? ( + ({ + px: headingBreakContainer ? 0 : 2, + [theme.breakpoints.up("md")]: { + px: headingBreakContainer ? 0 : 3, + }, + mt: isCompleted ? 4 : undefined, + })} + > + {isCompleted && Completed Clocks} + {sortedClockIds.map((clockId) => ( + + setEditingClock({ + clock: { + id: clockId, + clock: clocks[clockId], + }, + }) + } + onSelectedOracleChange={ + isCompleted + ? undefined + : (oracleKey) => + updateClock(clockId, { + oracleKey, + }).catch(() => {}) + } + onComplete={ + isCompleted + ? undefined + : () => + updateClock(clockId, { + status: TRACK_STATUS.COMPLETED, + }).catch(() => {}) + } + onValueChange={ + isCompleted + ? undefined + : (value) => updateClock(clockId, { value }).catch(() => {}) + } + /> + ))} + + ) : ( + + )} + {editingClock.clock && ( + setEditingClock({ clock: undefined })} + shared={editingClock.shared} + onClock={(clock) => + handleEditClock( + editingClock.clock?.id ?? "", + clock, + editingClock.shared + ) + } + /> + )} + + ); +} + +function getSortedClockIds(clocks: Record): string[] { + return Object.keys(clocks).sort((clockId1, clockId2) => { + const clock1 = clocks[clockId1]; + const clock2 = clocks[clockId2]; + + return clock2.createdDate.getTime() - clock1.createdDate.getTime(); + }); +} diff --git a/src/components/shared/SectionHeading.tsx b/src/components/shared/SectionHeading.tsx index 7e36bdbc..d3306b8b 100644 --- a/src/components/shared/SectionHeading.tsx +++ b/src/components/shared/SectionHeading.tsx @@ -1,4 +1,4 @@ -import { Box, SxProps, Theme, Typography } from "@mui/material"; +import { Box, Stack, SxProps, Theme, Typography } from "@mui/material"; import { ReactNode } from "react"; export interface SectionHeadingProps { @@ -20,7 +20,6 @@ export function SectionHeading(props: SectionHeadingProps) { justifyContent={"space-between"} sx={[ (theme) => ({ - flexDirection: "row", alignItems: "center", marginX: breakContainer ? -3 : 0, @@ -29,10 +28,13 @@ export function SectionHeading(props: SectionHeadingProps) { [theme.breakpoints.down("sm")]: { flexDirection: "column", paddingX: 2, - }, - [theme.breakpoints.down("sm")]: { marginX: breakContainer ? -2 : 0, }, + [theme.breakpoints.up("md")]: { + px: 3, + mx: breakContainer ? -3 : 0, + flexDirection: "row", + }, }), floating && { borderRadius: 1, @@ -48,7 +50,11 @@ export function SectionHeading(props: SectionHeadingProps) { > {label} - {action} + {action && ( + + {action} + + )} ); } diff --git a/src/pages/Campaign/CampaignGMScreenPage/components/TracksSection.tsx b/src/pages/Campaign/CampaignGMScreenPage/components/TracksSection.tsx index d35d62df..1090cf71 100644 --- a/src/pages/Campaign/CampaignGMScreenPage/components/TracksSection.tsx +++ b/src/pages/Campaign/CampaignGMScreenPage/components/TracksSection.tsx @@ -3,7 +3,10 @@ import { SectionHeading } from "components/shared/SectionHeading"; import { supplyTrack } from "data/defaultTracks"; import { Track } from "components/features/Track"; import { TRACK_STATUS, TRACK_TYPES } from "types/Track.type"; -import { ProgressTrackList } from "components/features/ProgressTrack"; +import { + ProgressTrack, + ProgressTrackList, +} from "components/features/ProgressTrack"; import { useStore } from "stores/store"; import { ClockSection } from "components/features/charactersAndCampaigns/Clocks/ClockSection"; import { useGameSystem } from "hooks/useGameSystem"; @@ -31,10 +34,6 @@ export function TracksSection(props: TracksSectionProps) { (store) => store.campaigns.currentCampaign.updateCampaignConditionMeter ); - const tracks = useStore( - (store) => store.campaigns.currentCampaign.tracks.trackMap - ); - const characterTracks = useStore( (store) => store.campaigns.currentCampaign.characters.characterTracks ); @@ -42,16 +41,6 @@ export function TracksSection(props: TracksSectionProps) { (store) => store.campaigns.currentCampaign.characters.characterMap ); - const vows = tracks[TRACK_TYPES.VOW]; - const journeys = tracks[TRACK_TYPES.JOURNEY]; - const frays = tracks[TRACK_TYPES.FRAY]; - - const addCampaignProgressTrack = useStore( - (store) => store.campaigns.currentCampaign.tracks.addTrack - ); - const updateCampaignProgressTrack = useStore( - (store) => store.campaigns.currentCampaign.tracks.updateTrack - ); const updateCharacterProgressTrack = useStore( (store) => store.campaigns.currentCampaign.tracks.updateCharacterTrack ); @@ -103,74 +92,60 @@ export function TracksSection(props: TracksSectionProps) { )}
addCampaignProgressTrack(newTrack)} - handleUpdateValue={(trackId, value) => - updateCampaignProgressTrack(trackId, { value }) - } - handleUpdateTrack={(trackId, track) => - updateCampaignProgressTrack(trackId, track) - } - handleDeleteTrack={(trackId) => - updateCampaignProgressTrack(trackId, { - status: TRACK_STATUS.COMPLETED, - }) - } + isCampaign /> addCampaignProgressTrack(newTrack)} - handleUpdateValue={(trackId, value) => - updateCampaignProgressTrack(trackId, { value }) - } - handleUpdateTrack={(trackId, track) => - updateCampaignProgressTrack(trackId, track) - } - handleDeleteTrack={(trackId) => - updateCampaignProgressTrack(trackId, { - status: TRACK_STATUS.COMPLETED, - }) - } + isCampaign /> addCampaignProgressTrack(newTrack)} - handleUpdateValue={(trackId, value) => - updateCampaignProgressTrack(trackId, { value }) - } - handleUpdateTrack={(trackId, track) => - updateCampaignProgressTrack(trackId, track) - } - handleDeleteTrack={(trackId) => - updateCampaignProgressTrack(trackId, { - status: TRACK_STATUS.COMPLETED, - }) - } + isCampaign /> {Object.keys(characterTracks).map((characterId) => (
{characters[characterId] && Object.keys(characterTracks[characterId]?.[TRACK_TYPES.VOW] ?? {}) .length > 0 && ( - - updateCharacterProgressTrack(characterId, trackId, { - value, - }) - } - handleUpdateTrack={(trackId, track) => - updateCharacterProgressTrack(characterId, trackId, track) - } - /> + <> + + + {Object.keys( + characterTracks[characterId]?.[TRACK_TYPES.VOW] ?? {} + ).map((trackId, index) => { + const track = + characterTracks[characterId][TRACK_TYPES.VOW][trackId]; + return ( + + updateCharacterProgressTrack(characterId, trackId, { + value, + }) + } + onDelete={() => + updateCharacterProgressTrack(characterId, trackId, { + status: TRACK_STATUS.COMPLETED, + }) + } + /> + ); + })} + + )}
))} diff --git a/src/pages/Campaign/CampaignSheetPage/CampaignSheetPage.tsx b/src/pages/Campaign/CampaignSheetPage/CampaignSheetPage.tsx index 900440c1..546fd266 100644 --- a/src/pages/Campaign/CampaignSheetPage/CampaignSheetPage.tsx +++ b/src/pages/Campaign/CampaignSheetPage/CampaignSheetPage.tsx @@ -98,9 +98,13 @@ export function CampaignSheetPage() { handleTabChange(value)} - indicatorColor="primary" + indicatorColor='primary' centered variant={"standard"} + sx={(theme) => ({ + borderTopRightRadius: theme.shape.borderRadius, + borderTopLeftRadius: theme.shape.borderRadius, + })} > diff --git a/src/pages/Campaign/CampaignSheetPage/components/CampaignProgressTracks.tsx b/src/pages/Campaign/CampaignSheetPage/components/CampaignProgressTracks.tsx index 2354b18d..31114b58 100644 --- a/src/pages/Campaign/CampaignSheetPage/components/CampaignProgressTracks.tsx +++ b/src/pages/Campaign/CampaignSheetPage/components/CampaignProgressTracks.tsx @@ -1,8 +1,7 @@ import { ProgressTrackList } from "components/features/ProgressTrack"; import { useGameSystem } from "hooks/useGameSystem"; -import { useStore } from "stores/store"; import { GAME_SYSTEMS } from "types/GameSystems.type"; -import { TRACK_STATUS, TRACK_TYPES } from "types/Track.type"; +import { TRACK_TYPES } from "types/Track.type"; export interface CampaignProgressTracksProps { addPadding?: boolean; @@ -13,78 +12,24 @@ export function CampaignProgressTracks(props: CampaignProgressTracksProps) { const isStarforged = useGameSystem().gameSystem === GAME_SYSTEMS.STARFORGED; - const vows = useStore( - (store) => store.campaigns.currentCampaign.tracks.trackMap[TRACK_TYPES.VOW] - ); - const journeys = useStore( - (store) => - store.campaigns.currentCampaign.tracks.trackMap[TRACK_TYPES.JOURNEY] - ); - const frays = useStore( - (store) => store.campaigns.currentCampaign.tracks.trackMap[TRACK_TYPES.FRAY] - ); - - const addCampaignProgressTrack = useStore( - (store) => store.campaigns.currentCampaign.tracks.addTrack - ); - const updateCampaignProgressTrack = useStore( - (store) => store.campaigns.currentCampaign.tracks.updateTrack - ); - return ( <> addCampaignProgressTrack(newTrack)} - handleUpdateValue={(trackId, value) => - updateCampaignProgressTrack(trackId, { value }) - } - handleUpdateTrack={(trackId, track) => - updateCampaignProgressTrack(trackId, track) - } - handleDeleteTrack={(trackId) => - updateCampaignProgressTrack(trackId, { - status: TRACK_STATUS.COMPLETED, - }) - } + isCampaign headingBreakContainer={!addPadding} /> addCampaignProgressTrack(newTrack)} - handleUpdateValue={(trackId, value) => - updateCampaignProgressTrack(trackId, { value }) - } - handleUpdateTrack={(trackId, track) => - updateCampaignProgressTrack(trackId, track) - } - handleDeleteTrack={(trackId) => - updateCampaignProgressTrack(trackId, { - status: TRACK_STATUS.COMPLETED, - }) - } + isCampaign headingBreakContainer={!addPadding} /> addCampaignProgressTrack(newTrack)} - handleUpdateValue={(trackId, value) => - updateCampaignProgressTrack(trackId, { value }) - } - handleUpdateTrack={(trackId, track) => - updateCampaignProgressTrack(trackId, track) - } - handleDeleteTrack={(trackId) => - updateCampaignProgressTrack(trackId, { - status: TRACK_STATUS.COMPLETED, - }) - } + isCampaign headingBreakContainer={!addPadding} /> diff --git a/src/pages/Character/CharacterSheetPage/Tabs/ProgressTrackSection.tsx b/src/pages/Character/CharacterSheetPage/Tabs/ProgressTrackSection.tsx index 18e392d8..38eba449 100644 --- a/src/pages/Character/CharacterSheetPage/Tabs/ProgressTrackSection.tsx +++ b/src/pages/Character/CharacterSheetPage/Tabs/ProgressTrackSection.tsx @@ -1,6 +1,6 @@ import { Box } from "@mui/material"; import { ProgressTrackList } from "components/features/ProgressTrack"; -import { TRACK_SECTION_PROGRESS_TRACKS, TRACK_STATUS } from "types/Track.type"; +import { TRACK_SECTION_PROGRESS_TRACKS } from "types/Track.type"; import { useStore } from "stores/store"; export interface ProgressTrackSectionProps { @@ -12,64 +12,23 @@ export interface ProgressTrackSectionProps { export function ProgressTrackSection(props: ProgressTrackSectionProps) { const { type, typeLabel, showPersonalIfInCampaign } = props; - const characterTracks = useStore( - (store) => store.characters.currentCharacter.tracks.trackMap[type] - ); - const addProgressTrack = useStore( - (store) => store.characters.currentCharacter.tracks.addTrack - ); - const updateProgressTrack = useStore( - (store) => store.characters.currentCharacter.tracks.updateTrack - ); - const isInCampaign = useStore( (store) => !!store.campaigns.currentCampaign.currentCampaignId ); - const campaignTracks = useStore( - (store) => store.campaigns.currentCampaign.tracks.trackMap[type] - ); - const addCampaignTrack = useStore( - (store) => store.campaigns.currentCampaign.tracks.addTrack - ); - const updateCampaignTrack = useStore( - (store) => store.campaigns.currentCampaign.tracks.updateTrack - ); - return ( {isInCampaign && ( addCampaignTrack(newTrack)} - handleUpdateValue={(trackId, value) => - updateCampaignTrack(trackId, { value }) - } - handleUpdateTrack={(trackId, track) => - updateCampaignTrack(trackId, track) - } - handleDeleteTrack={(trackId) => - updateCampaignTrack(trackId, { status: TRACK_STATUS.COMPLETED }) - } + isCampaign /> )} {(!isInCampaign || showPersonalIfInCampaign) && ( addProgressTrack(newTrack)} - handleUpdateValue={(trackId, value) => - updateProgressTrack(trackId, { value }) - } - handleUpdateTrack={(trackId, track) => - updateProgressTrack(trackId, track) - } - handleDeleteTrack={(trackId) => - updateProgressTrack(trackId, { status: TRACK_STATUS.COMPLETED }) - } /> )} diff --git a/src/stores/campaign/currentCampaign/tracks/campaignTracks.slice.default.ts b/src/stores/campaign/currentCampaign/tracks/campaignTracks.slice.default.ts index 63a8d4c2..b46dc922 100644 --- a/src/stores/campaign/currentCampaign/tracks/campaignTracks.slice.default.ts +++ b/src/stores/campaign/currentCampaign/tracks/campaignTracks.slice.default.ts @@ -1,12 +1,21 @@ -import { TRACK_TYPES } from "types/Track.type"; +import { TRACK_STATUS, TRACK_TYPES } from "types/Track.type"; import { CampaignTracksSliceData } from "./campaignTracks.slice.type"; export const defaultCampaignTracksSlice: CampaignTracksSliceData = { + loadCompletedTracks: false, trackMap: { - [TRACK_TYPES.FRAY]: {}, - [TRACK_TYPES.JOURNEY]: {}, - [TRACK_TYPES.VOW]: {}, - [TRACK_TYPES.CLOCK]: {}, + [TRACK_STATUS.ACTIVE]: { + [TRACK_TYPES.FRAY]: {}, + [TRACK_TYPES.JOURNEY]: {}, + [TRACK_TYPES.VOW]: {}, + [TRACK_TYPES.CLOCK]: {}, + }, + [TRACK_STATUS.COMPLETED]: { + [TRACK_TYPES.FRAY]: {}, + [TRACK_TYPES.JOURNEY]: {}, + [TRACK_TYPES.VOW]: {}, + [TRACK_TYPES.CLOCK]: {}, + }, }, error: "", loading: false, diff --git a/src/stores/campaign/currentCampaign/tracks/campaignTracks.slice.ts b/src/stores/campaign/currentCampaign/tracks/campaignTracks.slice.ts index df23c504..89b26f12 100644 --- a/src/stores/campaign/currentCampaign/tracks/campaignTracks.slice.ts +++ b/src/stores/campaign/currentCampaign/tracks/campaignTracks.slice.ts @@ -28,22 +28,22 @@ export const createCampaignTracksSlice: CreateSliceType = ( const track = tracks[trackId]; switch (track.type) { case TRACK_TYPES.FRAY: - store.campaigns.currentCampaign.tracks.trackMap[ + store.campaigns.currentCampaign.tracks.trackMap[status][ TRACK_TYPES.FRAY ][trackId] = track as ProgressTrack; break; case TRACK_TYPES.JOURNEY: - store.campaigns.currentCampaign.tracks.trackMap[ + store.campaigns.currentCampaign.tracks.trackMap[status][ TRACK_TYPES.JOURNEY ][trackId] = track as ProgressTrack; break; case TRACK_TYPES.VOW: - store.campaigns.currentCampaign.tracks.trackMap[ + store.campaigns.currentCampaign.tracks.trackMap[status][ TRACK_TYPES.VOW ][trackId] = track as ProgressTrack; break; case TRACK_TYPES.CLOCK: - store.campaigns.currentCampaign.tracks.trackMap[ + store.campaigns.currentCampaign.tracks.trackMap[status][ TRACK_TYPES.CLOCK ][trackId] = track as Clock; break; @@ -55,7 +55,9 @@ export const createCampaignTracksSlice: CreateSliceType = ( }, (trackId, type) => { set((store) => { - delete store.campaigns.currentCampaign.tracks.trackMap[type][trackId]; + delete store.campaigns.currentCampaign.tracks.trackMap[status][type][ + trackId + ]; }); }, (error) => { @@ -82,6 +84,12 @@ export const createCampaignTracksSlice: CreateSliceType = ( return updateProgressTrack({ characterId, trackId, track }); }, + setLoadCompletedTracks: () => { + set((store) => { + store.campaigns.currentCampaign.tracks.loadCompletedTracks = true; + }); + }, + resetStore: () => { set((store) => { store.campaigns.currentCampaign.tracks = { diff --git a/src/stores/campaign/currentCampaign/tracks/campaignTracks.slice.type.ts b/src/stores/campaign/currentCampaign/tracks/campaignTracks.slice.type.ts index 2658380f..bdf0ec52 100644 --- a/src/stores/campaign/currentCampaign/tracks/campaignTracks.slice.type.ts +++ b/src/stores/campaign/currentCampaign/tracks/campaignTracks.slice.type.ts @@ -8,12 +8,16 @@ import { } from "types/Track.type"; export interface CampaignTracksSliceData { - trackMap: { - [TRACK_TYPES.FRAY]: { [trackId: string]: ProgressTrack }; - [TRACK_TYPES.JOURNEY]: { [trackId: string]: ProgressTrack }; - [TRACK_TYPES.VOW]: { [trackId: string]: ProgressTrack }; - [TRACK_TYPES.CLOCK]: { [clockId: string]: Clock }; - }; + loadCompletedTracks: boolean; + trackMap: Record< + TRACK_STATUS, + { + [TRACK_TYPES.FRAY]: { [trackId: string]: ProgressTrack }; + [TRACK_TYPES.JOURNEY]: { [trackId: string]: ProgressTrack }; + [TRACK_TYPES.VOW]: { [trackId: string]: ProgressTrack }; + [TRACK_TYPES.CLOCK]: { [clockId: string]: Clock }; + } + >; error?: string; loading: boolean; } @@ -30,6 +34,8 @@ export interface CampaignTracksSliceActions { track: Partial ) => Promise; + setLoadCompletedTracks: () => void; + resetStore: () => void; } diff --git a/src/stores/campaign/currentCampaign/tracks/useListenToCampaignTracks.ts b/src/stores/campaign/currentCampaign/tracks/useListenToCampaignTracks.ts index d109652d..ad52c341 100644 --- a/src/stores/campaign/currentCampaign/tracks/useListenToCampaignTracks.ts +++ b/src/stores/campaign/currentCampaign/tracks/useListenToCampaignTracks.ts @@ -1,11 +1,15 @@ import { Unsubscribe } from "firebase/firestore"; import { useEffect } from "react"; import { useStore } from "stores/store"; +import { TRACK_STATUS } from "types/Track.type"; export function useListenToCampaignTracks() { const campaignId = useStore( (store) => store.campaigns.currentCampaign.currentCampaignId ); + const loadCompletedTracks = useStore( + (store) => store.campaigns.currentCampaign.tracks.loadCompletedTracks + ); const subscribe = useStore( (store) => store.campaigns.currentCampaign.tracks.subscribe ); @@ -19,4 +23,13 @@ export function useListenToCampaignTracks() { unsubscribe && unsubscribe(); }; }, [campaignId, subscribe]); + useEffect(() => { + let unsubscribe: Unsubscribe; + if (campaignId && loadCompletedTracks) { + unsubscribe = subscribe(campaignId, TRACK_STATUS.COMPLETED); + } + return () => { + unsubscribe && unsubscribe(); + }; + }, [campaignId, subscribe, loadCompletedTracks]); } diff --git a/src/stores/character/currentCharacter/tracks/characterTracks.slice.default.ts b/src/stores/character/currentCharacter/tracks/characterTracks.slice.default.ts index 9c74aa7a..6e00df5d 100644 --- a/src/stores/character/currentCharacter/tracks/characterTracks.slice.default.ts +++ b/src/stores/character/currentCharacter/tracks/characterTracks.slice.default.ts @@ -1,12 +1,21 @@ -import { TRACK_TYPES } from "types/Track.type"; +import { TRACK_STATUS, TRACK_TYPES } from "types/Track.type"; import { CharacterTracksSliceData } from "./characterTracks.slice.type"; export const defaultCharacterTracksSlice: CharacterTracksSliceData = { + loadCompletedTracks: false, trackMap: { - [TRACK_TYPES.FRAY]: {}, - [TRACK_TYPES.JOURNEY]: {}, - [TRACK_TYPES.VOW]: {}, - [TRACK_TYPES.CLOCK]: {}, + [TRACK_STATUS.ACTIVE]: { + [TRACK_TYPES.FRAY]: {}, + [TRACK_TYPES.JOURNEY]: {}, + [TRACK_TYPES.VOW]: {}, + [TRACK_TYPES.CLOCK]: {}, + }, + [TRACK_STATUS.COMPLETED]: { + [TRACK_TYPES.FRAY]: {}, + [TRACK_TYPES.JOURNEY]: {}, + [TRACK_TYPES.VOW]: {}, + [TRACK_TYPES.CLOCK]: {}, + }, }, error: "", loading: false, diff --git a/src/stores/character/currentCharacter/tracks/characterTracks.slice.ts b/src/stores/character/currentCharacter/tracks/characterTracks.slice.ts index 78100c98..a8a3cba8 100644 --- a/src/stores/character/currentCharacter/tracks/characterTracks.slice.ts +++ b/src/stores/character/currentCharacter/tracks/characterTracks.slice.ts @@ -27,22 +27,22 @@ export const createCharacterTracksSlice: CreateSliceType< const track = tracks[trackId]; switch (track.type) { case TRACK_TYPES.FRAY: - store.characters.currentCharacter.tracks.trackMap[ + store.characters.currentCharacter.tracks.trackMap[status][ TRACK_TYPES.FRAY ][trackId] = track as ProgressTrack; break; case TRACK_TYPES.JOURNEY: - store.characters.currentCharacter.tracks.trackMap[ + store.characters.currentCharacter.tracks.trackMap[status][ TRACK_TYPES.JOURNEY ][trackId] = track as ProgressTrack; break; case TRACK_TYPES.VOW: - store.characters.currentCharacter.tracks.trackMap[ + store.characters.currentCharacter.tracks.trackMap[status][ TRACK_TYPES.VOW ][trackId] = track as ProgressTrack; break; case TRACK_TYPES.CLOCK: - store.characters.currentCharacter.tracks.trackMap[ + store.characters.currentCharacter.tracks.trackMap[status][ TRACK_TYPES.CLOCK ][trackId] = track as Clock; break; @@ -54,9 +54,9 @@ export const createCharacterTracksSlice: CreateSliceType< }, (trackId, type) => { set((store) => { - delete store.characters.currentCharacter.tracks.trackMap[type][ - trackId - ]; + delete store.characters.currentCharacter.tracks.trackMap[status][ + type + ][trackId]; }); }, (error) => { @@ -81,6 +81,12 @@ export const createCharacterTracksSlice: CreateSliceType< return updateProgressTrack({ characterId, trackId, track }); }, + setLoadCompletedTracks: () => { + set((store) => { + store.characters.currentCharacter.tracks.loadCompletedTracks = true; + }); + }, + resetStore: () => { set((store) => { store.characters.currentCharacter.tracks = { diff --git a/src/stores/character/currentCharacter/tracks/characterTracks.slice.type.ts b/src/stores/character/currentCharacter/tracks/characterTracks.slice.type.ts index 862385b8..91bc2213 100644 --- a/src/stores/character/currentCharacter/tracks/characterTracks.slice.type.ts +++ b/src/stores/character/currentCharacter/tracks/characterTracks.slice.type.ts @@ -8,12 +8,16 @@ import { } from "types/Track.type"; export interface CharacterTracksSliceData { - trackMap: { - [TRACK_TYPES.FRAY]: { [trackId: string]: ProgressTrack }; - [TRACK_TYPES.JOURNEY]: { [trackId: string]: ProgressTrack }; - [TRACK_TYPES.VOW]: { [trackId: string]: ProgressTrack }; - [TRACK_TYPES.CLOCK]: { [trackId: string]: Clock }; - }; + loadCompletedTracks: boolean; + trackMap: Record< + TRACK_STATUS, + { + [TRACK_TYPES.FRAY]: { [trackId: string]: ProgressTrack }; + [TRACK_TYPES.JOURNEY]: { [trackId: string]: ProgressTrack }; + [TRACK_TYPES.VOW]: { [trackId: string]: ProgressTrack }; + [TRACK_TYPES.CLOCK]: { [trackId: string]: Clock }; + } + >; error?: string; loading: boolean; } @@ -24,6 +28,7 @@ export interface CharacterTracksSliceActions { addTrack: (track: Track) => Promise; updateTrack: (trackId: string, track: Partial) => Promise; + setLoadCompletedTracks: () => void; resetStore: () => void; } diff --git a/src/stores/character/currentCharacter/tracks/useListenToCharacterTracks.ts b/src/stores/character/currentCharacter/tracks/useListenToCharacterTracks.ts index 3ca6c812..95c9d5c6 100644 --- a/src/stores/character/currentCharacter/tracks/useListenToCharacterTracks.ts +++ b/src/stores/character/currentCharacter/tracks/useListenToCharacterTracks.ts @@ -1,11 +1,15 @@ import { Unsubscribe } from "firebase/firestore"; import { useEffect } from "react"; import { useStore } from "stores/store"; +import { TRACK_STATUS } from "types/Track.type"; export function useListenToCharacterTracks() { const characterId = useStore( (store) => store.characters.currentCharacter.currentCharacterId ); + const loadCompletedTracks = useStore( + (store) => store.characters.currentCharacter.tracks.loadCompletedTracks + ); const subscribe = useStore( (store) => store.characters.currentCharacter.tracks.subscribe ); @@ -19,4 +23,14 @@ export function useListenToCharacterTracks() { unsubscribe && unsubscribe(); }; }, [characterId, subscribe]); + + useEffect(() => { + let unsubscribe: Unsubscribe; + if (characterId && loadCompletedTracks) { + unsubscribe = subscribe(characterId, TRACK_STATUS.COMPLETED); + } + return () => { + unsubscribe && unsubscribe(); + }; + }, [characterId, subscribe, loadCompletedTracks]); }