init
This commit is contained in:
parent
23a1debeb7
commit
98aa111734
21
src/components/Footer.svelte
Normal file
21
src/components/Footer.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
import { Footer, FooterCopyright, FooterLinkGroup, FooterLink } from 'flowbite-svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Footer class="absolute bottom-0 w-full p-4">
|
||||||
|
<div class="sm:flex sm:items-center sm:justify-between">
|
||||||
|
<FooterCopyright
|
||||||
|
href="/"
|
||||||
|
by="edv-ring Gesellschaft für Hard- und Softwarelösungen im Gartenbau mbH"
|
||||||
|
year={2025}
|
||||||
|
/>
|
||||||
|
<FooterLinkGroup
|
||||||
|
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="/">Privacy Policy</FooterLink>
|
||||||
|
<FooterLink href="/">Terms & Conditions</FooterLink>
|
||||||
|
<FooterLink href="/">Contact</FooterLink>
|
||||||
|
</FooterLinkGroup>
|
||||||
|
</div>
|
||||||
|
</Footer>
|
136
src/components/ShoppingCart.svelte
Normal file
136
src/components/ShoppingCart.svelte
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fly, fade } from 'svelte/transition';
|
||||||
|
import { Button } from 'flowbite-svelte';
|
||||||
|
import { cart, clearCart, removeFromCart, updateQuantity } from '$lib/stores/cartStore';
|
||||||
|
import { ArrowRightOutline, CartOutline } from 'flowbite-svelte-icons';
|
||||||
|
import { clickOutside } from '$lib/utils/clickOutside';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
let isOpen = false;
|
||||||
|
let isUpdating = false;
|
||||||
|
let updateTimeout: NodeJS.Timeout;
|
||||||
|
|
||||||
|
// Calculate the total price
|
||||||
|
$: total = $cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||||
|
|
||||||
|
// Debounced quantity update
|
||||||
|
const handleQuantityChange = (id: number, quantity: number) => {
|
||||||
|
clearTimeout(updateTimeout);
|
||||||
|
isUpdating = true;
|
||||||
|
|
||||||
|
updateTimeout = setTimeout(() => {
|
||||||
|
updateQuantity(id, quantity);
|
||||||
|
isUpdating = false;
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickOutside = () => {
|
||||||
|
isOpen = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Animation for item removal
|
||||||
|
const removeItem = async (id: number) => {
|
||||||
|
const itemElement = document.getElementById(`cart-item-${id}`);
|
||||||
|
if (itemElement) {
|
||||||
|
itemElement.style.transition = 'all 0.3s ease-out';
|
||||||
|
itemElement.style.opacity = '0';
|
||||||
|
itemElement.style.transform = 'translateX(20px)';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
removeFromCart(id);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navigate to the webshop
|
||||||
|
const goToShop = () => {
|
||||||
|
isOpen = !isOpen;
|
||||||
|
goto('/main');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Cart Icon Button with Badge -->
|
||||||
|
<div class="relative">
|
||||||
|
<Button on:click={() => (isOpen = !isOpen)} class="relative h-12">
|
||||||
|
<CartOutline size="lg" />
|
||||||
|
{#if $cart.length > 0}
|
||||||
|
<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"
|
||||||
|
transition:fade
|
||||||
|
>
|
||||||
|
{$cart.length}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cart Popup -->
|
||||||
|
{#if isOpen}
|
||||||
|
<div
|
||||||
|
transition:fly={{ y: 50, duration: 300 }}
|
||||||
|
class="fixed right-40 top-20 z-50 w-96 rounded-lg bg-white p-4 shadow-lg"
|
||||||
|
use:clickOutside={handleClickOutside}
|
||||||
|
>
|
||||||
|
<!-- Rest of the cart popup content remains the same -->
|
||||||
|
<h2 class="mb-4 text-lg font-bold">Shopping Cart</h2>
|
||||||
|
|
||||||
|
{#if $cart.length === 0}
|
||||||
|
<p class="py-4 text-center text-gray-500">Your cart is empty</p>
|
||||||
|
<Button size="xl" on:click={goToShop}>
|
||||||
|
Shop Now <ArrowRightOutline class="ml-2 h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<ul class="max-h-96 space-y-3 overflow-y-auto">
|
||||||
|
{#each $cart as item (item.id)}
|
||||||
|
<li
|
||||||
|
id="cart-item-{item.id}"
|
||||||
|
class="flex items-center justify-between rounded-lg bg-gray-50 p-3 transition-all duration-200 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="font-semibold">{item.title}</h3>
|
||||||
|
<p class="text-sm text-gray-600">${item.price.toFixed(2)} each</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={item.quantity}
|
||||||
|
min="1"
|
||||||
|
on:input={(e) => handleQuantityChange(item.id, parseInt(e.currentTarget.value))}
|
||||||
|
class="w-16 rounded border p-1 text-center"
|
||||||
|
/>
|
||||||
|
{#if isUpdating}
|
||||||
|
<span
|
||||||
|
class="absolute -top-1 right-0 h-2 w-2 animate-pulse rounded-full bg-blue-500"
|
||||||
|
></span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
on:click={() => removeItem(item.id)}
|
||||||
|
class="rounded p-1 text-red-500 transition-colors hover:bg-red-50 hover:text-red-700"
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-4 border-t pt-4">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="font-semibold">Total:</span>
|
||||||
|
<span class="text-lg font-bold">${total.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 space-y-2">
|
||||||
|
<Button href="/checkout" on:click={() => (isOpen = !isOpen)} class="w-full"
|
||||||
|
>Proceed to Checkout</Button
|
||||||
|
>
|
||||||
|
<Button on:click={() => clearCart()} color="red" class="w-full" variant="outline">
|
||||||
|
Clear Cart
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
28
src/components/imageModal.svelte
Normal file
28
src/components/imageModal.svelte
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Modal, Button } from 'flowbite-svelte';
|
||||||
|
import { fade, scale } from 'svelte/transition';
|
||||||
|
import { quintOut } from 'svelte/easing';
|
||||||
|
|
||||||
|
export let open = false;
|
||||||
|
export let imageUrl: string = '';
|
||||||
|
export let title: string = '';
|
||||||
|
export let onClose: () => void;
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:open size="xl" autoclose class="w-full max-w-4xl" on:close={handleClose}>
|
||||||
|
<div class="relative" in:scale={{ duration: 300, easing: quintOut }} out:fade={{ duration: 200 }}>
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt={title}
|
||||||
|
class="h-full w-full rounded-lg object-cover p-4"
|
||||||
|
in:scale={{ duration: 400, delay: 100, easing: quintOut }}
|
||||||
|
/>
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">{title}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
@ -1 +0,0 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
262
src/lib/services/api.ts
Normal file
262
src/lib/services/api.ts
Normal file
@ -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<string, unknown>[];
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
addresses: Address[];
|
||||||
|
orders?: string[];
|
||||||
|
wishlist?: string[];
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Order>) {
|
||||||
|
return await this.pb.collection('orders').create(orderData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrder(id: string) {
|
||||||
|
return await this.pb.collection('orders').getOne<Order>(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<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;
|
47
src/lib/stores/cartStore.ts
Normal file
47
src/lib/stores/cartStore.ts
Normal file
@ -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<Product[]>([]);
|
||||||
|
|
||||||
|
// Add a product to the cart
|
||||||
|
export const addToCart = (product: Product) => {
|
||||||
|
cart.update((items) => {
|
||||||
|
const existingItem = items.find((item) => item.id === product.id);
|
||||||
|
if (existingItem) {
|
||||||
|
// If the product already exists, increase the quantity
|
||||||
|
return items.map((item) =>
|
||||||
|
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If the product doesn't exist, add it to the cart
|
||||||
|
return [...items, { ...product, quantity: 1 }];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove a product from the cart
|
||||||
|
export const removeFromCart = (productId: number) => {
|
||||||
|
cart.update((items) => items.filter((item) => item.id !== productId));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the quantity of a product in the cart
|
||||||
|
export const updateQuantity = (productId: number, quantity: number) => {
|
||||||
|
cart.update((items) =>
|
||||||
|
items.map((item) => (item.id === productId ? { ...item, quantity } : item))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear the cart
|
||||||
|
export const clearCart = () => {
|
||||||
|
cart.set([]);
|
||||||
|
};
|
86
src/lib/stores/store.ts
Normal file
86
src/lib/stores/store.ts
Normal file
@ -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<AppState>(initialState);
|
||||||
|
export const favorites = writable<Product[]>([]);
|
||||||
|
|
||||||
|
// Add a product to the cart
|
||||||
|
export const addToCart = (product: Product) => {
|
||||||
|
appStore.update((state) => {
|
||||||
|
const existingItem = state.cart.find((item) => item.id === product.id);
|
||||||
|
if (existingItem) {
|
||||||
|
// If the product already exists, increase the quantity
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cart: state.cart.map((item) =>
|
||||||
|
item.id === product.id ? { ...item, quantity: (item.quantity || 1) + 1 } : item
|
||||||
|
)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// If the product doesn't exist, add it to the cart with quantity 1
|
||||||
|
return { ...state, cart: [...state.cart, { ...product, quantity: 1 }] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove a product from the cart
|
||||||
|
export const removeFromCart = (productId: number) => {
|
||||||
|
appStore.update((state) => ({
|
||||||
|
...state,
|
||||||
|
cart: state.cart.filter((item) => item.id !== productId)
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the quantity of a product in the cart
|
||||||
|
export const updateCartQuantity = (productId: number, quantity: number) => {
|
||||||
|
appStore.update((state) => ({
|
||||||
|
...state,
|
||||||
|
cart: state.cart.map((item) => (item.id === productId ? { ...item, quantity } : item))
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a product to favorites
|
||||||
|
export const addToFavorites = (product: Product) => {
|
||||||
|
favorites.update((items) => {
|
||||||
|
if (!items.some((item) => item.id === product.id)) {
|
||||||
|
return [...items, product];
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove a product from favorites
|
||||||
|
export const removeFromFavorites = (productId: number) => {
|
||||||
|
favorites.update((items) => items.filter((item) => item.id !== productId));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear the cart
|
||||||
|
export const clearCart = () => {
|
||||||
|
appStore.update((state) => ({ ...state, cart: [] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear favorites
|
||||||
|
export const clearFavorites = () => {
|
||||||
|
favorites.update((state) => ({ ...state, favorites: [] }));
|
||||||
|
};
|
@ -1,4 +0,0 @@
|
|||||||
export interface User {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
18
src/lib/utils/clickOutside.ts
Normal file
18
src/lib/utils/clickOutside.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
8
src/routes/(auth)/+layout.ts
Normal file
8
src/routes/(auth)/+layout.ts
Normal file
@ -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");
|
||||||
|
}
|
||||||
|
}
|
45
src/routes/(auth)/about/+page.svelte
Normal file
45
src/routes/(auth)/about/+page.svelte
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { Button } from 'flowbite-svelte';
|
||||||
|
|
||||||
|
const goToShop = () => {
|
||||||
|
goto('/shop');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<h1 class="mb-4 text-5xl font-bold text-gray-900">About Us</h1>
|
||||||
|
<p class="mb-8 text-xl text-gray-600">
|
||||||
|
We are passionate about bringing beauty and joy to your life through fresh, handpicked
|
||||||
|
flowers.
|
||||||
|
</p>
|
||||||
|
<Button size="xl" on:click={goToShop}>Shop Now</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="py-16">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<h2 class="mb-4 text-3xl font-bold text-gray-900">Our Story</h2>
|
||||||
|
<p class="mb-4 text-gray-600">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p class="mb-4 text-gray-600">
|
||||||
|
We believe that flowers have the power to brighten any day, and we're committed to making
|
||||||
|
every bouquet special.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1520763185298-1b434c919102?q=80&w=1932&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||||
|
alt="Our Story"
|
||||||
|
class="rounded-lg shadow-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
192
src/routes/(auth)/checkout/+page.svelte
Normal file
192
src/routes/(auth)/checkout/+page.svelte
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { clearCart, cart, type Product } from '$lib/stores/cartStore';
|
||||||
|
import { Card, Input, Button, Alert } from 'flowbite-svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
let formData = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
address: '',
|
||||||
|
paymentMethod: 'credit_card'
|
||||||
|
};
|
||||||
|
|
||||||
|
let isProcessing = false;
|
||||||
|
let error = '';
|
||||||
|
let success = '';
|
||||||
|
|
||||||
|
// Calculate the total price
|
||||||
|
$: total = $cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
$: isFormValid =
|
||||||
|
formData.name.length > 0 &&
|
||||||
|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) &&
|
||||||
|
formData.address.length > 0;
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
const handleCheckout = async () => {
|
||||||
|
if (!isFormValid) {
|
||||||
|
error = 'Please fill in all required fields correctly.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessing = true;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
const checkoutRequest = {
|
||||||
|
...formData,
|
||||||
|
items: $cart
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
clearCart();
|
||||||
|
goto(`/order-confirmation/12345`);
|
||||||
|
} catch (err) {
|
||||||
|
error = 'Failed to process checkout. Please try again.';
|
||||||
|
} finally {
|
||||||
|
isProcessing = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let modalOpen = false;
|
||||||
|
let selectedImage = {
|
||||||
|
url: '',
|
||||||
|
title: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const openImageModal = (product: Product) => {
|
||||||
|
selectedImage = {
|
||||||
|
url: product.imageUrl,
|
||||||
|
title: product.title
|
||||||
|
};
|
||||||
|
modalOpen = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeImageModal = () => {
|
||||||
|
modalOpen = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="mx-auto max-w-4xl p-6">
|
||||||
|
<h1 class="mb-6 text-3xl font-bold">Checkout</h1>
|
||||||
|
|
||||||
|
{#if $cart.length === 0}
|
||||||
|
<Alert color="red" class="mb-6">
|
||||||
|
Your cart is empty. Please add items before checking out.
|
||||||
|
</Alert>
|
||||||
|
{:else}
|
||||||
|
<div class="flex gap-10">
|
||||||
|
<!-- Order Summary -->
|
||||||
|
<Card class="mb-6">
|
||||||
|
<h2 class="mb-4 text-xl font-semibold">Order Summary</h2>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
{#each $cart as item}
|
||||||
|
<li class="flex items-center justify-between rounded-lg bg-gray-50 p-3">
|
||||||
|
<div class="flex">
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
|
<img
|
||||||
|
src={item.imageUrl}
|
||||||
|
alt="item"
|
||||||
|
class="me-2 h-20 w-20"
|
||||||
|
on:click={() => openImageModal(item)}
|
||||||
|
on:keydown={(e) => e.key === 'Enter' && openImageModal(item)}
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">{item.title}</h3>
|
||||||
|
<p class="text-sm text-gray-600">Quantity: {item.quantity}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-lg font-medium">
|
||||||
|
${(item.price * item.quantity).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<div class="mt-4 border-t pt-4">
|
||||||
|
<div class="flex justify-between text-lg">
|
||||||
|
<strong>Total:</strong>
|
||||||
|
<span class="font-bold">${total.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Customer Details Form -->
|
||||||
|
<Card class="mb-6">
|
||||||
|
<h2 class="mb-4 text-xl font-semibold">Customer Details</h2>
|
||||||
|
<form on:submit|preventDefault={handleCheckout} class="space-y-4">
|
||||||
|
<Input
|
||||||
|
label="Full Name"
|
||||||
|
bind:value={formData.name}
|
||||||
|
placeholder="John Doe"
|
||||||
|
required
|
||||||
|
error={!formData.name && 'Name is required'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
bind:value={formData.email}
|
||||||
|
placeholder="john@example.com"
|
||||||
|
required
|
||||||
|
error={formData.email &&
|
||||||
|
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) &&
|
||||||
|
'Please enter a valid email'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Shipping Address"
|
||||||
|
bind:value={formData.address}
|
||||||
|
placeholder="123 Main St, City, Country"
|
||||||
|
required
|
||||||
|
error={!formData.address && 'Address is required'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block font-medium">Payment Method</label>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each [{ value: 'credit_card', label: 'Credit Card', icon: '💳' }, { value: 'paypal', label: 'PayPal', icon: '🅿️' }, { value: 'cash_on_delivery', label: 'Cash on Delivery', icon: '💵' }] as method}
|
||||||
|
<label
|
||||||
|
class="flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
bind:group={formData.paymentMethod}
|
||||||
|
value={method.value}
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<span>{method.icon}</span>
|
||||||
|
<span>{method.label}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<Alert color="red">
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if success}
|
||||||
|
<Alert color="green">
|
||||||
|
{success}
|
||||||
|
</Alert>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Button type="submit" color="blue" class="w-full" disabled={isProcessing || !isFormValid}>
|
||||||
|
{#if isProcessing}
|
||||||
|
<span class="inline-block animate-spin">⌛</span>
|
||||||
|
Processing...
|
||||||
|
{:else}
|
||||||
|
Place Order (${total.toFixed(2)})
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</main>
|
43
src/routes/(auth)/contact/+page.svelte
Normal file
43
src/routes/(auth)/contact/+page.svelte
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button, Input, Textarea } from 'flowbite-svelte';
|
||||||
|
|
||||||
|
let name = '';
|
||||||
|
let email = '';
|
||||||
|
let message = '';
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
alert('Thank you for contacting us! We will get back to you soon.');
|
||||||
|
name = '';
|
||||||
|
email = '';
|
||||||
|
message = '';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<h1 class="mb-4 text-5xl font-bold text-gray-900">Contact Us</h1>
|
||||||
|
<p class="mb-8 text-xl text-gray-600">Have questions or need assistance? We're here to help!</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="py-16">
|
||||||
|
<div class="container mx-auto max-w-2xl px-4">
|
||||||
|
<form on:submit|preventDefault={handleSubmit} class="space-y-6">
|
||||||
|
<Input label="Your Name" bind:value={name} placeholder="John Doe" required />
|
||||||
|
<Input
|
||||||
|
label="Your Email"
|
||||||
|
type="email"
|
||||||
|
bind:value={email}
|
||||||
|
placeholder="john@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
label="Your Message"
|
||||||
|
bind:value={message}
|
||||||
|
placeholder="How can we help you?"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button type="submit" class="w-full">Send Message</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
44
src/routes/(auth)/faq/+page.svelte
Normal file
44
src/routes/(auth)/faq/+page.svelte
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
const faqs = [
|
||||||
|
{
|
||||||
|
question: 'How do I place an order?',
|
||||||
|
answer:
|
||||||
|
'You can place an order directly through our website by selecting your desired products and proceeding to checkout.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'What payment methods do you accept?',
|
||||||
|
answer: 'We accept all major credit cards, PayPal, and Apple Pay.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Do you offer international shipping?',
|
||||||
|
answer: 'Currently, we only ship within the United States.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Can I cancel or change my order?',
|
||||||
|
answer:
|
||||||
|
'You can cancel or change your order within 1 hour of placing it. Please contact us immediately for assistance.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<h1 class="mb-4 text-5xl font-bold text-gray-900">Frequently Asked Questions</h1>
|
||||||
|
<p class="mb-8 text-xl text-gray-600">
|
||||||
|
Find answers to common questions about our products and services.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="py-16">
|
||||||
|
<div class="container mx-auto max-w-2xl px-4">
|
||||||
|
<div class="space-y-6">
|
||||||
|
{#each faqs as faq}
|
||||||
|
<div class="rounded-lg bg-white p-6 shadow-lg">
|
||||||
|
<h3 class="mb-2 text-xl font-bold text-gray-900">{faq.question}</h3>
|
||||||
|
<p class="text-gray-600">{faq.answer}</p>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
33
src/routes/(auth)/favourites/+page.svelte
Normal file
33
src/routes/(auth)/favourites/+page.svelte
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { favorites, removeFromFavorites, clearFavorites } from '$lib/stores/store';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="p-6">
|
||||||
|
<h1 class="mb-6 text-3xl font-bold">Favorites</h1>
|
||||||
|
{#if $favorites.length > 0}
|
||||||
|
<ul class="space-y-4">
|
||||||
|
{#each $favorites as item}
|
||||||
|
<li class="flex items-center justify-between border-b pb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold">{item.title}</h2>
|
||||||
|
<p class="text-gray-600">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
on:click={() => removeFromFavorites(item.id)}
|
||||||
|
class="text-red-500 hover:text-red-700"
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
on:click={clearFavorites}
|
||||||
|
class="mt-4 w-full rounded bg-red-500 p-2 text-white hover:bg-red-600"
|
||||||
|
>
|
||||||
|
Clear Favorites
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<p class="text-gray-600">You have no favorite products.</p>
|
||||||
|
{/if}
|
||||||
|
</main>
|
313
src/routes/(auth)/item/[id]/+page.svelte
Normal file
313
src/routes/(auth)/item/[id]/+page.svelte
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { addToCart } from '$lib/stores/cartStore';
|
||||||
|
import { Card, Button, Badge } from 'flowbite-svelte';
|
||||||
|
import { CartPlusOutline, HeartOutline, HeartSolid } from 'flowbite-svelte-icons';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { addToFavorites, favorites, removeFromFavorites } from '$lib/stores/store';
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
imageUrl: string;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let product: Product | undefined;
|
||||||
|
let relatedProducts: Product[] = [];
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const productId = parseInt($page.params.id);
|
||||||
|
|
||||||
|
product = products.find((item) => item.id === productId);
|
||||||
|
|
||||||
|
relatedProducts = products
|
||||||
|
.filter((item) => item.id !== productId) // Exclude the current product
|
||||||
|
.sort(() => 0.5 - Math.random()) // Shuffle the array
|
||||||
|
.slice(0, 3); // Get the first 3 items
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle favorite toggling
|
||||||
|
const toggleFavorite = () => {
|
||||||
|
if (product && $favorites.some((item: any) => item.id === product!.id)) {
|
||||||
|
removeFromFavorites(product.id);
|
||||||
|
} else if (product) {
|
||||||
|
addToFavorites(product);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to cart with feedback
|
||||||
|
const handleAddToCart = () => {
|
||||||
|
if (product) {
|
||||||
|
addToCart({ ...product, quantity: 1 });
|
||||||
|
// You could add a toast notification here
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="container mx-auto px-4 py-8">
|
||||||
|
{#if product}
|
||||||
|
<!-- Product Details Section -->
|
||||||
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||||
|
<!-- Product Image -->
|
||||||
|
<div class="relative aspect-[4/3] w-full overflow-hidden rounded-lg">
|
||||||
|
<img
|
||||||
|
src={product.imageUrl}
|
||||||
|
alt={product.title}
|
||||||
|
class="absolute h-full w-full object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Product Information -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h1 class="text-4xl font-bold text-gray-900 dark:text-white">{product.title}</h1>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-400">{product.description}</p>
|
||||||
|
<p class="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
|
${product.price.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<Button class="w-full" on:click={handleAddToCart}>
|
||||||
|
<CartPlusOutline class="mr-2 h-5 w-5" />
|
||||||
|
Add to Cart
|
||||||
|
</Button>
|
||||||
|
<Button class="w-full" on:click={toggleFavorite}>
|
||||||
|
{#if $favorites.some((item) => item.id === product!.id)}
|
||||||
|
<HeartSolid class="mr-2 h-5 w-5 text-red-500" />
|
||||||
|
{:else}
|
||||||
|
<HeartOutline class="mr-2 h-5 w-5 text-gray-500" />
|
||||||
|
{/if}
|
||||||
|
{#if $favorites.some((item) => item.id === product!.id)}
|
||||||
|
Remove from Favorites
|
||||||
|
{:else}
|
||||||
|
Add to Favorites
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Details -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Product Details</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
|
||||||
|
incididunt ut labore et dolore magna aliqua.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Related Products Section -->
|
||||||
|
<div class="mt-12">
|
||||||
|
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Related Products</h2>
|
||||||
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{#each relatedProducts as relatedProduct (relatedProduct.id)}
|
||||||
|
<Card padding="none" class="h-full overflow-hidden">
|
||||||
|
<!-- Image Container -->
|
||||||
|
<div class="relative aspect-[4/3] w-full overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={relatedProduct.imageUrl}
|
||||||
|
alt={relatedProduct.title}
|
||||||
|
class="absolute h-full w-full object-cover transition-transform duration-300 hover:scale-110"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content Section -->
|
||||||
|
<div class="flex h-full flex-col p-4">
|
||||||
|
<div class="flex-grow space-y-2">
|
||||||
|
<h5 class="text-xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||||
|
{relatedProduct.title}
|
||||||
|
</h5>
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-400">
|
||||||
|
{relatedProduct.description}
|
||||||
|
</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
${relatedProduct.price.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<Button
|
||||||
|
class="w-full"
|
||||||
|
on:click={() => addToCart({ ...relatedProduct, quantity: 1 })}
|
||||||
|
>
|
||||||
|
<CartPlusOutline class="mr-2 h-5 w-5" />
|
||||||
|
Add to Cart
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-gray-600">Loading product details...</p>
|
||||||
|
{/if}
|
||||||
|
</main>
|
152
src/routes/(auth)/landing/+page.svelte
Normal file
152
src/routes/(auth)/landing/+page.svelte
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { Button } from 'flowbite-svelte';
|
||||||
|
import { ArrowRightOutline, StarSolid, CheckCircleOutline } from 'flowbite-svelte-icons';
|
||||||
|
|
||||||
|
// Example featured products
|
||||||
|
const featuredProducts = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Tulip Bouquet',
|
||||||
|
description: 'Brighten your day with fresh tulips.',
|
||||||
|
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: 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Rose Bouquet',
|
||||||
|
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: 35
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Sunflower Bouquet',
|
||||||
|
description: 'Bright sunflowers to light up your day.',
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1563013544-824ae1b704d3?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
||||||
|
price: 30
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Example testimonials
|
||||||
|
const testimonials = [
|
||||||
|
{
|
||||||
|
name: 'Jane Doe',
|
||||||
|
review:
|
||||||
|
'The flowers were absolutely stunning! Delivery was fast, and the customer service was excellent.',
|
||||||
|
rating: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'John Smith',
|
||||||
|
review: 'I ordered roses for my wife, and she loved them! Highly recommend this shop.',
|
||||||
|
rating: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Emily Johnson',
|
||||||
|
review: 'The sunflowers brightened up my home. Will definitely order again!',
|
||||||
|
rating: 5
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Navigate to the webshop
|
||||||
|
const goToShop = () => {
|
||||||
|
goto('/main');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<h1 class="mb-4 text-5xl font-bold text-gray-900">Beautiful Flowers for Every Occasion</h1>
|
||||||
|
<p class="mb-8 text-xl text-gray-600">
|
||||||
|
Fresh, handpicked flowers delivered to your doorstep. Make every moment special with our
|
||||||
|
stunning bouquets.
|
||||||
|
</p>
|
||||||
|
<Button size="xl" on:click={goToShop}>
|
||||||
|
Shop Now <ArrowRightOutline class="ml-2 h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Value Proposition Section -->
|
||||||
|
<section class="py-16">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||||
|
<div class="text-center">
|
||||||
|
<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>
|
||||||
|
<p class="text-gray-600">We source the freshest flowers directly from local growers.</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<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>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Get your flowers delivered the same day or schedule a future delivery.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<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>
|
||||||
|
<p class="text-gray-600">We stand by our products with a 100% satisfaction guarantee.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Featured Products Section -->
|
||||||
|
<section class="bg-gray-50 py-16">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<h2 class="mb-8 text-center text-3xl font-bold text-gray-900">Featured Products</h2>
|
||||||
|
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{#each featuredProducts as product}
|
||||||
|
<div class="overflow-hidden rounded-lg bg-white shadow-lg">
|
||||||
|
<img src={product.imageUrl} alt={product.title} class="h-64 w-full object-cover" />
|
||||||
|
<div class="p-6">
|
||||||
|
<h3 class="mb-2 text-xl font-bold text-gray-900">{product.title}</h3>
|
||||||
|
<p class="mb-4 text-gray-600">{product.description}</p>
|
||||||
|
<p class="mb-4 text-2xl font-bold text-gray-900">${product.price.toFixed(2)}</p>
|
||||||
|
<Button class="w-full" on:click={goToShop}>
|
||||||
|
Shop Now <ArrowRightOutline class="ml-2 h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Testimonials Section -->
|
||||||
|
<section class="py-16">
|
||||||
|
<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>
|
||||||
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||||
|
{#each testimonials as testimonial}
|
||||||
|
<div class="rounded-lg bg-white p-6 shadow-lg">
|
||||||
|
<div class="mb-4 flex items-center">
|
||||||
|
{#each Array(testimonial.rating) as _}
|
||||||
|
<StarSolid class="h-5 w-5 text-yellow-400" />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<p class="mb-4 text-gray-600">"{testimonial.review}"</p>
|
||||||
|
<p class="text-lg font-bold text-gray-900">- {testimonial.name}</p>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA Section -->
|
||||||
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||||
|
<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>
|
||||||
|
<p class="mb-8 text-xl text-gray-600">
|
||||||
|
Shop our collection of beautiful flowers and make every moment special.
|
||||||
|
</p>
|
||||||
|
<Button size="xl" on:click={goToShop}>
|
||||||
|
Shop Now <ArrowRightOutline class="ml-2 h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
117
src/routes/(auth)/order-confirmation/[id]/+page.svelte
Normal file
117
src/routes/(auth)/order-confirmation/[id]/+page.svelte
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { Button } from 'flowbite-svelte';
|
||||||
|
import { ArrowRightOutline, CheckCircleOutline } from 'flowbite-svelte-icons';
|
||||||
|
|
||||||
|
// Example order data (replace with actual data from your store or API)
|
||||||
|
const order = {
|
||||||
|
id: '123456',
|
||||||
|
date: new Date().toLocaleDateString(),
|
||||||
|
status: 'Confirmed',
|
||||||
|
customer: {
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john.doe@example.com',
|
||||||
|
address: '123 Main St, City, Country'
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Tulip Bouquet',
|
||||||
|
quantity: 2,
|
||||||
|
price: 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Rose Bouquet',
|
||||||
|
quantity: 1,
|
||||||
|
price: 35
|
||||||
|
}
|
||||||
|
],
|
||||||
|
total: 85
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate total price
|
||||||
|
$: total = order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||||
|
|
||||||
|
// Navigate to the shop
|
||||||
|
const goToShop = () => {
|
||||||
|
goto('/main');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Confirmation Section -->
|
||||||
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<CheckCircleOutline class="mx-auto mb-4 h-16 w-16 text-green-500" />
|
||||||
|
<h1 class="mb-4 text-4xl font-bold text-gray-900">Order Confirmed!</h1>
|
||||||
|
<p class="mb-8 text-xl text-gray-600">
|
||||||
|
Thank you for your purchase. Your order has been successfully placed.
|
||||||
|
</p>
|
||||||
|
<Button size="xl" on:click={goToShop}>
|
||||||
|
Continue Shopping <ArrowRightOutline class="ml-2 h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Order Details Section -->
|
||||||
|
<section class="py-16">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="mx-auto max-w-3xl rounded-lg bg-white p-8 shadow-lg">
|
||||||
|
<h2 class="mb-6 text-2xl font-bold text-gray-900">Order Details</h2>
|
||||||
|
|
||||||
|
<!-- Order Summary -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900">Order Summary</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each order.items as item}
|
||||||
|
<div class="flex items-center justify-between border-b pb-4">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-lg font-semibold text-gray-900">{item.title}</h4>
|
||||||
|
<p class="text-gray-600">Quantity: {item.quantity}</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-bold text-gray-900">
|
||||||
|
${(item.price * item.quantity).toFixed(2)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 flex items-center justify-between">
|
||||||
|
<p class="text-xl font-bold text-gray-900">Total</p>
|
||||||
|
<p class="text-xl font-bold text-gray-900">${total.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Customer Details -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900">Customer Details</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-gray-900"><strong>Name:</strong> {order.customer.name}</p>
|
||||||
|
<p class="text-gray-900"><strong>Email:</strong> {order.customer.email}</p>
|
||||||
|
<p class="text-gray-900"><strong>Shipping Address:</strong> {order.customer.address}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Order Status -->
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900">Order Status</h3>
|
||||||
|
<p class="text-gray-900">
|
||||||
|
Your order is <strong class="text-green-500">{order.status}</strong>. You will receive a
|
||||||
|
confirmation email shortly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA Section -->
|
||||||
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<h2 class="mb-4 text-4xl font-bold text-gray-900">Continue Shopping</h2>
|
||||||
|
<p class="mb-8 text-xl text-gray-600">
|
||||||
|
Explore more beautiful flowers and make every moment special.
|
||||||
|
</p>
|
||||||
|
<Button size="xl" on:click={goToShop}>
|
||||||
|
Shop Now <ArrowRightOutline class="ml-2 h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
23
src/routes/(auth)/privacy-policy/+page.svelte
Normal file
23
src/routes/(auth)/privacy-policy/+page.svelte
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<h1 class="mb-4 text-5xl font-bold text-gray-900">Privacy Policy</h1>
|
||||||
|
<p class="mb-8 text-xl text-gray-600">
|
||||||
|
Your privacy is important to us. Learn how we protect your information.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="py-16">
|
||||||
|
<div class="container mx-auto max-w-2xl px-4">
|
||||||
|
<div class="rounded-lg bg-white p-6 shadow-lg">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900">Our Commitment to Privacy</h3>
|
||||||
|
<p class="mb-4 text-gray-600">
|
||||||
|
We are committed to protecting your personal information. This policy outlines how we
|
||||||
|
collect, use, and safeguard your data.
|
||||||
|
</p>
|
||||||
|
<p class="mb-4 text-gray-600">
|
||||||
|
We do not sell or share your information with third parties without your consent.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
29
src/routes/(auth)/returns-and-refunds/+page.svelte
Normal file
29
src/routes/(auth)/returns-and-refunds/+page.svelte
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<h1 class="mb-4 text-5xl font-bold text-gray-900">Returns and Refunds</h1>
|
||||||
|
<p class="mb-8 text-xl text-gray-600">
|
||||||
|
We want you to be completely satisfied with your purchase.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="py-16">
|
||||||
|
<div class="container mx-auto max-w-2xl px-4">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="rounded-lg bg-white p-6 shadow-lg">
|
||||||
|
<h3 class="mb-2 text-xl font-bold text-gray-900">Return Policy</h3>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
If you're not satisfied with your purchase, you can return it within 30 days for a full
|
||||||
|
refund. Please ensure the product is in its original condition.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-white p-6 shadow-lg">
|
||||||
|
<h3 class="mb-2 text-xl font-bold text-gray-900">Refund Process</h3>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Once we receive your return, we will process your refund within 5 business days. Refunds
|
||||||
|
will be issued to the original payment method.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
29
src/routes/(auth)/shipping-information/+page.svelte
Normal file
29
src/routes/(auth)/shipping-information/+page.svelte
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<h1 class="mb-4 text-5xl font-bold text-gray-900">Shipping Information</h1>
|
||||||
|
<p class="mb-8 text-xl text-gray-600">
|
||||||
|
Learn more about our shipping options and delivery times.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="py-16">
|
||||||
|
<div class="container mx-auto max-w-2xl px-4">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="rounded-lg bg-white p-6 shadow-lg">
|
||||||
|
<h3 class="mb-2 text-xl font-bold text-gray-900">Shipping Options</h3>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
We offer standard and express shipping options. Standard shipping takes 3-5 business days,
|
||||||
|
while express shipping delivers within 1-2 business days.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-white p-6 shadow-lg">
|
||||||
|
<h3 class="mb-2 text-xl font-bold text-gray-900">Delivery Areas</h3>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
We currently ship to all states within the United States. International shipping is not
|
||||||
|
available at this time.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
24
src/routes/(auth)/terms-of-service/+page.svelte
Normal file
24
src/routes/(auth)/terms-of-service/+page.svelte
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<h1 class="mb-4 text-5xl font-bold text-gray-900">Terms of Service</h1>
|
||||||
|
<p class="mb-8 text-xl text-gray-600">
|
||||||
|
Please read our terms and conditions carefully before using our services.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="py-16">
|
||||||
|
<div class="container mx-auto max-w-2xl px-4">
|
||||||
|
<div class="rounded-lg bg-white p-6 shadow-lg">
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900">Acceptance of Terms</h3>
|
||||||
|
<p class="mb-4 text-gray-600">
|
||||||
|
By using our website, you agree to these terms and conditions. If you do not agree, please
|
||||||
|
do not use our services.
|
||||||
|
</p>
|
||||||
|
<p class="mb-4 text-gray-600">
|
||||||
|
We reserve the right to modify these terms at any time. Continued use of our services
|
||||||
|
constitutes acceptance of the updated terms.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
16
src/routes/+error.svelte
Normal file
16
src/routes/+error.svelte
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { Button } from 'flowbite-svelte';
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
goto('/');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="bg-gradient-to-r from-pink-100 to-purple-100 py-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<h1 class="mb-4 text-5xl font-bold text-gray-900">404 - Page Not Found</h1>
|
||||||
|
<p class="mb-8 text-xl text-gray-600">Oops! The page you're looking for doesn't exist.</p>
|
||||||
|
<Button size="xl" on:click={goHome}>Go Home</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
55
src/routes/signup/+page.svelte
Normal file
55
src/routes/signup/+page.svelte
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Card, Button, Label, Input, Checkbox } from 'flowbite-svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import sessionSvelte from '$lib/session.svelte';
|
||||||
|
|
||||||
|
let username: string = '';
|
||||||
|
let password: string = '';
|
||||||
|
let success: boolean = true;
|
||||||
|
let loading: boolean = false;
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
if (!username || !password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
sessionSvelte.login(username, password);
|
||||||
|
success = true;
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
goto('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex h-[100svh] flex-wrap content-center">
|
||||||
|
<Card class="mx-auto max-h-fit">
|
||||||
|
<form class="flex flex-col space-y-6" action="/">
|
||||||
|
<h3 class="text-xl font-medium text-gray-900 dark:text-white">Sign in</h3>
|
||||||
|
<Label class="space-y-2">
|
||||||
|
<span>Email</span>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="name@company.com"
|
||||||
|
required
|
||||||
|
bind:value={username}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
<Label class="space-y-2">
|
||||||
|
<span>Your password</span>
|
||||||
|
<Input type="password" name="password" placeholder="•••••" required bind:value={password} />
|
||||||
|
</Label>
|
||||||
|
<div class="flex items-start">
|
||||||
|
<Checkbox>Remember me</Checkbox>
|
||||||
|
<a href="/" class="ms-auto text-sm text-primary-700 hover:underline dark:text-primary-500">
|
||||||
|
Lost password?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" class="w-full" on:click={login}>Login to your account</Button>
|
||||||
|
<span class="self-center text-red-500" class:invisible={success}>Login failed</span>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
8
src/routes/signup/+page.ts
Normal file
8
src/routes/signup/+page.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import session from '$lib/session.svelte';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function load() {
|
||||||
|
if (session.loggedIn()) {
|
||||||
|
redirect(307, '/landing');
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user