All sveltekit library files
21
src/lib/cartStore.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { writable, get, derived } from 'svelte/store';
|
||||
import type { Writable, Readable } from 'svelte/store';
|
||||
|
||||
export const cart: Writable<any> = writable([]);
|
||||
export const isOpen: Writable<boolean> = writable(false);
|
||||
|
||||
export const count: Readable<number> = derived(cart, ($cart) => $cart.length || 0);
|
||||
|
||||
export const subTotal: Readable<number> = 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);
|
||||
}
|
||||
188
src/lib/components/ApplePayButton.svelte
Normal file
@@ -0,0 +1,188 @@
|
||||
<script lang="ts">
|
||||
function getApplePaySession(validationURL: string) {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ validationURL })
|
||||
};
|
||||
|
||||
return fetch('/api/applepay/validateSession', options).then((resp) => resp.json());
|
||||
}
|
||||
|
||||
function makeApplePayPaymentTransaction(payment: object) {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ payment })
|
||||
};
|
||||
|
||||
return fetch('/api/applepay/pay', options).then((resp) => resp.json());
|
||||
}
|
||||
|
||||
function createPaymentRequest() {
|
||||
const paymentRequest = {
|
||||
countryCode: 'NO',
|
||||
currencyCode: 'NOK',
|
||||
shippingMethods: [
|
||||
{
|
||||
label: 'Free Shipping',
|
||||
amount: 0.0,
|
||||
identifier: 'free',
|
||||
detail: 'Delivers in five business days'
|
||||
},
|
||||
{
|
||||
label: 'Express Shipping',
|
||||
amount: 5.0,
|
||||
identifier: 'express',
|
||||
detail: 'Delivers in two business days'
|
||||
}
|
||||
],
|
||||
lineItems: [
|
||||
{
|
||||
label: 'Shipping',
|
||||
amount: 0.0
|
||||
}
|
||||
],
|
||||
total: {
|
||||
label: 'Apple Pay Example',
|
||||
amount: 8.99
|
||||
},
|
||||
supportedNetworks: ['amex', 'discover', 'masterCard', 'visa'],
|
||||
merchantCapabilities: ['supports3DS'],
|
||||
requiredShippingContactFields: ['postalAddress', 'email']
|
||||
};
|
||||
|
||||
const session = new ApplePaySession(6, paymentRequest);
|
||||
|
||||
session.onvalidatemerchant = (event) => {
|
||||
console.log('Validate merchante');
|
||||
console.log('event: ', event);
|
||||
|
||||
const validationURL = event.validationURL;
|
||||
this.getApplePaySession(validationURL).then((response) => {
|
||||
console.log('response from getApplePaySession:', response);
|
||||
session.completeMerchantValidation(response);
|
||||
});
|
||||
};
|
||||
|
||||
session.onpaymentauthorized = (event) => {
|
||||
this.makeApplePayPaymentTransaction(event.payment).then((response) => {
|
||||
console.log('response from pay:', response);
|
||||
|
||||
if (response.approved) session.completePayment(ApplePaySession.STATUS_SUCCESS);
|
||||
else session.completePayment(ApplePaySession.STATUS_FAILURE);
|
||||
});
|
||||
};
|
||||
|
||||
session.begin();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="apple-pay-button-with-text apple-pay-button-black-with-text"
|
||||
on:click="{createPaymentRequest}"
|
||||
>
|
||||
<span class="text">Hurtigkasse med</span>
|
||||
<svg
|
||||
x="116.59"
|
||||
y="7.92"
|
||||
height="15"
|
||||
width="35"
|
||||
class="logo"
|
||||
viewBox="0 0 105 43"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<title>Apple Logo</title>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g fill="#FFF">
|
||||
<path
|
||||
d="M19.4028,5.5674 C20.6008,4.0684 21.4138,2.0564 21.1998,0.0004 C19.4458,0.0874 17.3058,1.1574 16.0668,2.6564 C14.9538,3.9414 13.9688,6.0374 14.2258,8.0074 C16.1948,8.1784 18.1618,7.0244 19.4028,5.5674"
|
||||
></path>
|
||||
<path
|
||||
d="M21.1772,8.3926 C18.3182,8.2226 15.8872,10.0156 14.5212,10.0156 C13.1552,10.0156 11.0642,8.4786 8.8022,8.5196 C5.8592,8.5626 3.1282,10.2276 1.6342,12.8746 C-1.4378,18.1696 0.8232,26.0246 3.8112,30.3376 C5.2622,32.4716 7.0102,34.8206 9.3142,34.7366 C11.4912,34.6506 12.3442,33.3266 14.9902,33.3266 C17.6352,33.3266 18.4042,34.7366 20.7082,34.6936 C23.0972,34.6506 24.5922,32.5586 26.0422,30.4226 C27.7072,27.9906 28.3882,25.6426 28.4312,25.5126 C28.3882,25.4706 23.8232,23.7186 23.7812,18.4676 C23.7382,14.0706 27.3652,11.9786 27.5362,11.8496 C25.4882,8.8196 22.2872,8.4786 21.1772,8.3926"
|
||||
></path>
|
||||
<path
|
||||
d="M85.5508,43.0381 L85.5508,39.1991 C85.8628,39.2421 86.6158,39.2871 87.0158,39.2871 C89.2138,39.2871 90.4558,38.3551 91.2108,35.9581 L91.6548,34.5371 L83.2428,11.2321 L88.4368,11.2321 L94.2958,30.1421 L94.4068,30.1421 L100.2668,11.2321 L105.3278,11.2321 L96.6048,35.7141 C94.6078,41.3291 92.3208,43.1721 87.4828,43.1721 C87.1048,43.1721 85.8838,43.1271 85.5508,43.0381"
|
||||
></path>
|
||||
<path
|
||||
d="M42.6499,19.3555 L48.3549,19.3555 C52.6829,19.3555 55.1469,17.0255 55.1469,12.9855 C55.1469,8.9455 52.6829,6.6375 48.3769,6.6375 L42.6499,6.6375 L42.6499,19.3555 Z M49.6869,2.4425 C55.9009,2.4425 60.2289,6.7265 60.2289,12.9625 C60.2289,19.2225 55.8129,23.5285 49.5309,23.5285 L42.6499,23.5285 L42.6499,34.4705 L37.6779,34.4705 L37.6779,2.4425 L49.6869,2.4425 Z"
|
||||
></path>
|
||||
<path
|
||||
d="M76.5547,25.7705 L76.5547,23.9715 L71.0287,24.3275 C67.9207,24.5275 66.3007,25.6815 66.3007,27.7015 C66.3007,29.6545 67.9887,30.9195 70.6287,30.9195 C74.0027,30.9195 76.5547,28.7665 76.5547,25.7705 M61.4617,27.8345 C61.4617,23.7285 64.5917,21.3755 70.3627,21.0205 L76.5547,20.6425 L76.5547,18.8675 C76.5547,16.2705 74.8457,14.8495 71.8057,14.8495 C69.2967,14.8495 67.4777,16.1375 67.0997,18.1125 L62.6167,18.1125 C62.7497,13.9615 66.6567,10.9435 71.9387,10.9435 C77.6207,10.9435 81.3267,13.9175 81.3267,18.5345 L81.3267,34.4705 L76.7327,34.4705 L76.7327,30.6305 L76.6217,30.6305 C75.3127,33.1395 72.4267,34.7145 69.2967,34.7145 C64.6807,34.7145 61.4617,31.9625 61.4617,27.8345"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style lang="scss" module="scoped">
|
||||
@supports not (-webkit-appearance: -apple-pay-button) {
|
||||
.apple-pay-button-with-text {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.apple-pay-button-with-text {
|
||||
cursor: pointer;
|
||||
--apple-pay-scale: 1; /* (height / 32) */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
padding: 0.75rem 1rem;
|
||||
width: fit-content;
|
||||
box-sizing: border-box;
|
||||
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.apple-pay-button-with-text,
|
||||
svg g {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.apple-pay-button-black-with-text {
|
||||
background-color: black;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
border: 2px solid black;
|
||||
|
||||
&:hover {
|
||||
background-color: white;
|
||||
color: black;
|
||||
|
||||
g {
|
||||
fill: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.apple-pay-button-with-text > .text {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
||||
'Open Sans', 'Helvetica Neue', sans-serif, Helvetica, sans-serif;
|
||||
font-size: calc(1em * var(--apple-pay-scale));
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
align-self: center;
|
||||
margin-right: calc(2px * var(--apple-pay-scale));
|
||||
}
|
||||
|
||||
.apple-pay-button-with-text > .logo {
|
||||
width: 50px;
|
||||
/*width: calc(35px * var(--scale));*/
|
||||
height: 100%;
|
||||
background-size: 100% 60%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 50%;
|
||||
margin-top: calc(2px * var(--apple-pay-scale));
|
||||
margin-left: calc(2px * var(--apple-pay-scale));
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
60
src/lib/components/Badge.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import BadgeType from '../interfaces/BadgeType';
|
||||
|
||||
const badgeIcons = {
|
||||
[BadgeType.INFO]: '⏳',
|
||||
[BadgeType.PENDING]: '📦',
|
||||
[BadgeType.WARNING]: '⚠️',
|
||||
[BadgeType.SUCCESS]: '✓',
|
||||
[BadgeType.ERROR]: 'X'
|
||||
};
|
||||
|
||||
export let title = 'Info';
|
||||
export let type: BadgeType = BadgeType.INFO;
|
||||
export let icon: string = badgeIcons[type];
|
||||
|
||||
$: badgeClass = `badge ${type}`;
|
||||
</script>
|
||||
|
||||
<div class="{badgeClass}">
|
||||
<span>{title}</span>
|
||||
<span class="icon">{icon}</span>
|
||||
</div>
|
||||
|
||||
<style lang="scss" module="scoped">
|
||||
.badge {
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
width: fit-content;
|
||||
font-size: 0.95rem;
|
||||
background-color: rgb(235, 238, 241);
|
||||
color: rgb(84, 89, 105);
|
||||
margin: 0.2rem;
|
||||
|
||||
.icon {
|
||||
margin-left: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: rgb(215, 247, 194);
|
||||
color: rgb(0, 105, 8);
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: rgb(255, 231, 242);
|
||||
color: rgb(179, 9, 60);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: rgb(252, 237, 185);
|
||||
color: rgb(168, 44, 0);
|
||||
}
|
||||
|
||||
&.pending {
|
||||
background-color: lightblue;
|
||||
color: darkslateblue;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
42
src/lib/components/Button.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
export let text = 'Bestill';
|
||||
export let type: string | null = null;
|
||||
export let active = false;
|
||||
</script>
|
||||
|
||||
<button on:click type="{type}" class="{active ? 'active' : null}">{text}</button>
|
||||
|
||||
<style lang="scss" module="scoped">
|
||||
button {
|
||||
webkit-font-smoothing: antialiased;
|
||||
align-items: flex-start;
|
||||
display: block;
|
||||
appearance: auto;
|
||||
background-clip: border-box;
|
||||
background-color: var(--color-theme-1);
|
||||
border: 2px solid var(--color-theme-1);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
height: 48px;
|
||||
letter-spacing: 1.2px;
|
||||
line-height: 14px;
|
||||
margin: 0;
|
||||
|
||||
min-width: 140px;
|
||||
padding: 10px 30px;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
padding: 12px 32px;
|
||||
transform: translateX(-2px);
|
||||
background-color: white;
|
||||
color: var(--color-theme-1);
|
||||
border-color: var(--color-theme-1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
55
src/lib/components/Cart.svelte
Normal file
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import { count, toggleCart, isOpen } from '../cartStore';
|
||||
import IconCart from '../icons/IconCart.svelte';
|
||||
</script>
|
||||
|
||||
<div id="cart" class="{$isOpen && 'open'}" on:click="{() => toggleCart()}">
|
||||
{#if $count > 0}
|
||||
<span>{$count}</span>
|
||||
{/if}
|
||||
|
||||
<IconCart />
|
||||
</div>
|
||||
|
||||
<style lang="scss" module="scoped">
|
||||
#cart {
|
||||
padding: 0.3rem 0.6rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
// background-color: #E6E0DC;
|
||||
color: #18332f;
|
||||
// border: 1px solid rgba(0,0,0,0.1);
|
||||
z-index: 99;
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
cursor: pointer;
|
||||
background-color: #18332f;
|
||||
color: #e6e0dc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
margin-right: 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
|
||||
span {
|
||||
margin-right: 0.75rem;
|
||||
font-size: 1rem;
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(#cart svg) {
|
||||
max-width: 20px;
|
||||
width: 20px;
|
||||
fill: #18332f;
|
||||
transition: fill 0.25s ease;
|
||||
}
|
||||
:global(#cart:hover svg, #cart.open svg) {
|
||||
fill: #e6e0dc;
|
||||
}
|
||||
</style>
|
||||
64
src/lib/components/FrontText.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import type IFrontText from '$lib/interfaces/IFrontText';
|
||||
export let data: IFrontText;
|
||||
</script>
|
||||
|
||||
<div class="text-container">
|
||||
<blockquote style="{`color: ${data.color || 'black'}`}">
|
||||
<p class="title">{data.title}</p>
|
||||
<p class="text">{data.text}</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<style lang="scss" module="scoped">
|
||||
@import '../../styles/media-queries.scss';
|
||||
|
||||
.text-container {
|
||||
position: relative;
|
||||
|
||||
blockquote {
|
||||
max-width: 75%;
|
||||
margin: 12.5%;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
@include mobile {
|
||||
margin: 10% 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
@include desktop {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
text-align: center;
|
||||
font-size: 2rem;
|
||||
margin-top: 0;
|
||||
|
||||
@include desktop {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '\201C';
|
||||
position: relative;
|
||||
left: 0;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '\201D';
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
78
src/lib/components/FrontTextImage.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import type IFrontTextImage from '$lib/interfaces/IFrontTextImage';
|
||||
export let data: IFrontTextImage;
|
||||
</script>
|
||||
|
||||
<div class="parallax-container">
|
||||
<div class="image-layout" style="{data?.imageRight && 'order: 1'}">
|
||||
<img src="{data?.image}" alt="mushroom" />
|
||||
</div>
|
||||
|
||||
<div class="text-container">
|
||||
<h2>{data?.title}</h2>
|
||||
|
||||
<span>{data?.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../styles/media-queries.scss';
|
||||
|
||||
.parallax-container {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
// height: 75vh;
|
||||
min-height: 800px;
|
||||
|
||||
place-items: center;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@include desktop {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.image-layout {
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.text-container {
|
||||
-webkit-box-flex: 0;
|
||||
box-sizing: border-box;
|
||||
color: rgb(0, 0, 0);
|
||||
display: block;
|
||||
flex-basis: 75%;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
font-size: 23px;
|
||||
font-weight: 400;
|
||||
line-height: 31.999903px;
|
||||
padding-left: 1rem;
|
||||
|
||||
h2 {
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
margin-left: 99.3125px;
|
||||
width: 595.875px;
|
||||
max-width: 75%;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
98
src/lib/components/FrontTextImageBubble.svelte
Normal file
@@ -0,0 +1,98 @@
|
||||
<section>
|
||||
<div class="text-part">
|
||||
<h2>Å stoppe tapet av biologisk mangfold globalt</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="image-part">
|
||||
<div class="image-mask">
|
||||
<img src="https://i.imgur.com/WWbfhiZ.jpg" alt="bee inspection" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="scss" module="scoped">
|
||||
@import '../../styles/media-queries.scss';
|
||||
|
||||
section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 40% 2rem;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@include tablet {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 10%;
|
||||
margin: 20% 2rem;
|
||||
|
||||
.text-part {
|
||||
max-width: 480px;
|
||||
justify-self: end;
|
||||
|
||||
h2 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.image-part {
|
||||
width: 100%;
|
||||
justify-self: start !important;
|
||||
max-width: calc(100vw / 3) !important;
|
||||
max-height: calc(100vw / 3) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-part {
|
||||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
|
||||
h2 {
|
||||
font-size: 2.25rem;
|
||||
margin-bottom: 2.25rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.image-part {
|
||||
position: relative;
|
||||
height: 80vw;
|
||||
max-width: 80vw;
|
||||
width: 100%;
|
||||
justify-self: center;
|
||||
|
||||
.image-mask {
|
||||
background-image: url(data:image/gif;base64,R0lGODlhCAAFAPUAAHmAR4OKUIOJUo2SW46VWY+TYZeeYZecZJicaZyecZ6kZ5+ka6GobKGkcaOpcKKocqSpcaeqdqitdaSmeKanfqmueauueqqsfamufK2xfq2uhK2wgK+ygq6zgq+zha+0ha+1h7CzhbGyhrKzhrC0hLG1h7G3irK3igAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAAIf8LSW1hZ2VNYWdpY2sOZ2FtbWE9MC45MDkwOTEALAAAAAAIAAUAAAYlQBPIExqJNJRTicTZXCaJTydjiTQQBUxFAlkcBoKHg6EwEAKAIAA7);
|
||||
|
||||
background-size: cover;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: cover;
|
||||
mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjgwIiBoZWlnaHQ9IjY4MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNjQzLjMzIDE3Ni43NzRjNDkuMjQyIDE0MC4zMiA2NC41MzQgMzM3LjI1NC01NS4yMTYgNDE4LjI4LTExOS43NDkgODEuMDI3LTM2MC4zNTMgMTAyLjk4NC00OTguNjE4LTQuODg4cy05NS4wODgtNDAwLjkyNS0xMi44Mi00ODMuOWM4Mi4yNjgtODIuOTc1IDI0NS4wOTQtNzUuNTQ0IDMyMy4zNS04Ni45OUM0NzguMjgzIDcuODMgNTk0LjA5IDM2LjQ1NCA2NDMuMzMgMTc2Ljc3NHoiLz48L3N2Zz4=');
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
61
src/lib/components/Input.svelte
Normal file
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
export let id: string | null = null;
|
||||
export let label: string;
|
||||
export let value: string | number;
|
||||
export let type = 'text';
|
||||
export let name: string | null = null;
|
||||
export let required = true;
|
||||
export let autocomplete: string | null = null;
|
||||
export let autocapitalize: string | null = null;
|
||||
|
||||
function typeAction(node: HTMLInputElement) {
|
||||
node.type = type;
|
||||
}
|
||||
</script>
|
||||
|
||||
<label id="{id}" class="{required ? 'required' : null}">
|
||||
<span>{label}</span>
|
||||
<input
|
||||
bind:value="{value}"
|
||||
name="{name || id}"
|
||||
use:typeAction
|
||||
required="{required}"
|
||||
autocomplete="{autocomplete}"
|
||||
autocapitalize="{autocapitalize}"
|
||||
enterkeyhint="next"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<style lang="scss" module="scoped">
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
margin-left: 2px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
&.required span::after {
|
||||
content: '*';
|
||||
color: #e02b27;
|
||||
font-size: 1rem;
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
border: 2px solid grey;
|
||||
border-radius: 0px;
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:valid {
|
||||
border-color: var(--text-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
15
src/lib/components/LinkArrow.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
class="inline-block relative -top-1 w-10 h-10 mr-8 -stroke-green-boulogne"
|
||||
width="8"
|
||||
height="8"
|
||||
viewBox="0 0 8 8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g fill="none" fill-rule="evenodd" stroke-linejoin="round">
|
||||
<g stroke="#8BA17F">
|
||||
<path
|
||||
d="M4.534.93a.189.189 0 000 .267l2.614 2.614H.397a.189.189 0 100 .378h6.75l-2.88 2.88a.189.189 0 10.267.268l3.203-3.204a.189.189 0 000-.266L4.801.93a.189.189 0 00-.267 0"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 482 B |
69
src/lib/components/ProductVariationSelect.svelte
Normal file
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import type IProductVariation from '$lib/interfaces/IProductVariation';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let variations: IProductVariation[];
|
||||
let selectedVariation: IProductVariation;
|
||||
|
||||
function defaultVariationIndex(variations: IProductVariation[]) {
|
||||
return variations.findIndex((variation) => variation?.default_price || false);
|
||||
}
|
||||
|
||||
function emitSelectedVariation(_selectedVariation: IProductVariation) {
|
||||
selectedVariation = _selectedVariation;
|
||||
dispatch('selected', _selectedVariation);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!variations) return;
|
||||
|
||||
const defaultVariation = variations[defaultVariationIndex(variations) || 0];
|
||||
emitSelectedVariation(defaultVariation);
|
||||
});
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#each variations as variation}
|
||||
<li
|
||||
class="{`variation ${variation.sku_id === selectedVariation?.sku_id && 'selected'}`}"
|
||||
on:click="{() => emitSelectedVariation(variation)}"
|
||||
>
|
||||
<span>Størrelse: {variation.size}</span>
|
||||
<span>NOK {variation.price}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style lang="scss">
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
|
||||
max-width: 400px;
|
||||
|
||||
li.variation {
|
||||
padding: 1rem 1rem;
|
||||
margin: 0.5rem 0;
|
||||
border: 2px solid rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border: 2px solid rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: black;
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
137
src/lib/components/QuantitySelect.svelte
Normal file
@@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function increment() {
|
||||
if (value >= Number(input?.max)) return;
|
||||
value = value + 1;
|
||||
dispatch('increment');
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
if (value <= 1) return;
|
||||
|
||||
value = value - 1;
|
||||
dispatch('decrement');
|
||||
}
|
||||
|
||||
export let value: number;
|
||||
export let disabled = false;
|
||||
export let hideButtons = false;
|
||||
export let small = false;
|
||||
let max = 50;
|
||||
let input: HTMLInputElement;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="{`quantity-picker ${hideButtons ? 'hide-buttons' : null} ${small ? 'small' : null} ${
|
||||
disabled ? 'disabled' : null
|
||||
}`}"
|
||||
>
|
||||
<span class="button" on:click="{decrement}">-</span>
|
||||
<input
|
||||
bind:this="{input}"
|
||||
type="number"
|
||||
bind:value="{value}"
|
||||
on:input
|
||||
disabled="{disabled}"
|
||||
min="1"
|
||||
max="{max}"
|
||||
/>
|
||||
<span class="button" on:click="{increment}">+</span>
|
||||
</div>
|
||||
|
||||
<style lang="scss" module="scoped">
|
||||
@import '../../styles/media-queries.scss';
|
||||
|
||||
.quantity-picker {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
height: 2.5rem;
|
||||
border: 2px solid grey;
|
||||
font-size: 1.5rem;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
height: 2.5rem;
|
||||
font-size: 1.2rem;
|
||||
line-height: 0;
|
||||
|
||||
.button {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.hide-buttons {
|
||||
@include mobile {
|
||||
.button {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
&.small {
|
||||
height: 1.75rem;
|
||||
|
||||
.button {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border: unset;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
align-items: center;
|
||||
touch-action: manipulation;
|
||||
|
||||
&:hover {
|
||||
border: 2px solid #000;
|
||||
margin-left: -2px;
|
||||
margin-right: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
max-width: 1.5rem;
|
||||
text-align: center;
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
background-color: inherit;
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
&[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
114
src/lib/components/StripeCard.svelte
Normal file
@@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { loadStripe } from '@stripe/stripe-js/pure';
|
||||
import type { Stripe } from '@stripe/stripe-js';
|
||||
import { cart } from '../cartStore';
|
||||
|
||||
function mountCard() {
|
||||
const elements = stripe.elements();
|
||||
|
||||
const options = {
|
||||
hidePostalCode: true,
|
||||
style: {
|
||||
base: {
|
||||
color: '#32325d',
|
||||
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
|
||||
fontSmoothing: 'antialiased',
|
||||
fontSize: '18px',
|
||||
'::placeholder': {
|
||||
color: '#aab7c4'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
card = elements.create('card', options);
|
||||
|
||||
card.mount(cardElement);
|
||||
}
|
||||
|
||||
// function makeIntent() {
|
||||
// let url = "/api/payment/stripe";
|
||||
// if (window.location.href.includes("localhost"))
|
||||
// url = "http://localhost:30010".concat(url);
|
||||
|
||||
// fetch(url, {
|
||||
// method: "POST",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// cart: $cart
|
||||
// })
|
||||
// })
|
||||
// .then((resp) => resp.json())
|
||||
// .then((data) => (clientSecret = data.paymentIntent.clientSecret));
|
||||
// }
|
||||
|
||||
function pay() {
|
||||
stripe
|
||||
.confirmCardPayment(clientSecret, {
|
||||
payment_method: {
|
||||
card,
|
||||
billing_details: {
|
||||
name: 'Kevin Testost'
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.error) {
|
||||
confirmDiag.innerText = result.error.message || 'Unexpected payment ERROR!';
|
||||
} else {
|
||||
if (result.paymentIntent.status === 'succeeded') {
|
||||
confirmDiag.innerText = 'Confirmed transaction!';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function initStripe() {
|
||||
window.addEventListener('submit-stripe-payment', pay, false);
|
||||
|
||||
loadStripe.setLoadParameters({ advancedFraudSignals: false });
|
||||
stripe = await loadStripe('pk_test_YiU5HewgBoClZCwHdhXhTxUn');
|
||||
mountCard();
|
||||
// makeIntent();
|
||||
}
|
||||
|
||||
onMount(() => initStripe());
|
||||
// onDestroy(() => window.removeEventListener('submit-stripe-payment', null))
|
||||
|
||||
let stripe: Stripe;
|
||||
let card;
|
||||
let cardElement: HTMLElement;
|
||||
let clientSecret: string;
|
||||
let confirmDiag: HTMLElement;
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<div bind:this="{cardElement}"></div>
|
||||
</div>
|
||||
|
||||
<div class="stripe-feedback" bind:this="{confirmDiag}"></div>
|
||||
|
||||
<style lang="scss" module="scoped">
|
||||
@import '../../styles/media-queries.scss';
|
||||
|
||||
.card {
|
||||
// padding: 1rem;
|
||||
margin: 0 0.5rem;
|
||||
border: 2px solid black;
|
||||
|
||||
@include desktop {
|
||||
max-width: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
.stripe-feedback {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
:global(.card .StripeElement) {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
48
src/lib/components/VippsHurtigkasse.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div aria-role="button" on:click>
|
||||
<svg fill="none" height="44" viewBox="0 0 250 44" width="250" xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="m0 8c0-2.80026 0-4.20039.544967-5.26995.479363-.94081 1.244273-1.70572 2.185083-2.185083 1.06956-.544967 2.46969-.544967 5.26995-.544967h234c2.8 0 4.2 0 5.27.544967.941.479363 1.706 1.244273 2.185 2.185083.545 1.06956.545 2.46969.545 5.26995v28c0 2.8003 0 4.2004-.545 5.27-.479.9408-1.244 1.7057-2.185 2.185-1.07.545-2.47.545-5.27.545h-234c-2.80026 0-4.20039 0-5.26995-.545-.94081-.4793-1.70572-1.2442-2.185083-2.185-.544967-1.0696-.544967-2.4697-.544967-5.27z"
|
||||
fill="#ff5b24"></path><g fill="#fff"
|
||||
><path
|
||||
clip-rule="evenodd"
|
||||
d="m100.25 20.0884c-.7202-2.7477-2.4686-3.8384-4.8553-3.8384-1.9336 0-4.3605 1.0907-4.3605 3.7172 0 1.6968 1.1723 3.0304 3.0852 3.374l1.8103.323c1.2344.2221 1.5842.6869 1.5842 1.3132 0 .7071-.7612 1.111-1.8926 1.111-1.4809 0-2.4067-.5252-2.5508-2.0001l-2.6124.4042c.4112 2.8483 2.9619 4.0204 5.2658 4.0204 2.1808 0 4.5051-1.2528 4.5051-3.778 0-1.7174-1.0492-2.9696-3.0034-3.3337l-1.9951-.3633c-1.1111-.202-1.4812-.7475-1.4812-1.2727 0-.6668.7198-1.0907 1.7073-1.0907 1.255 0 2.1395.4239 2.1806 1.8179zm-58.075 4.3229 2.7149-7.8584h3.1884l-4.7314 11.6565h-2.3654l-4.7315-11.6563h3.1884zm16.6834-4.5249c0 .9292-.7406 1.5756-1.6047 1.5756-.864 0-1.6043-.6464-1.6043-1.5756 0-.9294.7403-1.5756 1.6043-1.5756.8641 0 1.6049.6462 1.6049 1.5756zm.4936 4.1213c-1.0699 1.3734-2.2013 2.3229-4.1967 2.323-2.036 0-3.6204-1.2121-4.8546-2.9897-.4939-.7275-1.255-.889-1.8105-.5051-.5142.3637-.6374 1.1314-.1645 1.7982 1.7073 2.5656 4.0729 4.0602 6.8294 4.0602 2.5305 0 4.5055-1.2119 6.0481-3.2322.5759-.7473.5553-1.515 0-1.9393-.5144-.4044-1.2755-.2624-1.8512.4849zm7.098-1.6568c0 2.384 1.3989 3.6367 2.9625 3.6367 1.4809 0 3.0034-1.1719 3.0034-3.6367 0-2.4244-1.5225-3.5959-2.9831-3.5959-1.5839 0-2.9828 1.1111-2.9828 3.5959zm0-4.1815v-1.5964h-2.9004v15.677h2.9004v-5.576c.9669 1.2932 2.2217 1.8389 3.6409 1.8389 2.6541 0 5.2459-2.0608 5.2459-6.3031 0-4.061-2.6948-5.9596-4.9989-5.9596-1.8309 0-3.0855.8281-3.8879 1.9192zm13.9275 4.1815c0 2.384 1.3987 3.6367 2.9623 3.6367 1.4809 0 3.0032-1.1719 3.0032-3.6367 0-2.4244-1.5223-3.5959-2.9828-3.5959-1.584 0-2.9829 1.1111-2.9829 3.5959zm0-4.1815v-1.5964h-.0002-2.9004v15.677h2.9004v-5.576c.967 1.2932 2.2217 1.8389 3.641 1.8389 2.6539 0 5.2459-2.0608 5.2459-6.3031 0-4.061-2.6948-5.9596-4.999-5.9596-1.8309 0-3.0854.8281-3.8877 1.9192z"
|
||||
fill-rule="evenodd"></path><path
|
||||
d="m118.384 28.25v-13.0349h-2.321v5.4199h-6.342v-5.4199h-2.33v13.0349h2.33v-5.6548h6.342v5.6548zm11.39-9.8733h-2.24v5.7542c0 1.4904-.795 2.3938-2.25 2.3938-1.337 0-1.96-.7408-1.96-2.2764v-5.8716h-2.24v6.4136c0 2.3125 1.265 3.6584 3.424 3.6584 1.508 0 2.475-.6413 2.953-1.7705h.154v1.5718h2.159zm2.546 9.8733h2.24v-5.7361c0-1.3911 1.012-2.2944 2.466-2.2944.38 0 .976.0632 1.147.1264v-2.0415c-.207-.0542-.605-.0903-.93-.0903-1.274 0-2.34.7227-2.611 1.6983h-.153v-1.5357h-2.159zm8.662-12.2942v2.4841h-1.563v1.7434h1.563v5.4471c0 1.906.903 2.6648 3.17 2.6648.434 0 .849-.0362 1.175-.0994v-1.7163c-.271.0271-.443.0452-.759.0452-.94 0-1.355-.4427-1.355-1.4273v-4.9141h2.114v-1.7434h-2.114v-2.4841zm7.523.7678c.741 0 1.346-.5871 1.346-1.3278 0-.7317-.605-1.3279-1.346-1.3279-.732 0-1.337.5962-1.337 1.3279 0 .7407.605 1.3278 1.337 1.3278zm-1.111 11.5264h2.231v-9.8733h-2.231zm9.059 3.7578c2.881 0 4.706-1.4363 4.706-3.7036v-9.9275h-2.159v1.5808h-.153c-.542-1.0749-1.717-1.7434-3.063-1.7434-2.52 0-4.083 1.9692-4.083 4.9954 0 2.9629 1.554 4.896 4.047 4.896 1.373 0 2.448-.5782 3.017-1.6441h.154v1.8789c0 1.2556-.894 1.9964-2.43 1.9964-1.229 0-2.023-.4427-2.168-1.1382h-2.249c.18 1.6982 1.843 2.8093 4.381 2.8093zm0-5.6819c-1.563 0-2.448-1.2104-2.448-3.1255 0-1.906.885-3.1164 2.448-3.1164 1.554 0 2.502 1.2104 2.502 3.1255 0 1.906-.939 3.1164-2.502 3.1164zm9.745-3.9023h-.153v-7.8679h-2.24v13.6943h2.24v-3.4688l.795-.7949 3.17 4.2637h2.71l-4.209-5.6367 3.938-4.2366h-2.601zm10.442 5.989c1.291 0 2.375-.5601 2.926-1.5176h.154v1.355h2.159v-6.7478c0-2.0867-1.427-3.3152-3.966-3.3152-2.348 0-3.983 1.1021-4.164 2.8274h2.114c.207-.6775.912-1.0478 1.942-1.0478 1.21 0 1.861.551 1.861 1.5356v.8491l-2.548.1536c-2.403.1355-3.748 1.1743-3.748 2.9448 0 1.8066 1.364 2.9629 3.27 2.9629zm.659-1.7253c-.994 0-1.707-.4969-1.707-1.346 0-.822.587-1.2827 1.842-1.364l2.232-.1536v.804c0 1.1743-1.012 2.0596-2.367 2.0596zm6.855-5.4922c0 1.4814.894 2.3486 2.791 2.7822l1.753.4065c.939.2168 1.364.5781 1.364 1.1382 0 .7497-.786 1.2646-1.915 1.2646-1.121 0-1.816-.4516-2.033-1.1833h-2.213c.172 1.7795 1.725 2.8454 4.191 2.8454s4.21-1.2556 4.21-3.1435c0-1.4544-.876-2.2583-2.764-2.6919l-1.744-.3884c-.993-.2349-1.463-.5872-1.463-1.1473 0-.7317.768-1.2285 1.788-1.2285 1.048 0 1.708.4517 1.87 1.1472h2.105c-.163-1.7795-1.635-2.8093-3.984-2.8093-2.33 0-3.956 1.2376-3.956 3.0081zm9.809 0c0 1.4814.894 2.3486 2.791 2.7822l1.752.4065c.94.2168 1.364.5781 1.364 1.1382 0 .7497-.785 1.2646-1.915 1.2646-1.12 0-1.815-.4516-2.032-1.1833h-2.213c.171 1.7795 1.725 2.8454 4.191 2.8454s4.21-1.2556 4.21-3.1435c0-1.4544-.877-2.2583-2.765-2.6919l-1.743-.3884c-.994-.2349-1.463-.5872-1.463-1.1473 0-.7317.768-1.2285 1.788-1.2285 1.048 0 1.708.4517 1.87 1.1472h2.105c-.163-1.7795-1.635-2.8093-3.984-2.8093-2.33 0-3.956 1.2376-3.956 3.0081zm16.656 4.354c-.326.7407-1.093 1.1562-2.204 1.1562-1.473 0-2.421-1.0478-2.476-2.719v-.1174h6.902v-.7136c0-3.0984-1.689-4.9683-4.508-4.9683-2.863 0-4.643 1.9963-4.643 5.167 0 3.1616 1.753 5.0947 4.661 5.0947 2.331 0 3.984-1.1201 4.39-2.8996zm-2.295-5.6187c1.346 0 2.231.9485 2.277 2.448h-4.653c.1-1.4814 1.039-2.448 2.376-2.448z"
|
||||
></path></g
|
||||
></svg
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
div {
|
||||
display: inline-block;
|
||||
width: max-content;
|
||||
height: 44px;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
border: 2px solid #ff5b24;
|
||||
background-color: #ff5b24;
|
||||
|
||||
path {
|
||||
transition: all 0.3s ease;
|
||||
fill: #ff5b24;
|
||||
}
|
||||
|
||||
g path {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
path {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
g,
|
||||
g path {
|
||||
fill: #ff5b24;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/lib/icons/CircleCheckmark.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<svg width="49" height="48" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
||||
<g transform="translate(24,24)">
|
||||
<g class="CheckSuccess-checkGroup" opacity="0" style="animation-delay: 700ms">
|
||||
<path
|
||||
class="CheckSuccess-check"
|
||||
fill="none"
|
||||
d="M-10 1.5c0 0 6.5 6 6.5 6c0 0 13.5-13 13.5-13"
|
||||
stroke="#24B47E"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-dasharray="28 28"
|
||||
stroke-dashoffset="28"
|
||||
style="animation-delay: 700ms"></path>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
class="CheckSuccess-circle"
|
||||
fill="none"
|
||||
stroke="#24B47E"
|
||||
stroke-width="2"
|
||||
d="M23 0c0 12.7-10.3 23-23 23c-12.7 0-23-10.3-23-23c0-12.7 10.3-23 23-23c12.7 0 23 10.3 23 23"
|
||||
stroke-linecap="round"
|
||||
stroke-dashoffset="145"
|
||||
stroke-dasharray="145 145"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="1"
|
||||
transform="translate(24,24) rotate(-35)"
|
||||
style="animation-delay: 700ms"></path>
|
||||
</svg>
|
||||
|
||||
<style lang="scss" module="scoped">
|
||||
@import './circle-feedback.scss';
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
34
src/lib/icons/CircleError.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<svg width="49" height="48" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
||||
<g transform="translate(24,24)">
|
||||
<g class="CheckError-checkGroup" opacity="0" style="animation-delay: 700ms">
|
||||
<path
|
||||
class="CheckError-cross"
|
||||
fill="none"
|
||||
d="M -10 -10 l 20 20 M -10 10 l 20 -20"
|
||||
stroke="#FF6245"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-dasharray="28 28"
|
||||
stroke-dashoffset="28"
|
||||
style="animation-delay: 700ms"></path>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
class="CheckError-circle"
|
||||
fill="none"
|
||||
stroke="#FF6245"
|
||||
stroke-width="2"
|
||||
d="M23 0c0 12.7-10.3 23-23 23c-12.7 0-23-10.3-23-23c0-12.7 10.3-23 23-23c12.7 0 23 10.3 23 23"
|
||||
stroke-linecap="round"
|
||||
stroke-dashoffset="145"
|
||||
stroke-dasharray="145 145"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="1"
|
||||
transform="translate(24,24) rotate(-35)"
|
||||
style="animation-delay: 700ms"></path>
|
||||
</svg>
|
||||
|
||||
<style lang="scss" module="scoped">
|
||||
@import './circle-feedback.scss';
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
34
src/lib/icons/CircleWarning.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<svg width="49" height="48" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
||||
<g transform="translate(24,24)">
|
||||
<g class="CheckWarning-checkGroup" opacity="0" style="animation-delay: 700ms">
|
||||
<path
|
||||
class="CheckWarning-exclamation"
|
||||
fill="none"
|
||||
d="M 0 -12 v 15 m 0 6 v 3"
|
||||
stroke="#FFC107"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-dasharray="28 28"
|
||||
stroke-dashoffset="28"
|
||||
style="animation-delay: 700ms"></path>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
class="CheckWarning-circle"
|
||||
fill="none"
|
||||
stroke="#FFC107"
|
||||
stroke-width="2"
|
||||
d="M23 0c0 12.7-10.3 23-23 23c-12.7 0-23-10.3-23-23c0-12.7 10.3-23 23-23c12.7 0 23 10.3 23 23"
|
||||
stroke-linecap="round"
|
||||
stroke-dashoffset="145"
|
||||
stroke-dasharray="145 145"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="1"
|
||||
transform="translate(24,24) rotate(-35)"
|
||||
style="animation-delay: 700ms"></path>
|
||||
</svg>
|
||||
|
||||
<style lang="scss" module="scoped">
|
||||
@import './circle-feedback.scss';
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
14
src/lib/icons/IconCart.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<title></title>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path
|
||||
fill="inherit"
|
||||
d="M352 672c0-17.664-7.2-33.696-18.752-45.248s-27.584-18.752-45.248-18.752-33.696 7.2-45.248 18.752-18.752 27.584-18.752 45.248 7.2 33.696 18.752 45.248 27.584 18.752 45.248 18.752 33.696-7.2 45.248-18.752 18.752-27.584 18.752-45.248zM704 672c0-17.664-7.2-33.696-18.752-45.248s-27.584-18.752-45.248-18.752-33.696 7.2-45.248 18.752-18.752 27.584-18.752 45.248 7.2 33.696 18.752 45.248 27.584 18.752 45.248 18.752 33.696-7.2 45.248-18.752 18.752-27.584 18.752-45.248zM231.072 224h466.24l-43.872 230.112c-1.472 7.296-5.312 13.6-10.624 18.176-5.76 4.992-13.216 7.872-22.016 7.712h-311.488c-7.424 0.096-14.432-2.272-20.032-6.496-6.080-4.576-10.528-11.232-12.128-19.296zM32 64h101.76l27.136 135.648c3.456 13.984 16.064 24.352 31.104 24.352h39.072l-12.8-64h-26.272c-17.664 0-32 14.336-32 32 0 1.696 0.128 3.36 0.384 4.96 0.128 0.896 0.32 1.824 0.544 2.688l53.472 267.104c4.768 24.032 18.24 44.256 36.48 57.952 16.672 12.544 37.44 19.616 59.328 19.296h310.592c23.936 0.48 46.56-8.352 63.84-23.264 15.808-13.632 27.136-32.416 31.52-53.856l51.264-268.864c3.296-17.376-8.064-34.112-25.44-37.44-2.080-0.416-4.16-0.608-5.984-0.576h-517.76l-26.88-134.272c-3.008-14.784-15.904-25.728-31.36-25.728h-128c-17.664 0-32 14.336-32 32s14.336 32 32 32z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
15
src/lib/icons/IconTrashcan.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
on:click
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<title></title>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path
|
||||
fill="inherit"
|
||||
d="M576 224v416c0 8.832-3.552 16.8-9.376 22.624s-13.792 9.376-22.624 9.376h-320c-8.832 0-16.8-3.552-22.624-9.376s-9.376-13.792-9.376-22.624v-416zM544 160v-32c0-26.496-10.784-50.56-28.128-67.872s-41.376-28.128-67.872-28.128h-128c-26.496 0-50.56 10.784-67.872 28.128s-28.128 41.376-28.128 67.872v32h-128c-17.664 0-32 14.336-32 32s14.336 32 32 32h32v416c0 26.496 10.784 50.56 28.128 67.872s41.376 28.128 67.872 28.128h320c26.496 0 50.56-10.784 67.872-28.128s28.128-41.376 28.128-67.872v-416h32c17.664 0 32-14.336 32-32s-14.336-32-32-32zM288 160v-32c0-8.832 3.552-16.8 9.376-22.624s13.792-9.376 22.624-9.376h128c8.832 0 16.8 3.552 22.624 9.376s9.376 13.792 9.376 22.624v32z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 895 B |
184
src/lib/icons/circle-feedback.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
1
src/lib/images/svelte-logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.1566,22.8189c-10.4-14.8851-30.94-19.2971-45.7914-9.8348L22.2825,29.6078A29.9234,29.9234,0,0,0,8.7639,49.6506a31.5136,31.5136,0,0,0,3.1076,20.2318A30.0061,30.0061,0,0,0,7.3953,81.0653a31.8886,31.8886,0,0,0,5.4473,24.1157c10.4022,14.8865,30.9423,19.2966,45.7914,9.8348L84.7167,98.3921A29.9177,29.9177,0,0,0,98.2353,78.3493,31.5263,31.5263,0,0,0,95.13,58.117a30,30,0,0,0,4.4743-11.1824,31.88,31.88,0,0,0-5.4473-24.1157" style="fill:#ff3e00"/><path d="M45.8171,106.5815A20.7182,20.7182,0,0,1,23.58,98.3389a19.1739,19.1739,0,0,1-3.2766-14.5025,18.1886,18.1886,0,0,1,.6233-2.4357l.4912-1.4978,1.3363.9815a33.6443,33.6443,0,0,0,10.203,5.0978l.9694.2941-.0893.9675a5.8474,5.8474,0,0,0,1.052,3.8781,6.2389,6.2389,0,0,0,6.6952,2.485,5.7449,5.7449,0,0,0,1.6021-.7041L69.27,76.281a5.4306,5.4306,0,0,0,2.4506-3.631,5.7948,5.7948,0,0,0-.9875-4.3712,6.2436,6.2436,0,0,0-6.6978-2.4864,5.7427,5.7427,0,0,0-1.6.7036l-9.9532,6.3449a19.0329,19.0329,0,0,1-5.2965,2.3259,20.7181,20.7181,0,0,1-22.2368-8.2427,19.1725,19.1725,0,0,1-3.2766-14.5024,17.9885,17.9885,0,0,1,8.13-12.0513L55.8833,23.7472a19.0038,19.0038,0,0,1,5.3-2.3287A20.7182,20.7182,0,0,1,83.42,29.6611a19.1739,19.1739,0,0,1,3.2766,14.5025,18.4,18.4,0,0,1-.6233,2.4357l-.4912,1.4978-1.3356-.98a33.6175,33.6175,0,0,0-10.2037-5.1l-.9694-.2942.0893-.9675a5.8588,5.8588,0,0,0-1.052-3.878,6.2389,6.2389,0,0,0-6.6952-2.485,5.7449,5.7449,0,0,0-1.6021.7041L37.73,51.719a5.4218,5.4218,0,0,0-2.4487,3.63,5.7862,5.7862,0,0,0,.9856,4.3717,6.2437,6.2437,0,0,0,6.6978,2.4864,5.7652,5.7652,0,0,0,1.602-.7041l9.9519-6.3425a18.978,18.978,0,0,1,5.2959-2.3278,20.7181,20.7181,0,0,1,22.2368,8.2427,19.1725,19.1725,0,0,1,3.2766,14.5024,17.9977,17.9977,0,0,1-8.13,12.0532L51.1167,104.2528a19.0038,19.0038,0,0,1-5.3,2.3287" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/lib/images/svelte-welcome.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
src/lib/images/svelte-welcome.webp
Normal file
|
After Width: | Height: | Size: 113 KiB |
17
src/lib/interfaces/ApiResponse.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type IProduct from './IProduct';
|
||||
import type { IOrder, IOrderSummary } from './IOrder';
|
||||
|
||||
export interface IProductResponse {
|
||||
success: boolean;
|
||||
products: Array<IProduct>;
|
||||
}
|
||||
|
||||
export interface IOrderResponse {
|
||||
success: boolean;
|
||||
order: IOrder;
|
||||
}
|
||||
|
||||
export interface IOrderSummaryResponse {
|
||||
success: boolean;
|
||||
order: IOrderSummary;
|
||||
}
|
||||
9
src/lib/interfaces/BadgeType.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
enum BadgeType {
|
||||
SUCCESS = 'success',
|
||||
WARNING = 'warning',
|
||||
ERROR = 'error',
|
||||
PENDING = 'pending',
|
||||
INFO = 'info'
|
||||
}
|
||||
|
||||
export default BadgeType;
|
||||
5
src/lib/interfaces/IFrontText.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default interface IFrontText {
|
||||
title: string;
|
||||
text: string;
|
||||
color: string;
|
||||
}
|
||||
6
src/lib/interfaces/IFrontTextImage.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default interface IFrontTextImage {
|
||||
title: string;
|
||||
text: string;
|
||||
image: string;
|
||||
imageRight?: boolean;
|
||||
}
|
||||
69
src/lib/interfaces/IOrder.ts
Normal file
@@ -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;
|
||||
}
|
||||
16
src/lib/interfaces/IProduct.ts
Normal file
@@ -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;
|
||||
}
|
||||
9
src/lib/interfaces/IProductVariation.ts
Normal file
@@ -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;
|
||||
}
|
||||
24
src/lib/utils/cookie.ts
Normal file
@@ -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]}`;
|
||||
},
|
||||
''
|
||||
)}`;
|
||||
}
|
||||
87
src/lib/utils/mock.ts
Normal file
@@ -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<IOrder> {
|
||||
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<IProduct> {
|
||||
return Array.from(Array(count)).map(() => mockProduct());
|
||||
}
|
||||
11
src/lib/utils/requestSessionCookie.ts
Normal file
@@ -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;
|
||||
}
|
||||
8
src/lib/utils/uuid.ts
Normal file
@@ -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;
|
||||
}
|
||||
5
src/lib/utils/validOrderId.ts
Normal file
@@ -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;
|
||||
}
|
||||
110
src/lib/websocketCart.ts
Normal file
@@ -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<typeof setTimeout> | 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();
|
||||
};
|
||||
}
|
||||