このコミットが含まれているのは:
2025-10-05 03:15:46 +09:00
コミット d5d7e0e22b
10個のファイルの変更372行の追加73行の削除
+3 -3
ファイルの表示
@@ -1,4 +1,4 @@
import { Link } from 'react-router-dom'
import PrefetchLink from '@/components/PrefetchLink'
import type { FC, MouseEvent } from 'react'
@@ -11,7 +11,7 @@ type Props = { posts: Post[]
export default (({ posts, onClick }: Props) => (
<div className="flex flex-wrap gap-6 p-4">
{posts.map ((post, i) => (
<Link to={`/posts/${ post.id }`}
<PrefetchLink to={`/posts/${ post.id }`}
key={post.id}
className="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg"
onClick={onClick}>
@@ -21,5 +21,5 @@ export default (({ posts, onClick }: Props) => (
loading={i < 12 ? 'eager' : 'lazy'}
decoding="async"
className="object-cover w-full h-full"/>
</Link>))}
</PrefetchLink>))}
</div>)) satisfies FC<Props>
+83
ファイルの表示
@@ -0,0 +1,83 @@
import { useQueryClient } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { useOverlayStore } from '@/components/RouteBlockerOverlay'
import { prefetchForURL } from '@/lib/prefetchers'
import { cn } from '@/lib/utils'
import type { AnchorHTMLAttributes, FC, MouseEvent, TouchEvent } from 'react'
type Props = AnchorHTMLAttributes<HTMLAnchorElement> & {
to: string
replace?: boolean
className?: string
cancelOnError?: boolean }
export default (({ to,
replace,
className,
onMouseEnter,
onTouchStart,
onClick,
cancelOnError = false,
...rest }: Props) => {
const navigate = useNavigate ()
const qc = useQueryClient ()
const url = useMemo (() => (new URL (to, location.origin)).toString (), [to])
const setOverlay = useOverlayStore (s => s.setActive)
const doPrefetch = async () => {
try
{
await prefetchForURL (qc, url)
return true
}
catch (e)
{
console.error ('データ取得エラー', e)
return false
}
}
const handleMouseEnter = async (ev: MouseEvent<HTMLAnchorElement>) => {
onMouseEnter?.(ev)
doPrefetch ()
}
const handleTouchStart = async (ev: TouchEvent<HTMLAnchorElement>) => {
onTouchStart?.(ev)
doPrefetch ()
}
const handleClick = async (ev: MouseEvent<HTMLAnchorElement>) => {
onClick?.(ev)
if (ev.defaultPrevented
|| ev.metaKey
|| ev.ctrlKey
|| ev.shiftKey
|| ev.altKey)
return
ev.preventDefault ()
setOverlay (true)
const ok = await doPrefetch ()
setOverlay (false)
if (!(ok) && cancelOnError)
return
navigate (to, { replace })
}
return (
<a href={to}
onMouseEnter={handleMouseEnter}
onTouchStart={handleTouchStart}
onClick={handleClick}
className={cn ('cursor-pointer', className)}
{...rest}/>)
}) satisfies FC<Props>
+46
ファイルの表示
@@ -0,0 +1,46 @@
import { useEffect } from 'react'
import { create } from 'zustand'
import type { FC } from 'react'
type OverlayStore = {
active: boolean
setActive: (v: boolean) => void }
export const useOverlayStore = create<OverlayStore> (set => ({
active: false,
setActive: v => set ({ active: v }) }))
export default (() => {
const active = useOverlayStore (s => s.active)
useEffect (() => {
if (active)
{
document.body.style.overflow = 'hidden'
document.body.setAttribute ('aria-busy', 'true')
}
else
{
document.body.style.overflow = ''
document.body.removeAttribute ('aria-busy')
}
}, [active])
if (!(active))
return null
return (
<div
role="progressbar"
aria-label="Loading"
className="fixed inset-0 z-[9999] bg-black/50 backdrop-blur-sm pointer-events-auto">
<div className="absolute inset-0 flex items-center justify-center">
<div className="rounded-2xl bg-black/60 text-white px-6 py-3 text-sm">
Loading...
</div>
</div>
</div>)
}) satisfies FC