Create checkout forms

Move section out of container because its more useful that way
give section more styling options
Move basket item out into its own div, as to not override containers styling for no reason
Change formMessage type from sting | Error to just string
Make the booking progress code more readable
This commit is contained in:
Michał 2024-05-16 19:50:48 +01:00
parent 488503d3ff
commit 5afe4386d6
11 changed files with 377 additions and 85 deletions

View file

@ -23,61 +23,67 @@
}
</script>
<div class="container">
{#if !item.data.availability}
<div class="basket-item-notice">
<SealWarning weight="fill" />&nbsp;&nbsp;<span>Item is no-longer for sale</span>
</div>
{/if}
{#if item.data.images && item.data.images[0]}
<img src="{item.data.images[0]}" alt="Item" class="basket-item-image">
{:else}
<img src={ImageLoading} alt="Item" class="basket-item-image">
{/if}
<div class="container" >
<div class="basket-item" class:basket-item-availability={!item.data.availability}>
{#if !item.data.availability}
<div class="basket-item-notice">
<SealWarning weight="fill" />&nbsp;&nbsp;<span>Item is no-longer for sale</span>
</div>
{/if}
<ul class="basket-item-data">
<li class="basket-item-name"><a href="/item/{item.data.uuid}" use:link>{item.data.name}</a></li>
<li class="basket-item-controls">
<button class="button" class:disabled={item.amount <= 1} on:click={reduce}><Minus /></button>
<p>{item.amount}</p>
<button class="button" class:disabled={item.amount >= 99} on:click={add}><Plus /></button>
<hr>
<button class="button evil" on:click={yeet}><Trash /></button>
</li>
<li class="basket-item-price">£{item.data.price * item.amount}{item.data.price})</li>
</ul>
{#if item.data.images && item.data.images[0]}
<img src="{item.data.images[0]}" alt="Item" class="basket-item-image">
{:else}
<img src={ImageLoading} alt="Item" class="basket-item-image">
{/if}
<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>
<ul class="basket-item-data">
<li class="basket-item-name"><a href="/item/{item.data.uuid}" use:link>{item.data.name}</a></li>
<li class="basket-item-controls">
<button class="button" class:disabled={item.amount <= 1} on:click={reduce}><Minus /></button>
<p>{item.amount}</p>
<button class="button" class:disabled={item.amount >= 99} on:click={add}><Plus /></button>
<hr>
<button class="button evil" on:click={yeet}><Trash /></button>
</li>
<li class="basket-item-price">£{item.data.price * item.amount}{item.data.price})</li>
</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">
@import "../styles/vars";
.container {
.basket-item {
position: relative;
display: flex;
flex-direction: row;
// Move background out of way of the image
background-position: 135px -43px;
border-radius: $border-radius-normal;
overflow: hidden;
&.basket-item-availability {
padding-top: 30px;
}
}
.basket-item-notice {
width: 100%;
@ -148,8 +154,8 @@
}
}
.basket-item-data {
margin: $spacing-small;
padding: 0;
margin: 0;
padding: $spacing-small;
display: flex;
flex-direction: column;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from "svelte";
import { link } from 'svelte-spa-router';
import { Acorn, Fish, Leaf, Pepper, ArrowUpRight, GrainsSlash, SealWarning } from 'phosphor-svelte';
import { Acorn, Fish, Leaf, Pepper, ArrowUpRight, GrainsSlash } from 'phosphor-svelte';
import { type Item, Labels} from "../lib/types";
import LoadingImage from '/assets/MenuItemLoadingAlt.svg';
@ -10,21 +10,19 @@
let element: HTMLElement;
onMount(() => {
keepSquare();
})
function keepSquare() {
element.style.height = "";
element.style.height = element.offsetWidth + "px";
}
onMount(keepSquare)
</script>
<svelte:window on:resize={keepSquare}></svelte:window>
<div class="menu-item" bind:this={element}>
{#if !item.availability}
<!-- <div class="menu-item-notice"><span>Item is no-longer for sale</span></div>-->
<!--<div class="menu-item-notice"><span>Item is no-longer for sale</span></div>-->
{/if}
{#if item.images && item.images[0]}
@ -51,13 +49,7 @@
{/if}
</ul>
<a class="menu-item-link" href="/item/{item.uuid}" use:link>
View&nbsp;<ArrowUpRight />
</a>
<ul class="menu-item-detail">
<li>{item.name}</li>
<li>£{item.price}</li>
</ul>
<a class="menu-item-link" href="/item/{item.uuid}" use:link>View&nbsp;<ArrowUpRight /></a>
<ul class="menu-item-detail"><li>£{item.price} | {item.name}</li></ul>
</div>

View file

@ -8,16 +8,14 @@
let progress = 1;
querystring.subscribe((params) => {
const url = new URLSearchParams(params);
const parsed = parseInt(url.get("progress"));
progress = 1;
if (
url.has("progress") &&
!isNaN(parseInt(url.get("progress")))
) {
progress = parseInt(url.get("progress"));
}
if ([1, 2, 3].indexOf(progress) == -1) {
if (url.has("progress") && !isNaN(parsed)) {
progress = parsed;
}
if ([1, 2, 3].indexOf(progress) === -1) {
progress = 1;
push("/booking?progress=1");
}
@ -226,7 +224,6 @@
margin: 0 $spacing-normal;
width: 100%;
max-width: 500px;
height: 5px;
position: relative;

View file

@ -1,20 +1,87 @@
<script lang="ts">
import { link } from "svelte-spa-router";
import { ArrowLeft } from "phosphor-svelte";
import { ArrowLeft, ArrowRight, Basket, SealWarning } from "phosphor-svelte";
import Cart from "../lib/cart";
import PaymentForm from "./Checkout/PaymentForm.svelte";
import LeFinal from "./Checkout/LeFinal.svelte";
export let params: {
progress?: string;
};
let unavailableItems = false;
Cart.getEntries().forEach(([_, item]) => {
if (!item.data.availability) unavailableItems = true;
});
</script>
{#if unavailableItems}
<div class="notice error">
<SealWarning weight="fill" />&nbsp;&nbsp;<span>Order contains an item that is no-longer available</span>
</div>
{/if}
<a href="/cart" use:link id="back-button"><ArrowLeft />&nbsp;Back</a>
<h1>Cart</h1>
<h2>Order total: £Balls</h2>
<div id="checkoutHeader">
<a href="/cart" use:link id="cancel-button"><ArrowLeft />&nbsp;Cancel</a>
</div>
{#if params.progress === "1"}
<PaymentForm />
<div id="checkoutFooter">
<div />
<a href="/cart/checkout/2" use:link id="navigation-button">Order&nbsp;<ArrowRight /></a>
</div>
{:else if params.progress === "2"}
<LeFinal />
<div id="checkoutFooter">
<a href="/cart/checkout/1" use:link id="navigation-button"><ArrowLeft />&nbsp;Payment</a>
<a href="/cart/checkout/3" use:link id="navigation-button">Confirm Purchase&nbsp;<ArrowRight /></a>
</div>
{:else}
<div id="checkoutLost">
<h1>Something went wrong!&nbsp;<Basket weight="fill" /></h1>
<p>Return <a href="/cart/checkout/1" use:link>to checkout</a>.</p>
</div>
{/if}
<style lang="scss">
@import "../styles/vars";
#back-button {
margin-top: 8px;
#checkoutHeader {
padding-bottom: $spacing-normal;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
> p {
padding: 0 $spacing-small;
width: max-content;
height: 30px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
font-size: $font-size-p;
}
}
#checkoutFooter {
padding-top: $spacing-normal;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
#cancel-button {
padding: 0 $spacing-small;
width: max-content;
@ -33,8 +100,85 @@
color: $color-on-background;
&:hover {
background-color: $color-light;
color: $color-on-light;
background-color: $color-error;
color: $color-on-error;
}
}
#navigation-button {
padding: 0 $spacing-small;
width: max-content;
height: 30px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
font-size: $font-size-p;
text-decoration: none;
border-radius: 9999px;
background-color: $color-light;
color: $color-on-light;
&:hover {
background-color: $color-dark;
color: $color-on-dark;
}
}
#checkoutLost {
margin-left: auto;
margin-right: auto;
max-width: $sizing-default-width;
height: 400px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> h1 {
display: flex;
justify-content: center;
align-items: center;
font-size: $font-size-very-fucking-big;
text-align: center;
}
> p {
text-align: center;
}
}
.notice {
margin-right: auto;
margin-bottom: $spacing-large;
margin-left: auto;
max-width: $sizing-default-width;
height: 40px;
position: sticky;
top: calc($spacing-small + $sizing-navigation-height);
display: flex;
justify-content: center;
align-items: center;
font-size: $font-size-p;
border-radius: $border-radius-large;
background: $color-dark;
color: $color-on-dark;
box-shadow: 0 1px 0.5px rgba(#000, 0.3);
&.error {
background: $color-error;
color: $color-on-error;
}
}
</style>

View file

@ -0,0 +1,74 @@
<script lang="ts">
import { type CartItem } from "../../lib/types";
import Cart from "../../lib/cart";
let items: [string, CartItem][];
let totalPrice: number;
Cart.subscribe(() => {
items = Cart.getEntries();
totalPrice = Cart.getTotalPrice();
});
</script>
<h1>Checkout > Order</h1>
<div class="container">
<div class="section small">
<div class="table">
<table>
<tr><th>Price (each)</th><th>Item Name</th><th>Amount</th></tr>
{#each items as [_, item]}
<tr>
<td>£{item.data.price * item.amount}{item.data.price})</td>
<td>{item.data.name}</td>
<td>{item.amount}</td>
</tr>
{/each}
</table>
</div>
<div class="spacer half" />
<p>Total: ${totalPrice}</p>
</div>
</div>
<style lang="scss">
@import "../../styles/vars";
.table {
border-radius: $border-radius-normal;
border: 1px solid rgba($color-dark, 0.3);
background-color: $color-light;
overflow: hidden;
table {
width: 100%;
border-collapse: collapse;
tr {
border-bottom: 1px solid rgba($color-dark, 0.3);
&:last-of-type {
border: 0 solid transparent;
}
th, td {
padding: $spacing-xsmall $spacing-small;
border-right: 1px solid rgba($color-dark, 0.3);
&:last-of-type {
border: 0 solid transparent;
}
}
th {
font-weight: $font-weight-bolder;
}
td {
font-weight: $font-weight-normal;
}
}
}
}
</style>

View file

@ -0,0 +1,66 @@
<script lang="ts">
import { SealCheck } from "phosphor-svelte";
let name = "";
let email = "";
let nameValid = true;
let emailValid = true;
function validateName() { nameValid = name.length > 1 }
function validateEmail() { emailValid = email.length > 1 }
function onSubmit() {
validateName();
validateEmail();
}
</script>
<h1>Checkout > Payment</h1>
<form on:submit|preventDefault={onSubmit}>
<div class="form-element">
<label class="form-label" for="name">Name</label>
<input
bind:value={name}
on:blur={validateName}
on:input={validateName}
type="text"
id="name"
name="name"
class="form-input"
/>
<span class="form-notice error">
{#if !nameValid}
Enter a name
{/if}
</span>
</div>
<div class="spacer half" />
<div class="form-element">
<label class="form-label" for="email">Email</label>
<input
bind:value={email}
on:blur={validateEmail}
on:input={validateEmail}
type="text"
id="email"
name="email"
class="form-input"
/>
<span class="form-notice error">
{#if !emailValid}
Email not valid
{/if}
</span>
</div>
<div class="spacer half" />
<p class="form-message success"><SealCheck weight="fill" />&nbsp;GwaGwa</p>
</form>
<style lang="scss">
@import "../../styles/vars";
</style>

View file

@ -7,7 +7,7 @@
const minMessageLength = 150;
let formMessage: Promise<string | Error>;
let formMessage: Promise<string>;
let name = "";
let email = "";

View file

@ -40,11 +40,11 @@ const routes = {
conditions: [],
userData: { showNavBar: true, fullWidth: false },
}),
"/cart/checkout": wrap({
"/cart/checkout/:progress?": wrap({
asyncComponent: () => import("./pages/Checkout.svelte"),
loadingComponent: PageLoading,
conditions: [],
userData: { showNavBar: true, fullWidth: true },
userData: { showNavBar: true, fullWidth: false },
}),
"/booking": wrap({
asyncComponent: () => import("./pages/Booking.svelte"),

View file

@ -9,7 +9,7 @@
box-shadow: 0 1px 0.5px rgba(#000, 0.3);
.header {
> .header {
padding: $spacing-normal;
display: flex;
flex-direction: row;
@ -20,8 +20,4 @@
padding-bottom: 0;
}
}
.section {
padding: $spacing-normal;
}
}

View file

@ -23,7 +23,7 @@
justify-content: center;
align-items: center;
font-size: $font-size-p;
font-size: $font-size-small;
text-align: center;
background: $color-error;
@ -120,7 +120,7 @@
}
.menu-item-detail {
padding: $spacing-normal;
padding: $spacing-small;
width: 100%;
@ -131,9 +131,12 @@
display: flex;
flex-direction: column;
font-weight: 800;
font-size: $font-size-xsmall;
font-weight: $font-weight-bolder;
background-image: linear-gradient(0deg, $color-dark, rgba($color-dark, 0));
//border-radius: $border-radius-large $border-radius-large 0 0;
//background-image: linear-gradient(0deg, rgba($color-dark, 1), rgba($color-dark, 0.6));
background-color: rgba($color-dark, 0.7);
color: $color-light;
z-index: 2;

View file

@ -107,6 +107,20 @@ button {
}
}
.section {
padding: $spacing-normal;
&.large {
padding: $spacing-large;
}
&.small {
padding: $spacing-small;
}
&.xsmall {
padding: $spacing-xsmall;
}
}
main {
margin-left: auto;
margin-right: auto;