cart rework
This commit is contained in:
parent
9342518a85
commit
174439cc3b
8 changed files with 415 additions and 179 deletions
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { cart } from '$lib/stores/cart';
|
||||
import { cart, cartItems } from '$lib/stores/cart';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
let isMobileMenuOpen = false;
|
||||
|
@ -33,16 +33,16 @@
|
|||
<!-- Cart Icon (Always Visible) -->
|
||||
<button
|
||||
class="btn btn-ghost btn-circle text-white"
|
||||
on:click={() => cart.toggleCart()}
|
||||
on:click={() => cart.toggle()}
|
||||
aria-label="Shopping cart"
|
||||
>
|
||||
<div class="indicator">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
{#if $cart.length > 0}
|
||||
{#if $cartItems.length > 0}
|
||||
<span class="badge badge-sm indicator-item">
|
||||
{$cart.reduce((sum, item) => sum + item.quantity, 0)}
|
||||
{$cartItems.reduce((sum, item) => sum + item.quantity, 0)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { cart } from '$lib/stores/cart';
|
||||
import { cartItems } from '$lib/stores/cart';
|
||||
import { region } from '$lib/stores/cart';
|
||||
import { PUBLIC_MEDUSA_KEY } from '$env/static/public';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let formData = {
|
||||
firstName: '',
|
||||
|
@ -14,17 +17,75 @@
|
|||
buildingNumber: ''
|
||||
};
|
||||
|
||||
let shippingOptions: any[] = [];
|
||||
let selectedShippingOption: string | null = null;
|
||||
let isLoadingShipping = true;
|
||||
|
||||
onMount(async () => {
|
||||
await loadShippingOptions();
|
||||
});
|
||||
|
||||
async function loadShippingOptions() {
|
||||
try {
|
||||
isLoadingShipping = true;
|
||||
let cartId = localStorage.getItem('cart_id');
|
||||
const response = await fetch(`http://localhost:9000/store/shipping-options?cart_id=${cartId}`, {
|
||||
headers: {
|
||||
'x-publishable-api-key': PUBLIC_MEDUSA_KEY,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
shippingOptions = data.shipping_options;
|
||||
|
||||
// If there's only one option, select it automatically
|
||||
if (shippingOptions.length === 1) {
|
||||
selectedShippingOption = shippingOptions[0].id;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading shipping options:', error);
|
||||
} finally {
|
||||
isLoadingShipping = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!selectedShippingOption) {
|
||||
alert('Please select a shipping option');
|
||||
return;
|
||||
}
|
||||
// Here you'll integrate payment processing later
|
||||
console.log('Form submitted:', formData);
|
||||
console.log('Cart items:', $cart);
|
||||
console.log('Form submitted:', {
|
||||
...formData,
|
||||
shippingOptionId: selectedShippingOption
|
||||
});
|
||||
console.log('Cart items:', $cartItems);
|
||||
}
|
||||
|
||||
function calculateItemTotal(item: any) {
|
||||
return (item.unit_price * item.quantity).toFixed(2);
|
||||
}
|
||||
|
||||
function calculateTotal(items: any[]) {
|
||||
return items.reduce((sum, item) =>
|
||||
sum + (item.variant.calculated_price.calculated_amount * item.quantity), 0
|
||||
sum + (item.unit_price * item.quantity), 0
|
||||
).toFixed(2);
|
||||
}
|
||||
|
||||
function getCurrencySymbol(currency_code: string) {
|
||||
switch (currency_code?.toLowerCase()) {
|
||||
case 'eur':
|
||||
return '€';
|
||||
case 'usd':
|
||||
return '$';
|
||||
case 'kwd':
|
||||
return 'KWD';
|
||||
default:
|
||||
return currency_code?.toUpperCase() || '';
|
||||
}
|
||||
}
|
||||
|
||||
let currencyCode = $region?.currency_code || 'KWD';
|
||||
let currencySymbol = getCurrencySymbol(currencyCode);
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-base-100">
|
||||
|
@ -34,22 +95,62 @@
|
|||
<div class="lg:col-span-1 h-fit bg-base-200 rounded-lg p-6">
|
||||
<h2 class="text-xl font-bold mb-4">Order Summary</h2>
|
||||
<div class="space-y-3 mb-4">
|
||||
{#each $cart as item}
|
||||
{#each $cartItems as item}
|
||||
<div class="flex justify-between items-start text-sm border-b border-base-300 pb-2">
|
||||
<div class="flex-1">
|
||||
<p class="font-medium">{item.product.title}</p>
|
||||
<p class="font-medium">{item.title}</p>
|
||||
<p class="text-xs text-base-content/70">
|
||||
{item.variant.title} × {item.quantity}
|
||||
{#if item.variant && item.variant.title}
|
||||
{item.variant.title}
|
||||
{/if}
|
||||
× {item.quantity}
|
||||
</p>
|
||||
</div>
|
||||
<p class="font-medium ml-4">{(item.variant.calculated_price.calculated_amount * item.quantity).toFixed(2)} €</p>
|
||||
<p class="font-medium ml-4">{calculateItemTotal(item)} {currencySymbol}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Shipping Options -->
|
||||
<div class="mb-4">
|
||||
<h3 class="font-medium mb-2">Shipping Method</h3>
|
||||
{#if isLoadingShipping}
|
||||
<div class="w-full h-12 flex items-center justify-center">
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
</div>
|
||||
{:else if shippingOptions.length === 0}
|
||||
<div class="alert alert-warning alert-sm">
|
||||
No shipping options available
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#each shippingOptions as option}
|
||||
<label class="flex items-center gap-3 p-2 border rounded-lg cursor-pointer hover:bg-base-300 transition-colors {selectedShippingOption === option.id ? 'bg-base-300 border-primary' : 'bg-base-100'}">
|
||||
<input
|
||||
type="radio"
|
||||
name="shipping_option"
|
||||
value={option.id}
|
||||
bind:group={selectedShippingOption}
|
||||
class="radio radio-primary radio-sm"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium">{option.name}</p>
|
||||
{#if option.data}
|
||||
<p class="text-xs text-base-content/70">{option.data.id}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- {#if option.insufficient_inventory}
|
||||
<span class="badge badge-warning badge-sm">Limited Stock</span>
|
||||
{/if} -->
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center pt-4">
|
||||
<div class="flex justify-between items-center pt-4 border-t">
|
||||
<span class="text-lg font-bold">Total:</span>
|
||||
<span class="text-lg font-bold">{calculateTotal($cart)} €</span>
|
||||
<span class="text-lg font-bold">{calculateTotal($cartItems)} {currencySymbol}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,28 +1,42 @@
|
|||
<script lang="ts">
|
||||
import { cart } from '$lib/stores/cart';
|
||||
import type { CartItem } from '$lib/stores/cart';
|
||||
import { cart, cartItems, cartIsOpen } from '$lib/stores/cart';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { region } from '$lib/stores/cart';
|
||||
|
||||
let isOpen: boolean;
|
||||
cart.subscribeToOpen(value => isOpen = value);
|
||||
|
||||
function calculateItemTotal(item: CartItem) {
|
||||
return (item.variant.calculated_price.calculated_amount * item.quantity).toFixed(2);
|
||||
function calculateItemTotal(item: any) {
|
||||
return (item.unit_price * item.quantity).toFixed(2);
|
||||
}
|
||||
|
||||
function calculateTotal(items: CartItem[]) {
|
||||
function calculateTotal(items: any[]) {
|
||||
return items.reduce((sum, item) =>
|
||||
sum + (item.variant.calculated_price.calculated_amount * item.quantity), 0
|
||||
sum + (item.unit_price * item.quantity), 0
|
||||
).toFixed(2);
|
||||
}
|
||||
|
||||
function getCurrencySymbol(currency_code: string) {
|
||||
switch (currency_code?.toLowerCase()) {
|
||||
case 'eur':
|
||||
return '€';
|
||||
case 'usd':
|
||||
return '$';
|
||||
case 'kwd':
|
||||
return 'KWD';
|
||||
default:
|
||||
return currency_code?.toUpperCase() || '';
|
||||
}
|
||||
}
|
||||
|
||||
let currencyCode = $region?.currency_code || 'KWD';
|
||||
let currencySymbol = getCurrencySymbol(currencyCode);
|
||||
console.log('currencySymbol', currencySymbol);
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
{#if $cartIsOpen}
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="fixed inset-0 bg-black/50 z-40"
|
||||
on:click={() => cart.toggleCart()}
|
||||
on:click={() => cart.toggle()}
|
||||
transition:fly={{ duration: 200, opacity: 0 }}
|
||||
></div>
|
||||
|
||||
|
@ -35,7 +49,7 @@
|
|||
<h3 class="font-bold text-lg">Your Cart</h3>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
on:click={() => cart.toggleCart()}
|
||||
on:click={() => cart.toggle()}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
|
@ -44,7 +58,7 @@
|
|||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
{#if $cart.length === 0}
|
||||
{#if $cartItems.length === 0}
|
||||
<div class="h-full flex flex-col items-center justify-center text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-base-300 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
|
@ -53,31 +67,35 @@
|
|||
<p class="text-base-content/70 mb-4">Add some items to get started</p>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={() => cart.toggleCart()}
|
||||
on:click={() => cart.toggle()}
|
||||
>
|
||||
Continue Shopping
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
{#each $cart as item, i}
|
||||
{#each $cartItems as item}
|
||||
<div class="flex gap-4 pb-4 border-b">
|
||||
{#if item.product.thumbnail}
|
||||
{#if item.thumbnail}
|
||||
<img
|
||||
src={item.product.thumbnail}
|
||||
alt={item.product.title}
|
||||
src={item.thumbnail}
|
||||
alt={item.title}
|
||||
class="w-20 h-20 object-cover rounded-lg bg-base-200"
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="font-medium">{item.product.title}</h3>
|
||||
<p class="text-sm text-base-content/70">{item.variant.title}</p>
|
||||
<h3 class="font-medium">{item.title}</h3>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{#if item.variant && item.variant.title}
|
||||
{item.variant.title}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
on:click={() => cart.removeItem(i)}
|
||||
on:click={() => cart.remove(item.id)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
|
@ -89,7 +107,7 @@
|
|||
<div class="join">
|
||||
<button
|
||||
class="btn btn-sm join-item"
|
||||
on:click={() => cart.updateQuantity(i, item.quantity - 1)}
|
||||
on:click={() => cart.update(item.id, item.quantity - 1)}
|
||||
>-</button>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -99,16 +117,16 @@
|
|||
const target = e.target as HTMLInputElement;
|
||||
const value = parseInt(target.value) || 1;
|
||||
if (value >= 1) {
|
||||
cart.updateQuantity(i, value);
|
||||
cart.update(item.id, value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
class="btn btn-sm join-item"
|
||||
on:click={() => cart.updateQuantity(i, item.quantity + 1)}
|
||||
on:click={() => cart.update(item.id, item.quantity + 1)}
|
||||
>+</button>
|
||||
</div>
|
||||
<p class="font-medium">{calculateItemTotal(item)} €</p>
|
||||
<p class="font-medium">{calculateItemTotal(item)} {currencySymbol}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -117,11 +135,11 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $cart.length > 0}
|
||||
{#if $cartItems.length > 0}
|
||||
<div class="border-t p-4 space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg font-bold">Total:</span>
|
||||
<span class="text-lg font-bold">{calculateTotal($cart)} €</span>
|
||||
<span class="text-lg font-bold">{calculateTotal($cartItems)} {currencySymbol}</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
|
@ -130,7 +148,7 @@
|
|||
</a>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm w-full"
|
||||
on:click={() => cart.toggleCart()}
|
||||
on:click={() => cart.toggle()}
|
||||
>
|
||||
Continue Shopping
|
||||
</button>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { cart } from '$lib/stores/cart';
|
||||
import type { MedusaProduct, MedusaVariant } from '$lib/types/medusa';
|
||||
import { region } from '$lib/stores/cart';
|
||||
export let products: MedusaProduct[];
|
||||
|
||||
let selectedVariants = new Map<string, MedusaVariant>(
|
||||
|
@ -10,6 +11,22 @@
|
|||
function handleVariantChange(productId: string, variant: MedusaVariant) {
|
||||
selectedVariants.set(productId, variant);
|
||||
}
|
||||
|
||||
function getCurrencySymbol(currency_code: string) {
|
||||
switch (currency_code?.toLowerCase()) {
|
||||
case 'eur':
|
||||
return '€';
|
||||
case 'usd':
|
||||
return '$';
|
||||
case 'kwd':
|
||||
return 'KWD';
|
||||
default:
|
||||
return currency_code?.toUpperCase() || '';
|
||||
}
|
||||
}
|
||||
|
||||
let currencyCode = $region?.currency_code || 'KWD';
|
||||
let currencySymbol = getCurrencySymbol(currencyCode);
|
||||
</script>
|
||||
|
||||
<section class="container mx-auto px-4 py-8">
|
||||
|
@ -38,14 +55,14 @@
|
|||
}}
|
||||
>
|
||||
{#each product.variants as variant}
|
||||
<option value={variant.id}>{variant.title} {variant.calculated_price.calculated_amount} {variant.calculated_price.currency_code}</option>
|
||||
<option value={variant.id}>{variant.title} {variant.calculated_price.calculated_amount} {currencySymbol}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button
|
||||
class="btn btn-primary btn-sm w-full"
|
||||
on:click={() => {
|
||||
const variant = selectedVariants.get(product.id);
|
||||
if (variant) cart.addToCart(product, variant);
|
||||
if (variant) cart.add(variant.id);
|
||||
}}
|
||||
>
|
||||
Add to cart
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { cart } from '$lib/stores/cart';
|
||||
import type { MedusaProduct, MedusaVariant, MedusaOptionValue } from '$lib/types/medusa';
|
||||
import { region } from '$lib/stores/cart';
|
||||
|
||||
export let product: MedusaProduct;
|
||||
let selectedVariant: MedusaVariant = product.variants[0];
|
||||
|
@ -58,81 +59,104 @@
|
|||
);
|
||||
}) || [];
|
||||
}
|
||||
|
||||
function getCurrencySymbol(currency_code: string) {
|
||||
switch (currency_code?.toLowerCase()) {
|
||||
case 'eur':
|
||||
return '€';
|
||||
case 'usd':
|
||||
return '$';
|
||||
case 'kwd':
|
||||
return 'KWD';
|
||||
default:
|
||||
return currency_code?.toUpperCase() || '';
|
||||
}
|
||||
}
|
||||
|
||||
let currencyCode = $region?.currency_code || 'KWD';
|
||||
let currencySymbol = getCurrencySymbol(currencyCode);
|
||||
</script>
|
||||
|
||||
<svelte:window on:scroll={handleScroll}/>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="space-y-4">
|
||||
<img
|
||||
src={product.images[currentImageIndex].url}
|
||||
alt={product.title}
|
||||
class="aspect-square rounded-lg object-cover bg-base-200"
|
||||
/>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
{#each product.images as image, i}
|
||||
<button
|
||||
class="p-0 border-2 rounded-lg {currentImageIndex === i ? 'border-primary' : 'border-transparent'}"
|
||||
on:click={() => currentImageIndex = i}
|
||||
>
|
||||
<img
|
||||
src={image.url}
|
||||
alt={product.title}
|
||||
class="aspect-square rounded-lg object-cover hover:opacity-75"
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 py-8">
|
||||
<!-- Product Images -->
|
||||
<div class="relative">
|
||||
<div class="aspect-square overflow-hidden rounded-lg bg-base-200">
|
||||
<img
|
||||
src={product.images[currentImageIndex]?.url || product.thumbnail}
|
||||
alt={product.title}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">{product.title}</h1>
|
||||
<h1 class="text-3xl font-bold">{selectedVariant.calculated_price.calculated_amount} {selectedVariant.calculated_price.currency_code}</h1>
|
||||
</div>
|
||||
|
||||
{#each product.options as option}
|
||||
<div>
|
||||
<label class="label" for={`option-${option.id}`}>
|
||||
<span class="label-text">{option.title}</span>
|
||||
</label>
|
||||
<select
|
||||
id={`option-${option.id}`}
|
||||
class="select select-bordered w-full"
|
||||
value={selectedOptions.get(option.id)}
|
||||
on:change={(e) => handleOptionChange(option.id, (e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
{#each getAvailableValues(option.id) as value}
|
||||
<option value={value.value}>
|
||||
{value.value}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<p class="text-lg">{product.description}</p>
|
||||
|
||||
<div bind:this={cartButton}>
|
||||
<button
|
||||
class="btn btn-primary btn-lg w-full {isSticky ? 'sticky-cart' : ''}"
|
||||
on:click={() => cart.addToCart(product, selectedVariant)}
|
||||
>
|
||||
Add to Cart - {selectedVariant.title}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if selectedVariant.sku || product.weight || product.material}
|
||||
<div class="prose pb-16 md:pb-0">
|
||||
<h3 class="text-lg font-semibold">Product Details</h3>
|
||||
<ul>
|
||||
{#if selectedVariant.sku}<li>SKU: {selectedVariant.sku}</li>{/if}
|
||||
{#if product.weight}<li>Weight: {product.weight}g</li>{/if}
|
||||
{#if product.material}<li>Material: {product.material}</li>{/if}
|
||||
</ul>
|
||||
{#if product.images.length > 1}
|
||||
<div class="grid grid-cols-4 gap-2 mt-2">
|
||||
{#each product.images as image, i}
|
||||
<button
|
||||
class="aspect-square rounded-lg overflow-hidden bg-base-200 hover:opacity-75 transition-opacity"
|
||||
class:ring-2={currentImageIndex === i}
|
||||
class:ring-primary={currentImageIndex === i}
|
||||
on:click={() => currentImageIndex = i}
|
||||
>
|
||||
<img
|
||||
src={image.url}
|
||||
alt={`${product.title} - View ${i + 1}`}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Product Info -->
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-4">{product.title}</h1>
|
||||
<p class="text-xl font-semibold mb-6">{selectedVariant.calculated_price.calculated_amount} {currencySymbol}</p>
|
||||
|
||||
<div class="prose max-w-none mb-8">
|
||||
<p>{product.description}</p>
|
||||
</div>
|
||||
|
||||
<!-- Product Options -->
|
||||
{#if product.options.length > 0}
|
||||
<div class="space-y-4 mb-8">
|
||||
{#each product.options as option}
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" for={option.id}>
|
||||
{option.title}
|
||||
</label>
|
||||
<select
|
||||
id={option.id}
|
||||
class="select select-bordered w-full"
|
||||
value={selectedOptions.get(option.id)}
|
||||
on:change={(e) => handleOptionChange(option.id, (e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
{#each getAvailableValues(option.id) as value}
|
||||
<option value={value.value}>
|
||||
{value.value}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Add to Cart -->
|
||||
<div
|
||||
class="card bg-base-200 p-4 sticky bottom-2 transition-all duration-300"
|
||||
class:sticky-cart={isSticky}
|
||||
bind:this={cartButton}
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary w-full"
|
||||
on:click={() => cart.add(selectedVariant.id)}
|
||||
>
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
// import {MedusaClient} from 'sveltekit-medusa-client'
|
||||
// import { PUBLIC_MEDUSA_URL, PUBLIC_MEDUSA_KEY } from '$env/static/public'
|
||||
import {MedusaClient} from 'sveltekit-medusa-client'
|
||||
import { PUBLIC_MEDUSA_URL, PUBLIC_MEDUSA_KEY } from '$env/static/public'
|
||||
|
||||
// export default new MedusaClient(PUBLIC_MEDUSA_URL, {
|
||||
// headers: {
|
||||
// 'x-publishable-api-key': PUBLIC_MEDUSA_KEY
|
||||
// },
|
||||
// retry: 0,
|
||||
// timeout: 10000
|
||||
// });
|
||||
export default new MedusaClient(PUBLIC_MEDUSA_URL, {
|
||||
headers: {
|
||||
'x-publishable-api-key': PUBLIC_MEDUSA_KEY
|
||||
},
|
||||
retry: 0,
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { writable } from 'svelte/store';
|
||||
import type { MedusaProduct, MedusaVariant } from '$lib/types/medusa';
|
||||
import toast from 'svelte-french-toast';
|
||||
import type { Region } from './region';
|
||||
import { PUBLIC_MEDUSA_KEY } from '$env/static/public';
|
||||
|
||||
export interface CartItem {
|
||||
product: MedusaProduct;
|
||||
|
@ -8,75 +10,143 @@ export interface CartItem {
|
|||
quantity: number;
|
||||
}
|
||||
|
||||
function createCartStore() {
|
||||
const { subscribe, set, update } = writable<CartItem[]>([]);
|
||||
const isOpenStore = writable<boolean>(false);
|
||||
export const cartId = writable<string | null>(null);
|
||||
export const cartItems = writable<any[]>([]);
|
||||
export const cartIsOpen = writable(false);
|
||||
|
||||
// Load initial state from localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedCart = localStorage.getItem('cart');
|
||||
if (savedCart) {
|
||||
set(JSON.parse(savedCart));
|
||||
function getStoredCartId() {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('cart_id');
|
||||
}
|
||||
|
||||
function setStoredCartId(id: string) {
|
||||
localStorage.setItem('cart_id', id);
|
||||
cartId.set(id);
|
||||
}
|
||||
|
||||
async function initCart() {
|
||||
const existing = getStoredCartId();
|
||||
if (existing) {
|
||||
cartId.set(existing);
|
||||
await syncCartFromBackend(existing);
|
||||
} else {
|
||||
const res = await fetch('http://localhost:9000/store/carts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-publishable-api-key': PUBLIC_MEDUSA_KEY
|
||||
}
|
||||
});
|
||||
if (res.ok) {
|
||||
const { cart } = await res.json();
|
||||
setStoredCartId(cart.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function syncCartFromBackend(id: string) {
|
||||
const res = await fetch(`http://localhost:9000/store/carts/${id}`, {
|
||||
headers: {
|
||||
'x-publishable-api-key': PUBLIC_MEDUSA_KEY
|
||||
}
|
||||
});
|
||||
if (res.ok) {
|
||||
const { cart } = await res.json();
|
||||
cartItems.set(cart.items || []);
|
||||
return cart;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function showAddToCartToast(title: string) {
|
||||
toast(`${title} added to cart`, {
|
||||
icon: '🛍️',
|
||||
duration: 2000,
|
||||
position: 'bottom-center',
|
||||
style: 'background-color: #000; color: #fff;',
|
||||
});
|
||||
}
|
||||
|
||||
function showAddToCartToast(product: MedusaProduct, variant: MedusaVariant) {
|
||||
toast(`${product.title} ${variant.title} added to cart`, {
|
||||
icon: '🛍️',
|
||||
duration: 2000,
|
||||
position: 'bottom-center',
|
||||
style: 'background-color: #000; color: #fff;',
|
||||
});
|
||||
async function addItemToCart(variant_id: string, quantity = 1) {
|
||||
const id = getStoredCartId();
|
||||
if (!id) {
|
||||
await initCart();
|
||||
return addItemToCart(variant_id, quantity);
|
||||
}
|
||||
|
||||
const res = await fetch(`http://localhost:9000/store/carts/${id}/line-items`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-publishable-api-key': PUBLIC_MEDUSA_KEY
|
||||
},
|
||||
body: JSON.stringify({ variant_id, quantity })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const cart = await syncCartFromBackend(id);
|
||||
const addedItem = cart?.items?.find((item: any) => item.variant_id === variant_id);
|
||||
if (addedItem) {
|
||||
showAddToCartToast(addedItem.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateItemQuantity(item_id: string, quantity: number) {
|
||||
const id = getStoredCartId();
|
||||
if (!id || quantity < 1) return;
|
||||
|
||||
const res = await fetch(`http://localhost:9000/store/carts/${id}/line-items/${item_id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-publishable-api-key': PUBLIC_MEDUSA_KEY
|
||||
},
|
||||
body: JSON.stringify({ quantity })
|
||||
});
|
||||
|
||||
if (res.ok) await syncCartFromBackend(id);
|
||||
}
|
||||
|
||||
async function removeItem(item_id: string) {
|
||||
const id = getStoredCartId();
|
||||
if (!id) return;
|
||||
|
||||
const res = await fetch(`http://localhost:9000/store/carts/${id}/line-items/${item_id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'x-publishable-api-key': PUBLIC_MEDUSA_KEY
|
||||
}
|
||||
});
|
||||
|
||||
if (res.ok) await syncCartFromBackend(id);
|
||||
}
|
||||
|
||||
export const cart = {
|
||||
init: initCart,
|
||||
add: addItemToCart,
|
||||
update: updateItemQuantity,
|
||||
remove: removeItem,
|
||||
toggle: () => cartIsOpen.update(v => !v),
|
||||
open: () => cartIsOpen.set(true),
|
||||
close: () => cartIsOpen.set(false),
|
||||
subscribe: cartItems.subscribe,
|
||||
subscribeToOpen: cartIsOpen.subscribe,
|
||||
sync: syncCartFromBackend
|
||||
};
|
||||
|
||||
function createRegionStore() {
|
||||
const { subscribe, set, update } = writable<Region>();
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedRegion = localStorage.getItem('selectedRegion');
|
||||
if (savedRegion) {
|
||||
set(JSON.parse(savedRegion));
|
||||
}
|
||||
}
|
||||
return {
|
||||
subscribe,
|
||||
subscribeToOpen: isOpenStore.subscribe,
|
||||
toggleCart: () => {
|
||||
isOpenStore.update(state => !state);
|
||||
},
|
||||
addToCart: (product: MedusaProduct, variant: MedusaVariant) => {
|
||||
update(items => {
|
||||
const existingItem = items.find(item =>
|
||||
item.product.id === product.id && item.variant.id === variant.id
|
||||
);
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity += 1;
|
||||
return [...items];
|
||||
}
|
||||
|
||||
const newItems = [...items, { product, variant, quantity: 1 }];
|
||||
localStorage.setItem('cart', JSON.stringify(newItems));
|
||||
return newItems;
|
||||
});
|
||||
|
||||
// Show toast notification instead of opening cart
|
||||
showAddToCartToast(product, variant);
|
||||
},
|
||||
updateQuantity: (index: number, quantity: number) => {
|
||||
update(items => {
|
||||
if (quantity < 1) {
|
||||
const newItems = items.filter((_, i) => i !== index);
|
||||
localStorage.setItem('cart', JSON.stringify(newItems));
|
||||
return newItems;
|
||||
}
|
||||
|
||||
items[index].quantity = quantity;
|
||||
localStorage.setItem('cart', JSON.stringify(items));
|
||||
return [...items];
|
||||
});
|
||||
},
|
||||
removeItem: (index: number) => {
|
||||
update(items => {
|
||||
const newItems = items.filter((_, i) => i !== index);
|
||||
localStorage.setItem('cart', JSON.stringify(newItems));
|
||||
return newItems;
|
||||
});
|
||||
}
|
||||
setRegion: (region: Region) => set(region),
|
||||
};
|
||||
}
|
||||
|
||||
export const cart = createCartStore();
|
||||
export const region = createRegionStore();
|
|
@ -4,6 +4,12 @@
|
|||
import Footer from '$lib/components/Footer.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { Toaster } from 'svelte-french-toast';
|
||||
import { cart } from '$lib/stores/cart';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
cart.init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
Loading…
Reference in a new issue