+ {/* Selector card skeleton */}
+
+
+
+
+
+
+
+ {Array.from({ length: 10 }).map((_, i) => (
+
+ ))}
+
+
+
+
+ {/* Pokemon card skeleton */}
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/react-query-demo/info-content.ts b/src/features/react-query-demo/info-content.ts
new file mode 100644
index 0000000..005f1fc
--- /dev/null
+++ b/src/features/react-query-demo/info-content.ts
@@ -0,0 +1,46 @@
+import type { InfobarContent } from '@/components/ui/infobar';
+
+export const reactQueryInfoContent: InfobarContent = {
+ title: 'React Query Pattern',
+ sections: [
+ {
+ title: 'Server Prefetch',
+ description:
+ 'Data is prefetched on the server using getQueryClient().prefetchQuery(). The dehydrated state is passed to HydrationBoundary so the client starts with cached data — no loading spinners on first load.',
+ links: [
+ {
+ title: 'TanStack Query SSR Docs',
+ url: 'https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr'
+ }
+ ]
+ },
+ {
+ title: 'Query Options',
+ description:
+ 'Query keys and fetch functions are defined in a shared queryOptions() object. This is reused across server prefetch and client hooks, keeping them in sync.',
+ links: [
+ {
+ title: 'queryOptions API',
+ url: 'https://tanstack.com/query/latest/docs/framework/react/reference/queryOptions'
+ }
+ ]
+ },
+ {
+ title: 'Suspense Query',
+ description:
+ 'The client uses useSuspenseQuery() which integrates with React Suspense. Combined with server prefetch, data is available immediately — Suspense only shows the fallback on subsequent navigations if the cache is stale.',
+ links: []
+ },
+ {
+ title: 'Optimistic Mutations',
+ description:
+ 'Mutations use onMutate to optimistically update the cache before the request completes. On error, the previous state is rolled back. On settle, the query is invalidated to refetch fresh data.',
+ links: [
+ {
+ title: 'Optimistic Updates Guide',
+ url: 'https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates'
+ }
+ ]
+ }
+ ]
+};
diff --git a/src/features/users/api/mutations.ts b/src/features/users/api/mutations.ts
new file mode 100644
index 0000000..3ea82ac
--- /dev/null
+++ b/src/features/users/api/mutations.ts
@@ -0,0 +1,27 @@
+import { mutationOptions } from '@tanstack/react-query';
+import { getQueryClient } from '@/lib/query-client';
+import { createUser, updateUser, deleteUser } from './service';
+import { userKeys } from './queries';
+import type { UserMutationPayload } from './types';
+
+export const createUserMutation = mutationOptions({
+ mutationFn: (data: UserMutationPayload) => createUser(data),
+ onSuccess: () => {
+ getQueryClient().invalidateQueries({ queryKey: userKeys.all });
+ }
+});
+
+export const updateUserMutation = mutationOptions({
+ mutationFn: ({ id, values }: { id: number; values: UserMutationPayload }) =>
+ updateUser(id, values),
+ onSuccess: () => {
+ getQueryClient().invalidateQueries({ queryKey: userKeys.all });
+ }
+});
+
+export const deleteUserMutation = mutationOptions({
+ mutationFn: (id: number) => deleteUser(id),
+ onSuccess: () => {
+ getQueryClient().invalidateQueries({ queryKey: userKeys.all });
+ }
+});
diff --git a/src/features/users/api/queries.ts b/src/features/users/api/queries.ts
new file mode 100644
index 0000000..e76c2da
--- /dev/null
+++ b/src/features/users/api/queries.ts
@@ -0,0 +1,17 @@
+import { queryOptions } from '@tanstack/react-query';
+import { getUsers } from './service';
+import type { User, UserFilters } from './types';
+
+export type { User };
+
+export const userKeys = {
+ all: ['users'] as const,
+ list: (filters: UserFilters) => [...userKeys.all, 'list', filters] as const,
+ detail: (id: number) => [...userKeys.all, 'detail', id] as const
+};
+
+export const usersQueryOptions = (filters: UserFilters) =>
+ queryOptions({
+ queryKey: userKeys.list(filters),
+ queryFn: () => getUsers(filters)
+ });
diff --git a/src/features/users/api/service.ts b/src/features/users/api/service.ts
new file mode 100644
index 0000000..c938e18
--- /dev/null
+++ b/src/features/users/api/service.ts
@@ -0,0 +1,47 @@
+// ============================================================
+// User Service — Data Access Layer
+// ============================================================
+// This is the ONLY file you modify when connecting to your backend.
+// Queries (queries.ts) and components import from here — they never change.
+//
+// Pick your pattern and replace the function bodies below:
+//
+// 1. Server Actions + ORM (Prisma / Drizzle / Supabase)
+// → Add 'use server' at the top of this file
+// → Call your ORM directly in each function
+//
+// 2. Route Handlers + ORM
+// → import { apiClient } from '@/lib/api-client'
+// → return apiClient