| Author | SHA1 | Message | Date | 
|---|---|---|---|
| 
							
							
								
									
								
								 | 
						214c91e3bf | #140 | 4 weeks ago | 
| 
							
							
								
									
								
								 | 
						5cbe21b5d7 | #140 | 1 month ago | 
| 
							
							
								
									
								
								 | 
						d5d7e0e22b | #140 | 1 month ago | 
| @@ -13,6 +13,7 @@ | |||
| "@radix-ui/react-dialog": "^1.1.14", | |||
| "@radix-ui/react-switch": "^1.2.5", | |||
| "@radix-ui/react-toast": "^1.2.14", | |||
| "@tanstack/react-query": "^5.90.2", | |||
| "axios": "^1.10.0", | |||
| "camelcase-keys": "^9.1.3", | |||
| "class-variance-authority": "^0.7.1", | |||
| @@ -20,6 +21,7 @@ | |||
| "humps": "^2.0.1", | |||
| "lucide-react": "^0.511.0", | |||
| "markdown-it": "^14.1.0", | |||
| "path-to-regexp": "^8.3.0", | |||
| "react": "^19.1.0", | |||
| "react-dom": "^19.1.0", | |||
| "react-helmet-async": "^2.0.5", | |||
| @@ -28,7 +30,8 @@ | |||
| "react-router-dom": "^6.30.0", | |||
| "react-youtube": "^10.1.0", | |||
| "remark-gfm": "^4.0.1", | |||
| "tailwind-merge": "^3.3.0" | |||
| "tailwind-merge": "^3.3.0", | |||
| "zustand": "^5.0.8" | |||
| }, | |||
| "devDependencies": { | |||
| "@eslint/js": "^9.25.0", | |||
| @@ -1908,6 +1911,32 @@ | |||
| "win32" | |||
| ] | |||
| }, | |||
| "node_modules/@tanstack/query-core": { | |||
| "version": "5.90.2", | |||
| "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", | |||
| "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==", | |||
| "license": "MIT", | |||
| "funding": { | |||
| "type": "github", | |||
| "url": "https://github.com/sponsors/tannerlinsley" | |||
| } | |||
| }, | |||
| "node_modules/@tanstack/react-query": { | |||
| "version": "5.90.2", | |||
| "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz", | |||
| "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==", | |||
| "license": "MIT", | |||
| "dependencies": { | |||
| "@tanstack/query-core": "5.90.2" | |||
| }, | |||
| "funding": { | |||
| "type": "github", | |||
| "url": "https://github.com/sponsors/tannerlinsley" | |||
| }, | |||
| "peerDependencies": { | |||
| "react": "^18 || ^19" | |||
| } | |||
| }, | |||
| "node_modules/@types/axios": { | |||
| "version": "0.14.4", | |||
| "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.4.tgz", | |||
| @@ -5452,6 +5481,16 @@ | |||
| "dev": true, | |||
| "license": "ISC" | |||
| }, | |||
| "node_modules/path-to-regexp": { | |||
| "version": "8.3.0", | |||
| "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", | |||
| "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", | |||
| "license": "MIT", | |||
| "funding": { | |||
| "type": "opencollective", | |||
| "url": "https://opencollective.com/express" | |||
| } | |||
| }, | |||
| "node_modules/picocolors": { | |||
| "version": "1.1.1", | |||
| "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", | |||
| @@ -7186,6 +7225,35 @@ | |||
| "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", | |||
| "license": "MIT" | |||
| }, | |||
| "node_modules/zustand": { | |||
| "version": "5.0.8", | |||
| "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", | |||
| "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", | |||
| "license": "MIT", | |||
| "engines": { | |||
| "node": ">=12.20.0" | |||
| }, | |||
| "peerDependencies": { | |||
| "@types/react": ">=18.0.0", | |||
| "immer": ">=9.0.6", | |||
| "react": ">=18.0.0", | |||
| "use-sync-external-store": ">=1.2.0" | |||
| }, | |||
| "peerDependenciesMeta": { | |||
| "@types/react": { | |||
| "optional": true | |||
| }, | |||
| "immer": { | |||
| "optional": true | |||
| }, | |||
| "react": { | |||
| "optional": true | |||
| }, | |||
| "use-sync-external-store": { | |||
| "optional": true | |||
| } | |||
| } | |||
| }, | |||
| "node_modules/zwitch": { | |||
| "version": "2.0.4", | |||
| "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", | |||
| @@ -15,6 +15,7 @@ | |||
| "@radix-ui/react-dialog": "^1.1.14", | |||
| "@radix-ui/react-switch": "^1.2.5", | |||
| "@radix-ui/react-toast": "^1.2.14", | |||
| "@tanstack/react-query": "^5.90.2", | |||
| "axios": "^1.10.0", | |||
| "camelcase-keys": "^9.1.3", | |||
| "class-variance-authority": "^0.7.1", | |||
| @@ -22,6 +23,7 @@ | |||
| "humps": "^2.0.1", | |||
| "lucide-react": "^0.511.0", | |||
| "markdown-it": "^14.1.0", | |||
| "path-to-regexp": "^8.3.0", | |||
| "react": "^19.1.0", | |||
| "react-dom": "^19.1.0", | |||
| "react-helmet-async": "^2.0.5", | |||
| @@ -30,7 +32,8 @@ | |||
| "react-router-dom": "^6.30.0", | |||
| "react-youtube": "^10.1.0", | |||
| "remark-gfm": "^4.0.1", | |||
| "tailwind-merge": "^3.3.0" | |||
| "tailwind-merge": "^3.3.0", | |||
| "zustand": "^5.0.8" | |||
| }, | |||
| "devDependencies": { | |||
| "@eslint/js": "^9.25.0", | |||
| @@ -1,8 +1,9 @@ | |||
| import axios from 'axios' | |||
| import toCamel from 'camelcase-keys' | |||
| import { useEffect, useState } from 'react' | |||
| import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom' | |||
| import { BrowserRouter, Navigate, Route, Routes, useLocation } from 'react-router-dom' | |||
| import RouteBlockerOverlay from '@/components/RouteBlockerOverlay' | |||
| import TopNav from '@/components/TopNav' | |||
| import { Toaster } from '@/components/ui/toaster' | |||
| import { API_BASE_URL } from '@/config' | |||
| @@ -25,6 +26,13 @@ import type { FC } from 'react' | |||
| import type { User } from '@/types' | |||
| const PostDetailRoute = ({ user }: { user: User | null }) => { | |||
| const location = useLocation () | |||
| const key = location.pathname | |||
| return <PostDetailPage key={key} user={user}/> | |||
| } | |||
| export default (() => { | |||
| const [user, setUser] = useState<User | null> (null) | |||
| const [status, setStatus] = useState (200) | |||
| @@ -71,26 +79,29 @@ export default (() => { | |||
| } | |||
| return ( | |||
| <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={<PostDetailPage user={user}/>}/> | |||
| <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> | |||
| </div> | |||
| <Toaster/> | |||
| </BrowserRouter>) | |||
| <> | |||
| <RouteBlockerOverlay/> | |||
| <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="/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> | |||
| </div> | |||
| <Toaster/> | |||
| </BrowserRouter> | |||
| </>) | |||
| }) satisfies FC | |||
| @@ -1,4 +1,4 @@ | |||
| import { Link } from 'react-router-dom' | |||
| import PrefetchLink from '@/components/PrefetchLink' | |||
| import type { FC, MouseEvent } from 'react' | |||
| @@ -11,15 +11,16 @@ 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 }`} | |||
| key={post.id} | |||
| className="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg" | |||
| onClick={onClick}> | |||
| <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"/> | |||
| </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 | |||
| @@ -0,0 +1,37 @@ | |||
| import axios from 'axios' | |||
| import toCamel from 'camelcase-keys' | |||
| import { API_BASE_URL } from '@/config' | |||
| import type { Post } from '@/types' | |||
| export const fetchPosts = async ({ tags, match, limit, cursor }: { | |||
| tags: string | |||
| match: 'any' | 'all' | |||
| limit: number | |||
| cursor?: string }): Promise<{ posts: Post[]; nextCursor: string }> => { | |||
| const res = await axios.get (`${ API_BASE_URL }/posts`, { | |||
| params: { tags, match, limit, ...(cursor && { cursor }) } }) | |||
| return toCamel (res.data as any, { deep: true }) as { posts: Post[] | |||
| nextCursor: string } | |||
| } | |||
| export const fetchPost = async (id: string): Promise<Post> => { | |||
| const res = await axios.get (`${ API_BASE_URL }/posts/${ id }`, { | |||
| headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) | |||
| return toCamel (res.data as any, { deep: true }) as Post | |||
| } | |||
| export const toggleViewedFlg = async (id: string, viewed: boolean): Promise<void> => { | |||
| const url = `${ API_BASE_URL }/posts/${ id }/viewed` | |||
| const opt = { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } } | |||
| if (viewed) | |||
| await axios.post (url, { }, opt) | |||
| else | |||
| await axios.delete (url, opt) | |||
| } | |||
| @@ -0,0 +1,45 @@ | |||
| import { QueryClient } from '@tanstack/react-query' | |||
| import { match } from 'path-to-regexp' | |||
| import { fetchPost, fetchPosts } from '@/lib/posts' | |||
| type Prefetcher = (qc: QueryClient, url: URL) => Promise<void> | |||
| const mPost = match<{ id: string }> ('/posts/:id') | |||
| const prefetchPostsIndex: Prefetcher = async (qc, url) => { | |||
| const tags = url.searchParams.get ('tags') ?? '' | |||
| const match = url.searchParams.get ('match') === 'any' ? 'any' : 'all' | |||
| const limit = Number (url.searchParams.get ('limit') || 20) | |||
| await qc.prefetchQuery ({ | |||
| queryKey: ['posts', 'index', { tags, match, limit }], | |||
| queryFn: () => fetchPosts ({ tags, match, limit }) }) | |||
| } | |||
| const prefetchPostShow: Prefetcher = async (qc, url) => { | |||
| const m = mPost (url.pathname) | |||
| if (!(m)) | |||
| return | |||
| const { id } = m.params | |||
| await qc.prefetchQuery ({ | |||
| queryKey: ['posts', id], | |||
| queryFn: () => fetchPost (id) }) | |||
| } | |||
| export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [ | |||
| { test: u => u.pathname === '/' || u.pathname === '/posts', run: prefetchPostsIndex }, | |||
| { test: u => Boolean (mPost (u.pathname)), run: prefetchPostShow }] | |||
| export const prefetchForURL = async (qc: QueryClient, urlLike: string): Promise<void> => { | |||
| const u = new URL (urlLike, location.origin) | |||
| const jobs = routePrefetchers.filter (r => r.test (u)).map (r => r.run (qc, u)) | |||
| if (jobs.length === 0) | |||
| return | |||
| await Promise.all (jobs) | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| import { QueryClient, QueryClientProvider } from '@tanstack/react-query' | |||
| import { createRoot } from 'react-dom/client' | |||
| import { HelmetProvider } from 'react-helmet-async' | |||
| @@ -6,7 +7,15 @@ import App from '@/App' | |||
| const helmetContext = { } | |||
| const client = new QueryClient ({ | |||
| defaultOptions: { | |||
| queries: { staleTime: 5 * 60 * 1000, | |||
| gcTime: 30 * 60 * 1000, | |||
| retry: 1 }}}) | |||
| createRoot (document.getElementById ('root')!).render ( | |||
| <HelmetProvider context={helmetContext}> | |||
| <App/> | |||
| <QueryClientProvider client={client}> | |||
| <App/> | |||
| </QueryClientProvider> | |||
| </HelmetProvider>) | |||
| @@ -1,5 +1,4 @@ | |||
| import axios from 'axios' | |||
| import toCamel from 'camelcase-keys' | |||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' | |||
| import { useEffect, useState } from 'react' | |||
| import { Helmet } from 'react-helmet-async' | |||
| import { useParams } from 'react-router-dom' | |||
| @@ -12,14 +11,15 @@ import TabGroup, { Tab } from '@/components/common/TabGroup' | |||
| import MainArea from '@/components/layout/MainArea' | |||
| import { Button } from '@/components/ui/button' | |||
| import { toast } from '@/components/ui/use-toast' | |||
| import { API_BASE_URL, SITE_TITLE } from '@/config' | |||
| import { SITE_TITLE } from '@/config' | |||
| import { fetchPost, toggleViewedFlg } from '@/lib/posts' | |||
| import { cn } from '@/lib/utils' | |||
| import NotFound from '@/pages/NotFound' | |||
| import ServiceUnavailable from '@/pages/ServiceUnavailable' | |||
| import type { FC } from 'react' | |||
| import type { Post, User } from '@/types' | |||
| import type { User } from '@/types' | |||
| type Props = { user: User | null } | |||
| @@ -27,48 +27,50 @@ type Props = { user: User | null } | |||
| export default (({ user }: Props) => { | |||
| const { id } = useParams () | |||
| const [post, setPost] = useState<Post | null> (null) | |||
| const { data: post, isError: errorFlg, error } = useQuery ({ | |||
| enabled: Boolean (id), | |||
| queryKey: ['posts', String (id)], | |||
| queryFn: () => fetchPost (String (id)), | |||
| placeholderData: undefined }) | |||
| const qc = useQueryClient () | |||
| const [status, setStatus] = useState (200) | |||
| const changeViewedFlg = async () => { | |||
| const url = `${ API_BASE_URL }/posts/${ id }/viewed` | |||
| const opt = { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } } | |||
| try | |||
| { | |||
| if (post!.viewed) | |||
| await axios.delete (url, opt) | |||
| else | |||
| await axios.post (url, { }, opt) | |||
| // 通信に成功したら “閲覧済” をトグル | |||
| setPost (post => ({ ...post!, viewed: !(post!.viewed) })) | |||
| } | |||
| catch | |||
| { | |||
| toast ({ title: '失敗……', description: '通信に失敗しました……' }) | |||
| } | |||
| } | |||
| const changeViewedFlg = useMutation ({ | |||
| mutationFn: async () => { | |||
| const next = !(post!.viewed) | |||
| await toggleViewedFlg (id!, next) | |||
| return next | |||
| }, | |||
| onMutate: async () => { | |||
| await qc.cancelQueries ({ queryKey: ['posts', String (id)] }) | |||
| const prev = qc.getQueryData<any> (['posts', String (id)]) | |||
| qc.setQueryData (['posts', String (id)], | |||
| (cur: any) => cur ? { ...cur, viewed: !(cur.viewed) } : cur) | |||
| return { prev } | |||
| }, | |||
| onError: (...[, , ctx]) => { | |||
| if (ctx?.prev) | |||
| qc.setQueryData (['posts', String (id)], ctx.prev) | |||
| toast ({ title: '失敗……', description: '通信に失敗しました……' }) | |||
| }, | |||
| onSuccess: () => { | |||
| qc.invalidateQueries ({ queryKey: ['posts', 'index'] }) | |||
| qc.invalidateQueries ({ queryKey: ['related', String (id)] }) | |||
| } }) | |||
| useEffect (() => { | |||
| setPost (null) | |||
| if (!(id)) | |||
| if (!(errorFlg)) | |||
| return | |||
| const fetchPost = async () => { | |||
| try | |||
| { | |||
| const res = await axios.get (`${ API_BASE_URL }/posts/${ id }`, { headers: { | |||
| 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) | |||
| setPost (toCamel (res.data as any, { deep: true }) as Post) | |||
| } | |||
| catch (err) | |||
| { | |||
| if (axios.isAxiosError (err)) | |||
| setStatus (err.status ?? 200) | |||
| } | |||
| } | |||
| fetchPost () | |||
| const code = (error as any)?.response.status ?? (error as any)?.status | |||
| if (code) | |||
| setStatus (code) | |||
| }, [errorFlg, error]) | |||
| useEffect (() => { | |||
| setStatus (200) | |||
| }, [id]) | |||
| switch (status) | |||
| @@ -90,15 +92,18 @@ export default (({ user }: Props) => { | |||
| <meta name="thumbnail" content={post.thumbnail || post.thumbnailBase}/>)} | |||
| {post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>} | |||
| </Helmet> | |||
| <div className="hidden md:block"> | |||
| <TagDetailSidebar post={post}/> | |||
| <TagDetailSidebar post={post ?? null}/> | |||
| </div> | |||
| <MainArea> | |||
| {post | |||
| ? ( | |||
| <> | |||
| <PostEmbed post={post}/> | |||
| <Button onClick={changeViewedFlg} | |||
| <Button onClick={() => changeViewedFlg.mutate ()} | |||
| disabled={changeViewedFlg.isPending} | |||
| className={cn ('text-white', viewedClass)}> | |||
| {post.viewed ? '閲覧済' : '未閲覧'} | |||
| </Button> | |||
| @@ -112,7 +117,10 @@ export default (({ user }: Props) => { | |||
| <Tab name="編輯"> | |||
| <PostEditForm post={post} | |||
| onSave={newPost => { | |||
| setPost (newPost) | |||
| qc.setQueryData (['posts', String (id)], | |||
| (prev: any) => newPost ?? prev) | |||
| qc.invalidateQueries ({ queryKey: ['posts', 'index'] }) | |||
| qc.invalidateQueries ({ queryKey: ['related', String (id)] }) | |||
| toast ({ description: '更新しました.' }) | |||
| }}/> | |||
| </Tab>)} | |||
| @@ -120,8 +128,9 @@ export default (({ user }: Props) => { | |||
| </>) | |||
| : 'Loading...'} | |||
| </MainArea> | |||
| <div className="md:hidden"> | |||
| <TagDetailSidebar post={post}/> | |||
| <TagDetailSidebar post={post ?? null}/> | |||
| </div> | |||
| </div>) | |||
| }) satisfies FC<Props> | |||