Compare commits

...

4 Commits

Author SHA1 Message Date
b56be97f86 Use env var API_HOST to direct where backend lives. 2024-03-03 19:23:20 +01:00
6d2550f2f3 Upload docker image to GHCR & deploy to kubernetes cluster 2024-03-03 19:11:25 +01:00
71e053297e Feat: Frontpage text content (#9)
* Front text blocks text content updated

* Health route

* Linting
2023-06-03 11:10:37 +02:00
63a1107427 Patch: Receipt page (#7)
* OrderSection receipts list of lineItems instead of always getting cart

* Hide express checkout behind feature flag

* Add torn receipt paper css look to order list

* Re-use OrderSection on receipt page

* Linting

* Reduced max font from 130->120%, now only applies scaling on div.main

Reducing relative font size for the largest screen width.
Scaling only applies to main container, not header and footer.

* Minor header size changes

* Set max-width to login input elements on desktop

* Prettier doesn't liek shorthand props
2023-03-28 18:35:14 +02:00
23 changed files with 313 additions and 128 deletions

View File

@@ -1,53 +1,130 @@
---
kind: pipeline
type: docker
name: Lint and build project
name: Build
platform:
os: linux
arch: amd64
steps:
- name: Build project
image: node:18
- name: Install dependencies
image: node:21-alpine3.17
commands:
- yarn
- yarn build
- name: Lint project
image: node:18
image: node:21-alpine3.17
commands:
- yarn
- yarn lint
- name: Build
image: node:21-alpine3.17
commands:
- yarn build
---
kind: pipeline
type: docker
name: Compile docker image
name: Publish
platform:
os: linux
arch: amd64
steps:
- name: Build
image: node:18
commands:
- yarn
- yarn build
depends_on:
- Lint and build project
- name: Publish to ghcr
image: plugins/docker
settings:
registry: ghcr.io
repo: ghcr.io/kevinmidboe/${DRONE_REPO_NAME}
dockerfile: Dockerfile
username:
from_secret: GITHUB_USERNAME
password:
from_secret: GHCR_UPLOAD_TOKEN
tags:
- latest
- ${DRONE_COMMIT_SHA}
trigger:
branch:
- main
event:
include:
- push
exclude:
- pull_request
branch:
- main
depends_on:
- Build
---
kind: pipeline
type: docker
name: Deploy
platform:
os: linux
arch: amd64
steps:
- name: Prepare kubernetes environment
image: alpine/k8s:1.25.15
environment:
VAULT_TOKEN:
from_secret: VAULT_TOKEN
VAULT_HOST:
from_secret: VAULT_HOST
commands:
- mkdir -p /root/.kube
- echo "IMAGE=ghcr.io/kevinmidboe/${DRONE_REPO_NAME}:${DRONE_COMMIT_SHA}" > /root/.kube/.env
- echo "NAMESPACE=${DRONE_REPO_NAME}" >> /root/.kube/.env
- 'curl -s
-H "X-Vault-Token: $VAULT_TOKEN"
$VAULT_HOST/v1/schleppe/data/kazan/_infra
| jq -r ".data.data.KUBE_CONFIG" > /root/.kube/config'
- 'curl -s
-H "X-Vault-Token: $VAULT_TOKEN"
$VAULT_HOST/v1/schleppe/data/kazan/_infra
| jq -cr ".data.data | .[\"ghcr-login-secret\"] | @base64" > /root/.kube/dockerconfig.json'
- echo "DOCKER_CONFIG=$(cat /root/.kube/dockerconfig.json)" >> /root/.kube/.env
- sed -i '/^$/!s/^/export /' /root/.kube/.env
volumes:
- name: kube-config
path: /root/.kube
- name: Deploy to kubernetes
image: alpine/k8s:1.25.15
commands:
- source /root/.kube/.env > /dev/null 2>&1
- cat .kubernetes/*.yml
| envsubst
| kubectl --kubeconfig=/root/.kube/config apply -f -
volumes:
- name: kube-config
path: /root/.kube
trigger:
event:
include:
- push
exclude:
- pull_request
branch:
- main
depends_on:
- Build
- Publish
volumes:
- name: kube-config
temp: {}
---
kind: signature
hmac: 84765f19d995d66f1d3409c4eddd1f68d1f2d297d65cd9e2612e6bb13e8ecb94
hmac: 1e803c7610cc5d3b586af3f10228a4a3477d877538813dee6c366c952771e3e0
...

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: planet
labels:
name: planet

View File

@@ -0,0 +1,41 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: planet-frontend
name: planet-frontend
namespace: planet
spec:
progressDeadlineSeconds: 600
replicas: 2
revisionHistoryLimit: 10
selector:
matchLabels:
app: planet-frontend
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
app: planet-frontend
spec:
containers:
- image: ${IMAGE}
imagePullPolicy: Always
name: planet-frontend
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
imagePullSecrets:
- name: ghcr-login-secret
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30

19
.kubernetes/ingress.yml Normal file
View File

@@ -0,0 +1,19 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: planet-frontend-ingress
namespace: planet
spec:
ingressClassName: traefik
rules:
- host: planet.kazan.schleppe.cloud
http:
paths:
- backend:
service:
name: planet-frontend-service
port:
number: 80
path: /
pathType: Prefix

18
.kubernetes/service.yml Normal file
View File

@@ -0,0 +1,18 @@
---
apiVersion: v1
kind: Service
metadata:
labels:
app: planet-frontend
name: planet-frontend-service
namespace: planet
spec:
selector:
app: planet-frontend
type: ClusterIP
ports:
- name: http
port: 80
targetPort: 3000
sessionAffinity: None

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
# Build the project
FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
RUN yarn
RUN yarn build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/build build/
COPY --from=builder /app/node_modules node_modules/
COPY package.json .
EXPOSE 3000
ENV NODE_ENV=production
CMD [ "node", "build" ]

View File

@@ -12,6 +12,7 @@
"format": "prettier --plugin-search-dir . --write src"
},
"devDependencies": {
"@sveltejs/adapter-node": "^4.0.1",
"@sveltejs/adapter-static": "1.0.0",
"@sveltejs/kit": "1.0.1",
"@types/cookie": "0.5.1",
@@ -34,4 +35,4 @@
"dependencies": {
"@stripe/stripe-js": "1.46.0"
}
}
}

View File

@@ -1,11 +1,13 @@
import { env } from '$env/dynamic/private';
import type { HandleFetch } from '@sveltejs/kit';
export const handleFetch: HandleFetch = async ({ request, fetch }) => {
const { origin } = new URL(request.url);
const host = env?.API_HOST || 'http://localhost:30010';
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);
request = new Request(request.url.replace(origin, host), request);
}
return fetch(request);

View File

@@ -10,19 +10,20 @@
const textImages: Array<IFrontTextImage> = [
{
title: 'Vårt oppdrag',
text: 'The new fabulous museum at Kistefos, designed by world renowned architect Bjarke Ingels Group, BIG, opened Wednesday September 18th, 2019. The building has been named the top architectural museum project in the world to open in 2019, by both the Daily Telegraph and Bloomberg.',
title: 'Our story',
text: 'I started making fabric gift bags as a way to combat the wastefulness of traditional paper packaging. As a lifelong crafter and DIY enthusiast, I wanted to create a product that was both beautiful and sustainable. After experimenting with different materials and designs, I landed on the perfect formula for a reusable fabric gift bag that was both eco-friendly and functional.',
image: 'https://storage.googleapis.com/planetposen-images/front-kf-1.jpg'
},
{
title: 'Paper waste and the planet',
text: "As the 50th artwork to be included in the park, a site-specific new commission by French artist Pierre Huyghe (b. 1962, Paris) was opened on the 12th of June. The vast permanent work will be the artist's largest site-specific work to date and the most ambitious to ever be conceived for Kistefos.",
title: 'Eco-Friendly Materials',
text: 'Our bags are made from organic cotton or recycled fabric, both of which are sustainably sourced and responsibly produced. By choosing to use our bags instead of disposable paper or plastic packaging, you are making a positive impact on the environment and reducing your carbon footprint.',
imageRight: true,
image: 'https://storage.googleapis.com/planetposen-images/front-kf-2.jpg'
// image: 'https://storage.googleapis.com/planetposen-images/front-kf-2.jpg'
image: 'https://storage.googleapis.com/planetposen-images/bags_backyard-upscaled-2.jpeg'
},
{
title: 'Our goal',
text: 'The scenic sculpture park has an impressive collection of works by internationally renowned contemporary artists including Anish Kapoor, Jeppe Hein, Tony Cragg, Olafur Eliasson, Fernando Bottero and Elmgreen & Dragset. The sculpture park focus is sight specific and international contemporary works of art and is available all year.',
title: 'Gift Ideas',
text: "Whether you're looking for a birthday present, a wedding gift, or a holiday surprise, our fabric gift bags are the perfect way to add a personal touch to any occasion. Use our bags to wrap a variety of gifts, from jewelry and accessories to small electronics and gadgets. Not sure where to start? Check out our gallery for inspiration!",
image: 'https://storage.googleapis.com/planetposen-images/front-kf-3.jpg'
},
{
@@ -32,8 +33,8 @@
image: 'https://storage.googleapis.com/planetposen-images/front-bee-1.jpg'
},
{
title: 'Sculpture park of international standing',
text: 'The scenic sculpture park has an impressive collection of works by internationally renowned contemporary artists including Anish Kapoor, Jeppe Hein, Tony Cragg, Olafur Eliasson, Fernando Bottero and Elmgreen & Dragset. The sculpture park focus is sight specific and international contemporary works of art and is available all year.',
title: 'Gift Ideas',
text: '',
imageRight: false,
image: 'https://storage.googleapis.com/planetposen-images/front-bee-2.jpg'
}
@@ -42,12 +43,12 @@
const textTitle: Array<IFrontText> = [
{
title: 'Katy Vandekerckhove:',
text: 'Kistefos was really a jewel on earth with high level art in fantastic surroundings',
text: 'Give a gift that keeps on giving - our fabric gift bags are reusable, eco-friendly, and stylish.',
color: '#27615d'
},
{
title: 'Katy Vandekerckhove:',
text: 'Kistefos was really a jewel on earth with high level art in fantastic surroundings',
text: 'Wrap it up in style and sustainability with planetposen fabric gift bags.',
color: 'orange'
}
];
@@ -104,7 +105,10 @@
<FrontText data="{textTitle[0]}" />
<FrontTextImage data="{textImages[3]}" />
<!--
<FrontTextImage data="{textImages[4]}" />
-->
<FrontText data="{textTitle[1]}" />
</section>

View File

@@ -14,7 +14,7 @@
<footer>
<section>
<h2>Personvern og vilkår</h2>
<h1>Personvern og vilkår</h1>
<ul>
<li><LinkArrow /><a href="/terms-and-conditions">Betingelser og vilkår</a></li>
<li><LinkArrow /><a href="/privacy-policy">Personvernerklæring</a></li>
@@ -31,7 +31,7 @@
</section>
<section>
<h2>Kontakt</h2>
<h1>Kontakt</h1>
<ul>
<li>Epost:&nbsp;<a class="link" href="mailto:post@planetposen.no">post@planetposen.no</a></li>
@@ -41,6 +41,7 @@
Kode:&nbsp;<a
class="link"
target="_blank"
rel="noreferrer"
href="https://github.com/search?q=user%3Akevinmidboe+sort%3Aupdated+planetposen&type=repositories"
>github.com</a
>
@@ -60,10 +61,6 @@
display: flex;
justify-content: space-around;
h2 {
font-size: 2.5rem;
}
section {
width: 30%;
padding: 2rem;
@@ -97,7 +94,7 @@
flex-direction: column;
padding: 3rem 0.5rem;
h2 {
h1 {
font-size: 1.8rem;
}

View File

@@ -0,0 +1 @@
export const GET = () => new Response('ok');

View File

@@ -7,7 +7,7 @@
import CheckoutButton from '$lib/components/Button.svelte';
import StripeCard from '$lib/components/StripeCard.svelte';
import ErrorStack from '$lib/components/ErrorStack.svelte';
import { cart } from '$lib/cartStore';
import { cart, subTotal } from '$lib/cartStore';
import stripeApi from '$lib/stripe/index';
import { OrderSubmitUnsuccessfullError } from '$lib/errors/OrderErrors';
import Loading from '$lib/components/loading/index.svelte';
@@ -28,6 +28,7 @@
let card: StripeCardElement;
let form: HTMLFormElement;
let errors: string[] = [];
let showExpressCheckout = false;
/* eslint-disable @typescript-eslint/no-explicit-any */
let resolvePaymentPromise: (value: any) => void;
@@ -147,13 +148,15 @@
<form class="checkout" bind:this="{form}" on:submit|preventDefault="{postOrder}">
<div class="main">
<section class="express-checkout" style="display: block;">
<h2>Hurtigkasse</h2>
{#if showExpressCheckout}
<section class="express-checkout" style="display: block;">
<h2>Hurtigkasse</h2>
<ExpressSection />
<ExpressSection />
<p style="margin: 0 0 -0.5rem 0.5rem; text-align: left; color: rgba(0,0,0,0.5);">eller</p>
</section>
<p style="margin: 0 0 -0.5rem 0.5rem; text-align: left; color: rgba(0,0,0,0.5);">eller</p>
</section>
{/if}
<section id="delivery">
<h2>Leveringsaddresse</h2>
@@ -178,7 +181,7 @@
<aside class="sidebar">
<section id="order">
<h2>Din ordre</h2>
<OrderSection />
<OrderSection lineItems="{$cart}" subTotal="{$subTotal}" />
</section>
</aside>
</form>

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import Vipps from '$lib/icons/Vipps.svelte';
import ShopPay from '$lib/icons/ShopPay.svelte';
import ApplePay from '$lib/icons/ApplePay.svelte';
import PayPal from '$lib/icons/PayPal.svelte';
import GooglePay from '$lib/icons/GooglePay.svelte';

View File

@@ -1,16 +1,9 @@
<script lang="ts">
import OrderTotalSection from './OrderTotalSection.svelte';
import QuantitySelect from '$lib/components/QuantitySelect.svelte';
import type ICart from '$lib/interfaces/ICart';
import { cart, subTotal } from '$lib/cartStore';
import { decrementProductInCart, incrementProductInCart } from '$lib/websocketCart';
const shippingPrice = 75;
$: totalPrice = $subTotal + shippingPrice;
function lineItemClass(id: number) {
return `lineitem-${id}`;
}
export let lineItems: ICart[];
export let subTotal: number;
</script>
<div class="order-summary">
@@ -26,7 +19,7 @@
</thead>
<tbody data-order-summary-section="line-items">
{#each $cart as cartItem}
{#each lineItems as lineItem}
<tr
class="product"
data-product-id="6718367989809"
@@ -40,18 +33,18 @@
<img
alt="Black Googly Eye Puff Print Logo Tee - XS"
class="product-thumbnail__image"
src="{cartItem.image}"
src="{lineItem.image}"
data-src="//cdn.shopify.com/s/files/1/0023/3789/8540/products/20220718_A24_GooglyEye_Tee_Black_15991x1gray_small.jpg?v=1659020903"
/>
</div>
<span class="product-thumbnail__quantity" aria-hidden="true">{cartItem.quantity}</span
<span class="product-thumbnail__quantity" aria-hidden="true">{lineItem.quantity}</span
>
</div>
</td>
<th class="product__description" scope="row">
<span class="product__description__name order-summary__emphasis">{cartItem.name}</span>
<span class="product__description__name order-summary__emphasis">{lineItem.name}</span>
<span class="product__description__variant order-summary__small-text"
>{cartItem.size}</span
>{lineItem.size}</span
>
</th>
<td class="product__quantity">
@@ -59,7 +52,7 @@
</td>
<td class="product__price">
<p class="order-summary__emphasis skeleton-while-loading">
NOK {cartItem.quantity * cartItem.price}
NOK {lineItem.quantity * lineItem.price}
</p>
</td>
</tr>
@@ -68,7 +61,7 @@
</tbody>
</table>
<OrderTotalSection />
<OrderTotalSection subTotal="{subTotal}" />
</div>
<style lang="scss" module="scoped">

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { subTotal } from '$lib/cartStore';
export let subTotal: number;
</script>
<div class="total">
@@ -19,7 +19,7 @@
class="order-summary__emphasis skeleton-while-loading"
data-checkout-subtotal-price-target="4000"
>
Nok {$subTotal}
Nok {subTotal}
</span>
</td>
</tr>
@@ -58,7 +58,7 @@
<td class="price payment-due" data-presentment-currency="NOK">
<span class="payment-due__currency remove-while-loading">Nok</span>
<span class="price skeleton-while-loading--lg" data-checkout-payment-due-target="4000">
{$subTotal + 75}
{subTotal + 75}
</span>
</td>
</tr>

View File

@@ -62,13 +62,14 @@
<style lang="scss" module="scoped">
@import '../../styles/media-queries.scss';
@include desktop {
section {
margin: 20% 2rem;
width: 60%;
section {
max-width: 600px;
margin: auto;
@include desktop {
margin: 20% auto;
}
}
.signin-button {
margin-top: 2rem;
}

View File

@@ -6,7 +6,7 @@ export const load: PageServerLoad = async ({ fetch, params }) => {
const { id } = params;
const res = await fetch(`/api/v1/order/${id}`);
const orderResponse = await res.json();
const orderResponse: IOrderDTO = await res.json();
if (orderResponse?.success == false || orderResponse?.order === undefined) {
console.log('throwing error', orderResponse);

View File

@@ -1,5 +1,5 @@
import { redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({ request }) => {
@@ -10,8 +10,6 @@ export const actions: Actions = {
const receiptUrl = `/receipt/${orderId}?email=${email}`;
throw redirect(303, receiptUrl);
return { success: false };
}
};

View File

@@ -2,17 +2,11 @@
import { page } from '$app/stores';
import CircleCheckmark from '$lib/components/loading/CircleCheckmark.svelte';
import CircleError from '$lib/components/loading/CircleError.svelte';
import OrderSection from '../../checkout/OrderSection.svelte';
import type { PageServerData } from './$types';
import type { ILineItem, IOrder } from '$lib/interfaces/IOrder';
import type { IOrder } from '$lib/interfaces/IOrder';
import CircleWarning from '$lib/components/loading/CircleWarning.svelte';
function subTotal(lineItems: Array<ILineItem> = []) {
let total = 0;
lineItems.forEach((lineItem) => (total = total + lineItem.price * lineItem.quantity));
return total;
}
let id: string;
let email: string;
let order: IOrder;
@@ -23,6 +17,8 @@
email = data.email || (data?.order?.customer?.email as string);
order = data.order as IOrder;
}
$: subTotal = Math.round((order?.payment?.amount || 1) / 100);
</script>
<section class="order-confirmation">
@@ -49,21 +45,9 @@
</div>
<div class="order-receipt">
{#each order?.lineItems as lineItem}
<p>
<code>{lineItem.name} x{lineItem.quantity}</code>
<code>NOK {lineItem.price * lineItem.quantity}</code>
</p>
{/each}
<p>
<code>Shipping</code>
<code>NOK 75</code>
</p>
<p>
<code>Total</code>
<code>NOK {subTotal(order?.lineItems)}</code>
</p>
<div class="receipt-box">
<OrderSection lineItems="{order?.lineItems}" subTotal="{subTotal}" } />
</div>
</div>
</section>
@@ -75,30 +59,52 @@
}
.order-receipt {
background-color: #f7f7f7;
max-width: 500px;
--receipt_color: #f7f7f7;
--tearOffHeight: 8px;
background-color: var(--receipt_color);
max-width: 800px;
width: calc(100% - 4rem);
padding: 2rem;
font-family: monospace;
position: relative;
p {
margin: 0.8rem 0;
display: flex;
justify-content: space-between;
border-bottom: 1px solid lightgrey;
/* Paper background effect */
.receipt-box {
height: auto;
overflow: hidden;
padding: 1rem;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.05);
&:last-of-type {
padding-top: 1.5rem;
border-width: 2px;
}
}
code {
opacity: 0.4;
font-size: 1rem;
&:first-of-type {
font-weight: 600;
&::after {
content: '';
height: var(--tearOffHeight);
position: absolute;
left: 0;
right: 0;
bottom: calc(var(--tearOffHeight) * -1);
background-color: var(--receipt_color);
clip-path: polygon(
0% 0%,
5% 100%,
10% 0%,
15% 100%,
20% 0%,
25% 100%,
30% 0%,
35% 100%,
40% 0%,
45% 100%,
50% 0%,
55% 100%,
60% 0%,
65% 100%,
70% 0%,
75% 100%,
80% 0%,
85% 100%,
90% 0%,
95% 100%,
100% 0%
);
}
}
}

View File

@@ -4,7 +4,6 @@
import { onMount } from 'svelte';
import CircleLoading from '$lib/components/loading/CircleLoading.svelte';
import { buildApiUrl } from '$lib/utils/apiUrl';
import type { PageServerData } from './$types';
const { data } = $page;
const id = data?.id as string;
@@ -22,7 +21,7 @@
goto(url);
}
function checkOrder() {
async function checkOrder() {
const url = buildApiUrl(`/api/v1/order/${id}`);
return fetch(url)
.then((resp) => resp.json())

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import ProductTile from '$lib/components/ProductTile.svelte';
import ProductVariationSelect from '$lib/components/ProductVariationSelect.svelte';
import QuantitySelect from '$lib/components/QuantitySelect.svelte';
import SizesSection from './SizesSection.svelte';
@@ -98,7 +97,7 @@
.details {
.name {
font-size: 2rem;
font-size: 2em;
}
.description {

View File

@@ -14,7 +14,7 @@
color: var(--color-text);
}
html {
body .app main {
font-size: 100%;
}
@@ -56,11 +56,11 @@ a.link {
}
h1 {
font-size: 2rem;
font-size: 2em;
}
h2 {
font-size: 1rem;
font-size: 1em;
}
.no-scroll {
@@ -104,12 +104,12 @@ button:focus:not(:focus-visible) {
}
@media screen and (min-width: 1500px) {
html {
body .app main {
font-size: 110%;
}
}
@media screen and (min-width: 2000px) {
html {
font-size: 130%;
body .app main {
font-size: 125%;
}
}

View File

@@ -1,3 +1,4 @@
import adapter from '@sveltejs/adapter-node';
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
@@ -7,6 +8,7 @@ const config = {
preprocess: preprocess(),
kit: {
adapter: adapter(),
csrf: {
checkOrigin: false
}