Finalise Checkout page design

Make CheckoutProgress type, hopefully to use with the checkout API thang
fix banner loading stuff
This commit is contained in:
Michał 2024-05-17 14:09:13 +01:00
parent d1b35005d4
commit e3e4e482cc
8 changed files with 316 additions and 290 deletions

View file

@ -27,8 +27,13 @@
border-radius: $border-radius-large;
background: linear-gradient(to right, $color-background 8%, rgba($color-dark, 0.3) 38%, $color-background 54%);
background-size: 1000px 100%;
background: linear-gradient(
to right,
rgba($color-dark, 0) 8%,
rgba($color-dark, 0.3) 38%,
rgba($color-dark, 0) 54%
) no-repeat;
background-size: 1500px 100%;
animation: loading 1s infinite linear;
overflow: hidden;
@ -43,7 +48,10 @@
left: $padding;
border-radius: calc($border-radius-large - $padding);
background-color: rgba($color-background, 0.9);
background-color: darken($color-background, 10%);
background-image: url("/assets/Noise.png");
opacity: 0.9;
}
}
@ -57,10 +65,10 @@
@keyframes loading{
0%{
background-position: -500px 0
background-position: -750px 0
}
100%{
background-position: 500px 0
background-position: 750px 0
}
}
</style>

View file

@ -12,7 +12,7 @@ export async function getAnnouncements(): Promise<{ image: string }> {
cache["announcement_banner"] = {
image: "/banner_images/BannerExampleImage.jpg",
};
await fakeDelay(200);
await fakeDelay(2000);
}
return cache["announcement_banner"];
}
@ -20,7 +20,7 @@ export async function getAnnouncements(): Promise<{ image: string }> {
export async function getPopularToday(): Promise<Item[]> {
if (cache["popular_today"] === undefined) {
cache["popular_today"] = TestData;
await fakeDelay(200);
await fakeDelay(2000);
}
return cache["popular_today"];
}
@ -58,7 +58,7 @@ export async function getItemsByUUID(items: string[]): Promise<Item[]> {
if (data.length === 0) throw new Error("Resource could not be found");
await fakeDelay(200);
await fakeDelay(2000);
return data;
}
@ -74,7 +74,7 @@ export async function postContactEmail(
reason: string,
message: string
): Promise<string> {
await fakeDelay(200);
await fakeDelay(2000);
if (!name) throw new Error("Name missing");
if (!email) throw new Error("Email missing");

View file

@ -19,7 +19,7 @@ const TestData: Item[] = [
},
{
uuid: "brick",
availability: true,
availability: false,
name: "Brick",
price: 0,
description: `Example`,
@ -44,14 +44,14 @@ const TestData: Item[] = [
{
uuid: "mouldy_bread",
availability: true,
name: "half eaten mouldy bread",
name: "Singular slice of bread",
price: 0.39,
description: `Example`,
labels: [Labels.nut],
},
{
uuid: "cup_cake_leg",
availability: false,
availability: true,
name: "Eated Cupcake",
price: 1.69,
description: `
@ -68,15 +68,15 @@ Contains:
"/item_images/cupcake.jpg",
],
},
{
uuid: "gwagwa",
availability: false,
name: "GwaGwa",
price: 69,
description: `Example`,
labels: [Labels.nut],
images: ["/item_images/dab.jpg"],
},
// {
// uuid: "gwagwa",
// availability: false,
// name: "GwaGwa",
// price: 69,
// description: `Example`,
// labels: [Labels.nut],
// images: ["/item_images/dab.jpg"],
// },
{
uuid: "hogmelon",
availability: true,

View file

@ -23,6 +23,20 @@ export type CartItem = {
export type CartRecord = Record<string, CartItem>;
export type Checkout = {
personal: {
name: string;
email: string;
phone?: number;
};
address: {
line1: string;
line2?: string;
town: string;
postcode: string;
}
};
export type JSONResponse = {
data?: {
item: Item[]; // Todo Make this not just item type

View file

@ -1,72 +1,179 @@
<script lang="ts">
import { link } from "svelte-spa-router";
import { ArrowLeft, ArrowRight, Basket } from "phosphor-svelte";
import { ArrowLeft, SealWarning, ArrowRight } from "phosphor-svelte";
import PaymentForm from "./Checkout/PaymentForm.svelte";
import LeFinal from "./Checkout/LeFinal.svelte";
import { type Checkout } from '../lib/types';
import Cart from "../lib/cart";
export let params: {
progress?: string;
};
const CheckoutData: Checkout = {
personal: {
name: "",
email: "",
},
address: {
line1: "",
line2: "",
town: "",
postcode: "",
},
}
$: nameValid = CheckoutData.personal.name.length > 1
$: emailValid = CheckoutData.personal.email.length > 1
$: phoneValid = isNaN(CheckoutData.personal.phone)
$: items = Cart.getEntries();
$: totalPrice = Cart.getTotalPrice();
let unavailableItems = false;
Cart.getEntries().forEach(([_, item]) => {
if (!item.data.availability) unavailableItems = true;
});
function onSubmit() {
console.log(CheckoutData);
}
</script>
<div id="checkoutHeader">
<a href="/cart" use:link id="cancel-button"><ArrowLeft />&nbsp;Cancel</a>
<div class="spacer half" />
<div class="checkout">
<div id="form-container">
<h2>Checkout</h2>
<form on:submit|preventDefault={onSubmit} id="form">
<div class="form-element">
<label class="form-label" for="name">Name</label>
<input
bind:value={CheckoutData.personal.name}
type="text"
id="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={CheckoutData.personal.email}
type="text"
id="email"
class="form-input"
/>
<span class="form-notice error">
{#if !emailValid}Email not valid{/if}
</span>
</div>
<div class="spacer half" />
<div class="form-element">
<label class="form-label" for="phone">Phone Number</label>
<input
bind:value={CheckoutData.personal.phone}
type="number"
id="phone"
class="form-input"
/>
<span class="form-notice error">
{#if !phoneValid}Phone Number not valid{/if}
</span>
</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 class="spacer" />
<div class="spacer" />
<div class="form-element">
<label class="form-label" for="line1">Address Line 1</label>
<input
bind:value={CheckoutData.address.line1}
type="text"
id="line1"
class="form-input"
/>
<span class="form-notice error">Line 1 required</span>
</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 class="spacer half" />
<div class="form-element">
<label class="form-label" for="line2">Address Line 2</label>
<input
bind:value={CheckoutData.address.line2}
type="text"
id="line2"
class="form-input"
/>
<span class="form-notice error"></span>
</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 class="spacer half" />
<div class="form-element">
<label class="form-label" for="town">Town</label>
<input
bind:value={CheckoutData.address.town}
type="text"
id="town"
class="form-input"
/>
<span class="form-notice error">Town name required</span>
</div>
<div class="spacer half" />
<div class="spacer half" />
<div class="form-element">
<label class="form-label" for="postcode">Postcode</label>
<input
bind:value={CheckoutData.address.postcode}
type="text"
id="postcode"
class="form-input"
/>
<span class="form-notice error">Postcode required</span>
</div>
<div class="spacer half" />
</form>
</div>
<div class="spacer" />
<div id="cart">
<div class="container">
{#if unavailableItems}
<div class="unavailable-items-banner">
<SealWarning weight="fill" />&nbsp;&nbsp;<span>Order contains unavailable items</span>
</div>
{/if}
<div class="header">
<h2>Cart Total: ${totalPrice}</h2>
</div>
<hr>
<div class="section">
<div class="table">
<table>
<tr><th>Price</th><th>Item Name</th><th>Amount</th></tr>
{#each items as [_, item]}
<tr class:table-row-error={!item.data.availability}>
<td>£{item.data.price * item.amount}{item.data.price})</td>
<td>{item.data.name}</td>
<td>{item.amount}</td>
</tr>
{/each}
</table>
</div>
</div>
<hr>
<div class="section">
<p>To make any changes to your order, <a href="/cart" use:link>return to the cart</a>.</p>
</div>
</div>
<div class="spacer half" />
<button id="checkout-button" form="form">Checkout&nbsp;<ArrowRight /></button>
</div>
</div>
<style lang="scss">
@import "../styles/vars";
#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;
}
#name { width: 300px; }
#email { width: 300px; }
#line1, #line2 { width: 400px; }
#town { width: 250px; }
#postcode { width: 200px; }
#cancel-button {
padding: 0 $spacing-small;
@ -82,7 +189,7 @@
font-size: $font-size-p;
text-decoration: none;
border-radius: 9999px;
border-radius: $border-radius-circle;
background-color: transparent;
color: $color-on-background;
@ -91,31 +198,8 @@
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 {
#checkoutError {
margin-left: auto;
margin-right: auto;
@ -140,16 +224,33 @@
}
}
.notice {
margin-right: auto;
margin-bottom: $spacing-large;
margin-left: auto;
#checkout-button {
padding: 0 $spacing-normal;
max-width: $sizing-default-width;
height: 40px;
width: 100%;
height: 35px;
position: sticky;
top: calc($spacing-small + $sizing-navigation-height);
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
font-size: $font-size-p;
text-decoration: none;
border-radius: $border-radius-circle;
border: 0 solid transparent;
background-color: $color-primary;
color: $color-on-primary;
&:hover {
background-color: $color-dark;
color: $color-on-dark;
}
}
.unavailable-items-banner {
height: 30px;
display: flex;
justify-content: center;
@ -157,15 +258,88 @@
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;
box-shadow: 0 1px 0.5px rgba(#000, 0.2);
}
.table {
border-radius: $border-radius-normal;
border: 1px solid rgba($color-dark, 0.2);
background-color: $color-light;
overflow: hidden;
table {
width: 100%;
border-collapse: collapse;
tr {
border-bottom: 1px solid rgba($color-dark, 0.2);
&:last-of-type {
border: 0 solid transparent;
}
&.table-row-error {
background-color: $color-error;
color: $color-on-error;
}
th, td {
padding: $spacing-xsmall $spacing-small;
font-size: $font-size-small;
border-right: 1px solid rgba($color-dark, 0.2);
&:last-of-type {
border: 0 solid transparent;
}
}
th {
font-weight: $font-weight-bolder;
}
td {
font-weight: $font-weight-normal;
}
}
}
}
.checkout {
display: flex;
flex-direction: row;
justify-content: normal;
align-items: flex-start;
}
.container {
overflow: hidden;
}
#form-container {
width: 100%;
position: relative;
}
#cart {
min-width: calc(400px - $spacing-normal);
width: 100%;
max-width: calc(400px - $spacing-normal);
position: sticky;
top: calc($sizing-navigation-height + $spacing-normal);
}
@media only screen and (max-width: 900px) {
.checkout {
flex-direction: column;
}
#cart {
max-width: unset;
position: unset;
}
}
</style>

View file

@ -1,104 +0,0 @@
<script lang="ts">
import { link } from "svelte-spa-router";
import { SealWarning } from "phosphor-svelte";
import Cart from "../../lib/cart";
let items = Cart.getEntries();
let totalPrice = Cart.getTotalPrice();
let unavailableItems = false;
Cart.getEntries().forEach(([_, item]) => {
if (!item.data.availability) unavailableItems = true;
});
</script>
<h1>Checkout > Order</h1>
<div class="container">
{#if unavailableItems}
<div class="unavailable-items-banner">
<SealWarning weight="fill" />&nbsp;&nbsp;<span>Order contains an item that is no-longer available</span>
</div>
{/if}
<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>
<hr>
<div class="section small">
<p>To make any changes to your order, <a href="/cart" use:link>please go back to your cart</a>.</p>
</div>
</div>
<style lang="scss">
@import "../../styles/vars";
.container {
overflow: hidden;
}
.unavailable-items-banner {
height: 30px;
display: flex;
justify-content: center;
align-items: center;
font-size: $font-size-p;
background: $color-error;
color: $color-on-error;
box-shadow: 0 1px 0.5px rgba(#000, 0.3);
}
.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

@ -1,66 +0,0 @@
<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

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