From 109b57bb56f17f7e2a9c7219b3262405819e00eb Mon Sep 17 00:00:00 2001 From: miteruzo Date: Sun, 1 Feb 2026 06:34:40 +0900 Subject: [PATCH] #139 --- frontend/src/components/PostList.tsx | 122 ++++++++---------- frontend/src/components/PrefetchLink.tsx | 46 ++++--- frontend/src/pages/posts/PostDetailPage.tsx | 124 ++++++++++--------- frontend/src/stores/sharedTransitionStore.ts | 19 +++ 4 files changed, 172 insertions(+), 139 deletions(-) create mode 100644 frontend/src/stores/sharedTransitionStore.ts diff --git a/frontend/src/components/PostList.tsx b/frontend/src/components/PostList.tsx index 3331f56..2dcb147 100644 --- a/frontend/src/components/PostList.tsx +++ b/frontend/src/components/PostList.tsx @@ -1,10 +1,9 @@ -import { useQueryClient } from '@tanstack/react-query' import { motion } from 'framer-motion' import { useRef } from 'react' -import { useNavigate } from 'react-router-dom' +import { useLocation } from 'react-router-dom' -import { fetchPost } from '@/lib/posts' import PrefetchLink from '@/components/PrefetchLink' +import { useSharedTransitionStore } from '@/stores/sharedTransitionStore' import type { FC, MouseEvent } from 'react' @@ -15,73 +14,62 @@ type Props = { posts: Post[] export default (({ posts, onClick }: Props) => { - const navigate = useNavigate () + const location = useLocation () - const qc = useQueryClient () + const setForLocationKey = useSharedTransitionStore (s => s.setForLocationKey) - const prefetch = (id: string) => qc.prefetchQuery ({ - queryKey: ['post', id], - queryFn: () => fetchPost (id) }) + const cardRef = useRef (null) - return ( -
- {posts.map ((post, i) => { - const id = String (post.id) - const hRef = `/posts/${ id }` - const cardRef = useRef (null) - - const handleClick = async (ev: MouseEvent) => { - onClick?.(ev) - - if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.button === 1) - return + const outboundSharedId = useSharedTransitionStore (s => s.byLocationKey[location.key]) - ev.preventDefault () - - await qc.ensureQueryData ({ - queryKey: ['post', id], - queryFn: () => fetchPost (id) }) - - navigate (hRef) - } + return ( + <> +
+ {posts.map ((post, i) => { + const id2 = `page-${ post.id }` + const layoutId = outboundSharedId === id2 ? id2 : undefined - return ( - prefetch (id)} - onFocus={() => prefetch (id)} - onClick={handleClick}> - { - if (cardRef.current) - { - cardRef.current.style.position = 'relative' - cardRef.current.style.zIndex = '9999' - } - }} - onLayoutAnimationComplete={() => { - if (cardRef.current) - { - cardRef.current.style.zIndex = '' - cardRef.current.style.position = '' - } - }} - transition={{ type: 'spring', stiffness: 500, damping: 40, mass: .5 }}> - {post.title - - ) - })} -
) + return ( + { + const sharedId = `page-${ post.id }` + setForLocationKey (location.key, sharedId) + onClick?.(e) + }}> + { + if (cardRef.current) + { + cardRef.current.style.position = 'relative' + cardRef.current.style.zIndex = '9999' + } + }} + onLayoutAnimationComplete={() => { + if (cardRef.current) + { + cardRef.current.style.zIndex = '' + cardRef.current.style.position = '' + } + }} + transition={{ type: 'spring', stiffness: 500, damping: 40, mass: .5 }}> + {post.title + + ) + })} +
+ ) }) satisfies FC diff --git a/frontend/src/components/PrefetchLink.tsx b/frontend/src/components/PrefetchLink.tsx index f983b0d..0aebe18 100644 --- a/frontend/src/components/PrefetchLink.tsx +++ b/frontend/src/components/PrefetchLink.tsx @@ -1,5 +1,6 @@ import { useQueryClient } from '@tanstack/react-query' import { forwardRef, useMemo } from 'react' +import { flushSync } from 'react-dom' import { createPath, useNavigate } from 'react-router-dom' import { useOverlayStore } from '@/components/RouteBlockerOverlay' @@ -11,6 +12,7 @@ import type { To } from 'react-router-dom' type Props = AnchorHTMLAttributes & { to: To + state?: Record replace?: boolean className?: string cancelOnError?: boolean } @@ -20,11 +22,15 @@ export default forwardRef (({ to, replace, className, + state, onMouseEnter, onTouchStart, onClick, cancelOnError = false, ...rest }, ref) => { + if ('onClick' in rest) + delete rest['onClick'] + const navigate = useNavigate () const qc = useQueryClient () const url = useMemo (() => { @@ -57,25 +63,37 @@ export default forwardRef (({ } const handleClick = async (ev: MouseEvent) => { - 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 ( diff --git a/frontend/src/pages/posts/PostDetailPage.tsx b/frontend/src/pages/posts/PostDetailPage.tsx index 7edec9b..71d17e2 100644 --- a/frontend/src/pages/posts/PostDetailPage.tsx +++ b/frontend/src/pages/posts/PostDetailPage.tsx @@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { motion } from 'framer-motion' import { useEffect, useState } from 'react' import { Helmet } from 'react-helmet-async' -import { useParams } from 'react-router-dom' +import { useLocation, useParams } from 'react-router-dom' import PostEditForm from '@/components/PostEditForm' import PostEmbed from '@/components/PostEmbed' @@ -31,6 +31,11 @@ export default (({ user }: Props) => { const postId = String (id ?? '') const postKey = postsKeys.show (postId) + const location = useLocation () + const navState = (location.state ?? { }) as { sharedId?: string } + + const inboundSharedId = navState.sharedId + const { data: post, isError: errorFlg, error } = useQuery ({ enabled: Boolean (id), queryKey: postKey, @@ -89,62 +94,65 @@ export default (({ user }: Props) => { : 'bg-gray-500 hover:bg-gray-600') return ( -
- - {(post?.thumbnail || post?.thumbnailBase) && ( - )} - {post && {`${ post.title || post.url } | ${ SITE_TITLE }`}} - - -
- -
- - - - {post?.url}/ - - - {post - ? ( - <> - - - - - {post.related.length > 0 - ? - : 'まだないよ(笑)'} - - {['admin', 'member'].some (r => user?.role === r) && ( - - { - qc.setQueryData (postsKeys.show (postId), - (prev: any) => newPost ?? prev) - qc.invalidateQueries ({ queryKey: postsKeys.root }) - toast ({ description: '更新しました.' }) - }}/> - )} - - ) - : 'Loading...'} - - -
- + <> +
+ + {(post?.thumbnail || post?.thumbnailBase) && ( + )} + {post && {`${ post.title || post.url } | ${ SITE_TITLE }`}} + + +
+ +
+ + + {post + ? ( + <> + {inboundSharedId === `page-${ id }` && ( + + {post.title + )} + + + + + + {post.related.length > 0 + ? + : 'まだないよ(笑)'} + + {['admin', 'member'].some (r => user?.role === r) && ( + + { + qc.setQueryData (postsKeys.show (postId), + (prev: any) => newPost ?? prev) + qc.invalidateQueries ({ queryKey: postsKeys.root }) + toast ({ description: '更新しました.' }) + }}/> + )} + + ) + : 'Loading...'} + + +
+ +
-
) + ) }) satisfies FC diff --git a/frontend/src/stores/sharedTransitionStore.ts b/frontend/src/stores/sharedTransitionStore.ts new file mode 100644 index 0000000..c0a7e98 --- /dev/null +++ b/frontend/src/stores/sharedTransitionStore.ts @@ -0,0 +1,19 @@ +import { create } from 'zustand' + +type SharedTransitionState = { + byLocationKey: Record + setForLocationKey: (locationKey: string, sharedId: string) => void + clearForLocationKey: (locationKey: string) => void } + + +export const useSharedTransitionStore = create (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 } + }) }))