|
| 1 | +import { useEffect, useState } from 'react'; |
1 | 2 | import { useFetcher } from '../../src/react/index'; |
2 | 3 | import type { RequestConfig } from '../../src/types/request-handler'; |
3 | 4 |
|
@@ -404,3 +405,209 @@ export const ErrorTypesComponent = ({ |
404 | 405 | </div> |
405 | 406 | ); |
406 | 407 | }; |
| 408 | + |
| 409 | +export const PaginationComponent = () => { |
| 410 | + const [page, setPage] = useState(1); |
| 411 | + const limit = 10; |
| 412 | + |
| 413 | + const { data, isLoading, error } = useFetcher<{ |
| 414 | + data: Array<{ id: number; title: string }>; |
| 415 | + pagination: { |
| 416 | + page: number; |
| 417 | + limit: number; |
| 418 | + total: number; |
| 419 | + totalPages: number; |
| 420 | + hasNext: boolean; |
| 421 | + hasPrev: boolean; |
| 422 | + }; |
| 423 | + }>('/api/posts', { |
| 424 | + params: { page, limit }, |
| 425 | + cacheTime: 30, |
| 426 | + cacheKey: `posts-page-${page}`, |
| 427 | + }); |
| 428 | + |
| 429 | + return ( |
| 430 | + <div> |
| 431 | + <div data-testid="pagination-loading"> |
| 432 | + {isLoading ? 'Loading' : 'Not Loading'} |
| 433 | + </div> |
| 434 | + <div data-testid="pagination-error"> |
| 435 | + {error ? error.message : 'No Error'} |
| 436 | + </div> |
| 437 | + <div data-testid="pagination-data"> |
| 438 | + {data?.data ? JSON.stringify(data.data) : 'No Data'} |
| 439 | + </div> |
| 440 | + <div data-testid="pagination-info"> |
| 441 | + {data?.pagination |
| 442 | + ? `Page ${data.pagination.page} of ${data.pagination.totalPages}` |
| 443 | + : 'No Pagination Info'} |
| 444 | + </div> |
| 445 | + <button |
| 446 | + onClick={() => setPage(page - 1)} |
| 447 | + disabled={!data?.pagination?.hasPrev} |
| 448 | + data-testid="prev-page" |
| 449 | + > |
| 450 | + Previous |
| 451 | + </button> |
| 452 | + <button |
| 453 | + onClick={() => setPage(page + 1)} |
| 454 | + disabled={!data?.pagination?.hasNext} |
| 455 | + data-testid="next-page" |
| 456 | + > |
| 457 | + Next |
| 458 | + </button> |
| 459 | + <div data-testid="current-page">{page}</div> |
| 460 | + </div> |
| 461 | + ); |
| 462 | +}; |
| 463 | + |
| 464 | +export const InfiniteScrollComponent = () => { |
| 465 | + const [allItems, setAllItems] = useState< |
| 466 | + Array<{ id: number; content: string }> |
| 467 | + >([]); |
| 468 | + const [offset, setOffset] = useState(0); |
| 469 | + const [hasMore, setHasMore] = useState(true); |
| 470 | + |
| 471 | + const { data, isLoading } = useFetcher<{ |
| 472 | + items: Array<{ id: number; content: string }>; |
| 473 | + hasMore: boolean; |
| 474 | + nextOffset: number | null; |
| 475 | + }>('/api/feed', { |
| 476 | + params: { offset, limit: 5 }, |
| 477 | + cacheTime: 0, // Don't cache for infinite scroll |
| 478 | + immediate: hasMore, // Only fetch if there's more data |
| 479 | + }); |
| 480 | + |
| 481 | + useEffect(() => { |
| 482 | + if (data?.items) { |
| 483 | + setAllItems((prev) => [...prev, ...data.items]); |
| 484 | + setHasMore(data.hasMore); |
| 485 | + if (data.nextOffset !== null) { |
| 486 | + // Don't auto-advance here, wait for user action |
| 487 | + } |
| 488 | + } |
| 489 | + }, [data]); |
| 490 | + |
| 491 | + const loadMore = () => { |
| 492 | + if (data && data.nextOffset !== null && hasMore) { |
| 493 | + setOffset(data.nextOffset); |
| 494 | + } |
| 495 | + }; |
| 496 | + |
| 497 | + return ( |
| 498 | + <div> |
| 499 | + <div data-testid="infinite-items"> |
| 500 | + {allItems.map((item) => ( |
| 501 | + <div key={item.id} data-testid={`item-${item.id}`}> |
| 502 | + {item.content} |
| 503 | + </div> |
| 504 | + ))} |
| 505 | + </div> |
| 506 | + <div data-testid="infinite-loading"> |
| 507 | + {isLoading ? 'Loading More' : 'Not Loading'} |
| 508 | + </div> |
| 509 | + <div data-testid="items-count">{allItems.length}</div> |
| 510 | + <button |
| 511 | + onClick={loadMore} |
| 512 | + disabled={!hasMore || isLoading} |
| 513 | + data-testid="load-more" |
| 514 | + > |
| 515 | + Load More |
| 516 | + </button> |
| 517 | + <div data-testid="has-more">{hasMore ? 'Has More' : 'No More'}</div> |
| 518 | + </div> |
| 519 | + ); |
| 520 | +}; |
| 521 | + |
| 522 | +export const SearchPaginationComponent = () => { |
| 523 | + const [search, setSearch] = useState('john'); |
| 524 | + const [status, setStatus] = useState('active'); |
| 525 | + const [page, setPage] = useState(1); |
| 526 | + |
| 527 | + const { data, isLoading } = useFetcher<{ |
| 528 | + users: Array<{ id: number; name: string; status: string }>; |
| 529 | + pagination: { |
| 530 | + page: number; |
| 531 | + limit: number; |
| 532 | + total: number; |
| 533 | + totalPages: number; |
| 534 | + }; |
| 535 | + }>('/api/users', { |
| 536 | + params: { search, status, page, limit: 3 }, |
| 537 | + cacheTime: 60, |
| 538 | + cacheKey: `users-${search}-${status}-${page}`, |
| 539 | + dedupeTime: 1000, // Dedupe rapid searches |
| 540 | + }); |
| 541 | + |
| 542 | + // Reset page when search changes |
| 543 | + useEffect(() => { |
| 544 | + setPage(1); |
| 545 | + }, [search, status]); |
| 546 | + |
| 547 | + return ( |
| 548 | + <div> |
| 549 | + <input |
| 550 | + value={search} |
| 551 | + onChange={(e) => setSearch(e.target.value)} |
| 552 | + data-testid="search-input" |
| 553 | + placeholder="Search users..." |
| 554 | + /> |
| 555 | + <select |
| 556 | + value={status} |
| 557 | + onChange={(e) => setStatus(e.target.value)} |
| 558 | + data-testid="status-filter" |
| 559 | + > |
| 560 | + <option value="active">Active</option> |
| 561 | + <option value="inactive">Inactive</option> |
| 562 | + </select> |
| 563 | + |
| 564 | + <div data-testid="search-loading"> |
| 565 | + {isLoading ? 'Searching' : 'Not Searching'} |
| 566 | + </div> |
| 567 | + |
| 568 | + <div data-testid="search-results"> |
| 569 | + {data?.users?.map((user) => ( |
| 570 | + <div key={user.id} data-testid={`user-${user.id}`}> |
| 571 | + {user.name} - {user.status} |
| 572 | + </div> |
| 573 | + )) || 'No Results'} |
| 574 | + </div> |
| 575 | + |
| 576 | + <div data-testid="search-total"> |
| 577 | + {data?.pagination ? `Total: ${data.pagination.total}` : 'No Total'} |
| 578 | + </div> |
| 579 | + |
| 580 | + <div data-testid="search-page"> |
| 581 | + {data?.pagination ? `Page: ${data.pagination.page}` : 'No Page'} |
| 582 | + </div> |
| 583 | + </div> |
| 584 | + ); |
| 585 | +}; |
| 586 | + |
| 587 | +export const ErrorPaginationComponent = ({ attemptCount = 0 }) => { |
| 588 | + const [page, setPage] = useState(1); |
| 589 | + |
| 590 | + const { data, error, isLoading } = useFetcher('/api/posts-error', { |
| 591 | + params: { page }, |
| 592 | + retry: { retries: 3, delay: 100, backoff: 1.5 }, |
| 593 | + cacheTime: 0, // Don't cache error responses |
| 594 | + }); |
| 595 | + |
| 596 | + return ( |
| 597 | + <div> |
| 598 | + <div data-testid="error-pagination-data"> |
| 599 | + {data?.data ? JSON.stringify(data.data) : 'No Data'} |
| 600 | + </div> |
| 601 | + <div data-testid="error-pagination-error"> |
| 602 | + {error ? `Error: ${error.status}` : 'No Error'} |
| 603 | + </div> |
| 604 | + <div data-testid="error-pagination-loading"> |
| 605 | + {isLoading ? 'Loading' : 'Not Loading'} |
| 606 | + </div> |
| 607 | + <button onClick={() => setPage(2)} data-testid="goto-page-2"> |
| 608 | + Go to Page 2 |
| 609 | + </button> |
| 610 | + <div data-testid="error-attempt-count">{attemptCount}</div> |
| 611 | + </div> |
| 612 | + ); |
| 613 | +}; |
0 commit comments