| @@ -31,7 +31,7 @@ | |||||
| "react-helmet-async": "^2.0.5", | "react-helmet-async": "^2.0.5", | ||||
| "react-markdown": "^10.1.0", | "react-markdown": "^10.1.0", | ||||
| "react-markdown-editor-lite": "^1.3.4", | "react-markdown-editor-lite": "^1.3.4", | ||||
| "react-router-dom": "^6.30.0", | |||||
| "react-router-dom": "^6.30.1", | |||||
| "react-youtube": "^10.1.0", | "react-youtube": "^10.1.0", | ||||
| "remark-gfm": "^4.0.1", | "remark-gfm": "^4.0.1", | ||||
| "tailwind-merge": "^3.3.0", | "tailwind-merge": "^3.3.0", | ||||
| @@ -6030,9 +6030,9 @@ | |||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/react-router": { | "node_modules/react-router": { | ||||
| "version": "6.30.0", | |||||
| "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", | |||||
| "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", | |||||
| "version": "6.30.1", | |||||
| "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", | |||||
| "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", | |||||
| "license": "MIT", | "license": "MIT", | ||||
| "dependencies": { | "dependencies": { | ||||
| "@remix-run/router": "1.23.0" | "@remix-run/router": "1.23.0" | ||||
| @@ -6045,13 +6045,13 @@ | |||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/react-router-dom": { | "node_modules/react-router-dom": { | ||||
| "version": "6.30.0", | |||||
| "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", | |||||
| "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", | |||||
| "version": "6.30.1", | |||||
| "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", | |||||
| "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", | |||||
| "license": "MIT", | "license": "MIT", | ||||
| "dependencies": { | "dependencies": { | ||||
| "@remix-run/router": "1.23.0", | "@remix-run/router": "1.23.0", | ||||
| "react-router": "6.30.0" | |||||
| "react-router": "6.30.1" | |||||
| }, | }, | ||||
| "engines": { | "engines": { | ||||
| "node": ">=14.0.0" | "node": ">=14.0.0" | ||||
| @@ -33,7 +33,7 @@ | |||||
| "react-helmet-async": "^2.0.5", | "react-helmet-async": "^2.0.5", | ||||
| "react-markdown": "^10.1.0", | "react-markdown": "^10.1.0", | ||||
| "react-markdown-editor-lite": "^1.3.4", | "react-markdown-editor-lite": "^1.3.4", | ||||
| "react-router-dom": "^6.30.0", | |||||
| "react-router-dom": "^6.30.1", | |||||
| "react-youtube": "^10.1.0", | "react-youtube": "^10.1.0", | ||||
| "remark-gfm": "^4.0.1", | "remark-gfm": "^4.0.1", | ||||
| "tailwind-merge": "^3.3.0", | "tailwind-merge": "^3.3.0", | ||||
| @@ -1,5 +1,10 @@ | |||||
| import { AnimatePresence, LayoutGroup } from 'framer-motion' | |||||
| import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||
| import { BrowserRouter, Navigate, Route, Routes, useLocation } from 'react-router-dom' | |||||
| import { BrowserRouter, | |||||
| Navigate, | |||||
| Route, | |||||
| Routes, | |||||
| useLocation } from 'react-router-dom' | |||||
| import RouteBlockerOverlay from '@/components/RouteBlockerOverlay' | import RouteBlockerOverlay from '@/components/RouteBlockerOverlay' | ||||
| import TopNav from '@/components/TopNav' | import TopNav from '@/components/TopNav' | ||||
| @@ -20,11 +25,41 @@ import WikiHistoryPage from '@/pages/wiki/WikiHistoryPage' | |||||
| import WikiNewPage from '@/pages/wiki/WikiNewPage' | import WikiNewPage from '@/pages/wiki/WikiNewPage' | ||||
| import WikiSearchPage from '@/pages/wiki/WikiSearchPage' | import WikiSearchPage from '@/pages/wiki/WikiSearchPage' | ||||
| import type { FC } from 'react' | |||||
| import type { Dispatch, FC, SetStateAction } from 'react' | |||||
| import type { User } from '@/types' | import type { User } from '@/types' | ||||
| const RouteTransitionWrapper = ({ user, setUser }: { | |||||
| user: User | null | |||||
| setUser: Dispatch<SetStateAction<User | null>> }) => { | |||||
| const location = useLocation () | |||||
| return ( | |||||
| <LayoutGroup id="gallery-shared"> | |||||
| <AnimatePresence mode="wait"> | |||||
| <Routes location={location} key={location.pathname}> | |||||
| <Route path="/" element={<Navigate to="/posts" replace/>}/> | |||||
| <Route path="/posts" element={<PostListPage/>}/> | |||||
| <Route path="/posts/new" element={<PostNewPage user={user}/>}/> | |||||
| <Route path="/posts/:id" element={<PostDetailRoute user={user}/>}/> | |||||
| <Route path="/posts/changes" element={<PostHistoryPage/>}/> | |||||
| <Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/> | |||||
| <Route path="/wiki" element={<WikiSearchPage/>}/> | |||||
| <Route path="/wiki/:title" element={<WikiDetailPage/>}/> | |||||
| <Route path="/wiki/new" element={<WikiNewPage user={user}/>}/> | |||||
| <Route path="/wiki/:id/edit" element={<WikiEditPage user={user}/>}/> | |||||
| <Route path="/wiki/:id/diff" element={<WikiDiffPage/>}/> | |||||
| <Route path="/wiki/changes" element={<WikiHistoryPage/>}/> | |||||
| <Route path="/users/settings" element={<SettingPage user={user} setUser={setUser}/>}/> | |||||
| <Route path="/settings" element={<Navigate to="/users/settings" replace/>}/> | |||||
| <Route path="*" element={<NotFound/>}/> | |||||
| </Routes> | |||||
| </AnimatePresence> | |||||
| </LayoutGroup>) | |||||
| } | |||||
| const PostDetailRoute = ({ user }: { user: User | null }) => { | const PostDetailRoute = ({ user }: { user: User | null }) => { | ||||
| const location = useLocation () | const location = useLocation () | ||||
| const key = location.pathname | const key = location.pathname | ||||
| @@ -81,23 +116,7 @@ export default (() => { | |||||
| <BrowserRouter> | <BrowserRouter> | ||||
| <div className="flex flex-col h-screen w-screen"> | <div className="flex flex-col h-screen w-screen"> | ||||
| <TopNav user={user}/> | <TopNav user={user}/> | ||||
| <Routes> | |||||
| <Route path="/" element={<Navigate to="/posts" replace/>}/> | |||||
| <Route path="/posts" element={<PostListPage/>}/> | |||||
| <Route path="/posts/new" element={<PostNewPage user={user}/>}/> | |||||
| <Route path="/posts/:id" element={<PostDetailRoute user={user}/>}/> | |||||
| <Route path="/posts/changes" element={<PostHistoryPage/>}/> | |||||
| <Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/> | |||||
| <Route path="/wiki" element={<WikiSearchPage/>}/> | |||||
| <Route path="/wiki/:title" element={<WikiDetailPage/>}/> | |||||
| <Route path="/wiki/new" element={<WikiNewPage user={user}/>}/> | |||||
| <Route path="/wiki/:id/edit" element={<WikiEditPage user={user}/>}/> | |||||
| <Route path="/wiki/:id/diff" element={<WikiDiffPage/>}/> | |||||
| <Route path="/wiki/changes" element={<WikiHistoryPage/>}/> | |||||
| <Route path="/users/settings" element={<SettingPage user={user} setUser={setUser}/>}/> | |||||
| <Route path="/settings" element={<Navigate to="/users/settings" replace/>}/> | |||||
| <Route path="*" element={<NotFound/>}/> | |||||
| </Routes> | |||||
| <RouteTransitionWrapper user={user} setUser={setUser}/> | |||||
| </div> | </div> | ||||
| <Toaster/> | <Toaster/> | ||||
| </BrowserRouter> | </BrowserRouter> | ||||
| @@ -1,4 +1,9 @@ | |||||
| import { motion } from 'framer-motion' | |||||
| import { useRef } from 'react' | |||||
| import { useLocation } from 'react-router-dom' | |||||
| import PrefetchLink from '@/components/PrefetchLink' | import PrefetchLink from '@/components/PrefetchLink' | ||||
| import { useSharedTransitionStore } from '@/stores/sharedTransitionStore' | |||||
| import type { FC, MouseEvent } from 'react' | import type { FC, MouseEvent } from 'react' | ||||
| @@ -8,19 +13,58 @@ type Props = { posts: Post[] | |||||
| onClick?: (event: MouseEvent<HTMLElement>) => void } | onClick?: (event: MouseEvent<HTMLElement>) => void } | ||||
| export default (({ posts, onClick }: Props) => ( | |||||
| <div className="flex flex-wrap gap-6 p-4"> | |||||
| {posts.map ((post, i) => ( | |||||
| <PrefetchLink | |||||
| to={`/posts/${ post.id }`} | |||||
| key={post.id} | |||||
| className="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg" | |||||
| onClick={onClick}> | |||||
| <img src={post.thumbnail || post.thumbnailBase || undefined} | |||||
| alt={post.title || post.url} | |||||
| title={post.title || post.url || undefined} | |||||
| loading={i < 12 ? 'eager' : 'lazy'} | |||||
| decoding="async" | |||||
| className="object-cover w-full h-full"/> | |||||
| </PrefetchLink>))} | |||||
| </div>)) satisfies FC<Props> | |||||
| export default (({ posts, onClick }: Props) => { | |||||
| const location = useLocation () | |||||
| const setForLocationKey = useSharedTransitionStore (s => s.setForLocationKey) | |||||
| const cardRef = useRef<HTMLDivElement> (null) | |||||
| return ( | |||||
| <div className="flex flex-wrap gap-6 p-4"> | |||||
| {posts.map ((post, i) => { | |||||
| const sharedId = `page-${ post.id }` | |||||
| const layoutId = sharedId | |||||
| return ( | |||||
| <PrefetchLink | |||||
| to={`/posts/${ post.id }`} | |||||
| key={post.id} | |||||
| className="w-40 h-40" | |||||
| state={{ sharedId }} | |||||
| onClick={e => { | |||||
| setForLocationKey (location.key, sharedId) | |||||
| onClick?.(e) | |||||
| }}> | |||||
| <motion.div | |||||
| ref={cardRef} | |||||
| layoutId={layoutId} | |||||
| className="w-full h-full overflow-hidden rounded-xl shadow | |||||
| transform-gpu will-change-transform" | |||||
| whileHover={{ scale: 1.02 }} | |||||
| onLayoutAnimationStart={() => { | |||||
| if (!(cardRef.current)) | |||||
| return | |||||
| cardRef.current.style.position = 'relative' | |||||
| cardRef.current.style.zIndex = '9999' | |||||
| }} | |||||
| onLayoutAnimationComplete={() => { | |||||
| if (!(cardRef.current)) | |||||
| return | |||||
| cardRef.current.style.zIndex = '' | |||||
| cardRef.current.style.position = '' | |||||
| }} | |||||
| transition={{ type: 'spring', stiffness: 500, damping: 40, mass: .5 }}> | |||||
| <img src={post.thumbnail || post.thumbnailBase || undefined} | |||||
| alt={post.title || post.url} | |||||
| title={post.title || post.url || undefined} | |||||
| loading={i < 12 ? 'eager' : 'lazy'} | |||||
| decoding="async" | |||||
| className="object-cover w-full h-full"/> | |||||
| </motion.div> | |||||
| </PrefetchLink>) | |||||
| })} | |||||
| </div>) | |||||
| }) satisfies FC<Props> | |||||
| @@ -1,5 +1,6 @@ | |||||
| import { useQueryClient } from '@tanstack/react-query' | import { useQueryClient } from '@tanstack/react-query' | ||||
| import { forwardRef, useMemo } from 'react' | import { forwardRef, useMemo } from 'react' | ||||
| import { flushSync } from 'react-dom' | |||||
| import { createPath, useNavigate } from 'react-router-dom' | import { createPath, useNavigate } from 'react-router-dom' | ||||
| import { useOverlayStore } from '@/components/RouteBlockerOverlay' | import { useOverlayStore } from '@/components/RouteBlockerOverlay' | ||||
| @@ -11,6 +12,7 @@ import type { To } from 'react-router-dom' | |||||
| type Props = AnchorHTMLAttributes<HTMLAnchorElement> & { | type Props = AnchorHTMLAttributes<HTMLAnchorElement> & { | ||||
| to: To | to: To | ||||
| state?: Record<string, string> | |||||
| replace?: boolean | replace?: boolean | ||||
| className?: string | className?: string | ||||
| cancelOnError?: boolean } | cancelOnError?: boolean } | ||||
| @@ -20,11 +22,15 @@ export default forwardRef<HTMLAnchorElement, Props> (({ | |||||
| to, | to, | ||||
| replace, | replace, | ||||
| className, | className, | ||||
| state, | |||||
| onMouseEnter, | onMouseEnter, | ||||
| onTouchStart, | onTouchStart, | ||||
| onClick, | onClick, | ||||
| cancelOnError = false, | cancelOnError = false, | ||||
| ...rest }, ref) => { | ...rest }, ref) => { | ||||
| if ('onClick' in rest) | |||||
| delete rest['onClick'] | |||||
| const navigate = useNavigate () | const navigate = useNavigate () | ||||
| const qc = useQueryClient () | const qc = useQueryClient () | ||||
| const url = useMemo (() => { | const url = useMemo (() => { | ||||
| @@ -57,25 +63,37 @@ export default forwardRef<HTMLAnchorElement, Props> (({ | |||||
| } | } | ||||
| const handleClick = async (ev: MouseEvent<HTMLAnchorElement>) => { | const handleClick = async (ev: MouseEvent<HTMLAnchorElement>) => { | ||||
| onClick?.(ev) | |||||
| try | |||||
| { | |||||
| onClick?.(ev) | |||||
| if (ev.defaultPrevented | |||||
| || ev.metaKey | |||||
| || ev.ctrlKey | |||||
| || ev.shiftKey | |||||
| || ev.altKey) | |||||
| return | |||||
| if (ev.defaultPrevented | |||||
| || ev.metaKey | |||||
| || ev.ctrlKey | |||||
| || ev.shiftKey | |||||
| || ev.altKey) | |||||
| return | |||||
| ev.preventDefault () | |||||
| ev.preventDefault () | |||||
| setOverlay (true) | |||||
| const ok = await doPrefetch () | |||||
| setOverlay (false) | |||||
| flushSync (() => { | |||||
| setOverlay (true) | |||||
| }) | |||||
| const ok = await doPrefetch () | |||||
| flushSync (() => { | |||||
| setOverlay (false) | |||||
| }) | |||||
| if (!(ok) && cancelOnError) | |||||
| return | |||||
| if (!(ok) && cancelOnError) | |||||
| return | |||||
| navigate (to, { replace }) | |||||
| navigate (to, { replace, ...(state && { state }) }) | |||||
| } | |||||
| catch (ex) | |||||
| { | |||||
| console.log (ex) | |||||
| ev.preventDefault () | |||||
| } | |||||
| } | } | ||||
| return ( | return ( | ||||
| @@ -138,9 +138,13 @@ export default (({ user }: Props) => { | |||||
| <nav className="px-3 flex justify-between items-center w-full min-h-[48px] | <nav className="px-3 flex justify-between items-center w-full min-h-[48px] | ||||
| bg-yellow-200 dark:bg-red-975 md:bg-yellow-50"> | bg-yellow-200 dark:bg-red-975 md:bg-yellow-50"> | ||||
| <div className="flex items-center gap-2 h-full"> | <div className="flex items-center gap-2 h-full"> | ||||
| <PrefetchLink to="/" | |||||
| className="mx-4 text-xl font-bold text-pink-600 hover:text-pink-400 | |||||
| dark:text-pink-300 dark:hover:text-pink-100"> | |||||
| <PrefetchLink | |||||
| to="/posts" | |||||
| className="mx-4 text-xl font-bold text-pink-600 hover:text-pink-400 | |||||
| dark:text-pink-300 dark:hover:text-pink-100" | |||||
| onClick={() => { | |||||
| scroll (0, 0) | |||||
| }}> | |||||
| ぼざクリ タグ広場 | ぼざクリ タグ広場 | ||||
| </PrefetchLink> | </PrefetchLink> | ||||
| @@ -1,9 +1,13 @@ | |||||
| import React from 'react' | |||||
| import { cn } from '@/lib/utils' | |||||
| type Props = { children: React.ReactNode } | |||||
| import type { FC, ReactNode } from 'react' | |||||
| type Props = { | |||||
| children: ReactNode | |||||
| className?: string } | |||||
| export default ({ children }: Props) => ( | |||||
| <main className="flex-1 overflow-y-auto p-4"> | |||||
| export default (({ children, className }: Props) => ( | |||||
| <main className={cn ('flex-1 overflow-y-auto p-4', className)}> | |||||
| {children} | {children} | ||||
| </main>) | |||||
| </main>)) satisfies FC<Props> | |||||
| @@ -72,5 +72,4 @@ export const apiDelete = async ( | |||||
| } | } | ||||
| export const isApiError = (err: unknown): err is AxiosError => | |||||
| axios.isAxiosError (err) | |||||
| export const isApiError = (err: unknown): err is AxiosError => axios.isAxiosError (err) | |||||
| @@ -11,7 +11,7 @@ const client = new QueryClient ({ | |||||
| defaultOptions: { | defaultOptions: { | ||||
| queries: { staleTime: 5 * 60 * 1000, | queries: { staleTime: 5 * 60 * 1000, | ||||
| gcTime: 30 * 60 * 1000, | gcTime: 30 * 60 * 1000, | ||||
| retry: 1 }}}) | |||||
| retry: 1 } } }) | |||||
| createRoot (document.getElementById ('root')!).render ( | createRoot (document.getElementById ('root')!).render ( | ||||
| <HelmetProvider context={helmetContext}> | <HelmetProvider context={helmetContext}> | ||||
| @@ -1,12 +1,13 @@ | |||||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' | ||||
| import { motion } from 'framer-motion' | |||||
| import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||
| import { Helmet } from 'react-helmet-async' | import { Helmet } from 'react-helmet-async' | ||||
| import { useParams } from 'react-router-dom' | import { useParams } from 'react-router-dom' | ||||
| import PostList from '@/components/PostList' | |||||
| import TagDetailSidebar from '@/components/TagDetailSidebar' | |||||
| import PostEditForm from '@/components/PostEditForm' | import PostEditForm from '@/components/PostEditForm' | ||||
| import PostEmbed from '@/components/PostEmbed' | import PostEmbed from '@/components/PostEmbed' | ||||
| import PostList from '@/components/PostList' | |||||
| import TagDetailSidebar from '@/components/TagDetailSidebar' | |||||
| import TabGroup, { Tab } from '@/components/common/TabGroup' | import TabGroup, { Tab } from '@/components/common/TabGroup' | ||||
| import MainArea from '@/components/layout/MainArea' | import MainArea from '@/components/layout/MainArea' | ||||
| import { Button } from '@/components/ui/button' | import { Button } from '@/components/ui/button' | ||||
| @@ -72,6 +73,8 @@ export default (({ user }: Props) => { | |||||
| }, [errorFlg, error]) | }, [errorFlg, error]) | ||||
| useEffect (() => { | useEffect (() => { | ||||
| scroll (0, 0) | |||||
| setStatus (200) | setStatus (200) | ||||
| }, [id]) | }, [id]) | ||||
| @@ -99,10 +102,24 @@ export default (({ user }: Props) => { | |||||
| <TagDetailSidebar post={post ?? null}/> | <TagDetailSidebar post={post ?? null}/> | ||||
| </div> | </div> | ||||
| <MainArea> | |||||
| <MainArea className="relative"> | |||||
| {post | {post | ||||
| ? ( | ? ( | ||||
| <> | <> | ||||
| {(post.thumbnail || post.thumbnailBase) && ( | |||||
| <motion.div | |||||
| layoutId={`page-${ id }`} | |||||
| className="absolute top-4 left-4 w-[min(640px,calc(100vw-2rem))] h-[360px] | |||||
| overflow-hidden rounded-xl pointer-events-none z-50" | |||||
| initial={{ opacity: 1 }} | |||||
| animate={{ opacity: 0 }} | |||||
| transition={{ duration: .2, ease: 'easeOut' }}> | |||||
| <img src={post.thumbnail || post.thumbnailBase} | |||||
| alt={post.title || post.url} | |||||
| title={post.title || post.url || undefined} | |||||
| className="object-cover w-full h-full"/> | |||||
| </motion.div>)} | |||||
| <PostEmbed post={post}/> | <PostEmbed post={post}/> | ||||
| <Button onClick={() => changeViewedFlg.mutate ()} | <Button onClick={() => changeViewedFlg.mutate ()} | ||||
| disabled={changeViewedFlg.isPending} | disabled={changeViewedFlg.isPending} | ||||
| @@ -15,10 +15,12 @@ import { fetchPosts } from '@/lib/posts' | |||||
| import { postsKeys } from '@/lib/queryKeys' | import { postsKeys } from '@/lib/queryKeys' | ||||
| import { fetchWikiPageByTitle } from '@/lib/wiki' | import { fetchWikiPageByTitle } from '@/lib/wiki' | ||||
| import type { FC } from 'react' | |||||
| import type { WikiPage } from '@/types' | import type { WikiPage } from '@/types' | ||||
| export default () => { | |||||
| export default (() => { | |||||
| const containerRef = useRef<HTMLDivElement | null> (null) | const containerRef = useRef<HTMLDivElement | null> (null) | ||||
| const [wikiPage, setWikiPage] = useState<WikiPage | null> (null) | const [wikiPage, setWikiPage] = useState<WikiPage | null> (null) | ||||
| @@ -41,21 +43,24 @@ export default () => { | |||||
| const totalPages = data ? Math.ceil (data.count / limit) : 0 | const totalPages = data ? Math.ceil (data.count / limit) : 0 | ||||
| useLayoutEffect (() => { | useLayoutEffect (() => { | ||||
| scroll (0, 0) | |||||
| setWikiPage (null) | setWikiPage (null) | ||||
| if (tags.length === 1) | |||||
| if (tags.length !== 1) | |||||
| return | |||||
| void (async () => { | |||||
| try | |||||
| { | |||||
| const tagName = tags[0] | |||||
| setWikiPage (await fetchWikiPageByTitle (tagName, { })) | |||||
| } | |||||
| catch | |||||
| { | { | ||||
| void (async () => { | |||||
| try | |||||
| { | |||||
| const tagName = tags[0] | |||||
| setWikiPage (await fetchWikiPageByTitle (tagName, { })) | |||||
| } | |||||
| catch | |||||
| { | |||||
| ; | |||||
| } | |||||
| }) () | |||||
| ; | |||||
| } | } | ||||
| }) () | |||||
| }, [location.search]) | }, [location.search]) | ||||
| return ( | return ( | ||||
| @@ -100,4 +105,4 @@ export default () => { | |||||
| </TabGroup> | </TabGroup> | ||||
| </MainArea> | </MainArea> | ||||
| </div>) | </div>) | ||||
| } | |||||
| }) satisfies FC | |||||
| @@ -0,0 +1,19 @@ | |||||
| import { create } from 'zustand' | |||||
| type SharedTransitionState = { | |||||
| byLocationKey: Record<string, string> | |||||
| setForLocationKey: (locationKey: string, sharedId: string) => void | |||||
| clearForLocationKey: (locationKey: string) => void } | |||||
| export const useSharedTransitionStore = create<SharedTransitionState> (set => ({ | |||||
| byLocationKey: { }, | |||||
| setForLocationKey: (locationKey, sharedId) => | |||||
| set (state => ({ byLocationKey: { ...state.byLocationKey, | |||||
| [locationKey]: sharedId } })), | |||||
| clearForLocationKey: (locationKey) => | |||||
| set (state => { | |||||
| const next = { ...state.byLocationKey } | |||||
| delete next[locationKey] | |||||
| return { byLocationKey: next } | |||||
| }) })) | |||||