Skip to content

feat(digital-cards): Rebuild the loyalty card wallet - #142

Open
OffCrazyFreak wants to merge 19 commits into
devfrom
feat/digital-cards-rework
Open

feat(digital-cards): Rebuild the loyalty card wallet#142
OffCrazyFreak wants to merge 19 commits into
devfrom
feat/digital-cards-rework

Conversation

@OffCrazyFreak

@OffCrazyFreak OffCrazyFreak commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Important

Merge order: #143, then #144, then this PR. Both are still open.

dev is merged in as of 432db56c, so this branch is currently even with it and mergeable. #145 (donate) already landed and is included.

After #144 merges, this branch still needs

Tracked so it cannot get lost, per the note on this PR. #144 deliberately skipped digital-cards, so this is the last domain on pre-#144 conventions.

Data layer

  • Split lib/api/digital-cards/index.ts into keys.ts / queries.ts / hooks.ts, matching every other domain.
  • Add DIGITAL_CARD_QUERY_KEYS and replace the inline ["digitalCards", "me"] / ["digitalCards"] literals.
  • Expose reads as digitalCardQueries queryOptions() descriptors instead of useGetCurrentUserDigitalCards.

Loading UI

  • digital-cards-client.tsx onto useAuthedQuery (drops the enabled: isAuthenticated gate and the userLoading || isLoading merge) and AsyncSection.
  • requiresAuth for the LoginRequired gate instead of !userLoading && !isAuthenticated.
  • Add digital-card-item-skeleton.tsx, digital-cards-skeleton.tsx and (user)/digital-cards/loading.tsx.
  • Replace the bare <Skeleton className="h-96 w-full" /> in digital-card-modal.tsx with a mirror of the real content.
  • Remove BlockLoadingSpinner from the page body; buttons and short inline actions only.

Offline

  • Keep the digitalCards key root in cached-query-keys.ts spelled exactly as-is, or persistence silently stops.

Do not branch on a query's isLoading in new code: under PersistQueryClientProvider it reads false with no data while the IndexedDB cache restores. Use useAuthedQuery's pending or useDataPending(...). Reference: docs/DATA-FETCHING.md.

Rebuilds digital cards from the ground up. The previous implementation was deleted rather than migrated: it had no store link, no images, no pinning and no offline writes, so there was nothing worth keeping.

Eight focused commits, ordered so each builds on the last.

What it does

  • Save a loyalty, gift, membership or other card, with a store picked from the official chains or typed freely.
  • Enter the number by hand or scan it, which fills both the value and the detected code type.
  • Render the barcode or QR on the fly, with a live preview in the form so a wrong symbology is caught before saving.
  • Attach an icon and photos of both card faces, as the backup when a generated code will not scan.
  • Pin, sort six ways, and search by name, store or note.
  • Works offline for reads and writes, which is the point: a shop with no signal is exactly where a card gets used.

Design notes

The card is drawn as a physical object rather than a table row: a colour gradient with a specular band, a light catching top edge and a raised icon medallion, flipping to its code on hover. The checkout modal is the deliberate opposite, a stark white maximum contrast panel, because a scanner needs contrast rather than styling.

Colour is locked to one saturation and lightness pair, so every hue a user can reach stays legible under white text. It is suggested from the chain's brand colour, then from an uploaded image, but only until the user picks one themselves.

Before merging

  • Run DROP TABLE public.digital_card; on production. It still holds the old table with real rows. ddl-auto=update never drops or renames, so otherwise the new schema merges into stale columns and the new NOT NULL columns fail against existing rows. The local dev table is already dropped.
  • Resolve one conflict with dev: digital-card-item.tsx was deleted here and modified on dev. Keep the deletion, it is the old list row replaced by digital-card-tile.tsx. The other two overlapping files auto-merge.
  • Confirm the backend compiles. It is read verified only, since Maven does not run in this worktree.
  • Pick one of the three colour picker variants, which currently ship behind a temporary switcher. The other two and the switcher then get deleted.

Verification done

tsc --noEmit and Prettier pass. ESLint reports 0 errors; the single new warning mirrors use-view-mode.ts, which hydrates from localStorage the same way for the same SSR reason. pnpm build compiled and typechecked, then stopped collecting page data on missing env vars, because the worktree has no .env.local.

Nothing behavioural is verified yet: the flip and its reduced motion gate, offline replay, scan to fill, wake lock, deep links and colour extraction all need the running app.

Things worth a reviewer's attention

  • bwip-js does not tree shake. BWIPP is one generated blob, so importing thirteen encoders costs the same as all hundred: 844 KB raw, 210 KB gzipped, confirmed by finding unimported symbologies in the output. Webpack scopes it to the /digital-cards route chunk, so no other page pays for it. Swapping to a 1D plus QR pair would cut it to roughly 50 KB at the cost of coverage.
  • StoreNameSuggestionService.record() is REQUIRES_NEW deliberately. It runs inside the card save transaction, and a concurrent duplicate on the unique normalized_name would otherwise mark that transaction rollback only and fail the save.
  • Community store names carry no user reference of any kind, since the suggestion list is public.
  • Card codes never reach localStorage (excluded from the form draft) and are not searchable, so a number cannot land in a ?q= URL or in history.
  • PWA shortcuts are now Skeniraj, Kartice, Popisi. Praćenje moves to fourth and no longer appears on Android, which resolves the pick-the-final-three question in Add Karta and Digitalne kartice to the PWA app shortcuts when they ship #127.

Relates to #127, which this only half resolves: Digitalne kartice now has its icons and a deliberate order, but Karta still needs a decision and a MapIcon entry, and the device reinstall check is unrun. Leave #127 open.

Closes #54. The rename from value to codeValue stopped a legacy draft rehydrating, but both of that issue's real halves were still live here and are now fixed in 511b8a70: a failed save came back with an empty code and could resubmit the stale server value, and pre-exclusion drafts still held a card number on disk.

🤖 Generated with Claude Code

Changes:
- Add @bwip-js/browser ^4.11.2 as a direct dependency

The digital cards rework renders loyalty barcodes and QR codes in the browser,
and nothing in the tree could generate them: barcode-detector only reads.
Takes a range rather than an exact pin, since it has no security or data blast
radius.
Changes:
- Add disscount.storeName package: entity, repository, DTO, normalizer, service, controller
- Expose GET /api/store-names returning visible suggestions ordered by usage

Free text store names users type on a card become suggestions for everyone else,
so the same shop does not accumulate under five spellings. The table carries no
user reference of any kind: the list is public, so attribution must not be
recoverable from it.

Notes:
- record() is REQUIRES_NEW deliberately. It is called from inside the card save
  transaction, and a concurrent insert colliding on the unique normalized_name
  would otherwise mark that transaction rollback only even when the exception is
  caught, failing the save. It swallows and logs instead, being best effort.
- hiddenAt rather than deletedAt: a hidden suggestion must keep occupying its
  normalized_name slot, or the next card save recreates it. The read filter ships
  now even though moderation does not, so adding it later cannot change what
  offline clients already see.
- usageCount is a monotonic submission count, never decremented, used only for
  ordering. A live card count would need decrements, orphan cleanup and a backfill.
- MIN_PUBLIC_USAGE is 1 so the group is not empty at launch. TODO to raise it to 2
  once there is volume, which is better for typo hygiene.
- Admin moderation (hide, restore, rename, merge) is a TODO on the entity, naming
  the columns and the requireAdmin-inside-the-service pattern it will need.
Changes:
- Replace the disscount.digitalCard package: entity, repository, DTOs, service, controller
- Add store, colour, three base64 images, pinning and updatedAt to the card
- Expose POST, GET /me, PUT, DELETE plus PATCH /{id}/pin and /{id}/unpin
- Record a custom store name as a suggestion on create, and on update only when it changed

The previous entity carried a title, a free text type and a code, which was too
thin to build a wallet on: no store link, no images, no pinning, no updatedAt.

Notes:
- cardType and codeType are plain String columns, never @Enumerated. ddl-auto=update
  leaves a stale CHECK constraint behind when a vocabulary changes, which then 500s
  on write.
- chainCode is a nullable opaque string beside a free text storeName, mirroring
  PinnedStore's storeApiId and storeName denormalisation, so a card stays readable
  with no join and a shop outside the official list still works.
- Pin is its own verb rather than a PUT, which is a full replace and would ship all
  three base64 images back just to set a timestamp.
- Every id addressed method resolves through findActiveByIdAndUser, so ownership
  cannot be forgotten. Deliberately unlike NotificationController, which omits it.
- Images are TEXT columns with @SiZe caps on the request DTO, following User.image.
  Three at cap is roughly a 2.8 MB body and no server side ceiling is configured,
  so the client pre-checks the encoded length before sending.
- Read verified only: Maven must not run here, so please compile before pushing.
Changes:
- Add the shared card and code vocabularies in constants/card-codes.ts
- Add zod schemas for the card request, form and DTO, plus store name suggestions
- Add lib/api/digital-cards and lib/api/store-names service triplets
- Register both services in the api barrel
- Persist the digitalCards and storeNames query roots for offline reads
- Register create, update, delete and pin as replayable offline mutations

Offline is the point of the feature: a shop with no signal is exactly where a
loyalty card gets used, so reads are cached and writes queue and replay.

Notes:
- codeType stores the Barcode Detection API's own format names (ean_13, qr_code),
  which is precisely what the scanner returns, so a scan needs no translation on
  the way in and bwip-js encoder names stay a rendering detail.
- The vocabulary adds "unknown" for a card whose number has no barcode, and it
  doubles as the graceful landing spot when a symbology rejects a value.
- digitalCardFormSchema omits the three images: they live outside react-hook-form
  like the settings avatar, since base64 would blow the localStorage draft quota.
- setDigitalCardPinned is one function over the two backend verbs, so the offline
  registry needs one key instead of two.
- storeNames is cached too, otherwise the form's autocomplete is empty offline.
Changes:
- Map stored code types onto bwip-js encoders and Croatian labels
- Add generateCodeSvg, returning a result object rather than throwing
- Add the card colour system: swatches, a locked hue scale, chain brand colours, hue sort
- Add the six sort comparators with a pinned split and a Croatian tie break
- Add dominant colour extraction from an uploaded image
- Add CardCode and CardIcon, the two shared render pieces

Colour is locked to one saturation and lightness pair, so every hue a user can
reach stays legible under white text and the wallet reads as one designed set.

Notes:
- bwip-js does NOT tree shake. BWIPP is one generated blob, so importing thirteen
  encoders costs the same as importing all hundred: 844 KB raw, 210 KB gzipped,
  verified by finding unimported symbologies in the output. Webpack scopes it to
  the digital-cards route chunk, so no other page pays for it, and generateCodeSvg
  stays synchronous because a lazy import would only move the same download behind
  a loading state on a page where every tile draws a code.
- Codabar's export is rationalizedCodabar, and bcid must be the bwip name rather
  than the scanner's format string. Both were wrong first time round.
- includetext is off and the value line is our own HTML, which keeps user input out
  of the SVG string the panel injects.
- CardIcon resolves the chain logo at render time instead of copying it into the
  card, so it never goes stale and costs nothing in the payload or the offline cache.
- Greys sort last: hueFromHex reports them as neutral rather than as red.
Changes:
- Register digital-card as a URL driven modal with new, edit and view actions
- Add the create and edit form: name, store autocomplete, type, code, colour, images, note
- Add scan to fill, which sets both the code and its detected type
- Add a live code preview that warns when the chosen symbology cannot encode the value
- Add three colour picker variants behind a temporary switcher
- Add the checkout modal: large high contrast code, card faces, pin, edit, delete
- Add useWakeLock, holding the screen on while a code is displayed

The two modals ship together because the entity outlet renders both, so splitting
them would leave it importing a module that does not exist yet.

Notes:
- View pushes to edit rather than swapping, so cancelling lands back on the card
  instead of the page, which is what closeModalUrl's history.back gives for free.
- The store field sets chainCode alongside storeName for an official chain and
  nulls it for free text, which is what gates the logo and the brand colour.
- Colour is suggested from the chain, then from an uploaded image, but only while
  the user has not touched the picker. A manual pick wins permanently.
- Images sit outside react-hook-form with their own dirty flag, folded into the
  shell's indicator. Drafts exclude the card code, so a number never reaches disk.
- Cards are read from the /me list rather than a by id endpoint, which is what makes
  a deep link work offline, and useCardImages adjusts state during render like
  use-lingering-target does, so an arriving card cannot flash the previous one's images.
- The picker switcher is deliberately temporary: two variants and the switcher get
  deleted once one is chosen.
Changes:
- Add the /digital-cards page and its client: auth gate, search, sort, empty state
- Add the card grid, two per row on mobile and three on desktop
- Add the card tile: a 1.5:1 plastic object that flips to its code on hover
- Add the pinned group, kept on top and separated by a divider
- Add the sort control over six modes, persisted per user
- Replace the old list row with the tile

The card is the feature, so it is drawn as a physical object rather than a table
row: a colour gradient with a specular band, a light catching top edge and a
raised icon medallion, which is what makes it scannable by eye in a wallet.

Notes:
- The tile is a div with a full bleed overlay button, not a wrapping button, so the
  pin toggle can sit above it without nesting interactive elements.
- Both controls live outside the flipping element, since a rotated button would be
  mirrored and unclickable, and the back face is aria-hidden because the code's real
  home is the detail modal that every input method can reach.
- motion-safe gates the transform itself, not just the transition, so reduced motion
  means no flip at all rather than an instant snap. Tailwind's hover variant already
  wraps in @media (hover: hover), so a tap never triggers it.
- codeValue is deliberately not searchable: the search term goes into ?q=, and a card
  number does not belong in a URL or in history.
- Sort persists as a bare string and is validated on read, following storeOptimizeMode.
- No list and grid toggle, unlike the old page. One visual grid is the whole point.
Changes:
- Drop comingSoon from the Kartice navigation item, unlocking it for everyone
- Enable the bottom nav long press, which opens a new card
- Uncomment the landing feature card's link
- Add the digital-cards shortcut glyph to the icon generator and generate both tiles
- State the PWA shortcut order explicitly instead of inheriting it from the nav
- Add an onboarding v2 TODO to nudge saving cards during onboarding

These are the reservations the earlier deletion left behind, now filled in rather
than recreated, which is why the bottom bar keeps its five cells and its symmetry.

Notes:
- Android surfaces only three shortcuts (Chrome keeps the fourth for Site settings),
  so the order decides what a phone shows. It is now Skeniraj, Kartice, Popisi, with
  Pracenje fourth and desktop only. Scanning leads as the fastest path from launcher
  to a price, and cards earn second place as a till errand rather than a browse.
  That resolves the pick-the-final-three question in #127.
- The order is stated in pwa-shortcuts.ts rather than inherited from navigation.ts,
  where the sequence is about the sidebar: a reshuffle there would otherwise drop a
  shortcut off phones silently.
- Both icon tiles are required and differ: the maskable one stays full bleed because
  Android fills transparent corners, and the any one keeps the brand corner radius.
@netlify

netlify Bot commented Jul 30, 2026

Copy link
Copy Markdown

Deploy Preview for disscount ready!

Name Link
🔨 Latest commit 7f68588
🔍 Latest deploy log https://app.netlify.com/projects/disscount/deploys/6a7604f2609869000853c20a
😎 Deploy Preview https://deploy-preview-142--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

Review Change Stack

Warning

Review limit reached

@OffCrazyFreak, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 57 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c6cec61e-775a-4fd6-8fa7-19fe8543a02d

📥 Commits

Reviewing files that changed from the base of the PR and between e695e4d and 7f68588.

⛔ Files ignored due to path filters (3)
  • frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • frontend/public/brand/shortcuts/digital-cards-any.png is excluded by !**/*.png
  • frontend/public/brand/shortcuts/digital-cards.png is excluded by !**/*.png
📒 Files selected for processing (99)
  • backend/src/main/java/disscount/digitalCard/dao/DigitalCardRepository.java
  • backend/src/main/java/disscount/digitalCard/domain/DigitalCard.java
  • backend/src/main/java/disscount/digitalCard/dto/DigitalCardDto.java
  • backend/src/main/java/disscount/digitalCard/dto/DigitalCardRequest.java
  • backend/src/main/java/disscount/digitalCard/rest/DigitalCardController.java
  • backend/src/main/java/disscount/digitalCard/service/DigitalCardService.java
  • backend/src/main/java/disscount/storeName/dao/StoreNameSuggestionRepository.java
  • backend/src/main/java/disscount/storeName/domain/StoreNameSuggestion.java
  • backend/src/main/java/disscount/storeName/dto/StoreNameSuggestionDto.java
  • backend/src/main/java/disscount/storeName/rest/StoreNameController.java
  • backend/src/main/java/disscount/storeName/service/StoreNameNormalizer.java
  • backend/src/main/java/disscount/storeName/service/StoreNameSuggestionService.java
  • docs/DIGITAL-CARDS.md
  • docs/LANDING.md
  • docs/MOBILE-NAV.md
  • docs/PWA.md
  • docs/README.md
  • docs/SEARCH.md
  • docs/STATE-PERSISTENCE.md
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/scripts/generate-shortcut-icons.mjs
  • frontend/src/app/(root)/data/features.ts
  • frontend/src/app/(user)/digital-cards/components/card-code.tsx
  • frontend/src/app/(user)/digital-cards/components/card-face-back.tsx
  • frontend/src/app/(user)/digital-cards/components/card-face-front.tsx
  • frontend/src/app/(user)/digital-cards/components/card-icon.tsx
  • frontend/src/app/(user)/digital-cards/components/create-digital-card-button.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-card-item.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-card-tile.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-cards-client.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-cards-empty.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-cards-grid.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-cards-sort-select.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/card-image-slot.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/card-images-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/card-name-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/card-type-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/code-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/code-preview.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/code-type-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/card-color-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/color-picker-honeycomb.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/color-picker-swatch-slider.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/color-picker-swatches.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/color-picker-switcher.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/hue-slider.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/digital-card-fields.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/digital-card-modal.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/note-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/store-name-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/store-name-option.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/store-name-trigger.tsx
  • frontend/src/app/(user)/digital-cards/components/pin-toggle-button.tsx
  • frontend/src/app/(user)/digital-cards/components/view/card-code-panel.tsx
  • frontend/src/app/(user)/digital-cards/components/view/card-detail-rows.tsx
  • frontend/src/app/(user)/digital-cards/components/view/card-face-images.tsx
  • frontend/src/app/(user)/digital-cards/components/view/card-view-actions.tsx
  • frontend/src/app/(user)/digital-cards/components/view/digital-card-view-modal.tsx
  • frontend/src/app/(user)/digital-cards/hooks/use-card-images.ts
  • frontend/src/app/(user)/digital-cards/hooks/use-digital-card-modal.ts
  • frontend/src/app/(user)/digital-cards/hooks/use-digital-card.ts
  • frontend/src/app/(user)/digital-cards/hooks/use-digital-cards-sort.ts
  • frontend/src/app/(user)/digital-cards/hooks/use-store-name-options.ts
  • frontend/src/app/(user)/digital-cards/page.tsx
  • frontend/src/app/(user)/digital-cards/utils/card-colors.ts
  • frontend/src/app/(user)/digital-cards/utils/card-labels.ts
  • frontend/src/app/(user)/digital-cards/utils/card-sorting.ts
  • frontend/src/app/(user)/digital-cards/utils/code-symbologies.ts
  • frontend/src/app/(user)/digital-cards/utils/generate-code-svg.ts
  • frontend/src/components/custom/bottom-nav/bottom-nav-items.ts
  • frontend/src/components/custom/modal-router/entity-modal-outlet.tsx
  • frontend/src/components/custom/settings/onboarding/onboarding-steps.ts
  • frontend/src/constants/card-codes.ts
  • frontend/src/constants/navigation.ts
  • frontend/src/constants/pwa-shortcuts.ts
  • frontend/src/hooks/use-wake-lock.ts
  • frontend/src/lib/api/digital-cards/hooks.ts
  • frontend/src/lib/api/digital-cards/index.ts
  • frontend/src/lib/api/digital-cards/keys.ts
  • frontend/src/lib/api/digital-cards/queries.ts
  • frontend/src/lib/api/index.ts
  • frontend/src/lib/api/schemas/digital-card.ts
  • frontend/src/lib/api/schemas/index.ts
  • frontend/src/lib/api/schemas/store-name.ts
  • frontend/src/lib/api/store-names/hooks.ts
  • frontend/src/lib/api/store-names/index.ts
  • frontend/src/lib/api/store-names/keys.ts
  • frontend/src/lib/api/store-names/queries.ts
  • frontend/src/lib/modal/modal-registry.ts
  • frontend/src/lib/modal/modal-retry-bus.ts
  • frontend/src/lib/offline/cached-query-keys.ts
  • frontend/src/lib/offline/card-write-failed.ts
  • frontend/src/lib/offline/offline-mutation-keys.ts
  • frontend/src/lib/offline/offline-mutations.ts
  • frontend/src/typings/local-storage.ts
  • frontend/src/utils/browser/extract-dominant-color.ts
  • frontend/src/utils/browser/local-storage.ts
  • frontend/src/utils/browser/storage/digital-cards.ts

Summary by CodeRabbit

  • New Features

    • Digital cards are now available with creation, editing, viewing, deletion, pinning, and offline support.
    • Added card details including store, card type, barcode/QR code, notes, colors, and front/back images.
    • Added barcode scanning, code previews, customizable colors, image uploads, and store-name suggestions.
    • Cards can be searched, sorted, grouped into pinned and other cards, and displayed with interactive card tiles.
    • Added a public store-name suggestions endpoint and improved card access protection.
  • Bug Fixes

    • Improved validation, update tracking, and barcode fallback messaging.

Walkthrough

The pull request expands digital cards into a richer wallet model with barcode and image support, pinning, sorting, store-name suggestions, dedicated viewing, color selection, and offline synchronization across the backend and frontend.

Changes

Digital card wallet

Layer / File(s) Summary
Backend card lifecycle and API
backend/src/main/java/disscount/digitalCard/...
Digital cards gain expanded metadata, timestamps, ownership-scoped lookups, validation, pinning endpoints, updated DTO mapping, and soft-delete handling.
Store-name suggestions
backend/src/main/java/disscount/storeName/...
Custom store names are normalized, counted, persisted, filtered by visibility and usage, and exposed through GET /api/store-names.
Frontend API and offline wiring
frontend/src/lib/api/..., frontend/src/lib/offline/..., frontend/src/lib/modal/...
Digital-card and store-name queries, mutations, schemas, modal routes, persisted sorting, shortcuts, and offline replay support are added or reorganized.
Card contracts and form controls
frontend/src/constants/card-codes.ts, frontend/src/app/(user)/digital-cards/utils/..., frontend/src/app/(user)/digital-cards/components/forms/...
New card, barcode, color, sorting, image, store-name, and form contracts support the expanded card workflow.
Creation and editing modal
frontend/src/app/(user)/digital-cards/components/forms/digital-card-modal.tsx, frontend/src/app/(user)/digital-cards/hooks/...
The modal uses the new form schema, manages image state and drafts, derives colors, and submits card fields together with image data.
Wallet and card viewing UI
frontend/src/app/(user)/digital-cards/components/...
The wallet displays grouped sortable card tiles with flip faces, pin controls, barcode previews, image views, delete/edit actions, and wake-lock behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Poem

A rabbit pinned a card with delight,
Then scanned its bright code in the night.
Store names hopped in,
New colors began,
And offline cards stayed tucked just right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely summarizes the primary change: rebuilding the digital-card loyalty wallet.
Description check ✅ Passed The description is directly related to the changeset and explains the wallet rebuild, features, implementation details, dependencies, and remaining merge requirements.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/main/java/disscount/digitalCard/service/DigitalCardService.java`:
- Around line 56-64: Replace the Collectors.toList() terminal operation with
Stream.toList() in getUserCards within
backend/src/main/java/disscount/digitalCard/service/DigitalCardService.java#L56-L64
and in listVisible within
backend/src/main/java/disscount/storeName/service/StoreNameSuggestionService.java#L66-L76,
preserving both pipelines’ existing mapping behavior and returning immutable
lists.

In
`@backend/src/main/java/disscount/storeName/service/StoreNameSuggestionService.java`:
- Around line 32-64: Update StoreNameSuggestionService.record to resolve
concurrent duplicates within its REQUIRES_NEW transaction before returning
control to createCard/updateCard. Reuse an existing suggestion for the same
normalized name, and handle duplicate insert failures inside record without
allowing the exception to propagate to the caller’s transaction; preserve the
best-effort card persistence behavior.

In `@frontend/src/app/`(user)/digital-cards/components/card-face-back.tsx:
- Around line 3-6: Remove the redundant `as CodeType` assertion when passing
`digitalCardDtoSchema.codeType` to the `CardCode` component in
`card-face-back.tsx`; use the inferred enum value directly and remove the
now-unused `CodeType` type import.

In `@frontend/src/app/`(user)/digital-cards/components/forms/card-image-slot.tsx:
- Around line 36-59: Update handleChange to clear event.currentTarget.value
immediately after capturing the selected file, before validation or asynchronous
processing, so the same file can be selected again after removal or failure.

In
`@frontend/src/app/`(user)/digital-cards/components/forms/color-picker/color-picker-switcher.tsx:
- Around line 18-21: Resolve the picker experiment by selecting one of the three
picker variants and using it directly. Remove the temporary picker switcher and
delete the two unused picker implementations, updating any imports or references
to point only to the chosen variant.

In
`@frontend/src/app/`(user)/digital-cards/components/forms/digital-card-modal.tsx:
- Around line 172-176: Update the onReset handler in the digital-card modal to
also restore colorCustomized to false, alongside clearDraft, resetImages, and
form.reset. Preserve the existing reset behavior so subsequent chain selections
and image color extraction can auto-suggest colors again.

In
`@frontend/src/app/`(user)/digital-cards/components/forms/store-name-trigger.tsx:
- Around line 16-67: Update StoreNameTrigger to use forwardRef, accepting the
inherited button props and ref injected by PopoverTrigger and FormControl.
Forward the ref and spread those props onto the inner Button while preserving
the component’s existing custom props, clear-button behavior, and visual
content.

In `@frontend/src/app/`(user)/digital-cards/components/pin-toggle-button.tsx:
- Around line 26-31: Update handleClick’s toast.error fallback to select the
message based on isPinned: use an unpinning message when the card is currently
pinned and the existing pinning message otherwise.

In `@frontend/src/app/`(user)/digital-cards/components/view/card-code-panel.tsx:
- Around line 21-33: Update CardCodePanel and CardCode so generateCodeSvg runs
only once per render: memoize or compute it in CardCodePanel, pass the result
through an optional result prop, and have CardCode reuse that value for both
validation and SVG rendering instead of encoding again.

In `@frontend/src/app/`(user)/digital-cards/components/view/card-detail-rows.tsx:
- Around line 13-26: Validate and narrow DigitalCardDto.cardType and codeType at
the API schema/DTO boundary using the existing card-domain unions, then remove
the unchecked casts in CardDetailRows and DigitalCardViewModal so
getCardTypeLabel and generateCodeSvg receive validated values; update
frontend/src/app/(user)/digital-cards/components/view/card-detail-rows.tsx#L13-L26
and
frontend/src/app/(user)/digital-cards/components/view/digital-card-view-modal.tsx#L69-L72
accordingly, with no direct changes needed beyond removing those call-site
casts.

In `@frontend/src/app/`(user)/digital-cards/hooks/use-store-name-options.ts:
- Around line 21-23: Change the sole exported useStoreNameOptions hook to a
default export, then update every consumer import to use the default-import form
while preserving the hook’s API and behavior.

In `@frontend/src/app/`(user)/digital-cards/utils/card-colors.ts:
- Around line 6-7: Update the card color generation and curated palette handling
around CARD_SATURATION, CARD_LIGHTNESS, and the applicable color-selection logic
so every rendered card background meets the required contrast threshold with its
foreground text. Either choose black or white based on computed contrast, or
constrain generated and brand colors to a threshold that guarantees the existing
white foreground remains readable; preserve the current palette behavior where
it already satisfies the requirement.

In `@frontend/src/utils/browser/extract-dominant-color.ts`:
- Around line 19-30: Update the image-processing flow around createImageBitmap,
drawImage, and getImageData so bitmap.close() executes on every exit path,
including drawing or pixel-extraction errors and a missing canvas context. Use a
finally block tied to the bitmap lifetime, while preserving the existing
null-return behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0c2b32cf-3bb7-4cef-b593-927813bfe5f3

📥 Commits

Reviewing files that changed from the base of the PR and between a52899a and 5eaac33.

⛔ Files ignored due to path filters (3)
  • frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • frontend/public/brand/shortcuts/digital-cards-any.png is excluded by !**/*.png
  • frontend/public/brand/shortcuts/digital-cards.png is excluded by !**/*.png
📒 Files selected for processing (87)
  • backend/src/main/java/disscount/digitalCard/dao/DigitalCardRepository.java
  • backend/src/main/java/disscount/digitalCard/domain/DigitalCard.java
  • backend/src/main/java/disscount/digitalCard/dto/DigitalCardDto.java
  • backend/src/main/java/disscount/digitalCard/dto/DigitalCardRequest.java
  • backend/src/main/java/disscount/digitalCard/rest/DigitalCardController.java
  • backend/src/main/java/disscount/digitalCard/service/DigitalCardService.java
  • backend/src/main/java/disscount/storeName/dao/StoreNameSuggestionRepository.java
  • backend/src/main/java/disscount/storeName/domain/StoreNameSuggestion.java
  • backend/src/main/java/disscount/storeName/dto/StoreNameSuggestionDto.java
  • backend/src/main/java/disscount/storeName/rest/StoreNameController.java
  • backend/src/main/java/disscount/storeName/service/StoreNameNormalizer.java
  • backend/src/main/java/disscount/storeName/service/StoreNameSuggestionService.java
  • frontend/package.json
  • frontend/scripts/generate-shortcut-icons.mjs
  • frontend/src/app/(root)/data/features.ts
  • frontend/src/app/(user)/digital-cards/components/card-code.tsx
  • frontend/src/app/(user)/digital-cards/components/card-face-back.tsx
  • frontend/src/app/(user)/digital-cards/components/card-face-front.tsx
  • frontend/src/app/(user)/digital-cards/components/card-icon.tsx
  • frontend/src/app/(user)/digital-cards/components/create-digital-card-button.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-card-item.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-card-tile.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-cards-client.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-cards-empty.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-cards-grid.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-cards-sort-select.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/card-image-slot.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/card-images-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/card-name-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/card-type-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/code-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/code-preview.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/code-type-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/card-color-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/color-picker-honeycomb.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/color-picker-swatch-slider.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/color-picker-swatches.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/color-picker-switcher.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/color-picker/hue-slider.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/digital-card-fields.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/digital-card-modal.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/note-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/store-name-field.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/store-name-option.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/store-name-trigger.tsx
  • frontend/src/app/(user)/digital-cards/components/pin-toggle-button.tsx
  • frontend/src/app/(user)/digital-cards/components/view/card-code-panel.tsx
  • frontend/src/app/(user)/digital-cards/components/view/card-detail-rows.tsx
  • frontend/src/app/(user)/digital-cards/components/view/card-face-images.tsx
  • frontend/src/app/(user)/digital-cards/components/view/card-view-actions.tsx
  • frontend/src/app/(user)/digital-cards/components/view/digital-card-view-modal.tsx
  • frontend/src/app/(user)/digital-cards/hooks/use-card-images.ts
  • frontend/src/app/(user)/digital-cards/hooks/use-digital-card-modal.ts
  • frontend/src/app/(user)/digital-cards/hooks/use-digital-card.ts
  • frontend/src/app/(user)/digital-cards/hooks/use-digital-cards-sort.ts
  • frontend/src/app/(user)/digital-cards/hooks/use-store-name-options.ts
  • frontend/src/app/(user)/digital-cards/page.tsx
  • frontend/src/app/(user)/digital-cards/utils/card-colors.ts
  • frontend/src/app/(user)/digital-cards/utils/card-labels.ts
  • frontend/src/app/(user)/digital-cards/utils/card-sorting.ts
  • frontend/src/app/(user)/digital-cards/utils/code-symbologies.ts
  • frontend/src/app/(user)/digital-cards/utils/generate-code-svg.ts
  • frontend/src/components/custom/bottom-nav/bottom-nav-items.ts
  • frontend/src/components/custom/modal-router/entity-modal-outlet.tsx
  • frontend/src/components/custom/settings/onboarding/onboarding-steps.ts
  • frontend/src/constants/card-codes.ts
  • frontend/src/constants/navigation.ts
  • frontend/src/constants/pwa-shortcuts.ts
  • frontend/src/hooks/use-wake-lock.ts
  • frontend/src/lib/api/digital-cards/hooks.ts
  • frontend/src/lib/api/digital-cards/index.ts
  • frontend/src/lib/api/digital-cards/queries.ts
  • frontend/src/lib/api/index.ts
  • frontend/src/lib/api/schemas/digital-card.ts
  • frontend/src/lib/api/schemas/index.ts
  • frontend/src/lib/api/schemas/store-name.ts
  • frontend/src/lib/api/store-names/hooks.ts
  • frontend/src/lib/api/store-names/index.ts
  • frontend/src/lib/api/store-names/queries.ts
  • frontend/src/lib/modal/modal-registry.ts
  • frontend/src/lib/offline/cached-query-keys.ts
  • frontend/src/lib/offline/offline-mutation-keys.ts
  • frontend/src/lib/offline/offline-mutations.ts
  • frontend/src/typings/local-storage.ts
  • frontend/src/utils/browser/extract-dominant-color.ts
  • frontend/src/utils/browser/local-storage.ts
  • frontend/src/utils/browser/storage/digital-cards.ts
💤 Files with no reviewable changes (3)
  • frontend/src/app/(user)/digital-cards/components/forms/digital-card-fields.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-card-item.tsx
  • frontend/src/constants/navigation.ts

Comment thread frontend/src/app/(user)/digital-cards/components/card-face-back.tsx
Comment on lines +18 to +21
/**
* TODO(digital-cards): temporary. Three pickers ship side by side so the final one can be
* chosen from real use; delete this switcher and the two losing variants once it is picked.
*/

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.

📐 Maintainability & Code Quality | 🔵 Trivial

Resolve the temporary picker experiment before merging.

The PR objective requires choosing one variant. Keep the selected picker and remove this switcher plus the two unused implementations. I can prepare that cleanup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@frontend/src/app/`(user)/digital-cards/components/forms/color-picker/color-picker-switcher.tsx
around lines 18 - 21, Resolve the picker experiment by selecting one of the
three picker variants and using it directly. Remove the temporary picker
switcher and delete the two unused picker implementations, updating any imports
or references to point only to the chosen variant.

Comment on lines +21 to +33
export default function CardCodePanel({
codeValue,
codeType,
}: ICardCodePanelProps) {
const result = generateCodeSvg(codeValue, codeType);
const twoDimensional = isTwoDimensional(codeType);

return (
<div className="space-y-2">
<div className="rounded-lg bg-white px-4 py-5 shadow-inner ring-1 ring-black/10">
<div className={twoDimensional ? "mx-auto max-w-56" : "w-full"}>
<CardCode codeValue={codeValue} codeType={codeType} showValue />
</div>

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.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Barcode/QR is encoded twice per render.

generateCodeSvg is called here (unmemoized) and again inside CardCode for the same codeValue/codeType. Consider computing once and passing the result down, or memoizing here with useMemo, so both the “ok” check and the rendered SVG come from a single encode pass.

♻️ Proposed direction
-export default function CardCodePanel({
-  codeValue,
-  codeType,
-}: ICardCodePanelProps) {
-  const result = generateCodeSvg(codeValue, codeType);
+export default function CardCodePanel({
+  codeValue,
+  codeType,
+}: ICardCodePanelProps) {
+  const result = useMemo(
+    () => generateCodeSvg(codeValue, codeType),
+    [codeValue, codeType],
+  );
   const twoDimensional = isTwoDimensional(codeType);

CardCode would then need an optional result prop to accept the precomputed value instead of recomputing internally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/`(user)/digital-cards/components/view/card-code-panel.tsx
around lines 21 - 33, Update CardCodePanel and CardCode so generateCodeSvg runs
only once per render: memoize or compute it in CardCodePanel, pass the result
through an optional result prop, and have CardCode reuse that value for both
validation and SVG rendering instead of encoding again.

Comment thread frontend/src/app/(user)/digital-cards/hooks/use-store-name-options.ts Outdated
Comment thread frontend/src/app/(user)/digital-cards/utils/card-colors.ts
Comment thread frontend/src/utils/browser/extract-dominant-color.ts Outdated
@OffCrazyFreak

Copy link
Copy Markdown
Owner Author

Cross-linking from #144 (app-wide skeleton loading system + data layer unification) so this does not get lost.

#144 deliberately skipped digital-cards entirely, because this PR rebuilds the feature and converting the old code would have just created conflicts. It is now the one domain still on the pre-#144 conventions.

Merge order: #143, then #144, then this one. Once #144 is in, the rework needs these applied:

Data layer

  • Split lib/api/digital-cards/index.ts into keys.ts / queries.ts / hooks.ts, matching every other domain.
  • Add DIGITAL_CARD_QUERY_KEYS and replace the inline ["digitalCards", "me"] / ["digitalCards"] literals in use-digital-card-modal.ts and digital-card-item.tsx.
  • Expose reads as digitalCardQueries queryOptions() descriptors, not useGetUserDigitalCards.

Loading UI

  • digital-cards-client.tsx moves onto useAuthedQuery (drops the enabled: isAuthenticated gate and the userLoading || isLoading merge) and AsyncSection.
  • Use requiresAuth for the LoginRequired gate instead of !userLoading && !isAuthenticated.
  • Add digital-card-item-skeleton.tsx, digital-cards-skeleton.tsx and (user)/digital-cards/loading.tsx.
  • Replace the bare <Skeleton className="h-64 w-full" /> in digital-card-modal.tsx with a mirror of the real content.
  • Remove the BlockLoadingSpinner from the page body; it is now for buttons and short inline actions only.

Offline

  • digitalCards is already allowlisted in lib/offline/cached-query-keys.ts. Keep that key root spelled exactly as-is, or the data silently stops persisting.

⚠️ Do not branch on a query's isLoading anywhere in the new code. Under PersistQueryClientProvider it reads false with no data while the IndexedDB cache restores, which is the bug #144 exists to fix. Use useAuthedQuery's pending, or useDataPending(...).

Full reference in docs/DATA-FETCHING.md, short version in AGENTS.md.

🤖 Generated with Claude Code

@OffCrazyFreak

Copy link
Copy Markdown
Owner Author

Heads-up: PR #145 must merge before this branch. It adds the public donate modal target and overlaps frontend/src/constants/navigation.ts and frontend/src/lib/modal/modal-registry.ts.

After rebasing, please verify that:

  • both digital-card and donate remain valid ModalTarget values and parse correctly;
  • ModalRouter still mounts the donation modal alongside the digital-card outlet;
  • the final support navigation item stays at the end of Pomoć i podrška.

Please resolve any conflicts in a way that preserves these behaviours and the project accessibility conventions.

# Conflicts:
#	frontend/src/app/(user)/digital-cards/components/digital-card-item.tsx
…s a retry

Changes:
- Add modal-retry-bus, an in-memory sibling of modal-error-bus with the same TTL
- Stash the submitted code when an optimistic save fails, and restore it on reopen
- Drop any draft still holding a card code under the old or current field name

Two halves of one rule: never on disk, fine in memory until the submit settles.

A failed save reopened the modal with the code field empty, because the draft
deliberately excludes it, and the edit path then re-seeded the code from the server
record, so a resubmit could silently write the old number back. Separately, drafts
written by earlier builds still hold a card number under the old "value" key, and
the manual draft merge reads localStorage directly, bypassing the exclusion.

Closes #54.

Notes:
- The whole draft key is deleted rather than sanitised field by field, since its shape
  comes from a build we no longer have.
- The stash is memory only, so it dies with the tab and is dropped the moment the
  retry reads it, which keeps the code off disk in every path.
@OffCrazyFreak

Copy link
Copy Markdown
Owner Author

Rebased onto current dev (merge 432db56c), so this is even with it and now MERGEABLE. #145 was already in, so the donate overlap resolved itself in the merge. Verified after merging, per the checklist on that note:

  • digital-card and donate are both valid ModalTarget values and both parse: donate in the flat name case, digital-card in its own case with new / edit / view.
  • ModalRouter mounts DonationModal alongside EntityModalOutlet, unchanged.
  • Podrži Disscount remains the last item in supportNavItems.
  • One conflict, resolved by keeping the deletion: digital-card-item.tsx is the old list row, replaced by digital-card-tile.tsx plus the two card faces.

tsc --noEmit clean, ESLint 0 errors, Prettier clean after the merge.

I have folded the #144 follow-up list into the PR description as an unchecked checklist so it survives context loss, and flagged the merge order at the top.

#54 is fixed here rather than deferred (511b8a70). Reading it closely, my rebuild had inherited both halves: the field rename from value to codeValue blocked the legacy rehydration path, but a failed save still reopened the modal with an empty code and could silently resubmit the stale server value, and pre-exclusion drafts still held a card number in localStorage. The fix follows the rule that issue sets out, never on disk but fine in memory until the submit settles: a new in-memory modal-retry-bus mirroring modal-error-bus, plus deleting any draft key still carrying a code.

#127 should stay open. This covers Digitalne kartice (icons generated, explicit order: Skeniraj, Kartice, Popisi), but Karta still needs a deliberate decision and a MapIcon entry in the generator, and the acceptance criterion about reinstalling the PWA on a real device is unverified.

🤖 Generated with Claude Code

# Conflicts:
#	backend/src/main/java/disscount/digitalCard/domain/DigitalCard.java
#	frontend/src/app/(user)/digital-cards/components/digital-card-item.tsx
#	frontend/src/lib/modal/modal-registry.ts
Changes:
- Stamp every digital card and store name suggestion timestamp through Timestamps.nowUtc()
- Key the card form and view modals by card id in the entity outlet

Both follow conventions that landed on dev while this branch was open.

The UTC rule is the one AGENTS.md now states: the columns carry no offset, so a
bare LocalDateTime.now() lets the container's zone leak into the wire format, and
the frontend reads these back as UTC. The merge applied it to deleteCard but left
setPinned and the whole storeName package behind.

The keying is the same fix add-to-list carries. The outlet stays mounted between
openings, and this form's draft merge bails out while the form is dirty, so
editing one card, closing, then opening another would have shown the first card's
edits under the second card's title.
Changes:
- Clear the file input after reading it, so the same file can be picked again
- Restore the colour suggestion rule when the form is reset
- Name the failing action correctly when unpinning
- Take the card type as the wire's plain string, falling back instead of rendering undefined
- Encode the checkout code once per render, and use Stream.toList on the backend

Picking a photo, removing it, then picking the same file again did nothing,
because the input still held the old value and fired no change event. Resetting a
new card left colourCustomized true, so chain and image suggestions stayed off for
the rest of the session.
# Conflicts:
#	frontend/src/app/(user)/digital-cards/components/digital-cards-client.tsx
#	frontend/src/components/custom/modal-router/entity-modal-outlet.tsx
#	frontend/src/lib/modal/modal-registry.ts
#	frontend/src/lib/offline/offline-mutation-keys.ts
#	frontend/src/lib/offline/offline-mutations.ts
…tions

Changes:
- Choose card text colour per card by luminance, and darken the hues no ink could carry
- Route every card query key through a keys.ts registry, matching the other domains
- Report a replayed card write that fails, which previously reverted in silence
- Sample the compressed image for the colour suggestion instead of decoding the original again
- Move to useWatch, shared loading labels, a copy button for the card number and theme tokens
- Add the noindex header for /digital-cards

The contrast claim in card-colors.ts was simply wrong: locking saturation and
lightness does not make white text legible, because yellow and green are far
brighter than blue at the same lightness. Hue 55 sat near 2.7:1 against a 4.5:1
minimum, and several chain brand colours were no better. Ink is now chosen per
card, which covers the freeform slider and the brand map together, and a band
around cyan where neither ink reached 4.5 is darkened until one does. Worst case
across all 360 hues is now 4.52.

Notes:
- foregroundFor and the contrast helpers sit above hexForHue on purpose: CARD_SWATCHES
  calls it at module scope, so a const declared later would be read in its dead zone.
- The four literal bg-white surfaces now say why they are literal. A scanner reads
  contrast, so a dark-mode barcode panel would not scan.
- convertToDto stays private, with a note. The mapper split on shopping lists exists to
  stop an owner view and a shared view drifting, and a card has one viewer.
- mvn -B verify passes. It proves compilation and packaging only: there is still no
  backend test suite.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

…invalidated

Changes:
- Add docs/DIGITAL-CARDS.md and index it in docs/README.md
- Correct the digital-cards lines in STATE-PERSISTENCE, PWA, MOBILE-NAV, SEARCH and LANDING

The new doc covers the data model, the code vocabulary and why it matches the
scanner's, the store autocomplete and its community half, the contrast rule, the
image pipeline, and the four-step offline write registration.

Corrections, all of them things the rework made untrue:
- The card code is excluded from drafts as `codeValue`, not `value`, and now
  survives a failed save through modal-retry-bus rather than being lost.
- digital-cards-client no longer consumes useViewMode, so products-client is its
  only remaining consumer.
- The PWA wake-lock TODO is done, `storeNames` joined the persistence whitelist,
  and the shortcut top three are now decided.
- The Kartice long press is live and the cell is no longer admin-only.
- The card search filters cardName, storeName and note.
- Digitalne kartice left the landing page's coming-soon list.

Checked and left alone: AUTH, BRAND, DEPLOYMENT, SHARING, SUPPORT, none of which
mention digital cards.
# Conflicts:
#	docs/PWA.md
#	frontend/next.config.ts
#	frontend/src/lib/api/schemas/digital-card.ts
#	frontend/src/lib/offline/offline-mutation-keys.ts
#	frontend/src/lib/offline/offline-mutations.ts
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Changes:
- Move the card write handlers into lib/offline/card-write-failed.ts, mirroring the list pair
- Report a failed write from the hook as well as the replay default, and drop the duplicate call-site toasts
- Silence a replayed delete that 404s, since the card being gone is the desired end state
- Strip only the code field from a legacy draft instead of the whole key
- Match the imperative validation copy the zod schemas moved to

Registering a handler only as a replay default was half a fix. A hook-level
onError replaces the default rather than running beside it, so a live failure
reported through whichever of the two happened to be attached, while the call
sites toasted a second time on top. The handler now sits in both places, and the
call sites only catch to keep the rejection from going unhandled.

Notes:
- Dropping the whole draft key took the user's half-typed name and note with the
  card number. removeFormDraftField, new on dev, removes just the sensitive field
  and carries the original savedAt so the rest keeps its own 24h clock.
- The backend validation copy is deliberately not changed: the imperative sweep was
  frontend-only, and UserRequest still reads "mora imati između", so the DTOs match
  their own side.
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.

1 participant