| @@ -1,15 +1,16 @@ | |||
| import { useQueryClient } from '@tanstack/react-query' | |||
| import { useMemo } from 'react' | |||
| import { useNavigate } from 'react-router-dom' | |||
| import { createPath, 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' | |||
| import type { To } from 'react-router-dom' | |||
| type Props = AnchorHTMLAttributes<HTMLAnchorElement> & { | |||
| to: string | |||
| to: To | |||
| replace?: boolean | |||
| className?: string | |||
| cancelOnError?: boolean } | |||
| @@ -25,7 +26,10 @@ export default (({ to, | |||
| ...rest }: Props) => { | |||
| const navigate = useNavigate () | |||
| const qc = useQueryClient () | |||
| const url = useMemo (() => (new URL (to, location.origin)).toString (), [to]) | |||
| const url = useMemo (() => { | |||
| const path = (typeof to === 'string') ? to : createPath (to) | |||
| return (new URL (path, location.origin)).toString () | |||
| }, [to]) | |||
| const setOverlay = useOverlayStore (s => s.setActive) | |||
| const doPrefetch = async () => { | |||
| @@ -74,7 +78,7 @@ export default (({ to, | |||
| } | |||
| return ( | |||
| <a href={to} | |||
| <a href={typeof to === 'string' ? to : createPath (to)} | |||
| onMouseEnter={handleMouseEnter} | |||
| onTouchStart={handleTouchStart} | |||
| onClick={handleClick} | |||
| @@ -1,5 +1,6 @@ | |||
| import { Link } from 'react-router-dom' | |||
| import PrefetchLink from '@/components/PrefetchLink' | |||
| import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts' | |||
| import { cn } from '@/lib/utils' | |||
| @@ -9,7 +10,8 @@ import type { Tag } from '@/types' | |||
| type CommonProps = { tag: Tag | |||
| withWiki?: boolean | |||
| withCount?: boolean } | |||
| withCount?: boolean | |||
| prefetch?: boolean } | |||
| type PropsWithLink = | |||
| CommonProps & { linkFlg?: true } & Partial<ComponentProps<typeof Link>> | |||
| @@ -24,6 +26,7 @@ export default (({ tag, | |||
| linkFlg = true, | |||
| withWiki = true, | |||
| withCount = true, | |||
| prefetch = false, | |||
| ...props }: Props) => { | |||
| const spanClass = cn ( | |||
| `text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE }`, | |||
| @@ -44,11 +47,19 @@ export default (({ tag, | |||
| </span>)} | |||
| {linkFlg | |||
| ? ( | |||
| <Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`} | |||
| prefetch | |||
| ? <PrefetchLink | |||
| to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`} | |||
| className={linkClass} | |||
| {...props}> | |||
| {tag.name} | |||
| </Link>) | |||
| {tag.name} | |||
| </PrefetchLink> | |||
| : <Link | |||
| to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`} | |||
| className={linkClass} | |||
| {...props}> | |||
| {tag.name} | |||
| </Link>) | |||
| : ( | |||
| <span className={spanClass} | |||
| {...props}> | |||
| @@ -10,16 +10,17 @@ import { API_BASE_URL } from '@/config' | |||
| import { CATEGORIES } from '@/consts' | |||
| import { cn } from '@/lib/utils' | |||
| import type { FC } from 'react' | |||
| import type { FC, MouseEvent } from 'react' | |||
| import type { Post, Tag } from '@/types' | |||
| type TagByCategory = Record<string, Tag[]> | |||
| type Props = { posts: Post[] } | |||
| type Props = { posts: Post[] | |||
| onClick?: (event: MouseEvent<HTMLElement>) => void } | |||
| export default (({ posts }: Props) => { | |||
| export default (({ posts, onClick }: Props) => { | |||
| const navigate = useNavigate () | |||
| const [tagsVsbl, setTagsVsbl] = useState (false) | |||
| @@ -64,11 +65,11 @@ export default (({ posts }: Props) => { | |||
| <div className={cn (!(tagsVsbl) && 'hidden', 'md:block mt-4')}> | |||
| <SectionTitle>タグ</SectionTitle> | |||
| <ul> | |||
| {CATEGORIES.flatMap (cat => cat in tags ? ( | |||
| {CATEGORIES.flatMap (cat => cat in tags ? | |||
| tags[cat].map (tag => ( | |||
| <li key={tag.id} className="mb-1"> | |||
| <TagLink tag={tag}/> | |||
| </li>))) : [])} | |||
| <TagLink tag={tag} prefetch onClick={onClick}/> | |||
| </li>)) : [])} | |||
| </ul> | |||
| <SectionTitle>関聯</SectionTitle> | |||
| {posts.length > 0 && ( | |||
| @@ -10,6 +10,7 @@ import WikiBody from '@/components/WikiBody' | |||
| import TabGroup, { Tab } from '@/components/common/TabGroup' | |||
| import MainArea from '@/components/layout/MainArea' | |||
| import { API_BASE_URL, SITE_TITLE } from '@/config' | |||
| import { fetchPosts } from '@/lib/posts' | |||
| import type { Post, WikiPage } from '@/types' | |||
| @@ -28,13 +29,11 @@ export default () => { | |||
| const loadMore = async (withCursor: boolean) => { | |||
| setLoading (true) | |||
| const res = await axios.get (`${ API_BASE_URL }/posts`, { | |||
| params: { tags: tags.join (' '), | |||
| match: anyFlg ? 'any' : 'all', | |||
| limit: '20', | |||
| ...(withCursor && { cursor }) } }) | |||
| const data = toCamel (res.data as any, { deep: true }) as { posts: Post[] | |||
| nextCursor: string } | |||
| const data = await fetchPosts ({ | |||
| tags: tags.join (' '), | |||
| match: anyFlg ? 'any' : 'all', | |||
| limit: 20, | |||
| ...(withCursor && { cursor }) }) | |||
| setPosts (posts => ( | |||
| [...((new Map ([...(withCursor ? posts : []), ...data.posts] | |||
| .map (post => [post.id, post]))) | |||
| @@ -111,7 +110,13 @@ export default () => { | |||
| </title> | |||
| </Helmet> | |||
| <TagSidebar posts={posts.slice (0, 20)}/> | |||
| <TagSidebar posts={posts.slice (0, 20)} onClick={() => { | |||
| const statesToSave = { | |||
| posts, cursor, | |||
| scroll: containerRef.current?.scrollTop ?? 0 } | |||
| sessionStorage.setItem (`posts:${ tagsQuery }`, | |||
| JSON.stringify (statesToSave)) | |||
| }}/> | |||
| <MainArea> | |||
| <TabGroup> | |||