imprv 2
This commit is contained in:
parent
ca49ce7730
commit
f347f80f1b
@ -9,10 +9,11 @@
|
|||||||
import { authService, cartService } from '$lib/services/api';
|
import { authService, cartService } from '$lib/services/api';
|
||||||
|
|
||||||
let isOpen = false;
|
let isOpen = false;
|
||||||
let isUpdating = false;
|
let updatingItemId: string | null = null;
|
||||||
let updateTimeout: NodeJS.Timeout;
|
let updateTimeout: NodeJS.Timeout;
|
||||||
let cartItems: CartItem[] = [];
|
let cartItems: CartItem[] = [];
|
||||||
let isLoading = true;
|
let isLoading = true;
|
||||||
|
let showClearConfirmation = false;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadCart();
|
await loadCart();
|
||||||
@ -41,7 +42,7 @@
|
|||||||
// Debounced quantity update
|
// Debounced quantity update
|
||||||
const handleQuantityChange = async (cartItemId: string, quantity: number) => {
|
const handleQuantityChange = async (cartItemId: string, quantity: number) => {
|
||||||
clearTimeout(updateTimeout);
|
clearTimeout(updateTimeout);
|
||||||
isUpdating = true;
|
updatingItemId = cartItemId;
|
||||||
|
|
||||||
updateTimeout = setTimeout(async () => {
|
updateTimeout = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
@ -50,10 +51,21 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update quantity:', error);
|
console.error('Failed to update quantity:', error);
|
||||||
}
|
}
|
||||||
isUpdating = false;
|
updatingItemId = null;
|
||||||
}, 500);
|
}, 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 = () => {
|
const handleClickOutside = () => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
};
|
};
|
||||||
@ -79,6 +91,7 @@
|
|||||||
|
|
||||||
// Clear entire cart
|
// Clear entire cart
|
||||||
const clearEntireCart = async () => {
|
const clearEntireCart = async () => {
|
||||||
|
showClearConfirmation = false;
|
||||||
try {
|
try {
|
||||||
const currentUser = authService.getCurrentUser();
|
const currentUser = authService.getCurrentUser();
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
@ -95,11 +108,22 @@
|
|||||||
isOpen = !isOpen;
|
isOpen = !isOpen;
|
||||||
goto('/main');
|
goto('/main');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keyboard event handling
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape' && isOpen) {
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Cart Icon Button with Badge -->
|
<!-- Cart Icon Button with Badge -->
|
||||||
<div class="relative">
|
<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" />
|
<CartOutline size="lg" />
|
||||||
{#if cartItems.length > 0}
|
{#if cartItems.length > 0}
|
||||||
<span
|
<span
|
||||||
@ -124,23 +148,32 @@
|
|||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<p class="py-4 text-center text-gray-500">Loading cart...</p>
|
<p class="py-4 text-center text-gray-500">Loading cart...</p>
|
||||||
{:else if cartItems.length === 0}
|
{: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}>
|
<Button size="xl" on:click={goToShop}>
|
||||||
Shop Now <ArrowRightOutline class="ml-2 h-5 w-5" />
|
Shop Now <ArrowRightOutline class="ml-2 h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
{:else}
|
{: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)}
|
{#each cartItems as item (item.id)}
|
||||||
<li
|
<li
|
||||||
id="cart-item-{item.id}"
|
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">
|
<div class="flex-grow">
|
||||||
<h3 class="font-semibold">{item.expand?.product?.title}</h3>
|
<h3 class="font-semibold">{item.expand?.product?.title}</h3>
|
||||||
<p class="text-sm text-gray-600">${item.expand?.product?.price.toFixed(2)} each</p>
|
<p class="text-sm text-gray-600">${item.expand?.product?.price.toFixed(2)} each</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={item.quantity}
|
value={item.quantity}
|
||||||
@ -148,11 +181,12 @@
|
|||||||
on:input={(e) => handleQuantityChange(item.id, parseInt(e.currentTarget.value))}
|
on:input={(e) => handleQuantityChange(item.id, parseInt(e.currentTarget.value))}
|
||||||
class="w-16 rounded border p-1 text-center"
|
class="w-16 rounded border p-1 text-center"
|
||||||
/>
|
/>
|
||||||
{#if isUpdating}
|
<button
|
||||||
<span
|
on:click={() => updateQuantity(item.id, 1)}
|
||||||
class="absolute -top-1 right-0 h-2 w-2 animate-pulse rounded-full bg-blue-500"
|
class="rounded bg-gray-200 p-1 hover:bg-gray-300"
|
||||||
></span>
|
>
|
||||||
{/if}
|
➕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
on:click={() => removeItem(item.id)}
|
on:click={() => removeItem(item.id)}
|
||||||
@ -173,13 +207,32 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 space-y-2">
|
<div class="mt-4 space-y-2">
|
||||||
<Button href="/checkout" on:click={() => (isOpen = !isOpen)} class="w-full"
|
<Button href="/checkout" on:click={() => (isOpen = !isOpen)} class="w-full">
|
||||||
>Proceed to Checkout</Button
|
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
|
Clear Cart
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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) {
|
} catch (error) {
|
||||||
throw handleError(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
|
user: string; // User ID
|
||||||
product: string; // Product ID
|
product: string; // Product ID
|
||||||
quantity: number;
|
quantity: number;
|
||||||
created: string;
|
created?: string;
|
||||||
updated: string;
|
updated?: string;
|
||||||
expand?: {
|
expand?: {
|
||||||
product: Product; // Expanded product details
|
product: Product; // Expanded product details
|
||||||
};
|
};
|
||||||
@ -39,7 +39,7 @@ export interface Favorite {
|
|||||||
created: string;
|
created: string;
|
||||||
updated: string;
|
updated: string;
|
||||||
expand?: {
|
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">
|
<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 { 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
|
// State variables
|
||||||
let formData = {
|
let cartItems: CartItem[] = [];
|
||||||
name: '',
|
let total = 0;
|
||||||
email: '',
|
let name = '';
|
||||||
address: '',
|
let email = '';
|
||||||
paymentMethod: 'credit_card'
|
let address = '';
|
||||||
};
|
let paymentMethod = 'credit_card';
|
||||||
|
let isLoading = false;
|
||||||
|
|
||||||
let isProcessing = false;
|
// Fetch the user's cart on mount
|
||||||
let error = '';
|
onMount(async () => {
|
||||||
let success = '';
|
const user = authService.getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
// Calculate the total price
|
goto('/login'); // Redirect to login if not authenticated
|
||||||
$: 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.';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isProcessing = true;
|
try {
|
||||||
error = '';
|
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 = {
|
// Handle form submission
|
||||||
...formData,
|
const handleCheckout = async () => {
|
||||||
items: $cart
|
const user = authService.getCurrentUser();
|
||||||
};
|
if (!user) {
|
||||||
|
goto('/login'); // Redirect to login if not authenticated
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clearCart();
|
// Prepare order items
|
||||||
|
const items = cartItems.map((item) => ({
|
||||||
|
product: item.product,
|
||||||
|
quantity: item.quantity
|
||||||
|
}));
|
||||||
|
|
||||||
goto(`/order-confirmation/12345`);
|
// Create the order
|
||||||
} catch (err) {
|
const order = await orderService.createOrder(user.id, items, total);
|
||||||
error = 'Failed to process checkout. Please try again.';
|
|
||||||
|
// 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 {
|
} finally {
|
||||||
isProcessing = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let modalOpen = false;
|
onMount(() => {
|
||||||
let selectedImage = {
|
const elements = document.querySelectorAll('.fade-in');
|
||||||
url: '',
|
elements.forEach((el) => {
|
||||||
title: ''
|
el.classList.add('visible');
|
||||||
};
|
});
|
||||||
|
});
|
||||||
const openImageModal = (product: Product) => {
|
|
||||||
selectedImage = {
|
|
||||||
url: product.imageUrl,
|
|
||||||
title: product.title
|
|
||||||
};
|
|
||||||
modalOpen = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeImageModal = () => {
|
|
||||||
modalOpen = false;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="mx-auto max-w-4xl p-6">
|
<main class="container mx-auto px-4 py-8">
|
||||||
<h1 class="mb-6 text-3xl font-bold">Checkout</h1>
|
<!-- 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}
|
<!-- Cart Summary -->
|
||||||
<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 -->
|
|
||||||
<Card class="mb-6">
|
<Card class="mb-6">
|
||||||
<h2 class="mb-4 text-xl font-semibold">Order Summary</h2>
|
<h2 class="mb-4 text-2xl font-bold">Order Summary</h2>
|
||||||
<ul class="space-y-3">
|
<ul class="space-y-4">
|
||||||
{#each $cart as item}
|
{#each cartItems as item}
|
||||||
<li class="flex items-center justify-between rounded-lg bg-gray-50 p-3">
|
<li class="flex items-center justify-between border-b pb-4">
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-semibold">{item.title}</h3>
|
<h3 class="text-xl font-semibold text-gray-900">{item.expand?.product.title}</h3>
|
||||||
<p class="text-sm text-gray-600">Quantity: {item.quantity}</p>
|
<p class="text-gray-600">x{item.quantity}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p class="text-2xl font-bold text-gray-900">
|
||||||
<span class="text-lg font-medium">
|
${((item.expand?.product.price || 0) * item.quantity).toFixed(2)}
|
||||||
${(item.price * item.quantity).toFixed(2)}
|
</p>
|
||||||
</span>
|
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
<div class="mt-4 border-t pt-4">
|
<div class="mt-6 border-t pt-4">
|
||||||
<div class="flex justify-between text-lg">
|
<p class="text-2xl font-bold text-gray-900">Total: ${total.toFixed(2)}</p>
|
||||||
<strong>Total:</strong>
|
|
||||||
<span class="font-bold">${total.toFixed(2)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Customer Details Form -->
|
<!-- Customer Details Form -->
|
||||||
<Card class="mb-6">
|
<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">
|
<form on:submit|preventDefault={handleCheckout} class="space-y-4">
|
||||||
<Input
|
<Input label="Full Name" bind:value={name} placeholder="John Doe" required />
|
||||||
label="Full Name"
|
|
||||||
bind:value={formData.name}
|
|
||||||
placeholder="John Doe"
|
|
||||||
required
|
|
||||||
error={!formData.name && 'Name is required'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Email"
|
label="Email"
|
||||||
type="email"
|
type="email"
|
||||||
bind:value={formData.email}
|
bind:value={email}
|
||||||
placeholder="john@example.com"
|
placeholder="john@example.com"
|
||||||
required
|
required
|
||||||
error={formData.email &&
|
|
||||||
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) &&
|
|
||||||
'Please enter a valid email'}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Shipping Address"
|
label="Shipping Address"
|
||||||
bind:value={formData.address}
|
bind:value={address}
|
||||||
placeholder="123 Main St, City, Country"
|
placeholder="123 Main St, City, Country"
|
||||||
required
|
required
|
||||||
error={!formData.address && 'Address is required'}
|
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
<div>
|
label="Payment Method"
|
||||||
<label class="mb-2 block font-medium">Payment Method</label>
|
bind:value={paymentMethod}
|
||||||
<div class="space-y-2">
|
options={[
|
||||||
{#each [{ value: 'credit_card', label: 'Credit Card', icon: '💳' }, { value: 'paypal', label: 'PayPal', icon: '🅿️' }, { value: 'cash_on_delivery', label: 'Cash on Delivery', icon: '💵' }] as method}
|
{ value: 'credit_card', name: 'Credit Card' },
|
||||||
<label
|
{ value: 'paypal', name: 'PayPal' },
|
||||||
class="flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors hover:bg-gray-50"
|
{ value: 'cash_on_delivery', name: 'Cash on Delivery' }
|
||||||
>
|
]}
|
||||||
<input
|
required
|
||||||
type="radio"
|
|
||||||
bind:group={formData.paymentMethod}
|
|
||||||
value={method.value}
|
|
||||||
class="h-4 w-4"
|
|
||||||
/>
|
/>
|
||||||
<span>{method.icon}</span>
|
<Button type="submit" color="blue" class="w-full" disabled={isLoading}>
|
||||||
<span>{method.label}</span>
|
{isLoading ? 'Placing Order...' : 'Place Order'}
|
||||||
</label>
|
<ArrowRightOutline class="ml-2 h-5 w-5" />
|
||||||
{/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>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</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>
|
</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">
|
<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>
|
</script>
|
||||||
|
|
||||||
<main class="p-6">
|
<main class="container mx-auto px-4 py-8">
|
||||||
<h1 class="mb-6 text-3xl font-bold">Favorites</h1>
|
<h1 class="mb-8 text-4xl font-bold text-gray-900">Your Favorites</h1>
|
||||||
{#if $favorites.length > 0}
|
|
||||||
<ul class="space-y-4">
|
{#if isLoading}
|
||||||
{#each $favorites as item}
|
<p class="text-gray-600">Loading your favorites...</p>
|
||||||
<li class="flex items-center justify-between border-b pb-4">
|
{:else if favorites.length === 0}
|
||||||
<div>
|
<p class="text-gray-600">You have no favorite products yet.</p>
|
||||||
<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>
|
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
@ -1,53 +1,74 @@
|
|||||||
<script lang="ts">
|
<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 { Card, Button, Badge, Input, Textarea } from 'flowbite-svelte';
|
||||||
import { StarSolid, CheckCircleOutline, PenOutline } from 'flowbite-svelte-icons';
|
import { StarSolid, CheckCircleOutline, PenOutline } from 'flowbite-svelte-icons';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
// Example user data
|
// User data
|
||||||
const user = {
|
let user: { id: string; name: string; email: string; address?: string } | null = null;
|
||||||
name: 'John Doe',
|
|
||||||
email: 'john.doe@example.com',
|
|
||||||
address: '123 Main St, City, Country'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Example order history
|
// Order history
|
||||||
const orders = [
|
let orders: Array<{
|
||||||
{
|
id: string;
|
||||||
id: 1,
|
created: string;
|
||||||
date: '2023-10-01',
|
items: Array<{ product: string; quantity: number }>;
|
||||||
items: ['Tulip Bouquet', 'Rose Bouquet'],
|
total: number;
|
||||||
total: 60,
|
status: string;
|
||||||
status: 'Delivered'
|
}> = [];
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
date: '2023-09-25',
|
|
||||||
items: ['Sunflower Bouquet'],
|
|
||||||
total: 30,
|
|
||||||
status: 'Shipped'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// State for editing profile
|
// State for editing profile
|
||||||
let isEditing = false;
|
let isEditing = false;
|
||||||
let name = user.name;
|
let name = '';
|
||||||
let email = user.email;
|
let email = '';
|
||||||
let address = user.address;
|
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
|
// 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.name = name;
|
||||||
user.email = email;
|
user.email = email;
|
||||||
user.address = address;
|
user.address = address;
|
||||||
isEditing = false;
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- Welcome Section -->
|
<!-- Welcome Section -->
|
||||||
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-12">
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-12">
|
||||||
<div class="container mx-auto px-4">
|
<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>
|
<p class="text-lg text-gray-600">Manage your orders, favorites, and account settings here.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -58,34 +79,40 @@
|
|||||||
<div class="lg:col-span-2">
|
<div class="lg:col-span-2">
|
||||||
<h2 class="mb-6 text-2xl font-bold text-gray-900">Order History</h2>
|
<h2 class="mb-6 text-2xl font-bold text-gray-900">Order History</h2>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
{#if orders.length > 0}
|
||||||
{#each orders as order}
|
{#each orders as order}
|
||||||
<Card class="p-6">
|
<Card class="p-6">
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<h3 class="text-xl font-bold text-gray-900">Order #{order.id}</h3>
|
<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}
|
{order.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-2 text-gray-600">
|
<p class="mb-2 text-gray-600">
|
||||||
<strong>Date:</strong>
|
<strong>Date:</strong>
|
||||||
{order.date}
|
{new Date(order.created).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-2 text-gray-600">
|
<p class="mb-2 text-gray-600">
|
||||||
<strong>Items:</strong>
|
<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>
|
||||||
<p class="text-gray-600">
|
<p class="text-gray-600">
|
||||||
<strong>Total:</strong> ${order.total.toFixed(2)}
|
<strong>Total:</strong> ${order.total.toFixed(2)}
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
{/each}
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<p class="text-gray-600">No orders found.</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Account Settings -->
|
<!-- Account Settings -->
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<!-- Account Settings -->
|
|
||||||
<div>
|
|
||||||
<h2 class="mb-6 text-2xl font-bold text-gray-900">Account Settings</h2>
|
<h2 class="mb-6 text-2xl font-bold text-gray-900">Account Settings</h2>
|
||||||
<Card class="p-6">
|
<Card class="p-6">
|
||||||
{#if isEditing}
|
{#if isEditing}
|
||||||
@ -100,9 +127,9 @@
|
|||||||
</form>
|
</form>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<p class="text-gray-600"><strong>Name:</strong> {user.name}</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>Email:</strong> {user?.email}</p>
|
||||||
<p class="text-gray-600"><strong>Address:</strong> {user.address}</p>
|
<p class="text-gray-600"><strong>Address:</strong> {user?.address || 'Not provided'}</p>
|
||||||
<Button on:click={() => (isEditing = true)}>
|
<Button on:click={() => (isEditing = true)}>
|
||||||
<PenOutline class="mr-2 h-5 w-5" />
|
<PenOutline class="mr-2 h-5 w-5" />
|
||||||
Edit Profile
|
Edit Profile
|
||||||
@ -112,4 +139,3 @@
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
Loading…
Reference in New Issue
Block a user