feat: add cart page, cart drawer, and shared cart infrastructure
- Cart context with pure functions for add/remove/update/hydrate - Price formatting via ex_money (replaces all float division) - CartHook on_mount with attach_hook for shared event handlers (open/close drawer, remove item, PubSub sync) - Accessible cart drawer with focus trap, scroll lock, aria-live - Cart page with increment/decrement quantity controls - Preview mode cart drawer support in theme editor - Cart persistence to session via JS hook + API endpoint - 19 tests covering all Cart pure functions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
<div
|
||||
id="shop-container"
|
||||
phx-hook="CartPersist"
|
||||
class="shop-container min-h-screen pb-20 md:pb-0"
|
||||
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
|
||||
>
|
||||
@@ -31,7 +33,14 @@
|
||||
|
||||
<.shop_footer theme_settings={@theme_settings} mode={@mode} />
|
||||
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
<.cart_drawer
|
||||
cart_items={@cart_items}
|
||||
subtotal={@cart_subtotal}
|
||||
cart_count={@cart_count}
|
||||
mode={@mode}
|
||||
open={assigns[:cart_drawer_open] || false}
|
||||
cart_status={assigns[:cart_status]}
|
||||
/>
|
||||
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div
|
||||
id="shop-container"
|
||||
phx-hook="CartPersist"
|
||||
class="shop-container min-h-screen pb-20 md:pb-0"
|
||||
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
|
||||
>
|
||||
@@ -19,12 +21,45 @@
|
||||
|
||||
<main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<.page_title text="Your basket" />
|
||||
<.cart_layout items={@cart_page_items} subtotal={@cart_page_subtotal} mode={@mode} />
|
||||
|
||||
<%= if @cart_items == [] do %>
|
||||
<.cart_empty_state mode={@mode} />
|
||||
<% else %>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div class="lg:col-span-2">
|
||||
<ul
|
||||
role="list"
|
||||
aria-label="Cart items"
|
||||
class="flex flex-col gap-4"
|
||||
style="list-style: none; margin: 0; padding: 0;"
|
||||
>
|
||||
<%= for item <- @cart_items do %>
|
||||
<li>
|
||||
<.shop_card class="p-4">
|
||||
<.cart_item_row item={item} size={:default} show_quantity_controls mode={@mode} />
|
||||
</.shop_card>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<.order_summary subtotal={@cart_page_subtotal} mode={@mode} />
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</main>
|
||||
|
||||
<.shop_footer theme_settings={@theme_settings} mode={@mode} />
|
||||
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
<.cart_drawer
|
||||
cart_items={@cart_items}
|
||||
subtotal={@cart_subtotal}
|
||||
cart_count={@cart_count}
|
||||
mode={@mode}
|
||||
open={assigns[:cart_drawer_open] || false}
|
||||
cart_status={assigns[:cart_status]}
|
||||
/>
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
<.mobile_bottom_nav active_page="cart" mode={@mode} />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div
|
||||
id="shop-container"
|
||||
phx-hook="CartPersist"
|
||||
class="shop-container min-h-screen pb-20 md:pb-0"
|
||||
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
|
||||
>
|
||||
@@ -39,7 +41,13 @@
|
||||
|
||||
<.shop_footer theme_settings={@theme_settings} mode={@mode} />
|
||||
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
<.cart_drawer
|
||||
cart_items={@cart_items}
|
||||
subtotal={@cart_subtotal}
|
||||
cart_count={@cart_count}
|
||||
mode={@mode}
|
||||
open={assigns[:cart_drawer_open] || false}
|
||||
/>
|
||||
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div
|
||||
id="shop-container"
|
||||
phx-hook="CartPersist"
|
||||
class="shop-container min-h-screen pb-20 md:pb-0"
|
||||
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
|
||||
>
|
||||
@@ -54,7 +56,14 @@
|
||||
|
||||
<.shop_footer theme_settings={@theme_settings} mode={@mode} />
|
||||
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
<.cart_drawer
|
||||
cart_items={@cart_items}
|
||||
subtotal={@cart_subtotal}
|
||||
cart_count={@cart_count}
|
||||
mode={@mode}
|
||||
open={assigns[:cart_drawer_open] || false}
|
||||
cart_status={assigns[:cart_status]}
|
||||
/>
|
||||
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
|
||||
@@ -50,7 +50,14 @@
|
||||
|
||||
<.shop_footer theme_settings={@theme_settings} mode={@mode} />
|
||||
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
<.cart_drawer
|
||||
cart_items={@cart_items}
|
||||
subtotal={@cart_subtotal}
|
||||
cart_count={@cart_count}
|
||||
mode={@mode}
|
||||
open={assigns[:cart_drawer_open] || false}
|
||||
cart_status={assigns[:cart_status]}
|
||||
/>
|
||||
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div
|
||||
id="shop-container"
|
||||
phx-hook="CartPersist"
|
||||
class="shop-container min-h-screen pb-20 md:pb-0"
|
||||
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
|
||||
>
|
||||
@@ -47,7 +49,14 @@
|
||||
|
||||
<.shop_footer theme_settings={@theme_settings} mode={@mode} />
|
||||
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
<.cart_drawer
|
||||
cart_items={@cart_items}
|
||||
subtotal={@cart_subtotal}
|
||||
cart_count={@cart_count}
|
||||
mode={@mode}
|
||||
open={assigns[:cart_drawer_open] || false}
|
||||
cart_status={assigns[:cart_status]}
|
||||
/>
|
||||
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div
|
||||
id="shop-container"
|
||||
phx-hook="CartPersist"
|
||||
class="shop-container min-h-screen pb-20 md:pb-0"
|
||||
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
|
||||
>
|
||||
@@ -66,7 +68,7 @@
|
||||
</div>
|
||||
|
||||
<.quantity_selector quantity={@quantity} in_stock={@product.in_stock} />
|
||||
<.add_to_cart_button />
|
||||
<.add_to_cart_button mode={@mode} />
|
||||
<.trust_badges :if={@theme_settings.pdp_trust_badges} />
|
||||
<.product_details product={@product} />
|
||||
</div>
|
||||
@@ -89,7 +91,14 @@
|
||||
|
||||
<.shop_footer theme_settings={@theme_settings} mode={@mode} />
|
||||
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
<.cart_drawer
|
||||
cart_items={@cart_items}
|
||||
subtotal={@cart_subtotal}
|
||||
cart_count={@cart_count}
|
||||
mode={@mode}
|
||||
open={assigns[:cart_drawer_open] || false}
|
||||
cart_status={assigns[:cart_status]}
|
||||
/>
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
<.mobile_bottom_nav active_page="pdp" mode={@mode} />
|
||||
|
||||
Reference in New Issue
Block a user