pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor overlay stack and modals to allow multiple and better navigation

overlays will not close previous ones so that they don't conflict and there can essentially be unlimited modal navigations. Done by passing the modal data to each modal using a global hook instead of relying on local data for each.
When navigating to a new path, it closes all modals.
On CollectionOverlay, when opening a new details modal overlay, it closes the previous using a custom ShowDetails handler.

This isn't the cleanest approach, but offers the greatest flexibility in the future

Pas ba594056 61593e7f

+108 -106
+7 -15
src/components/media/MediaCard.tsx
··· 16 16 import { MediaBookmarkButton } from "./MediaBookmark"; 17 17 import { IconPatch } from "../buttons/IconPatch"; 18 18 import { Icon, Icons } from "../Icon"; 19 - import { DetailsModal } from "../overlays/detailsModal"; 20 19 21 20 // Intersection Observer Hook 22 21 function useIntersectionObserver(options: IntersectionObserverInit = {}) { ··· 300 299 301 300 export function MediaCard(props: MediaCardProps) { 302 301 const { media, onShowDetails, forceSkeleton } = props; 303 - const [detailsData, setDetailsData] = useState<{ 304 - id: number; 305 - type: "movie" | "show"; 306 - } | null>(null); 307 302 const { showModal } = useOverlayStack(); 308 303 const enableDetailsModal = usePreferencesStore( 309 304 (state) => state.enableDetailsModal, ··· 335 330 return; 336 331 } 337 332 338 - setDetailsData({ 333 + // Show modal with data through overlayStack 334 + showModal("details", { 339 335 id: Number(media.id), 340 336 type: media.type === "movie" ? "movie" : "show", 341 337 }); 342 - showModal("details"); 343 338 }, [media, showModal, onShowDetails]); 344 339 345 340 const handleCardClick = (e: React.MouseEvent) => { ··· 355 350 }; 356 351 357 352 const content = ( 358 - <> 359 - <MediaCardContent 360 - {...props} 361 - onShowDetails={handleShowDetails} 362 - forceSkeleton={forceSkeleton} 363 - /> 364 - {detailsData && <DetailsModal id="details" data={detailsData} />} 365 - </> 353 + <MediaCardContent 354 + {...props} 355 + onShowDetails={handleShowDetails} 356 + forceSkeleton={forceSkeleton} 357 + /> 366 358 ); 367 359 368 360 if (!canLink) {
+14 -7
src/components/overlays/detailsModal/components/layout/DetailsModal.tsx
··· 23 23 import { OverlayPortal } from "../../../OverlayDisplay"; 24 24 import { DetailsModalProps } from "../../types"; 25 25 26 - export function DetailsModal({ id, data, minimal }: DetailsModalProps) { 27 - const { hideModal, isModalVisible, modalStack } = useOverlayStack(); 26 + export function DetailsModal({ id, data: _data, minimal }: DetailsModalProps) { 27 + const { hideModal, isModalVisible, modalStack, getModalData } = 28 + useOverlayStack(); 28 29 const [detailsData, setDetailsData] = useState<any>(null); 29 30 const [isLoading, setIsLoading] = useState(false); 30 31 ··· 33 34 34 35 const hide = useCallback(() => hideModal(id), [hideModal, id]); 35 36 const isShown = isModalVisible(id); 37 + const modalData = getModalData(id); 38 + 39 + // Only show modal if there's data to display 40 + const shouldShow = Boolean(isShown && (modalData?.id || _data?.id)); 36 41 37 42 useEffect(() => { 38 43 const fetchDetails = async () => { 44 + // Use data from overlayStack or fallback to props for backward compatibility 45 + const data = modalData || _data; 39 46 if (!data?.id || !data?.type) return; 40 47 41 48 setIsLoading(true); ··· 113 120 } 114 121 }; 115 122 116 - if (isShown && data?.id) { 123 + if (shouldShow) { 117 124 fetchDetails(); 118 125 } 119 - }, [isShown, data]); 126 + }, [shouldShow, modalData, _data]); 120 127 121 128 useEffect(() => { 122 - if (isShown && !data?.id && !isLoading) { 129 + if (isShown && !modalData?.id && !_data?.id && !isLoading) { 123 130 hide(); 124 131 } 125 - }, [isShown, data, isLoading, hide]); 132 + }, [isShown, modalData, _data, isLoading, hide]); 126 133 127 134 return ( 128 135 <OverlayPortal 129 136 darken 130 137 close={hide} 131 - show={isShown} 138 + show={shouldShow} 132 139 durationClass="duration-500" 133 140 zIndex={zIndex} 134 141 >
+16 -9
src/components/overlays/detailsModal/components/overlays/CollectionOverlay.tsx
··· 9 9 import { Flare } from "@/components/utils/Flare"; 10 10 import { useIsMobile } from "@/hooks/useIsMobile"; 11 11 import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons"; 12 + import { useOverlayStack } from "@/stores/interface/overlayStack"; 12 13 import { MediaItem } from "@/utils/mediaTypes"; 13 14 14 15 // Simple carousel component for collection overlay 15 16 interface SimpleCarouselProps { 16 17 mediaItems: MediaItem[]; 17 - onShowDetails: (movieId: number) => void; 18 + onShowDetails?: (media: MediaItem) => void; 18 19 categorySlug?: string; 19 20 } 20 21 21 22 function SimpleCarousel({ 22 23 mediaItems, 23 - onShowDetails, 24 + onShowDetails: _onShowDetails, 24 25 categorySlug = "collection", 25 26 }: SimpleCarouselProps) { 26 27 const { isMobile } = useIsMobile(); ··· 56 57 className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto" 57 58 style={{ scrollSnapAlign: "start" }} 58 59 > 59 - <MediaCard 60 - media={media} 61 - onShowDetails={() => onShowDetails(Number(media.id))} 62 - linkable 63 - /> 60 + <MediaCard media={media} linkable onShowDetails={_onShowDetails} /> 64 61 </div> 65 62 ))} 66 63 ··· 108 105 collectionId, 109 106 collectionName, 110 107 onClose, 111 - onMovieClick, 108 + onMovieClick: _onMovieClick, 112 109 }: CollectionOverlayProps) { 113 110 const { t } = useTranslation(); 111 + const { showModal } = useOverlayStack(); 114 112 const [collection, setCollection] = useState<CollectionData | null>(null); 115 113 const [loading, setLoading] = useState(true); 116 114 const [error, setError] = useState<string | null>(null); ··· 161 159 ? new Date(movie.release_date) 162 160 : undefined, 163 161 }; 162 + }; 163 + 164 + const handleShowDetails = (media: MediaItem) => { 165 + // Show details modal and close collection overlay 166 + showModal("details", { 167 + id: Number(media.id), 168 + type: media.type === "movie" ? "movie" : "show", 169 + }); 170 + onClose(); 164 171 }; 165 172 166 173 return ( ··· 311 318 {!loading && !error && sortedMovies.length > 0 && ( 312 319 <SimpleCarousel 313 320 mediaItems={sortedMovies.map(movieToMediaItem)} 314 - onShowDetails={onMovieClick} 321 + onShowDetails={handleShowDetails} 315 322 categorySlug="collection" 316 323 /> 317 324 )}
+12 -23
src/components/player/internals/InfoButton.tsx
··· 1 - import { useEffect, useState } from "react"; 1 + import { useEffect } from "react"; 2 2 3 3 import { Icons } from "@/components/Icon"; 4 - import { DetailsModal } from "@/components/overlays/detailsModal"; 5 - import { useModal } from "@/components/overlays/Modal"; 4 + import { useOverlayStack } from "@/stores/interface/overlayStack"; 6 5 import { usePlayerStore } from "@/stores/player/store"; 7 6 import { usePreferencesStore } from "@/stores/preferences"; 8 7 ··· 10 9 11 10 export function InfoButton() { 12 11 const meta = usePlayerStore((s) => s.meta); 13 - const modal = useModal("player-details"); 12 + const { showModal, isModalVisible } = useOverlayStack(); 14 13 const setHasOpenOverlay = usePlayerStore((s) => s.setHasOpenOverlay); 15 - const [detailsData, setDetailsData] = useState<{ 16 - id: number; 17 - type: "movie" | "show"; 18 - } | null>(null); 19 14 20 15 useEffect(() => { 21 - setHasOpenOverlay(modal.isShown); 22 - }, [setHasOpenOverlay, modal.isShown]); 16 + setHasOpenOverlay(isModalVisible("player-details")); 17 + }, [setHasOpenOverlay, isModalVisible]); 23 18 24 19 const handleClick = async () => { 25 20 if (!meta?.tmdbId) return; 26 21 27 - setDetailsData({ 22 + showModal("player-details", { 28 23 id: Number(meta.tmdbId), 29 24 type: meta.type === "movie" ? "movie" : "show", 30 25 }); 31 - modal.show(); 32 26 }; 33 27 34 28 const enableLowPerformanceMode = usePreferencesStore( ··· 40 34 } 41 35 42 36 return ( 43 - <> 44 - <VideoPlayerButton 45 - icon={Icons.CIRCLE_QUESTION} 46 - iconSizeClass="text-base" 47 - className="p-2 !-mr-2" 48 - onClick={handleClick} 49 - /> 50 - {detailsData && ( 51 - <DetailsModal id="player-details" data={detailsData} minimal /> 52 - )} 53 - </> 37 + <VideoPlayerButton 38 + icon={Icons.CIRCLE_QUESTION} 39 + iconSizeClass="text-base" 40 + className="p-2 !-mr-2" 41 + onClick={handleClick} 42 + /> 54 43 ); 55 44 }
+1 -6
src/pages/HomePage.tsx
··· 4 4 import { To, useNavigate } from "react-router-dom"; 5 5 6 6 import { WideContainer } from "@/components/layout/WideContainer"; 7 - import { DetailsModal } from "@/components/overlays/detailsModal"; 8 7 import { useDebounce } from "@/hooks/useDebounce"; 9 8 import { useRandomTranslation } from "@/hooks/useRandomTranslation"; 10 9 import { useSearchQuery } from "@/hooks/useSearchQuery"; ··· 62 61 const s = useSearch(search); 63 62 const [showBookmarks, setShowBookmarks] = useState(false); 64 63 const [showWatching, setShowWatching] = useState(false); 65 - const [detailsData, setDetailsData] = useState<any>(); 66 64 const { showModal } = useOverlayStack(); 67 65 const enableDiscover = usePreferencesStore((state) => state.enableDiscover); 68 66 const enableFeatured = usePreferencesStore((state) => state.enableFeatured); ··· 83 81 }; 84 82 85 83 const handleShowDetails = async (media: MediaItem | FeaturedMedia) => { 86 - setDetailsData({ 84 + showModal("details", { 87 85 id: Number(media.id), 88 86 type: media.type === "movie" ? "movie" : "show", 89 87 }); 90 - showModal("details"); 91 88 }; 92 89 93 90 const renderHomeSections = () => { ··· 235 232 </div> 236 233 )} 237 234 </WideContainer> 238 - 239 - {detailsData && <DetailsModal id="details" data={detailsData} />} 240 235 </HomeLayout> 241 236 ); 242 237 }
+1 -6
src/pages/bookmarks/AllBookmarks.tsx
··· 12 12 import { WideContainer } from "@/components/layout/WideContainer"; 13 13 import { MediaGrid } from "@/components/media/MediaGrid"; 14 14 import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; 15 - import { DetailsModal } from "@/components/overlays/detailsModal"; 16 15 import { EditGroupOrderModal } from "@/components/overlays/EditGroupOrderModal"; 17 16 import { useModal } from "@/components/overlays/Modal"; 18 17 import { UserIcon, UserIcons } from "@/components/UserIcon"; ··· 58 57 const [tempGroupOrder, setTempGroupOrder] = useState<string[]>([]); 59 58 const backendUrl = useBackendUrl(); 60 59 const account = useAuthStore((s) => s.account); 61 - const [detailsData, setDetailsData] = useState<any>(); 62 60 const { showModal } = useOverlayStack(); 63 61 64 62 const handleShowDetails = async (media: MediaItem) => { 65 63 if (onShowDetails) { 66 64 onShowDetails(media); 67 65 } else { 68 - setDetailsData({ 66 + showModal("details", { 69 67 id: Number(media.id), 70 68 type: media.type === "movie" ? "movie" : "show", 71 69 }); 72 - showModal("details"); 73 70 } 74 71 }; 75 72 ··· 431 428 setTempGroupOrder(newOrder); 432 429 }} 433 430 /> 434 - 435 - {detailsData && <DetailsModal id="details" data={detailsData} />} 436 431 </WideContainer> 437 432 </SubPageLayout> 438 433 );
+3 -7
src/pages/discover/AllMovieLists.tsx
··· 11 11 import { Icon, Icons } from "@/components/Icon"; 12 12 import { WideContainer } from "@/components/layout/WideContainer"; 13 13 import { MediaCard } from "@/components/media/MediaCard"; 14 - import { DetailsModal } from "@/components/overlays/detailsModal"; 15 - import { useModal } from "@/components/overlays/Modal"; 16 14 import { Heading1 } from "@/components/utils/Text"; 17 15 import { useIsMobile } from "@/hooks/useIsMobile"; 18 16 import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons"; 19 17 import { SubPageLayout } from "@/pages/layouts/SubPageLayout"; 20 18 import { useDiscoverStore } from "@/stores/discover"; 19 + import { useOverlayStack } from "@/stores/interface/overlayStack"; 21 20 import { MediaItem } from "@/utils/mediaTypes"; 22 21 23 22 import { MediaCarousel } from "./components/MediaCarousel"; 24 23 25 24 export function DiscoverMore() { 26 - const [detailsData, setDetailsData] = useState<any>(); 27 25 const [curatedLists, setCuratedLists] = useState<CuratedMovieList[]>([]); 28 26 const [movieDetails, setMovieDetails] = useState<{ 29 27 [listSlug: string]: TMDBMovieData[]; 30 28 }>({}); 31 - const detailsModal = useModal("discover-details"); 29 + const { showModal } = useOverlayStack(); 32 30 const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); 33 31 const navigate = useNavigate(); 34 32 const { lastView } = useDiscoverStore(); ··· 65 63 }, []); 66 64 67 65 const handleShowDetails = async (media: MediaItem) => { 68 - setDetailsData({ 66 + showModal("discover-details", { 69 67 id: Number(media.id), 70 68 type: media.type === "movie" ? "movie" : "show", 71 69 }); 72 - detailsModal.show(); 73 70 }; 74 71 75 72 const handleBack = () => { ··· 183 180 </div> 184 181 ))} 185 182 </WideContainer> 186 - {detailsData && <DetailsModal id="discover-details" data={detailsData} />} 187 183 </SubPageLayout> 188 184 ); 189 185 }
+3 -16
src/pages/discover/Discover.tsx
··· 1 - import { useEffect, useState } from "react"; 2 1 import { Helmet } from "react-helmet-async"; 3 2 4 - import { DetailsModal } from "@/components/overlays/detailsModal"; 5 - import { useModal } from "@/components/overlays/Modal"; 3 + import { useOverlayStack } from "@/stores/interface/overlayStack"; 6 4 7 5 import { SubPageLayout } from "../layouts/SubPageLayout"; 8 6 import { FeaturedCarousel } from "./components/FeaturedCarousel"; ··· 11 9 import { PageTitle } from "../parts/util/PageTitle"; 12 10 13 11 export function Discover() { 14 - const [detailsData, setDetailsData] = useState<any>(); 15 - const detailsModal = useModal("discover-details"); 16 - 17 - // Clear details data when modal is closed 18 - useEffect(() => { 19 - if (!detailsModal.isShown) { 20 - setDetailsData(undefined); 21 - } 22 - }, [detailsModal.isShown]); 12 + const { showModal } = useOverlayStack(); 23 13 24 14 const handleShowDetails = (media: FeaturedMedia) => { 25 - setDetailsData({ 15 + showModal("discover-details", { 26 16 id: Number(media.id), 27 17 type: media.type, 28 18 }); 29 - detailsModal.show(); 30 19 }; 31 20 32 21 return ( ··· 52 41 <div className="relative z-20 px-4 md:px-10"> 53 42 <DiscoverContent /> 54 43 </div> 55 - 56 - {detailsData && <DetailsModal id="discover-details" data={detailsData} />} 57 44 </SubPageLayout> 58 45 ); 59 46 }
+3 -7
src/pages/discover/MoreContent.tsx
··· 9 9 import { WideContainer } from "@/components/layout/WideContainer"; 10 10 import { MediaCard } from "@/components/media/MediaCard"; 11 11 import { MediaGrid } from "@/components/media/MediaGrid"; 12 - import { DetailsModal } from "@/components/overlays/detailsModal"; 13 - import { useModal } from "@/components/overlays/Modal"; 14 12 import { Heading1 } from "@/components/utils/Text"; 15 13 import { 16 14 DiscoverContentType, ··· 20 18 } from "@/pages/discover/hooks/useDiscoverMedia"; 21 19 import { SubPageLayout } from "@/pages/layouts/SubPageLayout"; 22 20 import { useDiscoverStore } from "@/stores/discover"; 21 + import { useOverlayStack } from "@/stores/interface/overlayStack"; 23 22 import { useProgressStore } from "@/stores/progress"; 24 23 import { MediaItem } from "@/utils/mediaTypes"; 25 24 ··· 30 29 export function MoreContent({ onShowDetails }: MoreContentProps) { 31 30 const { mediaType = "movie", contentType, id, category } = useParams(); 32 31 const [currentPage, setCurrentPage] = useState(1); 33 - const [detailsData, setDetailsData] = useState<any>(); 34 32 const [selectedProvider, setSelectedProvider] = useState<OptionItem | null>( 35 33 null, 36 34 ); ··· 40 38 const [isContentVisible, setIsContentVisible] = useState(false); 41 39 const { t } = useTranslation(); 42 40 const navigate = useNavigate(); 43 - const detailsModal = useModal("discover-details"); 41 + const { showModal } = useOverlayStack(); 44 42 const { lastView } = useDiscoverStore(); 45 43 const { width: windowWidth } = useWindowSize(); 46 44 const progressStore = useProgressStore(); ··· 113 111 onShowDetails(media); 114 112 return; 115 113 } 116 - setDetailsData({ 114 + showModal("discover-details", { 117 115 id: Number(media.id), 118 116 type: media.type === "movie" ? "movie" : "show", 119 117 }); 120 - detailsModal.show(); 121 118 }; 122 119 123 120 const handleLoadMore = async () => { ··· 386 383 )} 387 384 </div> 388 385 </WideContainer> 389 - {detailsData && <DetailsModal id="discover-details" data={detailsData} />} 390 386 </SubPageLayout> 391 387 ); 392 388 }
+5 -8
src/pages/discover/discoverContent.tsx
··· 1 1 import classNames from "classnames"; 2 2 import { t } from "i18next"; 3 - import { useRef, useState } from "react"; 3 + import { useRef } from "react"; 4 4 import { useNavigate } from "react-router-dom"; 5 5 6 6 import { Button } from "@/components/buttons/Button"; 7 7 import { WideContainer } from "@/components/layout/WideContainer"; 8 - import { DetailsModal } from "@/components/overlays/detailsModal"; 9 - import { useModal } from "@/components/overlays/Modal"; 10 8 import { useDiscoverStore } from "@/stores/discover"; 9 + import { useOverlayStack } from "@/stores/interface/overlayStack"; 11 10 import { useProgressStore } from "@/stores/progress"; 12 11 import { MediaItem } from "@/utils/mediaTypes"; 13 12 ··· 18 17 19 18 export function DiscoverContent() { 20 19 const { selectedCategory, setSelectedCategory } = useDiscoverStore(); 21 - const [detailsData, setDetailsData] = useState<any>(); 22 20 const navigate = useNavigate(); 23 - const detailsModal = useModal("discover-details"); 21 + const { showModal } = useOverlayStack(); 24 22 const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); 25 23 const progressItems = useProgressStore((state) => state.items); 26 24 ··· 34 32 }; 35 33 36 34 const handleShowDetails = async (media: MediaItem | FeaturedMedia) => { 37 - setDetailsData({ 35 + showModal("discover-details", { 38 36 id: Number(media.id), 39 37 type: media.type === "movie" ? "movie" : "show", 40 38 }); 41 - detailsModal.show(); 42 39 }; 43 40 44 41 const movieProgressItems = Object.entries(progressItems || {}).filter( ··· 240 237 241 238 <ScrollToTopButton /> 242 239 243 - {detailsData && <DetailsModal id="discover-details" data={detailsData} />} 240 + {/* DetailsModal is now managed by overlayStack */} 244 241 </div> 245 242 ); 246 243 }
+6
src/setup/App.tsx
··· 11 11 12 12 import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta"; 13 13 import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb"; 14 + import { DetailsModal } from "@/components/overlays/detailsModal"; 14 15 import { KeyboardCommandsModal } from "@/components/overlays/KeyboardCommandsModal"; 15 16 import { NotificationModal } from "@/components/overlays/notificationsModal"; 16 17 import { useGlobalKeyboardEvents } from "@/hooks/useGlobalKeyboardEvents"; ··· 39 40 import { SupportPage } from "@/pages/Support"; 40 41 import { Layout } from "@/setup/Layout"; 41 42 import { useHistoryListener } from "@/stores/history"; 43 + import { useClearModalsOnNavigation } from "@/stores/interface/overlayStack"; 42 44 import { LanguageProvider } from "@/stores/language"; 43 45 44 46 const DeveloperPage = lazy(() => import("@/pages/DeveloperPage")); ··· 103 105 useHistoryListener(); 104 106 useOnlineListener(); 105 107 useGlobalKeyboardEvents(); 108 + useClearModalsOnNavigation(); 106 109 const maintenance = false; // Shows maintance page 107 110 const [showDowntime, setShowDowntime] = useState(maintenance); 108 111 ··· 123 126 <LanguageProvider /> 124 127 <NotificationModal id="notifications" /> 125 128 <KeyboardCommandsModal id="keyboard-commands" /> 129 + <DetailsModal id="details" /> 130 + <DetailsModal id="discover-details" /> 131 + <DetailsModal id="player-details" /> 126 132 {!showDowntime && ( 127 133 <Routes> 128 134 {/* functional routes */}
+37 -2
src/stores/interface/overlayStack.ts
··· 1 + import { useEffect } from "react"; 2 + import { useLocation } from "react-router-dom"; 1 3 import { create } from "zustand"; 2 4 import { immer } from "zustand/middleware/immer"; 3 5 4 6 type OverlayType = "volume" | "subtitle" | "speed" | null; 5 7 8 + interface ModalData { 9 + id: number; 10 + type: "movie" | "show"; 11 + [key: string]: any; 12 + } 13 + 6 14 interface OverlayStackStore { 7 15 currentOverlay: OverlayType; 8 16 modalStack: string[]; 17 + modalData: Record<string, ModalData | undefined>; 9 18 setCurrentOverlay: (overlay: OverlayType) => void; 10 - showModal: (id: string) => void; 19 + showModal: (id: string, data?: ModalData) => void; 11 20 hideModal: (id: string) => void; 12 21 isModalVisible: (id: string) => boolean; 13 22 getTopModal: () => string | null; 23 + getModalData: (id: string) => ModalData | undefined; 24 + clearAllModals: () => void; 14 25 } 15 26 16 27 export const useOverlayStack = create<OverlayStackStore>()( 17 28 immer((set, get) => ({ 18 29 currentOverlay: null, 19 30 modalStack: [], 31 + modalData: {}, 20 32 setCurrentOverlay: (overlay) => 21 33 set((state) => { 22 34 state.currentOverlay = overlay; 23 35 }), 24 - showModal: (id: string) => 36 + showModal: (id: string, data?: ModalData) => 25 37 set((state) => { 26 38 if (!state.modalStack.includes(id)) { 27 39 state.modalStack.push(id); 28 40 } 41 + if (data) { 42 + state.modalData[id] = data; 43 + } 29 44 }), 30 45 hideModal: (id: string) => 31 46 set((state) => { 32 47 state.modalStack = state.modalStack.filter((modalId) => modalId !== id); 48 + delete state.modalData[id]; 33 49 }), 34 50 isModalVisible: (id: string) => { 35 51 return get().modalStack.includes(id); ··· 38 54 const stack = get().modalStack; 39 55 return stack.length > 0 ? stack[stack.length - 1] : null; 40 56 }, 57 + getModalData: (id: string) => { 58 + return get().modalData[id]; 59 + }, 60 + clearAllModals: () => 61 + set((state) => { 62 + state.modalStack = []; 63 + state.modalData = {}; 64 + state.currentOverlay = null; 65 + }), 41 66 })), 42 67 ); 68 + 69 + // Hook to clear modals on navigation 70 + export function useClearModalsOnNavigation() { 71 + const location = useLocation(); 72 + const clearAllModals = useOverlayStack((state) => state.clearAllModals); 73 + 74 + useEffect(() => { 75 + clearAllModals(); 76 + }, [location.pathname, clearAllModals]); 77 + }