feat: add authenticated list reading
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ListForm } from './list-form'
|
||||
import { SearchForm } from './search-form'
|
||||
import { UserForm } from './user-form'
|
||||
|
||||
@@ -29,6 +30,32 @@ describe('UserForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('ListForm', () => {
|
||||
it('submits the list ID from a pasted list URL', () => {
|
||||
const onSubmit = vi.fn()
|
||||
render(<ListForm initialTarget="" onSubmit={onSubmit} />)
|
||||
|
||||
fireEvent.change(screen.getByLabelText('リスト URL または ID'), {
|
||||
target: { value: 'https://x.com/i/lists/1234567890' },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '表示' }))
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith('1234567890')
|
||||
})
|
||||
|
||||
it('explains invalid input beside the field', () => {
|
||||
const onSubmit = vi.fn()
|
||||
render(<ListForm initialTarget="" onSubmit={onSubmit} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '表示' }))
|
||||
|
||||
expect(screen.getByRole('alert')).toHaveTextContent(
|
||||
'リストの URL または ID を入力してください。',
|
||||
)
|
||||
expect(onSubmit).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('SearchForm', () => {
|
||||
it('uses a labelled grouping for ranking controls', () => {
|
||||
render(
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { type FormEvent, useState } from 'react'
|
||||
import { normalizeListTarget } from '../inputs'
|
||||
|
||||
type Props = {
|
||||
initialTarget: string
|
||||
onSubmit: (target: string) => void
|
||||
}
|
||||
|
||||
export function ListForm({ initialTarget, onSubmit }: Props) {
|
||||
const [target, setTarget] = useState(initialTarget)
|
||||
const [error, setError] = useState('')
|
||||
const submit = (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
const listId = normalizeListTarget(target)
|
||||
setError('')
|
||||
onSubmit(listId)
|
||||
} catch (cause) {
|
||||
setError(
|
||||
cause instanceof Error ? cause.message : '入力内容を確認してください。',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="intent-form" onSubmit={submit}>
|
||||
<label className="field">
|
||||
<span>リスト URL または ID</span>
|
||||
<input
|
||||
autoComplete="off"
|
||||
aria-describedby={error ? 'list-error' : undefined}
|
||||
aria-invalid={Boolean(error)}
|
||||
name="target"
|
||||
value={target}
|
||||
onChange={(event) => setTarget(event.currentTarget.value)}
|
||||
placeholder="https://x.com/i/lists/1234567890 または ID"
|
||||
/>
|
||||
{error ? (
|
||||
<span className="field-error" id="list-error" role="alert">
|
||||
{error}
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
<button type="submit">表示</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { ListChoicesLoadError, useListChoices } from '../use-list-choices'
|
||||
|
||||
export function ListPicker({ selectedId }: { selectedId?: string }) {
|
||||
const query = useListChoices()
|
||||
|
||||
if (query.isPending) {
|
||||
return (
|
||||
<section aria-labelledby="list-picker-title" className="list-picker">
|
||||
<h2 id="list-picker-title">自分のリスト</h2>
|
||||
<p className="list-picker-state" role="status">
|
||||
リストを取得しています…
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
if (query.isError) {
|
||||
const error =
|
||||
query.error instanceof ListChoicesLoadError
|
||||
? query.error.detail
|
||||
: { message: 'リストを取得できませんでした。', retryable: true }
|
||||
return (
|
||||
<section aria-labelledby="list-picker-title" className="list-picker">
|
||||
<h2 id="list-picker-title">自分のリスト</h2>
|
||||
<div className="list-picker-state" role="alert">
|
||||
<p>{error.message}</p>
|
||||
{error.retryable ? (
|
||||
<button onClick={() => void query.refetch()} type="button">
|
||||
再試行
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section aria-labelledby="list-picker-title" className="list-picker">
|
||||
<h2 id="list-picker-title">自分のリスト</h2>
|
||||
{query.data.length === 0 ? (
|
||||
<p className="list-picker-state">リストはありません。</p>
|
||||
) : (
|
||||
<ul className="list-choices">
|
||||
{query.data.map((list) => (
|
||||
<li key={list.id}>
|
||||
<Link
|
||||
aria-current={selectedId === list.id ? 'page' : undefined}
|
||||
search={{ target: list.id }}
|
||||
to="/list"
|
||||
>
|
||||
<span>{list.name}</span>
|
||||
<small>
|
||||
{list.isPrivate ? '非公開 · ' : ''}
|
||||
{list.memberCount?.toLocaleString('ja-JP') ?? 0} メンバー
|
||||
</small>
|
||||
{list.description ? <p>{list.description}</p> : null}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user