@@ -28,22 +28,18 @@ import { Card, CardContent, CardDescription, CardHeader } from '@tecnova/ui/comp
2828import { Input } from '@tecnova/ui/components/input' ;
2929import { Skeleton } from '@tecnova/ui/components/skeleton' ;
3030import { Table , TableBody , TableCell , TableRow } from '@tecnova/ui/components/table' ;
31- import { apiFetch , readErrorMessage } from '@tecnova/ui/lib/ api-client ' ;
31+ import { useApiResource } from '@tecnova/ui/hooks/use- api-resource ' ;
3232import { motion , useReducedMotion } from 'motion/react' ;
3333import Link from 'next/link' ;
34- import { useCallback , useEffect , useMemo , useState } from 'react' ;
34+ import { useMemo , useState } from 'react' ;
3535import { AnimatedNumber } from '@/components/animated-number' ;
3636import { PageShell } from '@/components/page-shell' ;
3737import { PanelHeader } from '@/components/panel-header' ;
3838import { Reveal } from '@/components/reveal' ;
39+ import { CheckinErrorScreen } from '@/components/screen-error' ;
3940import { formatJapaneseDate } from '@/lib/format' ;
4041import { listItemTransition } from '@/lib/motion' ;
4142
42- type State =
43- | { kind : 'loading' }
44- | { kind : 'list' ; items : PreRegisteredListResponse [ 'participants' ] }
45- | { kind : 'error' ; message : string } ;
46-
4743type PreRegisteredParticipant = PreRegisteredListResponse [ 'participants' ] [ number ] ;
4844
4945function ParticipantDetails ( { item } : { item : PreRegisteredParticipant } ) {
@@ -124,33 +120,14 @@ function RegistrationSteps() {
124120
125121export default function FirstTimePage ( ) {
126122 const prefersReduced = useReducedMotion ( ) ;
127- const [ state , setState ] = useState < State > ( { kind : 'loading' } ) ;
123+ const { state, reload } = useApiResource < PreRegisteredListResponse > ( '/checkin/pre-registered' ) ;
128124 const [ query , setQuery ] = useState ( '' ) ;
129125
130- const loadParticipants = useCallback ( async ( ) => {
131- setState ( { kind : 'loading' } ) ;
132- try {
133- const r = await apiFetch ( '/checkin/pre-registered' ) ;
134- if ( ! r . ok ) throw new Error ( await readErrorMessage ( r ) ) ;
135- const data = ( await r . json ( ) ) as PreRegisteredListResponse ;
136- setState ( { kind : 'list' , items : data . participants } ) ;
137- } catch ( e ) {
138- setState ( {
139- kind : 'error' ,
140- message : e instanceof Error ? e . message : String ( e ) ,
141- } ) ;
142- }
143- } , [ ] ) ;
144-
145- useEffect ( ( ) => {
146- void loadParticipants ( ) ;
147- } , [ loadParticipants ] ) ;
148-
149126 const filteredItems = useMemo ( ( ) => {
150- if ( state . kind !== 'list ' ) return [ ] ;
127+ if ( state . kind !== 'ok ' ) return [ ] ;
151128 const normalizedQuery = query . trim ( ) . toLowerCase ( ) ;
152- if ( ! normalizedQuery ) return state . items ;
153- return state . items . filter ( ( item ) => {
129+ if ( ! normalizedQuery ) return state . data . participants ;
130+ return state . data . participants . filter ( ( item ) => {
154131 const values = [
155132 item . fullName ,
156133 item . nickname ,
@@ -162,7 +139,7 @@ export default function FirstTimePage() {
162139 } ) ;
163140 } , [ query , state ] ) ;
164141
165- if ( state . kind === 'loading' ) {
142+ if ( state . kind === 'idle' || state . kind === ' loading') {
166143 return (
167144 < PageShell >
168145 < div className = "mx-auto flex w-full max-w-6xl flex-1 flex-col gap-4" >
@@ -189,27 +166,21 @@ export default function FirstTimePage() {
189166
190167 if ( state . kind === 'error' ) {
191168 return (
192- < main className = "flex flex-1 flex-col items-center justify-center gap-6 bg-rose-50 p-6 text-center" >
193- < Alert variant = "destructive" className = "max-w-xl text-left text-lg" >
194- < IconAlertCircle className = "size-6" aria-hidden = "true" />
195- < AlertTitle > 一覧を表示できません</ AlertTitle >
196- < AlertDescription > { state . message } </ AlertDescription >
197- </ Alert >
198- < div className = "grid w-full max-w-xl grid-cols-1 gap-3 sm:grid-cols-2" >
199- < Button
200- type = "button"
201- size = "lg"
202- onClick = { ( ) => void loadParticipants ( ) }
203- className = "h-16 text-xl"
204- >
205- < IconRefresh className = "size-6" data-icon = "inline-start" />
206- 再読み込み
207- </ Button >
208- < Button asChild variant = "secondary" size = "lg" className = "h-16 text-xl" >
209- < Link href = "/" > ホームに戻る</ Link >
210- </ Button >
211- </ div >
212- </ main >
169+ < CheckinErrorScreen
170+ title = "一覧を表示できません"
171+ message = { state . message }
172+ actions = {
173+ < >
174+ < Button type = "button" size = "lg" onClick = { ( ) => reload ( ) } className = "h-16 text-xl" >
175+ < IconRefresh className = "size-6" data-icon = "inline-start" />
176+ 再読み込み
177+ </ Button >
178+ < Button asChild variant = "secondary" size = "lg" className = "h-16 text-xl" >
179+ < Link href = "/" > ホームに戻る</ Link >
180+ </ Button >
181+ </ >
182+ }
183+ />
213184 ) ;
214185 }
215186
@@ -233,14 +204,18 @@ export default function FirstTimePage() {
233204 style = { { height : 'auto' } }
234205 className = "w-fit px-4 py-2 text-base"
235206 >
236- 未登録 < AnimatedNumber value = { state . items . length } className = "mx-1 tabular-nums" />
207+ 未登録{ ' ' }
208+ < AnimatedNumber
209+ value = { state . data . participants . length }
210+ className = "mx-1 tabular-nums"
211+ />
237212 人
238213 </ Badge >
239214 </ div >
240215
241216 < RegistrationSteps />
242217
243- { state . items . length === 0 ? (
218+ { state . data . participants . length === 0 ? (
244219 < Alert className = "border-sky-200 bg-white" >
245220 < IconAlertCircle className = "size-5" aria-hidden = "true" />
246221 < AlertTitle > 今、登録できる人はいません</ AlertTitle >
@@ -282,7 +257,7 @@ export default function FirstTimePage() {
282257 type = "button"
283258 variant = "outline"
284259 size = "lg"
285- onClick = { ( ) => void loadParticipants ( ) }
260+ onClick = { ( ) => reload ( ) }
286261 className = "h-14 text-lg"
287262 >
288263 < IconRefresh className = "size-6" data-icon = "inline-start" />
0 commit comments