Skip to content

feat(a11y): Add app-wide skeleton loading system and unify the data layer - #144

Merged
OffCrazyFreak merged 19 commits into
feat/button-loading-labelsfrom
feat/loading-skeletons
Aug 2, 2026
Merged

feat(a11y): Add app-wide skeleton loading system and unify the data layer#144
OffCrazyFreak merged 19 commits into
feat/button-loading-labelsfrom
feat/loading-skeletons

Conversation

@OffCrazyFreak

Copy link
Copy Markdown
Owner

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:

  1. Detail pages flashed "Greška" / "Proizvod nije pronađen" on hard reload.
  2. Indexes flashed (0) in the heading before the real count landed.
  3. Everything else collapsed to a centred spinner, so the page jumped when content arrived.

PersistQueryClientProvider parks every query at fetchStatus: "idle" while it restores the IndexedDB cache. TanStack Query v5 derives isLoading as isPending && isFetching, so during that window isLoading reads false with data still undefined. All 147 isLoading guards fell straight through to the next branch and rendered the error state, the empty state, or (0).

Shopping list details had a second cause: useGetShoppingListById was 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

  • One keys.ts per domain, replacing three coexisting key styles. The JSON.stringify(params) cijene keys became explicit tuples, so cache seeding no longer depends on every caller passing identical fields.
  • CACHE_TIMES replaces 15 magic numbers, plus a global 60s staleTime so backend queries stop refetching on every mount.
  • Every domain split into keys.ts / queries.ts / hooks.ts, with reads exposed as queryOptions() descriptors rather than useGetX hooks.
  • toUserMessage unifies the two error shapes (RFC 9457 from our backend, CijeneApiError from upstream); ErrorState replaces the bespoke error JSX.
  • useAuthedQuery and useDataPending; useProductsByEans replaces four copies of the per-EAN useQueries block.
  • Authed queries now start on the session rather than the loaded profile, so the data request runs in parallel with /api/users/me instead of behind it.

Loading UI

  • Skeleton kit in components/custom/skeleton/, and AsyncSection with a structurally fixed pending → error → empty → data order.
  • ~27 colocated *-skeleton.tsx files and five route-level loading.tsx.
  • BlockLoadingSpinner retired from page bodies, kept for buttons and short inline actions.
  • Reduced-motion rule for animate-pulse, which no existing rule covered.
  • Remembered row counts, so list skeletons reserve close to the real height.

Docs: new docs/DATA-FETCHING.md, plus a corrected cache-buster value in PWA.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 layer is the large one at 63 files, and it is genuinely atomic: removing productByEanQueryKey and the useGetX hooks is a breaking API change that fans out to 17 call sites at once. Read it as lib/query/lib/api/*/keys.tslib/api/*/hooks.ts → the clients.

⚠️ Merge order

Merge this after #143 (feat/button-loading-labels) and before #142 (feat/digital-cards-rework).

⚠️ Deploy note

Query key shapes changed, so CACHE_BUSTER went "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 in cached-query-keys.ts still matches.

Follow-up: digital cards

(user)/digital-cards was deliberately skipped in this PR, because #142 rebuilds the feature. Once #142 merges, digital cards needs the conventions applied:

  • Split lib/api/digital-cards/ into keys.ts / queries.ts / hooks.ts, with digitalCardQueries as queryOptions() descriptors (it is currently the one domain still on inline keys and the old single-file layout).
  • Move digital-cards-client.tsx onto useAuthedQuery + AsyncSection, replacing the BlockLoadingSpinner and the enabled: isAuthenticated gate.
  • Add digital-card-item-skeleton.tsx and digital-cards-skeleton.tsx, plus (user)/digital-cards/loading.tsx.
  • Swap the bare <Skeleton className="h-64 w-full" /> in digital-card-modal.tsx for a real mirror.
  • Replace the inline ["digitalCards"] keys in use-digital-card-modal.ts and digital-card-item.tsx with the factory.

Conventions are documented in docs/DATA-FETCHING.md and summarised in AGENTS.md.

Verification

Check Result
tsc --noEmit passes, 0 errors, on every commit individually
eslint . 0 errors, 27 warnings, identical to dev baseline
next build passes, 37 routes
Browser not yet done, see below

Not 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

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
@netlify

netlify Bot commented Jul 30, 2026

Copy link
Copy Markdown

Deploy Preview for disscount ready!

Name Link
🔨 Latest commit fc3aef2
🔍 Latest deploy log https://app.netlify.com/projects/disscount/deploys/6a6ec31991698b0008a7df9c
😎 Deploy Preview https://deploy-preview-144--disscount.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 124 files, which is 24 over the limit of 100.

To get a review, narrow the scope:
• coderabbit review --committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3505f80b-004e-48e6-8df8-cb104b864959

📥 Commits

Reviewing files that changed from the base of the PR and between f49d4c7 and fc3aef2.

📒 Files selected for processing (124)
  • AGENTS.md
  • docs/DATA-FETCHING.md
  • docs/PWA.md
  • docs/README.md
  • frontend/src/app/(user)/shopping-lists/[id]/components/items/shopping-list-item-skeleton.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/items/shopping-list-items-skeleton.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-detail-client.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-detail-skeleton.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-header-skeleton.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-info-table-skeleton.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-price-history.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-store-card-skeleton.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-stores-list-skeleton.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-stores-list.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-stores-skeleton.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-data.ts
  • frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-item-mutations.ts
  • frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-mutations.ts
  • frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-price-history.ts
  • frontend/src/app/(user)/shopping-lists/[id]/hooks/use-store-chain-analysis.ts
  • frontend/src/app/(user)/shopping-lists/[id]/loading.tsx
  • frontend/src/app/(user)/shopping-lists/components/forms/shopping-list-modal.tsx
  • frontend/src/app/(user)/shopping-lists/components/shopping-list-item-skeleton.tsx
  • frontend/src/app/(user)/shopping-lists/components/shopping-lists-client.tsx
  • frontend/src/app/(user)/shopping-lists/components/shopping-lists-skeleton.tsx
  • frontend/src/app/(user)/shopping-lists/hooks/use-shopping-list-modal.ts
  • frontend/src/app/(user)/shopping-lists/loading.tsx
  • frontend/src/app/(user)/watchlist/components/watchlist-client.tsx
  • frontend/src/app/(user)/watchlist/components/watchlist-header.tsx
  • frontend/src/app/(user)/watchlist/components/watchlist-item.tsx
  • frontend/src/app/(user)/watchlist/components/watchlist-list.tsx
  • frontend/src/app/(user)/watchlist/components/watchlist-skeleton.tsx
  • frontend/src/app/(user)/watchlist/components/watchlist-suggestions.tsx
  • frontend/src/app/(user)/watchlist/hooks/use-watchlist-data.ts
  • frontend/src/app/(user)/watchlist/hooks/use-watchlist-suggestions.ts
  • frontend/src/app/(user)/watchlist/loading.tsx
  • frontend/src/app/dashboard/components/admin-contact-table.tsx
  • frontend/src/app/dashboard/components/admin-users-stats.tsx
  • frontend/src/app/dashboard/components/admin-users-table.tsx
  • frontend/src/app/dashboard/components/dashboard-guard.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/loading.tsx
  • frontend/src/app/products/[id]/components/price-history/price-history-panel.tsx
  • frontend/src/app/products/[id]/components/product-chains-list-skeleton.tsx
  • frontend/src/app/products/[id]/components/product-chains-section-skeleton.tsx
  • frontend/src/app/products/[id]/components/product-chains-section.tsx
  • frontend/src/app/products/[id]/components/product-detail-client.tsx
  • frontend/src/app/products/[id]/components/product-detail-skeleton.tsx
  • frontend/src/app/products/[id]/components/store-item/store-item-skeleton.tsx
  • frontend/src/app/products/[id]/hooks/use-product-detail.ts
  • frontend/src/app/products/[id]/loading.tsx
  • frontend/src/app/products/components/forms/product-actions-sheet.tsx
  • frontend/src/app/products/components/product-action-buttons.tsx
  • frontend/src/app/products/components/product-info-display-skeleton.tsx
  • frontend/src/app/products/components/products-client.tsx
  • frontend/src/app/products/components/products-skeleton.tsx
  • frontend/src/app/products/hooks/use-selected-shopping-list.ts
  • frontend/src/app/products/hooks/use-watchlist-item-form.ts
  • frontend/src/app/products/page.tsx
  • frontend/src/app/providers/react-query-provider.tsx
  • frontend/src/app/statistics/components/health-status.tsx
  • frontend/src/app/statistics/components/store-item-skeleton.tsx
  • frontend/src/app/statistics/components/store-item.tsx
  • frontend/src/app/statistics/components/stores-list.tsx
  • frontend/src/components/custom/bottom-nav/use-active-list-progress.ts
  • frontend/src/components/custom/common/async-section.tsx
  • frontend/src/components/custom/common/error-state.tsx
  • frontend/src/components/custom/header/components/header-actions-skeleton.tsx
  • frontend/src/components/custom/header/components/header-actions.tsx
  • frontend/src/components/custom/notifications/components/notification-item-skeleton.tsx
  • frontend/src/components/custom/notifications/components/notifications-list.tsx
  • frontend/src/components/custom/product/product-card-skeleton.tsx
  • frontend/src/components/custom/product/product-card.tsx
  • frontend/src/components/custom/product/product-info-skeleton.tsx
  • frontend/src/components/custom/product/product-summary-skeleton.tsx
  • frontend/src/components/custom/product/product-summary.tsx
  • frontend/src/components/custom/settings/hooks/use-settings-defaults.ts
  • frontend/src/components/custom/skeleton/chart-skeleton.tsx
  • frontend/src/components/custom/skeleton/count-skeleton.tsx
  • frontend/src/components/custom/skeleton/page-shell-skeleton.tsx
  • frontend/src/components/custom/skeleton/repeat-skeleton.tsx
  • frontend/src/components/custom/skeleton/section-header-skeleton.tsx
  • frontend/src/components/custom/skeleton/skeleton-region.tsx
  • frontend/src/components/custom/skeleton/table-skeleton.tsx
  • frontend/src/components/custom/skeleton/text-skeleton.tsx
  • frontend/src/context/use-watchlist-notifications.ts
  • frontend/src/context/user-context.tsx
  • frontend/src/hooks/use-product-modals.ts
  • frontend/src/hooks/use-product-navigation.ts
  • frontend/src/hooks/use-remembered-row-count.ts
  • frontend/src/lib/api/admin/hooks.ts
  • frontend/src/lib/api/admin/index.ts
  • frontend/src/lib/api/admin/keys.ts
  • frontend/src/lib/api/admin/queries.ts
  • frontend/src/lib/api/contact/hooks.ts
  • frontend/src/lib/api/contact/index.ts
  • frontend/src/lib/api/contact/keys.ts
  • frontend/src/lib/api/contact/queries.ts
  • frontend/src/lib/api/error-message.ts
  • frontend/src/lib/api/preferences/hooks.ts
  • frontend/src/lib/api/preferences/index.ts
  • frontend/src/lib/api/preferences/keys.ts
  • frontend/src/lib/api/preferences/queries.ts
  • frontend/src/lib/api/shopping-lists/hooks.ts
  • frontend/src/lib/api/shopping-lists/index.ts
  • frontend/src/lib/api/shopping-lists/keys.ts
  • frontend/src/lib/api/users/hooks.ts
  • frontend/src/lib/api/users/index.ts
  • frontend/src/lib/api/users/queries.ts
  • frontend/src/lib/api/watchlist/hooks.ts
  • frontend/src/lib/api/watchlist/index.ts
  • frontend/src/lib/api/watchlist/keys.ts
  • frontend/src/lib/api/watchlist/queries.ts
  • frontend/src/lib/cijene-api/hooks.ts
  • frontend/src/lib/cijene-api/index.ts
  • frontend/src/lib/cijene-api/keys.ts
  • frontend/src/lib/cijene-api/query-hooks.ts
  • frontend/src/lib/cijene-api/use-products-by-eans.ts
  • frontend/src/lib/offline/offline-mutations.ts
  • frontend/src/lib/offline/persister.ts
  • frontend/src/lib/query/cache-times.ts
  • frontend/src/lib/query/use-authed-query.ts
  • frontend/src/lib/query/use-data-pending.ts
  • frontend/src/lib/skeleton/row-count-store.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.
@OffCrazyFreak

Copy link
Copy Markdown
Owner Author

Heads-up: PR #145 must merge before this branch. This branch does not share the donation runtime files, but both changes update docs/README.md, and this PR changes global UI infrastructure.

After rebasing, please reconcile the documentation index so SUPPORT.md remains listed. Also confirm that the new URL-driven ?modal=donate dialog stays independent of loading shells and does not regress focus handling or footer navigation.

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.
Copilot AI review requested due to automatic review settings August 2, 2026 04:10
@OffCrazyFreak
OffCrazyFreak changed the base branch from dev to feat/button-loading-labels August 2, 2026 04:10
@OffCrazyFreak
OffCrazyFreak merged commit ede54ae into feat/button-loading-labels Aug 2, 2026
4 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/*) plus AsyncSection, and updated pages to avoid branching on TanStack Query isLoading under PersistQueryClientProvider.
  • Unified query keys and freshness windows via per-domain keys.ts and CACHE_TIMES, and added useAuthedQuery / useDataPending to standardize auth-gated reads and “pending” detection.
  • Added a public ?modal=donate support 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

  • NoResults renders the provided icon directly. These <Search /> icons are decorative but lack aria-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 be aria-hidden to 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 marked aria-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.

Comment on lines +44 to +47
<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" />}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants