diff --git a/src/lib/cartStore.ts b/src/lib/cartStore.ts new file mode 100644 index 0000000..000037c --- /dev/null +++ b/src/lib/cartStore.ts @@ -0,0 +1,21 @@ +import { writable, get, derived } from 'svelte/store'; +import type { Writable, Readable } from 'svelte/store'; + +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)); + return total; +}); + +export function toggleCart() { + isOpen.update((state) => !state); +} + +export function closeCart() { + isOpen.set(false); +} diff --git a/src/lib/components/ApplePayButton.svelte b/src/lib/components/ApplePayButton.svelte new file mode 100644 index 0000000..a2dedbc --- /dev/null +++ b/src/lib/components/ApplePayButton.svelte @@ -0,0 +1,188 @@ + + +
+ Hurtigkasse med + +
+ + diff --git a/src/lib/components/Badge.svelte b/src/lib/components/Badge.svelte new file mode 100644 index 0000000..34e6bcf --- /dev/null +++ b/src/lib/components/Badge.svelte @@ -0,0 +1,60 @@ + + +
+ {title} + {icon} +
+ + diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte new file mode 100644 index 0000000..9d02fe6 --- /dev/null +++ b/src/lib/components/Button.svelte @@ -0,0 +1,42 @@ + + + + + diff --git a/src/lib/components/Cart.svelte b/src/lib/components/Cart.svelte new file mode 100644 index 0000000..d0079ef --- /dev/null +++ b/src/lib/components/Cart.svelte @@ -0,0 +1,55 @@ + + +
+ {#if $count > 0} + {$count} + {/if} + + +
+ + diff --git a/src/lib/components/FrontText.svelte b/src/lib/components/FrontText.svelte new file mode 100644 index 0000000..932aa73 --- /dev/null +++ b/src/lib/components/FrontText.svelte @@ -0,0 +1,64 @@ + + +
+
+

{data.title}

+

{data.text}

+
+
+ + diff --git a/src/lib/components/FrontTextImage.svelte b/src/lib/components/FrontTextImage.svelte new file mode 100644 index 0000000..d5903dc --- /dev/null +++ b/src/lib/components/FrontTextImage.svelte @@ -0,0 +1,78 @@ + + +
+
+ mushroom +
+ +
+

{data?.title}

+ + {data?.text} +
+
+ + diff --git a/src/lib/components/FrontTextImageBubble.svelte b/src/lib/components/FrontTextImageBubble.svelte new file mode 100644 index 0000000..cf89d20 --- /dev/null +++ b/src/lib/components/FrontTextImageBubble.svelte @@ -0,0 +1,98 @@ +
+
+

Å stoppe tapet av biologisk mangfold globalt

+

+ En million arter av dyr, planter og sopp er nå truet med utryddelse. Biomangfold er viktig for + alt liv på jorden fordi det bidrar til stabile økosystemer. Naturmangfoldet kan bevares + gjennom mer kunnskap og vern. +

+
+ +
+
+ bee inspection +
+
+
+ + diff --git a/src/lib/components/Input.svelte b/src/lib/components/Input.svelte new file mode 100644 index 0000000..7dbcf88 --- /dev/null +++ b/src/lib/components/Input.svelte @@ -0,0 +1,61 @@ + + + + + diff --git a/src/lib/components/LinkArrow.svelte b/src/lib/components/LinkArrow.svelte new file mode 100644 index 0000000..de23de1 --- /dev/null +++ b/src/lib/components/LinkArrow.svelte @@ -0,0 +1,15 @@ + + + + + + + diff --git a/src/lib/components/ProductVariationSelect.svelte b/src/lib/components/ProductVariationSelect.svelte new file mode 100644 index 0000000..3338840 --- /dev/null +++ b/src/lib/components/ProductVariationSelect.svelte @@ -0,0 +1,69 @@ + + +
    + {#each variations as variation} +
  • + Størrelse: {variation.size} + NOK {variation.price} +
  • + {/each} +
+ + diff --git a/src/lib/components/QuantitySelect.svelte b/src/lib/components/QuantitySelect.svelte new file mode 100644 index 0000000..e2513d6 --- /dev/null +++ b/src/lib/components/QuantitySelect.svelte @@ -0,0 +1,137 @@ + + +
+ - + + + +
+ + diff --git a/src/lib/components/StripeCard.svelte b/src/lib/components/StripeCard.svelte new file mode 100644 index 0000000..5290be3 --- /dev/null +++ b/src/lib/components/StripeCard.svelte @@ -0,0 +1,114 @@ + + +
+
+
+ +
+ + diff --git a/src/lib/components/VippsHurtigkasse.svelte b/src/lib/components/VippsHurtigkasse.svelte new file mode 100644 index 0000000..b1c7253 --- /dev/null +++ b/src/lib/components/VippsHurtigkasse.svelte @@ -0,0 +1,48 @@ + + + diff --git a/src/lib/icons/CircleCheckmark.svelte b/src/lib/icons/CircleCheckmark.svelte new file mode 100644 index 0000000..46e6614 --- /dev/null +++ b/src/lib/icons/CircleCheckmark.svelte @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/src/lib/icons/CircleError.svelte b/src/lib/icons/CircleError.svelte new file mode 100644 index 0000000..327de50 --- /dev/null +++ b/src/lib/icons/CircleError.svelte @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/src/lib/icons/CircleWarning.svelte b/src/lib/icons/CircleWarning.svelte new file mode 100644 index 0000000..b34c5f3 --- /dev/null +++ b/src/lib/icons/CircleWarning.svelte @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/src/lib/icons/IconCart.svelte b/src/lib/icons/IconCart.svelte new file mode 100644 index 0000000..7ac338a --- /dev/null +++ b/src/lib/icons/IconCart.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/src/lib/icons/IconTrashcan.svelte b/src/lib/icons/IconTrashcan.svelte new file mode 100644 index 0000000..ee5f1da --- /dev/null +++ b/src/lib/icons/IconTrashcan.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/src/lib/icons/circle-feedback.scss b/src/lib/icons/circle-feedback.scss new file mode 100644 index 0000000..f07284e --- /dev/null +++ b/src/lib/icons/circle-feedback.scss @@ -0,0 +1,184 @@ +@-webkit-keyframes fadeIn { + 0% { + opacity: 0; + } + + 13.3% { + opacity: 0; + } + + 15% { + opacity: 1; + } + + to { + opacity: 1; + } +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + + 13.3% { + opacity: 0; + } + + 15% { + opacity: 1; + } + + to { + opacity: 1; + } +} + +@-webkit-keyframes drawCheckmark { + 0% { + stroke-dashoffset: 28px; + } + + 15% { + stroke-dashoffset: 28px; + -webkit-animation-timing-function: cubic-bezier(1, 0, 0, 1); + animation-timing-function: cubic-bezier(1, 0, 0, 1); + } + + 55% { + stroke-dashoffset: 0px; + } + + to { + stroke-dashoffset: 0px; + } +} + +@keyframes drawCheckmark { + 0% { + stroke-dashoffset: 28px; + } + + 15% { + stroke-dashoffset: 28px; + -webkit-animation-timing-function: cubic-bezier(1, 0, 0, 1); + animation-timing-function: cubic-bezier(1, 0, 0, 1); + } + + 55% { + stroke-dashoffset: 0px; + } + + to { + stroke-dashoffset: 0px; + } +} + +@-webkit-keyframes checkmarkCircleShimmer { + 0% { + -webkit-transform: translate(24px, 24px) rotate(-35deg); + transform: translate(24px, 24px) rotate(-35deg); + } + + 15% { + -webkit-animation-timing-function: cubic-bezier(1, 0, 0, 1); + animation-timing-function: cubic-bezier(1, 0, 0, 1); + -webkit-transform: translate(24px, 24px) rotate(-35deg); + transform: translate(24px, 24px) rotate(-35deg); + } + + 75% { + -webkit-transform: translate(24px, 24px) rotate(325deg); + transform: translate(24px, 24px) rotate(325deg); + } + + to { + -webkit-transform: translate(24px, 24px) rotate(325deg); + transform: translate(24px, 24px) rotate(325deg); + } +} + +@keyframes checkmarkCircleShimmer { + 0% { + -webkit-transform: translate(24px, 24px) rotate(-35deg); + transform: translate(24px, 24px) rotate(-35deg); + } + + 15% { + -webkit-animation-timing-function: cubic-bezier(1, 0, 0, 1); + animation-timing-function: cubic-bezier(1, 0, 0, 1); + -webkit-transform: translate(24px, 24px) rotate(-35deg); + transform: translate(24px, 24px) rotate(-35deg); + } + + 75% { + -webkit-transform: translate(24px, 24px) rotate(325deg); + transform: translate(24px, 24px) rotate(325deg); + } + + to { + -webkit-transform: translate(24px, 24px) rotate(325deg); + transform: translate(24px, 24px) rotate(325deg); + } +} + +.CheckSuccess-checkGroup, +.CheckError-checkGroup, +.CheckWarning-checkGroup { + -webkit-animation: fadeIn 1s linear both; + animation: fadeIn 1s linear both; +} + +.CheckSuccess-check, +.CheckError-cross, +.CheckWarning-exclamation { + -webkit-animation: drawCheckmark 1s linear both; + animation: drawCheckmark 1s linear both; +} + +.CheckSuccess-circle, +.CheckError-circle, +.CheckWarning-circle { + -webkit-animation: checkmarkCircleShimmer 1s linear both, drawCircle 1s linear both; + animation: checkmarkCircleShimmer 1s linear both, drawCircle 1s linear both; +} + +@-webkit-keyframes drawCircle { + 0% { + stroke-dashoffset: 145px; + } + + 8.35% { + stroke-dashoffset: 145px; + -webkit-animation-timing-function: cubic-bezier(1, 0, 0, 1); + animation-timing-function: cubic-bezier(1, 0, 0, 1); + } + + 38.35% { + stroke-dashoffset: 0px; + } + + to { + stroke-dashoffset: 0px; + } +} + +@keyframes drawCircle { + 0% { + stroke-dashoffset: 145px; + } + + 8.35% { + stroke-dashoffset: 145px; + -webkit-animation-timing-function: cubic-bezier(1, 0, 0, 1); + animation-timing-function: cubic-bezier(1, 0, 0, 1); + } + + 38.35% { + stroke-dashoffset: 0px; + } + + to { + stroke-dashoffset: 0px; + } +} diff --git a/src/lib/images/svelte-logo.svg b/src/lib/images/svelte-logo.svg new file mode 100644 index 0000000..49492a8 --- /dev/null +++ b/src/lib/images/svelte-logo.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/src/lib/images/svelte-welcome.png b/src/lib/images/svelte-welcome.png new file mode 100644 index 0000000..fe7d2d6 Binary files /dev/null and b/src/lib/images/svelte-welcome.png differ diff --git a/src/lib/images/svelte-welcome.webp b/src/lib/images/svelte-welcome.webp new file mode 100644 index 0000000..6ec1a28 Binary files /dev/null and b/src/lib/images/svelte-welcome.webp differ diff --git a/src/lib/interfaces/ApiResponse.ts b/src/lib/interfaces/ApiResponse.ts new file mode 100644 index 0000000..b179200 --- /dev/null +++ b/src/lib/interfaces/ApiResponse.ts @@ -0,0 +1,17 @@ +import type IProduct from './IProduct'; +import type { IOrder, IOrderSummary } from './IOrder'; + +export interface IProductResponse { + success: boolean; + products: Array; +} + +export interface IOrderResponse { + success: boolean; + order: IOrder; +} + +export interface IOrderSummaryResponse { + success: boolean; + order: IOrderSummary; +} diff --git a/src/lib/interfaces/BadgeType.ts b/src/lib/interfaces/BadgeType.ts new file mode 100644 index 0000000..906baba --- /dev/null +++ b/src/lib/interfaces/BadgeType.ts @@ -0,0 +1,9 @@ +enum BadgeType { + SUCCESS = 'success', + WARNING = 'warning', + ERROR = 'error', + PENDING = 'pending', + INFO = 'info' +} + +export default BadgeType; diff --git a/src/lib/interfaces/IFrontText.ts b/src/lib/interfaces/IFrontText.ts new file mode 100644 index 0000000..85bde75 --- /dev/null +++ b/src/lib/interfaces/IFrontText.ts @@ -0,0 +1,5 @@ +export default interface IFrontText { + title: string; + text: string; + color: string; +} diff --git a/src/lib/interfaces/IFrontTextImage.ts b/src/lib/interfaces/IFrontTextImage.ts new file mode 100644 index 0000000..736910c --- /dev/null +++ b/src/lib/interfaces/IFrontTextImage.ts @@ -0,0 +1,6 @@ +export default interface IFrontTextImage { + title: string; + text: string; + image: string; + imageRight?: boolean; +} diff --git a/src/lib/interfaces/IOrder.ts b/src/lib/interfaces/IOrder.ts new file mode 100644 index 0000000..60383cb --- /dev/null +++ b/src/lib/interfaces/IOrder.ts @@ -0,0 +1,69 @@ +// import type IProduct from './IProduct'; +// import type BadgeType from './BadgeType'; + +export interface IStripePayment { + amount: number; + currency: string; +} + +export interface IOrderSummary { + created: Date; + email: string; + first_name: string; + last_name: string; + order_id: string; + order_sum: number; + status: string; +} + +export interface IOrder { + customer: ICustomer; + lineItems: ILineItem[]; + orderid: string; + shipping: IShipping; + status: string; + updated?: Date; + created?: Date; +} + +export interface ICustomer { + city: string; + customer_no: string; + email: string; + firstname: string; + lastname: string; + streetaddress: string; + zipcode: number; +} + +export interface ILineItem { + image: string; + name: string; + price: number; + quantity: number; + size: string; +} + +export interface IShipping { + company: string; + tracking_code: string; + tracking_link: string; + user_notified: null; +} + +export interface IOrdersLineitem { + orders_lineitem_id: string; + order_id: string; + product_no: number; + product_sku_no: number; + quantity: number; + created?: Date; + updated?: Date; +} + +export interface ITracking { + orderId: string; + trackingCode: string; + trackingCompany: string; + trackingLink: string; +} diff --git a/src/lib/interfaces/IProduct.ts b/src/lib/interfaces/IProduct.ts new file mode 100644 index 0000000..a89a910 --- /dev/null +++ b/src/lib/interfaces/IProduct.ts @@ -0,0 +1,16 @@ +import type IProductVariation from './IProductVariation'; + +export default interface IProduct { + product_no: number; + name: string; + subtext?: string; + description?: string; + image: string; + currency: string; + variations?: IProductVariation[]; + primary_color?: string; + variation_count?: string; + sum_stock?: number; + updated?: Date; + created?: Date; +} diff --git a/src/lib/interfaces/IProductVariation.ts b/src/lib/interfaces/IProductVariation.ts new file mode 100644 index 0000000..2b2cb98 --- /dev/null +++ b/src/lib/interfaces/IProductVariation.ts @@ -0,0 +1,9 @@ +export default interface IProductVariation { + sku_id: number | null; + price: number; + size: string | null; + stock: number; + default_price: boolean; + updated?: Date; + created?: Date; +} diff --git a/src/lib/utils/cookie.ts b/src/lib/utils/cookie.ts new file mode 100644 index 0000000..35f4a9a --- /dev/null +++ b/src/lib/utils/cookie.ts @@ -0,0 +1,24 @@ +export function getCookie(name: string): string | object { + if (!document) return; + + const cookies = document.cookie.split(';').reduce((acc, cookieString) => { + const [key, value] = cookieString.split('=').map((s) => s.trim()); + if (key && value) { + acc[key] = decodeURIComponent(value); + } + return acc; + }, {}); + + return name ? cookies[name] || '' : cookies; +} + +export function setCookie(name: string, value: string, options = {}) { + if (!document) return; + + document.cookie = `${name}=${encodeURIComponent(value)}${Object.keys(options).reduce( + (acc, key) => { + return acc + `;${key.replace(/([A-Z])/g, ($1) => '-' + $1.toLowerCase())}=${options[key]}`; + }, + '' + )}`; +} diff --git a/src/lib/utils/mock.ts b/src/lib/utils/mock.ts new file mode 100644 index 0000000..76adac2 --- /dev/null +++ b/src/lib/utils/mock.ts @@ -0,0 +1,87 @@ +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/utils/requestSessionCookie.ts b/src/lib/utils/requestSessionCookie.ts new file mode 100644 index 0000000..a7b6181 --- /dev/null +++ b/src/lib/utils/requestSessionCookie.ts @@ -0,0 +1,11 @@ +import { dev } from '$app/environment'; + +export default async function requestSessionCookie() { + let url = '/api'; + if (dev) { + url = 'http://localhost:30010'.concat(url); + } + + await fetch(url); + return true; +} diff --git a/src/lib/utils/uuid.ts b/src/lib/utils/uuid.ts new file mode 100644 index 0000000..8cd4208 --- /dev/null +++ b/src/lib/utils/uuid.ts @@ -0,0 +1,8 @@ +export default function generateUUID(len = 8) { + const hex = '0123456789abcdef'; + let output = ''; + for (let i = 0; i < len; ++i) { + output += hex.charAt(Math.floor(Math.random() * hex.length)); + } + return output; +} diff --git a/src/lib/utils/validOrderId.ts b/src/lib/utils/validOrderId.ts new file mode 100644 index 0000000..b085103 --- /dev/null +++ b/src/lib/utils/validOrderId.ts @@ -0,0 +1,5 @@ +export default function validOrderId(orderId = '') { + const re = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + const isValidOrderId = String(orderId).match(re); + return isValidOrderId ? true : false; +} diff --git a/src/lib/websocketCart.ts b/src/lib/websocketCart.ts new file mode 100644 index 0000000..b4b49ff --- /dev/null +++ b/src/lib/websocketCart.ts @@ -0,0 +1,110 @@ +import { dev } from '$app/environment'; +import { cart as cartStore } from './cartStore'; + +const WS_HOST = '127.0.0.1'; +const WS_PORT = 30010; +let ws: WebSocket; +let wsReconnectTimeout: ReturnType | undefined; + +function getCookie(key: string) { + const name = key + '='; + const decodedCookie = decodeURIComponent(document?.cookie || ''); + const ca = decodedCookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return null; +} + +export function getCart() { + sendPayload({ command: 'cart' }); +} + +export function addProductToCart(product_no: number, product_sku_no: number, quantity: number) { + sendPayload({ command: 'add', product_no, product_sku_no, quantity }); +} + +export function removeProductFromCart(lineitem_id: number) { + sendPayload({ command: 'rm', lineitem_id }); +} + +export function decrementProductInCart(lineitem_id: number) { + sendPayload({ command: 'decrement', lineitem_id }); +} + +export function incrementProductInCart(lineitem_id: number) { + sendPayload({ command: 'increment', lineitem_id }); +} + +function sendPayload(payload: object) { + ws.send(JSON.stringify(payload)); +} + +export function reconnectIfCartWSClosed() { + const closed = ws?.readyState === 3; + if (!closed) return; + connectToCart(); +} + +function heartbeat() { + if (!ws) return; + if (ws.readyState !== 1) return; + ws.send('heartbeat'); + setTimeout(heartbeat, 40000); +} + +export function connectToCart(attempts = 0, maxAttempts = 6) { + attempts = attempts + 1; + clearTimeout(wsReconnectTimeout); + wsReconnectTimeout = undefined; + + if (attempts >= maxAttempts) { + console.debug('Max retries, waiting for explicit reconnect function call.'); + return; + } + + // TODO user feedback when max retries to reconnect, should refresh + // increasing timeout by a factor + const planetId = getCookie('planetId'); + if (!planetId) return console.log('no cookie'); + + let url = `wss://${window.location.hostname}/ws/cart`; + if (dev) { + url = `ws://${WS_HOST}:${WS_PORT}/ws/cart?planetId=${planetId}`; + } + + ws = new WebSocket(url); + + ws.onopen = () => { + console.debug('Websocket connection for cart established'); + getCart(); + 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.onclose = () => { + const seconds = attempts ** 2; + console.debug(`Socket is closed. Reconnect will be attempted in ${seconds} seconds.`); + wsReconnectTimeout = setTimeout(() => connectToCart(attempts, maxAttempts), seconds * 1000); + }; + + ws.onerror = (err: Event) => { + console.error('Unexpected websocket error:', err); + ws.close(); + }; +}