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>
|
<script>
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { cart } from '$lib/stores/cart';
|
import { cart, cartItems } from '$lib/stores/cart';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
|
|
||||||
let isMobileMenuOpen = false;
|
let isMobileMenuOpen = false;
|
||||||
|
@ -33,16 +33,16 @@
|
||||||
<!-- Cart Icon (Always Visible) -->
|
<!-- Cart Icon (Always Visible) -->
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-circle text-white"
|
class="btn btn-ghost btn-circle text-white"
|
||||||
on:click={() => cart.toggleCart()}
|
on:click={() => cart.toggle()}
|
||||||
aria-label="Shopping cart"
|
aria-label="Shopping cart"
|
||||||
>
|
>
|
||||||
<div class="indicator">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
{#if $cart.length > 0}
|
{#if $cartItems.length > 0}
|
||||||
<span class="badge badge-sm indicator-item">
|
<span class="badge badge-sm indicator-item">
|
||||||
{$cart.reduce((sum, item) => sum + item.quantity, 0)}
|
{$cartItems.reduce((sum, item) => sum + item.quantity, 0)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<script lang="ts">
|
<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 = {
|
let formData = {
|
||||||
firstName: '',
|
firstName: '',
|
||||||
|
@ -14,17 +17,75 @@
|
||||||
buildingNumber: ''
|
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() {
|
function handleSubmit() {
|
||||||
|
if (!selectedShippingOption) {
|
||||||
|
alert('Please select a shipping option');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Here you'll integrate payment processing later
|
// Here you'll integrate payment processing later
|
||||||
console.log('Form submitted:', formData);
|
console.log('Form submitted:', {
|
||||||
console.log('Cart items:', $cart);
|
...formData,
|
||||||
|
shippingOptionId: selectedShippingOption
|
||||||
|
});
|
||||||
|
console.log('Cart items:', $cartItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateItemTotal(item: any) {
|
||||||
|
return (item.unit_price * item.quantity).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateTotal(items: any[]) {
|
function calculateTotal(items: any[]) {
|
||||||
return items.reduce((sum, item) =>
|
return items.reduce((sum, item) =>
|
||||||
sum + (item.variant.calculated_price.calculated_amount * item.quantity), 0
|
sum + (item.unit_price * item.quantity), 0
|
||||||
).toFixed(2);
|
).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>
|
</script>
|
||||||
|
|
||||||
<div class="min-h-screen bg-base-100">
|
<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">
|
<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>
|
<h2 class="text-xl font-bold mb-4">Order Summary</h2>
|
||||||
<div class="space-y-3 mb-4">
|
<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 justify-between items-start text-sm border-b border-base-300 pb-2">
|
||||||
<div class="flex-1">
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</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">Total:</span>
|
||||||
<span class="text-lg font-bold">{calculateTotal($cart)} €</span>
|
<span class="text-lg font-bold">{calculateTotal($cartItems)} {currencySymbol}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,42 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cart } from '$lib/stores/cart';
|
import { cart, cartItems, cartIsOpen } from '$lib/stores/cart';
|
||||||
import type { CartItem } from '$lib/stores/cart';
|
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
|
import { region } from '$lib/stores/cart';
|
||||||
|
|
||||||
let isOpen: boolean;
|
function calculateItemTotal(item: any) {
|
||||||
cart.subscribeToOpen(value => isOpen = value);
|
return (item.unit_price * item.quantity).toFixed(2);
|
||||||
|
|
||||||
function calculateItemTotal(item: CartItem) {
|
|
||||||
return (item.variant.calculated_price.calculated_amount * item.quantity).toFixed(2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateTotal(items: CartItem[]) {
|
function calculateTotal(items: any[]) {
|
||||||
return items.reduce((sum, item) =>
|
return items.reduce((sum, item) =>
|
||||||
sum + (item.variant.calculated_price.calculated_amount * item.quantity), 0
|
sum + (item.unit_price * item.quantity), 0
|
||||||
).toFixed(2);
|
).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>
|
</script>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if $cartIsOpen}
|
||||||
<!-- Backdrop -->
|
<!-- Backdrop -->
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 bg-black/50 z-40"
|
class="fixed inset-0 bg-black/50 z-40"
|
||||||
on:click={() => cart.toggleCart()}
|
on:click={() => cart.toggle()}
|
||||||
transition:fly={{ duration: 200, opacity: 0 }}
|
transition:fly={{ duration: 200, opacity: 0 }}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
@ -35,7 +49,7 @@
|
||||||
<h3 class="font-bold text-lg">Your Cart</h3>
|
<h3 class="font-bold text-lg">Your Cart</h3>
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-sm"
|
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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
@ -44,7 +58,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-4">
|
<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">
|
<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">
|
<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" />
|
<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>
|
<p class="text-base-content/70 mb-4">Add some items to get started</p>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
on:click={() => cart.toggleCart()}
|
on:click={() => cart.toggle()}
|
||||||
>
|
>
|
||||||
Continue Shopping
|
Continue Shopping
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{#each $cart as item, i}
|
{#each $cartItems as item}
|
||||||
<div class="flex gap-4 pb-4 border-b">
|
<div class="flex gap-4 pb-4 border-b">
|
||||||
{#if item.product.thumbnail}
|
{#if item.thumbnail}
|
||||||
<img
|
<img
|
||||||
src={item.product.thumbnail}
|
src={item.thumbnail}
|
||||||
alt={item.product.title}
|
alt={item.title}
|
||||||
class="w-20 h-20 object-cover rounded-lg bg-base-200"
|
class="w-20 h-20 object-cover rounded-lg bg-base-200"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-medium">{item.product.title}</h3>
|
<h3 class="font-medium">{item.title}</h3>
|
||||||
<p class="text-sm text-base-content/70">{item.variant.title}</p>
|
<p class="text-sm text-base-content/70">
|
||||||
|
{#if item.variant && item.variant.title}
|
||||||
|
{item.variant.title}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-sm"
|
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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
@ -89,7 +107,7 @@
|
||||||
<div class="join">
|
<div class="join">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm join-item"
|
class="btn btn-sm join-item"
|
||||||
on:click={() => cart.updateQuantity(i, item.quantity - 1)}
|
on:click={() => cart.update(item.id, item.quantity - 1)}
|
||||||
>-</button>
|
>-</button>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -99,16 +117,16 @@
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
const value = parseInt(target.value) || 1;
|
const value = parseInt(target.value) || 1;
|
||||||
if (value >= 1) {
|
if (value >= 1) {
|
||||||
cart.updateQuantity(i, value);
|
cart.update(item.id, value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm join-item"
|
class="btn btn-sm join-item"
|
||||||
on:click={() => cart.updateQuantity(i, item.quantity + 1)}
|
on:click={() => cart.update(item.id, item.quantity + 1)}
|
||||||
>+</button>
|
>+</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="font-medium">{calculateItemTotal(item)} €</p>
|
<p class="font-medium">{calculateItemTotal(item)} {currencySymbol}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -117,11 +135,11 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $cart.length > 0}
|
{#if $cartItems.length > 0}
|
||||||
<div class="border-t p-4 space-y-4">
|
<div class="border-t p-4 space-y-4">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-lg font-bold">Total:</span>
|
<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>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
|
@ -130,7 +148,7 @@
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-sm w-full"
|
class="btn btn-ghost btn-sm w-full"
|
||||||
on:click={() => cart.toggleCart()}
|
on:click={() => cart.toggle()}
|
||||||
>
|
>
|
||||||
Continue Shopping
|
Continue Shopping
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cart } from '$lib/stores/cart';
|
import { cart } from '$lib/stores/cart';
|
||||||
import type { MedusaProduct, MedusaVariant } from '$lib/types/medusa';
|
import type { MedusaProduct, MedusaVariant } from '$lib/types/medusa';
|
||||||
|
import { region } from '$lib/stores/cart';
|
||||||
export let products: MedusaProduct[];
|
export let products: MedusaProduct[];
|
||||||
|
|
||||||
let selectedVariants = new Map<string, MedusaVariant>(
|
let selectedVariants = new Map<string, MedusaVariant>(
|
||||||
|
@ -10,6 +11,22 @@
|
||||||
function handleVariantChange(productId: string, variant: MedusaVariant) {
|
function handleVariantChange(productId: string, variant: MedusaVariant) {
|
||||||
selectedVariants.set(productId, variant);
|
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>
|
</script>
|
||||||
|
|
||||||
<section class="container mx-auto px-4 py-8">
|
<section class="container mx-auto px-4 py-8">
|
||||||
|
@ -38,14 +55,14 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#each product.variants as variant}
|
{#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}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-sm w-full"
|
class="btn btn-primary btn-sm w-full"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const variant = selectedVariants.get(product.id);
|
const variant = selectedVariants.get(product.id);
|
||||||
if (variant) cart.addToCart(product, variant);
|
if (variant) cart.add(variant.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add to cart
|
Add to cart
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cart } from '$lib/stores/cart';
|
import { cart } from '$lib/stores/cart';
|
||||||
import type { MedusaProduct, MedusaVariant, MedusaOptionValue } from '$lib/types/medusa';
|
import type { MedusaProduct, MedusaVariant, MedusaOptionValue } from '$lib/types/medusa';
|
||||||
|
import { region } from '$lib/stores/cart';
|
||||||
|
|
||||||
export let product: MedusaProduct;
|
export let product: MedusaProduct;
|
||||||
let selectedVariant: MedusaVariant = product.variants[0];
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:scroll={handleScroll}/>
|
<svelte:window on:scroll={handleScroll}/>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 py-8">
|
||||||
<div class="space-y-4">
|
<!-- Product Images -->
|
||||||
<img
|
<div class="relative">
|
||||||
src={product.images[currentImageIndex].url}
|
<div class="aspect-square overflow-hidden rounded-lg bg-base-200">
|
||||||
alt={product.title}
|
<img
|
||||||
class="aspect-square rounded-lg object-cover bg-base-200"
|
src={product.images[currentImageIndex]?.url || product.thumbnail}
|
||||||
/>
|
alt={product.title}
|
||||||
<div class="grid grid-cols-4 gap-2">
|
class="w-full h-full object-cover"
|
||||||
{#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>
|
</div>
|
||||||
</div>
|
{#if product.images.length > 1}
|
||||||
|
<div class="grid grid-cols-4 gap-2 mt-2">
|
||||||
<div class="space-y-6">
|
{#each product.images as image, i}
|
||||||
<div>
|
<button
|
||||||
<h1 class="text-3xl font-bold">{product.title}</h1>
|
class="aspect-square rounded-lg overflow-hidden bg-base-200 hover:opacity-75 transition-opacity"
|
||||||
<h1 class="text-3xl font-bold">{selectedVariant.calculated_price.calculated_amount} {selectedVariant.calculated_price.currency_code}</h1>
|
class:ring-2={currentImageIndex === i}
|
||||||
</div>
|
class:ring-primary={currentImageIndex === i}
|
||||||
|
on:click={() => currentImageIndex = i}
|
||||||
{#each product.options as option}
|
>
|
||||||
<div>
|
<img
|
||||||
<label class="label" for={`option-${option.id}`}>
|
src={image.url}
|
||||||
<span class="label-text">{option.title}</span>
|
alt={`${product.title} - View ${i + 1}`}
|
||||||
</label>
|
class="w-full h-full object-cover"
|
||||||
<select
|
/>
|
||||||
id={`option-${option.id}`}
|
</button>
|
||||||
class="select select-bordered w-full"
|
{/each}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
// import {MedusaClient} from 'sveltekit-medusa-client'
|
import {MedusaClient} from 'sveltekit-medusa-client'
|
||||||
// import { PUBLIC_MEDUSA_URL, PUBLIC_MEDUSA_KEY } from '$env/static/public'
|
import { PUBLIC_MEDUSA_URL, PUBLIC_MEDUSA_KEY } from '$env/static/public'
|
||||||
|
|
||||||
// export default new MedusaClient(PUBLIC_MEDUSA_URL, {
|
export default new MedusaClient(PUBLIC_MEDUSA_URL, {
|
||||||
// headers: {
|
headers: {
|
||||||
// 'x-publishable-api-key': PUBLIC_MEDUSA_KEY
|
'x-publishable-api-key': PUBLIC_MEDUSA_KEY
|
||||||
// },
|
},
|
||||||
// retry: 0,
|
retry: 0,
|
||||||
// timeout: 10000
|
timeout: 10000
|
||||||
// });
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import type { MedusaProduct, MedusaVariant } from '$lib/types/medusa';
|
import type { MedusaProduct, MedusaVariant } from '$lib/types/medusa';
|
||||||
import toast from 'svelte-french-toast';
|
import toast from 'svelte-french-toast';
|
||||||
|
import type { Region } from './region';
|
||||||
|
import { PUBLIC_MEDUSA_KEY } from '$env/static/public';
|
||||||
|
|
||||||
export interface CartItem {
|
export interface CartItem {
|
||||||
product: MedusaProduct;
|
product: MedusaProduct;
|
||||||
|
@ -8,75 +10,143 @@ export interface CartItem {
|
||||||
quantity: number;
|
quantity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCartStore() {
|
export const cartId = writable<string | null>(null);
|
||||||
const { subscribe, set, update } = writable<CartItem[]>([]);
|
export const cartItems = writable<any[]>([]);
|
||||||
const isOpenStore = writable<boolean>(false);
|
export const cartIsOpen = writable(false);
|
||||||
|
|
||||||
// Load initial state from localStorage
|
function getStoredCartId() {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window === 'undefined') return null;
|
||||||
const savedCart = localStorage.getItem('cart');
|
return localStorage.getItem('cart_id');
|
||||||
if (savedCart) {
|
}
|
||||||
set(JSON.parse(savedCart));
|
|
||||||
|
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) {
|
async function addItemToCart(variant_id: string, quantity = 1) {
|
||||||
toast(`${product.title} ${variant.title} added to cart`, {
|
const id = getStoredCartId();
|
||||||
icon: '🛍️',
|
if (!id) {
|
||||||
duration: 2000,
|
await initCart();
|
||||||
position: 'bottom-center',
|
return addItemToCart(variant_id, quantity);
|
||||||
style: 'background-color: #000; color: #fff;',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
subscribeToOpen: isOpenStore.subscribe,
|
setRegion: (region: Region) => set(region),
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const cart = createCartStore();
|
export const region = createRegionStore();
|
|
@ -4,6 +4,12 @@
|
||||||
import Footer from '$lib/components/Footer.svelte';
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { Toaster } from 'svelte-french-toast';
|
import { Toaster } from 'svelte-french-toast';
|
||||||
|
import { cart } from '$lib/stores/cart';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
cart.init();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
Loading…
Reference in a new issue