diff --git a/src/components/features/charactersAndCampaigns/MovesSection/MoveCategory.tsx b/src/components/features/charactersAndCampaigns/MovesSection/MoveCategory.tsx index 8aed7c0c..54c9adf2 100644 --- a/src/components/features/charactersAndCampaigns/MovesSection/MoveCategory.tsx +++ b/src/components/features/charactersAndCampaigns/MovesSection/MoveCategory.tsx @@ -11,15 +11,19 @@ import OpenIcon from "@mui/icons-material/ChevronRight"; import { useEffect, useState } from "react"; import { useNewMoveOracleView } from "hooks/featureFlags/useNewMoveOracleView"; import { CollapsibleSectionHeader } from "../CollapsibleSectionHeader"; +import { CATEGORY_VISIBILITY } from "./useFilterMoves"; export interface MoveCategoryProps { category: IMoveCategory; openMove: (move: Move) => void; forceOpen?: boolean; + visibleCategories: Record; + visibleMoves: Record; } export function MoveCategory(props: MoveCategoryProps) { - const { category, openMove, forceOpen } = props; + const { category, openMove, forceOpen, visibleCategories, visibleMoves } = + props; const showNewView = useNewMoveOracleView(); const [isExpanded, setIsExpanded] = useState(showNewView ? false : true); @@ -30,6 +34,10 @@ export function MoveCategory(props: MoveCategoryProps) { const isExpandedOrForced = isExpanded || forceOpen; + if (visibleCategories[category.$id] === CATEGORY_VISIBILITY.HIDDEN) { + return null; + } + return ( <> {showNewView ? ( @@ -59,39 +67,42 @@ export function MoveCategory(props: MoveCategoryProps) { mb: showNewView && isExpandedOrForced ? 0.5 : 0, }} > - {Object.values(category.Moves).map((move, index) => ( - ({ - "&:nth-of-type(even)": { - backgroundColor: theme.palette.background.paperInlay, - }, - })} - disablePadding - > - openMove(move)} - sx={{ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - }} + {Object.values(category.Moves).map((move, index) => + visibleCategories[category.$id] === CATEGORY_VISIBILITY.ALL || + visibleMoves[move.$id] === true ? ( + ({ + "&:nth-of-type(even)": { + backgroundColor: theme.palette.background.paperInlay, + }, + })} + disablePadding > - ({ - ...theme.typography.body2, - color: theme.palette.text.primary, - })} + openMove(move)} + sx={{ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + }} > - {move.Title.Standard} - - - - - - - ))} + ({ + ...theme.typography.body2, + color: theme.palette.text.primary, + })} + > + {move.Title.Standard} + + + + + + + ) : null + )} diff --git a/src/components/features/charactersAndCampaigns/MovesSection/MovesSection.tsx b/src/components/features/charactersAndCampaigns/MovesSection/MovesSection.tsx index bc8faaf2..43b872e8 100644 --- a/src/components/features/charactersAndCampaigns/MovesSection/MovesSection.tsx +++ b/src/components/features/charactersAndCampaigns/MovesSection/MovesSection.tsx @@ -3,10 +3,19 @@ import SearchIcon from "@mui/icons-material/Search"; import { MoveCategory } from "./MoveCategory"; import { useFilterMoves } from "./useFilterMoves"; import { useStore } from "stores/store"; +import { EmptyState } from "components/shared/EmptyState"; export function MovesSection() { - const { setSearch, filteredMoves, isSearchActive } = useFilterMoves(); + const { + moveCategories, + setSearch, + visibleMoveCategoryIds, + visibleMoveIds, + isSearchActive, + isEmpty, + } = useFilterMoves(); + console.debug(isEmpty); const openDialog = useStore((store) => store.appState.openDialog); return ( @@ -43,16 +52,22 @@ export function MovesSection() { flexGrow: 1, }} > - {filteredMoves.map((category, index) => ( - { - openDialog(move.$id); - }} - forceOpen={isSearchActive} - /> - ))} + {!isEmpty ? ( + moveCategories.map((category, index) => ( + { + openDialog(move.$id); + }} + forceOpen={isSearchActive} + visibleCategories={visibleMoveCategoryIds} + visibleMoves={visibleMoveIds} + /> + )) + ) : ( + + )} ); diff --git a/src/components/features/charactersAndCampaigns/MovesSection/useFilterMoves.ts b/src/components/features/charactersAndCampaigns/MovesSection/useFilterMoves.ts index 47c6d4d3..de6e784b 100644 --- a/src/components/features/charactersAndCampaigns/MovesSection/useFilterMoves.ts +++ b/src/components/features/charactersAndCampaigns/MovesSection/useFilterMoves.ts @@ -1,62 +1,70 @@ -import { useEffect, useState } from "react"; -import { useSearch } from "hooks/useSearch"; -import { orderedCategories } from "data/moves"; -import { Move, MoveCategory } from "dataforged"; -import { useCustomMoves } from "./useCustomMoves"; -import { useStore } from "stores/store"; +import { useMemo, useState } from "react"; +import { useMoves } from "./useMoves"; + +export enum CATEGORY_VISIBILITY { + HIDDEN, + SOME, + ALL, +} export function useFilterMoves() { - const { search, setSearch, debouncedSearch } = useSearch(); - const [filteredMoves, setFilteredMoves] = useState(orderedCategories); - const { customMoveCategories } = useCustomMoves(); - - const showDelveMoves = useStore( - (store) => store.settings.delve.showDelveMoves - ); - - useEffect(() => { - const results: MoveCategory[] = []; - - const allCategories = [ - ...orderedCategories, - ...customMoveCategories, - ].filter( - (category) => - showDelveMoves || category.Source.Title !== "Ironsworn: Delve" - ); - - allCategories.forEach((category) => { + const [search, setSearch] = useState(""); + + const moveCategories = useMoves(); + const { visibleMoveCategoryIds, visibleMoveIds, isEmpty } = useMemo(() => { + const visibleCategories: Record = {}; + const visibleMoves: Record = {}; + let isEmpty: boolean = true; + + moveCategories.forEach((category) => { if ( - category.Title.Standard.toLocaleLowerCase().includes( - debouncedSearch.toLocaleLowerCase() + !search || + (category.Title.Standard.toLocaleLowerCase().includes( + search.toLocaleLowerCase() ) && - Object.keys(category.Moves).length > 0 + Object.keys(category.Moves).length > 0) ) { - results.push(category); + visibleCategories[category.$id] = CATEGORY_VISIBILITY.ALL; + isEmpty = false; return; } - const Moves: { [key: string]: Move } = {}; + let hasMove = false; Object.keys(category.Moves).forEach((moveId) => { const move = category.Moves[moveId]; if ( move.Title.Standard.toLocaleLowerCase().includes( - debouncedSearch.toLocaleLowerCase() + search.toLocaleLowerCase() ) ) { - Moves[moveId] = move; + hasMove = true; + visibleMoves[move.$id] = true; } }); - if (Object.keys(Moves).length > 0) { - results.push({ ...category, Moves }); + if (hasMove) { + isEmpty = false; + visibleCategories[category.$id] = CATEGORY_VISIBILITY.SOME; + } else { + visibleCategories[category.$id] = CATEGORY_VISIBILITY.HIDDEN; } }); - setFilteredMoves(results); - }, [debouncedSearch, customMoveCategories, showDelveMoves]); + return { + visibleMoveCategoryIds: visibleCategories, + visibleMoveIds: visibleMoves, + isEmpty, + }; + }, [moveCategories, search]); - return { setSearch, filteredMoves, isSearchActive: !!search }; + return { + moveCategories, + setSearch, + visibleMoveCategoryIds, + visibleMoveIds, + isSearchActive: !!search, + isEmpty, + }; } diff --git a/src/components/features/charactersAndCampaigns/MovesSection/useMoves.ts b/src/components/features/charactersAndCampaigns/MovesSection/useMoves.ts new file mode 100644 index 00000000..bc92ff86 --- /dev/null +++ b/src/components/features/charactersAndCampaigns/MovesSection/useMoves.ts @@ -0,0 +1,21 @@ +import { useMemo } from "react"; +import { orderedCategories } from "data/moves"; +import { useCustomMoves } from "./useCustomMoves"; +import { useStore } from "stores/store"; + +export function useMoves() { + const { customMoveCategories } = useCustomMoves(); + + const showDelveMoves = useStore( + (store) => store.settings.delve.showDelveMoves + ); + + const moveCategories = useMemo(() => { + return [...orderedCategories, ...customMoveCategories].filter( + (category) => + showDelveMoves || category.Source.Title !== "Ironsworn: Delve" + ); + }, [customMoveCategories, showDelveMoves]); + + return moveCategories; +} diff --git a/src/components/features/charactersAndCampaigns/OracleSection/OracleCategory.tsx b/src/components/features/charactersAndCampaigns/OracleSection/OracleCategory.tsx index 8809c22a..4f7b3b23 100644 --- a/src/components/features/charactersAndCampaigns/OracleSection/OracleCategory.tsx +++ b/src/components/features/charactersAndCampaigns/OracleSection/OracleCategory.tsx @@ -7,15 +7,19 @@ import { useStore } from "stores/store"; import { useNewMoveOracleView } from "hooks/featureFlags/useNewMoveOracleView"; import { useEffect, useState } from "react"; import { CollapsibleSectionHeader } from "../CollapsibleSectionHeader"; +import { CATEGORY_VISIBILITY } from "./useFilterOracles"; export interface OracleCategoryProps { prefix?: string; category: OracleSet; forceOpen?: boolean; + visibleCategories: Record; + visibleOracles: Record; } export function OracleCategory(props: OracleCategoryProps) { - const { prefix, category, forceOpen } = props; + const { prefix, category, forceOpen, visibleCategories, visibleOracles } = + props; const { rollOracleTable } = useRoller(); const openDialog = useStore((store) => store.appState.openDialog); @@ -34,7 +38,10 @@ export function OracleCategory(props: OracleCategoryProps) { const isExpandedOrForced = isExpanded || forceOpen || false; - if (hiddenOracleCategoryIds[category.$id]) { + if ( + hiddenOracleCategoryIds[category.$id] || + visibleCategories[category.$id] === CATEGORY_VISIBILITY.HIDDEN + ) { return null; } @@ -108,7 +115,13 @@ export function OracleCategory(props: OracleCategoryProps) { return ( - + ); })} diff --git a/src/components/features/charactersAndCampaigns/OracleSection/OracleSection.tsx b/src/components/features/charactersAndCampaigns/OracleSection/OracleSection.tsx index 4954b4fc..25f20b1d 100644 --- a/src/components/features/charactersAndCampaigns/OracleSection/OracleSection.tsx +++ b/src/components/features/charactersAndCampaigns/OracleSection/OracleSection.tsx @@ -3,9 +3,17 @@ import { OracleCategory } from "./OracleCategory"; import SearchIcon from "@mui/icons-material/Search"; import { useFilterOracles } from "./useFilterOracles"; import { AskTheOracleButtons } from "./AskTheOracleButtons"; +import { EmptyState } from "components/shared/EmptyState"; export function OracleSection() { - const { isSearchActive, filteredOracles, setSearch } = useFilterOracles(); + const { + oracleCategories, + isSearchActive, + visibleOracleCategoryIds, + visibleOracleIds, + setSearch, + isEmpty, + } = useFilterOracles(); return ( <> @@ -44,13 +52,19 @@ export function OracleSection() { })} /> - {filteredOracles.map((category, index) => ( - - ))} + {!isEmpty ? ( + oracleCategories.map((category, index) => ( + + )) + ) : ( + + )} ); diff --git a/src/components/features/charactersAndCampaigns/OracleSection/useFilterOracles.ts b/src/components/features/charactersAndCampaigns/OracleSection/useFilterOracles.ts index 359a1d17..2db3033b 100644 --- a/src/components/features/charactersAndCampaigns/OracleSection/useFilterOracles.ts +++ b/src/components/features/charactersAndCampaigns/OracleSection/useFilterOracles.ts @@ -1,131 +1,107 @@ -import { useEffect, useMemo, useState } from "react"; -import { useSearch } from "hooks/useSearch"; -import type { OracleSet, OracleTable } from "dataforged"; -import { oracleMap, orderedCategories } from "data/oracles"; -import { License } from "types/Datasworn"; -import { useCustomOracles } from "./useCustomOracles"; +import { useMemo, useState } from "react"; +import type { OracleSet } from "dataforged"; +import { useOracles } from "./useOracles"; import { useStore } from "stores/store"; -import { useAppName } from "hooks/useAppName"; -export function useFilterOracles() { - const { search, setSearch, debouncedSearch } = useSearch(); - - const { customOracleCategories, allCustomOracleMap } = useCustomOracles(); - const pinnedOracles = useStore((store) => store.settings.pinnedOraclesIds); +export enum CATEGORY_VISIBILITY { + HIDDEN, + SOME, + ALL, +} +export function useFilterOracles() { + const [search, setSearch] = useState(""); + const oracleCategories = useOracles(); const showDelveOracles = useStore( (store) => store.settings.delve.showDelveOracles ); - const appName = useAppName(); - - const combinedOracles = useMemo(() => { - const pinnedOracleIds = Object.keys(pinnedOracles); - - const pinnedOracleTables: { [tableId: string]: OracleTable } = {}; + const { visibleOracleCategoryIds, visibleOracleIds, isEmpty } = + useMemo(() => { + const visibleCategories: Record = {}; + const visibleOracles: Record = {}; + let isEmpty: boolean = true; + + const isDelveSet = (set: OracleSet): boolean => { + return set.Source.Title === "Ironsworn: Delve"; + }; + + const hideRemainingDelveSets = (set: OracleSet): void => { + if (isDelveSet(set)) { + visibleCategories[set.$id] = CATEGORY_VISIBILITY.HIDDEN; + } else { + Object.values(set.Sets ?? {}).forEach((set) => { + hideRemainingDelveSets(set); + }); + } + }; - pinnedOracleIds.forEach((oracleId) => { - if (pinnedOracles[oracleId]) { - pinnedOracleTables[oracleId] = - oracleMap[oracleId] ?? allCustomOracleMap[oracleId]; - } - }); + const filterSet = (set: OracleSet): boolean => { + if (!showDelveOracles && isDelveSet(set)) { + visibleCategories[set.$id] = CATEGORY_VISIBILITY.HIDDEN; + return false; + } - const pinnedOracleSection: OracleSet | undefined = - Object.keys(pinnedOracleTables).length > 0 - ? { - $id: "ironsworn/oracles/pinned", - Title: { - $id: "ironsworn/oracles/pinned/title", - Short: "Pinned", - Standard: "Pinned Oracles", - Canonical: "Pinned Oracles", - }, - Tables: pinnedOracleTables, - Display: { - $id: "ironsworn/oracles/pinned/display", - }, - Ancestors: [], - Source: { - Title: appName, - Authors: [], - License: License.None, - }, + if ( + (Object.keys(set.Tables ?? {}).length > 0 || + Object.keys(set.Sets ?? {}).length > 0) && + (!search || + set.Title.Standard.toLocaleLowerCase().includes( + search.toLocaleLowerCase() + )) + ) { + isEmpty = false; + visibleCategories[set.$id] = CATEGORY_VISIBILITY.ALL; + if (!showDelveOracles) { + hideRemainingDelveSets(set); } - : undefined; - return pinnedOracleSection - ? [pinnedOracleSection, ...orderedCategories] - : [...orderedCategories]; - }, [pinnedOracles, appName, allCustomOracleMap]); - - const [filteredOracles, setFilteredOracles] = useState(combinedOracles); - - useEffect(() => { - const allOracles = [...combinedOracles, ...customOracleCategories].filter( - (category) => - showDelveOracles || category.Source.Title !== "Ironsworn: Delve" - ); - - const results: OracleSet[] = []; + return true; + } + let hasOracles = false; - const filterSet = (set: OracleSet): OracleSet | undefined => { - if ( - set.Title.Standard.toLocaleLowerCase().includes( - debouncedSearch.toLocaleLowerCase() - ) && - (Object.keys(set.Tables ?? {}).length > 0 || - Object.keys(set.Sets ?? {}).length > 0) - ) { - return set; - } else { - const filteredSets: { [key: string]: OracleSet } = {}; - Object.keys(set.Sets ?? {}).forEach((setId) => { - const newSet = filterSet(set.Sets?.[setId] as OracleSet); - if (newSet) { - filteredSets[setId] = newSet; + Object.values(set.Sets ?? {}).forEach((set) => { + if (filterSet(set)) { + hasOracles = true; } }); - const filteredTables: { [key: string]: OracleTable } = {}; - Object.keys(set.Tables ?? {}).forEach((tableId) => { - const table = (set.Tables ?? {})[tableId]; + Object.values(set.Tables ?? {}).forEach((table) => { if ( table && table.Title.Short.toLocaleLowerCase().includes( - debouncedSearch.toLocaleLowerCase() + search.toLocaleLowerCase() ) ) { - filteredTables[tableId] = table; + visibleOracles[table.$id] = true; + hasOracles = true; } }); - if ( - Object.keys(filteredSets).length > 0 || - Object.keys(filteredTables).length > 0 - ) { - return { - ...set, - Sets: filteredSets, - Tables: filteredTables, - }; + if (hasOracles) { + isEmpty = false; + visibleCategories[set.$id] = CATEGORY_VISIBILITY.SOME; + } else { + visibleCategories[set.$id] = CATEGORY_VISIBILITY.HIDDEN; } - return undefined; - } - }; - - allOracles.forEach((oracleSection) => { - const filteredSection = filterSet(oracleSection); - - if (filteredSection) { - results.push(filteredSection); - } - }); - setFilteredOracles(results); - }, [ - debouncedSearch, - combinedOracles, - customOracleCategories, - showDelveOracles, - ]); - return { isSearchActive: !!search, setSearch, filteredOracles }; + return hasOracles; + }; + oracleCategories.forEach((category) => { + filterSet(category); + }); + + return { + visibleOracleCategoryIds: visibleCategories, + visibleOracleIds: visibleOracles, + isEmpty, + }; + }, [oracleCategories, search, showDelveOracles]); + + return { + oracleCategories, + setSearch, + visibleOracleCategoryIds, + visibleOracleIds, + isSearchActive: !!search, + isEmpty, + }; } diff --git a/src/components/features/charactersAndCampaigns/OracleSection/useOracles.ts b/src/components/features/charactersAndCampaigns/OracleSection/useOracles.ts new file mode 100644 index 00000000..92bf048e --- /dev/null +++ b/src/components/features/charactersAndCampaigns/OracleSection/useOracles.ts @@ -0,0 +1,58 @@ +import { useStore } from "stores/store"; +import { useCustomOracles } from "./useCustomOracles"; +import { useMemo } from "react"; +import { orderedCategories, oracleMap } from "data/oracles"; +import { OracleSet, OracleTable } from "dataforged"; +import { License } from "types/Datasworn"; +import { useAppName } from "hooks/useAppName"; + +export function useOracles() { + const appName = useAppName(); + const { customOracleCategories, allCustomOracleMap } = useCustomOracles(); + + const pinnedOracles = useStore((store) => store.settings.pinnedOraclesIds); + + const oracleCategories = useMemo(() => { + const pinnedOracleTables: { [tableId: string]: OracleTable } = {}; + Object.keys(pinnedOracles).forEach((id) => { + if (pinnedOracles[id] && (oracleMap[id] ?? allCustomOracleMap[id])) { + pinnedOracleTables[id] = oracleMap[id] ?? allCustomOracleMap[id]; + } + }); + + const pinnedOracleSection: OracleSet | undefined = + Object.keys(pinnedOracleTables).length > 0 + ? { + $id: "ironsworn/oracles/pinned", + Title: { + $id: "ironsworn/oracles/pinned/title", + Short: "Pinned", + Standard: "Pinned Oracles", + Canonical: "Pinned Oracles", + }, + Tables: pinnedOracleTables, + Display: { + $id: "ironsworn/oracles/pinned/display", + }, + Ancestors: [], + Source: { + Title: appName, + Authors: [], + License: License.None, + }, + } + : undefined; + + let categories: OracleSet[] = [ + ...orderedCategories, + ...customOracleCategories, + ]; + if (pinnedOracleSection) { + categories = [pinnedOracleSection, ...categories]; + } + + return categories; + }, [customOracleCategories, pinnedOracles, allCustomOracleMap, appName]); + + return oracleCategories; +} diff --git a/src/hooks/featureFlags/activeFeatureFlags.ts b/src/hooks/featureFlags/activeFeatureFlags.ts index 99ff18ba..3449ec61 100644 --- a/src/hooks/featureFlags/activeFeatureFlags.ts +++ b/src/hooks/featureFlags/activeFeatureFlags.ts @@ -1,6 +1,6 @@ export const activeFeatureFlags: { testId: string; label: string }[] = [ - { - testId: "new-move-oracle-view", - label: "Collapsible Move and Oracle Categories", - }, + // { + // testId: "new-move-oracle-view", + // label: "Collapsible Move and Oracle Categories", + // }, ]; diff --git a/src/hooks/featureFlags/useNewMoveOracleView.ts b/src/hooks/featureFlags/useNewMoveOracleView.ts index 97f4a57b..07f4597c 100644 --- a/src/hooks/featureFlags/useNewMoveOracleView.ts +++ b/src/hooks/featureFlags/useNewMoveOracleView.ts @@ -1,5 +1,6 @@ -import { useFeatureFlag } from "./useFeatureFlag"; +// import { useFeatureFlag } from "./useFeatureFlag"; export function useNewMoveOracleView() { - return useFeatureFlag("new-move-oracle-view"); + return true; + // return useFeatureFlag("new-move-oracle-view"); } diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts index dcf14410..852906db 100644 --- a/src/hooks/useSearch.ts +++ b/src/hooks/useSearch.ts @@ -1,23 +1,23 @@ import { useEffect, useState } from "react"; -export function useSearchNoState(search: string) { +export function useSearchNoState(search: string, debounceTime: number = 500) { const [debouncedSearch, setDebouncedSearch] = useState(search); useEffect(() => { const timeout = setTimeout(() => { setDebouncedSearch(search); - }, 500); + }, debounceTime); return () => { clearTimeout(timeout); }; - }, [search]); + }, [debounceTime, search]); return { debouncedSearch }; } -export function useSearch() { +export function useSearch(debounceTime?: number) { const [search, setSearch] = useState(""); - const { debouncedSearch } = useSearchNoState(search); + const { debouncedSearch } = useSearchNoState(search, debounceTime); return { search, setSearch, debouncedSearch }; } diff --git a/src/pages/Campaign/CampaignGMScreenPage/CampaignGMScreenPage.tsx b/src/pages/Campaign/CampaignGMScreenPage/CampaignGMScreenPage.tsx index 4fc2a959..bd198979 100644 --- a/src/pages/Campaign/CampaignGMScreenPage/CampaignGMScreenPage.tsx +++ b/src/pages/Campaign/CampaignGMScreenPage/CampaignGMScreenPage.tsx @@ -5,7 +5,6 @@ import { useNavigate } from "react-router-dom"; import { TabsSection } from "./components/TabsSection"; import { CAMPAIGN_ROUTES, - constructCampaignPath, constructCampaignSheetPath, } from "pages/Campaign/routes"; import { PageContent, PageHeader } from "components/shared/Layout"; @@ -14,6 +13,7 @@ import { useSyncStore } from "./hooks/useSyncStore"; import { useStore } from "stores/store"; import { Sidebar } from "pages/Character/CharacterSheetPage/components/Sidebar"; import { SectionWithSidebar } from "components/shared/Layout/SectionWithSidebar"; +import { EmptyState } from "components/shared/EmptyState"; export function CampaignGMScreenPage() { useSyncStore(); @@ -33,10 +33,6 @@ export function CampaignGMScreenPage() { const navigate = useNavigate(); useEffect(() => { - if (!loading && (!campaignId || !campaigns[campaignId])) { - error("You aren't a member of this campaign"); - navigate(constructCampaignPath(CAMPAIGN_ROUTES.SELECT)); - } if ( !loading && campaignId && @@ -66,7 +62,9 @@ export function CampaignGMScreenPage() { } if (!campaign || !uid || !campaign?.gmIds?.includes(uid)) { - return null; + return ( + + ); } return (