feat(digital-cards): Rebuild the loyalty card wallet - #142
feat(digital-cards): Rebuild the loyalty card wallet#142OffCrazyFreak wants to merge 19 commits into
Conversation
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.
✅ Deploy Preview for disscount ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (3)
📒 Files selected for processing (99)
Summary by CodeRabbit
WalkthroughThe 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. ChangesDigital card wallet
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (3)
frontend/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlfrontend/public/brand/shortcuts/digital-cards-any.pngis excluded by!**/*.pngfrontend/public/brand/shortcuts/digital-cards.pngis excluded by!**/*.png
📒 Files selected for processing (87)
backend/src/main/java/disscount/digitalCard/dao/DigitalCardRepository.javabackend/src/main/java/disscount/digitalCard/domain/DigitalCard.javabackend/src/main/java/disscount/digitalCard/dto/DigitalCardDto.javabackend/src/main/java/disscount/digitalCard/dto/DigitalCardRequest.javabackend/src/main/java/disscount/digitalCard/rest/DigitalCardController.javabackend/src/main/java/disscount/digitalCard/service/DigitalCardService.javabackend/src/main/java/disscount/storeName/dao/StoreNameSuggestionRepository.javabackend/src/main/java/disscount/storeName/domain/StoreNameSuggestion.javabackend/src/main/java/disscount/storeName/dto/StoreNameSuggestionDto.javabackend/src/main/java/disscount/storeName/rest/StoreNameController.javabackend/src/main/java/disscount/storeName/service/StoreNameNormalizer.javabackend/src/main/java/disscount/storeName/service/StoreNameSuggestionService.javafrontend/package.jsonfrontend/scripts/generate-shortcut-icons.mjsfrontend/src/app/(root)/data/features.tsfrontend/src/app/(user)/digital-cards/components/card-code.tsxfrontend/src/app/(user)/digital-cards/components/card-face-back.tsxfrontend/src/app/(user)/digital-cards/components/card-face-front.tsxfrontend/src/app/(user)/digital-cards/components/card-icon.tsxfrontend/src/app/(user)/digital-cards/components/create-digital-card-button.tsxfrontend/src/app/(user)/digital-cards/components/digital-card-item.tsxfrontend/src/app/(user)/digital-cards/components/digital-card-tile.tsxfrontend/src/app/(user)/digital-cards/components/digital-cards-client.tsxfrontend/src/app/(user)/digital-cards/components/digital-cards-empty.tsxfrontend/src/app/(user)/digital-cards/components/digital-cards-grid.tsxfrontend/src/app/(user)/digital-cards/components/digital-cards-sort-select.tsxfrontend/src/app/(user)/digital-cards/components/forms/card-image-slot.tsxfrontend/src/app/(user)/digital-cards/components/forms/card-images-field.tsxfrontend/src/app/(user)/digital-cards/components/forms/card-name-field.tsxfrontend/src/app/(user)/digital-cards/components/forms/card-type-field.tsxfrontend/src/app/(user)/digital-cards/components/forms/code-field.tsxfrontend/src/app/(user)/digital-cards/components/forms/code-preview.tsxfrontend/src/app/(user)/digital-cards/components/forms/code-type-field.tsxfrontend/src/app/(user)/digital-cards/components/forms/color-picker/card-color-field.tsxfrontend/src/app/(user)/digital-cards/components/forms/color-picker/color-picker-honeycomb.tsxfrontend/src/app/(user)/digital-cards/components/forms/color-picker/color-picker-swatch-slider.tsxfrontend/src/app/(user)/digital-cards/components/forms/color-picker/color-picker-swatches.tsxfrontend/src/app/(user)/digital-cards/components/forms/color-picker/color-picker-switcher.tsxfrontend/src/app/(user)/digital-cards/components/forms/color-picker/hue-slider.tsxfrontend/src/app/(user)/digital-cards/components/forms/digital-card-fields.tsxfrontend/src/app/(user)/digital-cards/components/forms/digital-card-modal.tsxfrontend/src/app/(user)/digital-cards/components/forms/note-field.tsxfrontend/src/app/(user)/digital-cards/components/forms/store-name-field.tsxfrontend/src/app/(user)/digital-cards/components/forms/store-name-option.tsxfrontend/src/app/(user)/digital-cards/components/forms/store-name-trigger.tsxfrontend/src/app/(user)/digital-cards/components/pin-toggle-button.tsxfrontend/src/app/(user)/digital-cards/components/view/card-code-panel.tsxfrontend/src/app/(user)/digital-cards/components/view/card-detail-rows.tsxfrontend/src/app/(user)/digital-cards/components/view/card-face-images.tsxfrontend/src/app/(user)/digital-cards/components/view/card-view-actions.tsxfrontend/src/app/(user)/digital-cards/components/view/digital-card-view-modal.tsxfrontend/src/app/(user)/digital-cards/hooks/use-card-images.tsfrontend/src/app/(user)/digital-cards/hooks/use-digital-card-modal.tsfrontend/src/app/(user)/digital-cards/hooks/use-digital-card.tsfrontend/src/app/(user)/digital-cards/hooks/use-digital-cards-sort.tsfrontend/src/app/(user)/digital-cards/hooks/use-store-name-options.tsfrontend/src/app/(user)/digital-cards/page.tsxfrontend/src/app/(user)/digital-cards/utils/card-colors.tsfrontend/src/app/(user)/digital-cards/utils/card-labels.tsfrontend/src/app/(user)/digital-cards/utils/card-sorting.tsfrontend/src/app/(user)/digital-cards/utils/code-symbologies.tsfrontend/src/app/(user)/digital-cards/utils/generate-code-svg.tsfrontend/src/components/custom/bottom-nav/bottom-nav-items.tsfrontend/src/components/custom/modal-router/entity-modal-outlet.tsxfrontend/src/components/custom/settings/onboarding/onboarding-steps.tsfrontend/src/constants/card-codes.tsfrontend/src/constants/navigation.tsfrontend/src/constants/pwa-shortcuts.tsfrontend/src/hooks/use-wake-lock.tsfrontend/src/lib/api/digital-cards/hooks.tsfrontend/src/lib/api/digital-cards/index.tsfrontend/src/lib/api/digital-cards/queries.tsfrontend/src/lib/api/index.tsfrontend/src/lib/api/schemas/digital-card.tsfrontend/src/lib/api/schemas/index.tsfrontend/src/lib/api/schemas/store-name.tsfrontend/src/lib/api/store-names/hooks.tsfrontend/src/lib/api/store-names/index.tsfrontend/src/lib/api/store-names/queries.tsfrontend/src/lib/modal/modal-registry.tsfrontend/src/lib/offline/cached-query-keys.tsfrontend/src/lib/offline/offline-mutation-keys.tsfrontend/src/lib/offline/offline-mutations.tsfrontend/src/typings/local-storage.tsfrontend/src/utils/browser/extract-dominant-color.tsfrontend/src/utils/browser/local-storage.tsfrontend/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
| /** | ||
| * 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. | ||
| */ |
There was a problem hiding this comment.
📐 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.
| 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> |
There was a problem hiding this comment.
🚀 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.
|
Cross-linking from #144 (app-wide skeleton loading system + data layer unification) so this does not get lost. #144 deliberately skipped Merge order: #143, then #144, then this one. Once #144 is in, the rework needs these applied: Data layer
Loading UI
Offline
Full reference in 🤖 Generated with Claude Code |
|
Heads-up: PR #145 must merge before this branch. It adds the public After rebasing, please verify that:
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.
|
Rebased onto current
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 ( #127 should stay open. This covers Digitalne kartice (icons generated, explicit order: Skeniraj, Kartice, Popisi), but Karta still needs a deliberate decision and a 🤖 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.
|
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
|
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.
Important
Merge order: #143, then #144, then this PR. Both are still open.
devis merged in as of432db56c, 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
lib/api/digital-cards/index.tsintokeys.ts/queries.ts/hooks.ts, matching every other domain.DIGITAL_CARD_QUERY_KEYSand replace the inline["digitalCards", "me"]/["digitalCards"]literals.digitalCardQueriesqueryOptions()descriptors instead ofuseGetCurrentUserDigitalCards.Loading UI
digital-cards-client.tsxontouseAuthedQuery(drops theenabled: isAuthenticatedgate and theuserLoading || isLoadingmerge) andAsyncSection.requiresAuthfor theLoginRequiredgate instead of!userLoading && !isAuthenticated.digital-card-item-skeleton.tsx,digital-cards-skeleton.tsxand(user)/digital-cards/loading.tsx.<Skeleton className="h-96 w-full" />indigital-card-modal.tsxwith a mirror of the real content.BlockLoadingSpinnerfrom the page body; buttons and short inline actions only.Offline
digitalCardskey root incached-query-keys.tsspelled exactly as-is, or persistence silently stops.Do not branch on a query's
isLoadingin new code: underPersistQueryClientProviderit reads false with no data while the IndexedDB cache restores. UseuseAuthedQuery'spendingoruseDataPending(...). 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
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
DROP TABLE public.digital_card;on production. It still holds the old table with real rows.ddl-auto=updatenever drops or renames, so otherwise the new schema merges into stale columns and the newNOT NULLcolumns fail against existing rows. The local dev table is already dropped.dev:digital-card-item.tsxwas deleted here and modified on dev. Keep the deletion, it is the old list row replaced bydigital-card-tile.tsx. The other two overlapping files auto-merge.Verification done
tsc --noEmitand Prettier pass. ESLint reports 0 errors; the single new warning mirrorsuse-view-mode.ts, which hydrates from localStorage the same way for the same SSR reason.pnpm buildcompiled 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
/digital-cardsroute 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()isREQUIRES_NEWdeliberately. It runs inside the card save transaction, and a concurrent duplicate on the uniquenormalized_namewould otherwise mark that transaction rollback only and fail the save.?q=URL or in history.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
MapIconentry, and the device reinstall check is unrun. Leave #127 open.Closes #54. The rename from
valuetocodeValuestopped a legacy draft rehydrating, but both of that issue's real halves were still live here and are now fixed in511b8a70: 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