Compare commits

..

2 Commits

Author SHA1 Message Date
Mohamad
c91f5b0f65 commit 2025-01-13 14:31:53 +01:00
Mohamad
98aa111734 init 2025-01-13 14:31:37 +01:00
39 changed files with 2597 additions and 115 deletions

237
package-lock.json generated
View File

@ -10,15 +10,19 @@
"dependencies": { "dependencies": {
"flowbite": "^2.5.1", "flowbite": "^2.5.1",
"flowbite-svelte": "^0.46.16", "flowbite-svelte": "^0.46.16",
"flowbite-svelte-icons": "^1.6.1" "flowbite-svelte-icons": "^1.6.1",
"motion": "^11.17.0",
"pocketbase": "^0.25.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.28.1", "@playwright/test": "^1.28.1",
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.5.27", "@sveltejs/kit": "^2.5.27",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tailwindcss/typography": "^0.5.14", "@tailwindcss/typography": "^0.5.14",
"@types/eslint": "^9.6.0", "@types/eslint": "^9.6.0",
"@types/node": "^22.10.5",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.0.0", "eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@ -799,6 +803,92 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@rollup/plugin-commonjs": {
"version": "28.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz",
"integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"commondir": "^1.0.1",
"estree-walker": "^2.0.2",
"fdir": "^6.2.0",
"is-reference": "1.2.1",
"magic-string": "^0.30.3",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=16.0.0 || 14 >= 14.17"
},
"peerDependencies": {
"rollup": "^2.68.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-commonjs/node_modules/fdir": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz",
"integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-commonjs/node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/@rollup/plugin-commonjs/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@rollup/plugin-json": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-node-resolve": { "node_modules/@rollup/plugin-node-resolve": {
"version": "15.2.3", "version": "15.2.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
@ -1083,6 +1173,47 @@
"@sveltejs/kit": "^2.0.0" "@sveltejs/kit": "^2.0.0"
} }
}, },
"node_modules/@sveltejs/adapter-node": {
"version": "5.2.11",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.11.tgz",
"integrity": "sha512-lR7/dfUaKFf3aI408KRDy/BVDYoqUws7zNOJz2Hl4JoshlTnMgdha3brXBRFXB+cWtYvJjjPhvmq3xqpbioi4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"rollup": "^4.9.5"
},
"peerDependencies": {
"@sveltejs/kit": "^2.4.0"
}
},
"node_modules/@sveltejs/adapter-node/node_modules/@rollup/plugin-node-resolve": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz",
"integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@sveltejs/kit": { "node_modules/@sveltejs/kit": {
"version": "2.5.28", "version": "2.5.28",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.28.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.28.tgz",
@ -1203,6 +1334,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": {
"version": "22.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@ -1996,6 +2137,13 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true,
"license": "MIT"
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -2639,6 +2787,33 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/framer-motion": {
"version": "11.17.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.17.0.tgz",
"integrity": "sha512-uTNLH9JPMD3ad14WBt3KYRTR+If4tGPLgKTKTIIPaEBMkvazs6EkWNcmCh65qA/tyinOqIbQiuCorXX0qQsNoQ==",
"license": "MIT",
"dependencies": {
"motion-dom": "^11.16.4",
"motion-utils": "^11.16.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@ -3179,6 +3354,47 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/motion": {
"version": "11.17.0",
"resolved": "https://registry.npmjs.org/motion/-/motion-11.17.0.tgz",
"integrity": "sha512-mWZhIOWH2slNXPUWhr6cEu98bl9NMX7u9r7vdNI+Bm3/jrOEa3e44GmyUuwXr9hWR+rWII27YTnKb6CDD1vU2g==",
"license": "MIT",
"dependencies": {
"framer-motion": "^11.17.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": {
"version": "11.16.4",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.16.4.tgz",
"integrity": "sha512-2wuCie206pCiP2K23uvwJeci4pMFfyQKpWI0Vy6HrCTDzDCer4TsYtT7IVnuGbDeoIV37UuZiUr6SZMHEc1Vww==",
"license": "MIT",
"dependencies": {
"motion-utils": "^11.16.0"
}
},
"node_modules/motion-utils": {
"version": "11.16.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.16.0.tgz",
"integrity": "sha512-ngdWPjg31rD4WGXFi0eZ00DQQqKKu04QExyv/ymlC+3k+WIgYVFbt6gS5JsFPbJODTF/r8XiE/X+SsoT9c0ocw==",
"license": "MIT"
},
"node_modules/mri": { "node_modules/mri": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@ -3481,6 +3697,12 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/pocketbase": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.25.0.tgz",
"integrity": "sha512-xbjiQG/tnh2HsjZrTW7ZEJASvl4hmGAB5PQAmNRkRU8BmrPib7zwKyXdiYJl34QN7ADpqykZD2lAMdDtrrQbuw==",
"license": "MIT"
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.47", "version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
@ -4696,6 +4918,12 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -4747,6 +4975,13 @@
} }
} }
}, },
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",

View File

@ -17,10 +17,12 @@
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.28.1", "@playwright/test": "^1.28.1",
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.5.27", "@sveltejs/kit": "^2.5.27",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tailwindcss/typography": "^0.5.14", "@tailwindcss/typography": "^0.5.14",
"@types/eslint": "^9.6.0", "@types/eslint": "^9.6.0",
"@types/node": "^22.10.5",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.0.0", "eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@ -41,6 +43,8 @@
"dependencies": { "dependencies": {
"flowbite": "^2.5.1", "flowbite": "^2.5.1",
"flowbite-svelte": "^0.46.16", "flowbite-svelte": "^0.46.16",
"flowbite-svelte-icons": "^1.6.1" "flowbite-svelte-icons": "^1.6.1",
"motion": "^11.17.0",
"pocketbase": "^0.25.0"
} }
} }

View File

@ -1,3 +1,10 @@
@import 'tailwindcss/base'; @import 'tailwindcss/base';
@import 'tailwindcss/components'; @import 'tailwindcss/components';
@import 'tailwindcss/utilities'; @import 'tailwindcss/utilities';
body {
position: relative;
min-height: 100svh;
}
html {
overflow-y: scroll;
}

View 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>

View 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}

View 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>

View File

@ -9,34 +9,38 @@
Input, Input,
Avatar Avatar
} from 'flowbite-svelte'; } from 'flowbite-svelte';
import { CartOutline, SearchOutline } from 'flowbite-svelte-icons'; import { CartOutline, SearchOutline, StarOutline } from 'flowbite-svelte-icons';
import ShoppingCart from './ShoppingCart.svelte';
</script> </script>
<Navbar> <Navbar class="sticky top-0 z-50 bg-white shadow-sm">
<NavBrand href="/"> <NavBrand href="/">
<img <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" src="https://www.edvring.info/dam/jcr:334766c1-59b9-40ed-b5fa-727d549d4e88/edvLogo.svg"
class="me-3 h-6 sm:h-9" class="me-3 h-7 sm:h-9"
alt="Flowbite Logo" alt="Flowbite Logo"
/> />
<span class="self-center whitespace-nowrap text-xl font-semibold dark:text-white">Webshop</span> <span class="self-center whitespace-nowrap text-xl font-semibold dark:text-white">Webshop</span>
</NavBrand> </NavBrand>
<div class="relative hidden md:block"> <!-- <div class="relative hidden md:block">
<div class="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3"> <div class="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3">
<SearchOutline class="h-4 w-4" /> <SearchOutline class="h-4 w-4" />
</div> </div>
<Input id="search-navbar" class="ps-10" placeholder="Search..." /> <Input id="search-navbar" class="w-96 ps-10" placeholder="Search..." />
</div> </div> -->
<div class="flex"> <div class="flex">
<Avatar <Avatar
href="/profile" href="/profile"
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" src="https://media.istockphoto.com/id/1223671392/de/vektor/standardprofilbild-avatar-fotoplatzhalter-vektor-illustration.jpg?s=612x612&w=0&k=20&c=vtYE5RcgwgrJ1Zg6r66xN25LpXS_xsxZ8NqtvRQ9w6I="
class="m-1 me-4" class="m-1 me-4 h-10 border-2"
/> />
<Button> <a href="/favourites">
<CartOutline size="lg"></CartOutline> <Button class="me-2 h-12">
<StarOutline size="lg" />
</Button> </Button>
</a>
<ShoppingCart />
</div> </div>
</Navbar> </Navbar>

View File

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

262
src/lib/services/api.ts Normal file
View 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;

View File

@ -1,28 +1,21 @@
import type { User } from './types'; const USER_KEY = 'user';
const PASS_KEY = 'username';
const key = 'user'; export const login = (user: string, password: string) => {
const key2 = 'username'; localStorage.setItem(USER_KEY, user);
localStorage.setItem(PASS_KEY, password);
};
function login(user: User) { export const logout = () => {
localStorage.setItem(key, btoa(`${user.username}:${user.password}`)); localStorage.removeItem(USER_KEY);
localStorage.setItem(key2, user.username); localStorage.removeItem(PASS_KEY);
} window.location.reload();
};
function logout() { export const loggedIn = () => localStorage.getItem(USER_KEY) !== null;
localStorage.removeItem(key);
localStorage.removeItem(key2);
}
function loggedIn() { export const name = () => localStorage.getItem(PASS_KEY) ?? '';
return localStorage.getItem(key) !== null;
}
function name() { export const auth = () => localStorage.getItem(USER_KEY);
return localStorage.getItem(key2) ?? '';
}
function auth() {
return localStorage.getItem(key);
}
export default { login, logout, loggedIn, name, auth }; export default { login, logout, loggedIn, name, auth };

View 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
View 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: [] }));
};

View File

@ -1,4 +0,0 @@
export interface User {
username: string;
password: string;
}

View 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;
}
};
}

View File

@ -1,9 +1,10 @@
<script> <script>
import Footer from '../../components/Footer.svelte';
import Navbar from '../../components/navbar.svelte'; import Navbar from '../../components/navbar.svelte';
let { children } = $props(); let { children } = $props();
</script> </script>
<Navbar></Navbar> <Navbar></Navbar>
{@render children?.()} {@render children?.()}
<Footer></Footer>

View 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");
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -1,38 +1,461 @@
<script> <script lang="ts">
import { Card, Button } from 'flowbite-svelte'; import { addToCart } from '$lib/stores/cartStore';
import { CartPlusOutline, StarOutline } from 'flowbite-svelte-icons'; import { removeFromFavorites, addToFavorites, appStore, favorites } from '$lib/stores/store';
import { Card, Button, Badge, Input, Select, Toast, Spinner } from 'flowbite-svelte';
import { CartPlusOutline, HeartOutline, HeartSolid, SearchOutline } from 'flowbite-svelte-icons';
import ImageModal from '../../../components/imageModal.svelte';
import { derived, writable, type Writable } from 'svelte/store';
import { fade, fly, scale } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { goto } from '$app/navigation';
// Create an array to loop over interface Product {
let cards = Array(9).fill({ id: number;
title: 'Tulpe', title: string;
description: 'Lorem ipsum dolor sit, amet consectetur adipisicing elit.', 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: 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' '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 currentPage: Writable<number> = writable(1);
const searchQuery: Writable<string> = writable('');
const selectedCategory: Writable<string> = writable('all');
const isLoading: Writable<boolean> = writable(false);
const showToast: Writable<boolean> = writable(false);
const toastMessage: Writable<string> = writable('');
// Add loading simulation
const simulateLoading = async () => {
isLoading.set(true);
await new Promise((resolve) => setTimeout(resolve, 800));
isLoading.set(false);
};
// Enhanced filtered products with proper typing
const filteredProducts = derived<[Writable<string>, Writable<string>], Product[]>(
[searchQuery, selectedCategory],
([$searchQuery, $selectedCategory], set) => {
simulateLoading().then(() => {
if (!$searchQuery && $selectedCategory === 'all') {
set(products);
return;
}
const searchLower = $searchQuery.toLowerCase();
const filtered = products.filter((product) => {
if ($selectedCategory !== 'all' && product.category !== $selectedCategory) return false;
if (!searchLower) return true;
return (
product.title.toLowerCase().includes(searchLower) ||
product.description.toLowerCase().includes(searchLower)
);
}); });
set(filtered);
});
},
[] as Product[]
);
const paginatedProducts = derived<[typeof filteredProducts, Writable<number>], Product[]>(
[filteredProducts, currentPage],
([$filteredProducts, $currentPage]) => {
const start = ($currentPage - 1) * itemsPerPage;
return $filteredProducts.slice(start, start + itemsPerPage);
}
);
const totalPages = derived<typeof filteredProducts, number>(
filteredProducts,
($filteredProducts) => Math.ceil($filteredProducts.length / itemsPerPage)
);
const goToPage = (page: number) => {
currentPage.set(page);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
let searchTimeout: NodeJS.Timeout;
const handleSearch = (event: Event) => {
const target = event.target as HTMLInputElement;
clearTimeout(searchTimeout);
isLoading.set(true);
searchTimeout = setTimeout(() => {
searchQuery.set(target.value);
currentPage.set(1);
}, 300);
};
const handleCategoryChange = (event: Event) => {
const target = event.target as HTMLSelectElement;
selectedCategory.set(target.value);
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);
};
let modalOpen = false;
let selectedImage = {
url: '',
title: ''
};
const openImageModal = (product: Product) => {
selectedImage = {
url: product.imageUrl,
title: product.title
};
modalOpen = true;
};
const closeImageModal = () => {
modalOpen = false;
};
// Navigate to item page
const goToItemPage = (productId: number) => {
goto(`/item/${productId}`);
};
</script> </script>
<div class="mx-auto my-6 grid w-fit grid-cols-3 gap-10"> <main class="container mx-auto px-4 py-8">
{#each cards as card} <div class="mb-8 space-y-4" in:fly={{ y: -20, duration: 800, delay: 200 }}>
<Card img={card.imageUrl} class=""> <p class="text-lg text-gray-600 dark:text-gray-400">
<div class="holder flex"> Discover our beautiful collection of fresh flowers
<div class="left w-fit">
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
{card.title}
</h5>
<p class="mb-3 font-normal leading-tight text-gray-700 dark:text-gray-400">
{card.description}
</p> </p>
</div> </div>
<div class="right grid min-w-fit gap-4"> <div class="mb-6 grid gap-4 sm:grid-cols-2" in:fly={{ y: -20, duration: 600, delay: 200 }}>
<Button> <div class="relative">
<CartPlusOutline class="h-6 w-6 text-white" /> <Input
</Button> type="text"
<Button> placeholder="Search products..."
<StarOutline class=" h-6 w-6 text-white" /> value={$searchQuery}
on:input={handleSearch}
>
<SearchOutline slot="left" class="h-5 w-5" />
</Input>
</div>
<Select on:change={handleCategoryChange} value={$selectedCategory}>
<option value="all">All Categories</option>
<option value="bouquets">Bouquets</option>
<option value="single">Single Flowers</option>
<option value="arrangement">Arrangements</option>
</Select>
</div>
{#if $isLoading}
<div class="flex justify-center py-12" in:fade>
<Spinner size="12" />
</div>
{:else}
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{#each $paginatedProducts as product, i (product.id)}
<div in:fly={{ y: 20, duration: 400, delay: i * 100 }} out:fade={{ duration: 200 }}>
<Card
padding="none"
class="h-fit overflow-hidden transition-shadow duration-300 hover:shadow-lg"
>
<div
class="relative aspect-[4/3] w-full overflow-hidden"
on:click={() => openImageModal(product)}
on:keydown={(e) => e.key === 'Enter' && openImageModal(product)}
role="button"
tabindex="0"
>
<img
src={product.imageUrl}
alt={product.title}
class="absolute h-full w-full object-cover transition-transform duration-300 hover:scale-110"
loading="lazy"
/>
<button
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)}
>
{#if $favorites.some((item: Product) => item.id === product.id)}
<div in:scale={{ duration: 200 }}>
<HeartSolid class="h-6 w-6 text-red-500" />
</div>
{:else}
<div in:scale={{ duration: 200 }}>
<HeartOutline class="h-6 w-6 text-gray-500" />
</div>
{/if}
</button>
</div>
<div class="flex h-full flex-col p-4">
<div class="flex-grow space-y-2">
<h5
on:click={() => goToItemPage(product.id)}
class="text-xl font-bold tracking-tight text-gray-900 dark:text-white"
>
{product.title}
</h5>
<p class="text-sm text-gray-700 dark:text-gray-400">
{product.description}
</p>
<div class="flex items-center justify-between">
<p class="text-2xl font-bold text-gray-900 dark:text-white">
${product.price.toFixed(2)}
</p>
{#if $favorites.some((item: Product) => item.id === product.id)}
<Badge color="red" class="animate-pulse">Favorite</Badge>
{/if}
</div>
</div>
<div class="mt-4">
<Button
class="w-full transition-transform duration-200 hover:scale-105"
on:click={() => handleAddToCart(product)}
>
<CartPlusOutline class="mr-2 h-5 w-5" />
Add to Cart
</Button> </Button>
</div> </div>
</div> </div>
</Card> </Card>
</div>
{/each} {/each}
</div> </div>
{#if $filteredProducts.length === 0 && products.length === 0}
<div class="mt-8 text-center" in:fade>
<p class="text-lg text-gray-600">No products found matching your criteria.</p>
</div>
{/if}
{#if $totalPages > 1}
<div class="mt-8 flex flex-wrap justify-center gap-2" in:fly={{ y: 20, duration: 800 }}>
<Button
color="light"
disabled={$currentPage === 1}
on:click={() => goToPage($currentPage - 1)}
>
Previous
</Button>
{#each Array($totalPages) as _, i}
<Button
color={$currentPage === i + 1 ? 'primary' : 'light'}
class="transition-transform duration-200 hover:scale-105"
on:click={() => goToPage(i + 1)}
>
{i + 1}
</Button>
{/each}
<Button
color="light"
disabled={$currentPage === $totalPages}
on:click={() => goToPage($currentPage + 1)}
>
Next
</Button>
</div>
{/if}
{/if}
</main>
{#if $showToast}
<div
class="fixed bottom-4 right-4 z-50"
in:fly={{ x: 20, duration: 300, easing: quintOut }}
out:fade
>
<Toast>
{$toastMessage}
</Toast>
</div>
{/if}
<ImageModal
bind:open={modalOpen}
imageUrl={selectedImage.url}
title={selectedImage.title}
onClose={closeImageModal}
/>

View 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>

View 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>

View File

@ -1,23 +1,115 @@
<script> <script lang="ts">
import { Button, Card, Input } from 'flowbite-svelte'; import { favorites } from '$lib/stores/store';
import { Card, Button, Badge, Input, Textarea } from 'flowbite-svelte';
import { StarSolid, CheckCircleOutline, PenOutline } from 'flowbite-svelte-icons';
// Example user data
const user = {
name: 'John Doe',
email: 'john.doe@example.com',
address: '123 Main St, City, Country'
};
// Example order history
const orders = [
{
id: 1,
date: '2023-10-01',
items: ['Tulip Bouquet', 'Rose Bouquet'],
total: 60,
status: 'Delivered'
},
{
id: 2,
date: '2023-09-25',
items: ['Sunflower Bouquet'],
total: 30,
status: 'Shipped'
}
];
// State for editing profile
let isEditing = false;
let name = user.name;
let email = user.email;
let address = user.address;
// Handle profile update
const updateProfile = () => {
user.name = name;
user.email = email;
user.address = address;
isEditing = false;
// You could add an API call here to update the user's profile
};
</script> </script>
<div class="flex h-[90svh] flex-wrap content-center"> <!-- Welcome Section -->
<Card class="mx-auto max-h-fit"> <section class="bg-gradient-to-r from-pink-100 to-purple-100 py-12">
<form action=""> <div class="container mx-auto px-4">
<div class="formItem"> <h1 class="text-4xl font-bold text-gray-900">Welcome, {user.name}!</h1>
<label for="">Company Name:</label> <p class="text-lg text-gray-600">Manage your orders, favorites, and account settings here.</p>
<Input type="text"></Input>
</div> </div>
<div class="formItem"> </section>
<label for="">Email:</label>
<Input type="email"></Input> <!-- Dashboard Content -->
<div class="container mx-auto grid grid-cols-1 gap-8 px-4 py-8 lg:grid-cols-3">
<!-- Order History -->
<div class="lg:col-span-2">
<h2 class="mb-6 text-2xl font-bold text-gray-900">Order History</h2>
<div class="space-y-4">
{#each orders as order}
<Card class="p-6">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-xl font-bold text-gray-900">Order #{order.id}</h3>
<Badge color={order.status === 'Delivered' ? 'green' : 'blue'}>
{order.status}
</Badge>
</div> </div>
<div class="formItem"> <p class="mb-2 text-gray-600">
<label for="">GLN:</label> <strong>Date:</strong>
<Input type="number"></Input> {order.date}
</div> </p>
<Button>Submit</Button> <p class="mb-2 text-gray-600">
</form> <strong>Items:</strong>
{order.items.join(', ')}
</p>
<p class="text-gray-600">
<strong>Total:</strong> ${order.total.toFixed(2)}
</p>
</Card> </Card>
{/each}
</div>
</div>
<!-- Account Settings -->
<div class="space-y-8">
<!-- Account Settings -->
<div>
<h2 class="mb-6 text-2xl font-bold text-gray-900">Account Settings</h2>
<Card class="p-6">
{#if isEditing}
<form on:submit|preventDefault={updateProfile} class="space-y-4">
<Input label="Name" bind:value={name} required />
<Input label="Email" type="email" bind:value={email} required />
<Textarea label="Address" bind:value={address} required />
<div class="flex justify-end space-x-4">
<Button color="red" on:click={() => (isEditing = false)}>Cancel</Button>
<Button type="submit">Save</Button>
</div>
</form>
{:else}
<div class="space-y-4">
<p class="text-gray-600"><strong>Name:</strong> {user.name}</p>
<p class="text-gray-600"><strong>Email:</strong> {user.email}</p>
<p class="text-gray-600"><strong>Address:</strong> {user.address}</p>
<Button on:click={() => (isEditing = true)}>
<PenOutline class="mr-2 h-5 w-5" />
Edit Profile
</Button>
</div>
{/if}
</Card>
</div>
</div>
</div> </div>

View 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>

View 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>

View 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
View 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>

View File

@ -1,6 +1,9 @@
<script> <script>
import '../app.css'; import '../app.css';
let { children } = $props(); let { children } = $props();
import { fade, slide } from 'svelte/transition';
</script> </script>
{@render children?.()} <div transition:slide>
{@render children?.()}
</div>

View File

@ -2,6 +2,6 @@ import session from '$lib/session.svelte';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
export async function load() { export async function load() {
const page = session.loggedIn() ? '/main' : '/login'; const page = session.loggedIn() ? '/landing' : '/login';
redirect(307, page); redirect(307, page);
} }

View File

@ -1,6 +1,7 @@
<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';
let username: string = ''; let username: string = '';
let password: string = ''; let password: string = '';
@ -13,7 +14,8 @@
} }
loading = true; loading = true;
// success = await API.login({ username, password }); sessionSvelte.login(username, password);
success = true;
loading = false; loading = false;
if (success) { if (success) {
@ -25,7 +27,7 @@
<div class="flex h-[100svh] flex-wrap content-center"> <div class="flex h-[100svh] flex-wrap content-center">
<Card class="mx-auto max-h-fit"> <Card class="mx-auto max-h-fit">
<form class="flex flex-col space-y-6" action="/"> <form class="flex flex-col space-y-6" action="/">
<h3 class="text-xl font-medium text-gray-900 dark:text-white">Sign in to our platform</h3> <h3 class="text-xl font-medium text-gray-900 dark:text-white">Sign in</h3>
<Label class="space-y-2"> <Label class="space-y-2">
<span>Email</span> <span>Email</span>
<Input <Input
@ -48,12 +50,6 @@
</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}>Login to your account</Button>
<span class="self-center text-red-500" class:invisible={success}>Login failed</span> <span class="self-center text-red-500" class:invisible={success}>Login failed</span>
<div class="text-sm font-medium text-gray-500 dark:text-gray-300">
Not registered? <a href="/" class="text-primary-700 hover:underline dark:text-primary-500">
Create account
</a>
</div>
</form> </form>
</Card> </Card>
</div> </div>

View File

@ -3,6 +3,6 @@ import { redirect } from '@sveltejs/kit';
export async function load() { export async function load() {
if (session.loggedIn()) { if (session.loggedIn()) {
redirect(307, '/'); redirect(307, '/landing');
} }
} }

View 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>

View 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');
}
}

View File

@ -1,4 +1,4 @@
import adapter from '@sveltejs/adapter-auto'; import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */

View File

@ -13,16 +13,16 @@ export default {
colors: { colors: {
// flowbite-svelte // flowbite-svelte
primary: { primary: {
50: '#FFF5F2', 50: '#f2fbf2',
100: '#FFF1EE', 100: '#e6f7e6',
200: '#FFE4DE', 200: '#c0eac0',
300: '#FFD5CC', 300: '#99dc99',
400: '#FFBCAD', 400: '#66c566',
500: '#FE795D', 500: '#266d26' /* Base color */,
600: '#EF562F', 600: '#205b20',
700: '#EB4F27', 700: '#1a491a',
800: '#CC4522', 800: '#143714',
900: '#A5371B' 900: '#0e260e'
} }
} }
} }