🚧 Switch to monorepo with Turbo

This commit is contained in:
2023-01-10 12:53:42 +01:00
parent dd8715bb34
commit 25bb949a13
205 changed files with 14975 additions and 347 deletions

View File

@@ -0,0 +1,69 @@
<style lang="scss">
@import "../../style/molecules/cart-item";
</style>
<script lang="ts">
import { createEventDispatcher } from 'svelte'
// Components
import ButtonCircle from '$components/atoms/ButtonCircle.svelte'
import Select from '$components/molecules/Select.svelte'
export let item: any
const dispatch = createEventDispatcher()
const quantityLimit = 5
// When changing item quantity
const updateQuantity = ({ detail }: any) => {
dispatch('updatedQuantity', {
id: item.id,
quantity: Number(detail)
})
}
// When removing item
const removeItem = () => {
dispatch('removed', item.id)
}
</script>
<div class="cart-item shadow-small">
<div class="cart-item__left">
<img src={item.product.images[0].file.url} width={200} height={300} alt={item.product.name}>
</div>
<div class="cart-item__right">
<h3>Poster</h3>
<p>
{item.product.name}
<br> {item.price}
</p>
{#if item && item.quantity}
<Select
name="sort" id="filter_sort"
options={[...Array(item.quantity <= quantityLimit ? quantityLimit : item.quantity)].map((_, index) => {
return {
value: `${index + 1}`,
name: `${index + 1}`,
default: index === 0,
selected: index + 1 === item.quantity,
}
})}
on:change={updateQuantity}
value={String(item.quantity)}
>
<span>Quantity:</span>
</Select>
{/if}
<ButtonCircle class="remove"
size="tiny" color="gray"
on:click={removeItem}
>
<svg width="8" height="8">
<use xlink:href="#cross" />
</svg>
</ButtonCircle>
</div>
</div>

View File

@@ -0,0 +1,106 @@
<style lang="scss">
@import "../../style/molecules/newsletter-form";
</style>
<script lang="ts">
import { fly } from 'svelte/transition'
import { quartOut } from 'svelte/easing'
import { sendEvent } from '$utils/analytics'
// Components
import IconArrow from '$components/atoms/IconArrow.svelte'
import ButtonCircle from '$components/atoms/ButtonCircle.svelte'
export let past: boolean = false
let inputInFocus = false
let formStatus: FormStatus = null
let formMessageTimeout: ReturnType<typeof setTimeout> | number
interface FormStatus {
error?: string
success?: boolean
message: string
}
const formMessages = {
PENDING: `Almost there! Please confirm your email address through the email you'll receive soon.`,
MEMBER_EXISTS_WITH_EMAIL_ADDRESS: `This email address is already subscribed to the newsletter.`,
INVALID_EMAIL: `Woops. This email doesn't seem to be valid.`,
}
$: isSuccess = formStatus && formStatus.success
// Toggle input focus
const toggleFocus = () => inputInFocus = !inputInFocus
// Handle form submission
async function handleForm (event: Event | HTMLFormElement) {
const data = new FormData(this)
const email = data.get('email')
if (email) {
const req = await fetch(this.action, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email })
})
const result: FormStatus = await req.json()
formStatus = result
console.log('SK api response:', result)
// If successful
if (formStatus.success) {
sendEvent('newsletterSubscribe')
} else {
// Hide message for errors
clearTimeout(formMessageTimeout)
formMessageTimeout = requestAnimationFrame(() => setTimeout(() => formStatus = null, 4000))
}
}
}
</script>
<div class="newsletter-form">
{#if !isSuccess}
<form method="POST" action="/api/newsletter" on:submit|preventDefault={handleForm}
out:fly|local={{ y: -8, easing: quartOut, duration: 600 }}
>
<div class="newsletter-form__email" class:is-focused={inputInFocus}>
<input type="email" placeholder="Your email address" name="email" id="newsletter_email" required
on:focus={toggleFocus}
on:blur={toggleFocus}
>
<ButtonCircle
type="submit"
color="pink" size="small"
clone={true}
label="Subscribe"
>
<IconArrow color="white" />
</ButtonCircle>
</div>
<div class="newsletter-form__bottom">
{#if past}
<a href="/subscribe" class="past-issues" data-sveltekit-noscroll>
<svg width="20" height="16" viewBox="0 0 20 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-label="Newsletter icon">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 2.346H2a.5.5 0 0 0-.5.5v11.102a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5V2.846a.5.5 0 0 0-.5-.5ZM2 .846a2 2 0 0 0-2 2v11.102a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2.846a2 2 0 0 0-2-2H2Zm13.75 4.25h-2v3h2v-3Zm-2-1a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2ZM3.5 6.5a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Zm.25 3a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5Zm1.25 2a.5.5 0 0 0 0 1h6a.5.5 0 1 0 0-1H5Z" />
</svg>
<span>See past issues</span>
</a>
{/if}
<p>No spam, we promise!</p>
</div>
</form>
{/if}
{#if formStatus && formStatus.message}
<div class="newsletter-form__message shadow-small"
class:is-error={!isSuccess}
class:is-success={isSuccess}
in:fly|local={{ y: 8, easing: quartOut, duration: 600, delay: isSuccess ? 600 : 0 }}
out:fly|local={{ y: 8, easing: quartOut, duration: 600 }}
>
<p class="text-xsmall">{formMessages[formStatus.message]}</p>
</div>
{/if}
</div>

View File

@@ -0,0 +1,17 @@
<style lang="scss">
@import "../../style/molecules/heading";
</style>
<script lang="ts">
import SiteTitle from '$components/atoms/SiteTitle.svelte'
export let text: string
</script>
<section class="heading">
<SiteTitle variant="inline" />
<div class="text text-medium">
{@html text}
</div>
</section>

View File

@@ -0,0 +1,66 @@
<style lang="scss">
@import "../../style/molecules/house";
</style>
<script lang="ts">
import dayjs from 'dayjs'
// Components
import Image from '$components/atoms/Image.svelte'
import Icon from '$components/atoms/Icon.svelte'
export let url: string
export let photoId: string
export let photoAlt: string
export let title: string
export let index: string
export let ratio: number
export let date: string = undefined
export let city: string = undefined
export let location: string
</script>
<div class="house grid">
<div class="house__info">
<h2 class="title-image">
{title}
</h2>
<p class="info text-info">
{#if city}
<a href="https://www.openstreetmap.org/search?query={title}, {city} {location}" target="_blank" rel="noopener noreferrer">
<Icon class="icon" icon="map-pin" label="Map pin" /> {city}
</a>
<span class="sep">·</span>
{/if}
{#if date}
<time datetime={dayjs(date).format('YYYY-MM-DD')}>
{dayjs(date).format('MMMM YYYY')}
</time>
{/if}
</p>
</div>
<div class="house__photo grid" class:not-landscape={ratio < 1.475}>
<a href={url} tabindex="0">
<figure class="house__image shadow-photo">
<Image
class="photo"
id={photoId}
sizeKey="photo-list"
sizes={{
small: { width: 500 },
medium: { width: 850 },
large: { width: 1280 },
}}
ratio={1.5}
alt={photoAlt}
/>
</figure>
</a>
<span class="house__index title-index"
class:has-one-start={index.startsWith('1')}
class:has-one-end={index.endsWith('1')}
>
{index}
</span>
</div>
</div>

View File

@@ -0,0 +1,125 @@
<style lang="scss">
@import "../../style/molecules/location";
</style>
<script lang="ts">
import { getContext } from 'svelte'
import { spring } from 'svelte/motion'
import dayjs from 'dayjs'
import { lerp } from '$utils/functions'
import { PUBLIC_PREVIEW_COUNT } from '$env/static/public'
import { seenLocations } from '$utils/stores'
// Components
import Image from '$components/atoms/Image.svelte'
import Badge from '$components/atoms/Badge.svelte'
export let location: any
export let latestPhoto: any
const { settings }: any = getContext('global')
let locationEl: HTMLElement
let photoIndex = 0
// Location date limit
let isNew = false
const dateNowOffset = dayjs().subtract(settings.limit_new, 'day')
const parsedSeenLocations = JSON.parse($seenLocations)
$: if (latestPhoto) {
const dateUpdated = dayjs(latestPhoto.date_created)
// Detect if location has new content
const seenLocationDate = dayjs(parsedSeenLocations[location.id])
const isLocationSeen = parsedSeenLocations?.hasOwnProperty(location.id)
// Define if location is has new photos
if (seenLocationDate && isLocationSeen) {
// A more recent photo has been added (if has been seen and has a seen date)
isNew = dateUpdated.isAfter(dateNowOffset) && dateUpdated.isAfter(seenLocationDate)
} else {
// The photo is after the offset
isNew = dateUpdated.isAfter(dateNowOffset)
}
}
/**
* Moving cursor over
*/
const offset = spring({ x: 0, y: 0 }, {
stiffness: 0.075,
damping: 0.9
})
const handleMouseMove = ({ clientX }: MouseEvent) => {
const { width, left } = locationEl.getBoundingClientRect()
const moveProgress = (clientX - left) / width // 0 to 1
// Move horizontally
offset.update(_ => ({
x: lerp(-56, 56, moveProgress),
y: 0
}))
// Change photo index from mouse position percentage
photoIndex = Math.round(lerp(0, Number(PUBLIC_PREVIEW_COUNT) - 1, moveProgress))
}
// Leaving mouseover
const handleMouseLeave = () => {
offset.update($c => ({
x: $c.x,
y: 40
}))
}
</script>
<div class="location" bind:this={locationEl}
style:--offset-x="{$offset.x}px"
style:--offset-y="{$offset.y}px"
style:--rotate="{$offset.x * 0.125}deg"
>
<a href="/{location.country.slug}/{location.slug}"
on:mousemove={handleMouseMove}
on:mouseleave={handleMouseLeave}
tabindex="0"
>
<Image
class="flag"
id={location.country.flag.id}
sizeKey="square-small"
width={32} height={32}
alt="Flag of {location.country.name}"
/>
<div class="text">
<dl>
<dt class="location__name">
{location.name}
</dt>
<dd class="location__country text-label">
{location.country.name}
</dd>
</dl>
{#if isNew}
<Badge text="New" />
{/if}
</div>
</a>
{#if location.photos.length}
<div class="location__photos">
{#each location.photos as { image }, index}
{#if image}
{@const classes = ['location__photo', index === photoIndex ? 'is-visible' : null].join(' ').trim()}
<Image
class={classes}
id={image.id}
sizeKey="photo-thumbnail"
width={340} height={226}
alt={image.title}
/>
{/if}
{/each}
</div>
{/if}
</div>

View File

@@ -0,0 +1,33 @@
<style lang="scss">
@import "../../style/molecules/issue";
</style>
<script lang="ts">
import dayjs from 'dayjs'
import Image from '$components/atoms/Image.svelte'
export let title: string
export let issue: number
export let date: string
export let link: string
export let thumbnail: { id: string }
export let size: string = undefined
</script>
<div class="issue" class:is-large={size === 'large'}>
<a href={link} target="_blank" rel="external noopener" tabindex="0">
<Image
id={thumbnail.id}
sizeKey="issue-thumbnail-small"
width={160} height={112}
alt="Issue {issue} thumbnail"
/>
<dl>
<dt>Issue #{issue}</dt>
<dd>
<p>{title}</p>
<time>{dayjs(date).format('DD/MM/YYYY')}</time>
</dd>
</dl>
</a>
</div>

View File

@@ -0,0 +1,32 @@
<style lang="scss">
@import "../../style/molecules/notification-cart";
</style>
<script lang="ts">
import { fly } from 'svelte/transition'
import { quartOut } from 'svelte/easing'
import { cartOpen } from '$utils/stores/shop'
export let title: string
export let name: string
export let image: string
const closeNotification = () => {
// Open cart
$cartOpen = true
}
</script>
<div class="notification-cart shadow-small"
on:click={closeNotification}
transition:fly={{ y: 20, duration: 700, easing: quartOut }}
>
<div class="notification-cart__left">
<img src={image} width={58} height={88} alt={title}>
</div>
<div class="notification-cart__right">
<h3>{title}</h3>
<p>{name}</p>
</div>
</div>

View File

@@ -0,0 +1,23 @@
<style lang="scss">
@import "../../style/molecules/pagination";
</style>
<script lang="ts">
export let ended: boolean = false
export let current: number
export let total: number
</script>
<div class="pagination" role="button" tabindex="0"
disabled={ended ? ended : undefined}
on:click
on:keydown
>
<div class="pagination__progress">
<span class="current">{current}</span>
<span>/</span>
<span class="total">{total}</span>
<slot />
</div>
</div>

View File

@@ -0,0 +1,70 @@
<style lang="scss">
@import "../../style/molecules/photo-card";
</style>
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import Image from '$components/atoms/Image.svelte'
export let id: string
export let alt: string
export let url: string = undefined
export let title: string = undefined
export let location: any = undefined
export let city: string = undefined
export let hovered: boolean = false
export let lazy: boolean = true
const dispatch = createEventDispatcher()
const sizes = {
small: { width: 224 },
medium: { width: 464 },
large: { width: 864 },
}
const sendHover = (hover: boolean) => dispatch('hover', hover)
</script>
<div class="photo-card"
class:is-hovered={hovered}
on:mouseenter={() => sendHover(true)}
on:focus={() => sendHover(true)}
on:mouseout={() => sendHover(false)}
on:blur={() => sendHover(false)}
>
{#if url}
<div class="photo-card__content">
<a href={url} data-sveltekit-noscroll>
<Image
{id}
sizeKey="postcard"
{sizes}
ratio={1.5}
{alt}
{lazy}
/>
{#if title && location}
<div class="photo-card__info">
<Image
id={location.country.flag.id}
sizeKey="square-small"
width={24}
height={24}
alt="Flag of {location.country.name}"
/>
<p>{title} - {city ? `${city}, ` : ''}{location.name}, {location.country.name}</p>
</div>
{/if}
</a>
</div>
{:else}
<Image
{id}
sizeKey="postcard"
{sizes}
ratio={1.5}
{alt}
{lazy}
/>
{/if}
</div>

View File

@@ -0,0 +1,48 @@
<style lang="scss">
@import "../../style/organisms/postcard";
</style>
<script lang="ts">
import Image from '$components/atoms/Image.svelte'
export let street: string
export let location: string
export let region: string = undefined
export let country: string
export let flagId: string
export let size: string = undefined
const className = 'postcard'
$: classes = [
className,
...[size].map(variant => variant && `${className}--${variant}`),
$$props.class
].join(' ').trim()
</script>
<div class={classes}>
<div class="postcard__left">
<p class="postcard__country">
<span>Houses of</span><br>
<strong class="title-country__purple">{country}</strong>
</p>
</div>
<div class="postcard__right">
<div class="postcard__stamp">
<div class="frame">
<img src="/images/icons/stamp.svg" width="32" height="42" alt="Stamp">
</div>
<Image
class="flag"
id={flagId}
sizeKey="square-small"
width={32} height={32}
alt="Flag of {country}"
/>
</div>
<ul class="postcard__address">
<li>{street}</li>
<li>{location}{region ? `, ${region}` : ''}</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,50 @@
<style lang="scss">
@import "../../style/molecules/poster";
</style>
<script lang="ts">
import { addToCart } from '$utils/functions/shop'
import { smoothScroll } from '$utils/stores'
// Components
import Button from '$components/atoms/Button.svelte'
import Image from '$components/atoms/Image.svelte'
export let product: any
export let location: { name: string, slug: string }
export let image: any
</script>
<div class="poster">
{#if image}
<a href="/shop/poster-{location.slug}" data-sveltekit-noscroll
on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}
>
<Image
id={image.id}
sizeKey="product"
sizes={{
small: { width: 326 },
medium: { width: 326 },
large: { width: 326 },
}}
ratio={1.5}
alt="Poster of {location.name}"
/>
</a>
{/if}
<div class="buttons">
<Button
size="xsmall"
url="/shop/poster-{location.slug}"
text="View"
on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}
/>
<Button
tag="button"
size="xsmall"
text="Add to cart"
color="pink"
on:click={() => addToCart(product)}
/>
</div>
</div>

View File

@@ -0,0 +1,51 @@
<style lang="scss">
@import "../../style/molecules/process-step";
</style>
<script lang="ts">
import { scaleFade } from '$animations/transitions'
// Components
import Image from '$components/atoms/Image.svelte'
import { getAssetUrlKey } from '$utils/api'
export let index: number
export let text: string
export let image: any = undefined
export let video: any = undefined
const imageRatio = image ? image.width / image.height : undefined
</script>
<div class="step grid" style:--index={index}
in:scaleFade|local={{ scale: [1.1, 1], opacity: [0, 1], x: [20, 0], delay: 0.2 }}
out:scaleFade|local={{ scale: [1, 0.9], opacity: [1, 0], x: [0, -20] }}
>
{#if image || video}
<div class="media">
{#if image}
<Image
class="image shadow-box-dark"
id={image.id}
sizeKey="product"
sizes={{
small: { width: 400 },
medium: { width: 600 },
}}
ratio={imageRatio}
alt={image.title}
/>
{:else if video && video.mp4 && video.webm}
<video muted loop playsinline autoplay allow="autoplay">
<source type="video/mp4" src={getAssetUrlKey(video.mp4, 'step')} />
<source type="video/webm" src={getAssetUrlKey(video.webm, 'step')} />
<track kind="captions" />
</video>
{/if}
</div>
{/if}
<div class="text text-xsmall">
{@html text}
</div>
</div>

View File

@@ -0,0 +1,51 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
interface Option {
value: string
name: string
default?: boolean
selected?: boolean
}
export let id: string
export let name: string
export let options: Option[]
export let value: string = undefined
const dispatch = createEventDispatcher()
const defaultOption = options.find(option => option.default)
let selected = value || options[0].value
$: currentOption = options.find(option => option.value === selected)
// Redefine value from parent (when reset)
$: if (value === defaultOption.value) {
selected = defaultOption.value
}
/**
* When changing select value
*/
const handleChange = ({ target: { value }}: any) => {
const option = options.find(option => option.value === value)
// Dispatch event to parent
dispatch('change', option.value)
}
</script>
<div class="select">
<slot />
<span>{currentOption.name}</span>
<select {name} {id} bind:value={selected} on:change={handleChange}>
{#each options as { value, name }}
<option {value} selected={value === selected}>
{name}
</option>
{/each}
</select>
</div>

View File

@@ -0,0 +1,45 @@
<style lang="scss">
@import "../../style/molecules/shop-locationswitcher";
</style>
<script lang="ts">
import { goto } from '$app/navigation'
import { getContext, tick } from 'svelte'
import { shopCurrentProductSlug } from '$utils/stores/shop'
import { smoothScroll } from '$utils/stores'
export let isOver: boolean = false
const { shopLocations }: any = getContext('shop')
const classes = [
'shop-locationswitcher',
isOver && 'is-over',
$$props.class
].join(' ').trim()
// Quick location change
const quickLocationChange = async ({ target: { value }}: any) => {
const pathTo = `/shop/poster-${value}`
goto(pathTo, { replaceState: true, noscroll: true, keepfocus: true })
// Scroll to anchor
await tick()
$smoothScroll.scrollTo('#poster', { duration: 2 })
}
</script>
<dl class={classes}>
<dt class="text-label">Choose a city</dt>
<dd>
<svg width="18" height="18">
<use xlink:href="#icon-map-pin" />
</svg>
<select on:change={quickLocationChange}>
{#each shopLocations as { name, slug }}
<option value={slug} selected={slug === $shopCurrentProductSlug}>{name}</option>
{/each}
</select>
</dd>
</dl>

View File

@@ -0,0 +1,73 @@
<style lang="scss">
@import "../../style/molecules/switcher";
</style>
<script lang="ts">
import { page } from '$app/stores'
import { getContext } from 'svelte'
import reveal from '$animations/reveal'
import { sendEvent } from '$utils/analytics'
// Components
import Icon from '$components/atoms/Icon.svelte'
const { settings: { switcher_links }}: any = getContext('global')
let switcherEl: HTMLElement
let isOpen = false
/**
* Toggle switcher open state
*/
const toggleSwitcher = () => {
isOpen = !isOpen
// Record opening event
!isOpen && sendEvent('switcherOpen')
}
/**
* Detect outside click
*/
const windowClick = ({ target }) => {
if (!switcherEl.contains(target) && isOpen) {
// Close switcher
toggleSwitcher()
}
}
</script>
<svelte:window on:click={windowClick} />
<aside class="switcher" bind:this={switcherEl}
class:is-open={isOpen}
use:reveal={{
animation: { y: [24, 0], opacity: [0, 1] },
options: {
duration: 1,
delay: 0.6,
threshold: 0,
},
}}
>
<button class="switcher__button" title="{!isOpen ? 'Open' : 'Close'} menu" tabindex="0"
on:click={toggleSwitcher}
>
<span>
{#each Array(3) as _}
<i />
{/each}
</span>
</button>
<ul class="switcher__links" data-sveltekit-noscroll>
{#each switcher_links as { text, url, icon, icon_label }}
<li class:is-active={$page.url.pathname === url}>
<a href={url} on:click={toggleSwitcher} tabindex="0">
<Icon class="icon" icon={icon} label={icon_label} />
<span>{text}</span>
</a>
</li>
{/each}
</ul>
</aside>