diff --git a/src/components/Footer.svelte b/src/components/Footer.svelte new file mode 100644 index 0000000..13d8370 --- /dev/null +++ b/src/components/Footer.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/components/ShoppingCart.svelte b/src/components/ShoppingCart.svelte new file mode 100644 index 0000000..714e4ce --- /dev/null +++ b/src/components/ShoppingCart.svelte @@ -0,0 +1,136 @@ + + + +
+ +
+ + +{#if isOpen} +
+ +

Shopping Cart

+ + {#if $cart.length === 0} +

Your cart is empty

+ + {:else} + + +
+
+ Total: + ${total.toFixed(2)} +
+
+ +
+ + +
+ {/if} +
+{/if} diff --git a/src/components/imageModal.svelte b/src/components/imageModal.svelte new file mode 100644 index 0000000..beecfe3 --- /dev/null +++ b/src/components/imageModal.svelte @@ -0,0 +1,28 @@ + + + +
+ {title} +
+

{title}

+
+
+
diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index 856f2b6..0000000 --- a/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/services/api.ts b/src/lib/services/api.ts new file mode 100644 index 0000000..577deb2 --- /dev/null +++ b/src/lib/services/api.ts @@ -0,0 +1,262 @@ +import PocketBase from 'pocketbase'; + +// Types for our data models +interface Product { + id: string; + name: string; + description: string; + price: number; + stock: number; + category: string; + images: string[]; + variants?: Record[]; + metadata?: Record; + createdAt: string; + updatedAt: string; +} + +interface Order { + id: string; + userId: string; + items: OrderItem[]; + status: OrderStatus; + total: number; + shippingAddress: Address; + billingAddress: Address; + paymentInfo: PaymentInfo; + createdAt: string; + updatedAt: string; +} + +interface OrderItem { + productId: string; + quantity: number; + price: number; + variants?: Record; +} + +interface User { + id: string; + email: string; + name: string; + addresses: Address[]; + orders?: string[]; + wishlist?: string[]; + metadata?: Record; +} + +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, 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(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)); + } + + updateCartItem(productId: string, quantity: number) { + const cart = this.getCart(); + const item = cart.find((i) => i.productId === productId); + + if (item) { + item.quantity = quantity; + localStorage.setItem('cart', JSON.stringify(cart)); + } + } + + removeFromCart(productId: string) { + const cart = this.getCart(); + const updatedCart = cart.filter((i) => i.productId !== productId); + localStorage.setItem('cart', JSON.stringify(updatedCart)); + } + + clearCart() { + localStorage.removeItem('cart'); + } + + // Order methods + async createOrder(orderData: Partial) { + return await this.pb.collection('orders').create(orderData); + } + + async getOrder(id: string) { + return await this.pb.collection('orders').getOne(id); + } + + async getUserOrders(userId: string, page: number = 1, perPage: number = 20) { + return await this.pb.collection('orders').getList(page, perPage, { + filter: `userId = "${userId}"`, + 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(this.pb.authStore.model?.id as string); + } + + async updateUserProfile(userId: string, userData: Partial) { + 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) { + return await this.pb.collection('products').create(productData); + } + + async updateProduct(id: string, productData: Partial) { + 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; diff --git a/src/lib/stores/cartStore.ts b/src/lib/stores/cartStore.ts new file mode 100644 index 0000000..6403e88 --- /dev/null +++ b/src/lib/stores/cartStore.ts @@ -0,0 +1,47 @@ +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([]); + +// 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([]); +}; diff --git a/src/lib/stores/store.ts b/src/lib/stores/store.ts new file mode 100644 index 0000000..9c512db --- /dev/null +++ b/src/lib/stores/store.ts @@ -0,0 +1,86 @@ +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(initialState); +export const favorites = writable([]); + +// 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: [] })); +}; diff --git a/src/lib/types.ts b/src/lib/types.ts deleted file mode 100644 index 08d2663..0000000 --- a/src/lib/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface User { - username: string; - password: string; -} diff --git a/src/lib/utils/clickOutside.ts b/src/lib/utils/clickOutside.ts new file mode 100644 index 0000000..4de5ec8 --- /dev/null +++ b/src/lib/utils/clickOutside.ts @@ -0,0 +1,18 @@ +export function clickOutside(node: HTMLElement, handler: () => void) { + const handleClick = (event: MouseEvent) => { + if (node && !node.contains(event.target as Node)) { + handler(); + } + }; + + document.addEventListener('click', handleClick, true); + + return { + destroy() { + document.removeEventListener('click', handleClick, true); + }, + update(newHandler: () => void) { + handler = newHandler; + } + }; +} diff --git a/src/routes/(auth)/+layout.ts b/src/routes/(auth)/+layout.ts new file mode 100644 index 0000000..342fa15 --- /dev/null +++ b/src/routes/(auth)/+layout.ts @@ -0,0 +1,8 @@ +import session from "$lib/session.svelte"; +import { redirect } from "@sveltejs/kit"; + +export async function load() { + if (!session.loggedIn()) { + redirect(307, "/login"); + } +} diff --git a/src/routes/(auth)/about/+page.svelte b/src/routes/(auth)/about/+page.svelte new file mode 100644 index 0000000..2b07a22 --- /dev/null +++ b/src/routes/(auth)/about/+page.svelte @@ -0,0 +1,45 @@ + + +
+
+

About Us

+

+ We are passionate about bringing beauty and joy to your life through fresh, handpicked + flowers. +

+ +
+
+ +
+
+
+
+

Our Story

+

+ Founded in 2023, Flower Shop started as a small family business with a mission to deliver + fresh, high-quality flowers to our community. Over the years, we've grown into a trusted + online florist, serving customers across the country. +

+

+ We believe that flowers have the power to brighten any day, and we're committed to making + every bouquet special. +

+
+
+ Our Story +
+
+
+
diff --git a/src/routes/(auth)/checkout/+page.svelte b/src/routes/(auth)/checkout/+page.svelte new file mode 100644 index 0000000..f319c51 --- /dev/null +++ b/src/routes/(auth)/checkout/+page.svelte @@ -0,0 +1,192 @@ + + +
+

Checkout

+ + {#if $cart.length === 0} + + Your cart is empty. Please add items before checking out. + + {:else} +
+ + +

Order Summary

+
    + {#each $cart as item} +
  • +
    + + + item openImageModal(item)} + on:keydown={(e) => e.key === 'Enter' && openImageModal(item)} + tabindex="0" + /> +
    +

    {item.title}

    +

    Quantity: {item.quantity}

    +
    +
    + + ${(item.price * item.quantity).toFixed(2)} + +
  • + {/each} +
+
+
+ Total: + ${total.toFixed(2)} +
+
+
+ + + +

Customer Details

+
+ + + + + + +
+ +
+ {#each [{ value: 'credit_card', label: 'Credit Card', icon: '💳' }, { value: 'paypal', label: 'PayPal', icon: '🅿️' }, { value: 'cash_on_delivery', label: 'Cash on Delivery', icon: '💵' }] as method} + + {/each} +
+
+ + {#if error} + + {error} + + {/if} + + {#if success} + + {success} + + {/if} + + +
+
+
+ {/if} +
diff --git a/src/routes/(auth)/contact/+page.svelte b/src/routes/(auth)/contact/+page.svelte new file mode 100644 index 0000000..78168e0 --- /dev/null +++ b/src/routes/(auth)/contact/+page.svelte @@ -0,0 +1,43 @@ + + +
+
+

Contact Us

+

Have questions or need assistance? We're here to help!

+
+
+ +
+
+
+ + +