このコミットが含まれているのは:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
新しい課題から参照
ユーザをブロックする