Move to key-value basket, to keep track of information easier

Clean up code, add more types
Clean up error handling
This commit is contained in:
Michał 2024-05-03 18:35:39 +01:00
parent ec2ef95cca
commit 7066cc492b
7 changed files with 196 additions and 138 deletions

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { link } from 'svelte-spa-router'; import { link } from 'svelte-spa-router';
import { Plus, Minus, Trash } from 'phosphor-svelte'; import { Plus, Minus, Trash, Acorn, Fish, GrainsSlash, Leaf, Pepper } from "phosphor-svelte";
import type { CartItem } from "../lib/types"; import { type CartItem, Labels } from "../lib/types";
import Cart from "../lib/cart"; import Cart from "../lib/cart";
export let item: CartItem; export let item: CartItem;
@ -15,7 +15,7 @@
<img src="/MenuItemLoadingAlt.svg" alt="Item" class="basket-item-image"> <img src="/MenuItemLoadingAlt.svg" alt="Item" class="basket-item-image">
{/if} {/if}
<ul> <ul class="basket-item-data">
<li class="basket-item-name"><a href="/item/{item.uuid}" use:link>{item.data.name}</a></li> <li class="basket-item-name"><a href="/item/{item.uuid}" use:link>{item.data.name}</a></li>
<li class="basket-item-controls"> <li class="basket-item-controls">
<button class="button " on:click={() => { Cart.addToCart(item.uuid, -1) }}><Minus /></button> <button class="button " on:click={() => { Cart.addToCart(item.uuid, -1) }}><Minus /></button>
@ -26,6 +26,22 @@
</li> </li>
<li class="basket-item-price">£{item.data.price * item.amount}{item.data.price})</li> <li class="basket-item-price">£{item.data.price * item.amount}{item.data.price})</li>
</ul> </ul>
<ul class="basket-item-labels">
{#each item.data.labels as label}
{#if label === Labels.vegan}
<li class="vegan"><Leaf weight="fill" /></li>
{:else if label === Labels.fish}
<li class="fish"><Fish weight="fill" /></li>
{:else if label === Labels.nut}
<li class="nut"><Acorn weight="fill" /></li>
{:else if label === Labels.gluten}
<li class="gluten"><GrainsSlash weight="fill" /></li>
{:else if label === Labels.spicy}
<li class="spicy"><Pepper weight="fill" /></li>
{/if}
{/each}
</ul>
</div> </div>
<style lang="scss"> <style lang="scss">
@ -39,21 +55,7 @@
background-position: 135px -43px; background-position: 135px -43px;
overflow: hidden; overflow: hidden;
ul {
margin: $spacing-small;
padding: 0;
display: flex;
flex-direction: column;
li {
padding-bottom: $spacing-small;
list-style: none;
}
}
} }
.basket-item-image { .basket-item-image {
margin: $spacing-small; margin: $spacing-small;
@ -64,6 +66,58 @@
object-fit: cover; object-fit: cover;
} }
.basket-item-labels {
padding: $spacing-normal;
display: flex;
flex-direction: row;
> li {
margin: 0 0 0 -15px;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
border-radius: $border-radius-circle;
background-color: $color-dark;
color: $color-on-dark;
list-style: none;
&.vegan {
background-color: $color-vegan;
}
&.fish {
background-color: $color-fish;
}
&.nut {
background-color: $color-nut;
}
&.gluten {
background-color: $color-gluten;
}
&.spicy {
background-color: $color-spicy;
}
}
}
.basket-item-data {
margin: $spacing-small;
padding: 0;
display: flex;
flex-direction: column;
flex-grow: 1;
> li {
padding-bottom: $spacing-small;
list-style: none;
}
}
.basket-item-name a { .basket-item-name a {
font-size: $font-size-h2; font-size: $font-size-h2;
text-decoration: underline; text-decoration: underline;

View file

@ -1,86 +1,84 @@
import type { Writable } from "svelte/store";
import { get, writable } from "svelte/store"; import { get, writable } from "svelte/store";
import type { Item, CartItem } from './types'; import { type CartItem } from './types';
import { getItemByUUID, postVerifyCart } from "./test-api"; import { getItemByUUID, postVerifyCart } from "./test-api";
// Load content from localstorage // Load content from localstorage
let local: CartItem[] = []; let local: Record<string, CartItem> = {};
try { try {
let verified = await postVerifyCart( await postVerifyCart(JSON.parse(localStorage.getItem("basket")))
JSON.parse(localStorage.getItem("basket")) || [] .then((data: Record<string, CartItem>) => {
); local = data;
console.log(data);
if (verified instanceof Error) { })
throw new Error("Bruh"); .catch((error) => {
} throw error; // Hot potato style
});
local = <CartItem[]>verified;
} catch { } catch {
console.error("Failed to load cart") console.error("Failed to load cart")
} }
// Store function // Store function
function createCartStore() { function createCartStore() {
const cart = writable(local); const cart: Writable<Record<string, CartItem>> = writable(local);
async function addToCart(uuid: string, amount: number) { async function addToCart(uuid: string, amount: number) {
let found = false; if (get(cart)[uuid] !== undefined) {
cart.update((cart: Record<string, CartItem>) => {
get(cart).forEach((item: CartItem) => { cart[uuid].amount += amount;
if (item.uuid === uuid) { return cart;
item.amount += amount; });
found = true; } else {
} await getItemByUUID(uuid)
}); .then((data) => {
cart.update((cart: Record<string, CartItem>) =>
if (!found) { Object.assign({}, cart, {[uuid]: {
let cartData: Item; uuid: uuid,
amount: amount,
try { data: data,
let data: Item | Error = await getItemByUUID(uuid); }})
if (data instanceof Error) { )
return; });
}
cartData = <Item>data;
} finally {
const newItem: CartItem = {
uuid: uuid,
amount: amount,
data: cartData,
};
cart.update((cart: CartItem[]) => [...cart, newItem]);
}
} }
// Remove items that have an amount of 0 or lower
cart.update((cart) => cart.filter((item) => item.amount > 0))
} }
function getUniqueLength() { function getEntries(): [string, CartItem][] {
return get(cart).length; return Object.entries(get(cart));
} }
function getTotalLength() { function getUniqueLength(): number {
let amounts = get(cart).map((item) => item.amount); return Object.keys(get(cart)).length;
return amounts.reduce((a, b) => a + b, 0);
} }
function getTotalPrice() { function getTotalLength(): number {
let price = 0; let totalCartSize: number = 0;
get(cart).forEach((item) => { Object.values(get(cart)).forEach((item: CartItem) => {
price += item.amount * item.data.price; totalCartSize += item.amount;
}) });
return price; return totalCartSize;
}
function getTotalPrice(): number {
let totalCartPrice: number = 0;
Object.values(get(cart)).forEach((item: CartItem) => {
totalCartPrice += (item.amount * item.data.price);
});
return totalCartPrice;
} }
function removeByUUID(uuid: string) { function removeByUUID(uuid: string) {
cart.update((cart) => cart.filter((item) => item.uuid !== uuid)) cart.update((cart) => {
delete cart[uuid];
return cart;
})
} }
return { return {
...cart, ...cart,
addToCart, addToCart,
getEntries,
getUniqueLength, getUniqueLength,
getTotalLength, getTotalLength,
getTotalPrice, getTotalPrice,
@ -96,4 +94,9 @@ Cart.subscribe((value) => {
localStorage.setItem("basket", JSON.stringify(value)); localStorage.setItem("basket", JSON.stringify(value));
}); });
// Debug
Cart.subscribe((value) => {
console.log(value);
});
export default Cart; export default Cart;

View file

@ -1,4 +1,4 @@
import type {CartItem, Item} from './types'; import { type CartItem, type Item } from './types';
import TestData from './test-data'; import TestData from './test-data';
@ -19,13 +19,13 @@ export async function getAnnouncements(): Promise<{image: string}> {
if (cache.announcement_banner) { if (cache.announcement_banner) {
return cache.announcement_banner; return cache.announcement_banner;
} }
await fakeDelay(200)
const data = { const data = {
image: "/BannerExampleImage.jpg", image: "/BannerExampleImage.jpg",
}; };
cache.announcement_banner = data; cache.announcement_banner = data;
await fakeDelay(200)
return data; return data;
} }
@ -34,17 +34,18 @@ export async function getPopularToday(): Promise<Item[]> {
if (cache.popular_today) { if (cache.popular_today) {
return cache.popular_today; return cache.popular_today;
} }
await fakeDelay(200)
const data: Item[] = TestData; const data: Item[] = TestData;
cache.popular_today = data; cache.popular_today = data;
await fakeDelay(200)
return data; return data;
} }
export async function getMenuItems() { export async function getMenuItems(): Promise<{name: string, items: Item[]}[]> {
const data = [ await fakeDelay(20);
return [
{ {
name: "Main Menu", name: "Main Menu",
items: TestData, items: TestData,
@ -58,12 +59,12 @@ export async function getMenuItems() {
items: TestData, items: TestData,
}, },
]; ];
await fakeDelay(20)
return data;
} }
export async function getItemsByUUID(items: string[]): Promise<Item[] | Error> { export async function getItemsByUUID(items: string[]): Promise<Item[]> {
await fakeDelay(200)
let data: Item[] = []; let data: Item[] = [];
TestData.forEach((itemInDatabase: Item) => { TestData.forEach((itemInDatabase: Item) => {
@ -74,8 +75,6 @@ export async function getItemsByUUID(items: string[]): Promise<Item[] | Error> {
}); });
}); });
await fakeDelay(200)
if (data.length < 0) { if (data.length < 0) {
throw new Error("Resource could not be found"); throw new Error("Resource could not be found");
} }
@ -84,21 +83,26 @@ export async function getItemsByUUID(items: string[]): Promise<Item[] | Error> {
} }
export async function getItemByUUID(uuid: string): Promise<Item | Error> { export async function getItemByUUID(uuid: string): Promise<Item> {
let data: Item[] | Error = await getItemsByUUID([uuid]); let data: Item[];
if (data instanceof Error) { await getItemsByUUID([uuid])
throw new Error("Resource could not be found"); .then((result) => {
} if (result.length != 1) {
if (data.length != 1) { throw new Error("Resource could not be found");
throw new Error("Resource could not be found"); } else {
} data = result;
}
})
.catch((error) => {
throw error;
});
return data[0]; return data[0];
} }
export async function postContactEmail(name: string, email: string, message: string): Promise<string | Error> { export async function postContactEmail(name: string, email: string, message: string): Promise<string> {
await fakeDelay(200) await fakeDelay(200)
if (!name) { if (!name) {
@ -116,34 +120,28 @@ export async function postContactEmail(name: string, email: string, message: str
return "Check your email to confirm the message!"; return "Check your email to confirm the message!";
} }
export async function postVerifyCart(currentCartData: CartItem[]): Promise<CartItem[] | Error> { export async function postVerifyCart(currentCartData: Record<string, CartItem>): Promise<Record<string, CartItem>> {
if (currentCartData.length <= 0) { let verifiedItems: Item[] = []
return [];
}
let itemUUIDs: string[] = currentCartData.map((item) => item.uuid); await getItemsByUUID(Object.keys(currentCartData))
let verifiedItems: Item[] | Error = await getItemsByUUID(itemUUIDs); .then((data) => {
verifiedItems = data
if (verifiedItems instanceof Error) {
return new Error("Could not collect new cart information");
}
let newCartData: CartItem[] = [];
currentCartData.forEach((currentItem) => {
let data: Item;
verifiedItems.forEach((verifiedItem) => {
if (verifiedItem.uuid === currentItem.uuid) {
data = verifiedItem;
}
}) })
.catch(() => {
return new Error("Could not collect new cart information")
});
if (data) { let newCartData: Record<string, CartItem> = {};
newCartData.push({ Object.entries(currentCartData).forEach(([key, value]) => {
uuid: currentItem.uuid, verifiedItems.forEach((verifiedItem) => {
amount: currentItem.amount, if (verifiedItem.uuid === key) {
data: data, newCartData[key] = {
}); uuid: value.uuid,
} amount: value.amount,
data: verifiedItem,
};
}
});
}); });
return newCartData; return newCartData;

View file

@ -44,13 +44,13 @@ const TestData: Item[] = [
labels: [Labels.nut], labels: [Labels.nut],
detail: "Example", detail: "Example",
}, },
// { {
// uuid: "gwagwa", uuid: "gwagwa",
// name: "GwaGwa", name: "GwaGwa",
// price: 69, price: 69,
// labels: [Labels.nut], labels: [Labels.nut],
// image: "/dab.jpg", image: "/dab.jpg",
// }, },
{ {
uuid: "hogmelon", uuid: "hogmelon",
name: "Hogermellon", name: "Hogermellon",
@ -59,14 +59,14 @@ const TestData: Item[] = [
image: "/wathog.jpg", image: "/wathog.jpg",
detail: "Example", detail: "Example",
}, },
// { {
// uuid: "bluhog", uuid: "bluhog",
// name: "Blue HOGGGGG", name: "Blue HOGGGGG",
// price: 0, price: 0,
// labels: [Labels.nut, Labels.gluten, Labels.spicy], labels: [Labels.nut, Labels.gluten, Labels.spicy],
// image: "/sonichog.jpg", image: "/sonichog.jpg",
// detail: "Example", detail: "Example",
// }, },
]; ];
export default TestData; export default TestData;

View file

@ -2,7 +2,7 @@
import { link } from 'svelte-spa-router'; import { link } from 'svelte-spa-router';
import { Basket } from "phosphor-svelte"; import { Basket } from "phosphor-svelte";
import type { CartItem } from "../lib/types"; import { type CartItem } from "../lib/types";
import { getPopularToday } from "../lib/test-api"; import { getPopularToday } from "../lib/test-api";
import Cart from "../lib/cart"; import Cart from "../lib/cart";
import MenuList from "../components/MenuList.svelte"; import MenuList from "../components/MenuList.svelte";
@ -10,23 +10,23 @@
let popularToday = getPopularToday(); let popularToday = getPopularToday();
let items: CartItem[]; let items: [string, CartItem][];
let totalPrice: number; let totalPrice: number;
Cart.subscribe(() => { Cart.subscribe(() => {
items = $Cart; items = Cart.getEntries();
totalPrice = Cart.getTotalPrice(); totalPrice = Cart.getTotalPrice();
}); });
</script> </script>
{#if items.length > 0} {#if items.entries}
<h1>Cart</h1> <h1>Cart</h1>
<button id="checkout-button">Checkout</button> <button id="checkout-button">Checkout</button>
<h2>Order total: £{totalPrice}</h2> <h2>Order total: £{totalPrice}</h2>
{#each items as item} {#each items as [key, item]}
<div class="basket-item"> <div class="basket-item">
<BasketItem item={item}/> <BasketItem item={item}/>
</div> </div>

View file

@ -10,8 +10,7 @@
export let params; export let params;
$: item = getItemByUUID(params.uuid) $: item = getItemByUUID(params.uuid)
$: popularToday = getPopularToday();
let popularToday = getPopularToday();
</script> </script>
<div class="main"> <div class="main">

View file

@ -78,6 +78,10 @@ hr {
background-color: rgba($color-dark, 0.1); background-color: rgba($color-dark, 0.1);
} }
button {
font-family: $font-family;
}
.spacer { .spacer {
height: $spacing-large; height: $spacing-large;
} }