feat(sprint-5): Phase 2 — React Query API client layer

- @tanstack/react-query with QueryClientProvider in providers/index.tsx
- Typed api-client.ts fetch wrapper with ApiError class + apiDownload
- Service modules: members, distributions, stock, reports, dashboard, portal, staff
- Offline banner component (onlineManager subscription)
- API error boundary with retry button
- Loading skeleton components (card, table, chart, form, dashboard)
- i18n for error/loading states (de/en)
This commit is contained in:
Patrick Plate
2026-06-12 19:59:41 +02:00
parent 279f2f6de0
commit f42c166329
20 changed files with 2875 additions and 7 deletions
@@ -0,0 +1,46 @@
"use client"
import { useEffect, useState } from "react"
import { onlineManager } from "@tanstack/react-query"
import { useTranslations } from "next-intl"
import { WifiOff } from "lucide-react"
import type { ReactNode } from "react"
export function OfflineBanner() {
const t = useTranslations("api")
const [isOnline, setIsOnline] = useState(true)
useEffect(() => {
// Subscribe to online manager state changes
const unsubscribe = onlineManager.subscribe((online) => {
setIsOnline(online)
})
// Set initial state
setIsOnline(onlineManager.isOnline())
return () => unsubscribe()
}, [])
if (isOnline) return null
return (
<div
role="alert"
aria-live="polite"
className="bg-destructive/10 border-destructive/30 text-destructive flex items-center gap-2 border-b px-4 py-2 text-sm"
>
<WifiOff className="h-4 w-4 shrink-0" />
<span>{t("offline")}</span>
</div>
)
}
/**
* Wrapper that prevents hydration mismatch for online/offline state.
*/
export function OfflineBannerWrapper({ children }: { children: ReactNode }) {
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
return <>{children}</>
}