arbret/frontend/app/components/PageLayout.tsx
counterweight b86b506d72
Refactor frontend: Add reusable hooks and components
- Created useAsyncData hook: Eliminates repetitive data fetching boilerplate
  - Handles loading, error, and data state automatically
  - Supports enabled/disabled fetching
  - Provides refetch function

- Created PageLayout component: Standardizes page structure
  - Handles loading state, authorization checks, header, error display
  - Reduces ~10 lines of boilerplate per page

- Created useMutation hook: Simplifies action handling
  - Manages loading state and errors for mutations
  - Supports success/error callbacks
  - Used for cancel, create, revoke actions

- Created ErrorDisplay component: Standardizes error UI
  - Consistent error banner styling across app
  - Integrated into PageLayout

- Created useForm hook: Foundation for form state management
  - Handles form data, validation, dirty checking
  - Ready for future form migrations

- Migrated pages to use new patterns:
  - invites/page.tsx: useAsyncData + PageLayout
  - trades/page.tsx: useAsyncData + PageLayout + useMutation
  - trades/[id]/page.tsx: useAsyncData
  - admin/price-history/page.tsx: useAsyncData + PageLayout
  - admin/invites/page.tsx: useMutation for create/revoke

Benefits:
- ~40% reduction in boilerplate code
- Consistent patterns across pages
- Easier to maintain and extend
- Better type safety

All tests passing (32 frontend, 33 e2e)
2025-12-25 21:30:35 +01:00

65 lines
1.5 KiB
TypeScript

import { ReactNode } from "react";
import { Header } from "./Header";
import { LoadingState } from "./LoadingState";
import { ErrorDisplay } from "./ErrorDisplay";
import { layoutStyles } from "../styles/shared";
type PageId =
| "profile"
| "invites"
| "exchange"
| "trades"
| "admin-invites"
| "admin-availability"
| "admin-trades"
| "admin-price-history";
interface PageLayoutProps {
/** Current page ID for navigation highlighting */
currentPage: PageId;
/** Whether the page is loading (shows loading state) */
isLoading?: boolean;
/** Whether the user is authorized (hides page if false) */
isAuthorized?: boolean;
/** Error message to display */
error?: string | null;
/** Page content */
children: ReactNode;
/** Custom content wrapper style */
contentStyle?: React.CSSProperties;
}
/**
* Standard page layout component that handles common page structure:
* - Loading state
* - Authorization check
* - Header navigation
* - Error banner
* - Content wrapper
*/
export function PageLayout({
currentPage,
isLoading = false,
isAuthorized = true,
error,
children,
contentStyle,
}: PageLayoutProps) {
if (isLoading) {
return <LoadingState />;
}
if (!isAuthorized) {
return null;
}
return (
<main style={layoutStyles.main}>
<Header currentPage={currentPage} />
<div style={contentStyle || layoutStyles.contentCentered}>
<ErrorDisplay error={error} />
{children}
</div>
</main>
);
}