アニメーション(#139) (#252)

#139

#139

#139

#139

#139

Merge branch 'feature/140' into feature/139

Merge remote-tracking branch 'origin/main' into feature/139

#140

Merge remote-tracking branch 'origin/main' into feature/140

Merge remote-tracking branch 'origin/main' into feature/140

#140 ぼちぼち

Merge remote-tracking branch 'origin/main' into feature/140

#140

#140

#140

#139 アニメーション

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #252
This commit was merged in pull request #252.
This commit is contained in:
2026-02-05 23:25:27 +09:00
parent f3cd108b2e
commit 797e67ac37
22 changed files with 810 additions and 311 deletions
+65
View File
@@ -0,0 +1,65 @@
import axios from 'axios'
import toCamel from 'camelcase-keys'
import { API_BASE_URL } from '@/config'
type Opt = {
params?: Record<string, unknown>
headers?: Record<string, string> }
const client = axios.create ({ baseURL: API_BASE_URL })
const withUserCode = (opt?: Opt): Opt => ({
...opt,
headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '',
...(opt?.headers ?? { }) } })
const apiP = async <T> (
method: 'post' | 'put' | 'patch',
path: string,
body?: unknown,
opt?: Opt,
): Promise<T> => {
const res = await client[method] (path, body ?? { }, withUserCode (opt))
return toCamel (res.data as any, { deep: true }) as T
}
export const apiGet = async <T> (
path: string,
opt?: Opt,
): Promise<T> => {
const res = await client.get (path, withUserCode (opt))
return toCamel (res.data as any, { deep: true }) as T
}
export const apiPost = async <T> (
path: string,
body?: unknown,
opt?: Opt,
): Promise<T> => apiP ('post', path, body, opt)
export const apiPut = async <T> (
path: string,
body?: unknown,
opt?: Opt,
): Promise<T> => apiP ('put', path, body, opt)
export const apiPatch = async <T> (
path: string,
body?: unknown,
opt?: Opt,
): Promise<T> => apiP ('patch', path, body, opt)
export const apiDelete = async (
path: string,
opt?: Opt,
): Promise<void> => {
await client.delete (path, withUserCode (opt))
}
+30
View File
@@ -0,0 +1,30 @@
import { apiDelete, apiGet, apiPost } from '@/lib/api'
import type { Post } from '@/types'
export const fetchPosts = async (
{ tags, match, page, limit, cursor }: {
tags: string
match: 'any' | 'all'
page?: number
limit?: number
cursor?: string }
): Promise<{
posts: Post[]
count: number
nextCursor: string }> => await apiGet ('/posts', {
params: {
tags,
match,
...(page && { page }),
...(limit && { limit }),
...(cursor && { cursor }) } })
export const fetchPost = async (id: string): Promise<Post> => await apiGet (`/posts/${ id }`)
export const toggleViewedFlg = async (id: string, viewed: boolean): Promise<void> => {
await (viewed ? apiPost : apiDelete) (`/posts/${ id }/viewed`)
}
+49
View File
@@ -0,0 +1,49 @@
import { QueryClient } from '@tanstack/react-query'
import { match } from 'path-to-regexp'
import { fetchPost, fetchPosts } from '@/lib/posts'
import { postsKeys } from '@/lib/queryKeys'
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 m = url.searchParams.get ('match') === 'any' ? 'any' : 'all'
const page = Number (url.searchParams.get ('page') || 1)
const limit = Number (url.searchParams.get ('limit') || 20)
await qc.prefetchQuery ({
queryKey: postsKeys.index ({ tags, match: m, page, limit }),
queryFn: () => fetchPosts ({ tags, match: m, page, limit }) })
}
const prefetchPostShow: Prefetcher = async (qc, url) => {
const m = mPost (url.pathname)
if (!(m))
return
const { id } = m.params
await qc.prefetchQuery ({
queryKey: postsKeys.show (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)
}
+10
View File
@@ -0,0 +1,10 @@
export const postsKeys = {
root: ['posts'] as const,
index: (p: { tags: string; match: 'any' | 'all'; page: number; limit: number }) =>
['posts', 'index', p] as const,
show: (id: string) => ['posts', id] as const,
related: (id: string) => ['related', id] as const }
export const wikiKeys = {
root: ['wiki'] as const,
show: (title: string, p: { version: string }) => ['wiki', title, p] as const }
+7
View File
@@ -0,0 +1,7 @@
import { apiGet } from '@/lib/api'
import type { Tag } from '@/types'
export const fetchTagByName = async (name: string): Promise<Tag> =>
await apiGet (`/tags/name/${ name }`)
+14
View File
@@ -0,0 +1,14 @@
import { apiGet } from '@/lib/api'
import type { WikiPage } from '@/types'
export const fetchWikiPage = async (id: string): Promise<WikiPage> =>
await apiGet (`/wiki/${ id }`)
export const fetchWikiPageByTitle = async (
title: string,
{ version }: { version?: string },
): Promise<WikiPage> =>
await apiGet (`/wiki/title/${ title }`, { params: version ? { version } : { } })