feat(a11y): Add app-wide skeleton loading system and unify the data layer - #144
Conversation
Changes: - Add components/custom/skeleton/ primitives: SkeletonRegion, RepeatSkeleton, CountSkeleton, TextSkeleton, SectionHeaderSkeleton, TableSkeleton, ChartSkeleton and PageShellSkeleton - Add AsyncSection, which fixes the state order as pending, error, empty, data - Add ErrorState and toUserMessage, one entry point for turning either error shape into Croatian copy - Add useDataPending, plus the remembered row-count store and its hooks - Disable animate-pulse under prefers-reduced-motion The building blocks for the loading sweep, added first so later batches only compose them. AsyncSection makes the state order structural because several pages checked error or emptiness before loading had finished, which is what painted "Greška" and "(0)" on a cold cache. Notes: - animate-pulse was the one animation in the app that no reduced-motion rule covered, so skeletons would have kept pulsing for readers who asked for less. - CountSkeleton renders a span rather than the shared Skeleton div, because a heading only permits phrasing content.
Changes: - Add a sibling <component-name>-skeleton.tsx for every content component on the shopping list, product, watchlist and statistics surfaces - Export PRODUCT_SUMMARY_ROW_CLASSES and PRODUCT_SUMMARY_IMAGE_CLASSES from product-summary, and share them with its skeleton - Drop the isLoading prop from ProductSummary and ProductCard, so the caller picks the component instead of the component branching internally Skeletons are server-renderable and take no hooks, so loading.tsx and a client pending branch can share one file. Sharing the wrapper classes rather than re-typing them is what keeps a row and its placeholder the same height. Notes: - ProductCardSkeleton still accepts trailing and actions, because a watchlist row knows its controls before it knows its product and they stay live. - Bars are h-[1lh] inside a wrapper carrying the real text's font classes. This project sets --spacing to 0.2rem, so a fixed h-4 is 12.8px and matches nothing.
Changes: - Add loading.tsx to /shopping-lists, /shopping-lists/[id], /watchlist and /products/[id], each rendering that route's page skeleton - Swap the /products Suspense fallback from a spinner to ProductsSkeleton These paint during the RSC navigation, before the client component mounts, which is the window the single global spinner used to fill. Notes: - A page skeleton mirrors each collapsible section's stored default open state, so the page height does not jump once the real component reads localStorage. Price history is stored closed; items and stores are stored open. - loading.tsx renders on the server, so it cannot read the remembered row count and takes the fixed fallback. The client refines it on mount.
Changes: - Swap the price history chart spinners for ChartSkeleton - Convert statistics, the dashboard guard, the header auth button and the notifications list onto skeletons and AsyncSection - Make app/loading.tsx a neutral page shell instead of a centred spinner - Record the loading UI and data fetching conventions in AGENTS.md BlockLoadingSpinner now only appears where a spinner is genuinely right: the button loading state and short inline actions. Content loading gets a skeleton, so a page keeps its height instead of collapsing and then shoving the viewport. Notes: - The dashboard guard also covers the moment after a denial while its redirect runs, so a page shell is friendlier there than a bare spinner.
Changes: - Add a keys.ts per domain, replacing three coexisting key styles, and turn the stringified-params cijene keys into explicit tuples - Add CACHE_TIMES and a global 60s staleTime, replacing 15 magic numbers - Split every lib/api domain into keys.ts, queries.ts and hooks.ts, with reads exposed as queryOptions() descriptors rather than useGetX hooks - Add useAuthedQuery, which folds the session into enabled and returns pending and requiresAuth - Add useProductsByEans, replacing four copies of the per-EAN useQueries block - Wire every client onto AsyncSection, its skeleton, and a count pill - Gate the shopping list detail query on auth and show LoginRequired - Bump the offline cache buster to 2 Detail pages rendered "Greška" and indexes rendered "(0)" for a frame on every reload. PersistQueryClientProvider parks queries at fetchStatus idle while it restores IndexedDB, and v5 derives isLoading as isPending && isFetching, so it reads false with data still undefined and every guard fell through to the next branch. Shopping list detail had a second cause: it was the only user-scoped query with no auth gate, so it 401'd before the token existed and reported an auth-timing failure as "list not found". Notes: - Reads are descriptors, not hooks, because useAuthedQuery reads useUser and user-context imports the lib/api barrel, so a domain hook importing it would close an import cycle. - Query key shapes changed, hence the buster bump: existing users take one cold load after this deploys. Top-level key roots are unchanged, so the offline allowlist in cached-query-keys.ts still matches. - lib/api/digital-cards is left alone as dead code pending its own removal.
Changes: - Add docs/DATA-FETCHING.md covering the three layers, query keys, cache times, useAuthedQuery, AsyncSection, the skeleton convention and the gotchas - Correct the cache buster value in PWA.md, now "2" - Index the new doc in docs/README.md
Changes: - Expose hasSession from UserProvider, true as soon as better-auth resolves - Gate useAuthedQuery's enabled and requiresAuth on hasSession - Switch the remaining useProductsByEans gates from isAuthenticated to hasSession Authed pages queued behind a request they did not depend on. isAuthenticated is !!user, so it only turns true once /api/users/me returns, which put every data query behind the profile fetch: get-session, then token, then users/me, and only then the list. A request only needs a session to be authorised, so the profile and the data now go out together and total time is the slower of the two rather than their sum. Notes: - pending still waits for the profile on purpose. The watchlist sorts on user.pinnedStores, so painting rows before it lands would reorder them under the reader. That costs nothing now the fetch has already started. - requiresAuth keys on the session too, so a failed profile fetch surfaces as an error instead of telling a signed-in reader to sign in.
Changes: - Explain why useAuthedQuery gates on the session rather than the loaded profile, with a sequence diagram of the parallelised requests - Expand the HydrationBoundary prefetch note into a staged TODO, with the route order and the traps: the browser-only token path, key parity, the persister interaction, and keeping the skeletons for client navigation
✅ Deploy Preview for disscount ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Important Review skippedToo many files! This PR contains 124 files, which is 24 over the limit of 100. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (124)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Changes: - Add a public Ko-fi modal from sidebar and footer support controls - Preserve focus when the dialog is dismissed and record future recognition work - Clarify the current repository funding configuration Keeps voluntary support discoverable without adding payment logic or a landing-page prompt.
Changes: - Link the ten existing project skills from the repository root - Keep Claude-Code skill folders as the single source of truth Lets Codex discover the same project workflows without duplicating their instructions.
Changes: - Add the support-system reference and verification guidance - Refresh the README support copy and documentation index Explains the public Ko-fi path, GitHub funding configuration, and consent requirements for future recognition.
|
Heads-up: PR #145 must merge before this branch. This branch does not share the donation runtime files, but both changes update After rebasing, please reconcile the documentation index so Please resolve any resulting conflict or convention mismatch in this branch. |
feat(ui): Add Ko-fi donation entry points
Changes: - Update the donation copy, action layout, icon sizing, and future sponsors TODO - Keep the footer divider vertical and align the wordmark typography - Place the future community section TODO between pricing and FAQ Keeps the support experience consistent with existing shared UI conventions.
Changes: - Increase the back-to-top icon to an optically balanced 28px - Use an explicit rem size unaffected by the custom spacing scale - Document the relationship between the FAB and icon dimensions The previous spacing-based utility rendered the icon smaller than intended.
Changes: - Use 56px spacing between landing sections at every breakpoint - Remove shared main bottom padding - Align the final landing section with the footer spacing Keep the landing page rhythm consistent while allowing the shared footer to provide spacing for other pages.
Changes: - Keep header navigation beside the brand across visible breakpoints Remove unnecessary centering on mid-sized screens.
Changes: - Prevent the default mousedown on MultiSelectItem so the click does not blur the search input Command items render as divs, so pressing one moved focus to the body and the caret left the search box. Preventing the default keeps focus in the input while the click still selects, so typing can continue after a pick.
There was a problem hiding this comment.
Pull request overview
This PR introduces an app-wide, cache-restore-safe loading pattern (skeletons plus a fixed pending -> error -> empty -> data render order), and restructures the React Query data layer around consistent key factories, shared cache times, and read descriptors so the loading fixes hold across future pages.
Changes:
- Added a reusable skeleton system (
components/custom/skeleton/*) plusAsyncSection, and updated pages to avoid branching on TanStack QueryisLoadingunderPersistQueryClientProvider. - Unified query keys and freshness windows via per-domain
keys.tsandCACHE_TIMES, and addeduseAuthedQuery/useDataPendingto standardize auth-gated reads and “pending” detection. - Added a public
?modal=donatesupport flow (Ko-fi) and supporting docs, navigation, and funding copy.
Reviewed changes
Copilot reviewed 151 out of 151 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates public support copy and Ko-fi button label. |
| frontend/src/lib/skeleton/row-count-store.ts | Persists list skeleton row counts in localStorage. |
| frontend/src/lib/query/use-data-pending.ts | Provides a restore-safe “pending” signal (uses useIsRestoring). |
| frontend/src/lib/query/use-authed-query.ts | Standardizes auth-gated queries and exposes pending/requiresAuth. |
| frontend/src/lib/query/cache-times.ts | Centralizes named staleTime windows as CACHE_TIMES. |
| frontend/src/lib/offline/persister.ts | Bumps persisted cache buster to "2" for key-shape changes. |
| frontend/src/lib/offline/offline-mutations.ts | Updates invalidation keys to the new domain key factories. |
| frontend/src/lib/modal/modal-registry.ts | Adds a public donate modal target. |
| frontend/src/lib/cijene-api/use-products-by-eans.ts | Deduplicates per-EAN useQueries usage into a shared hook. |
| frontend/src/lib/cijene-api/query-hooks.ts | Switches cijene query keys and staleTimes to shared factories/constants. |
| frontend/src/lib/cijene-api/keys.ts | Adds tuple-based CIJENE_QUERY_KEYS to replace stringified params. |
| frontend/src/lib/cijene-api/index.ts | Re-exports CIJENE_QUERY_KEYS from the cijene barrel. |
| frontend/src/lib/cijene-api/hooks.ts | Uses CIJENE_QUERY_KEYS and CACHE_TIMES for price history queries. |
| frontend/src/lib/api/watchlist/queries.ts | Splits watchlist fetchers into a queries.ts module. |
| frontend/src/lib/api/watchlist/keys.ts | Adds WATCHLIST_QUERY_KEYS and documents offline root constraints. |
| frontend/src/lib/api/watchlist/index.ts | Converts watchlist barrel to queries/hooks/keys exports. |
| frontend/src/lib/api/watchlist/hooks.ts | Adds watchlistQueries descriptors plus mutation hooks. |
| frontend/src/lib/api/users/queries.ts | Splits user fetchers into a queries.ts module. |
| frontend/src/lib/api/users/index.ts | Converts users barrel to queries/hooks exports. |
| frontend/src/lib/api/users/hooks.ts | Keeps user update mutation as a hook, documents provider ownership. |
| frontend/src/lib/api/shopping-lists/keys.ts | Adds SHOPPING_LIST_QUERY_KEYS and documents offline root constraints. |
| frontend/src/lib/api/shopping-lists/index.ts | Re-exports shopping list keys alongside queries/hooks. |
| frontend/src/lib/api/shopping-lists/hooks.ts | Introduces shoppingListQueries read descriptors and updates invalidations. |
| frontend/src/lib/api/preferences/queries.ts | Splits preferences fetchers into a queries.ts module. |
| frontend/src/lib/api/preferences/keys.ts | Adds PREFERENCES_QUERY_KEYS and documents offline roots. |
| frontend/src/lib/api/preferences/index.ts | Converts preferences barrel to queries/hooks/keys exports. |
| frontend/src/lib/api/preferences/hooks.ts | Adds preferencesQueries descriptors plus update mutations. |
| frontend/src/lib/api/error-message.ts | Adds toUserMessage to unify backend Problem Details and upstream errors. |
| frontend/src/lib/api/contact/queries.ts | Splits contact fetchers into a queries.ts module. |
| frontend/src/lib/api/contact/keys.ts | Defines contact inbox/admin query keys. |
| frontend/src/lib/api/contact/index.ts | Converts contact barrel to queries/hooks/keys exports. |
| frontend/src/lib/api/contact/hooks.ts | Adds contactQueries descriptors and admin action mutations. |
| frontend/src/lib/api/admin/queries.ts | Splits admin fetchers into a queries.ts module. |
| frontend/src/lib/api/admin/keys.ts | Defines admin query keys (non-persisted). |
| frontend/src/lib/api/admin/index.ts | Converts admin barrel to queries/hooks/keys exports. |
| frontend/src/lib/api/admin/hooks.ts | Adds adminQueries descriptors and admin mutations. |
| frontend/src/hooks/use-remembered-row-count.ts | Client hooks for reading/writing remembered skeleton row counts. |
| frontend/src/hooks/use-product-navigation.ts | Updates product cache seeding to new CIJENE_QUERY_KEYS. |
| frontend/src/hooks/use-product-modals.ts | Updates product cache seeding to new CIJENE_QUERY_KEYS. |
| frontend/src/context/user-context.tsx | Adds hasSession for session-based gating (faster than profile). |
| frontend/src/context/use-watchlist-notifications.ts | Moves watchlist + product fetching to useAuthedQuery and useProductsByEans. |
| frontend/src/constants/navigation.ts | Adds “Podrži Disscount” nav item (donate modal) and icon import. |
| frontend/src/constants/donation.ts | Centralizes the Ko-fi URL constant. |
| frontend/src/components/custom/skeleton/text-skeleton.tsx | Adds reusable multi-line text skeleton component. |
| frontend/src/components/custom/skeleton/table-skeleton.tsx | Adds generic table skeleton for admin tables. |
| frontend/src/components/custom/skeleton/skeleton-region.tsx | Adds a11y wrapper for skeleton regions (role="status" plus aria-hidden). |
| frontend/src/components/custom/skeleton/section-header-skeleton.tsx | Adds skeleton for collapsible section header rows. |
| frontend/src/components/custom/skeleton/repeat-skeleton.tsx | Adds a small helper for repeating skeleton rows/cards. |
| frontend/src/components/custom/skeleton/page-shell-skeleton.tsx | Adds neutral fallback page skeleton for route-level loading. |
| frontend/src/components/custom/skeleton/count-skeleton.tsx | Adds heading-safe count placeholder (span, not div). |
| frontend/src/components/custom/skeleton/chart-skeleton.tsx | Adds chart footprint skeleton used by price history panels. |
| frontend/src/components/custom/sidebar/sidebar-support-nav.tsx | Updates sidebar support section description and includes donate item. |
| frontend/src/components/custom/settings/hooks/use-settings-defaults.ts | Switches pinned data reads to useAuthedQuery with descriptors. |
| frontend/src/components/custom/product/product-summary.tsx | Removes inline loading UI and exports shared class constants for skeleton parity. |
| frontend/src/components/custom/product/product-summary-skeleton.tsx | Adds skeleton variant that shares wrapper classes with ProductSummary. |
| frontend/src/components/custom/product/product-info-skeleton.tsx | Adds line-accurate skeleton for ProductInfo. |
| frontend/src/components/custom/product/product-card.tsx | Removes isLoading prop plumbing from ProductCard. |
| frontend/src/components/custom/product/product-card-skeleton.tsx | Adds ProductCard skeleton wrapper. |
| frontend/src/components/custom/notifications/components/notifications-list.tsx | Replaces spinner branch with AsyncSection plus skeleton items. |
| frontend/src/components/custom/notifications/components/notification-item-skeleton.tsx | Adds skeleton row for notifications. |
| frontend/src/components/custom/modal/modal-shell.tsx | Allows description to be ReactNode for richer modal copy. |
| frontend/src/components/custom/modal-router/modal-router.tsx | Mounts DonationModal from URL-driven modal routing. |
| frontend/src/components/custom/header/components/header-nav.tsx | Adjusts header nav layout (adds mr-auto). |
| frontend/src/components/custom/header/components/header-actions.tsx | Replaces inline skeleton with dedicated HeaderActionsSkeleton. |
| frontend/src/components/custom/header/components/header-actions-skeleton.tsx | Adds header actions skeleton component. |
| frontend/src/components/custom/form/multi-select.tsx | Prevents focus loss on item mousedown, keeps typing flow intact. |
| frontend/src/components/custom/fab/back-to-top-button.tsx | Adjusts icon sizing to match visual spec with custom spacing scale. |
| frontend/src/components/custom/donation/donation-modal.tsx | Adds public donate modal with Ko-fi outbound link and focus restoration. |
| frontend/src/components/custom/common/footer.tsx | Adjusts footer layout and swaps divider implementation. |
| frontend/src/components/custom/common/footer-support-icons.tsx | Updates footer support icon block description and uses shared nav items. |
| frontend/src/components/custom/common/error-state.tsx | Adds shared ErrorState component using toUserMessage. |
| frontend/src/components/custom/common/async-section.tsx | Adds fixed-order async state renderer for sections. |
| frontend/src/components/custom/bottom-nav/use-active-list-progress.ts | Uses useAuthedQuery for shopping list detail read. |
| frontend/src/app/statistics/components/stores-list.tsx | Migrates stores stats UI to AsyncSection and restore-safe pending. |
| frontend/src/app/statistics/components/store-item.tsx | Uses pending state and replaces spinner with TableSkeleton. |
| frontend/src/app/statistics/components/store-item-skeleton.tsx | Adds skeleton row for statistics store item. |
| frontend/src/app/statistics/components/health-status.tsx | Replaces spinner with skeleton and restore-safe pending state. |
| frontend/src/app/providers/react-query-provider.tsx | Sets global default staleTime to CACHE_TIMES.default. |
| frontend/src/app/products/page.tsx | Uses a route-level ProductsSkeleton as Suspense fallback. |
| frontend/src/app/products/hooks/use-watchlist-item-form.ts | Uses useAuthedQuery for watchlist-by-product lookups. |
| frontend/src/app/products/hooks/use-selected-shopping-list.ts | Uses descriptors and useAuthedQuery for list reads. |
| frontend/src/app/products/components/products-skeleton.tsx | Adds full products index skeleton (search, filters, heading, rows). |
| frontend/src/app/products/components/products-client.tsx | Replaces isLoading branching with useDataPending and AsyncSection. |
| frontend/src/app/products/components/product-info-display-skeleton.tsx | Adds detail-page info table skeleton. |
| frontend/src/app/products/components/product-action-buttons.tsx | Uses useAuthedQuery for watchlist read. |
| frontend/src/app/products/components/forms/product-actions-sheet.tsx | Uses ProductSummarySkeleton instead of loading prop branch. |
| frontend/src/app/products/[id]/loading.tsx | Adds route-level detail loading skeleton. |
| frontend/src/app/products/[id]/hooks/use-product-detail.ts | Switches to isPending naming for pending detection. |
| frontend/src/app/products/[id]/components/store-item/store-item-skeleton.tsx | Adds per-chain collapsed-row skeleton. |
| frontend/src/app/products/[id]/components/product-detail-skeleton.tsx | Adds full detail page skeleton. |
| frontend/src/app/products/[id]/components/product-detail-client.tsx | Uses AsyncSection with restore-safe pending and ErrorState. |
| frontend/src/app/products/[id]/components/product-chains-section.tsx | Uses AsyncSection for prices section (pending, error, empty). |
| frontend/src/app/products/[id]/components/product-chains-section-skeleton.tsx | Adds skeleton for the entire chains section. |
| frontend/src/app/products/[id]/components/product-chains-list-skeleton.tsx | Adds chains list skeleton (sort row plus store items). |
| frontend/src/app/products/[id]/components/price-history/price-history-panel.tsx | Replaces spinner with ChartSkeleton. |
| frontend/src/app/loading.tsx | Replaces centered spinner with a neutral PageShellSkeleton. |
| frontend/src/app/layout.tsx | Tweaks main padding to reduce layout shift. |
| frontend/src/app/globals.css | Disables skeleton pulse animation under reduced motion. |
| frontend/src/app/dashboard/components/dashboard-guard.tsx | Uses PageShellSkeleton during guard pending or redirect. |
| frontend/src/app/dashboard/components/admin-users-table.tsx | Uses useAuthedQuery and TableSkeleton for admin users. |
| frontend/src/app/dashboard/components/admin-users-stats.tsx | Shares useAuthedQuery for admin users list stats. |
| frontend/src/app/dashboard/components/admin-contact-table.tsx | Uses useAuthedQuery and TableSkeleton for admin contact inbox. |
| frontend/src/app/(user)/watchlist/loading.tsx | Adds route-level watchlist loading skeleton. |
| frontend/src/app/(user)/watchlist/hooks/use-watchlist-suggestions.ts | Switches product batching to useProductsByEans and isPending. |
| frontend/src/app/(user)/watchlist/hooks/use-watchlist-data.ts | Uses useAuthedQuery, requiresAuth, and useProductsByEans. |
| frontend/src/app/(user)/watchlist/components/watchlist-suggestions.tsx | Uses AsyncSection and CountSkeleton instead of spinners. |
| frontend/src/app/(user)/watchlist/components/watchlist-skeleton.tsx | Adds watchlist page skeleton. |
| frontend/src/app/(user)/watchlist/components/watchlist-list.tsx | Uses skeleton rows rather than centered spinner. |
| frontend/src/app/(user)/watchlist/components/watchlist-item.tsx | Uses ProductCardSkeleton while product loads, keeps controls live. |
| frontend/src/app/(user)/watchlist/components/watchlist-header.tsx | Uses CountSkeleton to avoid flashing (0) while loading. |
| frontend/src/app/(user)/watchlist/components/watchlist-client.tsx | Adds remembered skeleton row counts and uses requiresAuth gate. |
| frontend/src/app/(user)/shopping-lists/loading.tsx | Adds route-level shopping lists loading skeleton. |
| frontend/src/app/(user)/shopping-lists/hooks/use-shopping-list-modal.ts | Updates invalidation keys to SHOPPING_LIST_QUERY_KEYS. |
| frontend/src/app/(user)/shopping-lists/components/shopping-lists-skeleton.tsx | Adds shopping lists index skeleton. |
| frontend/src/app/(user)/shopping-lists/components/shopping-lists-client.tsx | Uses useAuthedQuery, AsyncSection, CountSkeleton, remembered rows. |
| frontend/src/app/(user)/shopping-lists/components/shopping-list-item-skeleton.tsx | Adds skeleton for a shopping list card. |
| frontend/src/app/(user)/shopping-lists/components/forms/shopping-list-modal.tsx | Uses useAuthedQuery by-id descriptor and key constants for seeding. |
| frontend/src/app/(user)/shopping-lists/[id]/loading.tsx | Adds route-level shopping list detail loading skeleton. |
| frontend/src/app/(user)/shopping-lists/[id]/hooks/use-store-chain-analysis.ts | Switches product batching to useProductsByEans. |
| frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-price-history.ts | Uses CIJENE_QUERY_KEYS for history queries. |
| frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-mutations.ts | Updates invalidation keys to SHOPPING_LIST_QUERY_KEYS. |
| frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-item-mutations.ts | Updates optimistic cache keys to SHOPPING_LIST_QUERY_KEYS. |
| frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-data.ts | Uses useAuthedQuery plus useProductsByEans for detail data. |
| frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-stores-skeleton.tsx | Adds stores section body skeleton. |
| frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-stores-list.tsx | Uses AsyncSection and restore-safe pending for stores section. |
| frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-stores-list-skeleton.tsx | Adds stores section wrapper skeleton (header plus body). |
| frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-store-card-skeleton.tsx | Adds per-chain store card skeleton. |
| frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-price-history.tsx | Replaces spinner with ChartSkeleton. |
| frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-info-table-skeleton.tsx | Adds skeleton for list info table. |
| frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-header-skeleton.tsx | Adds skeleton for detail header with real back link. |
| frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-detail-skeleton.tsx | Adds full shopping list detail skeleton. |
| frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-detail-client.tsx | Uses requiresAuth gate, AsyncSection, ErrorState, remembered rows. |
| frontend/src/app/(user)/shopping-lists/[id]/components/items/shopping-list-items-skeleton.tsx | Adds items section skeleton wrapper. |
| frontend/src/app/(user)/shopping-lists/[id]/components/items/shopping-list-item-skeleton.tsx | Adds per-item row skeleton. |
| frontend/src/app/(root)/page.tsx | Tweaks landing page spacing and adds a contributor/support TODO. |
| docs/SUPPORT.md | Documents Ko-fi support flow, a11y, and future recognition constraints. |
| docs/README.md | Links to DATA-FETCHING and SUPPORT docs. |
| docs/PWA.md | Updates persister cache buster documentation to "2". |
| AGENTS.md | Adds data fetching and skeleton conventions reference section. |
| .github/FUNDING.yml | Clarifies Ko-fi fee conditions and Sponsors enablement notes. |
Suppressed comments (3)
frontend/src/app/products/components/products-client.tsx:125
NoResultsrenders the provided icon directly. These<Search />icons are decorative but lackaria-hidden, so assistive tech may announce them as an unlabeled graphic. Mark them as decorative.
frontend/src/app/products/components/products-client.tsx:135- Same as above: this decorative
<Search />icon is rendered directly and should bearia-hiddento avoid being announced as an unlabeled graphic.
frontend/src/app/products/components/products-client.tsx:170 - This
<Search />icon in the empty prompt is decorative but not markedaria-hidden, so it may be announced as an unlabeled graphic before the heading text. Mark it decorative.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <Separator | ||
| orientation="vertical" | ||
| className="data-[orientation=vertical]:h-4" | ||
| /> |
| error={error} | ||
| errorState={ | ||
| <ErrorState | ||
| icon={<Search className="size-12 text-red-700 mx-auto mb-4" />} |
Fixes the loading behaviour across the app, and unifies the data layer underneath it so the fix holds for future pages.
Why
Three reported symptoms shared one root cause:
(0)in the heading before the real count landed.PersistQueryClientProviderparks every query atfetchStatus: "idle"while it restores the IndexedDB cache. TanStack Query v5 derivesisLoadingasisPending && isFetching, so during that windowisLoadingreads false withdatastillundefined. All 147isLoadingguards fell straight through to the next branch and rendered the error state, the empty state, or(0).Shopping list details had a second cause:
useGetShoppingListByIdwas the only user-scoped query with no auth gate, so it fired before the better-auth token existed, got a 401, and reported an auth-timing failure as "list not found".What changed
Data layer
keys.tsper domain, replacing three coexisting key styles. TheJSON.stringify(params)cijene keys became explicit tuples, so cache seeding no longer depends on every caller passing identical fields.CACHE_TIMESreplaces 15 magic numbers, plus a global 60sstaleTimeso backend queries stop refetching on every mount.keys.ts/queries.ts/hooks.ts, with reads exposed asqueryOptions()descriptors rather thanuseGetXhooks.toUserMessageunifies the two error shapes (RFC 9457 from our backend,CijeneApiErrorfrom upstream);ErrorStatereplaces the bespoke error JSX.useAuthedQueryanduseDataPending;useProductsByEansreplaces four copies of the per-EANuseQueriesblock./api/users/meinstead of behind it.Loading UI
components/custom/skeleton/, andAsyncSectionwith a structurally fixedpending → error → empty → dataorder.*-skeleton.tsxfiles and five route-levelloading.tsx.BlockLoadingSpinnerretired from page bodies, kept for buttons and short inline actions.animate-pulse, which no existing rule covered.Docs: new
docs/DATA-FETCHING.md, plus a corrected cache-buster value inPWA.md.Review order
The commits are ordered so each one builds standalone (verified with
git rebase --exec 'tsc --noEmit'). The first three are purely additive and are the easy ones to skim first.refactor(api): Unify the data layeris the large one at 63 files, and it is genuinely atomic: removingproductByEanQueryKeyand theuseGetXhooks is a breaking API change that fans out to 17 call sites at once. Read it aslib/query/→lib/api/*/keys.ts→lib/api/*/hooks.ts→ the clients.Merge this after #143 (
feat/button-loading-labels) and before #142 (feat/digital-cards-rework).Query key shapes changed, so
CACHE_BUSTERwent"1"→"2". Every existing user takes one cold load after this deploys, then it is back to normal. Top-level key roots are unchanged, so the offline allowlist incached-query-keys.tsstill matches.Follow-up: digital cards
(user)/digital-cardswas deliberately skipped in this PR, because #142 rebuilds the feature. Once #142 merges, digital cards needs the conventions applied:lib/api/digital-cards/intokeys.ts/queries.ts/hooks.ts, withdigitalCardQueriesasqueryOptions()descriptors (it is currently the one domain still on inline keys and the old single-file layout).digital-cards-client.tsxontouseAuthedQuery+AsyncSection, replacing theBlockLoadingSpinnerand theenabled: isAuthenticatedgate.digital-card-item-skeleton.tsxanddigital-cards-skeleton.tsx, plus(user)/digital-cards/loading.tsx.<Skeleton className="h-64 w-full" />indigital-card-modal.tsxfor a real mirror.["digitalCards"]keys inuse-digital-card-modal.tsanddigital-card-item.tsxwith the factory.Conventions are documented in
docs/DATA-FETCHING.mdand summarised inAGENTS.md.Verification
tsc --noEmiteslint .devbaselinenext buildNot yet verified in a browser: cold-cache reload per route, warm-cache renavigation, logged-out, offline replay, reduced motion, CLS. Worth doing before merge.
🤖 Generated with Claude Code