Merge branch 'main' into dev

This commit is contained in:
2022-12-23 20:00:59 +01:00
90 changed files with 3432 additions and 3002 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ node_modules
.env .env
.env.* .env.*
!.env.example !.env.example
.vercel

View File

@@ -15,40 +15,40 @@
"lint": "eslint --ignore-path .gitignore ." "lint": "eslint --ignore-path .gitignore ."
}, },
"dependencies": { "dependencies": {
"@studio-freight/lenis": "^0.2.6", "@studio-freight/lenis": "^0.2.28",
"dayjs": "^1.11.5", "dayjs": "^1.11.7",
"embla-carousel": "^7.0.3", "embla-carousel": "^7.0.5",
"focus-visible": "^5.2.0", "focus-visible": "^5.2.0",
"motion": "^10.14.2", "motion": "^10.15.3",
"ogl": "^0.0.99", "ogl": "^0.0.103",
"sanitize.css": "^13.0.0", "sanitize.css": "^13.0.0",
"swell-js": "^3.17.6", "swell-js": "3.18.2",
"tweakpane": "^3.1.0" "tweakpane": "^3.1.1"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^1.0.0-next.80", "@sveltejs/adapter-auto": "^1.0.0",
"@sveltejs/adapter-node": "^1.0.0-next.96", "@sveltejs/adapter-node": "^1.0.0",
"@sveltejs/adapter-vercel": "^1.0.0-next.77", "@sveltejs/adapter-vercel": "^1.0.0",
"@sveltejs/kit": "^1.0.0-next.504", "@sveltejs/kit": "^1.0.1",
"@typescript-eslint/eslint-plugin": "^5.38.1", "@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.38.1", "@typescript-eslint/parser": "^5.47.0",
"base-64": "^1.0.0", "base-64": "^1.0.0",
"browserslist": "^4.21.4", "browserslist": "^4.21.4",
"cssnano": "^5.1.13", "cssnano": "^5.1.14",
"eslint": "^8.24.0", "eslint": "^8.30.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"postcss": "^8.4.16", "postcss": "^8.4.20",
"postcss-focus-visible": "^7.1.0", "postcss-focus-visible": "^7.1.0",
"postcss-normalize": "^10.0.1", "postcss-normalize": "^10.0.1",
"postcss-preset-env": "^7.8.2", "postcss-preset-env": "^7.8.3",
"postcss-sort-media-queries": "^4.3.0", "postcss-sort-media-queries": "^4.3.0",
"sass": "^1.55.0", "sass": "^1.57.1",
"svelte": "^3.50.1", "svelte": "^3.55.0",
"svelte-check": "^2.9.1", "svelte-check": "^3.0.1",
"svelte-preprocess": "^4.10.7", "svelte-preprocess": "^5.0.0",
"tslib": "^2.4.0", "tslib": "^2.4.1",
"typescript": "^4.8.3", "typescript": "^4.9.4",
"vite": "^3.1.3" "vite": "^4.0.3"
}, },
"type": "module", "type": "module",
"browserslist": [ "browserslist": [

2021
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,18 @@
import { animate } from 'motion' import { animate, stagger } from 'motion'
import type { TransitionConfig } from 'svelte/transition'
import { quartOut } from './easings' import { quartOut } from './easings'
/**
* Scale and fade
*/
export const scaleFade = (node: HTMLElement, { export const scaleFade = (node: HTMLElement, {
delay = 0,
duration = 1,
scale = [0.7, 1], scale = [0.7, 1],
opacity = [1, 0], opacity = [1, 0],
x = null, x = null,
}) => { delay = 0,
duration = 1,
}): TransitionConfig => {
return { return {
css: () => { css: () => {
animate(node, { animate(node, {
@@ -20,6 +25,36 @@ export const scaleFade = (node: HTMLElement, {
duration, duration,
delay, delay,
}) })
return null
}
}
}
/**
* Scale and fade split text
*/
export const revealSplit = (node: HTMLElement, {
opacity = [0, 1],
y = ['110%', '0%'],
children = '.char',
duration = 1,
delay = 0,
}): TransitionConfig => {
return {
css: () => {
animate(node.querySelectorAll(children), {
opacity,
y,
z: 0,
}, {
easing: quartOut,
duration,
delay: stagger(0.04, { start: delay }),
})
return null
} }
} }
} }

View File

@@ -13,8 +13,8 @@
%sveltekit.head% %sveltekit.head%
</head> </head>
<body> <body data-sveltekit-preload-data="hover">
%sveltekit.body% <div style="display: contents">%sveltekit.body%</div>
<script> <script>
document.body.style.opacity = '0' document.body.style.opacity = '0'

View File

@@ -1,37 +1,15 @@
<script lang="ts"> <script lang="ts">
// @ts-nocheck export let domain: string
import { page } from '$app/stores' export let enabled: boolean = !import.meta.env.DEV
import { sendPage } from '$utils/analytics'
export let appKey: any
export let url: any
export let enabled: boolean = process.env.NODE_ENV !== 'development'
let loaded = false
const handleLoad = () => {
// Init Countly
if (Countly) {
Countly.init({
app_key: appKey,
url,
})
Countly.track_sessions()
Countly.track_pageview()
}
loaded = true
}
// Send page to Analytics when changing path
$: enabled && $page.url.pathname && loaded && sendPage()
</script> </script>
<svelte:head> <svelte:head>
{#if enabled} {#if enabled}
<script defer src="https://cdn.jsdelivr.net/npm/countly-sdk-web@latest/lib/countly.min.js" on:load={handleLoad} /> <script defer data-domain={domain} src="https://analytics.flayks.com/js/{import.meta.env.DEV ? 'script.local.outbound-links' : 'script.outbound-links'}.js"></script>
<script>
window.plausible = window.plausible || function() {
(window.plausible.q = window.plausible.q || []).push(arguments)
}
</script>
{/if} {/if}
</svelte:head> </svelte:head>
<noscript>
<img src="{url}/pixel.png?app_key={appKey}&begin_session=1" alt="countly" width="0" height="0" />
</noscript>

View File

@@ -18,10 +18,15 @@
<meta name="description" content={description}> <meta name="description" content={description}>
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />
<meta name="twitter:title" content={title} />
{#if description}
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
<meta name="twitter:description" content={description} />
{/if}
<meta property="og:type" content={type} /> <meta property="og:type" content={type} />
{#if image} {#if image}
<meta property="og:image" content={image} /> <meta property="og:image" content={image} />
<meta name="twitter:image" content={image} />
{/if} {/if}
{#if url} {#if url}
<meta property="og:url" content={url} /> <meta property="og:url" content={url} />

View File

@@ -1,18 +1,33 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores' import { page } from '$app/stores'
import { afterUpdate } from 'svelte'
import { fade } from 'svelte/transition' import { fade } from 'svelte/transition'
import { scrollToTop } from '$utils/functions' import { scrollToTop } from '$utils/functions'
import { DURATION } from '$utils/contants' import { pageLoading } from '$utils/stores'
import { DELAY, DURATION } from '$utils/constants'
export let name: string let loadingTimeout: ReturnType<typeof setTimeout> | number = null
$: doNotScroll = !$page.url.searchParams.get('country') && !$page.url.pathname.includes('/shop/') $: doNotScroll = !$page.url.searchParams.get('country') && !$page.url.pathname.includes('/shop/')
// Hide page loading indicator on page update
afterUpdate(() => {
clearTimeout(loadingTimeout)
loadingTimeout = setTimeout(() => $pageLoading = false, DURATION.PAGE_IN)
})
</script> </script>
<main class={name} <div class="page"
in:fade={{ duration: DURATION.PAGE_IN, delay: DURATION.PAGE_DELAY }} in:fade={{ duration: DURATION.PAGE_IN, delay: DELAY.PAGE_LOADING }}
out:fade={{ duration: DURATION.PAGE_OUT }} out:fade={{ duration: DURATION.PAGE_OUT }}
on:outroend={() => doNotScroll && scrollToTop()} on:outrostart={() => {
// Show page loading indicator
$pageLoading = true
}}
on:outroend={() => {
// Scroll back to top
doNotScroll && requestAnimationFrame(() => scrollToTop())
}}
> >
<slot /> <slot />
</main> </div>

View File

@@ -11,9 +11,7 @@
export let url: string export let url: string
</script> </script>
<a href={url} class="box-cta" <a href={url} class="box-cta">
data-sveltekit-prefetch={url.includes('http') ? true : undefined}
>
<div class="icon"> <div class="icon">
<Icon icon={icon} label={alt} /> <Icon icon={icon} label={alt} />
</div> </div>

View File

@@ -44,7 +44,6 @@
<a <a
href={url} class={classes} href={url} class={classes}
{target} {rel} {target} {rel}
data-sveltekit-prefetch={url && (isExternal || isProtocol) ? 'off' : ''}
data-sveltekit-noscroll={isExternal || isProtocol ? 'off' : ''} data-sveltekit-noscroll={isExternal || isProtocol ? 'off' : ''}
{disabled} {disabled}
tabindex="0" tabindex="0"

View File

@@ -6,12 +6,14 @@
import { scale } from 'svelte/transition' import { scale } from 'svelte/transition'
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
import { cartOpen, cartAmount } from '$utils/stores/shop' import { cartOpen, cartAmount } from '$utils/stores/shop'
import { sendEvent } from '$utils/analytics'
// Components // Components
import Icon from '$components/atoms/Icon.svelte' import Icon from '$components/atoms/Icon.svelte'
import ButtonCircle from '$components/atoms/ButtonCircle.svelte' import ButtonCircle from '$components/atoms/ButtonCircle.svelte'
const openCart = () => { const openCart = () => {
$cartOpen = true $cartOpen = true
sendEvent('cartOpen')
} }
</script> </script>

View File

@@ -1,5 +1,6 @@
<style lang="scss"> <style lang="scss">
:global(.scrolling-title) { .scrolling-title {
display: inline-block;
transform: translate3d(var(--parallax-x), 0, 0); transform: translate3d(var(--parallax-x), 0, 0);
transition: transform 1.2s var(--ease-quart); transition: transform 1.2s var(--ease-quart);
will-change: transform; will-change: transform;
@@ -25,8 +26,8 @@
// Define default values // Define default values
$: if (titleEl && !offsetStart && !offsetEnd) { $: if (titleEl && !offsetStart && !offsetEnd) {
offsetStart = titleEl.offsetTop - innerHeight * 0.75 offsetStart = titleEl.offsetTop - innerHeight * (innerWidth < 768 ? 0.2 : 0.75)
offsetEnd = titleEl.offsetTop + innerHeight * 0.25 offsetEnd = titleEl.offsetTop + innerHeight * (innerWidth < 768 ? 0.5 : 0.5)
} }
// Check if title is larger than viewport to translate it // Check if title is larger than viewport to translate it
@@ -34,7 +35,7 @@
// 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
} }
@@ -44,7 +45,6 @@
$$props.class $$props.class
].join(' ').trim() ].join(' ').trim()
const revealOptions = animate ? { const revealOptions = animate ? {
children: '.char', children: '.char',
animation: { y: ['-105%', 0] }, animation: { y: ['-105%', 0] },

View File

@@ -5,7 +5,7 @@
<script lang="ts"> <script lang="ts">
import SplitText from '$components/SplitText.svelte' import SplitText from '$components/SplitText.svelte'
import reveal from '$animations/reveal' import reveal from '$animations/reveal'
import { DURATION } from '$utils/contants' import { DURATION } from '$utils/constants'
export let variant: string = 'lines' export let variant: string = 'lines'
export let tag: string = 'h1' export let tag: string = 'h1'

View File

@@ -3,6 +3,8 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'
import { dev } from '$app/environment'
import { fly } from 'svelte/transition' import { fly } from 'svelte/transition'
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
import { sendEvent } from '$utils/analytics' import { sendEvent } from '$utils/analytics'
@@ -13,53 +15,41 @@
export let past: boolean = false export let past: boolean = false
let inputInFocus = false let inputInFocus = false
let formStatus: string = null let formStatus: { error: string, success: boolean, message: string } = null
let formMessageTimeout: ReturnType<typeof setTimeout> | number let formMessageTimeout: ReturnType<typeof setTimeout> | number
const formMessages = {
PENDING: `Almost there! Please confirm your email address through the email you'll receive soon.`, $: isSuccess = formStatus && formStatus.success
MEMBER_EXISTS_WITH_EMAIL_ADDRESS: `Uh oh! This email address is already subscribed to the newsletter.`,
INVALID_EMAIL: `Woops. This email doesn't seem to be valid.`,
}
// Toggle input focus // Toggle input focus
const toggleFocus = () => inputInFocus = !inputInFocus const toggleFocus = () => inputInFocus = !inputInFocus
// Handle form submission
const handleForm = () => {
return async ({ result, update }) => {
formStatus = result.data
/** if (dev) {
* Subscription form handling console.log(result)
*/
const formSubmission = async ({ target }) => {
const formData = new FormData(target)
const email = String(formData.get('email'))
if (email && email.match(/^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/)) {
const req = await fetch('/api/newsletter', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: email
})
const res = await req.json()
formStatus = res.code
if (res.code === 'PENDING') {
sendEvent({ action: 'newsletterSubscribe' })
} }
// If successful
if (result.data.success) {
sendEvent('newsletterSubscribe')
update()
} else { } else {
formStatus = 'INVALID_EMAIL' // Hide message for errors
}
}
$: if (formStatus !== 'PENDING') {
clearTimeout(formMessageTimeout) clearTimeout(formMessageTimeout)
formMessageTimeout = setTimeout(() => formStatus = null, 3000) formMessageTimeout = requestAnimationFrame(() => setTimeout(() => formStatus = null, 4000))
}
}
} }
</script> </script>
<div class="newsletter-form"> <div class="newsletter-form">
{#if formStatus !== 'PENDING'} {#if !isSuccess}
<form method="POST" on:submit|preventDefault={formSubmission} <form method="POST" action="?/subscribe"
out:fly={{ y: -8, easing: quartOut, duration: 600 }} use:enhance={handleForm}
out:fly|local={{ y: -8, easing: quartOut, duration: 600 }}
> >
<div class="newsletter-form__email" class:is-focused={inputInFocus}> <div class="newsletter-form__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
@@ -78,7 +68,7 @@
<div class="newsletter-form__bottom"> <div class="newsletter-form__bottom">
{#if past} {#if past}
<a href="/subscribe" class="past-issues" data-sveltekit-noscroll data-sveltekit-prefetch> <a href="/subscribe" class="past-issues" data-sveltekit-noscroll>
<svg width="20" height="16" viewBox="0 0 20 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-label="Newsletter icon"> <svg width="20" height="16" viewBox="0 0 20 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-label="Newsletter icon">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 2.346H2a.5.5 0 0 0-.5.5v11.102a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5V2.846a.5.5 0 0 0-.5-.5ZM2 .846a2 2 0 0 0-2 2v11.102a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2.846a2 2 0 0 0-2-2H2Zm13.75 4.25h-2v3h2v-3Zm-2-1a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2ZM3.5 6.5a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Zm.25 3a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5Zm1.25 2a.5.5 0 0 0 0 1h6a.5.5 0 1 0 0-1H5Z" /> <path fill-rule="evenodd" clip-rule="evenodd" d="M18 2.346H2a.5.5 0 0 0-.5.5v11.102a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5V2.846a.5.5 0 0 0-.5-.5ZM2 .846a2 2 0 0 0-2 2v11.102a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2.846a2 2 0 0 0-2-2H2Zm13.75 4.25h-2v3h2v-3Zm-2-1a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2ZM3.5 6.5a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Zm.25 3a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5Zm1.25 2a.5.5 0 0 0 0 1h6a.5.5 0 1 0 0-1H5Z" />
</svg> </svg>
@@ -90,14 +80,14 @@
</form> </form>
{/if} {/if}
{#if formStatus} {#if formStatus && formStatus.message}
<div class="newsletter-form__message shadow-small" <div class="newsletter-form__message shadow-small"
class:is-error={formStatus !== 'PENDING'} class:is-error={!isSuccess}
class:is-success={formStatus === 'PENDING'} class:is-success={isSuccess}
in:fly={{ y: 8, easing: quartOut, duration: 600, delay: 600 }} in:fly|local={{ y: 8, easing: quartOut, duration: 600, delay: isSuccess ? 600 : 0 }}
out:fly={{ y: 8, easing: quartOut, duration: 600 }} out:fly|local={{ y: 8, easing: quartOut, duration: 600 }}
> >
<p class="text-xsmall">{formMessages[formStatus]}</p> <p class="text-xsmall">{formStatus.message}</p>
</div> </div>
{/if} {/if}
</div> </div>

View File

@@ -5,7 +5,7 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte' import { getContext } from 'svelte'
import { spring } from 'svelte/motion' import { spring } from 'svelte/motion'
import dayjs, { type Dayjs } from 'dayjs' import dayjs from 'dayjs'
import { lerp } from '$utils/functions' import { lerp } from '$utils/functions'
import { PUBLIC_PREVIEW_COUNT } from '$env/static/public' import { PUBLIC_PREVIEW_COUNT } from '$env/static/public'
import { seenLocations } from '$utils/stores' import { seenLocations } from '$utils/stores'
@@ -23,15 +23,24 @@
// Location date limit // Location date limit
let isNew = false let isNew = false
let dateUpdated: Dayjs
const dateNowOffset = dayjs().subtract(settings.limit_new, 'day') const dateNowOffset = dayjs().subtract(settings.limit_new, 'day')
const parsedSeenLocations = JSON.parse($seenLocations)
$: if (latestPhoto && $seenLocations) { $: if (latestPhoto) {
dateUpdated = dayjs(latestPhoto.date_created) const dateUpdated = dayjs(latestPhoto.date_created)
// Detect if location has new content // Detect if location has new content
const seenLocation = JSON.parse($seenLocations)?.hasOwnProperty(location.id) const seenLocationDate = dayjs(parsedSeenLocations[location.id])
isNew = dateUpdated.isAfter(dateNowOffset) && !seenLocation const isLocationSeen = parsedSeenLocations?.hasOwnProperty(location.id)
// Define if location is has new photos
if (seenLocationDate && isLocationSeen) {
// A more recent photo has been added (if has been seen and has a seen date)
isNew = dateUpdated.isAfter(dateNowOffset) && dateUpdated.isAfter(seenLocationDate)
} else {
// The photo is after the offset
isNew = dateUpdated.isAfter(dateNowOffset)
}
} }

View File

@@ -16,7 +16,7 @@
<div class="poster"> <div class="poster">
{#if image} {#if image}
<a href="/shop/poster-{location.slug}" data-sveltekit-noscroll data-sveltekit-prefetch <a href="/shop/poster-{location.slug}" data-sveltekit-noscroll
on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })} on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}
> >
<Image <Image

View File

@@ -13,19 +13,10 @@
export let text: string export let text: string
export let image: any = undefined export let image: any = undefined
export let video: any = undefined export let video: any = undefined
export let visible: boolean = false
let videoEl: HTMLVideoElement
const imageRatio = image ? image.width / image.height : undefined const imageRatio = image ? image.width / image.height : undefined
// Toggle video playback if step is visible
$: if (videoEl) {
visible ? videoEl.play() : videoEl.pause()
}
</script> </script>
{#if visible}
<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] }}
@@ -44,8 +35,8 @@
ratio={imageRatio} ratio={imageRatio}
alt={image.title} alt={image.title}
/> />
{:else if video} {:else if video && video.mp4 && video.webm}
<video muted loop playsinline autoplay allow="autoplay" bind:this={videoEl}> <video muted loop playsinline autoplay allow="autoplay">
<source type="video/mp4" src={getAssetUrlKey(video.mp4, 'step')} /> <source type="video/mp4" src={getAssetUrlKey(video.mp4, 'step')} />
<source type="video/webm" src={getAssetUrlKey(video.webm, 'step')} /> <source type="video/webm" src={getAssetUrlKey(video.webm, 'step')} />
<track kind="captions" /> <track kind="captions" />
@@ -58,4 +49,3 @@
{@html text} {@html text}
</div> </div>
</div> </div>
{/if}

View File

@@ -6,6 +6,7 @@
import { page } from '$app/stores' import { page } from '$app/stores'
import { getContext } from 'svelte' import { getContext } from 'svelte'
import reveal from '$animations/reveal' import reveal from '$animations/reveal'
import { sendEvent } from '$utils/analytics'
// Components // Components
import Icon from '$components/atoms/Icon.svelte' import Icon from '$components/atoms/Icon.svelte'
@@ -20,6 +21,9 @@
*/ */
const toggleSwitcher = () => { const toggleSwitcher = () => {
isOpen = !isOpen isOpen = !isOpen
// Record opening event
!isOpen && sendEvent('switcherOpen')
} }
/** /**
@@ -56,7 +60,7 @@
</span> </span>
</button> </button>
<ul class="switcher__links" data-sveltekit-noscroll data-sveltekit-prefetch> <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} on:click={toggleSwitcher} tabindex="0">

View File

@@ -3,9 +3,11 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment'
import { onMount } from 'svelte' 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 { cartOpen, cartData, cartAmount, cartIsUpdating } from '$utils/stores/shop' import { cartOpen, cartData, cartAmount, cartIsUpdating } from '$utils/stores/shop'
import { initSwell, getCart, updateCartItem, removeCartItem } from '$utils/functions/shop' import { initSwell, getCart, updateCartItem, removeCartItem } from '$utils/functions/shop'
// Components // Components
@@ -13,6 +15,19 @@
import Icon from '$components/atoms/Icon.svelte' import Icon from '$components/atoms/Icon.svelte'
import CartItem from '$components/molecules/CartItem.svelte' import CartItem from '$components/molecules/CartItem.svelte'
import ShopLocationSwitcher from '$components/molecules/ShopLocationSwitcher.svelte' import ShopLocationSwitcher from '$components/molecules/ShopLocationSwitcher.svelte'
import { sendEvent } from '$utils/analytics';
// Block scroll if cart is open
$: if (browser && $smoothScroll) {
if ($cartOpen) {
$smoothScroll.stop()
} else {
$smoothScroll.start()
}
document.documentElement.classList.toggle('block-scroll', $cartOpen)
}
// Closing the cart // Closing the cart
@@ -117,7 +132,7 @@
{/if} {/if}
</div> </div>
<div class="cart__total--checkout"> <div class="cart__total--checkout">
<p>Shipping will be calculated from the delivery address during the checkout process</p> <p>Free shipping worldwide!</p>
{#if $cartData && $cartAmount > 0 && $cartData.checkout_url} {#if $cartData && $cartAmount > 0 && $cartData.checkout_url}
<div transition:fly={{ y: 8, duration: 600, easing: quartOut }}> <div transition:fly={{ y: 8, duration: 600, easing: quartOut }}>
<Button <Button
@@ -125,6 +140,7 @@
text="Checkout" text="Checkout"
color="pink" color="pink"
size="small" size="small"
on:click={() => sendEvent('cartCheckout', { props: { amount: $cartAmount }})}
/> />
</div> </div>
{/if} {/if}

View File

@@ -14,12 +14,12 @@
<footer class="footer"> <footer class="footer">
<div class="container grid"> <div class="container grid">
<a href="/" class="footer__title" data-sveltekit-prefetch data-sveltekit-noscroll tabindex="0"> <a href="/" class="footer__title" tabindex="0" data-sveltekit-noscroll>
<SiteTitle tag="div" /> <SiteTitle tag="div" />
</a> </a>
<nav class="footer__links"> <nav class="footer__links">
<ul data-sveltekit-prefetch data-sveltekit-noscroll> <ul data-sveltekit-noscroll>
{#each footer_links as { title, slug }} {#each footer_links as { title, slug }}
<li> <li>
<a href="/{slug}" class="link-3d" tabindex="0"> <a href="/{slug}" class="link-3d" tabindex="0">

View File

@@ -8,7 +8,7 @@
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
import { Globe, type Marker } from '$modules/globe' import { Globe, type Marker } from '$modules/globe'
import { getRandomItem, debounce } from '$utils/functions' import { getRandomItem, debounce } from '$utils/functions'
import reveal from '$animations/reveal' import { revealSplit } from '$animations/transitions'
// Components // Components
import SplitText from '$components/SplitText.svelte' import SplitText from '$components/SplitText.svelte'
@@ -131,7 +131,7 @@
<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}" data-sveltekit-noscroll <a href="/{country.slug}/{slug}" aria-label={name} data-sveltekit-noscroll
on:mouseenter={() => hoveredMarker = { name, country: country.name }} on:mouseenter={() => hoveredMarker = { name, country: country.name }}
on:mouseleave={() => hoveredMarker = null} on:mouseleave={() => hoveredMarker = null}
> >
@@ -145,19 +145,11 @@
{#if hoveredMarker} {#if hoveredMarker}
<div class="globe__location" <div class="globe__location"
transition:fade={{ duration: 300, easing: quartOut }} in:revealSplit={{ duration: 1 }}
use:reveal={{ out:fade={{ duration: 300, easing: quartOut }}
children: '.char',
animation: { y: ['110%', 0] },
options: {
stagger: 0.04,
duration: 1,
threshold: 0,
},
}}
> >
<SplitText text={hoveredMarker.name} mode="chars" class="name" /> <SplitText text={hoveredMarker.name} mode="chars" class="name" />
<p class="country" in:flySvelte={{ y: 16, duration: 800, easing: quartOut, delay: 900 }}> <p class="country" in:flySvelte={{ y: 16, duration: 800, easing: quartOut, delay: 700 }}>
{hoveredMarker.country} {hoveredMarker.country}
</p> </p>
</div> </div>

View File

@@ -54,7 +54,7 @@
class={'is-disabled'} class={'is-disabled'}
on:click={() => { on:click={() => {
filterLocation(slug) filterLocation(slug)
sendEvent({ action: 'filterContinent' }) sendEvent('filterContinent')
}} }}
> >
<svg width="12" height="12"> <svg width="12" height="12">

View File

@@ -14,9 +14,9 @@
<div class="newsletter newsletter--{theme} shadow-box-dark"> <div class="newsletter newsletter--{theme} shadow-box-dark">
<div class="newsletter__wrapper"> <div class="newsletter__wrapper">
<h3 class="title-medium"> <h2 class="title-medium">
<label for="newsletter_email">{newsletter_subtitle}</label> <label for="newsletter_email">{newsletter_subtitle}</label>
</h3> </h2>
<p class="text-small">{newsletter_text}</p> <p class="text-small">{newsletter_text}</p>
<EmailForm past={true} /> <EmailForm past={true} />

View File

@@ -7,7 +7,6 @@
import EmblaCarousel, { type EmblaCarouselType } from 'embla-carousel' import EmblaCarousel, { type EmblaCarouselType } from 'embla-carousel'
// Components // Components
import Poster from '$components/molecules/Poster.svelte' import Poster from '$components/molecules/Poster.svelte'
import EmailForm from '$components/molecules/EmailForm.svelte'
import { debounce } from '$utils/functions' import { debounce } from '$utils/functions'
export let posters: any = [] export let posters: any = []

View File

@@ -1,5 +1,5 @@
<style lang="scss"> <style lang="scss">
@import "../../style/pages/shop/intro"; @import "../../style/pages/shop/banner";
</style> </style>
<script lang="ts"> <script lang="ts">
@@ -8,7 +8,7 @@
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'
import { DELAY } from '$utils/contants' import { DELAY } from '$utils/constants'
import { quartOut } from '$animations/easings' import { quartOut } from '$animations/easings'
// Components // Components
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
@@ -19,6 +19,7 @@
const { shop, shopLocations }: any = getContext('shop') const { shop, shopLocations }: any = getContext('shop')
let innerWidth: number
let navObserver: IntersectionObserver let navObserver: IntersectionObserver
let introEl: HTMLElement, navChooseEl: HTMLElement let introEl: HTMLElement, navChooseEl: HTMLElement
let scrolledPastIntro = false let scrolledPastIntro = false
@@ -50,7 +51,7 @@
*/ */
const animation = timeline([ const animation = timeline([
// Hero image // Hero image
['.shop-page__background', { ['.background', {
scale: [1.06, 1], scale: [1.06, 1],
opacity: [0, 1], opacity: [0, 1],
z: 0, z: 0,
@@ -60,7 +61,7 @@
}], }],
// Intro top elements // Intro top elements
['.shop-page__intro .top > *', { ['.shop-banner .top > *', {
y: [-100, 0], y: [-100, 0],
opacity: [0, 1], opacity: [0, 1],
}, { }, {
@@ -69,7 +70,7 @@
}], }],
// Hero title // Hero title
['.shop-page__title h1', { ['.shop-banner .title h1', {
y: [32, 0], y: [32, 0],
opacity: [0, 1], opacity: [0, 1],
}, { }, {
@@ -77,7 +78,7 @@
}], }],
// Intro navbar // Intro navbar
['.shop-page__nav .container > *, .shop-page__intro .button-cart', { ['.shop-banner .nav .container > *, .shop-banner .button-cart', {
y: [100, 0], y: [100, 0],
opacity: [0, 1], opacity: [0, 1],
}, { }, {
@@ -104,7 +105,10 @@
}) })
</script> </script>
<section class="shop-page__intro" bind:this={introEl}> <svelte:window bind:innerWidth />
<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" xmlns="http://www.w3.org/2000/svg">
@@ -114,15 +118,15 @@
</a> </a>
</div> </div>
<div class="shop-page__title"> <div class="title">
<h1 class="title-big-sans">Shop</h1> <h1 class="title-big-sans">Shop</h1>
</div> </div>
<nav class="shop-page__nav"> <div class="nav">
<div class="container"> <div class="container">
<p class="text-label">Choose a city</p> <p class="text-label">Choose a city</p>
<nav> <nav>
<ul data-sveltekit-noscroll data-sveltekit-prefetch bind:this={navChooseEl}> <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}" on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}>
@@ -133,12 +137,12 @@
</ul> </ul>
</nav> </nav>
</div> </div>
</nav> </div>
<ButtonCart /> <ButtonCart />
<Image <Image
class="shop-page__background" class="background"
id={shop.page_heroimage.id} id={shop.page_heroimage.id}
alt={shop.page_heroimage.alt} alt={shop.page_heroimage.alt}
sizeKey="hero" sizeKey="hero"
@@ -151,10 +155,12 @@
/> />
</section> </section>
<nav class="shop-location" <nav class="shop-quicknav"
class:is-visible={scrolledPastIntro} class:is-visible={scrolledPastIntro}
class:is-overlaid={$cartOpen} class:is-overlaid={$cartOpen}
> >
{#if innerWidth > 768}
<ShopLocationSwitcher /> <ShopLocationSwitcher />
{/if}
<ButtonCart /> <ButtonCart />
</nav> </nav>

View File

@@ -62,7 +62,7 @@
<div class="content"> <div class="content">
<div class="shop__images"> <div class="shop__images">
{#if images} {#if images}
<a href={enabled ? url : undefined} title="Visit our shop" data-sveltekit-noscroll data-sveltekit-prefetch> <a href={enabled ? url : undefined} title="Visit our shop" data-sveltekit-noscroll>
{#each images as { directus_files_id: { id, title }}, index} {#each images as { directus_files_id: { id, title }}, index}
<Image <Image
class={index === currentImageIndex ? 'is-visible' : null} class={index === currentImageIndex ? 'is-visible' : null}
@@ -80,7 +80,7 @@
</div> </div>
<div class="shop__content"> <div class="shop__content">
<h3 class="title-medium">{title}</h3> <h2 class="title-medium">{title}</h2>
<p class="text-small">{text}</p> <p class="text-small">{text}</p>
{#if enabled} {#if enabled}
<Button {url} text={buttonText} color="pinklight" /> <Button {url} text={buttonText} color="pinklight" />

View File

@@ -1,6 +1,5 @@
// @ts-nocheck // @ts-nocheck
import { Renderer, Camera, Vec3, Orbit, Sphere, Transform, Program, Mesh, Texture } from 'ogl' import { Renderer, Camera, Vec3, Orbit, Sphere, Transform, Program, Mesh, Texture } from 'ogl'
import { map } from '$utils/functions'
// Shaders // Shaders
import VERTEX_SHADER from '$modules/globe/vertex.glsl?raw' import VERTEX_SHADER from '$modules/globe/vertex.glsl?raw'
import FRAGMENT_SHADER from '$modules/globe/frag.glsl?raw' import FRAGMENT_SHADER from '$modules/globe/frag.glsl?raw'
@@ -18,20 +17,20 @@ export class Globe {
this.zoom = 1.3075 this.zoom = 1.3075
// Calculate the current sun position from a given location // Calculate the current sun position from a given location
const locations = [ // const locations = [
{ // {
lat: -37.840935, // lat: -37.840935,
lng: 144.946457, // lng: 144.946457,
tz: 'Australia/Melbourne', // tz: 'Australia/Melbourne',
}, // },
{ // {
lat: 48.856614, // lat: 48.856614,
lng: 2.3522219, // lng: 2.3522219,
tz: 'Europe/Paris', // tz: 'Europe/Paris',
} // }
] // ]
const location = locations[1] // const location = locations[1]
const localDate = new Date(new Date().toLocaleString('en-US', { timeZone: location.tz })) // const localDate = new Date(new Date().toLocaleString('en-US', { timeZone: location.tz }))
// Parameters // Parameters
this.params = { this.params = {
@@ -122,8 +121,7 @@ export class Globe {
imgDark.src = this.options.mapFileDark imgDark.src = this.options.mapFileDark
// Create light // Create light
const dayTime = map(5, 0, 24, 0, 1, true) const lightD = degToRad(7 * 360 / 24)
const lightD = degToRad(360 / dayTime)
const sunPosition = new Vec3( const sunPosition = new Vec3(
Math.cos(lightD), Math.cos(lightD),
Math.sin(lightD) * Math.sin(0), Math.sin(lightD) * Math.sin(0),
@@ -263,6 +261,7 @@ export class Globe {
* Resize method * Resize method
*/ */
resize () { resize () {
if (this.renderer) {
this.width = this.el.offsetWidth this.width = this.el.offsetWidth
this.height = this.el.offsetHeight this.height = this.el.offsetHeight
this.renderer.setSize(this.width, this.height) this.renderer.setSize(this.width, this.height)
@@ -270,6 +269,7 @@ export class Globe {
aspect: this.gl.canvas.width / this.gl.canvas.height aspect: this.gl.canvas.width / this.gl.canvas.height
}) })
} }
}
/** /**

View File

@@ -25,7 +25,8 @@
/> />
<PageTransition name="shop-page"> <PageTransition>
<main class="shop-page">
<ShopHeader /> <ShopHeader />
<section class="shop-page__error"> <section class="shop-page__error">
@@ -38,4 +39,5 @@
</section> </section>
<PostersGrid {posters} /> <PostersGrid {posters} />
</main>
</PageTransition> </PageTransition>

View File

@@ -1,9 +1,9 @@
import { error } from '@sveltejs/kit' import { error } from '@sveltejs/kit'
import type { PageServerLoad } from './$types' import type { LayoutServerLoad } from './$types'
import { fetchAPI } from '$utils/api' import { fetchAPI } from '$utils/api'
import { fetchSwell } from '$utils/functions/shopServer' import { fetchSwell } from '$utils/functions/shopServer'
export const load: PageServerLoad = async () => { export const load: LayoutServerLoad = async () => {
try { try {
// Get content from API // Get content from API
const res = await fetchAPI(`query { const res = await fetchAPI(`query {

View File

@@ -1,5 +1,5 @@
<style lang="scss"> <style lang="scss">
@import "../../style/pages/shop"; @import "../../../style/pages/shop";
</style> </style>
<script lang="ts"> <script lang="ts">

View File

@@ -26,7 +26,8 @@
/> />
<PageTransition name="shop-page"> <PageTransition>
<main class="shop-page">
<ShopHeader {product} /> <ShopHeader {product} />
<PosterLayout <PosterLayout
@@ -35,4 +36,5 @@
/> />
<PostersGrid {posters} /> <PostersGrid {posters} />
</main>
</PageTransition> </PageTransition>

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types' import type { PageData } from './$types'
import { getContext } from 'svelte' import { getContext } from 'svelte'
import { getAssetUrlKey } from '$utils/api'
import { shopCurrentProductSlug } from '$utils/stores/shop' import { shopCurrentProductSlug } from '$utils/stores/shop'
import { capitalizeFirstLetter } from '$utils/functions' import { capitalizeFirstLetter } from '$utils/functions'
// Components // Components
@@ -19,12 +20,13 @@
<Metas <Metas
title="{data.product.location.name} {capitalizeFirstLetter(data.product.type)} Houses Of" title="{data.product.location.name} {capitalizeFirstLetter(data.product.type)} Houses Of"
description="" description={data.product.description}
image="" image={getAssetUrlKey(data.product.photos_product[2].directus_files_id.id, 'share-image')}
/> />
<PageTransition name="shop-page"> <PageTransition>
<main class="shop-page">
<ShopHeader product={data.product} /> <ShopHeader product={data.product} />
<PosterLayout <PosterLayout
@@ -33,4 +35,5 @@
/> />
<PostersGrid {posters} /> <PostersGrid {posters} />
</main>
</PageTransition> </PageTransition>

View File

@@ -1,9 +1,13 @@
import { error } from '@sveltejs/kit' import { error } from '@sveltejs/kit'
import type { PageServerLoad } from './$types' import type { Actions, PageServerLoad } from './$types'
import { PUBLIC_LIST_AMOUNT } from '$env/static/public' import { PUBLIC_LIST_AMOUNT } from '$env/static/public'
import { fetchAPI, photoFields } from '$utils/api' import { fetchAPI, photoFields } from '$utils/api'
import subscribe from '$utils/forms/subscribe'
/**
* Page Data
*/
export const load: PageServerLoad = async ({ params, setHeaders }) => { export const load: PageServerLoad = async ({ params, setHeaders }) => {
try { try {
const { location: slug } = params const { location: slug } = params
@@ -41,7 +45,8 @@ export const load: PageServerLoad = async ({ params, setHeaders }) => {
photos: photo ( photos: photo (
filter: { filter: {
location: { slug: { _eq: "${slug}" }} location: { slug: { _eq: "${slug}" }},
status: { _eq: "published" },
}, },
sort: "-date_created", sort: "-date_created",
limit: ${PUBLIC_LIST_AMOUNT}, limit: ${PUBLIC_LIST_AMOUNT},
@@ -56,7 +61,12 @@ export const load: PageServerLoad = async ({ params, setHeaders }) => {
} }
# Shop product # Shop product
product (filter: { location: { slug: { _eq: "${slug}" }}}) { product (
filter: {
location: { slug: { _eq: "${slug}" }},
status: { _eq: "published" },
}
) {
photos_product { photos_product {
directus_files_id { directus_files_id {
id id
@@ -83,3 +93,12 @@ export const load: PageServerLoad = async ({ params, setHeaders }) => {
throw error(500, err.message) throw error(500, err.message)
} }
} }
/**
* Form Data
*/
export const actions: Actions = {
// Form newsletter subscription
subscribe,
}

View File

@@ -1,5 +1,5 @@
<style lang="scss"> <style lang="scss">
@import "../../../style/pages/location"; @import "../../../../style/pages/location";
</style> </style>
<script lang="ts"> <script lang="ts">
@@ -11,7 +11,7 @@
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { quartOut } from '$animations/easings' import { quartOut } from '$animations/easings'
import { getAssetUrlKey } from '$utils/api' import { getAssetUrlKey } from '$utils/api'
import { DELAY } from '$utils/contants' import { DELAY } from '$utils/constants'
import { seenLocations } from '$utils/stores' import { seenLocations } from '$utils/stores'
import { photoFields } from '$utils/api' import { photoFields } from '$utils/api'
import { PUBLIC_LIST_INCREMENT } from '$env/static/public' import { PUBLIC_LIST_INCREMENT } from '$env/static/public'
@@ -210,16 +210,18 @@
}) })
</script> </script>
<svelte:window bind:scrollY />
<Metas <Metas
title="Houses Of {location.name}" title="Houses Of {location.name}"
description="Discover {totalPhotos} beautiful homes from {location.name}, {location.country.name}" description="Discover {totalPhotos} beautiful homes from {location.name}, {location.country.name}"
image={latestPhoto ? getAssetUrlKey(latestPhoto.image.id, 'share-image') : null} image={latestPhoto ? getAssetUrlKey(latestPhoto.image.id, 'share-image') : null}
/> />
<svelte:window bind:scrollY />
<PageTransition name="location-page"> <PageTransition>
<main class="location-page">
<section class="location-page__intro grid" bind:this={introEl}> <section class="location-page__intro grid" bind:this={introEl}>
<h1 class="title" class:is-short={location.name.length <= 4}> <h1 class="title" class:is-short={location.name.length <= 4}>
<span class="housesof mask"> <span class="housesof mask">
@@ -362,4 +364,5 @@
</p> </p>
</div> </div>
{/if} {/if}
</main>
</PageTransition> </PageTransition>

View File

@@ -1,5 +1,5 @@
<style lang="scss"> <style lang="scss">
@import "../../../../style/pages/viewer"; @import "../../../../../style/pages/viewer";
</style> </style>
<script lang="ts"> <script lang="ts">
@@ -14,7 +14,7 @@
import { stagger, timeline } from 'motion' import { stagger, timeline } from 'motion'
import { getAssetUrlKey } from '$utils/api' import { getAssetUrlKey } from '$utils/api'
import { previousPage } from '$utils/stores' import { previousPage } from '$utils/stores'
import { DELAY } from '$utils/contants' import { DELAY } from '$utils/constants'
import { throttle } from '$utils/functions' import { throttle } from '$utils/functions'
import { swipe } from '$utils/interactions/swipe' import { swipe } from '$utils/interactions/swipe'
// Components // Components
@@ -291,10 +291,7 @@
}) })
</script> </script>
<svelte:window <svelte:window bind:innerWidth on:keydown={handleKeydown} />
bind:innerWidth
on:keydown={handleKeydown}
/>
{#if currentPhoto} {#if currentPhoto}
<Metas <Metas
@@ -305,7 +302,8 @@
{/if} {/if}
<PageTransition name="photo-page"> <PageTransition>
<main class="photo-page">
<div class="container grid"> <div class="container grid">
<p class="photo-page__notice text-label">Tap for fullscreen</p> <p class="photo-page__notice text-label">Tap for fullscreen</p>
@@ -359,7 +357,7 @@
<h1 class="title-medium">{currentPhoto.title}</h1> <h1 class="title-medium">{currentPhoto.title}</h1>
<div class="detail text-info"> <div class="detail text-info">
<a href="/{location.country.slug}/{location.slug}" data-sveltekit-prefetch data-sveltekit-noscroll> <a href="/{location.country.slug}/{location.slug}" data-sveltekit-noscroll>
<Icon class="icon" icon="map-pin" label="Map pin" /> <Icon class="icon" icon="map-pin" label="Map pin" />
<span> <span>
{#if currentPhoto.city} {#if currentPhoto.city}
@@ -399,4 +397,5 @@
</div> </div>
</div> </div>
{/if} {/if}
</main>
</PageTransition> </PageTransition>

View File

@@ -0,0 +1,402 @@
<style lang="scss">
@import "../../../style/pages/about";
</style>
<script lang="ts">
import { navigating, page } from '$app/stores'
import { onMount, afterUpdate } from 'svelte'
import { quartOut as quartOutSvelte } from 'svelte/easing'
import { fade, fly } from 'svelte/transition'
import type { PageData } from './$types'
import { animate, inView, stagger, timeline } from 'motion'
import { mailtoClipboard, map } from '$utils/functions'
import { getAssetUrlKey } from '$utils/api'
import { DELAY } from '$utils/constants'
import { quartOut } from '$animations/easings'
// Components
import Metas from '$components/Metas.svelte'
import PageTransition from '$components/PageTransition.svelte'
import Image from '$components/atoms/Image.svelte'
import Button from '$components/atoms/Button.svelte'
import AboutGridPhoto from '$components/atoms/AboutGridPhoto.svelte'
import ProcessStep from '$components/molecules/ProcessStep.svelte'
import Banner from '$components/organisms/Banner.svelte'
import { sendEvent } from '$utils/analytics';
export let data: PageData
const { about, photos } = data
let scrollY: number, innerWidth: number, innerHeight: number
let photosGridEl: HTMLElement
let photosGridOffset: number = photosGridEl && photosGridEl.offsetTop
let currentStep: number = 0
let emailCopied: string = null
let emailCopiedTimeout: ReturnType<typeof setTimeout> | number
$: parallaxPhotos = photosGridEl && map(scrollY, photosGridOffset - innerHeight, photosGridOffset + innerHeight / 1.5, 0, innerHeight * 0.15, true)
$: fadedPhotosIndexes = innerWidth > 768
? [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]
onMount(() => {
/**
* Animations
*/
const animation = timeline([
// Banner
['.banner picture', {
scale: [1.06, 1],
opacity: [0, 1],
z: 0,
}, {
at: 0.4,
duration: 2.4,
}],
['.banner h1', {
y: [32, 0],
opacity: [0, 1],
}, {
at: 0.5,
}],
['.banner__top > *', {
y: [-100, 0],
opacity: [0, 1],
}, {
at: 0.4,
delay: stagger(0.25),
}],
// Intro elements
['.about__introduction .container > *', {
y: ['20%', 0],
opacity: [0, 1],
z: 0,
}, {
at: 0.75,
delay: stagger(0.25),
}],
['.first-photo', {
y: ['10%', 0],
opacity: [0, 1],
z: 0,
}, {
at: 1.2,
}],
['.first-photo img', {
scale: [1.06, 1],
opacity: [0, 1],
z: 0,
}, {
at: 1.5,
duration: 2.4,
}],
], {
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
})
animation.stop()
// Sections
inView('[data-reveal]', ({ target }) => {
animate(target, {
opacity: [0, 1],
y: ['20%', 0],
z: 0,
}, {
delay: 0.2,
duration: 1.6,
easing: quartOut,
})
})
// Global images
inView('[data-reveal-image] img', ({ target }) => {
animate(target, {
scale: [1.06, 1],
opacity: [0, 1],
z: 0,
}, {
delay: 0.3,
duration: 2.4,
easing: quartOut,
})
})
// Process
const processTimeline = timeline([
// Step links
['.about__process li a', {
y: [16, 0],
opacity: [0, 1],
z: 0,
}, {
at: 0,
delay: stagger(0.15),
}],
// First step
['.about__process .step', {
scale: [1.1, 1],
opacity: [0, 1],
x: [20, 0]
}, {
at: 0.6,
duration: 1,
}]
], {
defaultOptions: {
duration: 1.6,
easing: quartOut,
}
})
processTimeline.stop()
inView('.about__process', () => {
requestAnimationFrame(processTimeline.play)
}, {
amount: 0.35,
})
// Run animation
requestAnimationFrame(animation.play)
})
afterUpdate(() => {
// Update photos grid top offset
photosGridOffset = photosGridEl.offsetTop
})
</script>
<svelte:window bind:scrollY bind:innerWidth bind:innerHeight />
<Metas
title={about.seo_title}
description={about.seo_description}
image={about.seo_image ? getAssetUrlKey(about.seo_image.id, 'share-image') : null}
/>
<PageTransition>
<main class="about">
<Banner
title="About"
image={{
id: '699b4050-6bbf-4a40-be53-d84aca484f9d',
alt: 'Photo caption',
}}
/>
<section class="about__introduction">
<div class="container grid">
<h2 class="title-small">{about.intro_title}</h2>
<div class="heading text-big">
{@html about.intro_heading}
</div>
<div class="text text-small">
{@html about.intro_text}
</div>
</div>
</section>
<section class="about__creation">
<div class="container grid">
<figure class="first-photo">
<Image
class="picture shadow-box-dark"
id={about.intro_firstphoto.id}
alt={about.intro_firstphoto.title}
sizeKey="photo-list"
sizes={{
small: { width: 400 },
medium: { width: 600 },
large: { width: 800 },
}}
ratio={1.5}
/>
<figcaption class="text-info">
{about.intro_firstphoto_caption}<br>
in
<a href="/{about.intro_firstlocation.country.slug}/{about.intro_firstlocation.slug}" data-sveltekit-noscroll>
<img src="{getAssetUrlKey(about.intro_firstlocation.country.flag.id, 'square-small-jpg')}" width="32" height="32" alt="{about.intro_firstlocation.country.flag.title}">
<span>Naarm Australia (Melbourne)</span>
</a>
</figcaption>
</figure>
<h2 class="title-small" data-reveal>{about.creation_title}</h2>
<div class="heading text-huge" data-reveal>
{@html about.creation_heading}
</div>
<div class="text text-small" data-reveal>
{@html about.creation_text}
</div>
<figure class="picture portrait-photo" data-reveal-image>
<Image
class="shadow-box-dark"
id={about.creation_portrait.id}
alt={about.creation_portrait.title}
sizeKey="photo-list"
sizes={{
small: { width: 400 },
medium: { width: 750 },
}}
ratio={1.425}
/>
</figure>
<span class="portrait-photo__caption text-info">
{about.creation_portrait_caption}
</span>
</div>
</section>
<section class="about__present">
<div class="container grid">
<figure class="picture" data-reveal-image>
<Image
class="shadow-box-dark"
id={about.present_image.id}
alt={about.present_image.title}
sizeKey="photo-list"
sizes={{
small: { width: 400 },
medium: { width: 600 },
large: { width: 800 },
}}
ratio={1.5}
/>
</figure>
<h2 class="title-small" data-reveal>{about.present_title}</h2>
<div class="text text-small" data-reveal>
<p>{about.present_text}</p>
</div>
<div class="heading text-big" data-reveal>
{@html about.present_heading}
</div>
<div class="conclusion text-small" data-reveal>
<p>{about.present_conclusion}</p>
</div>
</div>
</section>
{#if about.image_showcase}
<div class="about__showcase container grid">
<Image
id={about.image_showcase.id}
alt={about.image_showcase.title}
sizeKey="showcase"
sizes={{
small: { width: 400 },
medium: { width: 1000 },
large: { width: 1800 },
}}
ratio={1.2}
/>
</div>
{/if}
<section class="about__process">
<div class="container grid">
<aside>
<div class="heading">
<h2 class="title-medium">{about.process_title}</h2>
<p class="text-xsmall">{about.process_subtitle}</p>
</div>
<ol>
{#each about.process_steps as { title }, index}
<li class:is-active={index === currentStep}>
<a href="#step-{index + 1}" class="title-big"
on:click|preventDefault={() => {
currentStep = index
sendEvent('aboutStepSwitch')
}}
>
<span>{title}</span>
</a>
</li>
{/each}
</ol>
</aside>
<div class="steps">
{#each about.process_steps as { text, image, video_mp4, video_webm }, index}
{#if index === currentStep}
<ProcessStep
{index} {text}
image={image ?? undefined}
video={{
mp4: video_mp4?.id,
webm: video_webm?.id
}}
/>
{/if}
{/each}
</div>
</div>
</section>
<section class="about__photos" bind:this={photosGridEl}>
<div class="container-wide">
<div class="photos-grid" style:--parallax-y="{parallaxPhotos}px">
{#each photos as { image: { id }, title }, index}
<AboutGridPhoto class="grid-photo"
{id}
alt={title}
disabled={fadedPhotosIndexes.includes(index)}
/>
{/each}
</div>
</div>
</section>
<section class="about__interest container grid">
<div class="container grid">
<h2 class="title-xl">{about.contact_title}</h2>
<div class="blocks">
{#each about.contact_blocks as { title, text, link, button }}
<div class="block">
<h3 class="text-label">{title}</h3>
<div class="text text-normal">
{@html text}
</div>
<div class="button-container">
{#if link}
{#key emailCopied === link}
<div class="wrap"
in:fly={{ y: 4, duration: 325, easing: quartOutSvelte, delay: 250 }}
out:fade={{ duration: 250, easing: quartOutSvelte }}
use:mailtoClipboard
on:copied={({ detail }) => {
emailCopied = detail.email
// Clear timeout and add timeout to hide message
clearTimeout(emailCopiedTimeout)
emailCopiedTimeout = setTimeout(() => emailCopied = null, 2500)
}}
>
{#if emailCopied !== link}
<Button size="small" url="mailto:{link}" text={button} />
{:else}
<span class="clipboard">Email copied in clipboard</span>
{/if}
</div>
{/key}
{/if}
</div>
</div>
{/each}
</div>
</div>
</section>
</main>
</PageTransition>

View File

@@ -0,0 +1,149 @@
<style lang="scss">
@import "../../../style/pages/credits";
</style>
<script lang="ts">
import { navigating } from '$app/stores'
import type { PageData } from './$types'
import { onMount } from 'svelte'
import { stagger, timeline } from 'motion'
import { DELAY } from '$utils/constants'
import { quartOut } from 'svelte/easing'
// Components
import Metas from '$components/Metas.svelte'
import PageTransition from '$components/PageTransition.svelte'
import Image from '$components/atoms/Image.svelte'
import Heading from '$components/molecules/Heading.svelte'
import InteractiveGlobe from '$components/organisms/InteractiveGlobe.svelte'
export let data: PageData
const { credit } = data
onMount(() => {
/**
* Animations
*/
const animation = timeline([
// Heading
['.heading .text', {
y: [24, 0],
opacity: [0, 1],
}],
// Categories
['.credits__category', {
opacity: [0, 1],
}, {
at: 0,
delay: stagger(0.35, { start: 0.5 }),
}],
// Names
['.credits__category > ul > li', {
y: [24, 0],
opacity: [0, 1],
}, {
at: 1.1,
delay: stagger(0.35),
}],
], {
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
})
animation.stop()
// Run animation
requestAnimationFrame(animation.play)
})
</script>
<Metas
title="Credits Houses Of"
description={data.credits.text}
/>
<PageTransition>
<main class="credits">
<Heading
text={data.credits.text}
/>
<section class="credits__list">
<div class="grid container">
{#each data.credits.list as { title, credits }}
<div class="credits__category grid">
<h2 class="title-small">{title}</h2>
<ul>
{#each credits as { name, role, website }}
<li>
<dl>
<dt>
{#if website}
<h3>
<a href={website} rel="noopener external" target="_blank" tabindex="0">{name}</a>
</h3>
{:else}
<h3>{name}</h3>
{/if}
</dt>
<dd>
{role}
</dd>
</dl>
</li>
{/each}
</ul>
</div>
{/each}
<div class="credits__category grid">
<h2 class="title-small">Photography</h2>
<ul>
{#each credit as { name, website, location }}
<li>
<dl>
<dt>
{#if website}
<h3>
<a href={website} rel="noopener external" target="_blank" tabindex="0">{name}</a>
</h3>
{:else}
<h3>{name}</h3>
{/if}
</dt>
<dd>
<ul data-sveltekit-noscroll>
{#each location as loc}
{#if loc.location_id}
<li>
<a href="/{loc.location_id.country.slug}/{loc.location_id.slug}" tabindex="0">
<Image
id={loc.location_id.country.flag.id}
sizeKey="square-small"
width={16}
height={16}
alt="Flag of {loc.location_id.country.slug}"
/>
<span>{loc.location_id.name}</span>
</a>
</li>
{/if}
{/each}
</ul>
</dd>
</dl>
</li>
{/each}
</ul>
</div>
</div>
</section>
<InteractiveGlobe type="cropped" />
</main>
</PageTransition>

View File

@@ -0,0 +1,94 @@
import { error } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
import { fetchSwell } from '$utils/functions/shopServer'
import { fetchAPI, getAssetUrlKey } from '$utils/api'
const gCategories = [
{
id: '61851d83cd16416c78a8e5ef',
type: 'Posters, Prints, &amp; Visual Artwork',
value: 'Home &amp; Garden &gt; Decor &gt; Artwork &gt; Posters, Prints, &amp; Visual Artwork'
}
]
export const GET: RequestHandler = async ({ url, setHeaders }) => {
try {
const products = []
// Get products from Swell API
const shopProducts: any = await fetchSwell(`/products`)
// Get products from site API
const siteProducts = await fetchAPI(`query {
products: product (filter: { status: { _eq: "published" }}) {
location { slug }
name
description
details
product_id
photos_product {
directus_files_id { id }
}
}
}`)
if (shopProducts && siteProducts) {
const { data } = siteProducts
// Loop through shop products
shopProducts.results.forEach((product: any) => {
// Find matching product from site to platform
const siteProduct = data.products.find((p: any) => p.product_id === product.id)
const category = gCategories.find(p => p.id === product.category_index.id[0])
products.push({
id: product.id,
name: `${product.name} - Poster`,
slug: siteProduct.location.slug,
description: siteProduct.description,
price: product.price,
images: siteProduct.photos_product.map(({ directus_files_id: { id }}: any) => getAssetUrlKey(id, `product-large-jpg`)),
gCategory: category.value,
gType: category.type,
})
})
}
const sitemap = render(url.origin, products)
setHeaders({
'Content-Type': 'application/xml',
'Cache-Control': 'max-age=0, s-max-age=600',
})
return new Response(sitemap)
} catch (err) {
throw error(500, err.message)
}
}
const render = (origin: string, products: any[]) => {
return `<?xml version="1.0"?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
<channel>
${products.map((product) => `<item>
<g:id>${product.id}</g:id>
<title>${product.name}</title>
<description>${product.description}</description>
<g:product_type>${product.gType}</g:product_type>
<g:google_product_category>${product.gCategory}</g:google_product_category>
<link>${origin}/shop/poster-${product.slug}</link>
<g:image_link>${product.images[0]}</g:image_link>
<g:condition>New</g:condition>
<g:availability>In Stock</g:availability>
<g:price>${product.price} EUR</g:price>
<g:brand>Houses Of</g:brand>
<g:identifier_exists>FALSE</g:identifier_exists>
${product.images.slice(1).map((image: any) => `
<g:additional_image_link>${image}</g:additional_image_link>
`).join('')}
</item>
`).join('')}
</channel>
</rss>`
}

View File

@@ -0,0 +1,8 @@
import type { Actions } from './$types'
import subscribe from '$utils/forms/subscribe'
export const actions: Actions = {
// Form newsletter subscription
subscribe,
}

View File

@@ -1,5 +1,5 @@
<style lang="scss"> <style lang="scss">
@import "../../style/pages/explore"; @import "../../../style/pages/explore";
</style> </style>
<script lang="ts"> <script lang="ts">
@@ -20,10 +20,11 @@
<Metas <Metas
title="Locations Houses Of" title="Locations Houses Of"
description={text} description={text}
image=""
/> />
<PageTransition name="explore">
<PageTransition>
<main class="explore">
<Heading {text} /> <Heading {text} />
<section class="explore__locations"> <section class="explore__locations">
@@ -37,4 +38,5 @@
<NewsletterModule /> <NewsletterModule />
</div> </div>
</section> </section>
</main>
</PageTransition> </PageTransition>

View File

@@ -1,9 +1,13 @@
import { error } from '@sveltejs/kit' import { error } from '@sveltejs/kit'
import type { PageServerLoad } from './$types' import type { Actions, PageServerLoad } from './$types'
import { fetchAPI } from '$utils/api' import { fetchAPI } from '$utils/api'
import { PUBLIC_FILTERS_DEFAULT_COUNTRY, PUBLIC_FILTERS_DEFAULT_SORT, PUBLIC_GRID_AMOUNT } from '$env/static/public' import { PUBLIC_FILTERS_DEFAULT_COUNTRY, PUBLIC_FILTERS_DEFAULT_SORT, PUBLIC_GRID_AMOUNT } from '$env/static/public'
import subscribe from '$utils/forms/subscribe'
/**
* Page Data
*/
export const load: PageServerLoad = async ({ url, setHeaders }) => { export const load: PageServerLoad = async ({ url, setHeaders }) => {
try { try {
// Query parameters // Query parameters
@@ -86,3 +90,12 @@ export const load: PageServerLoad = async ({ url, setHeaders }) => {
throw error(500, err.message) throw error(500, err.message)
} }
} }
/**
* Form Data
*/
export const actions: Actions = {
// Form newsletter subscription
subscribe,
}

View File

@@ -1,5 +1,5 @@
<style lang="scss"> <style lang="scss">
@import "../../style/pages/photos"; @import "../../../style/pages/photos";
</style> </style>
<script lang="ts"> <script lang="ts">
@@ -12,7 +12,7 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { stagger, timeline } from 'motion' import { stagger, timeline } from 'motion'
import { DELAY } from '$utils/contants' import { DELAY } from '$utils/constants'
import { map, lerp, throttle } from '$utils/functions' import { map, lerp, throttle } from '$utils/functions'
import { getAssetUrlKey } from '$utils/api' import { getAssetUrlKey } from '$utils/api'
import { quartOut } from '$animations/easings' import { quartOut } from '$animations/easings'
@@ -336,7 +336,8 @@
/> />
<PageTransition name="photos-page"> <PageTransition>
<main class="photos-page">
<section class="photos-page__intro" <section class="photos-page__intro"
class:is-passed={scrolledPastIntro} class:is-passed={scrolledPastIntro}
> >
@@ -392,13 +393,13 @@
options={[ options={[
{ {
value: 'latest', value: 'latest',
name: 'Latest photos', name: 'Latest',
default: true, default: true,
selected: filterSort === defaultSort selected: filterSort === defaultSort
}, },
{ {
value: 'oldest', value: 'oldest',
name: 'Oldest photos', name: 'Oldest',
selected: filterSort === 'oldest' selected: filterSort === 'oldest'
}, },
]} ]}
@@ -429,7 +430,7 @@
<section class="photos-page__content" bind:this={photosContentEl} style:--margin-sides="{sideMargins}px"> <section class="photos-page__content" bind:this={photosContentEl} style:--margin-sides="{sideMargins}px">
<div class="grid container"> <div class="grid container">
{#if photos} {#if photos}
<div class="photos-page__grid" bind:this={photosGridEl} data-sveltekit-noscroll data-sveltekit-prefetch> <div class="photos-page__grid" bind:this={photosGridEl} data-sveltekit-noscroll>
{#each photos as { id, image, slug, location, title, city }, index (id)} {#each photos as { id, image, slug, location, title, city }, index (id)}
<figure class="photo shadow-photo"> <figure class="photo shadow-photo">
<a href="/{location.country.slug}/{location.slug}/{slug}" tabindex="0"> <a href="/{location.country.slug}/{location.slug}/{slug}" tabindex="0">
@@ -494,4 +495,5 @@
</div> </div>
</div> </div>
</section> </section>
</main>
</PageTransition> </PageTransition>

View File

@@ -1,7 +1,12 @@
import { error } from '@sveltejs/kit' import { error } from '@sveltejs/kit'
import type { PageServerLoad } from './$types' import type { Actions, PageServerLoad } from './$types'
import { fetchAPI } from '$utils/api' import { fetchAPI } from '$utils/api'
import subscribe from '$utils/forms/subscribe'
/**
* Page Data
*/
export const load: PageServerLoad = async ({ setHeaders }) => { export const load: PageServerLoad = async ({ setHeaders }) => {
try { try {
const res = await fetchAPI(`query { const res = await fetchAPI(`query {
@@ -36,3 +41,12 @@ export const load: PageServerLoad = async ({ setHeaders }) => {
throw error(500, err.message) throw error(500, err.message)
} }
} }
/**
* Form Data
*/
export const actions: Actions = {
// Form newsletter subscription
subscribe,
}

View File

@@ -1,5 +1,5 @@
<style lang="scss"> <style lang="scss">
@import "../../style/pages/subscribe"; @import "../../../style/pages/subscribe";
</style> </style>
<script lang="ts"> <script lang="ts">
@@ -7,7 +7,7 @@
import type { PageData } from './$types' import type { PageData } from './$types'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { stagger, timeline } from 'motion' import { stagger, timeline } from 'motion'
import { DELAY } from '$utils/contants' import { DELAY } from '$utils/constants'
import { quartOut } from '$animations/easings' import { quartOut } from '$animations/easings'
// Components // Components
import Metas from '$components/Metas.svelte' import Metas from '$components/Metas.svelte'
@@ -65,7 +65,9 @@
description="Subscribe to the Houses Of newsletter to be notified when new photos or locations are added to the site and when more prints are available on our shop" description="Subscribe to the Houses Of newsletter to be notified when new photos or locations are added to the site and when more prints are available on our shop"
/> />
<PageTransition name="subscribe">
<PageTransition>
<main class="subscribe">
<div class="subscribe__top"> <div class="subscribe__top">
<Heading <Heading
text={data.newsletter_page_text} text={data.newsletter_page_text}
@@ -93,4 +95,5 @@
</section> </section>
<InteractiveGlobe type="cropped" /> <InteractiveGlobe type="cropped" />
</main>
</PageTransition> </PageTransition>

View File

@@ -0,0 +1,49 @@
<style lang="scss">
@import "../../../style/pages/terms";
</style>
<script lang="ts">
import type { PageData } from './$types'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
// Components
import PageTransition from '$components/PageTransition.svelte'
import Metas from '$components/Metas.svelte'
import Heading from '$components/molecules/Heading.svelte'
export let data: PageData
const { legal } = data
dayjs.extend(relativeTime)
</script>
<Metas
title="Terms and Conditions Houses Of"
description="Everything you need to know about using our website or buying our products"
/>
<PageTransition>
<main class="terms">
<Heading text="Everything you need to know about using our website or buying our products" />
<section class="terms__categories">
<div class="container grid">
{#each legal.terms as { title, text }, index}
<article class="terms__section grid">
<h2 class="title-small">{index + 1}. {title}</h2>
<div class="text text-info">
{@html text}
</div>
</article>
{/each}
<footer>
<p class="text-info">
Updated: <time datetime={dayjs(legal.date_updated).format('YYYY-MM-DD')}>{dayjs().to(dayjs(legal.date_updated))}</time>
</p>
</footer>
</div>
</section>
</main>
</PageTransition>

View File

@@ -34,7 +34,9 @@
title="{errors[$page.status].title} Houses Of" title="{errors[$page.status].title} Houses Of"
/> />
<PageTransition name="page-error">
<PageTransition>
<main class="page-error">
<div class="page-error__top"> <div class="page-error__top">
<Heading <Heading
text="{$page.error.message ?? errors[$page.status].message} <br>{defaultMessage}" text="{$page.error.message ?? errors[$page.status].message} <br>{defaultMessage}"
@@ -79,4 +81,5 @@
</div> </div>
</div> </div>
</div> </div>
</main>
</PageTransition> </PageTransition>

View File

@@ -1,10 +1,10 @@
import { error } from '@sveltejs/kit' import { error } from '@sveltejs/kit'
import type { PageServerLoad } from './$types' import type { LayoutServerLoad } from './$types'
import { fetchAPI } from '$utils/api' import { fetchAPI } from '$utils/api'
import { PUBLIC_PREVIEW_COUNT } from '$env/static/public' import { PUBLIC_PREVIEW_COUNT } from '$env/static/public'
export const load: PageServerLoad = async () => { export const load: LayoutServerLoad = async () => {
try { try {
const res = await fetchAPI(`query { const res = await fetchAPI(`query {
locations: location (filter: { status: { _eq: "published" }}) { locations: location (filter: { status: { _eq: "published" }}) {

View File

@@ -2,14 +2,13 @@
import '../style/global.scss' import '../style/global.scss'
import { browser } from '$app/environment' import { browser } from '$app/environment'
import { navigating, page } from '$app/stores' import { page } from '$app/stores'
import { beforeNavigate } from '$app/navigation' import { beforeNavigate } from '$app/navigation'
import { PUBLIC_ANALYTICS_DOMAIN } from '$env/static/public'
import type { PageData } from './$types' import type { PageData } from './$types'
import { onMount, setContext } from 'svelte' import { onMount, setContext } from 'svelte'
import { pageLoading, previousPage } from '$utils/stores' import { pageLoading, previousPage } from '$utils/stores'
import { DURATION } from '$utils/contants'
import '$utils/polyfills' import '$utils/polyfills'
import { PUBLIC_ANALYTICS_KEY, PUBLIC_ANALYTICS_URL } from '$env/static/public'
// Components // Components
import SVGSprite from '$components/SVGSprite.svelte' import SVGSprite from '$components/SVGSprite.svelte'
import SmoothScroll from '$components/SmoothScroll.svelte' import SmoothScroll from '$components/SmoothScroll.svelte'
@@ -44,17 +43,9 @@
$previousPage = from.url.pathname $previousPage = from.url.pathname
}) })
// Define page loading from navigating store // Define page loading
navigating.subscribe((store: any) => { $: browser && document.body.classList.toggle('is-loading', $pageLoading)
if (store) {
$pageLoading = true
// Turn page loading when changing page
setTimeout(() => {
$pageLoading = false
}, DURATION.PAGE_IN * 1.25)
}
})
onMount(() => { onMount(() => {
// Avoid FOUC // Avoid FOUC
@@ -65,6 +56,8 @@
<svelte:window bind:innerHeight /> <svelte:window bind:innerHeight />
<svelte:head> <svelte:head>
<link rel="canonical" href={$page.url.href} />
{#each fonts as font} {#each fonts as font}
<link rel="preload" href="/fonts/{font}.woff2" as="font" type="font/woff2" crossorigin="anonymous"> <link rel="preload" href="/fonts/{font}.woff2" as="font" type="font/woff2" crossorigin="anonymous">
{/each} {/each}
@@ -79,16 +72,9 @@
<Footer /> <Footer />
{/if} {/if}
{#if $pageLoading}
<div class="page-loading" />
{/if}
<SVGSprite /> <SVGSprite />
<SmoothScroll /> <SmoothScroll />
{#if browser} {#if browser}
<Analytics <Analytics domain={PUBLIC_ANALYTICS_DOMAIN} />
appKey={PUBLIC_ANALYTICS_KEY}
url={PUBLIC_ANALYTICS_URL}
/>
{/if} {/if}

View File

@@ -1,8 +1,13 @@
import { error } from '@sveltejs/kit' import { error } from '@sveltejs/kit'
import type { PageServerLoad } from './$types' import type { Actions, PageServerLoad } from './$types'
import { fetchAPI } from '$utils/api' import { fetchAPI } from '$utils/api'
import { getRandomItems } from '$utils/functions' import { getRandomItems } from '$utils/functions'
import subscribe from '$utils/forms/subscribe'
/**
* Page Data
*/
export const load: PageServerLoad = async ({ setHeaders }) => { export const load: PageServerLoad = async ({ setHeaders }) => {
try { try {
// Get total of published photos // Get total of published photos
@@ -53,3 +58,12 @@ export const load: PageServerLoad = async ({ setHeaders }) => {
throw error(500, err.message) throw error(500, err.message)
} }
} }
/**
* Form Data
*/
export const actions: Actions = {
// Form newsletter subscription
subscribe,
}

View File

@@ -3,11 +3,11 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { navigating } from '$app/stores' import { navigating, page } from '$app/stores'
import type { PageData } from './$types' import type { PageData } from './$types'
import { getContext, onMount } from 'svelte' import { getContext, onMount } from 'svelte'
import { timeline, stagger } from 'motion' import { timeline, stagger } from 'motion'
import { DELAY } from '$utils/contants' import { DELAY } from '$utils/constants'
import { smoothScroll } from '$utils/stores' import { smoothScroll } from '$utils/stores'
import { getAssetUrlKey } from '$utils/api' import { getAssetUrlKey } from '$utils/api'
import reveal from '$animations/reveal' import reveal from '$animations/reveal'
@@ -81,7 +81,9 @@
image={getAssetUrlKey(settings.seo_image.id, 'share-image')} image={getAssetUrlKey(settings.seo_image.id, 'share-image')}
/> />
<PageTransition name="homepage">
<PageTransition>
<main class="homepage">
<section class="homepage__intro" <section class="homepage__intro"
use:reveal={{ use:reveal={{
animation: { opacity: [0, 1] }, animation: { opacity: [0, 1] },
@@ -164,4 +166,5 @@
</div> </div>
</div> </div>
</div> </div>
</main>
</PageTransition> </PageTransition>

View File

@@ -1,394 +0,0 @@
<style lang="scss">
@import "../../style/pages/about";
</style>
<script lang="ts">
import { navigating, page } from '$app/stores'
import { onMount, afterUpdate } from 'svelte'
import { quartOut as quartOutSvelte } from 'svelte/easing'
import { fade, fly } from 'svelte/transition'
import type { PageData } from './$types'
import { animate, inView, stagger, timeline } from 'motion'
import { mailtoClipboard, map } from '$utils/functions'
import { getAssetUrlKey } from '$utils/api'
import { DELAY } from '$utils/contants'
import { quartOut } from '$animations/easings'
// Components
import Metas from '$components/Metas.svelte'
import PageTransition from '$components/PageTransition.svelte'
import Image from '$components/atoms/Image.svelte'
import Button from '$components/atoms/Button.svelte'
import AboutGridPhoto from '$components/atoms/AboutGridPhoto.svelte'
import ProcessStep from '$components/molecules/ProcessStep.svelte'
import Banner from '$components/organisms/Banner.svelte'
export let data: PageData
const { about, photos } = data
let scrollY: number, innerWidth: number, innerHeight: number
let photosGridEl: HTMLElement
let currentStep: number = 0
let photosGridOffset: number = photosGridEl && photosGridEl.offsetTop
let emailCopied: string = null
let emailCopiedTimeout: ReturnType<typeof setTimeout> | number
$: currentStep = $page.url.hash ? Number($page.url.hash.split('#step-')[1]) - 1 : 0
$: parallaxPhotos = photosGridEl && map(scrollY, photosGridOffset - innerHeight, photosGridOffset + innerHeight / 1.5, 0, innerHeight * 0.15, true)
$: fadedPhotosIndexes = innerWidth > 768
? [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]
onMount(() => {
/**
* Animations
*/
const animation = timeline([
// Banner
['.banner picture', {
scale: [1.06, 1],
opacity: [0, 1],
z: 0,
}, {
at: 0.4,
duration: 2.4,
}],
['.banner h1', {
y: [32, 0],
opacity: [0, 1],
}, {
at: 0.5,
}],
['.banner__top > *', {
y: [-100, 0],
opacity: [0, 1],
}, {
at: 0.4,
delay: stagger(0.25),
}],
// Intro elements
['.about__introduction .container > *', {
y: ['20%', 0],
opacity: [0, 1],
z: 0,
}, {
at: 0.75,
delay: stagger(0.25),
}],
['.first-photo', {
y: ['10%', 0],
opacity: [0, 1],
z: 0,
}, {
at: 1.2,
}],
['.first-photo img', {
scale: [1.06, 1],
opacity: [0, 1],
z: 0,
}, {
at: 1.5,
duration: 2.4,
}],
], {
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
})
animation.stop()
// Sections
inView('[data-reveal]', ({ target }) => {
animate(target, {
opacity: [0, 1],
y: ['20%', 0],
z: 0,
}, {
delay: 0.2,
duration: 1.6,
easing: quartOut,
})
})
// Global images
inView('[data-reveal-image] img', ({ target }) => {
animate(target, {
scale: [1.06, 1],
opacity: [0, 1],
z: 0,
}, {
delay: 0.3,
duration: 2.4,
easing: quartOut,
})
})
// Process
const processTimeline = timeline([
// Step links
['.about__process li a', {
y: [16, 0],
opacity: [0, 1],
z: 0,
}, {
at: 0,
delay: stagger(0.15),
}],
// First step
['.about__process .step', {
scale: [1.1, 1],
opacity: [0, 1],
x: [20, 0]
}, {
at: 0.6,
duration: 1,
}]
], {
defaultOptions: {
duration: 1.6,
easing: quartOut,
}
})
processTimeline.stop()
inView('.about__process', () => {
requestAnimationFrame(processTimeline.play)
}, {
amount: 0.35,
})
// Run animation
requestAnimationFrame(animation.play)
})
afterUpdate(() => {
// Update photos grid top offset
photosGridOffset = photosGridEl.offsetTop
})
</script>
<svelte:window bind:scrollY bind:innerWidth bind:innerHeight />
<Metas
title={about.seo_title}
description={about.seo_description}
image={about.seo_image ? getAssetUrlKey(about.seo_image.id, 'share-image') : null}
/>
<PageTransition name="about">
<Banner
title="About"
image={{
id: '699b4050-6bbf-4a40-be53-d84aca484f9d',
alt: 'Photo caption',
}}
/>
<section class="about__introduction">
<div class="container grid">
<h2 class="title-small">{about.intro_title}</h2>
<div class="heading text-big">
{@html about.intro_heading}
</div>
<div class="text text-small">
{@html about.intro_text}
</div>
</div>
</section>
<section class="about__creation">
<div class="container grid">
<figure class="first-photo">
<Image
class="picture shadow-box-dark"
id={about.intro_firstphoto.id}
alt={about.intro_firstphoto.title}
sizeKey="photo-list"
sizes={{
small: { width: 400 },
medium: { width: 600 },
large: { width: 800 },
}}
ratio={1.5}
/>
<figcaption class="text-info">
{about.intro_firstphoto_caption}<br>
in
<a href="/{about.intro_firstlocation.country.slug}/{about.intro_firstlocation.slug}" data-sveltekit-noscroll data-sveltekit-prefetch>
<img src="{getAssetUrlKey(about.intro_firstlocation.country.flag.id, 'square-small-jpg')}" width="32" height="32" alt="{about.intro_firstlocation.country.flag.title}">
<span>Naarm Australia (Melbourne)</span>
</a>
</figcaption>
</figure>
<h2 class="title-small" data-reveal>{about.creation_title}</h2>
<div class="heading text-huge" data-reveal>
{@html about.creation_heading}
</div>
<div class="text text-small" data-reveal>
{@html about.creation_text}
</div>
<figure class="picture portrait-photo" data-reveal-image>
<Image
class="shadow-box-dark"
id={about.creation_portrait.id}
alt={about.creation_portrait.title}
sizeKey="photo-list"
sizes={{
small: { width: 400 },
medium: { width: 750 },
}}
ratio={1.425}
/>
</figure>
<span class="portrait-photo__caption text-info">
{about.creation_portrait_caption}
</span>
</div>
</section>
<section class="about__present">
<div class="container grid">
<figure class="picture" data-reveal-image>
<Image
class="shadow-box-dark"
id={about.present_image.id}
alt={about.present_image.title}
sizeKey="photo-list"
sizes={{
small: { width: 400 },
medium: { width: 600 },
large: { width: 800 },
}}
ratio={1.5}
/>
</figure>
<h2 class="title-small" data-reveal>{about.present_title}</h2>
<div class="text text-small" data-reveal>
<p>{about.present_text}</p>
</div>
<div class="heading text-big" data-reveal>
{@html about.present_heading}
</div>
<div class="conclusion text-small" data-reveal>
<p>{about.present_conclusion}</p>
</div>
</div>
</section>
{#if about.image_showcase}
<div class="about__showcase container grid">
<Image
id={about.image_showcase.id}
alt={about.image_showcase.title}
sizeKey="showcase"
sizes={{
small: { width: 400 },
medium: { width: 1000 },
large: { width: 1800 },
}}
ratio={1.2}
/>
</div>
{/if}
<section class="about__process">
<div class="container grid">
<aside>
<div class="heading">
<h2 class="title-medium">{about.process_title}</h2>
<p class="text-xsmall">{about.process_subtitle}</p>
</div>
<ol>
{#each about.process_steps as { title }, index}
<li class:is-active={index === currentStep}>
<a href="#step-{index + 1}" class="title-big">
<span>{title}</span>
</a>
</li>
{/each}
</ol>
</aside>
<div class="steps">
{#each about.process_steps as { text, image, video_mp4, video_webm }, index}
<ProcessStep
{index} {text}
image={image ?? undefined}
video={video_mp4 && video_webm ? {
mp4: video_mp4.id,
webm: video_webm.id
} : undefined}
visible={index === currentStep}
/>
{/each}
</div>
</div>
</section>
<section class="about__photos" bind:this={photosGridEl}>
<div class="container-wide">
<div class="photos-grid" style:--parallax-y="{parallaxPhotos}px">
{#each photos as { image: { id }, title }, index}
<AboutGridPhoto class="grid-photo"
{id}
alt={title}
disabled={fadedPhotosIndexes.includes(index)}
/>
{/each}
</div>
</div>
</section>
<section class="about__interest container grid">
<div class="container grid">
<h2 class="title-xl">{about.contact_title}</h2>
<div class="blocks">
{#each about.contact_blocks as { title, text, link, button }}
<div class="block">
<h3 class="text-label">{title}</h3>
<div class="text text-normal">
{@html text}
</div>
<div class="button-container">
{#if link}
{#key emailCopied === link}
<div class="wrap"
in:fly={{ y: 4, duration: 325, easing: quartOutSvelte, delay: 250 }}
out:fade={{ duration: 250, easing: quartOutSvelte }}
use:mailtoClipboard
on:copied={({ detail }) => {
emailCopied = detail.email
// Clear timeout and add timeout to hide message
clearTimeout(emailCopiedTimeout)
emailCopiedTimeout = setTimeout(() => emailCopied = null, 2500)
}}
>
{#if emailCopied !== link}
<Button size="small" url="mailto:{link}" text={button} />
{:else}
<span class="clipboard">Email copied in clipboard</span>
{/if}
</div>
{/key}
{/if}
</div>
</div>
{/each}
</div>
</div>
</section>
</PageTransition>

View File

@@ -1,30 +0,0 @@
import { error } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
import { NEWSLETTER_API_TOKEN, NEWSLETTER_LIST_ID } from '$env/static/private'
export const POST: RequestHandler = async ({ request }) => {
try {
const body = await request.text()
if (body) {
const req = await fetch(`https://emailoctopus.com/api/1.6/lists/${NEWSLETTER_LIST_ID}/contacts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: NEWSLETTER_API_TOKEN,
email_address: body,
})
})
const res = await req.json()
if (res) {
return new Response(JSON.stringify({
code: res.error ? res.error.code : res.status
}))
}
}
} catch (err) {
throw error(403, err.message)
}
}

View File

@@ -1,147 +0,0 @@
<style lang="scss">
@import "../../style/pages/credits";
</style>
<script lang="ts">
import { navigating } from '$app/stores'
import type { PageData } from './$types'
import { onMount } from 'svelte'
import { stagger, timeline } from 'motion'
import { DELAY } from '$utils/contants'
import { quartOut } from 'svelte/easing'
// Components
import Metas from '$components/Metas.svelte'
import PageTransition from '$components/PageTransition.svelte'
import Image from '$components/atoms/Image.svelte'
import Heading from '$components/molecules/Heading.svelte'
import InteractiveGlobe from '$components/organisms/InteractiveGlobe.svelte'
export let data: PageData
const { credits, credit } = data
onMount(() => {
/**
* Animations
*/
const animation = timeline([
// Heading
['.heading .text', {
y: [24, 0],
opacity: [0, 1],
}],
// Categories
['.credits__category', {
opacity: [0, 1],
}, {
at: 0,
delay: stagger(0.35, { start: 0.5 }),
}],
// Names
['.credits__category > ul > li', {
y: [24, 0],
opacity: [0, 1],
}, {
at: 1.1,
delay: stagger(0.35),
}],
], {
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
})
animation.stop()
// Run animation
requestAnimationFrame(animation.play)
})
</script>
<Metas
title="Credits Houses Of"
description={credits.text}
/>
<PageTransition name="credits">
<Heading
text={credits.text}
/>
<section class="credits__list">
<div class="grid container">
{#each credits.list as { title, credits }}
<div class="credits__category grid">
<h2 class="title-small">{title}</h2>
<ul>
{#each credits as { name, role, website }}
<li>
<dl>
<dt>
{#if website}
<h3>
<a href={website} rel="noopener external" target="_blank" tabindex="0">{name}</a>
</h3>
{:else}
<h3>{name}</h3>
{/if}
</dt>
<dd>
{role}
</dd>
</dl>
</li>
{/each}
</ul>
</div>
{/each}
<div class="credits__category grid">
<h2 class="title-small">Photography</h2>
<ul>
{#each credit as { name, website, location }}
<li>
<dl>
<dt>
{#if website}
<h3>
<a href={website} rel="noopener external" target="_blank" tabindex="0">{name}</a>
</h3>
{:else}
<h3>{name}</h3>
{/if}
</dt>
<dd>
<ul data-sveltekit-noscroll>
{#each location as loc}
{#if loc.location_id}
<li>
<a href="/{loc.location_id.country.slug}/{loc.location_id.slug}" tabindex="0">
<Image
id={loc.location_id.country.flag.id}
sizeKey="square-small"
width={16}
height={16}
alt="Flag of {loc.location_id.country.slug}"
/>
<span>{loc.location_id.name}</span>
</a>
</li>
{/if}
{/each}
</ul>
</dd>
</dl>
</li>
{/each}
</ul>
</div>
</div>
</section>
<InteractiveGlobe type="cropped" />
</PageTransition>

View File

@@ -0,0 +1,93 @@
import { error } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
import { fetchAPI } from '$utils/api'
export const GET: RequestHandler = async ({ url, setHeaders }) => {
try {
const locations = []
const products = []
// Get dynamic data from API
const res = await fetchAPI(`query {
locations: location (filter: { status: { _eq: "published" }}) {
slug
country { slug }
}
products: product (filter: { status: { _eq: "published" }}) {
location { slug }
}
}`)
if (res) {
const { data } = res
locations.push(...data.locations)
products.push(...data.products)
}
// Static pages
const pages = [
['/', '1.0', 'daily'],
['/photos', '1.0', 'daily'],
['/locations', '0.6', 'weekly'],
['/shop', '0.8', 'weekly'],
['/about', '0.6', 'weekly'],
['/terms', '0.6', 'weekly'],
['/subscribe', '0.6', 'weekly'],
['/credits', '0.6', 'monthly'],
]
// All pages
const allPages = [
// Static pages
...pages.map(([path, priority, frequency]) => ({
path,
priority,
frequency,
})),
// Locations
...locations.map(({ slug, country }) => ({
path: `/${country.slug}/${slug}`,
priority: 0.7,
frequency: 'monthly',
})),
// Products
...products.map(({ location: { slug }}) => ({
path: `/shop/poster-${slug}`,
priority: 0.7,
frequency: 'monthly',
})),
]
const sitemap = render(url.origin, allPages)
setHeaders({
'Content-Type': 'application/xml',
'Cache-Control': 'max-age=0, s-max-age=600',
})
return new Response(sitemap)
} catch (err) {
throw error(500, err.message)
}
}
const render = (origin: string, pages: any[]) => {
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="https://www.w3.org/1999/xhtml"
xmlns:mobile="https://www.google.com/schemas/sitemap-mobile/1.0"
xmlns:news="https://www.google.com/schemas/sitemap-news/0.9"
xmlns:image="https://www.google.com/schemas/sitemap-image/1.1"
xmlns:video="https://www.google.com/schemas/sitemap-video/1.1"
>
${pages.map(({ path, priority, frequency }) => `<url>
<loc>${origin}${path}</loc>
<priority>${priority}</priority>
<changefreq>${frequency}</changefreq>
</url>`).join('')}
</urlset>`
}

View File

@@ -1,47 +0,0 @@
<style lang="scss">
@import "../../style/pages/terms";
</style>
<script lang="ts">
import type { PageData } from './$types'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
// Components
import PageTransition from '$components/PageTransition.svelte'
import Metas from '$components/Metas.svelte'
import Heading from '$components/molecules/Heading.svelte'
export let data: PageData
const { legal } = data
dayjs.extend(relativeTime)
</script>
<Metas
title="Terms and Conditions Houses Of"
description="Everything you need to know about using our website or buying our products"
/>
<PageTransition name="terms">
<Heading text="Everything you need to know about using our website or buying our products" />
<section class="terms__categories">
<div class="container grid">
{#each legal.terms as { title, text }, index}
<article class="terms__section grid">
<h2 class="title-small">{index + 1}. {title}</h2>
<div class="text text-info">
{@html text}
</div>
</article>
{/each}
<footer>
<p class="text-info">
Updated: <time datetime={dayjs(legal.date_updated).format('YYYY-MM-DD')}>{dayjs().to(dayjs(legal.date_updated))}</time>
</p>
</footer>
</div>
</section>
</PageTransition>

114
src/service-workers.ts Normal file
View File

@@ -0,0 +1,114 @@
import { build, files, version } from '$service-worker'
const ASSETS = 'assets' + version
// `build` is an array of all the files generated by the bundler, `files` is an
// array of everything in the `static` directory (except exlucdes defined in svelte.config.js)
const cached = build.concat(files);
// if you use typescript:
(self as unknown as ServiceWorkerGlobalScope).addEventListener('install', event => {
// self.addEventListener(
event.waitUntil(
caches
.open(ASSETS)
.then(cache => cache.addAll(cached))
.then(() => {
// if you use typescript:
(self as unknown as ServiceWorkerGlobalScope).skipWaiting();
// self.skipWaiting();
})
);
}
);
// self.addEventListener(
// if you use typescript:
(self as unknown as ServiceWorkerGlobalScope).addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(async keys => {
// delete old caches
keys.map(async key => {
if (key !== ASSETS) {
await caches.delete(key)
}
});
// if you use typescript:
(self as unknown as ServiceWorkerGlobalScope).clients.claim()
// self.clients.claim()
})
);
})
/**
* Fetch the asset from the network and store it in the cache.
* Fall back to the cache if the user is offline.
*/
// async function fetchAndCache (request) {
// if you use typescript:
async function fetchAndCache(request: Request) {
const cache = await caches.open(`offline${version}`)
try {
const response = await fetch(request)
if (response.status === 200) {
cache.put(request, response.clone())
}
return response
} catch (err) {
const response = await cache.match(request)
if (response) {
return response
}
throw err
}
}
// self.addEventListener('fetch', event => {
// if you use typescript:
(self as unknown as ServiceWorkerGlobalScope).addEventListener('fetch', event => {
if (event.request.method !== 'GET' || event.request.headers.has('range')) {
return
}
const url = new URL(event.request.url)
if (
// don't try to handle e.g. data: URIs
!url.protocol.startsWith('http') ||
// ignore dev server requests
(url.hostname === self.location.hostname &&
url.port !== self.location.port) ||
// ignore /_app/version.json
url.pathname === '/_app/version.json'
) {
return
}
// always serve static files and bundler-generated assets from cache
const isStaticAsset = url.host === self.location.host && cached.indexOf(url.pathname) > -1
if (event.request.cache === 'only-if-cached' && !isStaticAsset) {
return
}
// for everything else, try the network first, falling back to cache if the
// user is offline. (If the pages never change, you might prefer a cache-first
// approach to a network-first one.)
event.respondWith(
(async () => {
// always serve static files and bundler-generated assets from cache.
// if your application has other URLs with data that will never change,
// set this variable to true for them and they will only be fetched once.
const cachedAsset = isStaticAsset && (await caches.match(event.request))
return cachedAsset || fetchAndCache(event.request)
})()
)
}
)

View File

@@ -32,6 +32,13 @@ body {
cursor: default; cursor: default;
overflow-x: hidden; overflow-x: hidden;
overscroll-behavior: none; overscroll-behavior: none;
&.block-scroll {
overflow: hidden;
}
&.is-loading * {
cursor: wait !important;
}
} }
*, *:before, *:after { *, *:before, *:after {
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;

View File

@@ -13,6 +13,11 @@
left: 50%; left: 50%;
transform: translateX(-50%) translateZ(0); transform: translateX(-50%) translateZ(0);
width: var(--width); width: var(--width);
pointer-events: none;
@media (hover: hover) {
pointer-events: auto;
}
// Responsive square padding // Responsive square padding
&:after { &:after {
@@ -62,6 +67,7 @@
top: 50%; top: 50%;
left: 0; left: 0;
width: 100%; width: 100%;
padding-top: 0.25em;
overflow: hidden; overflow: hidden;
transform: translateY(-50%) translateZ(0); transform: translateY(-50%) translateZ(0);
pointer-events: none; pointer-events: none;
@@ -74,16 +80,22 @@
font-family: $font-serif; font-family: $font-serif;
font-weight: 100; font-weight: 100;
letter-spacing: -0.035em; letter-spacing: -0.035em;
line-height: 0.75;
color: $color-secondary; color: $color-secondary;
font-size: clamp(#{rem(88px)}, 20vw, #{rem(320px)}); font-size: clamp(#{rem(88px)}, 20vw, #{rem(320px)});
} }
.country { .country {
display: block; display: block;
text-transform: uppercase; text-transform: uppercase;
margin-top: 2em;
font-size: rem(14px); font-size: rem(14px);
color: $color-tertiary; color: $color-tertiary;
letter-spacing: 0.1em; letter-spacing: 0.1em;
font-weight: 500; font-weight: 500;
@include bp (md) {
margin-top: 4em;
}
} }
} }
@@ -112,27 +124,38 @@
a { a {
position: relative; position: relative;
top: -10px; top: -8px;
left: -10px; left: -8px;
display: block;
width: 20px;
height: 20px;
display: flex; display: flex;
width: 16px;
height: 16px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-decoration: none; text-decoration: none;
color: $color-secondary; color: $color-secondary;
pointer-events: auto; pointer-events: auto;
@include bp (md) {
top: -10px;
left: -10px;
width: 20px;
height: 20px;
}
// Dot // Dot
i { i {
display: block; display: block;
width: 10px; width: 6px;
height: 10px; height: 6px;
border-radius: 32px; border-radius: 32px;
background: $color-secondary; background: $color-secondary;
transition: box-shadow 0.4s var(--ease-quart), transform 0.4s var(--ease-quart); transition: box-shadow 0.4s var(--ease-quart), transform 0.4s var(--ease-quart);
transform-origin: 50% 50%; transform-origin: 50% 50%;
@include bp (md) {
width: 10px;
height: 10px;
}
} }
// Name // Name
span { span {
@@ -142,10 +165,14 @@
// Hover: Grow marker outline // Hover: Grow marker outline
&:hover { &:hover {
i { i {
box-shadow: 0 0 0 8px rgba($color-tertiary, 0.25);
@include bp (md) {
box-shadow: 0 0 0 10px rgba($color-tertiary, 0.25); box-shadow: 0 0 0 10px rgba($color-tertiary, 0.25);
} }
} }
} }
}
// State: Is hidden // State: Is hidden
&:global(.is-hidden) { &:global(.is-hidden) {

View File

@@ -118,6 +118,7 @@
position: absolute; position: absolute;
top: 16px; top: 16px;
right: 16px; right: 16px;
padding: 0;
} }
} }
} }

View File

@@ -88,16 +88,21 @@
// Arrow // Arrow
&__arrow { &__arrow {
$color-shadow: rgba(#000, 0.075); $color-shadow: rgba(#000, 0.075);
display: none;
// Enable only on devices with hover
@media (hover: hover) {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
opacity: 0; opacity: 0;
pointer-events: none;
display: block; display: block;
pointer-events: none;
transform: translate3d(var(--x), var(--y), 0); transform: translate3d(var(--x), var(--y), 0);
transition: transform 0.6s var(--ease-quart), opacity 0.6s var(--ease-quart); transition: transform 0.6s var(--ease-quart), opacity 0.6s var(--ease-quart);
transform-origin: 50% 50%; transform-origin: 50% 50%;
filter: drop-shadow(0 2px 2px $color-shadow) drop-shadow(0 8px 8px $color-shadow) drop-shadow(0 16px 16px $color-shadow); filter: drop-shadow(0 2px 2px $color-shadow) drop-shadow(0 8px 8px $color-shadow) drop-shadow(0 16px 16px $color-shadow);
}
svg { svg {
display: block; display: block;

View File

@@ -121,6 +121,7 @@
&--checkout { &--checkout {
@include bp (md) { @include bp (md) {
display: flex; display: flex;
align-items: center;
} }
p { p {
@@ -132,7 +133,8 @@
color: $color-gray; color: $color-gray;
@include bp (sm) { @include bp (sm) {
font-size: rem(12px); margin-bottom: 0;
font-size: rem(14px);
line-height: 1.6; line-height: 1.6;
} }
@include bp (md) { @include bp (md) {

View File

@@ -16,7 +16,7 @@
grid-column: 4 / span var(--columns); grid-column: 4 / span var(--columns);
} }
h3 { h2 {
color: $color-secondary; color: $color-secondary;
margin-bottom: 8px; margin-bottom: 8px;

View File

@@ -38,7 +38,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
opacity: 0; opacity: 0;
transform: scale3d(1.075, 1.075, 1.075); transform: scale(1.075);
transition: opacity 0.8s, transform 1.6s var(--ease-quart); transition: opacity 0.8s, transform 1.6s var(--ease-quart);
} }
:global(img) { :global(img) {
@@ -47,7 +47,7 @@
:global(.is-visible) { :global(.is-visible) {
opacity: 1; opacity: 1;
transform: scale3d(1,1,1); transform: scale(1);
} }
:global(img) { :global(img) {

View File

@@ -1,4 +1,6 @@
:global(.about) { .about {
overflow: hidden;
:global(.picture) { :global(.picture) {
overflow: hidden; overflow: hidden;
background: $color-primary-tertiary20; background: $color-primary-tertiary20;
@@ -9,9 +11,7 @@
border-radius: 16px; border-radius: 16px;
} }
} }
}
.about {
/* /*
** Introduction ** Introduction
*/ */

View File

@@ -23,9 +23,7 @@
} }
} }
} }
}
:global(.page-error) {
// Globe // Globe
:global(.globe) { :global(.globe) {
margin-top: 96px; margin-top: 96px;

View File

@@ -1,9 +1,10 @@
// Explore Page // Explore Page
:global(.explore) { .explore {
overflow: hidden; overflow: hidden;
}
:global(.explore__locations) { &__locations {
@include bp (sm, max) { @include bp (sm, max) {
margin-top: 72px; margin-top: 72px;
} }
}
} }

View File

@@ -1,9 +1,7 @@
:global(.homepage) {
overflow: hidden;
}
// Homepage // Homepage
.homepage { .homepage {
overflow: hidden;
// Intro Section // Intro Section
&__intro { &__intro {
padding-bottom: calc(96px + 20vw); padding-bottom: calc(96px + 20vw);

View File

@@ -1,9 +1,7 @@
:global(.location-page) {
background: #fff;
}
// Location Page // Location Page
.location-page { .location-page {
background: #fff;
// Intro // Intro
&__intro { &__intro {
position: relative; position: relative;

View File

@@ -9,6 +9,7 @@
// Title // Title
:global(h1) { :global(h1) {
margin: -20px 0 48px; margin: -20px 0 48px;
overflow: hidden;
color: $color-secondary; color: $color-secondary;
line-height: 1; line-height: 1;
@@ -175,14 +176,6 @@
} }
} }
// Additional spacing between grid patterns // Additional spacing between grid patterns
&:nth-child(10n + 10){
@include bp (sm) {
margin-top: 12px;
}
@include bp (sd) {
margin-top: clamp(8px, 1vw, 16px);
}
}
&:nth-child(10n + 1), &:nth-child(10n + 1),
&:nth-child(10n + 4), &:nth-child(10n + 4),
&:nth-child(10n + 5){ &:nth-child(10n + 5){
@@ -302,7 +295,7 @@
margin-right: 12px; margin-right: 12px;
color: #fff; color: #fff;
border-radius: 100%; border-radius: 100%;
transition: color 0.3s; transition: color 0.3s;;
@include bp (sm) { @include bp (sm) {
width: 26px; width: 26px;
@@ -313,6 +306,7 @@
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
transform: translateZ(0);
} }
} }
@@ -373,14 +367,12 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: 20px;
@include bp (sm) { @include bp (sm) {
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 20px; right: 20px;
transform: translateY(-50%); transform: translateY(-50%);
margin-top: 0;
} }
// Reset link // Reset link

View File

@@ -1,34 +1,8 @@
:global(.shop-page) { :global(.shop-page) {
position: relative; position: relative;
}
// Nav // Error
:global(.shop-location) { :global(.shop-page__error) {
--inset: 20px;
display: flex;
position: fixed;
z-index: 20;
top: var(--inset);
left: var(--inset);
right: var(--inset);
justify-content: space-between;
transform: translate3d(0, -88px, 0);
transition: transform 1s var(--ease-quart);
transition-delay: 100ms;
pointer-events: none;
@include bp (sm) {
--inset: 32px;
}
}
// Visible state
:global(.shop-location.is-visible) {
transform: translate3d(0,0,0);
}
// Error
:global(.shop-page__error) {
padding: 64px 0; padding: 64px 0;
background: $color-cream; background: $color-cream;
color: $color-text; color: $color-text;
@@ -54,6 +28,7 @@
margin-bottom: 16px; margin-bottom: 16px;
} }
} }
}
} }
// Notifications // Notifications

View File

@@ -22,12 +22,13 @@
// Past Issues // Past Issues
&__issues { &__issues {
margin: 64px auto 0; margin: 64px auto 96px;
padding: 0 20px; padding: 0 20px;
@include bp (sm) { @include bp (sm) {
max-width: 800px; max-width: 800px;
margin-top: 0; margin-top: 0;
margin-bottom: 156px;
} }
// Title // Title
@@ -63,12 +64,3 @@
} }
} }
} }
// Globe
:global(.subscribe .globe) {
margin-top: 96px;
@include bp (sm) {
margin-top: 156px;
}
}

View File

@@ -1,4 +1,4 @@
:global(.photo-page) { .photo-page {
position: relative; position: relative;
width: 100vw; width: 100vw;
height: var(--vh); height: var(--vh);
@@ -16,7 +16,6 @@
} }
} }
.photo-page {
// Carousel // Carousel
&__carousel { &__carousel {
position: absolute; position: absolute;
@@ -431,7 +430,6 @@
display: none; display: none;
} }
} }
}
// Close button // Close button
:global(.close) { :global(.close) {

View File

@@ -1,7 +1,7 @@
/* /*
** Shop: Intro ** Shop: Intro banner
*/ */
.shop-page__intro { .shop-banner {
position: relative; position: relative;
z-index: 30; z-index: 30;
height: 30vw; height: 30vw;
@@ -70,8 +70,8 @@
} }
} }
// Site Title // Page title
.shop-page__title { .title {
position: absolute; position: absolute;
z-index: 2; z-index: 2;
top: 42%; top: 42%;
@@ -98,56 +98,8 @@
} }
} }
// Background Image // Navigation
:global(picture) { .nav {
position: relative;
display: flex;
align-items: flex-end;
width: 100%;
height: 100%;
background-color: $color-primary-darker;
pointer-events: none;
user-select: none;
:global(img) {
opacity: 0.55;
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
// Gradient
&:before {
content: "";
display: block;
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, rgba(45, 4, 88, 0) 72.5%, #1E0538 100%);
}
}
// Cart button
:global(.button-cart) {
position: absolute;
z-index: 21;
top: 16px;
right: 16px;
@include bp (sm) {
top: auto;
bottom: 32px;
right: 32px;
}
}
}
// Intro: Navigation
.shop-page__nav {
position: absolute; position: absolute;
z-index: 20; z-index: 20;
bottom: 0; bottom: 0;
@@ -221,4 +173,79 @@
} }
} }
} }
}
// Background Image
:global(picture) {
position: relative;
display: flex;
align-items: flex-end;
width: 100%;
height: 100%;
background-color: $color-primary-darker;
pointer-events: none;
user-select: none;
:global(img) {
opacity: 0.55;
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
// Gradient
&:before {
content: "";
display: block;
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, rgba(45, 4, 88, 0) 72.5%, #1E0538 100%);
}
}
// Cart button
:global(.button-cart) {
position: absolute;
z-index: 21;
top: 16px;
right: 16px;
@include bp (sm) {
top: auto;
bottom: 32px;
right: 32px;
}
}
}
// Quick nav
.shop-quicknav {
--inset: 20px;
display: flex;
position: fixed;
z-index: 20;
top: var(--inset);
left: var(--inset);
right: var(--inset);
justify-content: flex-end;
transform: translate3d(0, -88px, 0);
transition: transform 1s var(--ease-quart);
transition-delay: 100ms;
pointer-events: none;
@include bp (sm) {
--inset: 32px;
justify-content: space-between;
}
// Visible state
&.is-visible {
transform: translate3d(0,0,0);
}
} }

View File

@@ -1,21 +1,8 @@
// @ts-nocheck // @ts-nocheck
// Send page
export const sendPage = (path: string = '') => {
if (typeof Countly !== 'undefined') {
Countly.track_pageview(path)
}
}
// Send event // Send event
export const sendEvent = ({ action, segments = {}, amount = 1 }) => { export const sendEvent = (action: string, props?: any) => {
if (typeof Countly !== 'undefined') { if (typeof plausible !== 'undefined') {
Countly.add_event({ plausible(action, props)
key: action,
count: amount,
segmentation: {
...segments
}
})
} }
} }

View File

@@ -7,5 +7,4 @@ export const DELAY = {
export const DURATION = { export const DURATION = {
PAGE_IN: 400, PAGE_IN: 400,
PAGE_OUT: 400, PAGE_OUT: 400,
PAGE_DELAY: 600,
} }

View File

@@ -0,0 +1,49 @@
import { NEWSLETTER_API_TOKEN, NEWSLETTER_LIST_ID } from '$env/static/private'
import { fail } from '@sveltejs/kit'
const formMessages = {
PENDING: `Almost there! Please confirm your email address through the email you'll receive soon.`,
MEMBER_EXISTS_WITH_EMAIL_ADDRESS: `This email address is already subscribed to the newsletter.`,
INVALID_EMAIL: `Woops. This email doesn't seem to be valid.`,
}
export default async ({ request }) => {
const formData = await request.formData()
const email = formData.get('email')
// Newsletter API request
const req = await fetch(`https://emailoctopus.com/api/1.6/lists/${NEWSLETTER_LIST_ID}/contacts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: NEWSLETTER_API_TOKEN,
email_address: email,
})
})
const res = await req.json()
if (res) {
if (res && res.status !== 'PENDING') {
// Invalid email
if (!email.match(/^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/)) {
return fail(400, {
error: 'INVALID_EMAIL',
message () { return formMessages[this.error] }
})
}
// Other error
return fail(400, {
error: res.error.code,
message: formMessages[res.error.code],
})
}
return {
success: true,
message: formMessages[res.status],
}
}
}

View File

@@ -1,3 +1,6 @@
import { sendEvent } from '$utils/analytics'
/** /**
* Throttle function * Throttle function
*/ */
@@ -128,7 +131,7 @@ export const scrollToTop = (delay?: number) => {
if (delay && delay > 0) { if (delay && delay > 0) {
setTimeout(scroll, delay) setTimeout(scroll, delay)
} else { } else {
scroll() return scroll()
} }
} }
@@ -150,6 +153,9 @@ export const mailtoClipboard = (node: HTMLElement) => {
detail: { email: emailAddress } detail: { email: emailAddress }
})) }))
// Record event in analytics
sendEvent('emailCopy')
event.preventDefault() event.preventDefault()
} }

View File

@@ -1,6 +1,7 @@
import swell from 'swell-js' import swell from 'swell-js'
import { addNotification } from '$utils/functions/notifications' import { addNotification } from '$utils/functions/notifications'
import { cartData } from '$utils/stores/shop' import { cartData } from '$utils/stores/shop'
import { sendEvent } from '$utils/analytics'
import { PUBLIC_SWELL_STORE_ID, PUBLIC_SWELL_API_PUBLIC_TOKEN } from '$env/static/public' import { PUBLIC_SWELL_STORE_ID, PUBLIC_SWELL_API_PUBLIC_TOKEN } from '$env/static/public'
@@ -40,6 +41,13 @@ export const addToCart = async (product: any, quantity: number = 1) => {
name: `${product.name} - x1`, name: `${product.name} - x1`,
image: product.images[0].file.url, image: product.images[0].file.url,
}) })
// Send event
sendEvent('addToCart', {
props: {
product: product.name,
},
})
} }
} }

View File

@@ -6,12 +6,12 @@ Twitter: https://twitter.com/Flayks
Location: Toulouse, France (Project originally started in April 2019 in Brisbane, Australia) Location: Toulouse, France (Project originally started in April 2019 in Brisbane, Australia)
/* THANKS */ /* THANKS */
Name: Julien Espagnon (https://twitter.com/Julien_Espagnon) - Help with the interactive WebGL globe Name: Julien Espagnon (https://twitter.com/Julien_Espagnon) - Help with the interactive globe
Name: Grafikart (https://jonathan-boyer.fr) - Many tips and help about Javascript and code logic Name: Grafikart (https://jonathan-boyer.fr) - Many tips and help about Javascript and code logic
/* SITE */ /* SITE */
Original launch: April 22th, 2020 Original launch: April 22th, 2020
Version 2 launch: September 2022 Version 2 launch: September 27th, 2022
Standards: HTML5, CSS3, Javascript Standards: HTML5, CSS3, Javascript
Front-End: SvelteKit, Motion One, normalize.css Front-End: SvelteKit, Motion One, normalize.css
Back-End: Vercel, Directus, Docker Back-End: Vercel, Directus, Docker

View File

@@ -1,7 +1,7 @@
{ {
"short_name": "Houses Of", "short_name": "Houses Of",
"name": "Houses Of - Beautiful houses around the world", "name": "Houses Of the World",
"description": "", "description": "Houses Of is a project showcasing charismatic houses around the world.",
"icons": [ "icons": [
{ {
"src": "/images/siteicon-192.png", "src": "/images/siteicon-192.png",

4
static/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Allow: /
Sitemap: https://housesof.world/sitemap.xml

View File

@@ -27,6 +27,7 @@ const config = {
$utils: 'src/utils', $utils: 'src/utils',
$style: 'src/style', $style: 'src/style',
}, },
csrf: false,
} }
} }

View File

@@ -1,7 +1,7 @@
{ {
"extends": "./.svelte-kit/tsconfig.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true,
"lib": ["WebWorker"],
}, },
"exclude": ["./src/modules/globe/**/*"],
} }