fcee4d0faae976e2703bd54b066f6bc835646060
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
03f08074d1 |
feat(frontend): preview an inventory item as a customer sees it (#89)
Clicking an item's name in the admin Inventory opens a drawer rendering the real storefront ItemCard for it, so the way a listing will look can be checked without publishing it and going to see. The name cell is a link-styled button rather than a clickable cell, so it stays reachable by keyboard and announces itself as an action. Admin already imports Item from ../api, the same type the storefront uses, so the row object goes straight into the card with no adapter and nothing to drift. The part that needed care is that ItemCard is not a passive component. It wires into the cart and favorites contexts and has working buttons, and both providers wrap the whole app — so a naive preview would have been fully functional, and an admin browsing inventory could have added their own stock to their own cart. On a one-of-a-kind catalogue that reserves the item and takes it off sale. ItemCard therefore takes an optional preview prop that short-circuits its two click handlers. Those two are the only entry points, so guarding them also covers the shared auth modal and the favorite-alerts consent prompt hanging off them. Deliberately not `disabled` on the buttons. A disabled antd button renders in a different colour with a different cursor and no hover, and the whole point of this panel is to show what a customer will actually see. The controls keep their normal appearance and their correct state for the item's status; only the handlers stop. The comment on the prop says so, because "simplifying" this to a disabled button would quietly defeat the feature while appearing to implement it. Four end-to-end tests, two of which are the ones worth having. Clicking Add to Cart in the preview must do nothing — asserted by the sign-in prompt never appearing, which a real click on a signed-out card always raises, so its absence proves the handler stopped before doing any work. And the storefront card must still be live where it is actually used, or this change would have quietly broken buying things. ItemCard's props are now Readonly, which was an existing lint warning on a file this change already touches: frontend warnings drop from 31 to 30. Verified: build clean, lint 0 errors, 91 end-to-end tests passing against a freshly created database. Refs #89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c058b3ed2e |
feat(ci): add ESLint to both workspaces with a defect-only CI gate (#60)
TypeScript's strict mode checks types and nothing else, so nothing enforced the React hook rules, the SonarJS rules, or unhandled-promise detection. Adds a flat config per workspace, a lint script in each, and a lint job in tests.yml. The rule selection is the substance of this change and is measured rather than guessed. A full-strength config reports 435 violations across 50 files, but 325 of those are the no-unsafe-* family from recommendedTypeChecked, every one downstream of pool.query() returning any rows and untyped fetch responses. Typing those boundaries is the whole of #65, so enabling the rules here would ship a linter whose output is three-quarters another issue's backlog — the reliable way to teach everyone to ignore lint output. This enables recommended plus the two type-aware rules that catch defects rather than describe type debt, which leaves 110 findings. Both configs downgrade every preset to a warning and then list the error rules explicitly at the bottom, so the CI gate is readable in one place instead of inferred from four presets' defaults. Errors are no-floating-promises, no-misused-promises, rules-of-hooks, exhaustive-deps and jsx-a11y/alt-text; everything else warns. No --max-warnings flag is needed because ESLint already exits non-zero on errors and zero on warnings. no-misused-promises runs with checksVoidReturn.attributes false, since onClick={async () => ...} is idiomatic React and safe when the handler catches its own errors — at the default it flags every antd button in the admin screens, 25 of its 28 hits, and a rule that is 89% noise gets switched off within a week. The 37 errors this surfaced were mostly not the mechanical fix they looked like. The plan assumed the 30 floating promises were fire-and-forget loaders that already handled their own failures, which was true of the one sampled when writing the design and false for most of the rest: Admin, Categories, Customers, Tags, Settings, Account and CustomerAuthContext all had no rejection handling at all, so `void` on them would have hidden real failures rather than annotated deliberate ones. Each of those loaders now catches and surfaces the failure before the call site voids it. The CustomerAuthContext one was a live bug — a rejected fetchMe left loading true forever, rendering as a permanent spinner instead of a signed-out page. Admin's load became a useCallback so its effect can name it honestly rather than suppress the dependency, Categories' drop handler was split so the function antd receives returns void as its type says, and Cart's effect now names refreshCartContext, which is a useCallback with an empty dependency list and so cannot re-run it. The only disable added is in asyncRoute, where returning a promise where Express expects void is the entire point of the wrapper and the promise cannot reject. Two of the issue's premises did not survive measurement, both recorded in the spec: exhaustive-deps flags 2 cases rather than the 10 inferred from empty dependency arrays, and the backend was already clean on the defect rules because #59 wrapped every async route. Verified: lint, build, 78 unit, 134 integration and 83 e2e all pass in both workspaces, and the CI gate was confirmed to fail by introducing a deliberate violation rather than by assuming the job is wired correctly. Closes #60 |
||
|
|
f626f27e75 |
feat: favorite items and notify when a favorite is sold (#34)
Customers can favorite and unfavorite items from the storefront, opt in to being told when a favorite is sold to someone else, and manage that preference from their account page. The opt-in is a consent of its own rather than the existing marketing flag. Being told that a specific item you asked about has gone is a narrower thing than agreeing to marketing, and folding one into the other would leave marketing_consent_text no longer describing what was actually agreed to. It is recorded the same way as the marketing consent — flag, timestamp, and the exact wording shown — and accepting it does not set marketing_consent. The prompt appears only after a customer has actually favorited something, so the reason for asking is concrete rather than an abstract marketing ask, and it says plainly that it is separate from marketing email. Declining keeps the favorite. Notifications fire when an item reaches sold, either through checkout or an admin marking it sold, and never to the buyer — telling someone the item they just bought is unavailable reads as a bug. Reserved is deliberately not a trigger: reservations expire and get released, so a "gone" email would often be about an item still for sale. Disabled accounts are excluded, per #33. completeCheckout now returns the sold item ids and the buyer so its three call sites can notify after COMMIT. Sending inside the transaction would email people about a sale that then rolled back, and would hold the transaction open for SMTP. Each message is sent independently so one bad address cannot stop the rest, and the sale has already succeeded regardless. Favoriting while signed out opens the existing inline register/login modal, exactly as Add to Cart does, and completes the favorite on success. Also fixes a latent bug in the same component: while the session was still resolving, `customer` is null for a signed-in visitor too, so clicking Add to Cart or the new heart in that window prompted them to sign in again. Both now ignore clicks until the session has resolved, and the control shows as loading meanwhile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d28fb5634a |
feat(ui): storefront filters and admin category/tag management (#23)
Storefront gains a Filters drawer holding the category tree, colour-coded tag pills, and a price range, with applied filters shown as removable chips. Filter state lives in the URL query string, so a filtered view is shareable and the back button works. Item cards now show their category and tags. Admin gains Categories and Tags tabs, and the item form gains a category TreeSelect plus a tags Select that creates new tags on the fly. The admin category tree tracks expansion in state rather than using defaultExpandAll: that prop is evaluated once at mount, so a branch added afterwards rendered collapsed and its children were unreachable. Creating or moving a node now expands its parent. Caught by the new admin e2e spec. The chip row is marked as a named group so its "Clear all" stays distinguishable from the drawer's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9ab689e624 | feat: add customer cart with expiry, shipping addresses with USPS validation, and multi-item PayPal checkout | ||
|
|
9be4986dd3 | Initial commit: redefined-designs storefront |