refactor: migrate to Svelte 5

use runes ($props, $state, $derived, $effect, etc)
This commit is contained in:
2024-08-02 17:50:16 +02:00
parent 245049222b
commit 6f8a619af2
60 changed files with 1120 additions and 859 deletions

View File

@@ -45,7 +45,7 @@
"postcss-preset-env": "^9.6.0", "postcss-preset-env": "^9.6.0",
"postcss-sort-media-queries": "^5.2.0", "postcss-sort-media-queries": "^5.2.0",
"sass": "^1.77.8", "sass": "^1.77.8",
"svelte": "^4.2.18", "svelte": "^5.0.0-next.205",
"svelte-check": "^3.8.5", "svelte-check": "^3.8.5",
"svelte-preprocess": "^6.0.2", "svelte-preprocess": "^6.0.2",
"tslib": "^2.6.3", "tslib": "^2.6.3",

View File

@@ -1,6 +1,11 @@
<script lang="ts"> <script lang="ts">
export let domain: string let {
export let enabled = !import.meta.env.DEV domain,
enabled = !import.meta.env.DEV,
}: {
domain: string
enabled?: boolean
} = $props()
</script> </script>
<svelte:head> <svelte:head>

View File

@@ -4,13 +4,23 @@
const { settings }: any = getContext('global') const { settings }: any = getContext('global')
export let title: string let {
export let description: string = undefined title,
export let image: string = getAssetUrlKey(settings.seo_image.id, 'share-image') description,
export let url: string = undefined image = getAssetUrlKey(settings.seo_image.id, 'share-image'),
export let type = 'website' url,
export let card = 'summary_large_image' type = 'website',
export let creator: string = undefined card = 'summary_large_image',
creator,
}: {
title: string
description?: string
image?: string
url?: string
type?: string
card?: string
creator?: string
} = $props()
</script> </script>
<svelte:head> <svelte:head>

View File

@@ -1,11 +1,10 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'
import Lenis from 'lenis' import Lenis from 'lenis'
import { smoothScroll } from '$utils/stores' import { smoothScroll } from '$utils/stores'
let smoothScrollRAF = 0 let smoothScrollRAF = $state(0)
onMount(() => { $effect(() => {
// Setup smooth scroll // Setup smooth scroll
$smoothScroll = new Lenis({ $smoothScroll = new Lenis({
duration: 1.2, duration: 1.2,

View File

@@ -2,16 +2,23 @@
import { cx } from 'classix' import { cx } from 'classix'
import { splitText } from 'utils/text' import { splitText } from 'utils/text'
export let text: string let {
export let mode: string = undefined text,
export let clone = false mode,
clone = false,
...props
}: {
text: string
mode?: string
clone?: boolean
class?: string
} = $props()
$: split = splitText(text, mode) const split = $derived(splitText(text, mode))
const classes = $derived(cx(
$: classes = cx(
'text-split', 'text-split',
$$props.class, props.class,
) ))
</script> </script>
{#if clone} {#if clone}

View File

@@ -2,20 +2,31 @@
import { cx } from 'classix' import { cx } from 'classix'
import Image from './Image.svelte' import Image from './Image.svelte'
export let id: string let {
export let alt: string id,
export let disabled = false alt,
disabled = false,
...props
}: {
id: string
alt: string
disabled?: boolean
class?: string
} = $props()
let hovering = false let hovering = $state(false)
let timer: ReturnType<typeof setTimeout> | number = null let timer: ReturnType<typeof setTimeout>
$: classes = cx( const classes = $derived(cx(
hovering ? 'is-hovered' : undefined, hovering ? 'is-hovered' : undefined,
disabled ? 'is-disabled' : undefined, disabled ? 'is-disabled' : undefined,
$$props.class props.class,
) ))
// Hovering functions
/**
* Hovering functions
*/
const handleMouseEnter = () => { const handleMouseEnter = () => {
clearTimeout(timer) clearTimeout(timer)
hovering = true hovering = true
@@ -26,9 +37,10 @@
} }
</script> </script>
<figure class={classes} <figure
on:mouseenter={handleMouseEnter} class={classes}
on:mouseleave={handleMouseLeave} onmouseenter={handleMouseEnter}
onmouseleave={handleMouseLeave}
> >
<Image <Image
{id} {id}

View File

@@ -23,8 +23,13 @@
</style> </style>
<script lang="ts"> <script lang="ts">
export let text: string let {
export let size = 'small' text,
size = 'small',
}: {
text: string
size?: string
} = $props()
</script> </script>
<div class="badge badge--{size}"> <div class="badge badge--{size}">

View File

@@ -42,7 +42,7 @@
margin-left: 20px; margin-left: 20px;
color: $color-secondary-light; color: $color-secondary-light;
text-align: left; text-align: left;
font-weight: 300; font-weight: 400;
@include bp (sm) { @include bp (sm) {
margin-left: 0; margin-left: 0;

View File

@@ -5,10 +5,17 @@
<script lang="ts"> <script lang="ts">
import Icon from '$components/atoms/Icon.svelte' import Icon from '$components/atoms/Icon.svelte'
export let icon: string let {
export let alt: string icon,
export let label: string alt,
export let url: string label,
url,
}: {
icon: string
alt: string
label: string
url: string
} = $props()
</script> </script>
<a href={url} class="box-cta"> <a href={url} class="box-cta">

View File

@@ -6,56 +6,71 @@
import { cx } from 'classix' import { cx } from 'classix'
import SplitText from '$components/SplitText.svelte' import SplitText from '$components/SplitText.svelte'
export let text: string let {
export let url: string = undefined text,
export let color: string = undefined url,
export let size: 'xsmall' | 'small' | 'medium' | 'large' color,
export let effect = 'link-3d' size,
export let disabled: boolean = undefined effect = 'link-3d',
export let slotPosition = 'before' disabled,
slotPosition = 'before',
onclick,
children,
...props
}: {
text: string
url?: string
color?: string
size: 'xsmall' | 'small' | 'medium' | 'large'
effect?: string
disabled?: boolean
slotPosition?: 'before' | 'after'
onclick?: any
children?: any
class?: string
} = $props()
let tag: 'a' | 'button' const tag = $derived(url ? 'a' : 'button')
$: tag = url ? 'a' : 'button' const classes = $derived(cx(
$: classes = cx(
'button', 'button',
effect ? effect : undefined, effect,
...[color, size].map(variant => variant && `button--${variant}`), ...[color, size].map(variant => variant && `button--${variant}`),
Object.keys($$slots).length !== 0 ? `has-icon-${slotPosition}` : undefined, children && `has-icon-${slotPosition}`,
$$props.class, props.class,
) ))
// Define external links // Define external links
$: isExternal = /^(http|https):\/\//i.test(url) const isExternal = $derived(/^(http|https):\/\//i.test(url))
$: isProtocol = /^(mailto|tel):/i.test(url) const isProtocol = $derived(/^(mailto|tel):/i.test(url))
$: rel = isExternal ? 'external noopener' : null const rel = $derived(isExternal ? 'external noopener' : null)
$: target = isExternal ? '_blank' : null const target = $derived(isExternal ? '_blank' : null)
</script> </script>
{#if tag === 'button'} {#if tag === 'button'}
<button class={classes} tabindex="0" {disabled} on:click> <button class={classes} tabindex="0" {disabled} {onclick}>
{#if slotPosition === 'before'} {#if children && slotPosition === 'before'}
<slot /> {@render children()}
{/if} {/if}
<SplitText {text} clone={true} /> <SplitText {text} clone={true} />
{#if slotPosition === 'after'} {#if children && slotPosition === 'after'}
<slot /> {@render children()}
{/if} {/if}
</button> </button>
{:else if tag === 'a'} {:else if tag === 'a'}
<a <a
href={url} class={classes} href={url}
class={classes}
{target} {rel} {target} {rel}
data-sveltekit-noscroll={isExternal || isProtocol ? 'off' : ''} data-sveltekit-noscroll={isExternal || isProtocol ? 'off' : ''}
tabindex="0" tabindex="0"
on:click {onclick}
> >
{#if slotPosition === 'before'} {#if children && slotPosition === 'before'}
<slot /> {@render children()}
{/if} {/if}
<SplitText {text} clone={true} /> <SplitText {text} clone={true} />
{#if slotPosition === 'after'} {#if children && slotPosition === 'after'}
<slot /> {@render children()}
{/if} {/if}
</a> </a>
{/if} {/if}

View File

@@ -18,7 +18,7 @@
</script> </script>
<div class="button-cart"> <div class="button-cart">
<ButtonCircle color="purple" on:click={openCart}> <ButtonCircle color="purple" onclick={openCart}>
<Icon icon="bag" label="Cart icon" /> <Icon icon="bag" label="Cart icon" />
{#if $cartAmount > 0} {#if $cartAmount > 0}
<span class="quantity" transition:scale={{ start: 0.6, duration: 400, easing: quartOut }}>{$cartAmount}</span> <span class="quantity" transition:scale={{ start: 0.6, duration: 400, easing: quartOut }}>{$cartAmount}</span>

View File

@@ -5,42 +5,58 @@
<script lang="ts"> <script lang="ts">
import { cx } from 'classix' import { cx } from 'classix'
export let tag = 'button' let {
export let url: string = undefined tag = 'button',
export let color: string = undefined url,
export let size: string = undefined color,
export let type: 'button' | 'reset' | 'submit' = undefined size,
export let clone = false type,
export let disabled: boolean = undefined clone = false,
export let label: string = undefined disabled,
label,
children,
onclick,
...props
}: {
tag?: string
url?: string
color?: string
size?: string
type?: 'button' | 'reset' | 'submit'
clone?: boolean
disabled?: boolean
label?: string
children?: any
onclick?: any
class?: string
} = $props()
const className = 'button-circle' const buttonClass = 'button-circle'
$: classes = cx( const classes = $derived(cx(
className, buttonClass,
...[color, size].map(variant => variant && `${className}--${variant}`), ...[color, size].map(variant => variant && `${buttonClass}--${variant}`),
clone ? 'has-clone' : null, clone ? 'has-clone' : null,
$$props.class props.class,
) ))
</script> </script>
{#if tag === 'a'} {#snippet content()}
<a href={url} class={classes} tabindex="0" aria-label={label} on:click>
{#if clone} {#if clone}
{#each Array(2) as _} {#each Array(2) as _}
<slot /> {@render children()}
{/each} {/each}
{:else} {:else}
<slot /> {@render children()}
{/if} {/if}
{/snippet}
{#if tag === 'a'}
<a href={url} class={classes} tabindex="0" aria-label={label} {onclick}>
{@render content()}
</a> </a>
{:else} {:else}
<button {type} class={classes} disabled={disabled} tabindex="0" aria-label={label} on:click> <button {type} class={classes} disabled={disabled} tabindex="0" aria-label={label} {onclick}>
{#if clone} {@render content()}
{#each Array(2) as _}
<slot />
{/each}
{:else}
<slot />
{/if}
</button> </button>
{/if} {/if}

View File

@@ -1,12 +1,15 @@
<script lang="ts"> <script lang="ts">
import { cx } from 'classix' let {
icon,
export let icon: string label,
export let label: string = undefined ...props
}: {
$: classes = cx($$props.class) icon: string
label?: string
class?: string
} = $props()
</script> </script>
<svg class={classes} aria-label={label} width="32" height="32"> <svg class={props.class} aria-label={label} width="32" height="32">
<use xlink:href="#icon-{icon}" /> <use xlink:href="#icon-{icon}" />
</svg> </svg>

View File

@@ -18,11 +18,17 @@
</style> </style>
<script lang="ts"> <script lang="ts">
export let color: string = undefined let {
export let flip = false color,
flip = false,
}: {
color?: string
flip?: boolean
} = $props()
</script> </script>
<svg width="12" height="14" <svg
width="12" height="14"
class="arrow arrow--{color}" class="arrow arrow--{color}"
class:arrow--flip={flip} class:arrow--flip={flip}
> >

View File

@@ -9,12 +9,18 @@
<script lang="ts"> <script lang="ts">
import { cx } from 'classix' import { cx } from 'classix'
export let animate = false let {
animate = false,
...props
}: {
animate?: boolean
class?: string
} = $props()
$: classes = cx( const classes = $derived(cx(
'icon-earth', 'icon-earth',
$$props.class, props.class,
) ))
</script> </script>
<svg class={classes} width="48" height="48" viewBox="0 0 48 48" fill="none"> <svg class={classes} width="48" height="48" viewBox="0 0 48 48" fill="none">

View File

@@ -1,16 +1,31 @@
<script lang="ts"> <script lang="ts">
import { getAssetUrlKey } from '$utils/api' import { getAssetUrlKey } from '$utils/api'
export let src: string = undefined let {
export let id: string = undefined src,
export let sizeKey: string = undefined id,
export let sizes: Sizes = undefined sizeKey,
export let width: number = sizes?.medium?.width sizes,
export let height: number = sizes?.medium?.height width = sizes?.medium?.width,
export let ratio: number = undefined height = sizes?.medium?.height,
export let alt: string ratio,
export let lazy = true alt,
export let decoding: 'auto' | 'sync' | 'async' = 'auto' lazy = true,
decoding,
...props
}: {
src?: string
id?: string
sizeKey?: string
sizes?: Sizes
width?: number
height?: number
ratio?: number
alt: string
lazy?: boolean
decoding?: 'auto' | 'sync' | 'async'
class?: string
} = $props()
interface Sizes { interface Sizes {
small?: { width?: number; height?: number } small?: { width?: number; height?: number }
@@ -29,19 +44,19 @@
} }
} }
$: imgWidth = sizes?.small?.width || width const imgWidth = $derived(sizes?.small?.width || width)
$: imgHeight = sizes?.small?.height || height const imgHeight = $derived(sizes?.small?.height || height)
$: imgSrc = id ? getAssetUrlKey(id, `${sizeKey}-small`) : src const imgSrc = $derived(id ? getAssetUrlKey(id, `${sizeKey}-small`) : src)
$: srcSet = sizes const srcSet = $derived(
? [ sizes ? [
`${getAssetUrlKey(id, `${sizeKey}-small`)} 345w`, `${getAssetUrlKey(id, `${sizeKey}-small`)} 345w`,
sizes.medium && `${getAssetUrlKey(id, `${sizeKey}-medium`)} 768w`, sizes.medium && `${getAssetUrlKey(id, `${sizeKey}-medium`)} 768w`,
sizes.large && `${getAssetUrlKey(id, `${sizeKey}-large`)} 1280w`, sizes.large && `${getAssetUrlKey(id, `${sizeKey}-large`)} 1280w`,
] ] : [getAssetUrlKey(id, sizeKey)]
: [getAssetUrlKey(id, sizeKey)] )
</script> </script>
<picture class={$$props.class}> <picture class={props.class}>
<img <img
src={imgSrc} src={imgSrc}
sizes={sizes ? '(min-width: 1200px) 864px, (min-width: 992px) 708px, (min-width: 768px) 540px, 100%' : undefined} sizes={sizes ? '(min-width: 1200px) 864px, (min-width: 992px) 708px, (min-width: 768px) 540px, 100%' : undefined}

View File

@@ -12,39 +12,53 @@
import { map } from 'utils/math' import { map } from 'utils/math'
import reveal from '$animations/reveal' import reveal from '$animations/reveal'
export let tag: string let {
export let label: string = undefined tag,
export let parallax: number = undefined label,
export let offsetStart: number = undefined parallax,
export let offsetEnd: number = undefined offsetStart,
export let animate = true offsetEnd,
animate = true,
children,
...props
}: {
tag: string
label?: string
parallax?: number
offsetStart?: number
offsetEnd?: number
animate?: boolean
class?: string
children?: any
} = $props()
let scrollY: number let scrollY = $state<number>()
let innerWidth: number let innerWidth = $state<number>()
let innerHeight: number let innerHeight = $state<number>()
let titleEl: HTMLElement let titleEl = $state<HTMLElement>()
let isLarger: boolean
// Check if title is larger than viewport to translate it
const isLarger = $derived<boolean>(titleEl && titleEl.offsetWidth >= innerWidth)
$effect(() => {
// Define default values // Define default values
$: if (titleEl && !offsetStart && !offsetEnd) { if (titleEl && !offsetStart && !offsetEnd) {
offsetStart = titleEl.offsetTop - innerHeight * (innerWidth < 768 ? 0.2 : 0.75) offsetStart = titleEl.offsetTop - innerHeight * (innerWidth < 768 ? 0.2 : 0.75)
offsetEnd = titleEl.offsetTop + innerHeight * (innerWidth < 768 ? 0.5 : 0.5) offsetEnd = titleEl.offsetTop + innerHeight * (innerWidth < 768 ? 0.5 : 0.5)
} }
// Check if title is larger than viewport to translate it
$: isLarger = titleEl && titleEl.offsetWidth >= innerWidth
// Calculate the parallax value // Calculate the parallax value
$: if (titleEl) { if (titleEl) {
const toTranslate = 100 - (innerWidth / titleEl.offsetWidth * 100) const toTranslate = 100 - (innerWidth / titleEl.offsetWidth * 100)
parallax = isLarger ? map(scrollY, offsetStart, offsetEnd, 0, -toTranslate, true) : 0 parallax = isLarger ? map(scrollY, offsetStart, offsetEnd, 0, -toTranslate, true) : 0
} }
})
$: classes = cx( const classes = $derived(cx(
'scrolling-title', 'scrolling-title',
'title-huge', 'title-huge',
$$props.class props.class,
) ))
const revealOptions = animate ? { const revealOptions = animate ? {
children: '.char', children: '.char',
@@ -65,9 +79,10 @@
<svelte:element this={tag} <svelte:element this={tag}
bind:this={titleEl} bind:this={titleEl}
class={classes} aria-label={label} class={classes}
aria-label={label}
style:--parallax-x="{parallax}%" style:--parallax-x="{parallax}%"
use:reveal={revealOptions} use:reveal={revealOptions}
> >
<slot /> {@render children()}
</svelte:element> </svelte:element>

View File

@@ -7,8 +7,13 @@
import reveal from '$animations/reveal' import reveal from '$animations/reveal'
import { DURATION } from '$utils/constants' import { DURATION } from '$utils/constants'
export let variant = 'lines' let {
export let tag = 'h1' variant = 'lines',
tag = 'h1',
}: {
variant?: 'inline' | 'lines'
tag?: string
} = $props()
</script> </script>
{#if tag === 'h1'} {#if tag === 'h1'}

View File

@@ -12,20 +12,16 @@
import ScrollingTitle from '$components/atoms/ScrollingTitle.svelte' import ScrollingTitle from '$components/atoms/ScrollingTitle.svelte'
import Carousel from '$components/organisms/Carousel/Carousel.svelte' import Carousel from '$components/organisms/Carousel/Carousel.svelte'
export let product: any let {
export let shopProduct: any product,
shopProduct,
}: {
product: any
shopProduct: any
} = $props()
$: hasStock = shopProduct.stock_level > 0 const hasStock = $derived(shopProduct.stock_level > 0)
const lastPreviewPhoto = $derived<any>(product?.photos_preview[product.photos_preview.length - 1]?.directus_files_id)
/**
* Preview photos specs
*/
let lastPreviewPhoto: any = undefined
$: if (product && product.photos_preview.length) {
lastPreviewPhoto = product.photos_preview[product.photos_preview.length - 1].directus_files_id
}
// Images sizes // Images sizes
const photosPreview = [ const photosPreview = [
@@ -82,7 +78,7 @@
text={hasStock ? 'Add to cart' : 'Sold out'} text={hasStock ? 'Add to cart' : 'Sold out'}
color="pinklight" color="pinklight"
disabled={!hasStock} disabled={!hasStock}
on:click={() => addToCart(shopProduct)} onclick={() => addToCart(shopProduct)}
/> />
</div> </div>

View File

@@ -3,29 +3,28 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'
// Components
import ButtonCircle from '$components/atoms/ButtonCircle/ButtonCircle.svelte' import ButtonCircle from '$components/atoms/ButtonCircle/ButtonCircle.svelte'
import Select from '$components/molecules/Select.svelte' import Select from '$components/molecules/Select.svelte'
export let item: any let {
item,
quantityLimit = 5,
onUpdatedQuantity,
onremoved,
}: {
item: any
quantityLimit?: number
onUpdatedQuantity?: any
onremoved?: any
} = $props()
const dispatch = createEventDispatcher() /** Handle item quantity change */
const quantityLimit = 5 const updateQuantity = (value: string) => {
onUpdatedQuantity({
// When changing item quantity
const updateQuantity = ({ detail }: any) => {
dispatch('updatedQuantity', {
id: item.id, id: item.id,
quantity: Number(detail) quantity: Number(value)
}) })
} }
// When removing item
const removeItem = () => {
dispatch('removed', item.id)
}
</script> </script>
<div class="cart-item shadow-small"> <div class="cart-item shadow-small">
@@ -50,16 +49,17 @@
selected: index + 1 === item.quantity, selected: index + 1 === item.quantity,
} }
})} })}
on:change={updateQuantity} onchange={updateQuantity}
value={String(item.quantity)} value={String(item.quantity)}
> >
<span>Quantity:</span> <span>Quantity:</span>
</Select> </Select>
{/if} {/if}
<ButtonCircle class="remove" <ButtonCircle
class="remove"
size="tiny" color="gray" size="tiny" color="gray"
on:click={removeItem} onclick={() => onremoved(item.id)}
> >
<svg width="8" height="8"> <svg width="8" height="8">
<use xlink:href="#cross" /> <use xlink:href="#cross" />

View File

@@ -10,10 +10,10 @@
import IconArrow from '$components/atoms/IconArrow.svelte' import IconArrow from '$components/atoms/IconArrow.svelte'
import ButtonCircle from '$components/atoms/ButtonCircle/ButtonCircle.svelte' import ButtonCircle from '$components/atoms/ButtonCircle/ButtonCircle.svelte'
export let past = false let { past = false }: { past?: boolean } = $props()
let inputInFocus = false let inputInFocus = $state(false)
let formStatus: FormStatus = null let formStatus = $state<FormStatus>()
let formMessageTimeout: ReturnType<typeof setTimeout> | number let formMessageTimeout: ReturnType<typeof setTimeout> | number
interface FormStatus { interface FormStatus {
@@ -27,13 +27,15 @@
INVALID_EMAIL: `Woops. This email doesn't seem to be valid.`, INVALID_EMAIL: `Woops. This email doesn't seem to be valid.`,
} }
$: isSuccess = formStatus && formStatus.success const isSuccess = $derived(formStatus && formStatus.success)
// Toggle input focus // Toggle input focus
const toggleFocus = () => inputInFocus = !inputInFocus const toggleFocus = () => inputInFocus = !inputInFocus
// Handle form submission // Handle form submission
async function handleForm (event: Event | HTMLFormElement) { async function handleForm (event: Event | HTMLFormElement) {
event.preventDefault()
const data = new FormData(this) const data = new FormData(this)
const email = data.get('email') const email = data.get('email')
@@ -61,13 +63,16 @@
<div class="newsletter-form"> <div class="newsletter-form">
{#if !isSuccess} {#if !isSuccess}
<form method="POST" action="/api/newsletter" on:submit|preventDefault={handleForm} <form
method="POST"
action="/api/newsletter"
onsubmit={handleForm}
out:fly|local={{ y: -8, easing: quartOut, duration: 600 }} out:fly|local={{ y: -8, easing: quartOut, duration: 600 }}
> >
<div class="email" class:is-focused={inputInFocus}> <div class="email" class:is-focused={inputInFocus}>
<input type="email" placeholder="Your email address" name="email" id="newsletter_email" required <input type="email" placeholder="Your email address" name="email" id="newsletter_email" required
on:focus={toggleFocus} onfocus={toggleFocus}
on:blur={toggleFocus} onblur={toggleFocus}
> >
<ButtonCircle <ButtonCircle
type="submit" type="submit"
@@ -94,7 +99,8 @@
{/if} {/if}
{#if formStatus && formStatus.message} {#if formStatus && formStatus.message}
<div class="message shadow-small" <div
class="message shadow-small"
class:is-error={!isSuccess} class:is-error={!isSuccess}
class:is-success={isSuccess} class:is-success={isSuccess}
in:fly|local={{ y: 8, easing: quartOut, duration: 600, delay: isSuccess ? 600 : 0 }} in:fly|local={{ y: 8, easing: quartOut, duration: 600, delay: isSuccess ? 600 : 0 }}

View File

@@ -5,7 +5,7 @@
<script lang="ts"> <script lang="ts">
import SiteTitle from '$components/atoms/SiteTitle/SiteTitle.svelte' import SiteTitle from '$components/atoms/SiteTitle/SiteTitle.svelte'
export let text: string let { text }: { text: string } = $props()
</script> </script>
<section class="heading"> <section class="heading">

View File

@@ -8,15 +8,27 @@
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
import Icon from '$components/atoms/Icon.svelte' import Icon from '$components/atoms/Icon.svelte'
export let url: string let {
export let photoId: string url,
export let photoAlt: string photoId,
export let title: string photoAlt,
export let index: string title,
export let ratio: number index,
export let date: string = undefined ratio,
export let city: string = undefined date,
export let location: string city,
location,
}: {
url: string
photoId: string
photoAlt: string
title: string
index: string
ratio: number
date?: string
city?: string
location: string
} = $props()
</script> </script>
<div class="house grid"> <div class="house grid">

View File

@@ -3,31 +3,37 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { PUBLIC_PREVIEW_COUNT } from '$env/static/public'
import { getContext } from 'svelte' import { getContext } from 'svelte'
import { spring } from 'svelte/motion' import { spring } from 'svelte/motion'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { cx } from 'classix' import { cx } from 'classix'
import { lerp } from 'utils/math' import { lerp } from 'utils/math'
import { PUBLIC_PREVIEW_COUNT } from '$env/static/public'
import { seenLocations } from '$utils/stores' import { seenLocations } from '$utils/stores'
// Components // Components
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
import Badge from '$components/atoms/Badge.svelte' import Badge from '$components/atoms/Badge.svelte'
export let location: any let {
export let latestPhoto: any location,
latestPhoto,
}: {
location: any
latestPhoto: any
} = $props()
const { settings }: any = getContext('global') const { settings }: any = getContext('global')
let locationEl: HTMLElement let locationEl = $state<HTMLElement>()
let photoIndex = 0 let photoIndex = $state(0)
// Location date limit // Location date limit
let isNew = false let isNew = $state(false)
const dateNowOffset = dayjs().subtract(settings.limit_new, 'day') const dateNowOffset = dayjs().subtract(settings.limit_new, 'day')
const parsedSeenLocations = JSON.parse($seenLocations) const parsedSeenLocations = JSON.parse($seenLocations)
$: if (latestPhoto) { $effect(() => {
if (latestPhoto) {
const dateUpdated = dayjs(latestPhoto.date_created) const dateUpdated = dayjs(latestPhoto.date_created)
// Detect if location has new content // Detect if location has new content
@@ -43,6 +49,7 @@
isNew = dateUpdated.isAfter(dateNowOffset) isNew = dateUpdated.isAfter(dateNowOffset)
} }
} }
})
/** /**
@@ -66,7 +73,7 @@
photoIndex = Math.round(lerp(0, Number(PUBLIC_PREVIEW_COUNT) - 1, moveProgress)) photoIndex = Math.round(lerp(0, Number(PUBLIC_PREVIEW_COUNT) - 1, moveProgress))
} }
// Leaving mouseover /** Leaving mouseover */
const handleMouseLeave = () => { const handleMouseLeave = () => {
offset.update($c => ({ offset.update($c => ({
x: $c.x, x: $c.x,
@@ -80,9 +87,10 @@
style:--offset-y="{$offset.y}px" style:--offset-y="{$offset.y}px"
style:--rotate="{$offset.x * 0.125}deg" style:--rotate="{$offset.x * 0.125}deg"
> >
<a href="/{location.country.slug}/{location.slug}" <a
on:mousemove={handleMouseMove} href="/{location.country.slug}/{location.slug}"
on:mouseleave={handleMouseLeave} onmousemove={handleMouseMove}
onmouseleave={handleMouseLeave}
tabindex="0" tabindex="0"
> >
<Image <Image

View File

@@ -6,12 +6,21 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
export let title: string let {
export let issue: number title,
export let date: string issue,
export let link: string date,
export let thumbnail: { id: string } link,
export let size: string = undefined thumbnail,
size,
}: {
title: string
issue: number
date: string
link: string
thumbnail: { id: string }
size?: string
} = $props()
</script> </script>
<div class="issue" class:is-large={size === 'large'}> <div class="issue" class:is-large={size === 'large'}>

View File

@@ -7,9 +7,15 @@
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
import { cartOpen } from '$utils/stores/shop' import { cartOpen } from '$utils/stores/shop'
export let title: string let {
export let name: string image,
export let image: string name,
title,
}: {
image: string
name: string
title: string
} = $props()
const closeNotification = () => { const closeNotification = () => {
@@ -18,10 +24,10 @@
} }
</script> </script>
<div class="notification-cart shadow-small" <div
class="notification-cart shadow-small"
transition:fly={{ y: 20, duration: 700, easing: quartOut }} transition:fly={{ y: 20, duration: 700, easing: quartOut }}
on:click={closeNotification} onclick={closeNotification}
on:keydown
role="presentation" role="presentation"
> >
<div class="left"> <div class="left">

View File

@@ -3,23 +3,32 @@
</style> </style>
<script lang="ts"> <script lang="ts">
export let ended = false let {
export let current: number ended = false,
export let total: number current,
total,
onclick,
children,
}: {
ended?: boolean
current: number
total: number
onclick?: any
children?: any
} = $props()
</script> </script>
<div <div
class="pagination" class="pagination"
class:is-disabled={ended ? ended : undefined} class:is-disabled={ended ? ended : undefined}
role="button" tabindex="0" role="presentation"
on:click {onclick}
on:keydown
> >
<div class="pagination__progress"> <div class="pagination__progress">
<span class="current">{current}</span> <span class="current">{current}</span>
<span>/</span> <span>/</span>
<span class="total">{total}</span> <span class="total">{total}</span>
<slot /> {@render children()}
</div> </div>
</div> </div>

View File

@@ -3,38 +3,38 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
export let id: string let {
export let alt: string id,
export let url: string = undefined alt,
export let title: string = undefined url,
export let location: any = undefined title,
export let city: string = undefined location,
export let hovered = false city,
export let lazy = true hovered = false,
lazy = true,
onhover,
}: {
id: string
alt: string
url?: string
title?: string
location?: any
city?: string
hovered?: boolean
lazy?: boolean
onhover?: any
} = $props()
const dispatch = createEventDispatcher()
const sizes = { const sizes = {
small: { width: 224 }, small: { width: 224 },
medium: { width: 464 }, medium: { width: 464 },
large: { width: 864 }, large: { width: 864 },
} }
const sendHover = (hover: boolean) => dispatch('hover', hover)
</script> </script>
<div class="photo-card" {#snippet image()}
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 <Image
{id} {id}
sizeKey="postcard" sizeKey="postcard"
@@ -43,6 +43,22 @@
{alt} {alt}
{lazy} {lazy}
/> />
{/snippet}
<div
class="photo-card"
class:is-hovered={hovered}
onmouseenter={() => onhover(true)}
onfocus={() => onhover(true)}
onmouseout={() => onhover(false)}
onblur={() => onhover(false)}
role="presentation"
>
{#if url}
<div class="photo-card__content">
<a href={url} data-sveltekit-noscroll>
{@render image()}
{#if title && location} {#if title && location}
<div class="photo-card__info"> <div class="photo-card__info">
<Image <Image
@@ -58,13 +74,6 @@
</a> </a>
</div> </div>
{:else} {:else}
<Image {@render image()}
{id}
sizeKey="postcard"
{sizes}
ratio={1.5}
{alt}
{lazy}
/>
{/if} {/if}
</div> </div>

View File

@@ -6,19 +6,30 @@
import { cx } from 'classix' import { cx } from 'classix'
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
export let street: string let {
export let location: string street,
export let region: string = undefined location,
export let country: string region,
export let flagId: string country,
export let size: string = undefined flagId,
size,
...props
}: {
street: string
location: string
region?: string
country: string
flagId: string
size?: string
class?: string
} = $props()
const className = 'postcard' const cardClass = 'postcard'
$: classes = cx( const classes = $derived(cx(
className, cardClass,
...[size].map(variant => variant && `${className}--${variant}`), ...[size].map(variant => variant && `${cardClass}--${variant}`),
$$props.class props.class,
) ))
</script> </script>
<div class={classes}> <div class={classes}>

View File

@@ -9,15 +9,23 @@
import Button from '$components/atoms/Button/Button.svelte' import Button from '$components/atoms/Button/Button.svelte'
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
export let product: any let {
export let location: { name: string, slug: string } product,
export let image: any location,
image,
}: {
product: any
location: { name: string, slug: string }
image: any
} = $props()
</script> </script>
<div class="poster"> <div class="poster">
{#if image} {#if image}
<a href="/shop/poster-{location.slug}" data-sveltekit-noscroll <a
on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })} href="/shop/poster-{location.slug}"
onclick={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}
data-sveltekit-noscroll
> >
<Image <Image
id={image.id} id={image.id}
@@ -37,13 +45,13 @@
size="xsmall" size="xsmall"
url="/shop/poster-{location.slug}" url="/shop/poster-{location.slug}"
text="View" text="View"
on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })} onclick={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}
/> />
<Button <Button
size="xsmall" size="xsmall"
text="Add to cart" text="Add to cart"
color="pink" color="pink"
on:click={() => addToCart(product)} onclick={() => addToCart(product)}
/> />
</div> </div>
</div> </div>

View File

@@ -4,20 +4,28 @@
<script lang="ts"> <script lang="ts">
import { scaleFade } from '$animations/transitions' import { scaleFade } from '$animations/transitions'
// Components // Components
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
import { getAssetUrlKey } from '$utils/api' import { getAssetUrlKey } from '$utils/api'
export let index: number let {
export let text: string index,
export let image: any = undefined text,
export let video: any = undefined image,
video,
}: {
index: number
text: string
image?: any
video?: any
} = $props()
const imageRatio = image ? image.width / image.height : undefined const imageRatio = $derived(image ? image.width / image.height : undefined)
</script> </script>
<div class="step grid" style:--index={index} <div
class="step grid"
style:--index={index}
in:scaleFade|local={{ scale: [1.1, 1], opacity: [0, 1], x: [20, 0], delay: 0.2 }} 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] }} out:scaleFade|local={{ scale: [1, 0.9], opacity: [1, 0], x: [0, -20] }}
> >

View File

@@ -1,28 +1,36 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' let {
id,
interface Option { name,
options,
value,
onchange,
children,
}: {
id: string
name: string
options: {
value: string value: string
name: string name: string
default?: boolean default?: boolean
selected?: boolean selected?: boolean
} }[]
value?: string
onchange?: any
children?: any
} = $props()
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) const defaultOption = options.find(option => option.default)
let selected = value || options[0].value let selected = $state(value || options[0].value)
$: currentOption = options.find(option => option.value === selected) const currentOption = $derived(options.find(option => option.value === selected))
// Redefine value from parent (when reset) // Redefine value from parent (when reset)
$: if (value === defaultOption.value) { $effect(() => {
if (value === defaultOption.value) {
selected = defaultOption.value selected = defaultOption.value
} }
})
/** /**
@@ -31,17 +39,17 @@
const handleChange = ({ target: { value } }: any) => { const handleChange = ({ target: { value } }: any) => {
const option = options.find(option => option.value === value) const option = options.find(option => option.value === value)
// Dispatch event to parent // Send value to parent
dispatch('change', option.value) onchange(option.value)
} }
</script> </script>
<div class="select"> <div class="select">
<slot /> {@render children()}
<span>{currentOption.name}</span> <span>{currentOption.name}</span>
<select {name} {id} bind:value={selected} on:change={handleChange}> <select {name} {id} bind:value={selected} onchange={handleChange}>
{#each options as { value, name }} {#each options as { value, name }}
<option {value} selected={value === selected}> <option {value} selected={value === selected}>
{name} {name}

View File

@@ -9,18 +9,26 @@
import { shopCurrentProductSlug } from '$utils/stores/shop' import { shopCurrentProductSlug } from '$utils/stores/shop'
import { smoothScroll } from '$utils/stores' import { smoothScroll } from '$utils/stores'
export let isOver = false let {
isOver = false,
...props
}: {
isOver?: boolean
class?: string
} = $props()
const { shopLocations }: any = getContext('shop') const { shopLocations }: any = getContext('shop')
const classes = cx( const classes = $derived(cx(
'shop-locationswitcher', 'shop-locationswitcher',
isOver && 'is-over', isOver && 'is-over',
$$props.class props.class,
) ))
// Quick location change /**
* Quick location change
*/
const quickLocationChange = async ({ target: { value } }: any) => { const quickLocationChange = async ({ target: { value } }: any) => {
const pathTo = `/shop/poster-${value}` const pathTo = `/shop/poster-${value}`
goto(pathTo, { replaceState: true, noScroll: true, keepFocus: true }) goto(pathTo, { replaceState: true, noScroll: true, keepFocus: true })
@@ -37,7 +45,7 @@
<svg width="18" height="18"> <svg width="18" height="18">
<use xlink:href="#icon-map-pin" /> <use xlink:href="#icon-map-pin" />
</svg> </svg>
<select on:change={quickLocationChange}> <select onchange={quickLocationChange}>
{#each shopLocations as { name, slug }} {#each shopLocations as { name, slug }}
<option value={slug} selected={slug === $shopCurrentProductSlug}>{name}</option> <option value={slug} selected={slug === $shopCurrentProductSlug}>{name}</option>
{/each} {/each}

View File

@@ -12,8 +12,8 @@
const { settings: { switcher_links } }: any = getContext('global') const { settings: { switcher_links } }: any = getContext('global')
let switcherEl: HTMLElement let switcherEl = $state<HTMLElement>()
let isOpen = false let isOpen = $state(false)
/** /**
@@ -37,9 +37,11 @@
} }
</script> </script>
<svelte:window on:click={windowClick} /> <svelte:window onclick={windowClick} />
<aside class="switcher" bind:this={switcherEl} <aside
bind:this={switcherEl}
class="switcher"
class:is-open={isOpen} class:is-open={isOpen}
use:reveal={{ use:reveal={{
animation: { y: [24, 0], opacity: [0, 1] }, animation: { y: [24, 0], opacity: [0, 1] },
@@ -50,12 +52,15 @@
}, },
}} }}
> >
<button class="switcher__button" title="{!isOpen ? 'Open' : 'Close'} menu" tabindex="0" <button
on:click={toggleSwitcher} class="switcher__button"
title="{!isOpen ? 'Open' : 'Close'} menu"
tabindex="0"
onclick={toggleSwitcher}
> >
<span> <span>
{#each Array(3) as _} {#each Array(3) as _}
<i /> <i></i>
{/each} {/each}
</span> </span>
</button> </button>
@@ -63,7 +68,7 @@
<ul class="switcher__links" data-sveltekit-noscroll> <ul class="switcher__links" data-sveltekit-noscroll>
{#each switcher_links as { text, url, icon, icon_label }} {#each switcher_links as { text, url, icon, icon_label }}
<li class:is-active={$page.url.pathname === url}> <li class:is-active={$page.url.pathname === url}>
<a href={url} on:click={toggleSwitcher} tabindex="0"> <a href={url} onclick={toggleSwitcher} tabindex="0">
<Icon class="icon" icon={icon} label={icon_label} /> <Icon class="icon" icon={icon} label={icon_label} />
<span>{text}</span> <span>{text}</span>
</a> </a>

View File

@@ -3,7 +3,6 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'
import { fade, fly } from 'svelte/transition' import { fade, fly } from 'svelte/transition'
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
import { browser } from '$app/environment' import { browser } from '$app/environment'
@@ -12,25 +11,33 @@
import Button from '$components/atoms/Button/Button.svelte' import Button from '$components/atoms/Button/Button.svelte'
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
export let id: string let {
export let type: 'global' | 'local' id,
export let text: string type,
export let cta: { text,
cta,
images,
show = false,
loopDuration = 3000,
...props
}: {
id: string
type: 'global' | 'local'
text: string
cta?: {
label: string label: string
url: string url: string
color: string color: string
} = undefined
export let images: { id: string, title: string }[] = undefined
export let show = false
$: if (browser) {
show = !localStorage.getItem(`toast-${id}`)
} }
images?: { id: string, title: string }[]
show?: boolean
loopDuration?: number
class?: string
} = $props()
// Image rotation // Image rotation
let imagesLoop: ReturnType<typeof setTimeout> let imagesLoop: ReturnType<typeof setTimeout>
let currentImageIndex = 0 let currentImageIndex = $state(0)
const loopDuration = 3000
const incrementCurrentImageIndex = () => { const incrementCurrentImageIndex = () => {
currentImageIndex = currentImageIndex === images.length - 1 ? 0 : currentImageIndex + 1 currentImageIndex = currentImageIndex === images.length - 1 ? 0 : currentImageIndex + 1
@@ -43,15 +50,19 @@
show = false show = false
} }
$: classes = cx( const classes = $derived(cx(
'toast', 'toast',
`toast--${type}`, `toast--${type}`,
'shadow-small', 'shadow-small',
$$props.class, props.class,
) ))
onMount(() => { $effect(() => {
if (browser) {
show = !localStorage.getItem(`toast-${id}`)
}
if (images.length > 1) { if (images.length > 1) {
imagesLoop = setTimeout(incrementCurrentImageIndex, loopDuration) imagesLoop = setTimeout(incrementCurrentImageIndex, loopDuration)
} }
@@ -103,7 +114,7 @@
{/if} {/if}
</div> </div>
<button class="close" on:click={close} title="Close"> <button class="close" onclick={close} title="Close">
<svg width="10" height="10"> <svg width="10" height="10">
<use xlink:href="#cross" /> <use xlink:href="#cross" />
</svg> </svg>

View File

@@ -6,9 +6,15 @@
// Components // Components
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
export let title: string let {
export let image: any title,
export let back = false image,
back = false,
}: {
title: string
image: any
back?: boolean
} = $props()
</script> </script>
<section class="banner"> <section class="banner">

View File

@@ -3,20 +3,27 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import EmblaCarousel, { type EmblaCarouselType } from 'embla-carousel' import EmblaCarousel, { type EmblaCarouselType } from 'embla-carousel'
import cx from 'classix'
// Components // Components
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
export let slides: any let {
slides,
...props
}: {
slides: any[]
class?: string
} = $props()
let carouselEl: HTMLElement let carouselEl = $state<HTMLElement>()
let carousel: EmblaCarouselType let carousel: EmblaCarouselType
let currentSlide = 0 let currentSlide = $state(0)
let arrowDirection: string = null let arrowDirection = $state<'next' | 'prev'>()
$: isFirstSlide = currentSlide === 0
$: isLastSlide = currentSlide === slides.length - 1 const isFirstSlide = $derived(currentSlide === 0)
const isLastSlide = $derived(currentSlide === slides.length - 1)
/** Navigate to specific slide */ /** Navigate to specific slide */
@@ -65,7 +72,7 @@
} }
onMount(() => { $effect(() => {
// Init carousel // Init carousel
carousel = EmblaCarousel(carouselEl, { carousel = EmblaCarousel(carouselEl, {
loop: false loop: false
@@ -82,12 +89,13 @@
}) })
</script> </script>
<div class="carousel {$$props.class ?? ''}"> <div class={cx('carousel', props.class)}>
{#if slides.length} {#if slides.length}
<div class="carousel__viewport" bind:this={carouselEl} <div
on:mousemove={handleArrowMove} bind:this={carouselEl}
on:click={handleArrowClick} class="carousel__viewport"
on:keydown onmousemove={handleArrowMove}
onclick={handleArrowClick}
role="presentation" role="presentation"
> >
<div class="carousel__slides"> <div class="carousel__slides">
@@ -111,15 +119,16 @@
<ul class="carousel__dots"> <ul class="carousel__dots">
{#each slides as _, index} {#each slides as _, index}
<li class:is-active={index === currentSlide}> <li class:is-active={index === currentSlide}>
<button on:click={() => goToSlide(index)} aria-label="Go to slide #{index + 1}" /> <button onclick={() => goToSlide(index)} aria-label="Go to slide #{index + 1}"></button>
</li> </li>
{/each} {/each}
</ul> </ul>
<span class="carousel__arrow" <span
class="carousel__arrow"
class:is-flipped={arrowDirection === 'prev' && !isFirstSlide || isLastSlide}
style:--x="{$arrowPosition.x}px" style:--x="{$arrowPosition.x}px"
style:--y="{$arrowPosition.y}px" style:--y="{$arrowPosition.y}px"
class:is-flipped={arrowDirection === 'prev' && !isFirstSlide || isLastSlide}
> >
<svg width="29" height="32"> <svg width="29" height="32">
<use xlink:href="#arrow" /> <use xlink:href="#arrow" />

View File

@@ -3,8 +3,6 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment'
import { onMount } from 'svelte'
import { fade, fly } from 'svelte/transition' import { fade, fly } from 'svelte/transition'
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
import { smoothScroll } from '$utils/stores' import { smoothScroll } from '$utils/stores'
@@ -18,24 +16,12 @@
import ShopLocationSwitcher from '$components/molecules/ShopLocationSwitcher/ShopLocationSwitcher.svelte' import ShopLocationSwitcher from '$components/molecules/ShopLocationSwitcher/ShopLocationSwitcher.svelte'
// Block scroll if cart is open /** Closing the cart */
$: if (browser && $smoothScroll) {
if ($cartOpen) {
$smoothScroll.stop()
} else {
$smoothScroll.start()
}
document.documentElement.classList.toggle('block-scroll', $cartOpen)
}
// Closing the cart
const handleCloseCart = () => { const handleCloseCart = () => {
$cartOpen = false $cartOpen = false
} }
// Item quantity changed /** Item quantity changed */
const changedQuantity = async ({ detail: { id, quantity } }) => { const changedQuantity = async ({ detail: { id, quantity } }) => {
// Cart is now updating // Cart is now updating
$cartIsUpdating = true $cartIsUpdating = true
@@ -50,7 +36,7 @@
} }
} }
// Item removed /** Item removed */
const removedItem = async ({ detail: id }) => { const removedItem = async ({ detail: id }) => {
// Cart is now updating // Cart is now updating
$cartIsUpdating = true $cartIsUpdating = true
@@ -66,17 +52,29 @@
} }
onMount(async () => { $effect(() => {
// Init Swell // Init Swell
initSwell() initSwell()
// Fetch cart // Fetch cart
const cart = await getCart() getCart().then(cart => {
if (cart) {
// Store cart data // Store cart data
if (cart) {
$cartData = cart $cartData = cart
} }
}) })
// Block scroll if cart is open
if ($smoothScroll) {
if ($cartOpen) {
$smoothScroll.stop()
} else {
$smoothScroll.start()
}
document.documentElement.classList.toggle('block-scroll', $cartOpen)
}
})
</script> </script>
{#if $cartOpen} {#if $cartOpen}
@@ -84,21 +82,22 @@
<ShopLocationSwitcher isOver={true} /> <ShopLocationSwitcher isOver={true} />
</div> </div>
<aside class="cart shadow-box-dark" <aside
class="cart shadow-box-dark"
class:is-updating={$cartIsUpdating} class:is-updating={$cartIsUpdating}
transition:fly={{ x: 48, duration: 600, easing: quartOut }} transition:fly={{ x: 48, duration: 600, easing: quartOut }}
> >
<header class="cart__heading"> <header class="cart__heading">
<h2>Cart</h2> <h2>Cart</h2>
<button class="text-label" on:click={handleCloseCart}>Close</button> <button class="text-label" onclick={handleCloseCart}>Close</button>
</header> </header>
<div class="cart__content"> <div class="cart__content">
{#if $cartAmount > 0} {#if $cartAmount > 0}
{#each $cartData.items as item} {#each $cartData.items as item}
<CartItem {item} <CartItem {item}
on:updatedQuantity={changedQuantity} onUpdatedQuantity={changedQuantity}
on:removed={removedItem} onremoved={removedItem}
/> />
{/each} {/each}
{:else} {:else}
@@ -140,7 +139,7 @@
url={$cartData && $cartData.checkout_url} url={$cartData && $cartData.checkout_url}
text="Checkout" text="Checkout"
color="pink" color="pink"
on:click={() => sendEvent('cartCheckout', { props: { amount: $cartAmount } })} onclick={() => sendEvent('cartCheckout', { props: { amount: $cartAmount } })}
/> />
</div> </div>
{/if} {/if}
@@ -148,9 +147,10 @@
</footer> </footer>
</aside> </aside>
<div class="cart-overlay" <div
class="cart-overlay"
transition:fade={{ duration: 600, easing: quartOut }} transition:fade={{ duration: 600, easing: quartOut }}
on:click={handleCloseCart} onclick={handleCloseCart}
on:keydown role="presentation"
/> ></div>
{/if} {/if}

View File

@@ -5,9 +5,9 @@
<script lang="ts"> <script lang="ts">
import PhotoCard from '$components/molecules/PhotoCard/PhotoCard.svelte' import PhotoCard from '$components/molecules/PhotoCard/PhotoCard.svelte'
export let photos: any[] = [] let { photos }: { photos: any[] } = $props()
let hovered: number = null let hovered = $state<number>(null)
</script> </script>
{#if photos} {#if photos}
@@ -22,7 +22,7 @@
city={city} city={city}
hovered={hovered === index} hovered={hovered === index}
lazy={false} lazy={false}
on:hover={({ detail }) => hovered = detail ? index : null} onhover={(id: number) => hovered = id ? index : null}
/> />
{/each} {/each}
</div> </div>

View File

@@ -3,7 +3,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { getContext, onMount } from 'svelte' import { getContext } from 'svelte'
import { fade, fly as flySvelte } from 'svelte/transition' import { fade, fly as flySvelte } from 'svelte/transition'
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
import { Globe, type Marker } from '$modules/globe' import { Globe, type Marker } from '$modules/globe'
@@ -15,20 +15,31 @@
const isDev = import.meta.env.DEV const isDev = import.meta.env.DEV
export let type: string = undefined let {
export let autoRotate = true type,
export let enableMarkers = true autoRotate = true,
export let enableMarkersLinks = true enableMarkers = true,
export let speed = 0.1 enableMarkersLinks = true,
export let pane: boolean = isDev speed = 0.1,
export let width: number = undefined pane = isDev,
width,
}: {
type?: string
autoRotate?: boolean
enableMarkers?: boolean
enableMarkersLinks?: boolean
speed?: number
pane?: boolean
width?: number
} = $props()
let innerWidth: number let innerWidth = $state<number>()
let globeParentEl: HTMLElement, globeEl: HTMLElement let globeParentEl = $state<HTMLElement>()
let globe: any let globeEl = $state<HTMLElement>()
let globe = $state<any>()
let observer: IntersectionObserver let observer: IntersectionObserver
let animation: number let animation = $state<number>()
let hoveredMarker: { name: string, country: string } = null let hoveredMarker: { name: string, country: string } = $state()
const { continents, locations }: any = getContext('global') const { continents, locations }: any = getContext('global')
const randomContinent: any = getRandomItem(continents) const randomContinent: any = getRandomItem(continents)
@@ -41,7 +52,7 @@
})) }))
onMount(() => { $effect(() => {
const globeResolution = innerWidth > 1440 && window.devicePixelRatio > 1 ? 4 : 2 const globeResolution = innerWidth > 1440 && window.devicePixelRatio > 1 ? 4 : 2
globe = new Globe({ globe = new Globe({
@@ -92,51 +103,56 @@
}) })
/** /** Update rendering */
* Methods
*/
// Update
const update = () => { const update = () => {
animation = requestAnimationFrame(update) animation = requestAnimationFrame(update)
globe.render() globe.render()
} }
// Stop /** Stop rendering */
const stop = () => { const stop = () => {
cancelAnimationFrame(animation) cancelAnimationFrame(animation)
} }
// Resize /** Handle resize */
const resize = debounce(() => { const resize = debounce(() => {
globe.resize() globe.resize()
}, 100) }, 100)
// Destroy /** Destroy globe */
const destroy = () => { const destroy = () => {
stop() stop()
globe.destroy() globe.destroy()
} }
</script> </script>
<svelte:window bind:innerWidth <svelte:window
on:resize={resize} bind:innerWidth
onresize={resize}
/> />
<div class="globe" bind:this={globeParentEl} <div
bind:this={globeParentEl}
class="globe"
class:is-cropped={type === 'cropped'} class:is-cropped={type === 'cropped'}
style:--width={width ? `${width}px` : null} style:--width={width ? `${width}px` : null}
> >
<div class="globe__canvas" bind:this={globeEl} <div
bind:this={globeEl}
class="globe__canvas"
class:is-faded={hoveredMarker} class:is-faded={hoveredMarker}
> >
<ul class="globe__markers"> <ul class="globe__markers">
{#each markers as { name, slug, country, lat, lng }} {#each markers as { name, slug, country, lat, lng }}
<li class="globe__marker" data-location={slug} data-lat={lat} data-lng={lng}> <li class="globe__marker" data-location={slug} data-lat={lat} data-lng={lng}>
<a href="/{country.slug}/{slug}" aria-label={name} data-sveltekit-noscroll <a
on:mouseenter={() => hoveredMarker = { name, country: country.name }} href="/{country.slug}/{slug}"
on:mouseleave={() => hoveredMarker = null} aria-label={name}
data-sveltekit-noscroll
onmouseenter={() => hoveredMarker = { name, country: country.name }}
onmouseleave={() => hoveredMarker = null}
> >
<i /> <i></i>
<span>{name}</span> <span>{name}</span>
</a> </a>
</li> </li>
@@ -145,7 +161,8 @@
</div> </div>
{#if hoveredMarker} {#if hoveredMarker}
<div class="globe__location" <div
class="globe__location"
in:revealSplit={{ duration: 1 }} in:revealSplit={{ duration: 1 }}
out:fade={{ duration: 300, easing: quartOut }} out:fade={{ duration: 300, easing: quartOut }}
> >

View File

@@ -19,6 +19,10 @@
} }
</style> </style>
<script lang="ts">
let { children }: { children: any } = $props()
</script>
<ul class="list-cta" data-sveltekit-noscroll> <ul class="list-cta" data-sveltekit-noscroll>
<slot /> {@render children()}
</ul> </ul>

View File

@@ -14,14 +14,17 @@
import Button from '$components/atoms/Button/Button.svelte' import Button from '$components/atoms/Button/Button.svelte'
import Location from '$components/molecules/Location/Location.svelte' import Location from '$components/molecules/Location/Location.svelte'
export let locations: any[] let { locations }: { locations: any[] } = $props()
const { continents, settings: { explore_list } }: any = getContext('global') const { continents, settings: { explore_list } }: any = getContext('global')
// Continents filtering logic
let currentContinent: string = undefined
$: filteredLocations = locations.filter(({ country: { continent } }: any) => { /**
* Continents filtering logic
*/
let currentContinent = $state<string>()
const filteredLocations = $derived.by(() => {
return locations.filter(({ country: { continent } }: any) => {
if (!currentContinent) { if (!currentContinent) {
// Show all locations by default // Show all locations by default
return true return true
@@ -30,13 +33,13 @@
return continent.slug === currentContinent return continent.slug === currentContinent
} }
}) })
})
/** /* Filter locations from continent */
* Filter locations from continent
*/
const filterLocation = throttle((continent: string) => { const filterLocation = throttle((continent: string) => {
currentContinent = continent !== currentContinent ? continent : null currentContinent = continent !== currentContinent ? continent : null
sendEvent('filterContinent')
}, 600) }, 600)
</script> </script>
@@ -53,10 +56,7 @@
text={name} text={name}
slotPosition="after" slotPosition="after"
class={'is-disabled'} class={'is-disabled'}
on:click={() => { onclick={() => filterLocation(slug)}
filterLocation(slug)
sendEvent('filterContinent')
}}
> >
<svg width="12" height="12"> <svg width="12" height="12">
<use xlink:href="#cross" /> <use xlink:href="#cross" />

View File

@@ -7,7 +7,7 @@
// Components // Components
import EmailForm from '$components/molecules/EmailForm/EmailForm.svelte' import EmailForm from '$components/molecules/EmailForm/EmailForm.svelte'
export let theme = 'default' let { theme = 'default' }: { theme?: 'default' | 'light' } = $props()
const { settings: { newsletter_text, newsletter_subtitle } }: any = getContext('global') const { settings: { newsletter_text, newsletter_subtitle } }: any = getContext('global')
</script> </script>

View File

@@ -3,19 +3,23 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { getContext, onMount } from 'svelte' import { getContext } from 'svelte'
import EmblaCarousel, { type EmblaCarouselType } from 'embla-carousel' import EmblaCarousel, { type EmblaCarouselType } from 'embla-carousel'
// Components // Components
import Poster from '$components/molecules/Poster/Poster.svelte' import Poster from '$components/molecules/Poster/Poster.svelte'
import { debounce } from 'utils/actions' import { debounce } from 'utils/actions'
export let posters: any = [] let {
posters,
}: {
posters: any[]
} = $props()
let innerWidth: number let innerWidth = $state<number>()
let carouselEl: HTMLElement let carouselEl = $state<HTMLElement>()
let carousel: EmblaCarouselType let carousel = $state<EmblaCarouselType>()
let currentSlide = 0 let currentSlide = $state(0)
let carouselDots = [] let carouselDots = $state([])
const { shopProducts }: any = getContext('shop') const { shopProducts }: any = getContext('shop')
@@ -72,7 +76,7 @@
const handleResize = debounce(initCarousel, 200) const handleResize = debounce(initCarousel, 200)
onMount(() => { $effect(() => {
if (innerWidth < 1200) { if (innerWidth < 1200) {
initCarousel() initCarousel()
} }
@@ -88,7 +92,7 @@
<svelte:window <svelte:window
bind:innerWidth bind:innerWidth
on:resize={handleResize} onresize={handleResize}
/> />
<section class="shop-page__posters grid"> <section class="shop-page__posters grid">
@@ -110,7 +114,7 @@
<ul class="set__dots"> <ul class="set__dots">
{#each carouselDots as _, index} {#each carouselDots as _, index}
<li class:is-active={index === currentSlide}> <li class:is-active={index === currentSlide}>
<button on:click={() => goToSlide(index)} aria-label="Go to slide #{index + 1}" /> <button onclick={() => goToSlide(index)} aria-label="Go to slide #{index + 1}"></button>
</li> </li>
{/each} {/each}
</ul> </ul>

View File

@@ -4,7 +4,7 @@
<script lang="ts"> <script lang="ts">
import { navigating } from '$app/stores' import { navigating } from '$app/stores'
import { getContext, onMount } from 'svelte' import { getContext } from 'svelte'
import { stagger, timeline } from 'motion' import { stagger, timeline } from 'motion'
import { smoothScroll } from '$utils/stores' import { smoothScroll } from '$utils/stores'
import { cartOpen } from '$utils/stores/shop' import { cartOpen } from '$utils/stores/shop'
@@ -15,17 +15,18 @@
import ButtonCart from '$components/atoms/ButtonCart/ButtonCart.svelte' import ButtonCart from '$components/atoms/ButtonCart/ButtonCart.svelte'
import ShopLocationSwitcher from '$components/molecules/ShopLocationSwitcher/ShopLocationSwitcher.svelte' import ShopLocationSwitcher from '$components/molecules/ShopLocationSwitcher/ShopLocationSwitcher.svelte'
export let product: any = undefined let { product }: { product?: any } = $props()
const { shop, shopLocations }: any = getContext('shop') const { shop, shopLocations }: any = getContext('shop')
let innerWidth: number let innerWidth = $state<number>()
let navObserver: IntersectionObserver let navObserver: IntersectionObserver
let introEl: HTMLElement, navChooseEl: HTMLElement let introEl = $state<HTMLElement>()
let scrolledPastIntro = false let navChooseEl = $state<HTMLElement>()
let scrolledPastIntro = $state(false)
onMount(() => { $effect(() => {
// Reveal the nav past the Intro // Reveal the nav past the Intro
navObserver = new IntersectionObserver(entries => { navObserver = new IntersectionObserver(entries => {
entries.forEach(entry => { entries.forEach(entry => {
@@ -111,7 +112,7 @@
<section class="shop-banner" bind:this={introEl}> <section class="shop-banner" bind:this={introEl}>
<div class="top container"> <div class="top container">
<a href="/" class="back" data-sveltekit-noscroll> <a href="/" class="back" data-sveltekit-noscroll>
<svg width="5" height="8" viewBox="0 0 5 8" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="5" height="8" viewBox="0 0 5 8" fill="none">
<path d="M4 1 1 4l3 3" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/> <path d="M4 1 1 4l3 3" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
<span>Back to Houses Of</span> <span>Back to Houses Of</span>
@@ -129,7 +130,7 @@
<ul bind:this={navChooseEl} data-sveltekit-noscroll> <ul bind:this={navChooseEl} data-sveltekit-noscroll>
{#each shopLocations as { name, slug }} {#each shopLocations as { name, slug }}
<li class:is-active={product && slug === product.location.slug}> <li class:is-active={product && slug === product.location.slug}>
<a href="/shop/poster-{slug}" on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}> <a href="/shop/poster-{slug}" onclick={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}>
{name} {name}
</a> </a>
</li> </li>
@@ -155,7 +156,8 @@
/> />
</section> </section>
<nav class="shop-quicknav" <nav
class="shop-quicknav"
class:is-visible={scrolledPastIntro} class:is-visible={scrolledPastIntro}
class:is-overlaid={$cartOpen} class:is-overlaid={$cartOpen}
> >

View File

@@ -3,7 +3,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { getContext, onMount } from 'svelte' import { getContext } from 'svelte'
// Components // Components
import Button from '$components/atoms/Button/Button.svelte' import Button from '$components/atoms/Button/Button.svelte'
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
@@ -17,13 +17,25 @@
// Return name only // Return name only
.map((loc: Location) => loc.name) .map((loc: Location) => loc.name)
export let images: any[] = shop.module_images let {
export let title: string = shop.module_title images = shop.module_images,
export let text: string = shop.module_text title = shop.module_title,
export let textBottom: string = undefined text = shop.module_text,
export let buttonText = 'Shop' textBottom,
export let url = '/shop' buttonText = 'Shop',
export let enabled = true url = '/shop',
enabled = true,
loopDuration = 3000,
}: {
images?: any[]
title?: string
text?: string
textBottom?: string
buttonText?: string
url?: string
enabled?: boolean
loopDuration?: number
} = $props()
if (textBottom !== null) { if (textBottom !== null) {
textBottom = `Posters available for ${locationsWithPoster.join(', ').replace(/,(?!.*,)/gmi, ' and')}.` textBottom = `Posters available for ${locationsWithPoster.join(', ').replace(/,(?!.*,)/gmi, ' and')}.`
@@ -37,15 +49,14 @@
// Image rotation // Image rotation
let imagesLoop: ReturnType<typeof setTimeout> let imagesLoop: ReturnType<typeof setTimeout>
let currentImageIndex = 0 let currentImageIndex = $state(0)
const loopDuration = 3000
const incrementCurrentImageIndex = () => { const incrementCurrentImageIndex = () => {
currentImageIndex = currentImageIndex === images.length - 1 ? 0 : currentImageIndex + 1 currentImageIndex = currentImageIndex === images.length - 1 ? 0 : currentImageIndex + 1
imagesLoop = setTimeout(() => requestAnimationFrame(incrementCurrentImageIndex), loopDuration) imagesLoop = setTimeout(() => requestAnimationFrame(incrementCurrentImageIndex), loopDuration)
} }
onMount(() => { $effect(() => {
if (images.length > 1) { if (images.length > 1) {
imagesLoop = setTimeout(incrementCurrentImageIndex, loopDuration) imagesLoop = setTimeout(incrementCurrentImageIndex, loopDuration)
} }

View File

@@ -9,11 +9,14 @@
import Cart from '$components/organisms/Cart/Cart.svelte' import Cart from '$components/organisms/Cart/Cart.svelte'
import NotificationCart from '$components/molecules/NotificationCart/NotificationCart.svelte' import NotificationCart from '$components/molecules/NotificationCart/NotificationCart.svelte'
export let data let {
data,
children,
} = $props()
const { shop, locations, posters, shopProducts, settings } = data const { shop, locations, posters, shopProducts, settings } = data
let scrollY: number let scrollY = $state<number>()
// Locations with an existing poster product // Locations with an existing poster product
const shopLocations = locations.filter(({ slug }: any) => { const shopLocations = locations.filter(({ slug }: any) => {
@@ -38,12 +41,8 @@
<div class="notifications" class:is-top={scrollY <= 100}> <div class="notifications" class:is-top={scrollY <= 100}>
{#each $cartNotifications as { id, title, name, image } (id)} {#each $cartNotifications as { id, title, name, image } (id)}
<NotificationCart <NotificationCart {title} {name} {image} />
title={title}
name={name}
image={image}
/>
{/each} {/each}
</div> </div>
<slot /> {@render children()}

View File

@@ -8,7 +8,7 @@
import ShopHeader from '$components/organisms/ShopBanner/ShopBanner.svelte' import ShopHeader from '$components/organisms/ShopBanner/ShopBanner.svelte'
import PosterLayout from '$components/layouts/PosterLayout/PosterLayout.svelte' import PosterLayout from '$components/layouts/PosterLayout/PosterLayout.svelte'
export let data let { data } = $props()
const { product, shopProduct }: { product: any, shopProduct: any } = data const { product, shopProduct }: { product: any, shopProduct: any } = data
const { posters, settings }: any = getContext('shop') const { posters, settings }: any = getContext('shop')

View File

@@ -9,7 +9,7 @@
import PostersGrid from '$components/organisms/PostersGrid/PostersGrid.svelte' import PostersGrid from '$components/organisms/PostersGrid/PostersGrid.svelte'
import PosterLayout from '$components/layouts/PosterLayout/PosterLayout.svelte' import PosterLayout from '$components/layouts/PosterLayout/PosterLayout.svelte'
export let data let { data } = $props()
const { posters }: any = getContext('shop') const { posters }: any = getContext('shop')

View File

@@ -4,7 +4,6 @@
<script lang="ts"> <script lang="ts">
import { page, navigating } from '$app/stores' import { page, navigating } from '$app/stores'
import { onMount } from 'svelte'
import { stagger, timeline } from 'motion' import { stagger, timeline } from 'motion'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
@@ -24,34 +23,33 @@
import NewsletterModule from '$components/organisms/NewsletterModule/NewsletterModule.svelte' import NewsletterModule from '$components/organisms/NewsletterModule/NewsletterModule.svelte'
import ShopModule from '$components/organisms/ShopModule/ShopModule.svelte' import ShopModule from '$components/organisms/ShopModule/ShopModule.svelte'
export let data let { data } = $props()
let photos = $state<any[]>(data.photos)
let totalPhotos = $state(data.totalPhotos)
let { photos, totalPhotos }: { photos: any[], totalPhotos: number } = data
$: ({ photos, totalPhotos } = data)
const { location, product = undefined }: { location: any, totalPhotos: number, product: any } = data const { location, product = undefined }: { location: any, totalPhotos: number, product: any } = data
const { params } = $page const { params } = $page
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
let introEl: HTMLElement let introEl = $state<HTMLElement>()
let photosListEl: HTMLElement let photosListEl = $state<HTMLElement>()
let scrollY: number let scrollY = $state<number>()
let observerPhotos: IntersectionObserver let observerPhotos: IntersectionObserver
let mutationPhotos: MutationObserver let mutationPhotos: MutationObserver
let currentPage = 1 let currentPage = $state(1)
let ended: boolean let currentPhotosAmount = $derived(photos.length)
let currentPhotosAmount: number let heroOffsetY = $state(0)
let heroOffsetY = 0
$: latestPhoto = photos[0] const ended = $derived(currentPhotosAmount === totalPhotos)
$: currentPhotosAmount = photos.length const latestPhoto = $derived(photos[0])
$: ended = currentPhotosAmount === totalPhotos
/** /**
* Load photos * Load photos
*/ */
// Load more photos from CTA /** Load more photos from CTA */
const loadMorePhotos = async () => { const loadMorePhotos = async () => {
// Append more photos from API // Append more photos from API
const newPhotos: any = await loadPhotos(currentPage + 1) const newPhotos: any = await loadPhotos(currentPage + 1)
@@ -64,9 +62,6 @@
// Increment the current page // Increment the current page
currentPage++ currentPage++
} }
// Increment the currently visible amount of photos
currentPhotosAmount += newPhotos.length
} }
} }
@@ -102,12 +97,14 @@
/** /**
* Add parallax on illustration when scrolling * Add parallax on illustration when scrolling
*/ */
$: if (scrollY && scrollY < introEl.offsetHeight) { $effect(() => {
if (scrollY && scrollY < introEl.offsetHeight) {
heroOffsetY = scrollY * 0.1 heroOffsetY = scrollY * 0.1
} }
})
onMount(() => { $effect(() => {
// Define location's last seen state // Define location's last seen state
$seenLocations = JSON.stringify({ $seenLocations = JSON.stringify({
// Add existing values // Add existing values
@@ -264,9 +261,7 @@
</Button> </Button>
{#if location.has_poster} {#if location.has_poster}
<Button size="medium" url="/shop/poster-{location.slug}" text="Buy the poster" color="pinklight" class="shadow-small"> <Button size="medium" url="/shop/poster-{location.slug}" text="Buy the poster" color="pinklight" class="shadow-small" />
<!-- <IconEarth /> -->
</Button>
{/if} {/if}
</div> </div>
</div> </div>
@@ -309,7 +304,7 @@
ended={ended} ended={ended}
current={currentPhotosAmount} current={currentPhotosAmount}
total={totalPhotos} total={totalPhotos}
on:click={() => !ended && loadMorePhotos()} onclick={() => !ended && loadMorePhotos()}
> >
{#if !ended} {#if !ended}
<p class="more">See more photos</p> <p class="more">See more photos</p>

View File

@@ -3,10 +3,9 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment'
import { page, navigating } from '$app/stores' import { page, navigating } from '$app/stores'
import { goto } from '$app/navigation' import { goto, replaceState } from '$app/navigation'
import { onMount, tick } from 'svelte' import { tick } from 'svelte'
import { fade, scale } from 'svelte/transition' import { fade, scale } from 'svelte/transition'
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@@ -24,70 +23,74 @@
import IconArrow from '$components/atoms/IconArrow.svelte' import IconArrow from '$components/atoms/IconArrow.svelte'
import ButtonCircle from '$components/atoms/ButtonCircle/ButtonCircle.svelte' import ButtonCircle from '$components/atoms/ButtonCircle/ButtonCircle.svelte'
export let data let { data } = $props()
let { photos, currentIndex }: { photos: any[], currentIndex: number } = data
const { location, countPhotos, limit, offset }: { location: any, countPhotos: number, limit: number, offset: number } = data const { location, countPhotos, limit, offset }: { location: any, countPhotos: number, limit: number, offset: number } = data
enum directions { PREV, NEXT } enum directions { PREV, NEXT }
let innerWidth: number let innerWidth = $state<number>()
let fullscreenEl: HTMLElement let fullscreenEl = $state<HTMLElement>()
let globalOffset = offset let photos = $state<any[]>(data.photos)
let isLoading = false let currentIndex = $state(data.currentIndex)
let isFullscreen = false let globalOffset = $state(offset)
let hasNext = offset + limit < countPhotos let isLoading = $state(false)
let hasPrev = offset > 0 let isFullscreen = $state(false)
let hasNext = $state(offset + limit < countPhotos)
let hasPrev = $state(offset > 0)
// Define if we can navigate depending on loading state, existing photos and index being first or last // Define if we can navigate depending on loading state, existing photos and index being first or last
$: canGoPrev = !isLoading && (hasNext || currentIndex !== photos.length - 1) const canGoPrev = $derived(!isLoading && (hasNext || currentIndex !== photos.length - 1))
$: canGoNext = !isLoading && (hasPrev || currentIndex !== 0) const canGoNext = $derived(!isLoading && (hasPrev || currentIndex !== 0))
// Define current photo // Define current photo
$: currentPhoto = photos[currentIndex] const currentPhoto = $derived(photos[currentIndex])
$: currentPhotoIndex = globalOffset + currentIndex + 1 const currentPhotoIndex = $derived(globalOffset + currentIndex + 1)
// Take 7 photos in the global photos array (5 for current, 1 before first and 1 after last) // Take 7 photos in the global photos array (5 for current, 1 before first and 1 after last)
// Start one index before the current image since the first one will be invisible // Start one index before the current image since the first one will be invisible
$: sliceStart = Math.max(currentIndex - 1, 0) const sliceStart = $derived(Math.max(currentIndex - 1, 0))
$: visiblePhotos = photos.slice(sliceStart, sliceStart + 7) const visiblePhotos = $derived(photos.slice(sliceStart, sliceStart + 7))
$effect(() => {
// Load previous photos // Load previous photos
$: if (browser && currentIndex === 0 && hasPrev) { if (currentIndex === 0 && hasPrev) {
loadPhotos(photos[0].id) loadPhotos(photos[0].id)
} }
// Load next photos // Load next photos
$: if (browser && currentIndex === photos.length - 5 && hasNext) { if (currentIndex === photos.length - 5 && hasNext) {
loadPhotos(photos[photos.length - 1].id, directions.NEXT) loadPhotos(photos[photos.length - 1].id, directions.NEXT)
} }
// Change URL to current photo slug // Change URL to current photo slug
$: if (browser && currentPhoto) { if (currentPhoto) {
window.history.replaceState(null, '', $page.url.pathname.replace($page.params.photo, currentPhoto.slug)) replaceState('', $page.url.pathname.replace($page.params.photo, currentPhoto.slug))
} }
})
// Define previous URL // Define previous URL
$: previousUrl = $previousPage ? $previousPage : `/${location.country.slug}/${location.slug}` const previousUrl = $derived($previousPage ? $previousPage : `/${location.country.slug}/${location.slug}`)
/** /**
* Photo navigation * Photo navigation
*/ */
// Go to next photo /** Go to next photo */
const goToNext = throttle(() => { const goToNext = throttle(() => {
canGoPrev && currentIndex++ canGoPrev && currentIndex++
}, 200) }, 200)
// Go to previous photo /** Go to previous photo */
const goToPrevious = throttle(() => { const goToPrevious = throttle(() => {
canGoNext && (currentIndex = Math.max(currentIndex - 1, 0)) canGoNext && (currentIndex = Math.max(currentIndex - 1, 0))
}, 200) }, 200)
// Close viewer and go to previous page /** Close viewer and go to previous page */
const closeViewer = () => { const closeViewer = () => {
goto(previousUrl, { replaceState: false, noScroll: true, keepFocus: true }) goto(previousUrl, { replaceState: false, noScroll: true, keepFocus: true })
} }
// Enable navigation with keyboard /** Enable navigation with keyboard */
const handleKeydown = ({ key, defaultPrevented }: KeyboardEvent) => { const handleKeydown = ({ key, defaultPrevented }: KeyboardEvent) => {
if (defaultPrevented) return if (defaultPrevented) return
switch (key) { switch (key) {
@@ -98,7 +101,7 @@
} }
} }
// Enable swipe gestures /** Enable swipe gestures */
const handleSwipe = ({ detail }: CustomEvent<string>) => { const handleSwipe = ({ detail }: CustomEvent<string>) => {
// Swipe up and down on mobile/small screens // Swipe up and down on mobile/small screens
if (innerWidth < 992) { if (innerWidth < 992) {
@@ -214,7 +217,7 @@
} }
onMount(() => { $effect(() => {
/** /**
* Animations * Animations
*/ */
@@ -289,14 +292,14 @@
}) })
</script> </script>
<svelte:window bind:innerWidth on:keydown={handleKeydown} /> <svelte:window bind:innerWidth onkeydown={handleKeydown} />
{#if currentPhoto} {#if currentPhoto}
<Metas <Metas
title="{currentPhoto.title} - Houses Of {location.name}" title="{currentPhoto.title} - Houses Of {location.name}"
description="Photo of a beautiful home from {location.name}, {location.country.name}" description="Photo of a beautiful home from {location.name}, {location.country.name}"
image={getAssetUrlKey(currentPhoto.image.id, 'share')} image={getAssetUrlKey(currentPhoto.image.id, 'share')}
/> />
{/if} {/if}
@@ -317,10 +320,11 @@
</ButtonCircle> </ButtonCircle>
<div class="photo-page__carousel"> <div class="photo-page__carousel">
<div class="photo-page__images" <div
use:swipe use:swipe
on:swipe={handleSwipe} class="photo-page__images"
on:tap={toggleFullscreen} onswipe={handleSwipe}
ontap={toggleFullscreen}
> >
{#each visiblePhotos as { id, image, title }, index (id)} {#each visiblePhotos as { id, image, title }, index (id)}
<div class="photo-page__picture is-{currentIndex === 0 ? index + 1 : index}"> <div class="photo-page__picture is-{currentIndex === 0 ? index + 1 : index}">
@@ -340,10 +344,10 @@
{/each} {/each}
<div class="photo-page__controls"> <div class="photo-page__controls">
<ButtonCircle class="prev shadow-box-dark" label="Previous" disabled={!canGoNext} clone={true} on:click={goToPrevious}> <ButtonCircle class="prev shadow-box-dark" label="Previous" disabled={!canGoNext} clone={true} onclick={goToPrevious}>
<IconArrow color="pink" flip={true} /> <IconArrow color="pink" flip={true} />
</ButtonCircle> </ButtonCircle>
<ButtonCircle class="next shadow-box-dark" label="Next" disabled={!canGoPrev} clone={true} on:click={goToNext}> <ButtonCircle class="next shadow-box-dark" label="Next" disabled={!canGoPrev} clone={true} onclick={goToNext}>
<IconArrow color="pink" /> <IconArrow color="pink" />
</ButtonCircle> </ButtonCircle>
</div> </div>
@@ -378,10 +382,13 @@
</div> </div>
{#if isFullscreen} {#if isFullscreen}
<div class="photo-page__fullscreen" bind:this={fullscreenEl} <div
on:click={toggleFullscreen} on:keydown bind:this={fullscreenEl}
class="photo-page__fullscreen"
onclick={toggleFullscreen}
in:fade={{ easing: quartOut, duration: 1000 }} in:fade={{ easing: quartOut, duration: 1000 }}
out:fade={{ easing: quartOut, duration: 1000, delay: 300 }} out:fade={{ easing: quartOut, duration: 1000, delay: 300 }}
role="presentation"
> >
<div class="inner" transition:scale={{ easing: quartOut, start: 1.1, duration: 1000 }}> <div class="inner" transition:scale={{ easing: quartOut, start: 1.1, duration: 1000 }}>
<Image <Image

View File

@@ -4,7 +4,6 @@
<script lang="ts"> <script lang="ts">
import { navigating } from '$app/stores' import { navigating } from '$app/stores'
import { onMount, afterUpdate } from 'svelte'
import { quartOut as quartOutSvelte } from 'svelte/easing' import { quartOut as quartOutSvelte } from 'svelte/easing'
import { fade, fly } from 'svelte/transition' import { fade, fly } from 'svelte/transition'
import { animate, inView, stagger, timeline } from 'motion' import { animate, inView, stagger, timeline } from 'motion'
@@ -22,23 +21,30 @@
import ProcessStep from '$components/molecules/ProcessStep/ProcessStep.svelte' import ProcessStep from '$components/molecules/ProcessStep/ProcessStep.svelte'
import Banner from '$components/organisms/Banner/Banner.svelte' import Banner from '$components/organisms/Banner/Banner.svelte'
export let data let { data } = $props()
const { about, photos } = data
let scrollY: number, innerWidth: number, innerHeight: number let scrollY = $state<number>()
let photosGridEl: HTMLElement let innerWidth = $state<number>()
let photosGridOffset: number = photosGridEl && photosGridEl.offsetTop let innerHeight = $state<number>()
let currentStep = 0 let photosGridEl = $state<HTMLElement>()
let emailCopied: string = null let photosGridOffset = $state<number>()
let currentStep = $state(0)
let emailCopied = $state<string>()
let emailCopiedTimeout: ReturnType<typeof setTimeout> | number let emailCopiedTimeout: ReturnType<typeof setTimeout> | number
$: parallaxPhotos = photosGridEl && map(scrollY, photosGridOffset - innerHeight, photosGridOffset + innerHeight / 1.5, 0, innerHeight * 0.15, true) const parallaxPhotos = $derived(photosGridEl && map(scrollY, photosGridOffset - innerHeight, photosGridOffset + innerHeight / 1.5, 0, innerHeight * 0.15, true))
$: fadedPhotosIndexes = innerWidth > 768 const fadedPhotosIndexes = $derived(
innerWidth > 768
? [0, 2, 5, 7, 9, 12, 17, 20, 22, 26, 30, 32, 34] ? [0, 2, 5, 7, 9, 12, 17, 20, 22, 26, 30, 32, 34]
: [1, 4, 5, 7, 11, 14, 17, 20, 24, 27, 30, 33, 34, 36, 40, 43] : [1, 4, 5, 7, 11, 14, 17, 20, 24, 27, 30, 33, 34, 36, 40, 43]
)
$effect(() => {
// Update photos grid top offset
photosGridOffset = photosGridEl.offsetTop
onMount(() => {
/** /**
* Animations * Animations
*/ */
@@ -163,20 +169,14 @@
// Run animation // Run animation
requestAnimationFrame(animation.play) requestAnimationFrame(animation.play)
}) })
afterUpdate(() => {
// Update photos grid top offset
photosGridOffset = photosGridEl.offsetTop
})
</script> </script>
<svelte:window bind:scrollY bind:innerWidth bind:innerHeight /> <svelte:window bind:scrollY bind:innerWidth bind:innerHeight />
<Metas <Metas
title={about.seo_title} title={data.data.about.seo_title}
description={about.seo_description} description={data.about.seo_description}
image={about.seo_image ? getAssetUrlKey(about.seo_image.id, 'share-image') : null} image={data.about.seo_image ? getAssetUrlKey(data.about.seo_image.id, 'share-image') : null}
/> />
@@ -191,13 +191,13 @@
<section class="about__introduction"> <section class="about__introduction">
<div class="container grid"> <div class="container grid">
<h2 class="title-small">{about.intro_title}</h2> <h2 class="title-small">{data.about.intro_title}</h2>
<div class="heading text-big"> <div class="heading text-big">
{@html about.intro_heading} {@html data.about.intro_heading}
</div> </div>
<div class="text text-small"> <div class="text text-small">
{@html about.intro_text} {@html data.about.intro_text}
</div> </div>
</div> </div>
</section> </section>
@@ -207,8 +207,8 @@
<figure class="first-photo"> <figure class="first-photo">
<Image <Image
class="picture shadow-box-dark" class="picture shadow-box-dark"
id={about.intro_firstphoto.id} id={data.about.intro_firstphoto.id}
alt={about.intro_firstphoto.title} alt={data.about.intro_firstphoto.title}
sizeKey="photo-list" sizeKey="photo-list"
sizes={{ sizes={{
small: { width: 400 }, small: { width: 400 },
@@ -218,29 +218,29 @@
ratio={1.5} ratio={1.5}
/> />
<figcaption class="text-info"> <figcaption class="text-info">
{about.intro_firstphoto_caption}<br> {data.about.intro_firstphoto_caption}<br>
in in
<a href="/{about.intro_firstlocation.country.slug}/{about.intro_firstlocation.slug}" data-sveltekit-noscroll> <a href="/{data.about.intro_firstlocation.country.slug}/{data.about.intro_firstlocation.slug}" data-sveltekit-noscroll>
<img src={getAssetUrlKey(about.intro_firstlocation.country.flag.id, 'square-small')} width="32" height="32" alt={about.intro_firstlocation.country.flag.title}> <img src={getAssetUrlKey(data.about.intro_firstlocation.country.flag.id, 'square-small')} width="32" height="32" alt={data.about.intro_firstlocation.country.flag.title}>
<span>Naarm Australia (Melbourne)</span> <span>Naarm Australia (Melbourne)</span>
</a> </a>
</figcaption> </figcaption>
</figure> </figure>
<h2 class="title-small" data-reveal>{about.creation_title}</h2> <h2 class="title-small" data-reveal>{data.about.creation_title}</h2>
<div class="heading text-huge" data-reveal> <div class="heading text-huge" data-reveal>
{@html about.creation_heading} {@html data.about.creation_heading}
</div> </div>
<div class="text text-small" data-reveal> <div class="text text-small" data-reveal>
{@html about.creation_text} {@html data.about.creation_text}
</div> </div>
<figure class="picture portrait-photo" data-reveal-image> <figure class="picture portrait-photo" data-reveal-image>
<Image <Image
class="shadow-box-dark" class="shadow-box-dark"
id={about.creation_portrait.id} id={data.about.creation_portrait.id}
alt={about.creation_portrait.title} alt={data.about.creation_portrait.title}
sizeKey="photo-list" sizeKey="photo-list"
sizes={{ sizes={{
small: { width: 400 }, small: { width: 400 },
@@ -250,7 +250,7 @@
/> />
</figure> </figure>
<span class="portrait-photo__caption text-info"> <span class="portrait-photo__caption text-info">
{about.creation_portrait_caption} {data.about.creation_portrait_caption}
</span> </span>
</div> </div>
</section> </section>
@@ -260,8 +260,8 @@
<figure class="picture" data-reveal-image> <figure class="picture" data-reveal-image>
<Image <Image
class="shadow-box-dark" class="shadow-box-dark"
id={about.present_image.id} id={data.about.present_image.id}
alt={about.present_image.title} alt={data.about.present_image.title}
sizeKey="photo-list" sizeKey="photo-list"
sizes={{ sizes={{
small: { width: 400 }, small: { width: 400 },
@@ -272,26 +272,26 @@
/> />
</figure> </figure>
<h2 class="title-small" data-reveal>{about.present_title}</h2> <h2 class="title-small" data-reveal>{data.about.present_title}</h2>
<div class="text text-small" data-reveal> <div class="text text-small" data-reveal>
<p>{about.present_text}</p> <p>{data.about.present_text}</p>
</div> </div>
<div class="heading text-big" data-reveal> <div class="heading text-big" data-reveal>
{@html about.present_heading} {@html data.about.present_heading}
</div> </div>
<div class="conclusion text-small" data-reveal> <div class="conclusion text-small" data-reveal>
<p>{about.present_conclusion}</p> <p>{data.about.present_conclusion}</p>
</div> </div>
</div> </div>
</section> </section>
{#if about.image_showcase} {#if data.about.image_showcase}
<div class="about__showcase container grid"> <div class="about__showcase container grid">
<Image <Image
id={about.image_showcase.id} id={data.about.image_showcase.id}
alt={about.image_showcase.title} alt={data.about.image_showcase.title}
sizeKey="showcase" sizeKey="showcase"
sizes={{ sizes={{
small: { width: 400 }, small: { width: 400 },
@@ -307,15 +307,18 @@
<div class="container grid"> <div class="container grid">
<aside> <aside>
<div class="heading"> <div class="heading">
<h2 class="title-medium">{about.process_title}</h2> <h2 class="title-medium">{data.about.process_title}</h2>
<p class="text-xsmall">{about.process_subtitle}</p> <p class="text-xsmall">{data.about.process_subtitle}</p>
</div> </div>
<ol> <ol>
{#each about.process_steps as { title }, index} {#each data.about.process_steps as { title }, index}
<li class:is-active={index === currentStep}> <li class:is-active={index === currentStep}>
<a href="#step-{index + 1}" class="title-big" <a
on:click|preventDefault={() => { class="title-big"
href="#step-{index + 1}"
onclick={(event) => {
event.preventDefault()
currentStep = index currentStep = index
sendEvent('aboutStepSwitch') sendEvent('aboutStepSwitch')
}} }}
@@ -328,7 +331,7 @@
</aside> </aside>
<div class="steps"> <div class="steps">
{#each about.process_steps as { text, image, video_mp4, video_webm }, index} {#each data.about.process_steps as { text, image, video_mp4, video_webm }, index}
{#if index === currentStep} {#if index === currentStep}
<ProcessStep <ProcessStep
{index} {text} {index} {text}
@@ -347,8 +350,9 @@
<section class="about__photos" bind:this={photosGridEl}> <section class="about__photos" bind:this={photosGridEl}>
<div class="container-wide"> <div class="container-wide">
<div class="photos-grid" style:--parallax-y="{parallaxPhotos}px"> <div class="photos-grid" style:--parallax-y="{parallaxPhotos}px">
{#each photos as { image: { id }, title }, index} {#each data.photos as { image: { id }, title }, index}
<AboutGridPhoto class="grid-photo" <AboutGridPhoto
class="grid-photo"
{id} {id}
alt={title} alt={title}
disabled={fadedPhotosIndexes.includes(index)} disabled={fadedPhotosIndexes.includes(index)}
@@ -360,9 +364,9 @@
<section class="about__interest container grid"> <section class="about__interest container grid">
<div class="container grid"> <div class="container grid">
<h2 class="title-xl">{about.contact_title}</h2> <h2 class="title-xl">{data.about.contact_title}</h2>
<div class="blocks"> <div class="blocks">
{#each about.contact_blocks as { title, text, link, button }} {#each data.about.contact_blocks as { title, text, link, button }}
<div class="block"> <div class="block">
<h3 class="text-label">{title}</h3> <h3 class="text-label">{title}</h3>
<div class="text text-normal"> <div class="text text-normal">
@@ -375,8 +379,8 @@
in:fly={{ y: 4, duration: 325, easing: quartOutSvelte, delay: 250 }} in:fly={{ y: 4, duration: 325, easing: quartOutSvelte, delay: 250 }}
out:fade={{ duration: 250, easing: quartOutSvelte }} out:fade={{ duration: 250, easing: quartOutSvelte }}
use:mailtoClipboard use:mailtoClipboard
on:copied={({ detail }) => { oncopied={(email: string) => {
emailCopied = detail.email emailCopied = email
// Clear timeout and add timeout to hide message // Clear timeout and add timeout to hide message
clearTimeout(emailCopiedTimeout) clearTimeout(emailCopiedTimeout)
emailCopiedTimeout = setTimeout(() => emailCopied = null, 2500) emailCopiedTimeout = setTimeout(() => emailCopied = null, 2500)

View File

@@ -4,7 +4,6 @@
<script lang="ts"> <script lang="ts">
import { navigating } from '$app/stores' import { navigating } from '$app/stores'
import { onMount } from 'svelte'
import { stagger, timeline } from 'motion' import { stagger, timeline } from 'motion'
import { DELAY } from '$utils/constants' import { DELAY } from '$utils/constants'
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
@@ -14,11 +13,11 @@
import Heading from '$components/molecules/Heading/Heading.svelte' import Heading from '$components/molecules/Heading/Heading.svelte'
import InteractiveGlobe from '$components/organisms/InteractiveGlobe/InteractiveGlobe.svelte' import InteractiveGlobe from '$components/organisms/InteractiveGlobe/InteractiveGlobe.svelte'
export let data let { data } = $props()
const { credit } = data const { credit } = data
onMount(() => { $effect(() => {
/** /**
* Animations * Animations
*/ */

View File

@@ -5,7 +5,7 @@
<script lang="ts"> <script lang="ts">
import { page, navigating } from '$app/stores' import { page, navigating } from '$app/stores'
import { goto } from '$app/navigation' import { goto } from '$app/navigation'
import { getContext, onMount } from 'svelte' import { getContext } from 'svelte'
import { fly } from 'svelte/transition' import { fly } from 'svelte/transition'
import { quartOut as quartOutSvelte } from 'svelte/easing' import { quartOut as quartOutSvelte } from 'svelte/easing'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@@ -13,7 +13,6 @@
import { stagger, timeline } from 'motion' import { stagger, timeline } from 'motion'
import { DELAY } from '$utils/constants' import { DELAY } from '$utils/constants'
import { map, lerp } from 'utils/math' import { map, lerp } from 'utils/math'
import { throttle } from 'utils/actions'
import { getAssetUrlKey } from '$utils/api' import { getAssetUrlKey } from '$utils/api'
import { quartOut } from '$animations/easings' import { quartOut } from '$animations/easings'
import { PUBLIC_FILTERS_DEFAULT_COUNTRY, PUBLIC_FILTERS_DEFAULT_SORT, PUBLIC_GRID_INCREMENT } from '$env/static/public' import { PUBLIC_FILTERS_DEFAULT_COUNTRY, PUBLIC_FILTERS_DEFAULT_SORT, PUBLIC_GRID_INCREMENT } from '$env/static/public'
@@ -30,64 +29,53 @@
import ShopModule from '$components/organisms/ShopModule/ShopModule.svelte' import ShopModule from '$components/organisms/ShopModule/ShopModule.svelte'
import NewsletterModule from '$components/organisms/NewsletterModule/NewsletterModule.svelte' import NewsletterModule from '$components/organisms/NewsletterModule/NewsletterModule.svelte'
export let data let { data } = $props()
let { photos, totalPhotos }: { photos: any[], totalPhotos: number } = data let photos = $state<any[]>(data.photos)
$: ({ photos, totalPhotos } = data) const totalPhotos = $derived<number>(data.totalPhotos)
const { filteredCountryExists, settings }: { filteredCountryExists: boolean, settings: any } = data const { filteredCountryExists, settings }: { filteredCountryExists: boolean, settings: any } = data
const { countries, locations }: any = getContext('global') const { countries, locations }: any = getContext('global')
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
let photosGridEl: HTMLElement let photosGridEl = $state<HTMLElement>()
let observerPhotos: IntersectionObserver let observerPhotos: IntersectionObserver
let mutationPhotos: MutationObserver let mutationPhotos: MutationObserver
let scrollY: number let scrollY = $state<number>()
let innerWidth: number, innerHeight: number let innerWidth = $state<number>()
let innerHeight = $state<number>()
/** /**
* Filters * Filters
*/ */
const defaultCountry: string = PUBLIC_FILTERS_DEFAULT_COUNTRY const defaultCountry = PUBLIC_FILTERS_DEFAULT_COUNTRY
const defaultSort: string = PUBLIC_FILTERS_DEFAULT_SORT const defaultSort = PUBLIC_FILTERS_DEFAULT_SORT
const urlFiltersParams = new URLSearchParams() const urlFiltersParams = new URLSearchParams()
let filtered: boolean let filterCountry = $state($page.url.searchParams.get('country') || defaultCountry)
let filterCountry = $page.url.searchParams.get('country') || defaultCountry let filterSort = $state($page.url.searchParams.get('sort') || defaultSort)
let filterSort = $page.url.searchParams.get('sort') || defaultSort const filtered = $derived(filterCountry !== defaultCountry || filterSort !== defaultSort)
let countryFlagId: string const latestPhoto = $derived(photos && photos[0])
$: filtered = filterCountry !== defaultCountry || filterSort !== defaultSort const currentCountry = $derived(countries.find((country: any) => country.slug === filterCountry))
$: latestPhoto = photos && photos[0]
$: currentCountry = countries.find((country: any) => country.slug === filterCountry)
// Pages related informations // Pages related informations
let currentPage = 1 let currentPage = $state(1)
let ended: boolean const currentPhotosAmount = $derived(photos?.length)
let currentPhotosAmount: number const ended = $derived(currentPhotosAmount === totalPhotos)
$: currentPhotosAmount = photos && photos.length
$: ended = currentPhotosAmount === totalPhotos
/** /**
* Container margins * Container margins
*/ */
let scrollProgress: number const viewportScroll = $derived((innerHeight / innerWidth) <= 0.6 ? innerHeight * 1.5 : innerHeight)
let sideMargins: number = innerWidth < 1200 ? 16 : 8 const scrollProgress = $derived(map(scrollY, 0, viewportScroll, 0, 1, true))
$: viewportScroll = (innerHeight / innerWidth) <= 0.6 ? innerHeight * 1.5 : innerHeight const sideMargins = $derived(lerp(innerWidth < 1200 ? 16 : 8, 30, scrollProgress))
// Define sides margin on scroll
const setSidesMargin = throttle(() => {
if (window.innerWidth >= 992) {
scrollProgress = map(scrollY, 0, viewportScroll, 0, 1, true)
sideMargins = lerp(innerWidth < 1200 ? 16 : 8, 30, scrollProgress)
}
}, 50)
/** /**
* Handle URL query params * Handle URL query params
*/ */
$: countryFlagId = currentCountry ? currentCountry.flag.id : undefined const countryFlagId = $derived(currentCountry ? currentCountry.flag.id : undefined)
// Update URL filtering params from filter values // Update URL filtering params from filter values
const applyFilters = () => { const applyFilters = () => {
@@ -103,7 +91,7 @@
* Define small photo size from index * Define small photo size from index
* With different grid patterns depending on window width * With different grid patterns depending on window width
*/ */
$: isSmall = (index: number) => { const isSmall = (index: number) => {
let modulo = index % 5 let modulo = index % 5
let notOn = [0] let notOn = [0]
@@ -122,21 +110,21 @@
/** /**
* Filters change events * Filters change events
*/ */
// Country select /** Country select */
const handleCountryChange = ({ detail: value }) => { const handleCountryChange = ({ detail: value }) => {
filterCountry = value === defaultCountry ? defaultCountry : value filterCountry = value === defaultCountry ? defaultCountry : value
currentPage = 1 currentPage = 1
applyFilters() applyFilters()
} }
// Sort select /** Sort select */
const handleSortChange = ({ detail: value }) => { const handleSortChange = ({ detail: value }) => {
filterSort = value === defaultSort ? defaultSort : value filterSort = value === defaultSort ? defaultSort : value
currentPage = 1 currentPage = 1
applyFilters() applyFilters()
} }
// Reset filters /** Reset filters */
const resetFiltered = () => { const resetFiltered = () => {
filterCountry = defaultCountry filterCountry = defaultCountry
filterSort = defaultSort filterSort = defaultSort
@@ -148,7 +136,7 @@
/** /**
* Load photos * Load photos
*/ */
// [function] Load photos helper /** Load photos helper */
const loadPhotos = async (page: number) => { const loadPhotos = async (page: number) => {
const res = await fetch('/api/data', { const res = await fetch('/api/data', {
method: 'POST', method: 'POST',
@@ -194,7 +182,7 @@
} }
} }
// Load more photos from CTA /** Load more photos from CTA */
const loadMorePhotos = async () => { const loadMorePhotos = async () => {
// Append more photos from API including options and page // Append more photos from API including options and page
const newPhotos: any = await loadPhotos(currentPage + 1) const newPhotos: any = await loadPhotos(currentPage + 1)
@@ -207,14 +195,11 @@
// Increment the current page // Increment the current page
currentPage++ currentPage++
} }
// Increment the currently visible amount of photos
currentPhotosAmount += newPhotos.length
} }
} }
onMount(() => { $effect(() => {
/** /**
* Observers * Observers
*/ */
@@ -290,15 +275,12 @@
image={getAssetUrlKey(settings.seo_image_photos.id, 'share-image')} image={getAssetUrlKey(settings.seo_image_photos.id, 'share-image')}
/> />
<svelte:window <svelte:window bind:scrollY bind:innerWidth bind:innerHeight />
bind:scrollY bind:innerWidth bind:innerHeight
on:scroll={setSidesMargin}
/>
<main class="photos-page"> <main class="photos-page">
<section class="photos-page__intro"> <section class="photos-page__intro">
<ScrollingTitle tag="h1" text="Houses"> <ScrollingTitle tag="h1" label="Houses">
<SplitText text="Houses" mode="chars" /> <SplitText text="Houses" mode="chars" />
</ScrollingTitle> </ScrollingTitle>
@@ -324,7 +306,7 @@
selected: filterCountry === slug, selected: filterCountry === slug,
})) }))
]} ]}
on:change={handleCountryChange} onchange={handleCountryChange}
value={filterCountry} value={filterCountry}
> >
{#if countryFlagId} {#if countryFlagId}
@@ -356,7 +338,7 @@
selected: filterSort === 'oldest' selected: filterSort === 'oldest'
}, },
]} ]}
on:change={handleSortChange} onchange={handleSortChange}
value={filterSort} value={filterSort}
> >
<svg class="icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-label="Sort icon"> <svg class="icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-label="Sort icon">
@@ -368,8 +350,9 @@
<div class="filters__actions"> <div class="filters__actions">
{#if filtered} {#if filtered}
<button class="reset button-link" <button
on:click={resetFiltered} class="reset button-link"
onclick={resetFiltered}
transition:fly={{ y: 4, duration: 600, easing: quartOutSvelte }} transition:fly={{ y: 4, duration: 600, easing: quartOutSvelte }}
> >
Reset Reset
@@ -422,7 +405,7 @@
size="large" size="large"
color="beige" color="beige"
text={!ended ? 'See more photos' : "You've seen it all!"} text={!ended ? 'See more photos' : "You've seen it all!"}
on:click={loadMorePhotos} onclick={loadMorePhotos}
disabled={ended} disabled={ended}
/> />

View File

@@ -4,7 +4,6 @@
<script lang="ts"> <script lang="ts">
import { navigating } from '$app/stores' import { navigating } from '$app/stores'
import { onMount } from 'svelte'
import { stagger, timeline } from 'motion' import { stagger, timeline } from 'motion'
import { DELAY } from '$utils/constants' import { DELAY } from '$utils/constants'
import { quartOut } from '$animations/easings' import { quartOut } from '$animations/easings'
@@ -15,13 +14,12 @@
import NewsletterIssue from '$components/molecules/NewsletterIssue/NewsletterIssue.svelte' import NewsletterIssue from '$components/molecules/NewsletterIssue/NewsletterIssue.svelte'
import InteractiveGlobe from '$components/organisms/InteractiveGlobe/InteractiveGlobe.svelte' import InteractiveGlobe from '$components/organisms/InteractiveGlobe/InteractiveGlobe.svelte'
export let data let { data } = $props()
const { issues } = data const latestIssue = $derived(data.issues[0])
const latestIssue = issues[0]
onMount(() => { $effect(() => {
/** /**
* Animations * Animations
*/ */
@@ -79,10 +77,10 @@
<NewsletterIssue size="large" date={latestIssue.date_sent} {...latestIssue} /> <NewsletterIssue size="large" date={latestIssue.date_sent} {...latestIssue} />
</div> </div>
{#if issues.length > 1} {#if data.issues.length > 1}
<h2 class="title-small">Past Issues</h2> <h2 class="title-small">Past Issues</h2>
<ul> <ul>
{#each issues.slice(1) as { issue, title, date_sent: date, link, thumbnail }} {#each data.issues.slice(1) as { issue, title, date_sent: date, link, thumbnail }}
<li class="issue-container"> <li class="issue-container">
<NewsletterIssue {issue} {title} {link} {thumbnail} {date} /> <NewsletterIssue {issue} {title} {link} {thumbnail} {date} />
</li> </li>

View File

@@ -9,8 +9,7 @@
import Metas from '$components/Metas.svelte' import Metas from '$components/Metas.svelte'
import Heading from '$components/molecules/Heading/Heading.svelte' import Heading from '$components/molecules/Heading/Heading.svelte'
export let data let { data } = $props()
const { legal } = data
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
</script> </script>
@@ -26,7 +25,7 @@
<section class="terms__categories"> <section class="terms__categories">
<div class="container grid"> <div class="container grid">
{#each legal.terms as { title, text }, index} {#each data.legal.terms as { title, text }, index}
<article class="terms__section grid"> <article class="terms__section grid">
<h2 class="title-small">{index + 1}. {title}</h2> <h2 class="title-small">{index + 1}. {title}</h2>
<div class="text text-info"> <div class="text text-info">
@@ -37,7 +36,7 @@
<footer> <footer>
<p class="text-info"> <p class="text-info">
Updated: <time datetime={dayjs(legal.date_updated).format('YYYY-MM-DD')}>{dayjs().to(dayjs(legal.date_updated))}</time> Updated: <time datetime={dayjs(data.legal.date_updated).format('YYYY-MM-DD')}>{dayjs().to(dayjs(data.legal.date_updated))}</time>
</p> </p>
</footer> </footer>
</div> </div>

View File

@@ -21,7 +21,7 @@
import { page } from '$app/stores' import { page } from '$app/stores'
import { beforeNavigate, afterNavigate } from '$app/navigation' import { beforeNavigate, afterNavigate } from '$app/navigation'
import { PUBLIC_ANALYTICS_DOMAIN } from '$env/static/public' import { PUBLIC_ANALYTICS_DOMAIN } from '$env/static/public'
import { setContext, onMount } from 'svelte' import { setContext } from 'svelte'
import { fade } from 'svelte/transition' import { fade } from 'svelte/transition'
import { DELAY, DURATION } from '$utils/constants' import { DELAY, DURATION } from '$utils/constants'
import { pageLoading, previousPage } from '$utils/stores' import { pageLoading, previousPage } from '$utils/stores'
@@ -35,10 +35,9 @@
import Toast from '$components/molecules/Toast/Toast.svelte' import Toast from '$components/molecules/Toast/Toast.svelte'
import Footer from '$components/organisms/Footer/Footer.svelte' import Footer from '$components/organisms/Footer/Footer.svelte'
export let data let { data, children } = $props()
let innerHeight: number let innerHeight = $state<number>()
$: innerHeight && document.body.style.setProperty('--vh', `${innerHeight}px`)
// Fonts to preload // Fonts to preload
const fonts = [ const fonts = [
@@ -77,17 +76,18 @@
} }
}) })
$: if (browser) {
$effect(() => {
// Set viewport height
innerHeight && document.body.style.setProperty('--vh', `${innerHeight}px`)
// Avoid FOUC
document.body.style.opacity = '1'
// Define page loading // Define page loading
document.body.classList.toggle('is-loading', $pageLoading) document.body.classList.toggle('is-loading', $pageLoading)
// Block scroll on certain conditions // Block scroll on certain conditions
// document.body.classList.toggle('block-scroll', condition) // document.body.classList.toggle('block-scroll', condition)
}
onMount(() => {
// Avoid FOUC
document.body.style.opacity = '1'
}) })
</script> </script>
@@ -109,7 +109,7 @@
in:fade={{ duration: DURATION.PAGE_IN, delay: DELAY.PAGE_LOADING }} in:fade={{ duration: DURATION.PAGE_IN, delay: DELAY.PAGE_LOADING }}
out:fade={{ duration: DURATION.PAGE_OUT }} out:fade={{ duration: DURATION.PAGE_OUT }}
> >
<slot /> {@render children()}
{#if !$page.params.photo} {#if !$page.params.photo}
<Footer /> <Footer />

View File

@@ -4,7 +4,7 @@
<script lang="ts"> <script lang="ts">
import { navigating } from '$app/stores' import { navigating } from '$app/stores'
import { getContext, onMount } from 'svelte' import { getContext } from 'svelte'
import { timeline, stagger } from 'motion' import { timeline, stagger } from 'motion'
import { DELAY } from '$utils/constants' import { DELAY } from '$utils/constants'
import { smoothScroll } from '$utils/stores' import { smoothScroll } from '$utils/stores'
@@ -26,15 +26,15 @@
import ShopModule from '$components/organisms/ShopModule/ShopModule.svelte' import ShopModule from '$components/organisms/ShopModule/ShopModule.svelte'
import NewsletterModule from '$components/organisms/NewsletterModule/NewsletterModule.svelte' import NewsletterModule from '$components/organisms/NewsletterModule/NewsletterModule.svelte'
export let data let { data } = $props()
const { photos } = data
const { settings, locations }: any = getContext('global') const { settings, locations }: any = getContext('global')
let scrollY: number, innerHeight: number let scrollY = $state<number>()
let innerHeight = $state<number>()
onMount(() => { $effect(() => {
/** /**
* Animations * Animations
*/ */
@@ -81,7 +81,8 @@
<main class="homepage"> <main class="homepage">
<section class="homepage__intro" <section
class="homepage__intro"
use:reveal={{ use:reveal={{
animation: { opacity: [0, 1] }, animation: { opacity: [0, 1] },
options: { options: {
@@ -108,7 +109,7 @@
size="medium" size="medium"
url="#locations" url="#locations"
text="Explore locations" text="Explore locations"
on:click={() => $smoothScroll.scrollTo('#locations', { duration: 2 })} onclick={() => $smoothScroll.scrollTo('#locations', { duration: 2 })}
> >
<IconEarth animate={true} /> <IconEarth animate={true} />
</Button> </Button>
@@ -116,7 +117,7 @@
</section> </section>
<section class="homepage__photos"> <section class="homepage__photos">
<Collage {photos} /> <Collage photos={data.photos} />
</section> </section>
<div class="homepage__ctas"> <div class="homepage__ctas">
@@ -124,28 +125,13 @@
<ListCTAs> <ListCTAs>
<li> <li>
<BoxCTA <BoxCTA url="/photos" icon="photos" label="Browse all photos" alt="Photos" />
url="/photos"
icon="photos"
label="Browse all photos"
alt="Photos"
/>
</li> </li>
<li> <li>
<BoxCTA <BoxCTA url="/shop" icon="bag" label="Shop our products" alt="Shopping bag" />
url="/shop"
icon="bag"
label="Shop our products"
alt="Shopping bag"
/>
</li> </li>
<li> <li>
<BoxCTA <BoxCTA url="/about" icon="compass" label="Learn about the project" alt="Compass" />
url="/about"
icon="compass"
label="Learn about the project"
alt="Compass"
/>
</li> </li>
</ListCTAs> </ListCTAs>
</div> </div>

View File

@@ -14,9 +14,7 @@ export const mailtoClipboard = (node: HTMLElement) => {
navigator.clipboard.writeText(emailAddress) navigator.clipboard.writeText(emailAddress)
// Send event // Send event
node.dispatchEvent(new CustomEvent('copied', { node.dispatchEvent(new CustomEvent('copied', emailAddress))
detail: { email: emailAddress }
}))
// Record event in analytics // Record event in analytics
sendEvent('emailCopy') sendEvent('emailCopy')

BIN
bun.lockb

Binary file not shown.