imprv 2
This commit is contained in:
parent
ca49ce7730
commit
f347f80f1b
@ -9,10 +9,11 @@
|
||||
import { authService, cartService } from '$lib/services/api';
|
||||
|
||||
let isOpen = false;
|
||||
let isUpdating = false;
|
||||
let updatingItemId: string | null = null;
|
||||
let updateTimeout: NodeJS.Timeout;
|
||||
let cartItems: CartItem[] = [];
|
||||
let isLoading = true;
|
||||
let showClearConfirmation = false;
|
||||
|
||||
onMount(async () => {
|
||||
await loadCart();
|
||||
@ -41,7 +42,7 @@
|
||||
// Debounced quantity update
|
||||
const handleQuantityChange = async (cartItemId: string, quantity: number) => {
|
||||
clearTimeout(updateTimeout);
|
||||
isUpdating = true;
|
||||
updatingItemId = cartItemId;
|
||||
|
||||
updateTimeout = setTimeout(async () => {
|
||||
try {
|
||||
@ -50,10 +51,21 @@
|
||||
} catch (error) {
|
||||
console.error('Failed to update quantity:', error);
|
||||
}
|
||||
isUpdating = false;
|
||||
updatingItemId = null;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// Handle increment/decrement buttons
|
||||
const updateQuantity = async (cartItemId: string, delta: number) => {
|
||||
const item = cartItems.find((item) => item.id === cartItemId);
|
||||
if (item) {
|
||||
const newQuantity = item.quantity + delta;
|
||||
if (newQuantity >= 1) {
|
||||
await handleQuantityChange(cartItemId, newQuantity);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickOutside = () => {
|
||||
isOpen = false;
|
||||
};
|
||||
@ -79,6 +91,7 @@
|
||||
|
||||
// Clear entire cart
|
||||
const clearEntireCart = async () => {
|
||||
showClearConfirmation = false;
|
||||
try {
|
||||
const currentUser = authService.getCurrentUser();
|
||||
if (currentUser) {
|
||||
@ -95,11 +108,22 @@
|
||||
isOpen = !isOpen;
|
||||
goto('/main');
|
||||
};
|
||||
|
||||
// Keyboard event handling
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isOpen) {
|
||||
isOpen = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Cart Icon Button with Badge -->
|
||||
<div class="relative">
|
||||
<Button on:click={() => (isOpen = !isOpen)} class="relative h-12">
|
||||
<Button on:click={() => (isOpen = !isOpen)} class="relative h-12" aria-label="Open Cart">
|
||||
<CartOutline size="lg" />
|
||||
{#if cartItems.length > 0}
|
||||
<span
|
||||
@ -124,23 +148,32 @@
|
||||
{#if isLoading}
|
||||
<p class="py-4 text-center text-gray-500">Loading cart...</p>
|
||||
{:else if cartItems.length === 0}
|
||||
<p class="py-4 text-center text-gray-500">Your cart is empty</p>
|
||||
<div class="flex flex-col items-center py-6">
|
||||
<img src="/empty-cart.svg" alt="Empty Cart" class="mb-4 h-32 w-32" />
|
||||
<p class="mb-4 text-center text-gray-500">Your cart is empty</p>
|
||||
<Button size="xl" on:click={goToShop}>
|
||||
Shop Now <ArrowRightOutline class="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="max-h-96 space-y-3 overflow-y-auto">
|
||||
<ul class="max-h-96 space-y-3 overflow-x-hidden overflow-y-scroll">
|
||||
{#each cartItems as item (item.id)}
|
||||
<li
|
||||
id="cart-item-{item.id}"
|
||||
class="flex items-center justify-between rounded-lg bg-gray-50 p-3 transition-all duration-200 hover:bg-gray-100"
|
||||
class=" flex items-center justify-between rounded-lg bg-gray-50 p-3 transition-all duration-200 hover:scale-105 hover:bg-gray-100"
|
||||
>
|
||||
<div class="flex-grow">
|
||||
<h3 class="font-semibold">{item.expand?.product?.title}</h3>
|
||||
<p class="text-sm text-gray-600">${item.expand?.product?.price.toFixed(2)} each</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
on:click={() => updateQuantity(item.id, -1)}
|
||||
class="rounded bg-gray-200 p-1 hover:bg-gray-300"
|
||||
>
|
||||
➖
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
value={item.quantity}
|
||||
@ -148,11 +181,12 @@
|
||||
on:input={(e) => handleQuantityChange(item.id, parseInt(e.currentTarget.value))}
|
||||
class="w-16 rounded border p-1 text-center"
|
||||
/>
|
||||
{#if isUpdating}
|
||||
<span
|
||||
class="absolute -top-1 right-0 h-2 w-2 animate-pulse rounded-full bg-blue-500"
|
||||
></span>
|
||||
{/if}
|
||||
<button
|
||||
on:click={() => updateQuantity(item.id, 1)}
|
||||
class="rounded bg-gray-200 p-1 hover:bg-gray-300"
|
||||
>
|
||||
➕
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
on:click={() => removeItem(item.id)}
|
||||
@ -173,13 +207,32 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-2">
|
||||
<Button href="/checkout" on:click={() => (isOpen = !isOpen)} class="w-full"
|
||||
>Proceed to Checkout</Button
|
||||
<Button href="/checkout" on:click={() => (isOpen = !isOpen)} class="w-full">
|
||||
Proceed to Checkout
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => (showClearConfirmation = true)}
|
||||
color="red"
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
>
|
||||
<Button on:click={clearEntireCart} color="red" class="w-full" variant="outline">
|
||||
Clear Cart
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Confirmation Dialog for Clearing Cart -->
|
||||
{#if showClearConfirmation}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div class="rounded-lg bg-white p-6 shadow-lg">
|
||||
<h3 class="mb-4 text-lg font-bold">Are you sure?</h3>
|
||||
<p class="mb-6">This will remove all items from your cart.</p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<Button on:click={() => (showClearConfirmation = false)} variant="outline">Cancel</Button>
|
||||
<Button on:click={clearEntireCart} color="red">Clear Cart</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -178,6 +178,19 @@ export const favoritesService = {
|
||||
} catch (error) {
|
||||
throw handleError(error);
|
||||
}
|
||||
},
|
||||
|
||||
clearFavorites: async (userId: string): Promise<void> => {
|
||||
try {
|
||||
const favorites = await pb.collection('favorites').getFullList({
|
||||
filter: `user = "${userId}"`
|
||||
});
|
||||
for (const favorite of favorites) {
|
||||
await pb.collection('favorites').delete(favorite.id);
|
||||
}
|
||||
} catch (error) {
|
||||
throw handleError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -24,8 +24,8 @@ export interface CartItem {
|
||||
user: string; // User ID
|
||||
product: string; // Product ID
|
||||
quantity: number;
|
||||
created: string;
|
||||
updated: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
expand?: {
|
||||
product: Product; // Expanded product details
|
||||
};
|
||||
@ -39,7 +39,7 @@ export interface Favorite {
|
||||
created: string;
|
||||
updated: string;
|
||||
expand?: {
|
||||
product: Product; // Expanded product details
|
||||
product?: Product[]; // Expanded product details
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,21 +0,0 @@
|
||||
const USER_KEY = 'user';
|
||||
const PASS_KEY = 'username';
|
||||
|
||||
export const login = (user: string, password: string) => {
|
||||
localStorage.setItem(USER_KEY, user);
|
||||
localStorage.setItem(PASS_KEY, password);
|
||||
};
|
||||
|
||||
export const logout = () => {
|
||||
localStorage.removeItem(USER_KEY);
|
||||
localStorage.removeItem(PASS_KEY);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
export const loggedIn = () => localStorage.getItem(USER_KEY) !== null;
|
||||
|
||||
export const name = () => localStorage.getItem(PASS_KEY) ?? '';
|
||||
|
||||
export const auth = () => localStorage.getItem(USER_KEY);
|
||||
|
||||
export default { login, logout, loggedIn, name, auth };
|
@ -1,47 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
// Define the type for a product
|
||||
export interface Product {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
imageUrl: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
// Initialize the cart store
|
||||
export const cart = writable<Product[]>([]);
|
||||
|
||||
// Add a product to the cart
|
||||
export const addToCart = (product: Product) => {
|
||||
cart.update((items) => {
|
||||
const existingItem = items.find((item) => item.id === product.id);
|
||||
if (existingItem) {
|
||||
// If the product already exists, increase the quantity
|
||||
return items.map((item) =>
|
||||
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
|
||||
);
|
||||
} else {
|
||||
// If the product doesn't exist, add it to the cart
|
||||
return [...items, { ...product, quantity: 1 }];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Remove a product from the cart
|
||||
export const removeFromCart = (productId: number) => {
|
||||
cart.update((items) => items.filter((item) => item.id !== productId));
|
||||
};
|
||||
|
||||
// Update the quantity of a product in the cart
|
||||
export const updateQuantity = (productId: number, quantity: number) => {
|
||||
cart.update((items) =>
|
||||
items.map((item) => (item.id === productId ? { ...item, quantity } : item))
|
||||
);
|
||||
};
|
||||
|
||||
// Clear the cart
|
||||
export const clearCart = () => {
|
||||
cart.set([]);
|
||||
};
|
@ -1,86 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
// Define the type for a product
|
||||
export interface Product {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
imageUrl: string;
|
||||
price: number; // Added price for cart calculations
|
||||
quantity?: number; // Optional quantity for cart items
|
||||
}
|
||||
|
||||
// Define the type for the cart and favorites
|
||||
export interface AppState {
|
||||
cart: Product[];
|
||||
favorites: Product[];
|
||||
}
|
||||
|
||||
// Initialize the store
|
||||
const initialState: AppState = {
|
||||
cart: [],
|
||||
favorites: []
|
||||
};
|
||||
|
||||
export const appStore = writable<AppState>(initialState);
|
||||
export const favorites = writable<Product[]>([]);
|
||||
|
||||
// Add a product to the cart
|
||||
export const addToCart = (product: Product) => {
|
||||
appStore.update((state) => {
|
||||
const existingItem = state.cart.find((item) => item.id === product.id);
|
||||
if (existingItem) {
|
||||
// If the product already exists, increase the quantity
|
||||
return {
|
||||
...state,
|
||||
cart: state.cart.map((item) =>
|
||||
item.id === product.id ? { ...item, quantity: (item.quantity || 1) + 1 } : item
|
||||
)
|
||||
};
|
||||
} else {
|
||||
// If the product doesn't exist, add it to the cart with quantity 1
|
||||
return { ...state, cart: [...state.cart, { ...product, quantity: 1 }] };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Remove a product from the cart
|
||||
export const removeFromCart = (productId: number) => {
|
||||
appStore.update((state) => ({
|
||||
...state,
|
||||
cart: state.cart.filter((item) => item.id !== productId)
|
||||
}));
|
||||
};
|
||||
|
||||
// Update the quantity of a product in the cart
|
||||
export const updateCartQuantity = (productId: number, quantity: number) => {
|
||||
appStore.update((state) => ({
|
||||
...state,
|
||||
cart: state.cart.map((item) => (item.id === productId ? { ...item, quantity } : item))
|
||||
}));
|
||||
};
|
||||
|
||||
// Add a product to favorites
|
||||
export const addToFavorites = (product: Product) => {
|
||||
favorites.update((items) => {
|
||||
if (!items.some((item) => item.id === product.id)) {
|
||||
return [...items, product];
|
||||
}
|
||||
return items;
|
||||
});
|
||||
};
|
||||
|
||||
// Remove a product from favorites
|
||||
export const removeFromFavorites = (productId: number) => {
|
||||
favorites.update((items) => items.filter((item) => item.id !== productId));
|
||||
};
|
||||
|
||||
// Clear the cart
|
||||
export const clearCart = () => {
|
||||
appStore.update((state) => ({ ...state, cart: [] }));
|
||||
};
|
||||
|
||||
// Clear favorites
|
||||
export const clearFavorites = () => {
|
||||
favorites.update((state) => ({ ...state, favorites: [] }));
|
||||
};
|
@ -1,193 +1,170 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { cartService, orderService, authService } from '$lib/services/api';
|
||||
import { Button, Input, Card, Badge } from 'flowbite-svelte';
|
||||
import { ArrowRightOutline, CheckCircleOutline } from 'flowbite-svelte-icons';
|
||||
import type { CartItem } from '$lib/services/types';
|
||||
import { goto } from '$app/navigation';
|
||||
import { clearCart, cart, type Product } from '$lib/stores/cartStore';
|
||||
import { Card, Input, Button, Alert } from 'flowbite-svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
// Form state
|
||||
let formData = {
|
||||
name: '',
|
||||
email: '',
|
||||
address: '',
|
||||
paymentMethod: 'credit_card'
|
||||
};
|
||||
// State variables
|
||||
let cartItems: CartItem[] = [];
|
||||
let total = 0;
|
||||
let name = '';
|
||||
let email = '';
|
||||
let address = '';
|
||||
let paymentMethod = 'credit_card';
|
||||
let isLoading = false;
|
||||
|
||||
let isProcessing = false;
|
||||
let error = '';
|
||||
let success = '';
|
||||
|
||||
// Calculate the total price
|
||||
$: total = $cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||
|
||||
// Form validation
|
||||
$: isFormValid =
|
||||
formData.name.length > 0 &&
|
||||
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) &&
|
||||
formData.address.length > 0;
|
||||
|
||||
// Handle form submission
|
||||
const handleCheckout = async () => {
|
||||
if (!isFormValid) {
|
||||
error = 'Please fill in all required fields correctly.';
|
||||
// Fetch the user's cart on mount
|
||||
onMount(async () => {
|
||||
const user = authService.getCurrentUser();
|
||||
if (!user) {
|
||||
goto('/login'); // Redirect to login if not authenticated
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessing = true;
|
||||
error = '';
|
||||
try {
|
||||
cartItems = await cartService.getCart(user.id);
|
||||
total = cartItems.reduce(
|
||||
(sum, item) => sum + (item.expand?.product.price || 0) * item.quantity,
|
||||
0
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch cart:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const checkoutRequest = {
|
||||
...formData,
|
||||
items: $cart
|
||||
};
|
||||
// Handle form submission
|
||||
const handleCheckout = async () => {
|
||||
const user = authService.getCurrentUser();
|
||||
if (!user) {
|
||||
goto('/login'); // Redirect to login if not authenticated
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
clearCart();
|
||||
// Prepare order items
|
||||
const items = cartItems.map((item) => ({
|
||||
product: item.product,
|
||||
quantity: item.quantity
|
||||
}));
|
||||
|
||||
goto(`/order-confirmation/12345`);
|
||||
} catch (err) {
|
||||
error = 'Failed to process checkout. Please try again.';
|
||||
// Create the order
|
||||
const order = await orderService.createOrder(user.id, items, total);
|
||||
|
||||
// Clear the cart
|
||||
await cartService.clearCart(user.id);
|
||||
|
||||
// Redirect to order confirmation page
|
||||
goto(`/order-confirmation/${order.id}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to place order:', error);
|
||||
alert('Failed to place order. Please try again.');
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
let modalOpen = false;
|
||||
let selectedImage = {
|
||||
url: '',
|
||||
title: ''
|
||||
};
|
||||
|
||||
const openImageModal = (product: Product) => {
|
||||
selectedImage = {
|
||||
url: product.imageUrl,
|
||||
title: product.title
|
||||
};
|
||||
modalOpen = true;
|
||||
};
|
||||
|
||||
const closeImageModal = () => {
|
||||
modalOpen = false;
|
||||
};
|
||||
onMount(() => {
|
||||
const elements = document.querySelectorAll('.fade-in');
|
||||
elements.forEach((el) => {
|
||||
el.classList.add('visible');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="mx-auto max-w-4xl p-6">
|
||||
<h1 class="mb-6 text-3xl font-bold">Checkout</h1>
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
<!-- Hero Section -->
|
||||
<section class="mb-8 rounded-lg bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
<h1 class="mb-4 text-5xl font-bold text-gray-900">Checkout</h1>
|
||||
<p class="mb-8 text-xl text-gray-600">
|
||||
Review your order and enter your details to complete the purchase.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if $cart.length === 0}
|
||||
<Alert color="red" class="mb-6">
|
||||
Your cart is empty. Please add items before checking out.
|
||||
</Alert>
|
||||
{:else}
|
||||
<div class="flex gap-10">
|
||||
<!-- Order Summary -->
|
||||
<!-- Cart Summary -->
|
||||
<Card class="mb-6">
|
||||
<h2 class="mb-4 text-xl font-semibold">Order Summary</h2>
|
||||
<ul class="space-y-3">
|
||||
{#each $cart as item}
|
||||
<li class="flex items-center justify-between rounded-lg bg-gray-50 p-3">
|
||||
<div class="flex">
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<img
|
||||
src={item.imageUrl}
|
||||
alt="item"
|
||||
class="me-2 h-20 w-20"
|
||||
on:click={() => openImageModal(item)}
|
||||
on:keydown={(e) => e.key === 'Enter' && openImageModal(item)}
|
||||
tabindex="0"
|
||||
/>
|
||||
<h2 class="mb-4 text-2xl font-bold">Order Summary</h2>
|
||||
<ul class="space-y-4">
|
||||
{#each cartItems as item}
|
||||
<li class="flex items-center justify-between border-b pb-4">
|
||||
<div>
|
||||
<h3 class="font-semibold">{item.title}</h3>
|
||||
<p class="text-sm text-gray-600">Quantity: {item.quantity}</p>
|
||||
<h3 class="text-xl font-semibold text-gray-900">{item.expand?.product.title}</h3>
|
||||
<p class="text-gray-600">x{item.quantity}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-lg font-medium">
|
||||
${(item.price * item.quantity).toFixed(2)}
|
||||
</span>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
${((item.expand?.product.price || 0) * item.quantity).toFixed(2)}
|
||||
</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="mt-4 border-t pt-4">
|
||||
<div class="flex justify-between text-lg">
|
||||
<strong>Total:</strong>
|
||||
<span class="font-bold">${total.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="mt-6 border-t pt-4">
|
||||
<p class="text-2xl font-bold text-gray-900">Total: ${total.toFixed(2)}</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Customer Details Form -->
|
||||
<Card class="mb-6">
|
||||
<h2 class="mb-4 text-xl font-semibold">Customer Details</h2>
|
||||
<h2 class="mb-4 text-2xl font-bold">Customer Details</h2>
|
||||
<form on:submit|preventDefault={handleCheckout} class="space-y-4">
|
||||
<Input
|
||||
label="Full Name"
|
||||
bind:value={formData.name}
|
||||
placeholder="John Doe"
|
||||
required
|
||||
error={!formData.name && 'Name is required'}
|
||||
/>
|
||||
|
||||
<Input label="Full Name" bind:value={name} placeholder="John Doe" required />
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
bind:value={formData.email}
|
||||
bind:value={email}
|
||||
placeholder="john@example.com"
|
||||
required
|
||||
error={formData.email &&
|
||||
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) &&
|
||||
'Please enter a valid email'}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Shipping Address"
|
||||
bind:value={formData.address}
|
||||
bind:value={address}
|
||||
placeholder="123 Main St, City, Country"
|
||||
required
|
||||
error={!formData.address && 'Address is required'}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block font-medium">Payment Method</label>
|
||||
<div class="space-y-2">
|
||||
{#each [{ value: 'credit_card', label: 'Credit Card', icon: '💳' }, { value: 'paypal', label: 'PayPal', icon: '🅿️' }, { value: 'cash_on_delivery', label: 'Cash on Delivery', icon: '💵' }] as method}
|
||||
<label
|
||||
class="flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors hover:bg-gray-50"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={formData.paymentMethod}
|
||||
value={method.value}
|
||||
class="h-4 w-4"
|
||||
<Input
|
||||
label="Payment Method"
|
||||
bind:value={paymentMethod}
|
||||
options={[
|
||||
{ value: 'credit_card', name: 'Credit Card' },
|
||||
{ value: 'paypal', name: 'PayPal' },
|
||||
{ value: 'cash_on_delivery', name: 'Cash on Delivery' }
|
||||
]}
|
||||
required
|
||||
/>
|
||||
<span>{method.icon}</span>
|
||||
<span>{method.label}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<Alert color="red">
|
||||
{error}
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<Alert color="green">
|
||||
{success}
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<Button type="submit" color="blue" class="w-full" disabled={isProcessing || !isFormValid}>
|
||||
{#if isProcessing}
|
||||
<span class="inline-block animate-spin">⌛</span>
|
||||
Processing...
|
||||
{:else}
|
||||
Place Order (${total.toFixed(2)})
|
||||
{/if}
|
||||
<Button type="submit" color="blue" class="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Placing Order...' : 'Place Order'}
|
||||
<ArrowRightOutline class="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Guarantee Section -->
|
||||
<section class="rounded-lg bg-gradient-to-r from-pink-100 to-purple-100 py-12 text-center">
|
||||
<CheckCircleOutline class="mx-auto mb-4 h-12 w-12 text-purple-500" />
|
||||
<h2 class="mb-4 text-2xl font-bold text-gray-900">Satisfaction Guaranteed</h2>
|
||||
<p class="text-gray-600">
|
||||
We stand by our products with a 100% satisfaction guarantee. If you're not happy, we'll make
|
||||
it right.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
/* Add custom animations */
|
||||
.fade-in {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition:
|
||||
opacity 0.6s ease-out,
|
||||
transform 0.6s ease-out;
|
||||
}
|
||||
|
||||
.fade-in.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
|
@ -1,33 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { favorites, removeFromFavorites, clearFavorites } from '$lib/stores/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { favoritesService, authService } from '$lib/services/api';
|
||||
import { Button, Card } from 'flowbite-svelte';
|
||||
import { TrashBinOutline } from 'flowbite-svelte-icons';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Favorite } from '$lib/services/types';
|
||||
|
||||
let favorites: Favorite[] = [];
|
||||
let isLoading = true;
|
||||
|
||||
const fetchFavorites = async () => {
|
||||
const user = authService.getCurrentUser();
|
||||
if (!user) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchedFavorites = await favoritesService.getFavorites(user.id);
|
||||
favorites = fetchedFavorites.map((favorite: Favorite) => ({
|
||||
...favorite,
|
||||
expand: favorite.expand ?? { product: [] }
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch favorites:', error);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const removeFromFavorites = async (favoriteId: string) => {
|
||||
try {
|
||||
await favoritesService.removeFromFavorites(favoriteId);
|
||||
favorites = favorites.filter((favorite) => favorite.id !== favoriteId);
|
||||
} catch (error) {
|
||||
console.error('Failed to remove favorite:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
fetchFavorites();
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="p-6">
|
||||
<h1 class="mb-6 text-3xl font-bold">Favorites</h1>
|
||||
{#if $favorites.length > 0}
|
||||
<ul class="space-y-4">
|
||||
{#each $favorites as item}
|
||||
<li class="flex items-center justify-between border-b pb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">{item.title}</h2>
|
||||
<p class="text-gray-600">{item.description}</p>
|
||||
</div>
|
||||
<button
|
||||
on:click={() => removeFromFavorites(item.id)}
|
||||
class="text-red-500 hover:text-red-700"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<button
|
||||
on:click={clearFavorites}
|
||||
class="mt-4 w-full rounded bg-red-500 p-2 text-white hover:bg-red-600"
|
||||
>
|
||||
Clear Favorites
|
||||
</button>
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
<h1 class="mb-8 text-4xl font-bold text-gray-900">Your Favorites</h1>
|
||||
|
||||
{#if isLoading}
|
||||
<p class="text-gray-600">Loading your favorites...</p>
|
||||
{:else if favorites.length === 0}
|
||||
<p class="text-gray-600">You have no favorite products yet.</p>
|
||||
{:else}
|
||||
<p class="text-gray-600">You have no favorite products.</p>
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each favorites as favorite}
|
||||
<Card class="h-full">
|
||||
<div class="flex h-full flex-col">
|
||||
<img
|
||||
src={favorite.expand?.product?.[0]?.imageUrl || '/fallback-image.jpg'}
|
||||
alt={favorite.expand?.product?.[0]?.title || 'Product image'}
|
||||
class="h-48 w-full rounded-t-lg object-cover"
|
||||
/>
|
||||
<div class="flex-grow p-4">
|
||||
<h2 class="mb-2 text-xl font-bold text-gray-900">
|
||||
{favorite.expand?.product?.[0]?.title || 'Unnamed Product'}
|
||||
</h2>
|
||||
<p class="mb-4 text-gray-600">
|
||||
{favorite.expand?.product?.[0]?.description || 'No description available.'}
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
${favorite.expand?.product?.[0]?.price || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<Button color="red" class="w-full" on:click={() => removeFromFavorites(favorite.id)}>
|
||||
<TrashBinOutline class="mr-2 h-5 w-5" />
|
||||
Remove from Favorites
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
@ -1,53 +1,74 @@
|
||||
<script lang="ts">
|
||||
import { favorites } from '$lib/stores/store';
|
||||
import { authService, orderService } from '$lib/services/api';
|
||||
import { Card, Button, Badge, Input, Textarea } from 'flowbite-svelte';
|
||||
import { StarSolid, CheckCircleOutline, PenOutline } from 'flowbite-svelte-icons';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// Example user data
|
||||
const user = {
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
address: '123 Main St, City, Country'
|
||||
};
|
||||
// User data
|
||||
let user: { id: string; name: string; email: string; address?: string } | null = null;
|
||||
|
||||
// Example order history
|
||||
const orders = [
|
||||
{
|
||||
id: 1,
|
||||
date: '2023-10-01',
|
||||
items: ['Tulip Bouquet', 'Rose Bouquet'],
|
||||
total: 60,
|
||||
status: 'Delivered'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: '2023-09-25',
|
||||
items: ['Sunflower Bouquet'],
|
||||
total: 30,
|
||||
status: 'Shipped'
|
||||
}
|
||||
];
|
||||
// Order history
|
||||
let orders: Array<{
|
||||
id: string;
|
||||
created: string;
|
||||
items: Array<{ product: string; quantity: number }>;
|
||||
total: number;
|
||||
status: string;
|
||||
}> = [];
|
||||
|
||||
// State for editing profile
|
||||
let isEditing = false;
|
||||
let name = user.name;
|
||||
let email = user.email;
|
||||
let address = user.address;
|
||||
let name = '';
|
||||
let email = '';
|
||||
let address = '';
|
||||
|
||||
// Fetch user and order data on component mount
|
||||
onMount(async () => {
|
||||
// Get the current user
|
||||
user = authService.getCurrentUser();
|
||||
if (user) {
|
||||
name = user.name;
|
||||
email = user.email;
|
||||
address = user.address || '';
|
||||
|
||||
// Fetch the user's orders
|
||||
try {
|
||||
const userOrders = await orderService.getOrders(user.id);
|
||||
orders = userOrders.map((order) => ({
|
||||
id: order.id,
|
||||
created: order.created,
|
||||
items: order.items,
|
||||
total: order.total,
|
||||
status: order.status
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch orders:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle profile update
|
||||
const updateProfile = () => {
|
||||
const updateProfile = async () => {
|
||||
if (user) {
|
||||
try {
|
||||
// Update the user's profile (this is a placeholder; you'll need to implement the API call)
|
||||
// Example: await authService.updateProfile(user.id, { name, email, address });
|
||||
user.name = name;
|
||||
user.email = email;
|
||||
user.address = address;
|
||||
isEditing = false;
|
||||
// You could add an API call here to update the user's profile
|
||||
console.log('Profile updated successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to update profile:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Welcome Section -->
|
||||
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-12">
|
||||
<div class="container mx-auto px-4">
|
||||
<h1 class="text-4xl font-bold text-gray-900">Welcome, {user.name}!</h1>
|
||||
<h1 class="text-4xl font-bold text-gray-900">Welcome, {user?.name || user?.email}!</h1>
|
||||
<p class="text-lg text-gray-600">Manage your orders, favorites, and account settings here.</p>
|
||||
</div>
|
||||
</section>
|
||||
@ -58,34 +79,40 @@
|
||||
<div class="lg:col-span-2">
|
||||
<h2 class="mb-6 text-2xl font-bold text-gray-900">Order History</h2>
|
||||
<div class="space-y-4">
|
||||
{#if orders.length > 0}
|
||||
{#each orders as order}
|
||||
<Card class="p-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-xl font-bold text-gray-900">Order #{order.id}</h3>
|
||||
<Badge color={order.status === 'Delivered' ? 'green' : 'blue'}>
|
||||
<Badge color={order.status === 'delivered' ? 'green' : 'blue'}>
|
||||
{order.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="mb-2 text-gray-600">
|
||||
<strong>Date:</strong>
|
||||
{order.date}
|
||||
{new Date(order.created).toLocaleDateString()}
|
||||
</p>
|
||||
<p class="mb-2 text-gray-600">
|
||||
<strong>Items:</strong>
|
||||
{order.items.join(', ')}
|
||||
{#each order.items as item}
|
||||
{item.product} (x{item.quantity}){@const isLast =
|
||||
item === order.items[order.items.length - 1]}{#if !isLast},
|
||||
{/if}
|
||||
{/each}
|
||||
</p>
|
||||
<p class="text-gray-600">
|
||||
<strong>Total:</strong> ${order.total.toFixed(2)}
|
||||
</p>
|
||||
</Card>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="text-gray-600">No orders found.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Settings -->
|
||||
<div class="space-y-8">
|
||||
<!-- Account Settings -->
|
||||
<div>
|
||||
<h2 class="mb-6 text-2xl font-bold text-gray-900">Account Settings</h2>
|
||||
<Card class="p-6">
|
||||
{#if isEditing}
|
||||
@ -100,9 +127,9 @@
|
||||
</form>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
<p class="text-gray-600"><strong>Name:</strong> {user.name}</p>
|
||||
<p class="text-gray-600"><strong>Email:</strong> {user.email}</p>
|
||||
<p class="text-gray-600"><strong>Address:</strong> {user.address}</p>
|
||||
<p class="text-gray-600"><strong>Name:</strong> {user?.name}</p>
|
||||
<p class="text-gray-600"><strong>Email:</strong> {user?.email}</p>
|
||||
<p class="text-gray-600"><strong>Address:</strong> {user?.address || 'Not provided'}</p>
|
||||
<Button on:click={() => (isEditing = true)}>
|
||||
<PenOutline class="mr-2 h-5 w-5" />
|
||||
Edit Profile
|
||||
@ -112,4 +139,3 @@
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user