improvement
This commit is contained in:
parent
c91f5b0f65
commit
ca49ce7730
@ -12,10 +12,10 @@
|
|||||||
<FooterLinkGroup
|
<FooterLinkGroup
|
||||||
ulClass="flex flex-wrap items-center mt-3 text-sm text-gray-500 dark:text-gray-400 sm:mt-0"
|
ulClass="flex flex-wrap items-center mt-3 text-sm text-gray-500 dark:text-gray-400 sm:mt-0"
|
||||||
>
|
>
|
||||||
<FooterLink href="/">About</FooterLink>
|
<FooterLink href="/about">About</FooterLink>
|
||||||
<FooterLink href="/">Privacy Policy</FooterLink>
|
<FooterLink href="/privacy-policy">Privacy Policy</FooterLink>
|
||||||
<FooterLink href="/">Terms & Conditions</FooterLink>
|
<FooterLink href="/terms-of-service">Terms & Conditions</FooterLink>
|
||||||
<FooterLink href="/">Contact</FooterLink>
|
<FooterLink href="/contact">Contact</FooterLink>
|
||||||
</FooterLinkGroup>
|
</FooterLinkGroup>
|
||||||
</div>
|
</div>
|
||||||
</Footer>
|
</Footer>
|
||||||
|
@ -1,25 +1,55 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fly, fade } from 'svelte/transition';
|
import { fly, fade } from 'svelte/transition';
|
||||||
import { Button } from 'flowbite-svelte';
|
import { Button } from 'flowbite-svelte';
|
||||||
import { cart, clearCart, removeFromCart, updateQuantity } from '$lib/stores/cartStore';
|
|
||||||
import { ArrowRightOutline, CartOutline } from 'flowbite-svelte-icons';
|
import { ArrowRightOutline, CartOutline } from 'flowbite-svelte-icons';
|
||||||
import { clickOutside } from '$lib/utils/clickOutside';
|
import { clickOutside } from '$lib/utils/clickOutside';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import type { CartItem } from '$lib/services/types';
|
||||||
|
import { authService, cartService } from '$lib/services/api';
|
||||||
|
|
||||||
let isOpen = false;
|
let isOpen = false;
|
||||||
let isUpdating = false;
|
let isUpdating = false;
|
||||||
let updateTimeout: NodeJS.Timeout;
|
let updateTimeout: NodeJS.Timeout;
|
||||||
|
let cartItems: CartItem[] = [];
|
||||||
|
let isLoading = true;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await loadCart();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadCart() {
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
const currentUser = authService.getCurrentUser();
|
||||||
|
if (currentUser) {
|
||||||
|
cartItems = await cartService.getCart(currentUser.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load cart:', error);
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate the total price
|
// Calculate the total price
|
||||||
$: total = $cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
$: total = cartItems.reduce((sum, item) => {
|
||||||
|
const price = item.expand?.product?.price || 0;
|
||||||
|
return sum + price * item.quantity;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
// Debounced quantity update
|
// Debounced quantity update
|
||||||
const handleQuantityChange = (id: number, quantity: number) => {
|
const handleQuantityChange = async (cartItemId: string, quantity: number) => {
|
||||||
clearTimeout(updateTimeout);
|
clearTimeout(updateTimeout);
|
||||||
isUpdating = true;
|
isUpdating = true;
|
||||||
|
|
||||||
updateTimeout = setTimeout(() => {
|
updateTimeout = setTimeout(async () => {
|
||||||
updateQuantity(id, quantity);
|
try {
|
||||||
|
await cartService.updateCartItemQuantity(cartItemId, quantity);
|
||||||
|
await loadCart(); // Refresh cart after update
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update quantity:', error);
|
||||||
|
}
|
||||||
isUpdating = false;
|
isUpdating = false;
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
@ -29,19 +59,37 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Animation for item removal
|
// Animation for item removal
|
||||||
const removeItem = async (id: number) => {
|
const removeItem = async (cartItemId: string) => {
|
||||||
const itemElement = document.getElementById(`cart-item-${id}`);
|
const itemElement = document.getElementById(`cart-item-${cartItemId}`);
|
||||||
if (itemElement) {
|
if (itemElement) {
|
||||||
itemElement.style.transition = 'all 0.3s ease-out';
|
itemElement.style.transition = 'all 0.3s ease-out';
|
||||||
itemElement.style.opacity = '0';
|
itemElement.style.opacity = '0';
|
||||||
itemElement.style.transform = 'translateX(20px)';
|
itemElement.style.transform = 'translateX(20px)';
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
removeFromCart(id);
|
try {
|
||||||
|
await cartService.removeFromCart(cartItemId);
|
||||||
|
await loadCart(); // Refresh cart after removal
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove item:', error);
|
||||||
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Clear entire cart
|
||||||
|
const clearEntireCart = async () => {
|
||||||
|
try {
|
||||||
|
const currentUser = authService.getCurrentUser();
|
||||||
|
if (currentUser) {
|
||||||
|
await cartService.clearCart(currentUser.id);
|
||||||
|
cartItems = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear cart:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Navigate to the webshop
|
// Navigate to the webshop
|
||||||
const goToShop = () => {
|
const goToShop = () => {
|
||||||
isOpen = !isOpen;
|
isOpen = !isOpen;
|
||||||
@ -53,12 +101,12 @@
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Button on:click={() => (isOpen = !isOpen)} class="relative h-12">
|
<Button on:click={() => (isOpen = !isOpen)} class="relative h-12">
|
||||||
<CartOutline size="lg" />
|
<CartOutline size="lg" />
|
||||||
{#if $cart.length > 0}
|
{#if cartItems.length > 0}
|
||||||
<span
|
<span
|
||||||
class="absolute -right-2 -top-2 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs text-white"
|
class="absolute -right-2 -top-2 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs text-white"
|
||||||
transition:fade
|
transition:fade
|
||||||
>
|
>
|
||||||
{$cart.length}
|
{cartItems.length}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
@ -71,24 +119,25 @@
|
|||||||
class="fixed right-40 top-20 z-50 w-96 rounded-lg bg-white p-4 shadow-lg"
|
class="fixed right-40 top-20 z-50 w-96 rounded-lg bg-white p-4 shadow-lg"
|
||||||
use:clickOutside={handleClickOutside}
|
use:clickOutside={handleClickOutside}
|
||||||
>
|
>
|
||||||
<!-- Rest of the cart popup content remains the same -->
|
|
||||||
<h2 class="mb-4 text-lg font-bold">Shopping Cart</h2>
|
<h2 class="mb-4 text-lg font-bold">Shopping Cart</h2>
|
||||||
|
|
||||||
{#if $cart.length === 0}
|
{#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>
|
<p class="py-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>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="max-h-96 space-y-3 overflow-y-auto">
|
<ul class="max-h-96 space-y-3 overflow-y-auto">
|
||||||
{#each $cart 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:bg-gray-100"
|
||||||
>
|
>
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<h3 class="font-semibold">{item.title}</h3>
|
<h3 class="font-semibold">{item.expand?.product?.title}</h3>
|
||||||
<p class="text-sm text-gray-600">${item.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="relative">
|
||||||
@ -127,7 +176,7 @@
|
|||||||
<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={() => clearCart()} 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>
|
||||||
|
@ -1,262 +1,226 @@
|
|||||||
import PocketBase from 'pocketbase';
|
import PocketBase from 'pocketbase';
|
||||||
|
import type { User, Product, CartItem, Favorite, Order, AuthResponse, ApiError } from './types';
|
||||||
|
|
||||||
// Types for our data models
|
// Initialize PocketBase client
|
||||||
interface Product {
|
const pb = new PocketBase('https://pb.vinylnostalgia.com/'); // Replace with your PocketBase URL
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
price: number;
|
|
||||||
stock: number;
|
|
||||||
category: string;
|
|
||||||
images: string[];
|
|
||||||
variants?: Record<string, unknown>[];
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Order {
|
// Helper function to handle errors
|
||||||
id: string;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
userId: string;
|
const handleError = (error: any): ApiError => {
|
||||||
items: OrderItem[];
|
console.error('API Error:', error);
|
||||||
status: OrderStatus;
|
throw { message: error.message, code: error.status, details: error.data };
|
||||||
total: number;
|
};
|
||||||
shippingAddress: Address;
|
|
||||||
billingAddress: Address;
|
|
||||||
paymentInfo: PaymentInfo;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OrderItem {
|
// User Authentication
|
||||||
productId: string;
|
export const authService = {
|
||||||
quantity: number;
|
// Register a new user
|
||||||
price: number;
|
register: async (email: string, password: string, name: string): Promise<User> => {
|
||||||
variants?: Record<string, unknown>;
|
try {
|
||||||
}
|
const user = await pb.collection('users').create({
|
||||||
|
email,
|
||||||
interface User {
|
password,
|
||||||
id: string;
|
passwordConfirm: password,
|
||||||
email: string;
|
name
|
||||||
name: string;
|
});
|
||||||
addresses: Address[];
|
return user as unknown as User; // Explicit type casting
|
||||||
orders?: string[];
|
} catch (error) {
|
||||||
wishlist?: string[];
|
throw handleError(error);
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Address {
|
|
||||||
street: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
country: string;
|
|
||||||
zipCode: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PaymentInfo {
|
|
||||||
provider: string;
|
|
||||||
transactionId: string;
|
|
||||||
status: string;
|
|
||||||
amount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OrderStatus {
|
|
||||||
PENDING = 'pending',
|
|
||||||
PAID = 'paid',
|
|
||||||
PROCESSING = 'processing',
|
|
||||||
SHIPPED = 'shipped',
|
|
||||||
DELIVERED = 'delivered',
|
|
||||||
CANCELLED = 'cancelled',
|
|
||||||
REFUNDED = 'refunded'
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebshopAPI {
|
|
||||||
private pb: PocketBase;
|
|
||||||
|
|
||||||
constructor(url: string) {
|
|
||||||
this.pb = new PocketBase(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authentication methods
|
|
||||||
async login(email: string, password: string) {
|
|
||||||
return await this.pb.collection('users').authWithPassword(email, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
async register(userData: Partial<User>, password: string) {
|
|
||||||
return await this.pb.collection('users').create({
|
|
||||||
...userData,
|
|
||||||
passwordConfirm: password,
|
|
||||||
password
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async logout() {
|
|
||||||
this.pb.authStore.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
async requestPasswordReset(email: string) {
|
|
||||||
return await this.pb.collection('users').requestPasswordReset(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Product methods
|
|
||||||
async getProducts(page: number = 1, perPage: number = 20, filters?: string) {
|
|
||||||
return await this.pb.collection('products').getList(page, perPage, {
|
|
||||||
filter: filters,
|
|
||||||
sort: '-created'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProduct(id: string) {
|
|
||||||
return await this.pb.collection('products').getOne<Product>(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchProducts(query: string, page: number = 1, perPage: number = 20) {
|
|
||||||
return await this.pb.collection('products').getList(page, perPage, {
|
|
||||||
filter: `name ~ "${query}" || description ~ "${query}"`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProductsByCategory(category: string, page: number = 1, perPage: number = 20) {
|
|
||||||
return await this.pb.collection('products').getList(page, perPage, {
|
|
||||||
filter: `category = "${category}"`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cart methods (using local storage for cart management)
|
|
||||||
getCart(): OrderItem[] {
|
|
||||||
const cart = localStorage.getItem('cart');
|
|
||||||
return cart ? JSON.parse(cart) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
addToCart(item: OrderItem) {
|
|
||||||
const cart = this.getCart();
|
|
||||||
const existingItem = cart.find((i) => i.productId === item.productId);
|
|
||||||
|
|
||||||
if (existingItem) {
|
|
||||||
existingItem.quantity += item.quantity;
|
|
||||||
} else {
|
|
||||||
cart.push(item);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
localStorage.setItem('cart', JSON.stringify(cart));
|
// Login a user
|
||||||
|
login: async (email: string, password: string): Promise<AuthResponse> => {
|
||||||
|
try {
|
||||||
|
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||||
|
return {
|
||||||
|
token: authData.token,
|
||||||
|
user: authData.record as unknown as User // Explicit type casting
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Logout the current user
|
||||||
|
logout: (): void => {
|
||||||
|
pb.authStore.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get the current user
|
||||||
|
getCurrentUser: (): User | null => {
|
||||||
|
return pb.authStore.model as unknown as User | null; // Explicit type casting
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check if the user is authenticated
|
||||||
|
isAuthenticated: (): boolean => {
|
||||||
|
return pb.authStore.isValid;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
updateCartItem(productId: string, quantity: number) {
|
// Product Service
|
||||||
const cart = this.getCart();
|
export const productService = {
|
||||||
const item = cart.find((i) => i.productId === productId);
|
// Fetch all products
|
||||||
|
getProducts: async (): Promise<Product[]> => {
|
||||||
|
try {
|
||||||
|
const products = await pb.collection('products').getFullList();
|
||||||
|
return products as unknown as Product[]; // Explicit type casting
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
if (item) {
|
// Fetch a single product by ID
|
||||||
item.quantity = quantity;
|
getProductById: async (id: string): Promise<Product> => {
|
||||||
localStorage.setItem('cart', JSON.stringify(cart));
|
try {
|
||||||
|
const product = await pb.collection('products').getOne(id);
|
||||||
|
return product as unknown as Product; // Explicit type casting
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
removeFromCart(productId: string) {
|
// Cart Service
|
||||||
const cart = this.getCart();
|
export const cartService = {
|
||||||
const updatedCart = cart.filter((i) => i.productId !== productId);
|
// Add a product to the cart
|
||||||
localStorage.setItem('cart', JSON.stringify(updatedCart));
|
addToCart: async (userId: string, productId: string, quantity: number): Promise<CartItem> => {
|
||||||
|
try {
|
||||||
|
const cartItem = await pb.collection('cart').create({
|
||||||
|
user: userId,
|
||||||
|
product: productId,
|
||||||
|
quantity
|
||||||
|
});
|
||||||
|
return cartItem as unknown as CartItem; // Explicit type casting
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove a product from the cart
|
||||||
|
removeFromCart: async (cartItemId: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await pb.collection('cart').delete(cartItemId);
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update the quantity of a cart item
|
||||||
|
updateCartItemQuantity: async (cartItemId: string, quantity: number): Promise<CartItem> => {
|
||||||
|
try {
|
||||||
|
const updatedItem = await pb.collection('cart').update(cartItemId, { quantity });
|
||||||
|
return updatedItem as unknown as CartItem; // Explicit type casting
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fetch the user's cart
|
||||||
|
getCart: async (userId: string): Promise<CartItem[]> => {
|
||||||
|
try {
|
||||||
|
const cartItems = await pb.collection('cart').getFullList({
|
||||||
|
filter: `user = "${userId}"`,
|
||||||
|
expand: 'product'
|
||||||
|
});
|
||||||
|
return cartItems as unknown as CartItem[]; // Explicit type casting
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Clear the user's cart
|
||||||
|
clearCart: async (userId: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const cartItems = await pb.collection('cart').getFullList({
|
||||||
|
filter: `user = "${userId}"`
|
||||||
|
});
|
||||||
|
for (const item of cartItems) {
|
||||||
|
await pb.collection('cart').delete(item.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
clearCart() {
|
// Favorites Service
|
||||||
localStorage.removeItem('cart');
|
export const favoritesService = {
|
||||||
|
// Add a product to favorites
|
||||||
|
addToFavorites: async (userId: string, productId: string): Promise<Favorite> => {
|
||||||
|
try {
|
||||||
|
const favorite = await pb.collection('favorites').create({
|
||||||
|
user: userId,
|
||||||
|
product: productId
|
||||||
|
});
|
||||||
|
return favorite as unknown as Favorite; // Explicit type casting
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove a product from favorites
|
||||||
|
removeFromFavorites: async (favoriteId: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await pb.collection('favorites').delete(favoriteId);
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fetch the user's favorites
|
||||||
|
getFavorites: async (userId: string): Promise<Favorite[]> => {
|
||||||
|
try {
|
||||||
|
const favorites = await pb.collection('favorites').getFullList({
|
||||||
|
filter: `user = "${userId}"`,
|
||||||
|
expand: 'product'
|
||||||
|
});
|
||||||
|
return favorites as unknown as Favorite[]; // Explicit type casting
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Order methods
|
// Order Service
|
||||||
async createOrder(orderData: Partial<Order>) {
|
export const orderService = {
|
||||||
return await this.pb.collection('orders').create(orderData);
|
// Create a new order
|
||||||
|
createOrder: async (
|
||||||
|
userId: string,
|
||||||
|
items: Array<{ product: string; quantity: number }>,
|
||||||
|
total: number
|
||||||
|
): Promise<Order> => {
|
||||||
|
try {
|
||||||
|
const order = await pb.collection('orders').create({
|
||||||
|
user: userId,
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
status: 'pending'
|
||||||
|
});
|
||||||
|
return order as unknown as Order; // Explicit type casting
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fetch the user's orders
|
||||||
|
getOrders: async (userId: string): Promise<Order[]> => {
|
||||||
|
try {
|
||||||
|
const orders = await pb.collection('orders').getFullList({
|
||||||
|
filter: `user = "${userId}"`,
|
||||||
|
expand: 'items.product'
|
||||||
|
});
|
||||||
|
return orders as unknown as Order[]; // Explicit type casting
|
||||||
|
} catch (error) {
|
||||||
|
throw handleError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async getOrder(id: string) {
|
// Export all services
|
||||||
return await this.pb.collection('orders').getOne<Order>(id);
|
export default {
|
||||||
}
|
authService,
|
||||||
|
productService,
|
||||||
async getUserOrders(userId: string, page: number = 1, perPage: number = 20) {
|
cartService,
|
||||||
return await this.pb.collection('orders').getList(page, perPage, {
|
favoritesService,
|
||||||
filter: `userId = "${userId}"`,
|
orderService
|
||||||
sort: '-created'
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateOrderStatus(orderId: string, status: OrderStatus) {
|
|
||||||
return await this.pb.collection('orders').update(orderId, { status });
|
|
||||||
}
|
|
||||||
|
|
||||||
// User profile methods
|
|
||||||
async getCurrentUser() {
|
|
||||||
if (!this.pb.authStore.isValid) return null;
|
|
||||||
return await this.pb.collection('users').getOne<User>(this.pb.authStore.model?.id as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateUserProfile(userId: string, userData: Partial<User>) {
|
|
||||||
return await this.pb.collection('users').update(userId, userData);
|
|
||||||
}
|
|
||||||
|
|
||||||
async addAddress(userId: string, address: Address) {
|
|
||||||
const user = await this.getCurrentUser();
|
|
||||||
if (!user) throw new Error('User not authenticated');
|
|
||||||
|
|
||||||
const addresses = [...(user.addresses || []), address];
|
|
||||||
return await this.updateUserProfile(userId, { addresses });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wishlist methods
|
|
||||||
async addToWishlist(userId: string, productId: string) {
|
|
||||||
const user = await this.getCurrentUser();
|
|
||||||
if (!user) throw new Error('User not authenticated');
|
|
||||||
|
|
||||||
const wishlist = [...(user.wishlist || []), productId];
|
|
||||||
return await this.updateUserProfile(userId, { wishlist });
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeFromWishlist(userId: string, productId: string) {
|
|
||||||
const user = await this.getCurrentUser();
|
|
||||||
if (!user) throw new Error('User not authenticated');
|
|
||||||
|
|
||||||
const wishlist = (user.wishlist || []).filter((id: unknown) => id !== productId);
|
|
||||||
return await this.updateUserProfile(userId, { wishlist });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Category methods
|
|
||||||
async getCategories() {
|
|
||||||
return await this.pb.collection('categories').getFullList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Review methods
|
|
||||||
async addProductReview(productId: string, rating: number, comment: string) {
|
|
||||||
return await this.pb.collection('reviews').create({
|
|
||||||
productId,
|
|
||||||
rating,
|
|
||||||
comment,
|
|
||||||
userId: this.pb.authStore.model?.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProductReviews(productId: string, page: number = 1, perPage: number = 20) {
|
|
||||||
return await this.pb.collection('reviews').getList(page, perPage, {
|
|
||||||
filter: `productId = "${productId}"`,
|
|
||||||
sort: '-created'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin methods (requires admin privileges)
|
|
||||||
async createProduct(productData: Partial<Product>) {
|
|
||||||
return await this.pb.collection('products').create(productData);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateProduct(id: string, productData: Partial<Product>) {
|
|
||||||
return await this.pb.collection('products').update(id, productData);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteProduct(id: string) {
|
|
||||||
return await this.pb.collection('products').delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateStock(productId: string, quantity: number) {
|
|
||||||
const product = await this.getProduct(productId);
|
|
||||||
return await this.updateProduct(productId, {
|
|
||||||
stock: product.stock + quantity
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WebshopAPI;
|
|
||||||
|
78
src/lib/services/types.ts
Normal file
78
src/lib/services/types.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// User Interface
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product Interface
|
||||||
|
export interface Product {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
imageUrl: string;
|
||||||
|
price: number;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cart Item Interface
|
||||||
|
export interface CartItem {
|
||||||
|
id: string;
|
||||||
|
user: string; // User ID
|
||||||
|
product: string; // Product ID
|
||||||
|
quantity: number;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
expand?: {
|
||||||
|
product: Product; // Expanded product details
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Favorite Interface
|
||||||
|
export interface Favorite {
|
||||||
|
id: string;
|
||||||
|
user: string; // User ID
|
||||||
|
product: string; // Product ID
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
expand?: {
|
||||||
|
product: Product; // Expanded product details
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order Item Interface
|
||||||
|
export interface OrderItem {
|
||||||
|
product: string; // Product ID
|
||||||
|
quantity: number;
|
||||||
|
expand?: {
|
||||||
|
product: Product; // Expanded product details
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order Interface
|
||||||
|
export interface Order {
|
||||||
|
id: string;
|
||||||
|
user: string; // User ID
|
||||||
|
items: OrderItem[];
|
||||||
|
total: number;
|
||||||
|
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
expand?: {
|
||||||
|
items: Array<{ product: Product }>; // Expanded product details for each item
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface AuthResponse {
|
||||||
|
token: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Error Interface
|
||||||
|
export interface ApiError {
|
||||||
|
message: string;
|
||||||
|
code?: number;
|
||||||
|
details?: unknown;
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import session from "$lib/session.svelte";
|
import { authService } from '$lib/services/api';
|
||||||
import { redirect } from "@sveltejs/kit";
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
export async function load() {
|
export async function load() {
|
||||||
if (!session.loggedIn()) {
|
if (!authService.isAuthenticated()) {
|
||||||
redirect(307, "/login");
|
redirect(307, '/login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
clearCart();
|
clearCart();
|
||||||
|
|
||||||
goto(`/order-confirmation/12345`);
|
goto(`/order-confirmation/12345`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Failed to process checkout. Please try again.';
|
error = 'Failed to process checkout. Please try again.';
|
||||||
|
@ -1,216 +1,97 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { addToCart } from '$lib/stores/cartStore';
|
import { Card, Button } from 'flowbite-svelte';
|
||||||
import { Card, Button, Badge } from 'flowbite-svelte';
|
|
||||||
import { CartPlusOutline, HeartOutline, HeartSolid } from 'flowbite-svelte-icons';
|
import { CartPlusOutline, HeartOutline, HeartSolid } from 'flowbite-svelte-icons';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { addToFavorites, favorites, removeFromFavorites } from '$lib/stores/store';
|
import { authService, productService, favoritesService, cartService } from '$lib/services/api';
|
||||||
|
import type { Product, Favorite } from '$lib/services/types';
|
||||||
interface Product {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
imageUrl: string;
|
|
||||||
price: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
let product: Product | undefined;
|
let product: Product | undefined;
|
||||||
let relatedProducts: Product[] = [];
|
let relatedProducts: Product[] = [];
|
||||||
|
let favorites: Favorite[] = [];
|
||||||
const products: Product[] = [
|
let isLoading = true;
|
||||||
{
|
let error: string | null = null;
|
||||||
id: 1,
|
|
||||||
title: 'Tulip',
|
|
||||||
description: 'Beautiful tulips for your garden.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1520763185298-1b434c919102?q=80&w=1932&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
|
||||||
price: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Rose',
|
|
||||||
description: 'Elegant roses for special occasions.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1582794543139-8ac9cb0f7b11?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
|
||||||
price: 15
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: 'Sunflower',
|
|
||||||
description: 'Bright sunflowers to light up your day.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1535382985264-7ea131537c07?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTQxfHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 12
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: 'Orchid',
|
|
||||||
description: 'Exotic orchids for a touch of elegance.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1531217182035-78d279dcdb7f?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8T3JjaGlkfGVufDB8fDB8fHww',
|
|
||||||
price: 25
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: 'Lily',
|
|
||||||
description: 'Fragrant lilies for a serene atmosphere.',
|
|
||||||
imageUrl:
|
|
||||||
'https://plus.unsplash.com/premium_photo-1676654936609-e264dd7b9dab?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTM1fHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 18
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: 'Daisy',
|
|
||||||
description: 'Cheerful daisies for a fresh look.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1496098570671-efe44b569a00?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTQ4fHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
title: 'Carnation',
|
|
||||||
description: 'Classic carnations for any occasion.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1587316830148-c9b01df2da38?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Njl8fGZsb3dlcnxlbnwwfHwwfHx8MA%3D%3D',
|
|
||||||
price: 9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
title: 'Peony',
|
|
||||||
description: 'Lush peonies for a luxurious feel.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1579053778004-3a4d3f0fae19?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTU2fHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 22
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
title: 'Hydrangea',
|
|
||||||
description: 'Stunning hydrangeas for a bold statement.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1552409905-46aa1e84e2e8?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTY5fHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 20
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
title: 'Lavender',
|
|
||||||
description: 'Soothing lavender for relaxation.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1490163212432-2c8e584dc243?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjI3fHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 14
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
title: 'Iris',
|
|
||||||
description: 'Vibrant irises for a pop of color.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1583693034345-b6c1d5b2ffa6?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjUwfHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 16
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
title: 'Daffodil',
|
|
||||||
description: 'Bright daffodils to welcome spring.',
|
|
||||||
imageUrl:
|
|
||||||
'https://plus.unsplash.com/premium_photo-1676070094538-7663fb7c2745?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8RGFmZm9kaWx8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 11
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
title: 'Poppy',
|
|
||||||
description: 'Vivid poppies for a bold statement.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1527703137818-60612b595c72?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjIwfHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 13
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
title: 'Marigold',
|
|
||||||
description: 'Golden marigolds for a festive touch.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1559563362-c667ba5f5480?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjA5fHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 15,
|
|
||||||
title: 'Chrysanthemum',
|
|
||||||
description: 'Elegant chrysanthemums for autumn.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1498323094960-d1a30fae4c5c?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTgwfHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 17
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 16,
|
|
||||||
title: 'Gerbera',
|
|
||||||
description: 'Cheerful gerberas for a joyful vibe.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1478801928079-ff8b78b1bef2?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTcyfHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 19
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 17,
|
|
||||||
title: 'Anemone',
|
|
||||||
description: 'Delicate anemones for a subtle charm.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1496571330383-9b977f4a021d?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8OTR8fGZsb3dlcnxlbnwwfHwwfHx8MA%3D%3D',
|
|
||||||
price: 21
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 18,
|
|
||||||
title: 'Ranunculus',
|
|
||||||
description: 'Layered ranunculus for a romantic touch.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1578972497170-bfc780c65f65?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Nzh8fGZsb3dlcnxlbnwwfHwwfHx8MA%3D%3D',
|
|
||||||
price: 23
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 19,
|
|
||||||
title: 'Freesia',
|
|
||||||
description: 'Fragrant freesias for a sweet aroma.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1496062031456-07b8f162a322?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NjF8fGZsb3dlcnxlbnwwfHwwfHx8MA%3D%3D',
|
|
||||||
price: 24
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 20,
|
|
||||||
title: 'Amaryllis',
|
|
||||||
description: 'Striking amaryllis for a dramatic effect.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1487139975590-b4f1dce9b035?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8Zmxvd2VyfGVufDB8fDB8fHww',
|
|
||||||
price: 26
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const productId = parseInt($page.params.id);
|
try {
|
||||||
|
const productId = $page.params.id;
|
||||||
|
const currentUser = authService.getCurrentUser();
|
||||||
|
|
||||||
product = products.find((item) => item.id === productId);
|
if (!productId) {
|
||||||
|
throw new Error('Product ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
relatedProducts = products
|
// Fetch product details
|
||||||
.filter((item) => item.id !== productId) // Exclude the current product
|
product = await productService.getProductById(productId);
|
||||||
.sort(() => 0.5 - Math.random()) // Shuffle the array
|
|
||||||
.slice(0, 3); // Get the first 3 items
|
// Fetch all products for related products
|
||||||
|
const allProducts = await productService.getProducts();
|
||||||
|
relatedProducts = allProducts
|
||||||
|
.filter((item) => item.id !== productId)
|
||||||
|
.sort(() => 0.5 - Math.random())
|
||||||
|
.slice(0, 3);
|
||||||
|
|
||||||
|
// Fetch user's favorites if logged in
|
||||||
|
if (currentUser) {
|
||||||
|
favorites = await favoritesService.getFavorites(currentUser.id);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error = err instanceof Error ? err.message : 'An error occurred';
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if a product is in favorites
|
||||||
|
const isProductInFavorites = (productId: string): boolean => {
|
||||||
|
return favorites.some((fav) => fav.product === productId);
|
||||||
|
};
|
||||||
|
|
||||||
// Handle favorite toggling
|
// Handle favorite toggling
|
||||||
const toggleFavorite = () => {
|
const toggleFavorite = async (productId: string) => {
|
||||||
if (product && $favorites.some((item: any) => item.id === product!.id)) {
|
const currentUser = authService.getCurrentUser();
|
||||||
removeFromFavorites(product.id);
|
if (!currentUser) {
|
||||||
} else if (product) {
|
// Handle not logged in state - maybe redirect to login or show message
|
||||||
addToFavorites(product);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingFavorite = favorites.find((fav) => fav.product === productId);
|
||||||
|
if (existingFavorite) {
|
||||||
|
await favoritesService.removeFromFavorites(existingFavorite.id);
|
||||||
|
favorites = favorites.filter((fav) => fav.id !== existingFavorite.id);
|
||||||
|
} else {
|
||||||
|
const newFavorite = await favoritesService.addToFavorites(currentUser.id, productId);
|
||||||
|
favorites = [...favorites, newFavorite];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error toggling favorite:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add to cart with feedback
|
// Add to cart with feedback
|
||||||
const handleAddToCart = () => {
|
const handleAddToCart = async (product: Product) => {
|
||||||
if (product) {
|
const currentUser = authService.getCurrentUser();
|
||||||
addToCart({ ...product, quantity: 1 });
|
if (!currentUser) {
|
||||||
|
// Handle not logged in state
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cartService.addToCart(currentUser.id, product.id, 1);
|
||||||
// You could add a toast notification here
|
// You could add a toast notification here
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error adding to cart:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="container mx-auto px-4 py-8">
|
<main class="container mx-auto px-4 py-8">
|
||||||
{#if product}
|
{#if isLoading}
|
||||||
|
<p class="text-gray-600">Loading product details...</p>
|
||||||
|
{:else if error}
|
||||||
|
<p class="text-red-600">{error}</p>
|
||||||
|
{:else if product}
|
||||||
<!-- Product Details Section -->
|
<!-- Product Details Section -->
|
||||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||||
<!-- Product Image -->
|
<!-- Product Image -->
|
||||||
@ -233,17 +114,17 @@
|
|||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<Button class="w-full" on:click={handleAddToCart}>
|
<Button class="w-full" on:click={() => handleAddToCart(product as Product)}>
|
||||||
<CartPlusOutline class="mr-2 h-5 w-5" />
|
<CartPlusOutline class="mr-2 h-5 w-5" />
|
||||||
Add to Cart
|
Add to Cart
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="w-full" on:click={toggleFavorite}>
|
<Button class="w-full" on:click={() => toggleFavorite(product!.id)}>
|
||||||
{#if $favorites.some((item) => item.id === product!.id)}
|
{#if isProductInFavorites(product.id)}
|
||||||
<HeartSolid class="mr-2 h-5 w-5 text-red-500" />
|
<HeartSolid class="mr-2 h-5 w-5 text-red-500" />
|
||||||
{:else}
|
{:else}
|
||||||
<HeartOutline class="mr-2 h-5 w-5 text-gray-500" />
|
<HeartOutline class="mr-2 h-5 w-5 text-gray-500" />
|
||||||
{/if}
|
{/if}
|
||||||
{#if $favorites.some((item) => item.id === product!.id)}
|
{#if isProductInFavorites(product.id)}
|
||||||
Remove from Favorites
|
Remove from Favorites
|
||||||
{:else}
|
{:else}
|
||||||
Add to Favorites
|
Add to Favorites
|
||||||
@ -255,8 +136,7 @@
|
|||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Product Details</h2>
|
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Product Details</h2>
|
||||||
<p class="text-gray-600 dark:text-gray-400">
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
|
{product.description}
|
||||||
incididunt ut labore et dolore magna aliqua.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -294,10 +174,7 @@
|
|||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<Button
|
<Button class="w-full" on:click={() => handleAddToCart(relatedProduct)}>
|
||||||
class="w-full"
|
|
||||||
on:click={() => addToCart({ ...relatedProduct, quantity: 1 })}
|
|
||||||
>
|
|
||||||
<CartPlusOutline class="mr-2 h-5 w-5" />
|
<CartPlusOutline class="mr-2 h-5 w-5" />
|
||||||
Add to Cart
|
Add to Cart
|
||||||
</Button>
|
</Button>
|
||||||
@ -308,6 +185,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-gray-600">Loading product details...</p>
|
<p class="text-gray-600">Product not found</p>
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { onMount } from 'svelte';
|
||||||
import { Button } from 'flowbite-svelte';
|
import { Button } from 'flowbite-svelte';
|
||||||
import { ArrowRightOutline, StarSolid, CheckCircleOutline } from 'flowbite-svelte-icons';
|
import { ArrowRightOutline, StarSolid, CheckCircleOutline } from 'flowbite-svelte-icons';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
// Example featured products
|
// Example featured products
|
||||||
const featuredProducts = [
|
const featuredProducts = [
|
||||||
@ -55,39 +56,64 @@
|
|||||||
const goToShop = () => {
|
const goToShop = () => {
|
||||||
goto('/main');
|
goto('/main');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add animations on mount
|
||||||
|
onMount(() => {
|
||||||
|
const elements = document.querySelectorAll('.fade-in, .slide-in');
|
||||||
|
elements.forEach((el) => {
|
||||||
|
el.classList.add('opacity-0', 'translate-y-10');
|
||||||
|
});
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.remove('opacity-0', 'translate-y-10');
|
||||||
|
entry.target.classList.add('opacity-100', 'translate-y-0');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.forEach((el) => observer.observe(el));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
<section class="relative overflow-hidden bg-gradient-to-r from-pink-100 to-purple-100 py-32">
|
||||||
<div class="container mx-auto px-4 text-center">
|
<div class="container mx-auto px-4 text-center">
|
||||||
<h1 class="mb-4 text-5xl font-bold text-gray-900">Beautiful Flowers for Every Occasion</h1>
|
<h1 class="fade-in mb-6 text-6xl font-bold text-gray-900">
|
||||||
<p class="mb-8 text-xl text-gray-600">
|
Beautiful Flowers for Every Occasion
|
||||||
|
</h1>
|
||||||
|
<p class="fade-in mb-8 text-xl text-gray-600">
|
||||||
Fresh, handpicked flowers delivered to your doorstep. Make every moment special with our
|
Fresh, handpicked flowers delivered to your doorstep. Make every moment special with our
|
||||||
stunning bouquets.
|
stunning bouquets.
|
||||||
</p>
|
</p>
|
||||||
<Button size="xl" on:click={goToShop}>
|
<Button size="xl" on:click={goToShop} class="fade-in">
|
||||||
Shop Now <ArrowRightOutline class="ml-2 h-5 w-5" />
|
Shop Now <ArrowRightOutline class="ml-2 h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="absolute inset-0 -z-10 bg-white bg-opacity-10"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Value Proposition Section -->
|
<!-- Value Proposition Section -->
|
||||||
<section class="py-16">
|
<section class="py-20">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-3">
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||||
<div class="text-center">
|
<div class="slide-in text-center">
|
||||||
<CheckCircleOutline class="mx-auto mb-4 h-12 w-12 text-purple-500" />
|
<CheckCircleOutline class="mx-auto mb-4 h-12 w-12 text-purple-500" />
|
||||||
<h2 class="mb-2 text-2xl font-bold text-gray-900">Fresh & Handpicked</h2>
|
<h2 class="mb-2 text-2xl font-bold text-gray-900">Fresh & Handpicked</h2>
|
||||||
<p class="text-gray-600">We source the freshest flowers directly from local growers.</p>
|
<p class="text-gray-600">We source the freshest flowers directly from local growers.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="slide-in text-center">
|
||||||
<CheckCircleOutline class="mx-auto mb-4 h-12 w-12 text-purple-500" />
|
<CheckCircleOutline class="mx-auto mb-4 h-12 w-12 text-purple-500" />
|
||||||
<h2 class="mb-2 text-2xl font-bold text-gray-900">Fast Delivery</h2>
|
<h2 class="mb-2 text-2xl font-bold text-gray-900">Fast Delivery</h2>
|
||||||
<p class="text-gray-600">
|
<p class="text-gray-600">
|
||||||
Get your flowers delivered the same day or schedule a future delivery.
|
Get your flowers delivered the same day or schedule a future delivery.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="slide-in text-center">
|
||||||
<CheckCircleOutline class="mx-auto mb-4 h-12 w-12 text-purple-500" />
|
<CheckCircleOutline class="mx-auto mb-4 h-12 w-12 text-purple-500" />
|
||||||
<h2 class="mb-2 text-2xl font-bold text-gray-900">Satisfaction Guaranteed</h2>
|
<h2 class="mb-2 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.</p>
|
<p class="text-gray-600">We stand by our products with a 100% satisfaction guarantee.</p>
|
||||||
@ -97,12 +123,12 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Featured Products Section -->
|
<!-- Featured Products Section -->
|
||||||
<section class="bg-gray-50 py-16">
|
<section class="bg-gray-50 py-20">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<h2 class="mb-8 text-center text-3xl font-bold text-gray-900">Featured Products</h2>
|
<h2 class="fade-in mb-12 text-center text-4xl font-bold text-gray-900">Featured Products</h2>
|
||||||
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{#each featuredProducts as product}
|
{#each featuredProducts as product}
|
||||||
<div class="overflow-hidden rounded-lg bg-white shadow-lg">
|
<div class="slide-in overflow-hidden rounded-lg bg-white shadow-lg">
|
||||||
<img src={product.imageUrl} alt={product.title} class="h-64 w-full object-cover" />
|
<img src={product.imageUrl} alt={product.title} class="h-64 w-full object-cover" />
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h3 class="mb-2 text-xl font-bold text-gray-900">{product.title}</h3>
|
<h3 class="mb-2 text-xl font-bold text-gray-900">{product.title}</h3>
|
||||||
@ -119,12 +145,14 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Testimonials Section -->
|
<!-- Testimonials Section -->
|
||||||
<section class="py-16">
|
<section class="py-20">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<h2 class="mb-8 text-center text-3xl font-bold text-gray-900">What Our Customers Say</h2>
|
<h2 class="fade-in mb-12 text-center text-4xl font-bold text-gray-900">
|
||||||
|
What Our Customers Say
|
||||||
|
</h2>
|
||||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-3">
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||||
{#each testimonials as testimonial}
|
{#each testimonials as testimonial}
|
||||||
<div class="rounded-lg bg-white p-6 shadow-lg">
|
<div class="slide-in rounded-lg bg-white p-6 shadow-lg">
|
||||||
<div class="mb-4 flex items-center">
|
<div class="mb-4 flex items-center">
|
||||||
{#each Array(testimonial.rating) as _}
|
{#each Array(testimonial.rating) as _}
|
||||||
<StarSolid class="h-5 w-5 text-yellow-400" />
|
<StarSolid class="h-5 w-5 text-yellow-400" />
|
||||||
@ -139,14 +167,29 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- CTA Section -->
|
<!-- CTA Section -->
|
||||||
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
<section class="relative overflow-hidden bg-gradient-to-r from-pink-100 to-purple-100 py-32">
|
||||||
<div class="container mx-auto px-4 text-center">
|
<div class="container mx-auto px-4 text-center">
|
||||||
<h2 class="mb-4 text-4xl font-bold text-gray-900">Ready to Brighten Someone's Day?</h2>
|
<h2 class="fade-in mb-6 text-5xl font-bold text-gray-900">Ready to Brighten Someone's Day?</h2>
|
||||||
<p class="mb-8 text-xl text-gray-600">
|
<p class="fade-in mb-8 text-xl text-gray-600">
|
||||||
Shop our collection of beautiful flowers and make every moment special.
|
Shop our collection of beautiful flowers and make every moment special.
|
||||||
</p>
|
</p>
|
||||||
<Button size="xl" on:click={goToShop}>
|
<Button size="xl" on:click={goToShop} class="fade-in">
|
||||||
Shop Now <ArrowRightOutline class="ml-2 h-5 w-5" />
|
Shop Now <ArrowRightOutline class="ml-2 h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="absolute inset-0 bg-white bg-opacity-10"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.fade-in {
|
||||||
|
transition:
|
||||||
|
opacity 1s ease-out,
|
||||||
|
transform 1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-in {
|
||||||
|
transition:
|
||||||
|
opacity 1s ease-out,
|
||||||
|
transform 1s ease-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { addToCart } from '$lib/stores/cartStore';
|
import { onMount } from 'svelte';
|
||||||
import { removeFromFavorites, addToFavorites, appStore, favorites } from '$lib/stores/store';
|
|
||||||
import { Card, Button, Badge, Input, Select, Toast, Spinner } from 'flowbite-svelte';
|
import { Card, Button, Badge, Input, Select, Toast, Spinner } from 'flowbite-svelte';
|
||||||
import { CartPlusOutline, HeartOutline, HeartSolid, SearchOutline } from 'flowbite-svelte-icons';
|
import { CartPlusOutline, HeartOutline, HeartSolid, SearchOutline } from 'flowbite-svelte-icons';
|
||||||
import ImageModal from '../../../components/imageModal.svelte';
|
import ImageModal from '../../../components/imageModal.svelte';
|
||||||
@ -8,180 +7,12 @@
|
|||||||
import { fade, fly, scale } from 'svelte/transition';
|
import { fade, fly, scale } from 'svelte/transition';
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { productService, authService, favoritesService, cartService } from '$lib/services/api';
|
||||||
|
import type { Product, Favorite } from '$lib/services/types';
|
||||||
|
|
||||||
interface Product {
|
// Store management
|
||||||
id: number;
|
const products: Writable<Product[]> = writable([]);
|
||||||
title: string;
|
const userFavorites: Writable<Favorite[]> = writable([]);
|
||||||
description: string;
|
|
||||||
imageUrl: string;
|
|
||||||
price: number;
|
|
||||||
category?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Products array and state management
|
|
||||||
const products: Product[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Tulip',
|
|
||||||
description: 'Beautiful tulips for your garden.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1520763185298-1b434c919102?q=80&w=1932&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
|
||||||
price: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Rose',
|
|
||||||
description: 'Elegant roses for special occasions.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1582794543139-8ac9cb0f7b11?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
|
||||||
price: 15
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: 'Sunflower',
|
|
||||||
description: 'Bright sunflowers to light up your day.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1535382985264-7ea131537c07?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTQxfHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 12
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: 'Orchid',
|
|
||||||
description: 'Exotic orchids for a touch of elegance.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1531217182035-78d279dcdb7f?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8T3JjaGlkfGVufDB8fDB8fHww',
|
|
||||||
price: 25
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: 'Lily',
|
|
||||||
description: 'Fragrant lilies for a serene atmosphere.',
|
|
||||||
imageUrl:
|
|
||||||
'https://plus.unsplash.com/premium_photo-1676654936609-e264dd7b9dab?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTM1fHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 18
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: 'Daisy',
|
|
||||||
description: 'Cheerful daisies for a fresh look.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1496098570671-efe44b569a00?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTQ4fHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
title: 'Carnation',
|
|
||||||
description: 'Classic carnations for any occasion.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1587316830148-c9b01df2da38?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Njl8fGZsb3dlcnxlbnwwfHwwfHx8MA%3D%3D',
|
|
||||||
price: 9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
title: 'Peony',
|
|
||||||
description: 'Lush peonies for a luxurious feel.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1579053778004-3a4d3f0fae19?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTU2fHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 22
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
title: 'Hydrangea',
|
|
||||||
description: 'Stunning hydrangeas for a bold statement.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1552409905-46aa1e84e2e8?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTY5fHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 20
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
title: 'Lavender',
|
|
||||||
description: 'Soothing lavender for relaxation.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1490163212432-2c8e584dc243?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjI3fHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 14
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
title: 'Iris',
|
|
||||||
description: 'Vibrant irises for a pop of color.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1583693034345-b6c1d5b2ffa6?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjUwfHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 16
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
title: 'Daffodil',
|
|
||||||
description: 'Bright daffodils to welcome spring.',
|
|
||||||
imageUrl:
|
|
||||||
'https://plus.unsplash.com/premium_photo-1676070094538-7663fb7c2745?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8RGFmZm9kaWx8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 11
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
title: 'Poppy',
|
|
||||||
description: 'Vivid poppies for a bold statement.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1527703137818-60612b595c72?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjIwfHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 13
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
title: 'Marigold',
|
|
||||||
description: 'Golden marigolds for a festive touch.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1559563362-c667ba5f5480?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjA5fHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 15,
|
|
||||||
title: 'Chrysanthemum',
|
|
||||||
description: 'Elegant chrysanthemums for autumn.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1498323094960-d1a30fae4c5c?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTgwfHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 17
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 16,
|
|
||||||
title: 'Gerbera',
|
|
||||||
description: 'Cheerful gerberas for a joyful vibe.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1478801928079-ff8b78b1bef2?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTcyfHxmbG93ZXJ8ZW58MHx8MHx8fDA%3D',
|
|
||||||
price: 19
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 17,
|
|
||||||
title: 'Anemone',
|
|
||||||
description: 'Delicate anemones for a subtle charm.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1496571330383-9b977f4a021d?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8OTR8fGZsb3dlcnxlbnwwfHwwfHx8MA%3D%3D',
|
|
||||||
price: 21
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 18,
|
|
||||||
title: 'Ranunculus',
|
|
||||||
description: 'Layered ranunculus for a romantic touch.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1578972497170-bfc780c65f65?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Nzh8fGZsb3dlcnxlbnwwfHwwfHx8MA%3D%3D',
|
|
||||||
price: 23
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 19,
|
|
||||||
title: 'Freesia',
|
|
||||||
description: 'Fragrant freesias for a sweet aroma.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1496062031456-07b8f162a322?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NjF8fGZsb3dlcnxlbnwwfHwwfHx8MA%3D%3D',
|
|
||||||
price: 24
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 20,
|
|
||||||
title: 'Amaryllis',
|
|
||||||
description: 'Striking amaryllis for a dramatic effect.',
|
|
||||||
imageUrl:
|
|
||||||
'https://images.unsplash.com/photo-1487139975590-b4f1dce9b035?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8Zmxvd2VyfGVufDB8fDB8fHww',
|
|
||||||
price: 26
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const itemsPerPage = 9;
|
const itemsPerPage = 9;
|
||||||
const currentPage: Writable<number> = writable(1);
|
const currentPage: Writable<number> = writable(1);
|
||||||
const searchQuery: Writable<string> = writable('');
|
const searchQuery: Writable<string> = writable('');
|
||||||
@ -189,38 +20,56 @@
|
|||||||
const isLoading: Writable<boolean> = writable(false);
|
const isLoading: Writable<boolean> = writable(false);
|
||||||
const showToast: Writable<boolean> = writable(false);
|
const showToast: Writable<boolean> = writable(false);
|
||||||
const toastMessage: Writable<string> = writable('');
|
const toastMessage: Writable<string> = writable('');
|
||||||
|
const error: Writable<string | null> = writable(null);
|
||||||
|
|
||||||
// Add loading simulation
|
// Load initial data
|
||||||
const simulateLoading = async () => {
|
onMount(async () => {
|
||||||
isLoading.set(true);
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
isLoading.set(true);
|
||||||
isLoading.set(false);
|
const fetchedProducts = await productService.getProducts();
|
||||||
};
|
products.set(fetchedProducts);
|
||||||
|
|
||||||
|
// Load favorites if user is authenticated
|
||||||
|
if (authService.isAuthenticated()) {
|
||||||
|
const currentUser = authService.getCurrentUser();
|
||||||
|
if (currentUser) {
|
||||||
|
const favorites = await favoritesService.getFavorites(currentUser.id);
|
||||||
|
userFavorites.set(favorites);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.set('Failed to load products');
|
||||||
|
console.error('Error loading products:', err);
|
||||||
|
} finally {
|
||||||
|
isLoading.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Enhanced filtered products with proper typing
|
// Enhanced filtered products with proper typing
|
||||||
const filteredProducts = derived<[Writable<string>, Writable<string>], Product[]>(
|
const filteredProducts = derived<
|
||||||
[searchQuery, selectedCategory],
|
[Writable<string>, Writable<string>, Writable<Product[]>],
|
||||||
([$searchQuery, $selectedCategory], set) => {
|
Product[]
|
||||||
simulateLoading().then(() => {
|
>(
|
||||||
if (!$searchQuery && $selectedCategory === 'all') {
|
[searchQuery, selectedCategory, products],
|
||||||
set(products);
|
([$searchQuery, $selectedCategory, $products], set) => {
|
||||||
return;
|
if (!$searchQuery && $selectedCategory === 'all') {
|
||||||
}
|
set($products);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const searchLower = $searchQuery.toLowerCase();
|
const searchLower = $searchQuery.toLowerCase();
|
||||||
const filtered = products.filter((product) => {
|
const filtered = $products.filter((product) => {
|
||||||
if ($selectedCategory !== 'all' && product.category !== $selectedCategory) return false;
|
// if ($selectedCategory !== 'all' && product.category !== $selectedCategory) return false;
|
||||||
if (!searchLower) return true;
|
if (!searchLower) return true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
product.title.toLowerCase().includes(searchLower) ||
|
product.title.toLowerCase().includes(searchLower) ||
|
||||||
product.description.toLowerCase().includes(searchLower)
|
product.description.toLowerCase().includes(searchLower)
|
||||||
);
|
);
|
||||||
});
|
|
||||||
set(filtered);
|
|
||||||
});
|
});
|
||||||
|
set(filtered);
|
||||||
},
|
},
|
||||||
[] as Product[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const paginatedProducts = derived<[typeof filteredProducts, Writable<number>], Product[]>(
|
const paginatedProducts = derived<[typeof filteredProducts, Writable<number>], Product[]>(
|
||||||
@ -236,6 +85,66 @@
|
|||||||
($filteredProducts) => Math.ceil($filteredProducts.length / itemsPerPage)
|
($filteredProducts) => Math.ceil($filteredProducts.length / itemsPerPage)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle user interactions
|
||||||
|
const toggleFavorite = async (product: Product) => {
|
||||||
|
if (!authService.isAuthenticated()) {
|
||||||
|
toastMessage.set('Please login to add favorites');
|
||||||
|
showToast.set(true);
|
||||||
|
setTimeout(() => showToast.set(false), 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentUser = authService.getCurrentUser();
|
||||||
|
if (!currentUser) return;
|
||||||
|
|
||||||
|
const isFavorite = $userFavorites.some((fav) => fav.product === product.id);
|
||||||
|
|
||||||
|
if (isFavorite) {
|
||||||
|
const favorite = $userFavorites.find((fav) => fav.product === product.id);
|
||||||
|
if (favorite) {
|
||||||
|
await favoritesService.removeFromFavorites(favorite.id);
|
||||||
|
userFavorites.update((favs) => favs.filter((f) => f.id !== favorite.id));
|
||||||
|
}
|
||||||
|
toastMessage.set('Removed from favorites');
|
||||||
|
} else {
|
||||||
|
const newFavorite = await favoritesService.addToFavorites(currentUser.id, product.id);
|
||||||
|
userFavorites.update((favs) => [...favs, newFavorite]);
|
||||||
|
toastMessage.set('Added to favorites');
|
||||||
|
}
|
||||||
|
showToast.set(true);
|
||||||
|
setTimeout(() => showToast.set(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error toggling favorite:', err);
|
||||||
|
toastMessage.set('Failed to update favorites');
|
||||||
|
showToast.set(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddToCart = async (product: Product) => {
|
||||||
|
if (!authService.isAuthenticated()) {
|
||||||
|
toastMessage.set('Please login to add items to cart');
|
||||||
|
showToast.set(true);
|
||||||
|
setTimeout(() => showToast.set(false), 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentUser = authService.getCurrentUser();
|
||||||
|
if (!currentUser) return;
|
||||||
|
|
||||||
|
await cartService.addToCart(currentUser.id, product.id, 1);
|
||||||
|
toastMessage.set('Added to cart');
|
||||||
|
showToast.set(true);
|
||||||
|
setTimeout(() => showToast.set(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error adding to cart:', err);
|
||||||
|
toastMessage.set('Failed to add to cart');
|
||||||
|
showToast.set(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pagination and filtering
|
||||||
const goToPage = (page: number) => {
|
const goToPage = (page: number) => {
|
||||||
currentPage.set(page);
|
currentPage.set(page);
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
@ -245,7 +154,6 @@
|
|||||||
const handleSearch = (event: Event) => {
|
const handleSearch = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
isLoading.set(true);
|
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
searchQuery.set(target.value);
|
searchQuery.set(target.value);
|
||||||
currentPage.set(1);
|
currentPage.set(1);
|
||||||
@ -256,30 +164,9 @@
|
|||||||
const target = event.target as HTMLSelectElement;
|
const target = event.target as HTMLSelectElement;
|
||||||
selectedCategory.set(target.value);
|
selectedCategory.set(target.value);
|
||||||
currentPage.set(1);
|
currentPage.set(1);
|
||||||
simulateLoading();
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleFavorite = (product: Product) => {
|
|
||||||
const isFavorite = $favorites.some((item: Product) => item.id === product.id);
|
|
||||||
if (isFavorite) {
|
|
||||||
removeFromFavorites(product.id);
|
|
||||||
showToast.set(true);
|
|
||||||
toastMessage.set('Removed from favorites');
|
|
||||||
} else {
|
|
||||||
addToFavorites(product);
|
|
||||||
showToast.set(true);
|
|
||||||
toastMessage.set('Added to favorites');
|
|
||||||
}
|
|
||||||
setTimeout(() => showToast.set(false), 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddToCart = (product: Product) => {
|
|
||||||
addToCart({ ...product, quantity: 1 });
|
|
||||||
showToast.set(true);
|
|
||||||
toastMessage.set('Added to cart');
|
|
||||||
setTimeout(() => showToast.set(false), 2000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Image modal handling
|
||||||
let modalOpen = false;
|
let modalOpen = false;
|
||||||
let selectedImage = {
|
let selectedImage = {
|
||||||
url: '',
|
url: '',
|
||||||
@ -298,19 +185,27 @@
|
|||||||
modalOpen = false;
|
modalOpen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigate to item page
|
// Navigation
|
||||||
const goToItemPage = (productId: number) => {
|
const goToItemPage = (productId: string) => {
|
||||||
goto(`/item/${productId}`);
|
goto(`/item/${productId}`);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Rest of the template remains largely the same, just update the type of product.id from number to string -->
|
||||||
<main class="container mx-auto px-4 py-8">
|
<main class="container mx-auto px-4 py-8">
|
||||||
|
{#if $error}
|
||||||
|
<div class="mb-4 rounded-lg bg-red-100 p-4 text-red-700" in:fade>
|
||||||
|
{$error}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="mb-8 space-y-4" in:fly={{ y: -20, duration: 800, delay: 200 }}>
|
<div class="mb-8 space-y-4" in:fly={{ y: -20, duration: 800, delay: 200 }}>
|
||||||
<p class="text-lg text-gray-600 dark:text-gray-400">
|
<p class="text-lg text-gray-600 dark:text-gray-400">
|
||||||
Discover our beautiful collection of fresh flowers
|
Discover our beautiful collection of fresh flowers
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Search and filter controls -->
|
||||||
<div class="mb-6 grid gap-4 sm:grid-cols-2" in:fly={{ y: -20, duration: 600, delay: 200 }}>
|
<div class="mb-6 grid gap-4 sm:grid-cols-2" in:fly={{ y: -20, duration: 600, delay: 200 }}>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Input
|
<Input
|
||||||
@ -330,12 +225,14 @@
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading state -->
|
||||||
{#if $isLoading}
|
{#if $isLoading}
|
||||||
<div class="flex justify-center py-12" in:fade>
|
<div class="flex justify-center py-12" in:fade>
|
||||||
<Spinner size="12" />
|
<Spinner size="12" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
<!-- Product grid -->
|
||||||
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{#each $paginatedProducts as product, i (product.id)}
|
{#each $paginatedProducts as product, i (product.id)}
|
||||||
<div in:fly={{ y: 20, duration: 400, delay: i * 100 }} out:fade={{ duration: 200 }}>
|
<div in:fly={{ y: 20, duration: 400, delay: i * 100 }} out:fade={{ duration: 200 }}>
|
||||||
<Card
|
<Card
|
||||||
@ -359,7 +256,7 @@
|
|||||||
class="absolute right-2 top-2 rounded-full bg-white/80 p-2 transition-all duration-300 hover:scale-110 hover:bg-white"
|
class="absolute right-2 top-2 rounded-full bg-white/80 p-2 transition-all duration-300 hover:scale-110 hover:bg-white"
|
||||||
on:click|stopPropagation={() => toggleFavorite(product)}
|
on:click|stopPropagation={() => toggleFavorite(product)}
|
||||||
>
|
>
|
||||||
{#if $favorites.some((item: Product) => item.id === product.id)}
|
{#if $userFavorites.some((fav) => fav.product === product.id)}
|
||||||
<div in:scale={{ duration: 200 }}>
|
<div in:scale={{ duration: 200 }}>
|
||||||
<HeartSolid class="h-6 w-6 text-red-500" />
|
<HeartSolid class="h-6 w-6 text-red-500" />
|
||||||
</div>
|
</div>
|
||||||
@ -385,7 +282,7 @@
|
|||||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
${product.price.toFixed(2)}
|
${product.price.toFixed(2)}
|
||||||
</p>
|
</p>
|
||||||
{#if $favorites.some((item: Product) => item.id === product.id)}
|
{#if $userFavorites.some((fav) => fav.product === product.id)}
|
||||||
<Badge color="red" class="animate-pulse">Favorite</Badge>
|
<Badge color="red" class="animate-pulse">Favorite</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -405,12 +302,14 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $filteredProducts.length === 0 && products.length === 0}
|
<!-- Empty state -->
|
||||||
|
{#if $filteredProducts.length === 0}
|
||||||
<div class="mt-8 text-center" in:fade>
|
<div class="mt-8 text-center" in:fade>
|
||||||
<p class="text-lg text-gray-600">No products found matching your criteria.</p>
|
<p class="text-lg text-gray-600">No products found matching your criteria.</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
{#if $totalPages > 1}
|
{#if $totalPages > 1}
|
||||||
<div class="mt-8 flex flex-wrap justify-center gap-2" in:fly={{ y: 20, duration: 800 }}>
|
<div class="mt-8 flex flex-wrap justify-center gap-2" in:fly={{ y: 20, duration: 800 }}>
|
||||||
<Button
|
<Button
|
||||||
@ -441,6 +340,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Toast notifications -->
|
||||||
{#if $showToast}
|
{#if $showToast}
|
||||||
<div
|
<div
|
||||||
class="fixed bottom-4 right-4 z-50"
|
class="fixed bottom-4 right-4 z-50"
|
||||||
@ -453,6 +353,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Image modal -->
|
||||||
<ImageModal
|
<ImageModal
|
||||||
bind:open={modalOpen}
|
bind:open={modalOpen}
|
||||||
imageUrl={selectedImage.url}
|
imageUrl={selectedImage.url}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import session from '$lib/session.svelte';
|
import { authService } from '$lib/services/api';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
export async function load() {
|
export async function load() {
|
||||||
const page = session.loggedIn() ? '/landing' : '/login';
|
const page = authService.isAuthenticated() ? '/landing' : '/login';
|
||||||
redirect(307, page);
|
redirect(307, page);
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,40 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Card, Button, Label, Input, Checkbox } from 'flowbite-svelte';
|
import { Card, Button, Label, Input, Checkbox } from 'flowbite-svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import sessionSvelte from '$lib/session.svelte';
|
import { authService } from '$lib/services/api'; // Import the authService from your API
|
||||||
|
|
||||||
let username: string = '';
|
let username: string = '';
|
||||||
let password: string = '';
|
let password: string = '';
|
||||||
let success: boolean = true;
|
let success: boolean = true;
|
||||||
let loading: boolean = false;
|
let loading: boolean = false;
|
||||||
|
let errorMessage: string = '';
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
|
errorMessage = 'Please fill in all fields.';
|
||||||
|
success = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
sessionSvelte.login(username, password);
|
errorMessage = '';
|
||||||
success = true;
|
|
||||||
loading = false;
|
|
||||||
|
|
||||||
if (success) {
|
try {
|
||||||
goto('/');
|
// Use the authService.login function to authenticate the user
|
||||||
|
const authResponse = await authService.login(username, password);
|
||||||
|
|
||||||
|
// If login is successful, store the token and user data (if needed)
|
||||||
|
localStorage.setItem('authToken', authResponse.token); // Store the token in localStorage
|
||||||
|
|
||||||
|
success = true;
|
||||||
|
goto('/'); // Redirect to the home page after successful login
|
||||||
|
} catch (error: any) {
|
||||||
|
// Handle login errors
|
||||||
|
success = false;
|
||||||
|
errorMessage = error.message || 'Login failed. Please try again.';
|
||||||
|
console.error('Login error:', error);
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -48,8 +63,12 @@
|
|||||||
Lost password?
|
Lost password?
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" class="w-full" on:click={login}>Login to your account</Button>
|
<Button type="submit" class="w-full" on:click={login} disabled={loading}>
|
||||||
<span class="self-center text-red-500" class:invisible={success}>Login failed</span>
|
{loading ? 'Logging in...' : 'Login to your account'}
|
||||||
|
</Button>
|
||||||
|
{#if !success}
|
||||||
|
<span class="self-center text-red-500">{errorMessage}</span>
|
||||||
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import session from '$lib/session.svelte';
|
import { authService } from '$lib/services/api';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
export async function load() {
|
export async function load() {
|
||||||
if (session.loggedIn()) {
|
if (authService.isAuthenticated()) {
|
||||||
redirect(307, '/landing');
|
redirect(307, '/landing');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user