Current completed phase: Phase 5 — Wishlist feature and a full Cart → Checkout → Order Confirmation flow (scope confirmed with the user up front: "both", i.e. the two candidates the Phase 4 handoff had flagged).
- Wishlist, site-wide —
WishlistContext+WishlistProvider(newsrc/context/files), mounted inApp.tsxalongsideCartProvider. Persisted tolocalStorage(crafteevee-wishlist) using the exact same read-on-init / write-on-change pattern asCartProvider. Only product ids are stored; product data is joined fromALL_PRODUCTSat read time viaitems, mirroring howCartProviderjoinslines. WishlistButton(new,src/components/ui/) — reusable heart toggle. Used onProductCard(stacked with the tag badge in the top-right corner of the image) and onProductDetail(next to "Add to cart"). Stops event propagation so tapping it inside aProductCard's<Link>toggles the wishlist instead of navigating to the product page./wishlistpage (new) — grid of saved products, reusingProductCardandEmptyState. No simulated fetch here (unlike Shop/ProductDetail) — wishlist data never left the browser, so there's nothing to "load."- Navbar wishlist icon is now real — badge shows the live wishlist
count and the icon navigates to
/wishlist(a page, not a drawer — wishlists tend to get browsed, not glanced at, so a full page fit better than mirroringCartDrawer). /cartpage (new) — full-page cart review: the roomier counterpart to the Navbar'sCartDrawer. Same line-item editing (quantity, remove) as the drawer, plus an order-summary sidebar with "Proceed to checkout."/checkoutpage (new) — shipping form (contact + address), payment method (Cash on Delivery / GCash-or-bank-transfer — both plausible for a Philippines-based shop, no real payment processing exists), live order summary with shipping fee (₱80, free at ₱1,500+ subtotal), and a simulated order submission (placeOrder(), same fake-latency pattern as Newsletter'ssubscribe()from Phase 2). Redirects to/cartif there's nothing to check out./order-confirmationpage (new) — receipt shown after a successful order: order number, line items, totals, shipping address. Order data arrives via routerlocation.state(set by Checkout'snavigatecall) rather than global state, since it's a one-time receipt. A visit with no state redirects to/shop.CartDrawer's missing checkout button is filled in — the Phase 4 handoff flagged this as an open question. The drawer's primary CTA is now "View cart & checkout" →/cart(a modal is the wrong place for a multi-field shipping form, so the drawer hands off to the full page rather than growing a form itself). "Continue shopping" is now a secondary text link below it.src/lib/checkout.ts(new) —validateCheckout()(required-field + email-format validation),PAYMENT_METHODS,SHIPPING_FEE/FREE_SHIPPING_THRESHOLDconstants, andplaceOrder()(the simulated submission). Keeping the shipping-fee math in one function means Cart, Checkout, and the confirmation page can't drift out of sync on the number shown.src/types/order.ts(new) —CheckoutFormData,OrderLine,Order,PaymentMethod.OrderLineis a snapshot (name/price copied at order time), not a live join back toALL_PRODUCTS, since a real order shouldn't silently change if the catalog changes later.
None found this session — Phase 4 was verified clean (build + lint passed before any Phase 5 code was written).
ProductCard's top-right corner now stacks the tag badge and the wishlist heart in aflex-colwrapper instead of each being independentlyabsolute-positioned, so both can coexist without overlapping regardless of whether a tag is present.
src/
├── context/
│ ├── WishlistContext.ts (createContext, WishlistContextValue, useWishlist hook)
│ └── WishlistProvider.tsx (WishlistProvider component - state, localStorage, derived values)
├── components/
│ └── ui/
│ └── WishlistButton.tsx (heart toggle, used on ProductCard + ProductDetail)
├── pages/
│ ├── Wishlist.tsx
│ ├── Cart.tsx
│ ├── Checkout.tsx
│ └── OrderConfirmation.tsx
├── lib/
│ └── checkout.ts (validation, shipping constants, simulated placeOrder)
├── types/
│ └── order.ts (CheckoutFormData, OrderLine, Order, PaymentMethod)
HANDOFF-PHASE-5.md
src/App.tsx— addedWishlistProvider(wrapping the router alongsideCartProvider) and four new routes:wishlist,cart,checkout,order-confirmation.src/components/layout/Navbar.tsx— WishlistIconButtonnow shows the realcountfromuseWishlist()and navigates to/wishlistinstead of a no-op; updated the doc comment (both cart and wishlist are real now).src/components/cart/CartDrawer.tsx— primary action is now "View cart & checkout" (navigates to/cart), with "Continue shopping" demoted to a secondary text link; updated the doc comment.src/components/ui/ProductCard.tsx— addedWishlistButton, stacked with the tag badge in the top-right corner.src/pages/ProductDetail.tsx— addedWishlistButtonnext to "Add to cart."
None.
- New reusable components:
WishlistButton(src/components/ui/). - New utilities:
src/lib/checkout.ts(validateCheckout,placeOrder,PAYMENT_METHODS,SHIPPING_FEE,FREE_SHIPPING_THRESHOLD). - State management: second global context, same shape as Phase 4's
cart.
WishlistContext+WishlistProvider(src/context/), split into two files for the samereact/only-export-components/ component-file-purity reasonCartContext/CartProviderwere split.WishlistProviderwraps<BrowserRouter>inApp.tsx, same asCartProvider— nesting order between the two doesn't matter, since neither reads from the other. - Data model:
CheckoutFormData/OrderLine/Order/PaymentMethodadded insrc/types/order.ts. No changes toProductthis phase. - Wishlist persistence:
localStoragekeycrafteevee-wishlist, an array of product ids (not full items) — same defensive stored-id-no-longer-in-catalog filtering asCartProvider. - Checkout flow / order placement: no real backend, same as every
other "form submit" in the app.
placeOrder()insrc/lib/checkout.tsis a documentedsetTimeout-wrapped promise, same pattern asfetchProducts()/fetchProductById()(Shop/ProductDetail) andsubscribe()(Newsletter). Order data is handed toOrderConfirmationvia React Router'snavigate(path, { state })rather than stored anywhere global — it's a one-time receipt, not something that needs to survive independently of that specific navigation. - Shipping fee logic: flat ₱80, free at a ₱1,500+ subtotal — both are
named constants in
src/lib/checkout.ts(SHIPPING_FEE,FREE_SHIPPING_THRESHOLD), not repeated magic numbers, so Cart, Checkout, and the confirmation receipt can't disagree. - API changes: none —
placeOrder()is a documented simulated call, matching the existing pattern. - Database changes: none.
- Configuration updates: none — no new dependencies were installed,
package.jsonis unchanged from Phase 4.
Verified this session (headless Chromium via Playwright against a
production vite build + vite preview, plus a scripted click-through
of every interactive flow — 21/22 automated checks passed; the one
"failure" turned out to be a correct browser behavior, not a bug, see
Known Issues):
-
npm run buildsucceeds,npm run lint(oxlint src) reports 0 errors/warnings - Wishlist heart on a
ProductCardtoggles without navigating to the product page; Navbar wishlist badge updates immediately - Wishlist heart on
ProductDetailtoggles independently and stays in sync with the same product's card elsewhere -
/wishlistshows saved products; persists across a full page reload - Empty wishlist shows
EmptyStatewith a working "Browse the shop" action - Add to cart still works exactly as in Phase 4 (badge updates, confirmation message shows)
-
CartDrawer→ "View cart & checkout" closes the drawer and lands on/cartwith the same items and subtotal -
/cart→ "Proceed to checkout" lands on/checkout - Submitting the checkout form empty shows validation errors on every required field and does not navigate away
- Filling the form correctly and submitting shows a loading state on
the submit button, then navigates to
/order-confirmationwith the correct order number, line items, subtotal, shipping fee, total, and shipping address - Cart badge is cleared (back to no badge) immediately after an order is placed
- Direct visit to
/checkoutwith an empty cart redirects to/cart - Empty
/cartshowsEmptyStatewith a working "Browse the shop" action - A genuinely fresh visit to
/order-confirmation(new browser context, no prior navigation) redirects to/shop - Light mode, dark mode, and mobile (390px) visual check via screenshots: Shop (with wishlist hearts), Cart, Wishlist, Product Detail (with wishlist button next to Add to cart), Checkout (full form + payment method group + order summary) — all render cleanly, zero console errors from the app itself (only pre-existing 403s from Google Fonts, which is a sandboxed-environment network restriction, not an app issue)
- Homepage regression check: Home, Shop, and ProductDetail still work exactly as they did at the end of Phase 4
Still needs a human pass:
- Real browser check on an actual mobile device (Playwright's mobile viewport emulation was used here, not a physical device)
- Content review — shipping fee (₱80) and free-shipping threshold (₱1,500) are placeholder numbers chosen to feel plausible against the ₱85–₱890 catalog price range; swap for real numbers whenever they're decided
- Decide whether the GCash/bank-transfer payment option needs any additional confirmation step (e.g. "we'll email payment instructions") before this ships publicly — left simple this session since there's no real payment processing to hook up yet
- Refreshing
/order-confirmationin the same tab right after placing an order still shows the receipt (does not redirect). This is correct, expected browser behavior, not a bug: the browser's History API preserveslocation.stateacross a same-URL reload, so a user who accidentally hits refresh right after checkout doesn't lose their receipt. A genuinely fresh visit (new tab, or navigating in from elsewhere with no prior history entry for that URL) does redirect to/shopas intended — verified separately with a brand-new browser context. Worth knowing about if this ever gets debugged again, since a same-tab-refresh test will look like a "failure" against a naive "redirects to /shop" assertion when it's actually working as designed. - No payment processing — "Place order" is a simulated submission with no real payment gateway behind either payment method. Intentional for this phase's scope (real checkout flow and state, not real payments).
- No order history — a placed order only exists as the receipt shown once
on
/order-confirmation; there's no account area or "my orders" list to look it up again afterward. Not part of this phase's scope (no accounts/auth exist yet at all). MAX_QTY(10, from Phase 4'sCartProvider) is unchanged and still a hardcoded constant — same known limitation as before.- The simulated
placeOrder()/fetchProductById()/fetchProducts()can't actually fail, so their error-state retry paths remain untestable in a meaningful way until a real API exists (same known limitation since Phase 3). - No test suite yet (unchanged from Phase 1-4 — still slated for Phase 14).
- Nothing outstanding for Phase 5 as scoped. The human QA items above are worth a pass before calling it fully signed off.
Phase 6 — not yet defined/confirmed with the user. Natural candidates
given what's now built: user accounts (needed for real order history,
which Phase 5's Known Issues flags as missing), or the About/Contact
pages (still PagePlaceholder stubs from Phase 1/2 and the only
remaining unbuilt nav destinations). Do not begin Phase 6 work until
scope is confirmed and Phase 5 is approved.
- Stack: Vite + React 19 + TypeScript + Tailwind v4 (CSS-first config
via
@themeinsrc/index.css) + React Router v7 + Framer Motion. Unchanged from Phase 1-4. - Tokens: unchanged — still all in
src/index.cssunder@theme. No new tokens were added this phase. - Product catalog: unchanged from Phase 4 —
src/data/products.tsexportsALL_PRODUCTS(24 items, prices ₱85-₱890). - Cart: unchanged API from Phase 4 —
useCart()from@/context/CartContext. Still{ items, lines, totalCount, subtotal, addItem, removeItem, updateQuantity, clearCart }. - Wishlist (new):
useWishlist()from@/context/WishlistContextgives you{ productIds, items, count, isWishlisted, toggleWishlist, removeItem, clearWishlist }. Useitems(notproductIds) for anything that renders product info.WishlistProvideris mounted once inApp.tsx, alongsideCartProvider— don't mount it again anywhere else. - Checkout:
useCart()'sclearCart()is called right after a successfulplaceOrder(), before navigating to/order-confirmation. If you touchCheckout.tsx, keep theplacedOrderstate guard on the empty-cart redirect effect — without it, the redirect-to-/carteffect races the navigate-to-/order-confirmationcall and can win, sinceclearCart()emptieslinesa render before the navigate takes effect. - Routing: new routes this phase are
/wishlist,/cart,/checkout,/order-confirmation. All hang off the same<Layout />as everything else. - Known gotcha (repeated from Phase 1-4): lucide-react v1.x has no
brand icons — use inline SVGs or a generic icon instead. Didn't come up
this phase (only used existing/common icons plus
HeartandCircleCheck, both already used elsewhere in the app). - Completed: full Phase 1 foundation + Phase 2 homepage + Phase 3 shop
- Phase 4 product detail/cart + Phase 5 wishlist and cart/checkout/ order-confirmation, as listed above.
- Pending: Phase 6 onward — likely accounts or About/Contact content, but confirm with the user before starting either.
- No known bugs as of this handoff — production build and lint both verified clean; the one thing that looked like a bug during this session's QA (refresh-preserves-receipt) turned out to be correct browser behavior on investigation, not a defect — see Known Issues for the full explanation if it comes up again.