Merge remote-tracking branch 'origin/main' into feature/140
This commit is contained in:
Generated
+8
-8
@@ -31,7 +31,7 @@
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-markdown-editor-lite": "^1.3.4",
|
||||
"react-router-dom": "^6.30.0",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-youtube": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
@@ -6030,9 +6030,9 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.0"
|
||||
@@ -6045,13 +6045,13 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.0",
|
||||
"react-router": "6.30.0"
|
||||
"react-router": "6.30.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-markdown-editor-lite": "^1.3.4",
|
||||
"react-router-dom": "^6.30.0",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-youtube": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
|
||||
+38
-19
@@ -1,5 +1,10 @@
|
||||
import { AnimatePresence, LayoutGroup } from 'framer-motion'
|
||||
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 TopNav from '@/components/TopNav'
|
||||
@@ -20,11 +25,41 @@ import WikiHistoryPage from '@/pages/wiki/WikiHistoryPage'
|
||||
import WikiNewPage from '@/pages/wiki/WikiNewPage'
|
||||
import WikiSearchPage from '@/pages/wiki/WikiSearchPage'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { Dispatch, FC, SetStateAction } from 'react'
|
||||
|
||||
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 location = useLocation ()
|
||||
const key = location.pathname
|
||||
@@ -81,23 +116,7 @@ export default (() => {
|
||||
<BrowserRouter>
|
||||
<div className="flex flex-col h-screen w-screen">
|
||||
<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>
|
||||
<Toaster/>
|
||||
</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 { useSharedTransitionStore } from '@/stores/sharedTransitionStore'
|
||||
|
||||
import type { FC, MouseEvent } from 'react'
|
||||
|
||||
@@ -8,19 +13,58 @@ type Props = { posts: Post[]
|
||||
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 { 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<HTMLAnchorElement> & {
|
||||
to: To
|
||||
state?: Record<string, string>
|
||||
replace?: boolean
|
||||
className?: string
|
||||
cancelOnError?: boolean }
|
||||
@@ -20,11 +22,15 @@ export default forwardRef<HTMLAnchorElement, Props> (({
|
||||
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<HTMLAnchorElement, Props> (({
|
||||
}
|
||||
|
||||
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 (
|
||||
|
||||
@@ -138,9 +138,13 @@ export default (({ user }: Props) => {
|
||||
<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">
|
||||
<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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
</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: {
|
||||
queries: { staleTime: 5 * 60 * 1000,
|
||||
gcTime: 30 * 60 * 1000,
|
||||
retry: 1 }}})
|
||||
retry: 1 } } })
|
||||
|
||||
createRoot (document.getElementById ('root')!).render (
|
||||
<HelmetProvider context={helmetContext}>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
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 PostList from '@/components/PostList'
|
||||
import TagDetailSidebar from '@/components/TagDetailSidebar'
|
||||
import PostEditForm from '@/components/PostEditForm'
|
||||
import PostEmbed from '@/components/PostEmbed'
|
||||
import PostList from '@/components/PostList'
|
||||
import TagDetailSidebar from '@/components/TagDetailSidebar'
|
||||
import TabGroup, { Tab } from '@/components/common/TabGroup'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -72,6 +73,8 @@ export default (({ user }: Props) => {
|
||||
}, [errorFlg, error])
|
||||
|
||||
useEffect (() => {
|
||||
scroll (0, 0)
|
||||
|
||||
setStatus (200)
|
||||
}, [id])
|
||||
|
||||
@@ -99,10 +102,24 @@ export default (({ user }: Props) => {
|
||||
<TagDetailSidebar post={post ?? null}/>
|
||||
</div>
|
||||
|
||||
<MainArea>
|
||||
<MainArea className="relative">
|
||||
{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}/>
|
||||
<Button onClick={() => changeViewedFlg.mutate ()}
|
||||
disabled={changeViewedFlg.isPending}
|
||||
|
||||
@@ -15,10 +15,12 @@ import { fetchPosts } from '@/lib/posts'
|
||||
import { postsKeys } from '@/lib/queryKeys'
|
||||
import { fetchWikiPageByTitle } from '@/lib/wiki'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { WikiPage } from '@/types'
|
||||
|
||||
|
||||
export default () => {
|
||||
export default (() => {
|
||||
const containerRef = useRef<HTMLDivElement | null> (null)
|
||||
|
||||
const [wikiPage, setWikiPage] = useState<WikiPage | null> (null)
|
||||
@@ -41,21 +43,24 @@ export default () => {
|
||||
const totalPages = data ? Math.ceil (data.count / limit) : 0
|
||||
|
||||
useLayoutEffect (() => {
|
||||
scroll (0, 0)
|
||||
|
||||
setWikiPage (null)
|
||||
if (tags.length === 1)
|
||||
|
||||
if (tags.length !== 1)
|
||||
return
|
||||
|
||||
void (async () => {
|
||||
try
|
||||
{
|
||||
void (async () => {
|
||||
try
|
||||
{
|
||||
const tagName = tags[0]
|
||||
setWikiPage (await fetchWikiPageByTitle (tagName, { }))
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
}
|
||||
}) ()
|
||||
const tagName = tags[0]
|
||||
setWikiPage (await fetchWikiPageByTitle (tagName, { }))
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
}
|
||||
}) ()
|
||||
}, [location.search])
|
||||
|
||||
return (
|
||||
@@ -100,4 +105,4 @@ export default () => {
|
||||
</TabGroup>
|
||||
</MainArea>
|
||||
</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 }
|
||||
}) }))
|
||||
Reference in New Issue
Block a user