refactor: use X-style route paths
This commit is contained in:
@@ -12,11 +12,7 @@ export function AppShell({
|
||||
<header className="app-header">
|
||||
<p className="brand">TWITTER LITE</p>
|
||||
<nav aria-label="閲覧方法" className="tabs">
|
||||
<Link
|
||||
aria-current={active === 'user' ? 'page' : undefined}
|
||||
search={{ target: '' }}
|
||||
to="/user"
|
||||
>
|
||||
<Link aria-current={active === 'user' ? 'page' : undefined} to="/">
|
||||
ユーザー
|
||||
</Link>
|
||||
<Link
|
||||
@@ -28,8 +24,7 @@ export function AppShell({
|
||||
</Link>
|
||||
<Link
|
||||
aria-current={active === 'list' ? 'page' : undefined}
|
||||
search={{ target: '' }}
|
||||
to="/list"
|
||||
to="/i/lists"
|
||||
>
|
||||
リスト
|
||||
</Link>
|
||||
|
||||
@@ -46,8 +46,8 @@ export function ListPicker({ selectedId }: { selectedId?: string }) {
|
||||
<li key={list.id}>
|
||||
<Link
|
||||
aria-current={selectedId === list.id ? 'page' : undefined}
|
||||
search={{ target: list.id }}
|
||||
to="/list"
|
||||
params={{ listId: list.id }}
|
||||
to="/i/lists/$listId"
|
||||
>
|
||||
<span>{list.name}</span>
|
||||
<small>
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('PostCard', () => {
|
||||
|
||||
expect(
|
||||
screen.getByRole('link', { name: '詳細・スレッド' }),
|
||||
).toHaveAttribute('href', '/status/123')
|
||||
).toHaveAttribute('href', '/yuta/status/123')
|
||||
})
|
||||
|
||||
it('marks the focal post without linking to its current page', () => {
|
||||
|
||||
@@ -91,7 +91,9 @@ export function PostCard({
|
||||
{post.likeCount ?? 0}
|
||||
</span>
|
||||
{!current ? (
|
||||
<a href={`/status/${encodeURIComponent(post.id)}`}>
|
||||
<a
|
||||
href={`/${encodeURIComponent(post.author.username)}/status/${encodeURIComponent(post.id)}`}
|
||||
>
|
||||
詳細・スレッド
|
||||
</a>
|
||||
) : null}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildSearchQuery,
|
||||
listRouteSearchSchema,
|
||||
normalizeListTarget,
|
||||
normalizeTweetId,
|
||||
normalizeUserTarget,
|
||||
@@ -91,14 +90,6 @@ describe('normalizeListTarget', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('listRouteSearchSchema', () => {
|
||||
it('keeps a numeric list target from a hand-entered URL', () => {
|
||||
expect(listRouteSearchSchema.parse({ target: 1234567890 })).toEqual({
|
||||
target: '1234567890',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildSearchQuery', () => {
|
||||
it('keeps a deliberate query unchanged', () => {
|
||||
expect(buildSearchQuery(' AI lang:ja ', false)).toBe('AI lang:ja')
|
||||
|
||||
@@ -43,15 +43,6 @@ export const searchPageInputSchema = z.object({
|
||||
cursor: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
export const userRouteSearchSchema = z.object({
|
||||
target: z.string().catch(''),
|
||||
})
|
||||
|
||||
export const listRouteSearchSchema = z.object({
|
||||
// List IDs are numeric, and the router JSON-parses numeric search values.
|
||||
target: z.coerce.string().catch(''),
|
||||
})
|
||||
|
||||
export const postSearchRouteSchema = z.object({
|
||||
q: z.string().catch(''),
|
||||
product: z.enum(['Top', 'Latest']).catch('Latest'),
|
||||
|
||||
+86
-49
@@ -9,25 +9,21 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as UserRouteImport } from './routes/user'
|
||||
import { Route as SearchRouteImport } from './routes/search'
|
||||
import { Route as ListRouteImport } from './routes/list'
|
||||
import { Route as HandleRouteImport } from './routes/$handle'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as StatusTweetIdRouteImport } from './routes/status.$tweetId'
|
||||
import { Route as IListsRouteImport } from './routes/i.lists'
|
||||
import { Route as IListsListIdRouteImport } from './routes/i.lists_.$listId'
|
||||
import { Route as HandleStatusTweetIdRouteImport } from './routes/$handle_.status.$tweetId'
|
||||
|
||||
const UserRoute = UserRouteImport.update({
|
||||
id: '/user',
|
||||
path: '/user',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const SearchRoute = SearchRouteImport.update({
|
||||
id: '/search',
|
||||
path: '/search',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ListRoute = ListRouteImport.update({
|
||||
id: '/list',
|
||||
path: '/list',
|
||||
const HandleRoute = HandleRouteImport.update({
|
||||
id: '/$handle',
|
||||
path: '/$handle',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
@@ -35,59 +31,85 @@ const IndexRoute = IndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const StatusTweetIdRoute = StatusTweetIdRouteImport.update({
|
||||
id: '/status/$tweetId',
|
||||
path: '/status/$tweetId',
|
||||
const IListsRoute = IListsRouteImport.update({
|
||||
id: '/i/lists',
|
||||
path: '/i/lists',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IListsListIdRoute = IListsListIdRouteImport.update({
|
||||
id: '/i/lists_/$listId',
|
||||
path: '/i/lists/$listId',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const HandleStatusTweetIdRoute = HandleStatusTweetIdRouteImport.update({
|
||||
id: '/$handle_/status/$tweetId',
|
||||
path: '/$handle/status/$tweetId',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/list': typeof ListRoute
|
||||
'/$handle': typeof HandleRoute
|
||||
'/search': typeof SearchRoute
|
||||
'/user': typeof UserRoute
|
||||
'/status/$tweetId': typeof StatusTweetIdRoute
|
||||
'/i/lists': typeof IListsRoute
|
||||
'/$handle/status/$tweetId': typeof HandleStatusTweetIdRoute
|
||||
'/i/lists/$listId': typeof IListsListIdRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/list': typeof ListRoute
|
||||
'/$handle': typeof HandleRoute
|
||||
'/search': typeof SearchRoute
|
||||
'/user': typeof UserRoute
|
||||
'/status/$tweetId': typeof StatusTweetIdRoute
|
||||
'/i/lists': typeof IListsRoute
|
||||
'/$handle/status/$tweetId': typeof HandleStatusTweetIdRoute
|
||||
'/i/lists/$listId': typeof IListsListIdRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/list': typeof ListRoute
|
||||
'/$handle': typeof HandleRoute
|
||||
'/search': typeof SearchRoute
|
||||
'/user': typeof UserRoute
|
||||
'/status/$tweetId': typeof StatusTweetIdRoute
|
||||
'/i/lists': typeof IListsRoute
|
||||
'/$handle_/status/$tweetId': typeof HandleStatusTweetIdRoute
|
||||
'/i/lists_/$listId': typeof IListsListIdRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/list' | '/search' | '/user' | '/status/$tweetId'
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/$handle'
|
||||
| '/search'
|
||||
| '/i/lists'
|
||||
| '/$handle/status/$tweetId'
|
||||
| '/i/lists/$listId'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/list' | '/search' | '/user' | '/status/$tweetId'
|
||||
id: '__root__' | '/' | '/list' | '/search' | '/user' | '/status/$tweetId'
|
||||
to:
|
||||
| '/'
|
||||
| '/$handle'
|
||||
| '/search'
|
||||
| '/i/lists'
|
||||
| '/$handle/status/$tweetId'
|
||||
| '/i/lists/$listId'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/$handle'
|
||||
| '/search'
|
||||
| '/i/lists'
|
||||
| '/$handle_/status/$tweetId'
|
||||
| '/i/lists_/$listId'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
ListRoute: typeof ListRoute
|
||||
HandleRoute: typeof HandleRoute
|
||||
SearchRoute: typeof SearchRoute
|
||||
UserRoute: typeof UserRoute
|
||||
StatusTweetIdRoute: typeof StatusTweetIdRoute
|
||||
IListsRoute: typeof IListsRoute
|
||||
HandleStatusTweetIdRoute: typeof HandleStatusTweetIdRoute
|
||||
IListsListIdRoute: typeof IListsListIdRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/user': {
|
||||
id: '/user'
|
||||
path: '/user'
|
||||
fullPath: '/user'
|
||||
preLoaderRoute: typeof UserRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/search': {
|
||||
id: '/search'
|
||||
path: '/search'
|
||||
@@ -95,11 +117,11 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof SearchRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/list': {
|
||||
id: '/list'
|
||||
path: '/list'
|
||||
fullPath: '/list'
|
||||
preLoaderRoute: typeof ListRouteImport
|
||||
'/$handle': {
|
||||
id: '/$handle'
|
||||
path: '/$handle'
|
||||
fullPath: '/$handle'
|
||||
preLoaderRoute: typeof HandleRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
@@ -109,11 +131,25 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/status/$tweetId': {
|
||||
id: '/status/$tweetId'
|
||||
path: '/status/$tweetId'
|
||||
fullPath: '/status/$tweetId'
|
||||
preLoaderRoute: typeof StatusTweetIdRouteImport
|
||||
'/i/lists': {
|
||||
id: '/i/lists'
|
||||
path: '/i/lists'
|
||||
fullPath: '/i/lists'
|
||||
preLoaderRoute: typeof IListsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/i/lists_/$listId': {
|
||||
id: '/i/lists_/$listId'
|
||||
path: '/i/lists/$listId'
|
||||
fullPath: '/i/lists/$listId'
|
||||
preLoaderRoute: typeof IListsListIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/$handle_/status/$tweetId': {
|
||||
id: '/$handle_/status/$tweetId'
|
||||
path: '/$handle/status/$tweetId'
|
||||
fullPath: '/$handle/status/$tweetId'
|
||||
preLoaderRoute: typeof HandleStatusTweetIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
@@ -121,10 +157,11 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
ListRoute: ListRoute,
|
||||
HandleRoute: HandleRoute,
|
||||
SearchRoute: SearchRoute,
|
||||
UserRoute: UserRoute,
|
||||
StatusTweetIdRoute: StatusTweetIdRoute,
|
||||
IListsRoute: IListsRoute,
|
||||
HandleStatusTweetIdRoute: HandleStatusTweetIdRoute,
|
||||
IListsListIdRoute: IListsListIdRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { UserPage } from './index'
|
||||
|
||||
export const Route = createFileRoute('/$handle')({
|
||||
component: UserRoute,
|
||||
})
|
||||
|
||||
function UserRoute() {
|
||||
const { handle } = Route.useParams()
|
||||
return <UserPage handle={handle} />
|
||||
}
|
||||
@@ -1,24 +1,27 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { AppShell } from '#/components/app-shell'
|
||||
import { PostFeed } from '#/features/posts/components/post-feed'
|
||||
import { InputError, normalizeTweetId } from '#/features/posts/inputs'
|
||||
import {
|
||||
InputError,
|
||||
normalizeTweetId,
|
||||
normalizeUserTarget,
|
||||
} from '#/features/posts/inputs'
|
||||
|
||||
export const Route = createFileRoute('/status/$tweetId')({
|
||||
export const Route = createFileRoute('/$handle_/status/$tweetId')({
|
||||
component: StatusRoute,
|
||||
})
|
||||
|
||||
function StatusRoute() {
|
||||
const { tweetId: rawTweetId } = Route.useParams()
|
||||
const { handle: rawHandle, tweetId: rawTweetId } = Route.useParams()
|
||||
let tweetId: string | undefined
|
||||
let error: string | undefined
|
||||
|
||||
try {
|
||||
normalizeUserTarget(rawHandle)
|
||||
tweetId = normalizeTweetId(rawTweetId)
|
||||
} catch (cause) {
|
||||
error =
|
||||
cause instanceof InputError
|
||||
? cause.message
|
||||
: '投稿 ID を確認してください。'
|
||||
cause instanceof InputError ? cause.message : 'URL を確認してください。'
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -72,7 +72,7 @@ describe('post feed route wiring', () => {
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
it('keeps an empty user URL disabled', async () => {
|
||||
await renderRoute('/user?target=')
|
||||
await renderRoute('/')
|
||||
|
||||
expect(
|
||||
await screen.findByRole('heading', { name: '誰の投稿を見ますか?' }),
|
||||
@@ -81,7 +81,7 @@ describe('post feed route wiring', () => {
|
||||
})
|
||||
|
||||
it('turns a manually entered user URL into a deliberate request', async () => {
|
||||
await renderRoute('/user?target=yuta')
|
||||
await renderRoute('/yuta')
|
||||
|
||||
await waitFor(() =>
|
||||
expect(usePostFeed).toHaveBeenCalledWith({
|
||||
@@ -91,8 +91,17 @@ describe('post feed route wiring', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps an invalid user path local and idle', async () => {
|
||||
await renderRoute('/not-a-handle')
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(
|
||||
'ハンドルは英数字とアンダースコアで入力してください。',
|
||||
)
|
||||
expect(usePostFeed).toHaveBeenCalledWith(undefined)
|
||||
})
|
||||
|
||||
it('keeps an empty list URL disabled', async () => {
|
||||
await renderRoute('/list?target=')
|
||||
await renderRoute('/i/lists')
|
||||
|
||||
expect(
|
||||
await screen.findByRole('heading', { name: 'どのリストを読みますか?' }),
|
||||
@@ -101,7 +110,7 @@ describe('post feed route wiring', () => {
|
||||
})
|
||||
|
||||
it('turns a manually entered list URL into a deliberate request', async () => {
|
||||
await renderRoute('/list?target=1234567890')
|
||||
await renderRoute('/i/lists/1234567890')
|
||||
|
||||
await waitFor(() =>
|
||||
expect(usePostFeed).toHaveBeenCalledWith({
|
||||
@@ -114,8 +123,17 @@ describe('post feed route wiring', () => {
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
it('keeps an invalid list path local and idle', async () => {
|
||||
await renderRoute('/i/lists/not-a-list')
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(
|
||||
'リスト ID は数字で入力してください。',
|
||||
)
|
||||
expect(usePostFeed).toHaveBeenCalledWith(undefined)
|
||||
})
|
||||
|
||||
it('turns a selected authenticated list into a deliberate request', async () => {
|
||||
await renderRoute('/list?target=')
|
||||
await renderRoute('/i/lists')
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('link', { name: /My List.*10 メンバー/ }),
|
||||
@@ -143,7 +161,7 @@ describe('post feed route wiring', () => {
|
||||
})
|
||||
|
||||
it('turns a decimal status path into a deliberate thread request', async () => {
|
||||
await renderRoute('/status/123')
|
||||
await renderRoute('/yuta/status/123')
|
||||
|
||||
expect(await screen.findByRole('heading', { name: '会話' })).toBeVisible()
|
||||
await waitFor(() =>
|
||||
@@ -160,12 +178,21 @@ describe('post feed route wiring', () => {
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps an invalid status path local and idle', async () => {
|
||||
await renderRoute('/status/not-a-tweet')
|
||||
it('keeps an invalid tweet path local and idle', async () => {
|
||||
await renderRoute('/yuta/status/not-a-tweet')
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(
|
||||
'投稿 ID を確認してください。',
|
||||
)
|
||||
expect(usePostFeed).toHaveBeenCalledWith(undefined)
|
||||
})
|
||||
|
||||
it('keeps an invalid status handle local and idle', async () => {
|
||||
await renderRoute('/not-a-handle/status/123')
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(
|
||||
'ハンドルは英数字とアンダースコアで入力してください。',
|
||||
)
|
||||
expect(usePostFeed).toHaveBeenCalledWith(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { AppShell } from '#/components/app-shell'
|
||||
import { ListForm } from '#/features/posts/components/list-form'
|
||||
import { ListPicker } from '#/features/posts/components/list-picker'
|
||||
import { PostFeed } from '#/features/posts/components/post-feed'
|
||||
import { InputError, normalizeListTarget } from '#/features/posts/inputs'
|
||||
|
||||
export const Route = createFileRoute('/i/lists')({
|
||||
component: ListIndexRoute,
|
||||
})
|
||||
|
||||
function ListIndexRoute() {
|
||||
return <ListPage />
|
||||
}
|
||||
|
||||
export function ListPage({ listId = '' }: { listId?: string }) {
|
||||
const navigate = useNavigate()
|
||||
let normalizedListId: string | undefined
|
||||
let error: string | undefined
|
||||
|
||||
if (listId) {
|
||||
try {
|
||||
normalizedListId = normalizeListTarget(listId)
|
||||
} catch (cause) {
|
||||
error =
|
||||
cause instanceof InputError
|
||||
? cause.message
|
||||
: 'リスト ID を確認してください。'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell active="list">
|
||||
<h1>どのリストを読みますか?</h1>
|
||||
<p className="intro">
|
||||
自分のリストを選ぶか、リストURLを入力してください。
|
||||
</p>
|
||||
<ListPicker selectedId={listId || undefined} />
|
||||
<ListForm
|
||||
initialTarget={listId}
|
||||
key={listId}
|
||||
onSubmit={(nextListId) =>
|
||||
navigate({
|
||||
to: '/i/lists/$listId',
|
||||
params: { listId: nextListId },
|
||||
})
|
||||
}
|
||||
/>
|
||||
{error ? (
|
||||
<p className="state" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
<PostFeed
|
||||
request={
|
||||
normalizedListId
|
||||
? { kind: 'list', target: normalizedListId }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { ListPage } from './i.lists'
|
||||
|
||||
export const Route = createFileRoute('/i/lists_/$listId')({
|
||||
component: ListRoute,
|
||||
})
|
||||
|
||||
function ListRoute() {
|
||||
const { listId } = Route.useParams()
|
||||
return <ListPage listId={listId} />
|
||||
}
|
||||
+53
-4
@@ -1,7 +1,56 @@
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { AppShell } from '#/components/app-shell'
|
||||
import { PostFeed } from '#/features/posts/components/post-feed'
|
||||
import { UserForm } from '#/features/posts/components/user-form'
|
||||
import { InputError, normalizeUserTarget } from '#/features/posts/inputs'
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
beforeLoad: () => {
|
||||
throw redirect({ to: '/user', search: { target: '' } })
|
||||
},
|
||||
component: UserIndexRoute,
|
||||
})
|
||||
|
||||
function UserIndexRoute() {
|
||||
return <UserPage />
|
||||
}
|
||||
|
||||
export function UserPage({ handle = '' }: { handle?: string }) {
|
||||
const navigate = useNavigate()
|
||||
let normalizedHandle: string | undefined
|
||||
let error: string | undefined
|
||||
|
||||
if (handle) {
|
||||
try {
|
||||
normalizedHandle = normalizeUserTarget(handle)
|
||||
} catch (cause) {
|
||||
error =
|
||||
cause instanceof InputError
|
||||
? cause.message
|
||||
: 'ハンドルを確認してください。'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell active="user">
|
||||
<h1>誰の投稿を見ますか?</h1>
|
||||
<p className="intro">履歴やおすすめは表示しません。</p>
|
||||
<UserForm
|
||||
initialTarget={handle}
|
||||
key={handle}
|
||||
onSubmit={(nextHandle) =>
|
||||
navigate({ to: '/$handle', params: { handle: nextHandle } })
|
||||
}
|
||||
/>
|
||||
{error ? (
|
||||
<p className="state" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
<PostFeed
|
||||
request={
|
||||
normalizedHandle
|
||||
? { kind: 'user', target: normalizedHandle }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { AppShell } from '#/components/app-shell'
|
||||
import { ListForm } from '#/features/posts/components/list-form'
|
||||
import { ListPicker } from '#/features/posts/components/list-picker'
|
||||
import { PostFeed } from '#/features/posts/components/post-feed'
|
||||
import { listRouteSearchSchema } from '#/features/posts/inputs'
|
||||
|
||||
export const Route = createFileRoute('/list')({
|
||||
validateSearch: (search) => listRouteSearchSchema.parse(search),
|
||||
component: ListRoute,
|
||||
})
|
||||
|
||||
function ListRoute() {
|
||||
const { target } = Route.useSearch()
|
||||
const navigate = Route.useNavigate()
|
||||
return (
|
||||
<AppShell active="list">
|
||||
<h1>どのリストを読みますか?</h1>
|
||||
<p className="intro">
|
||||
自分のリストを選ぶか、リストURLを入力してください。
|
||||
</p>
|
||||
<ListPicker selectedId={target || undefined} />
|
||||
<ListForm
|
||||
initialTarget={target}
|
||||
key={target}
|
||||
onSubmit={(nextTarget) => navigate({ search: { target: nextTarget } })}
|
||||
/>
|
||||
<PostFeed request={target ? { kind: 'list', target } : undefined} />
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { AppShell } from '#/components/app-shell'
|
||||
import { PostFeed } from '#/features/posts/components/post-feed'
|
||||
import { UserForm } from '#/features/posts/components/user-form'
|
||||
import { userRouteSearchSchema } from '#/features/posts/inputs'
|
||||
|
||||
export const Route = createFileRoute('/user')({
|
||||
validateSearch: (search) => userRouteSearchSchema.parse(search),
|
||||
component: UserRoute,
|
||||
})
|
||||
|
||||
function UserRoute() {
|
||||
const { target } = Route.useSearch()
|
||||
const navigate = Route.useNavigate()
|
||||
return (
|
||||
<AppShell active="user">
|
||||
<h1>誰の投稿を見ますか?</h1>
|
||||
<p className="intro">履歴やおすすめは表示しません。</p>
|
||||
<UserForm
|
||||
initialTarget={target}
|
||||
key={target}
|
||||
onSubmit={(nextTarget) => navigate({ search: { target: nextTarget } })}
|
||||
/>
|
||||
<PostFeed request={target ? { kind: 'user', target } : undefined} />
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user