From 5f0b357d883a936c1bc7456041de3f5a9ff2457d Mon Sep 17 00:00:00 2001 From: Kevin Midboe Date: Mon, 5 Dec 2022 18:22:08 +0100 Subject: [PATCH 01/33] Updated ts types, sync w/ backend --- src/lib/cartStore.ts | 7 ++++--- src/lib/interfaces/ApiResponse.ts | 18 +++++++++++++--- src/lib/interfaces/IOrder.ts | 12 ++--------- src/lib/websocketCart.ts | 1 + src/routes/checkout/DeliverySection.svelte | 13 ++++++++---- src/routes/checkout/OrderSection.svelte | 14 ++++++------- src/routes/login/+page.svelte | 4 ++-- src/routes/orders/[id]/CustomerDetails.svelte | 8 +++---- src/routes/orders/[id]/OrderSummary.svelte | 2 +- src/routes/receipt/[[id]]/+page.svelte | 21 +++++++++---------- src/routes/sitemap.xml/+server.ts | 4 +--- 11 files changed, 56 insertions(+), 48 deletions(-) diff --git a/src/lib/cartStore.ts b/src/lib/cartStore.ts index 000037c..ef93556 100644 --- a/src/lib/cartStore.ts +++ b/src/lib/cartStore.ts @@ -1,14 +1,15 @@ -import { writable, get, derived } from 'svelte/store'; +import { writable, derived } from 'svelte/store'; import type { Writable, Readable } from 'svelte/store'; +import type ICart from './interfaces/ICart'; -export const cart: Writable = writable([]); +export const cart: Writable = writable([]); export const isOpen: Writable = writable(false); export const count: Readable = derived(cart, ($cart) => $cart.length || 0); export const subTotal: Readable = derived(cart, ($cart) => { let total = 0; - $cart.forEach((cartItem) => (total += cartItem.price * cartItem.quantity)); + $cart.forEach((cartItem: ICart) => (total += cartItem.price * cartItem.quantity)); return total; }); diff --git a/src/lib/interfaces/ApiResponse.ts b/src/lib/interfaces/ApiResponse.ts index e32e071..4dc221b 100644 --- a/src/lib/interfaces/ApiResponse.ts +++ b/src/lib/interfaces/ApiResponse.ts @@ -1,27 +1,39 @@ import type { IProduct } from './IProduct'; import type { IOrder, IOrderSummary } from './IOrder'; +import type ICustomer from './ICustomer'; +import type ICart from './ICart'; export interface IProductResponse { success: boolean; products: Array; } -export interface IOrderResponse { +export interface IOrderDTO { success: boolean; order: IOrder; } +export interface IOrderCreateDTO { + customer: ICustomer; + cart: ICart[]; +} + export interface IOrderSummaryResponse { success: boolean; order: IOrderSummary; } -export interface IProductResponse { +export interface IProductDTO { success: boolean; product: IProduct; } -export interface IProductsResponse { +export interface IProductsDTO { success: boolean; products: Array; } + +export interface ICartDTO { + cart: ICart[]; + success: boolean; +} diff --git a/src/lib/interfaces/IOrder.ts b/src/lib/interfaces/IOrder.ts index 60383cb..f30afb3 100644 --- a/src/lib/interfaces/IOrder.ts +++ b/src/lib/interfaces/IOrder.ts @@ -1,5 +1,6 @@ // import type IProduct from './IProduct'; // import type BadgeType from './BadgeType'; +import type ICustomer from './ICustomer'; export interface IStripePayment { amount: number; @@ -26,17 +27,8 @@ export interface IOrder { created?: Date; } -export interface ICustomer { - city: string; - customer_no: string; - email: string; - firstname: string; - lastname: string; - streetaddress: string; - zipcode: number; -} - export interface ILineItem { + sku_id: number; image: string; name: string; price: number; diff --git a/src/lib/websocketCart.ts b/src/lib/websocketCart.ts index b4b49ff..bede984 100644 --- a/src/lib/websocketCart.ts +++ b/src/lib/websocketCart.ts @@ -1,5 +1,6 @@ import { dev } from '$app/environment'; import { cart as cartStore } from './cartStore'; +import type { ICartDTO } from './interfaces/ApiResponse'; const WS_HOST = '127.0.0.1'; const WS_PORT = 30010; diff --git a/src/routes/checkout/DeliverySection.svelte b/src/routes/checkout/DeliverySection.svelte index 73ffcef..73822eb 100644 --- a/src/routes/checkout/DeliverySection.svelte +++ b/src/routes/checkout/DeliverySection.svelte @@ -11,11 +11,16 @@
- - + + - - + +
diff --git a/src/routes/checkout/OrderSection.svelte b/src/routes/checkout/OrderSection.svelte index 7ddff40..2ef2e13 100644 --- a/src/routes/checkout/OrderSection.svelte +++ b/src/routes/checkout/OrderSection.svelte @@ -21,23 +21,23 @@ {#if $cart.length} - {#each $cart as order} + {#each $cart as cartItem}
- {order.name} - Størrelse: {order.size} + {cartItem.name} + Størrelse: {cartItem.size}
- Nok {order.quantity * order.price} + Nok {cartItem.quantity * cartItem.price} {/each} {:else} diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index 8e96e53..2012ebb 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -6,9 +6,9 @@ let password: string; let displayMessage: string | null; - function postLogin(event: any) { + function postLogin(event: SubmitEvent) { displayMessage = null; - const formData = new FormData(event.target); + const formData = new FormData(event.target as HTMLFormElement); const data = {}; formData.forEach((value, key) => (data[key] = value)); diff --git a/src/routes/orders/[id]/CustomerDetails.svelte b/src/routes/orders/[id]/CustomerDetails.svelte index f265a27..c07cdb7 100644 --- a/src/routes/orders/[id]/CustomerDetails.svelte +++ b/src/routes/orders/[id]/CustomerDetails.svelte @@ -1,5 +1,5 @@
@@ -31,14 +30,14 @@ A payment to PLANETPOSEN, AS will appear on your statement with order number: {id}.

-

Order receipt has been email to: {email}

+

En ordrebekreftelse er sent til: {email}

- {#each products as product} + {#each order?.lineItems as lineItem}

- {product.name} x{product.quantity} - {product.currency} {product.price * product.quantity} + {lineItem.name} x{lineItem.quantity} + NOK {lineItem.price * lineItem.quantity}

{/each}

@@ -48,7 +47,7 @@

Total - NOK {subTotal(products)} + NOK {subTotal(order?.lineItems)}

diff --git a/src/routes/sitemap.xml/+server.ts b/src/routes/sitemap.xml/+server.ts index 4ad1381..71957d1 100644 --- a/src/routes/sitemap.xml/+server.ts +++ b/src/routes/sitemap.xml/+server.ts @@ -1,6 +1,4 @@ -import { dev } from '$app/environment'; -import { env } from '$env/dynamic/private'; -import type { IProductResponse } from '$lib/interfaces/ApiResponse'; +import type { IProductsDTO } from '$lib/interfaces/ApiResponse'; const domain = 'planet.schleppe.cloud'; const pages: Array = [ From 86920d254fc6bd38a1b860aab9a0a7e563c15930 Mon Sep 17 00:00:00 2001 From: Kevin Midboe Date: Mon, 5 Dec 2022 18:25:37 +0100 Subject: [PATCH 02/33] Use hooks to check env and rewrite API call url if localhost --- src/hooks.server.ts | 12 ++++++++++++ src/routes/orders/+page.server.ts | 10 +--------- src/routes/orders/[id]/+page.server.ts | 13 +++---------- src/routes/shop/+page.server.ts | 13 +++---------- src/routes/shop/[id]/+page.server.ts | 13 +++---------- src/routes/sitemap.xml/+server.ts | 11 +++-------- src/routes/warehouse/+page.server.ts | 9 +-------- 7 files changed, 26 insertions(+), 55 deletions(-) create mode 100644 src/hooks.server.ts diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..366931d --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,12 @@ +import type { HandleFetch } from '@sveltejs/kit'; + +export const handleFetch: HandleFetch = async ({ request, fetch }) => { + const { origin } = new URL(request.url); + + if (request.url.startsWith(`${origin}/api`)) { + // clone the original request, but change the URL + request = new Request(request.url.replace(origin, 'http://localhost:30010'), request); + } + + return fetch(request); +}; diff --git a/src/routes/orders/+page.server.ts b/src/routes/orders/+page.server.ts index 33f3b62..7ba128c 100644 --- a/src/routes/orders/+page.server.ts +++ b/src/routes/orders/+page.server.ts @@ -1,16 +1,8 @@ -import { dev } from '$app/environment'; -import { env } from '$env/dynamic/private'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ fetch }) => { - let url = '/api/orders'; - if (dev || env.API_HOST) { - url = (env.API_HOST || 'http://localhost:30010').concat(url); - } - - const res = await fetch(url); + const res = await fetch('/api/orders'); const response = await res.json(); - console.log('orders length:', response?.orders); return { orders: response?.orders || [] diff --git a/src/routes/orders/[id]/+page.server.ts b/src/routes/orders/[id]/+page.server.ts index ba13079..e96d356 100644 --- a/src/routes/orders/[id]/+page.server.ts +++ b/src/routes/orders/[id]/+page.server.ts @@ -1,18 +1,11 @@ -import { dev } from '$app/environment'; -import { env } from '$env/dynamic/private'; -import type { IOrderResponse } from '$lib/interfaces/ApiResponse'; +import type { IOrderDTO } from '$lib/interfaces/ApiResponse'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ fetch, params }) => { const { id } = params; - let url = `/api/order/${id}`; - if (dev || env.API_HOST) { - url = (env.API_HOST || 'http://localhost:30010').concat(url); - } - - const res = await fetch(url); - const orderResponse: IOrderResponse = await res.json(); + const res = await fetch(`/api/order/${id}`); + const orderResponse: IOrderDTO = await res.json(); if (orderResponse?.success == false || orderResponse?.order === undefined) { throw Error(':('); diff --git a/src/routes/shop/+page.server.ts b/src/routes/shop/+page.server.ts index e2602c7..8b06193 100644 --- a/src/routes/shop/+page.server.ts +++ b/src/routes/shop/+page.server.ts @@ -1,16 +1,9 @@ -import { dev } from '$app/environment'; -import { env } from '$env/dynamic/private'; -import type { IProductsResponse } from '$lib/interfaces/ApiResponse'; +import type { IProductsDTO } from '$lib/interfaces/ApiResponse'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ fetch }) => { - let url = '/api/products'; - if (dev || env.API_HOST) { - url = (env.API_HOST || 'http://localhost:30010').concat(url); - } - - const res = await fetch(url); - const products: IProductsResponse = await res.json(); + const res = await fetch('/api/products'); + const products: IProductsDTO = await res.json(); return products; }; diff --git a/src/routes/shop/[id]/+page.server.ts b/src/routes/shop/[id]/+page.server.ts index faf78f6..cd56bdd 100644 --- a/src/routes/shop/[id]/+page.server.ts +++ b/src/routes/shop/[id]/+page.server.ts @@ -1,19 +1,12 @@ -import { dev } from '$app/environment'; -import { env } from '$env/dynamic/private'; import generateProductJsonLd from '$lib/jsonld/product'; -import type { IProductResponse } from '$lib/interfaces/ApiResponse'; +import type { IProductDTO } from '$lib/interfaces/ApiResponse'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ fetch, params }) => { const { id } = params; - let url = `/api/product/${id}`; - if (dev || env.API_HOST) { - url = (env.API_HOST || 'http://localhost:30010').concat(url); - } - - const res = await fetch(url); - const productResponse: IProductResponse = await res.json(); + const res = await fetch(`/api/product/${id}`); + const productResponse: IProductDTO = await res.json(); const jsonld = generateProductJsonLd(productResponse?.product); return { diff --git a/src/routes/sitemap.xml/+server.ts b/src/routes/sitemap.xml/+server.ts index 71957d1..83ab058 100644 --- a/src/routes/sitemap.xml/+server.ts +++ b/src/routes/sitemap.xml/+server.ts @@ -52,15 +52,10 @@ function sitemapPages(): string { } async function sitemapShopPages(): Promise { - let url = `/api/products`; - if (dev || env.API_HOST) { - url = (env.API_HOST || 'http://localhost:30010').concat(url); - } + const res = await fetch('/api/products'); + const productResponse: IProductsDTO = await res.json(); - const res = await fetch(url); - const products: IProductResponse = await res.json(); - - return products?.products + return productResponse?.products ?.map((product) => buildSitemapUrl(`/shop/${product.product_no}`, String(product.updated), 'daily') ) diff --git a/src/routes/warehouse/+page.server.ts b/src/routes/warehouse/+page.server.ts index e548827..212859f 100644 --- a/src/routes/warehouse/+page.server.ts +++ b/src/routes/warehouse/+page.server.ts @@ -1,14 +1,7 @@ -import { dev } from '$app/environment'; -import { env } from '$env/dynamic/private'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ fetch }) => { - let url = '/api/warehouse'; - if (dev || env.API_HOST) { - url = (env.API_HOST || 'http://localhost:30010').concat(url); - } - - const res = await fetch(url); + const res = await fetch('/api/warehouse'); const warehouse = await res.json(); return { From 416303b6013e22c08366c2bc44ed9208fa896ff6 Mon Sep 17 00:00:00 2001 From: Kevin Midboe Date: Mon, 5 Dec 2022 18:27:24 +0100 Subject: [PATCH 03/33] Refactored shared css & removed unused styles --- .../components/ProductVariationSelect.svelte | 5 --- src/lib/icons/CircleCheckmark.svelte | 6 ++-- src/lib/icons/CircleError.svelte | 6 ++-- src/lib/icons/CircleWarning.svelte | 6 ++-- src/lib/icons/circle-feedback.scss | 36 ++++++++----------- src/routes/Header.svelte | 10 +++--- src/routes/orders/OrdersTable.svelte | 13 +++++++ src/routes/orders/[id]/OrderProducts.svelte | 12 +++---- src/routes/orders/[id]/OrderSummary.svelte | 13 ++++++- src/routes/privacy-policy/+page.svelte | 4 +++ src/styles/generic-article.scss | 4 --- 11 files changed, 64 insertions(+), 51 deletions(-) diff --git a/src/lib/components/ProductVariationSelect.svelte b/src/lib/components/ProductVariationSelect.svelte index 3338840..ea1297f 100644 --- a/src/lib/components/ProductVariationSelect.svelte +++ b/src/lib/components/ProductVariationSelect.svelte @@ -59,11 +59,6 @@ &.selected { border-color: black; } - - p { - padding: 0; - margin: 0; - } } } diff --git a/src/lib/icons/CircleCheckmark.svelte b/src/lib/icons/CircleCheckmark.svelte index 46e6614..25a47d1 100644 --- a/src/lib/icons/CircleCheckmark.svelte +++ b/src/lib/icons/CircleCheckmark.svelte @@ -1,8 +1,8 @@ - + - + - + @import '../../styles/media-queries.scss'; + h2 { + // text-decoration: underline; + font-size: 1.2rem; + + .section-count { + background-color: rgba(0,0,0,0.15); + padding: 0.3rem 0.4rem; + margin-left: 0.5rem; + border-radius: 0.5rem; + font-size: 1rem; + } + } + table { width: 100%; border-collapse: collapse; diff --git a/src/routes/orders/[id]/OrderProducts.svelte b/src/routes/orders/[id]/OrderProducts.svelte index 68a4c57..dc24521 100644 --- a/src/routes/orders/[id]/OrderProducts.svelte +++ b/src/routes/orders/[id]/OrderProducts.svelte @@ -39,9 +39,13 @@
Date: Mon, 5 Dec 2022 18:39:32 +0100 Subject: [PATCH 06/33] Remvoed & simplified or refactored general functionality --- src/lib/components/Badge.svelte | 6 +- src/lib/components/Cart.svelte | 4 +- src/lib/utils/mock.ts | 87 ------------------------- src/lib/websocketCart.ts | 23 ++++--- src/routes/checkout/OrderSection.svelte | 2 +- 5 files changed, 23 insertions(+), 99 deletions(-) delete mode 100644 src/lib/utils/mock.ts diff --git a/src/lib/components/Badge.svelte b/src/lib/components/Badge.svelte index 34e6bcf..5baf82c 100644 --- a/src/lib/components/Badge.svelte +++ b/src/lib/components/Badge.svelte @@ -11,8 +11,12 @@ export let title = 'Info'; export let type: BadgeType = BadgeType.INFO; - export let icon: string = badgeIcons[type]; + if (title === 'CONFIRMED') { + type = BadgeType.SUCCESS; + } + + $: icon = badgeIcons[type]; $: badgeClass = `badge ${type}`; diff --git a/src/lib/components/Cart.svelte b/src/lib/components/Cart.svelte index d0079ef..2f3b033 100644 --- a/src/lib/components/Cart.svelte +++ b/src/lib/components/Cart.svelte @@ -1,9 +1,11 @@ -
+
{#if $count > 0} {$count} {/if} diff --git a/src/lib/utils/mock.ts b/src/lib/utils/mock.ts deleted file mode 100644 index 76adac2..0000000 --- a/src/lib/utils/mock.ts +++ /dev/null @@ -1,87 +0,0 @@ -import generateUUID from './uuid'; -import type IProduct from '../interfaces/IProduct'; -import type { IOrder, ICustomer } from '../interfaces/IOrders'; -import BadgeType from '../interfaces/BadgeType'; - -const productNames = ["Who's Who", 'Lullaby', 'The Buried Life', 'The Illegitimate']; - -const images = [ - 'https://cdn-fsly.yottaa.net/551561a7312e580499000a44/www.joann.com/v~4b.100/dw/image/v2/AAMM_PRD/on/demandware.static/-/Sites-joann-product-catalog/default/dw4a83425c/images/hi-res/18/18163006.jpg?sw=556&sh=680&sm=fit&yocs=7x_7C_7D_', - 'https://cdn-fsly.yottaa.net/55d09df20b53443653002f02/www.joann.com/v~4b.ed/dw/image/v2/AAMM_PRD/on/demandware.static/-/Sites-joann-product-catalog/default/dwc10a651e/images/hi-res/alt/17767534Alt1.jpg?sw=350&sh=350&sm=fit&yocs=f_', - 'https://cdn-fsly.yottaa.net/55d09df20b53443653002f02/www.joann.com/v~4b.ed/dw/image/v2/AAMM_PRD/on/demandware.static/-/Sites-joann-product-catalog/default/dw3f43e4d8/images/hi-res/alt/18995779ALT1.jpg?sw=350&sh=350&sm=fit&yocs=f_', - 'https://cdn-fsly.yottaa.net/551561a7312e580499000a44/www.joann.com/v~4b.100/dw/image/v2/AAMM_PRD/on/demandware.static/-/Sites-joann-product-catalog/default/dw029904bd/images/hi-res/alt/18162834alt1.jpg?sw=350&sh=350&sm=fit&yocs=7x_7C_7D_', - 'https://adrianbrinkerhoff.imgix.net/AdrianBrinkerhoff-MatthewThompson-103.jpg?auto=compress%2Cformat&bg=%23FFFFFF&crop=focalpoint&fit=crop&fp-x=0.5&fp-y=0.5&h=431&q=90&w=310&s=018ae410aa6b64e6c9c5ca6bb18a1137', - 'https://adrianbrinkerhoff.imgix.net/AdrianBrinkerhoff-MatthewThompson-166.jpg?auto=compress%2Cformat&bg=%23FFFFFF&crop=focalpoint&fit=crop&fp-x=0.5&fp-y=0.5&h=431&q=90&w=310&s=50a1f0fb259452fb84453ee4216dd4f1', - 'https://adrianbrinkerhoff.imgix.net/AdrianBrinkerhoff-MatthewThompson-108.jpg?auto=compress%2Cformat&bg=%23FFFFFF&crop=focalpoint&fit=crop&fp-x=0.5&fp-y=0.5&h=431&q=90&w=310&s=b4a75bdea66974a4f766ded52bfe9ba0', - 'https://adrianbrinkerhoff.imgix.net/AdrianBrinkerhoff-MatthewThompson-32.jpg?auto=compress%2Cformat&bg=%23FFFFFF&crop=focalpoint&fit=crop&fp-x=0.5&fp-y=0.5&h=431&q=90&w=310&s=9199c53ea58a923373f7bcce1145193e' -]; - -const statusText = { - [BadgeType.INFO]: 'Pending', - [BadgeType.SUCCESS]: 'Succeeded', - [BadgeType.WARNING]: 'Warning', - [BadgeType.PENDING]: 'In transit', - [BadgeType.ERROR]: 'Error' -}; - -const statusTypes = [ - BadgeType.INFO, - BadgeType.SUCCESS, - BadgeType.WARNING, - BadgeType.PENDING, - BadgeType.ERROR -]; - -function mockCustomer(): ICustomer { - const customer: ICustomer = { - email: 'kevin.midboe@gmail.com', - firstName: 'kevin', - lastName: 'midbøe', - streetAddress: 'Schleppegrells gate 18', - zipCode: '0556', - city: 'Oslo' - }; - - customer.fullName = `${customer.firstName} ${customer.lastName}`; - return customer; -} - -export function mockOrder(id: string | null = null): IOrder { - const products = mockProducts(4); - const status = statusTypes[Math.floor(Math.random() * statusTypes.length)]; - - return { - uuid: id || generateUUID(), - products, - customer: mockCustomer(), - payment: { - amount: Math.round(Math.random() * 800), - currency: 'NOK' - }, - createdDate: new Date(), - updatedDate: new Date(), - status: { - type: status, - text: statusText[status] - } - }; -} - -export function mockOrders(count: number): Array { - return Array.from(Array(count)).map(() => mockOrder()); -} - -export function mockProduct(): IProduct { - return { - uuid: generateUUID(), - name: productNames[Math.floor(Math.random() * productNames.length)], - price: Math.floor(Math.random() * 999), - quantity: Math.floor(Math.random() * 4) + 1, - currency: 'NOK', - image: images[Math.floor(Math.random() * images.length)] - }; -} - -export function mockProducts(count: number): Array { - return Array.from(Array(count)).map(() => mockProduct()); -} diff --git a/src/lib/websocketCart.ts b/src/lib/websocketCart.ts index bede984..1440e3b 100644 --- a/src/lib/websocketCart.ts +++ b/src/lib/websocketCart.ts @@ -47,6 +47,19 @@ function sendPayload(payload: object) { ws.send(JSON.stringify(payload)); } +// websocket.onmessage +function receivePayload(event: MessageEvent) { + try { + const json = JSON.parse(event?.data || {}); + const { success, cart } = json as ICartDTO; + if (success && cart) cartStore.set(cart); + } catch { + console.debug('Non parsable message from server: ', event?.data); + } +} + +// Called by routes/+layout.svelte on every navigation, +// if ws is closed we try reconnect export function reconnectIfCartWSClosed() { const closed = ws?.readyState === 3; if (!closed) return; @@ -88,15 +101,7 @@ export function connectToCart(attempts = 0, maxAttempts = 6) { heartbeat(); }; - ws.onmessage = (event: MessageEvent) => { - try { - const json = JSON.parse(event?.data || {}); - const { success, cart } = json; - if (success && cart) cartStore.set(cart); - } catch { - console.debug('Non parsable message from server: ', event?.data); - } - }; + ws.onmessage = (event) => receivePayload(event); ws.onclose = () => { const seconds = attempts ** 2; diff --git a/src/routes/checkout/OrderSection.svelte b/src/routes/checkout/OrderSection.svelte index 2ef2e13..33744ab 100644 --- a/src/routes/checkout/OrderSection.svelte +++ b/src/routes/checkout/OrderSection.svelte @@ -68,7 +68,7 @@ - + diff --git a/src/lib/icons/CircleWarning.svelte b/src/lib/components/loading/CircleWarning.svelte similarity index 76% rename from src/lib/icons/CircleWarning.svelte rename to src/lib/components/loading/CircleWarning.svelte index 80a0d05..6f94b6d 100644 --- a/src/lib/icons/CircleWarning.svelte +++ b/src/lib/components/loading/CircleWarning.svelte @@ -1,6 +1,6 @@ - + + stroke-dashoffset="28"> + transform="translate(24,24) rotate(-35)"> diff --git a/src/lib/components/StripeCard.svelte b/src/lib/components/StripeCard.svelte index 5290be3..5edf616 100644 --- a/src/lib/components/StripeCard.svelte +++ b/src/lib/components/StripeCard.svelte @@ -1,11 +1,15 @@ @@ -96,7 +48,6 @@ .card { // padding: 1rem; - margin: 0 0.5rem; border: 2px solid black; @include desktop { diff --git a/src/lib/interfaces/IOrderValidationError.ts b/src/lib/interfaces/IOrderValidationError.ts new file mode 100644 index 0000000..9cd620e --- /dev/null +++ b/src/lib/interfaces/IOrderValidationError.ts @@ -0,0 +1,5 @@ +export default interface IOrderValidationError { + type: string; + field: number | string; + message: string; +} diff --git a/src/lib/stripe/index.ts b/src/lib/stripe/index.ts new file mode 100644 index 0000000..77d4942 --- /dev/null +++ b/src/lib/stripe/index.ts @@ -0,0 +1,100 @@ +import { buildApiUrl } from '$lib/utils/apiUrl'; +import { loadStripe } from '@stripe/stripe-js/pure'; +import type { + ConfirmCardPaymentData, + PaymentIntentResult, + Stripe, + StripeError +} from '@stripe/stripe-js'; + +let stripeInstance: Stripe; + +class StripeUnableToAccepPaymentsError extends Error { + constructor() { + const message = 'Card failed to load or never got a client secret. Unable to accept payments'; + super(message); + } +} + +// class StripePaymentError extends Error { +// code: string | undefined +// type: string + +// constructor(error: StripeError) { +// const message = error.message || 'Unexptected error from stripe'; +// super(message); + +// console.log('stripe error:', error); +// this.code = error.code; +// this.type = error.type; +// } +// } + +class StripeMissingPaymentIntent extends Error { + constructor() { + const message = 'Stripe responded without payment intent'; + super(message); + } +} + +// Calls backend for a client secret. order_id & customer_no +// is sent to attach to stripe payment intent +function clientSecret(order_id: string, customer_no: string): Promise { + const url = buildApiUrl('/api/v1/payment/stripe'); + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ order_id, customer_no }) + }; + + return fetch(url, options) + .then((resp) => resp.json()) + .then((data) => data?.clientSecret); +} + +// Creates stripe instance from package: @stripe/stripe-js +async function load(apiKey: string): Promise { + if (stripeInstance) return stripeInstance; + + loadStripe.setLoadParameters({ advancedFraudSignals: false }); + const stripe: Stripe | null = await loadStripe(apiKey); + if (!stripe) { + console.error('Unable to load stripe payment'); + return; + } + + stripeInstance = stripe; + return stripe; +} + +// Uses @stripe/stripe-js to make payment request from +// stripe Elements directly to Stripe. Server receives +// async webhook updates from Stripe. +function pay(secret: string, data: ConfirmCardPaymentData) { + if (!clientSecret || !stripeInstance) { + throw new StripeUnableToAccepPaymentsError(); + } + + return stripeInstance + .confirmCardPayment(secret, data) + .then((stripePaymentResponse: PaymentIntentResult) => { + const { error, paymentIntent } = stripePaymentResponse; + + if (error) { + throw error; + } else if (paymentIntent) { + console.log('payment intent from stripe:', paymentIntent); + return paymentIntent.status === 'succeeded'; + } + + throw new StripeMissingPaymentIntent(); + }); +} + +export default { + clientSecret, + load, + pay +}; diff --git a/src/lib/utils/apiUrl.ts b/src/lib/utils/apiUrl.ts new file mode 100644 index 0000000..6783bfd --- /dev/null +++ b/src/lib/utils/apiUrl.ts @@ -0,0 +1,14 @@ +import { dev, browser } from '$app/environment'; + +const LOCALHOST_API = 'http://localhost:30010'; + +export function buildApiUrl(path: string) { + let localhostApi = false; + + if (dev) localhostApi = true; + if (browser && window?.location?.href.includes('localhost')) { + localhostApi = true; + } + + return localhostApi ? LOCALHOST_API.concat(path) : path; +} diff --git a/src/routes/checkout/+page.server.ts b/src/routes/checkout/+page.server.ts new file mode 100644 index 0000000..0fee320 --- /dev/null +++ b/src/routes/checkout/+page.server.ts @@ -0,0 +1,8 @@ +import { env } from '$env/dynamic/private'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = () => { + return { + stripeApiKey: env.STRIPE_API_KEY || '' + }; +}; diff --git a/src/routes/checkout/+page.svelte b/src/routes/checkout/+page.svelte index afdc8dd..918d689 100644 --- a/src/routes/checkout/+page.svelte +++ b/src/routes/checkout/+page.svelte @@ -1,36 +1,140 @@ @@ -39,8 +143,8 @@ description="Kasse for bestilling og betaling av produkter i handlekurven" /> -

Checkout

-
+

Kassen

+

Leveringsaddresse

@@ -48,31 +152,30 @@

Din ordre

- - - +

Betalingsinformasjon

- +
+
+ {#if paymentPromise} + + {/if} +
+ + diff --git a/src/routes/checkout/OrderSection.svelte b/src/routes/checkout/OrderSection.svelte index 33744ab..e61d68c 100644 --- a/src/routes/checkout/OrderSection.svelte +++ b/src/routes/checkout/OrderSection.svelte @@ -4,76 +4,78 @@ import { cart, subTotal } from '$lib/cartStore'; import { decrementProductInCart, incrementProductInCart } from '$lib/websocketCart'; - // $: totalPrice = $cart - // .map((order: IOrderLineItem) => order?.price * order.quantity) - // .reduce((a, b) => a + b); const shippingPrice = 75; + $: totalPrice = $subTotal + shippingPrice; + + function lineItemClass(id: number) { + return `lineitem-${id}`; + } - - - - - - - - - - - {#if $cart.length} - {#each $cart as cartItem} - - - - - - {/each} - {:else} - - - - +
+
VarenavnAntallPris
-
- {cartItem.name} - Størrelse: {cartItem.size} -
-
- - Nok {cartItem.quantity * cartItem.price}
(ingen produkter)0Nok 0
+ + + + + - {/if} + - - - - - + + {#if $cart.length} + {#each $cart as cartItem} + + + + + + {/each} + {:else} + + + + + + {/if} - - - - - + + + + + - - - - - - -
VarenavnAntallPris
Totalpris:Nok {$subTotal}
+
+ {cartItem.name} + Størrelse: {cartItem.size} +
+
+ + Nok {cartItem.quantity * cartItem.price}
(ingen produkter)0Nok 0
Frakt:Nok {shippingPrice}
Totalsum:Nok {$subTotal}
Totalsum:Nok {$subTotal}
+ + Frakt: + + Nok {shippingPrice} + + + + Pris: + + Nok {totalPrice} + + + +
- diff --git a/src/routes/receipt/[[id]]/ReceiptNotFound.svelte b/src/routes/receipt/[[id]]/ReceiptNotFound.svelte index 050e761..299e5cc 100644 --- a/src/routes/receipt/[[id]]/ReceiptNotFound.svelte +++ b/src/routes/receipt/[[id]]/ReceiptNotFound.svelte @@ -1,33 +1,16 @@
- +

Fant ikke din bestilling!

@@ -40,7 +23,8 @@