Merge branch 'main' into dev
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ node_modules
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.vercel
|
||||
48
package.json
48
package.json
@@ -15,40 +15,40 @@
|
||||
"lint": "eslint --ignore-path .gitignore ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@studio-freight/lenis": "^0.2.6",
|
||||
"dayjs": "^1.11.5",
|
||||
"embla-carousel": "^7.0.3",
|
||||
"@studio-freight/lenis": "^0.2.28",
|
||||
"dayjs": "^1.11.7",
|
||||
"embla-carousel": "^7.0.5",
|
||||
"focus-visible": "^5.2.0",
|
||||
"motion": "^10.14.2",
|
||||
"ogl": "^0.0.99",
|
||||
"motion": "^10.15.3",
|
||||
"ogl": "^0.0.103",
|
||||
"sanitize.css": "^13.0.0",
|
||||
"swell-js": "^3.17.6",
|
||||
"tweakpane": "^3.1.0"
|
||||
"swell-js": "3.18.2",
|
||||
"tweakpane": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^1.0.0-next.80",
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.96",
|
||||
"@sveltejs/adapter-vercel": "^1.0.0-next.77",
|
||||
"@sveltejs/kit": "^1.0.0-next.504",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
||||
"@typescript-eslint/parser": "^5.38.1",
|
||||
"@sveltejs/adapter-auto": "^1.0.0",
|
||||
"@sveltejs/adapter-node": "^1.0.0",
|
||||
"@sveltejs/adapter-vercel": "^1.0.0",
|
||||
"@sveltejs/kit": "^1.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.47.0",
|
||||
"@typescript-eslint/parser": "^5.47.0",
|
||||
"base-64": "^1.0.0",
|
||||
"browserslist": "^4.21.4",
|
||||
"cssnano": "^5.1.13",
|
||||
"eslint": "^8.24.0",
|
||||
"cssnano": "^5.1.14",
|
||||
"eslint": "^8.30.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"postcss": "^8.4.16",
|
||||
"postcss": "^8.4.20",
|
||||
"postcss-focus-visible": "^7.1.0",
|
||||
"postcss-normalize": "^10.0.1",
|
||||
"postcss-preset-env": "^7.8.2",
|
||||
"postcss-preset-env": "^7.8.3",
|
||||
"postcss-sort-media-queries": "^4.3.0",
|
||||
"sass": "^1.55.0",
|
||||
"svelte": "^3.50.1",
|
||||
"svelte-check": "^2.9.1",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.8.3",
|
||||
"vite": "^3.1.3"
|
||||
"sass": "^1.57.1",
|
||||
"svelte": "^3.55.0",
|
||||
"svelte-check": "^3.0.1",
|
||||
"svelte-preprocess": "^5.0.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.0.3"
|
||||
},
|
||||
"type": "module",
|
||||
"browserslist": [
|
||||
|
||||
2021
pnpm-lock.yaml
generated
2021
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,18 @@
|
||||
import { animate } from 'motion'
|
||||
import { animate, stagger } from 'motion'
|
||||
import type { TransitionConfig } from 'svelte/transition'
|
||||
import { quartOut } from './easings'
|
||||
|
||||
|
||||
/**
|
||||
* Scale and fade
|
||||
*/
|
||||
export const scaleFade = (node: HTMLElement, {
|
||||
delay = 0,
|
||||
duration = 1,
|
||||
scale = [0.7, 1],
|
||||
opacity = [1, 0],
|
||||
x = null,
|
||||
}) => {
|
||||
delay = 0,
|
||||
duration = 1,
|
||||
}): TransitionConfig => {
|
||||
return {
|
||||
css: () => {
|
||||
animate(node, {
|
||||
@@ -20,6 +25,36 @@ export const scaleFade = (node: HTMLElement, {
|
||||
duration,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body>
|
||||
%sveltekit.body%
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
|
||||
<script>
|
||||
document.body.style.opacity = '0'
|
||||
|
||||
@@ -1,37 +1,15 @@
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import { page } from '$app/stores'
|
||||
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()
|
||||
export let domain: string
|
||||
export let enabled: boolean = !import.meta.env.DEV
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#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}
|
||||
</svelte:head>
|
||||
|
||||
<noscript>
|
||||
<img src="{url}/pixel.png?app_key={appKey}&begin_session=1" alt="countly" width="0" height="0" />
|
||||
</noscript>
|
||||
@@ -18,10 +18,15 @@
|
||||
<meta name="description" content={description}>
|
||||
|
||||
<meta property="og:title" content={title} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
{#if description}
|
||||
<meta property="og:description" content={description} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
{/if}
|
||||
<meta property="og:type" content={type} />
|
||||
{#if image}
|
||||
<meta property="og:image" content={image} />
|
||||
<meta name="twitter:image" content={image} />
|
||||
{/if}
|
||||
{#if url}
|
||||
<meta property="og:url" content={url} />
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { afterUpdate } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
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/')
|
||||
|
||||
// Hide page loading indicator on page update
|
||||
afterUpdate(() => {
|
||||
clearTimeout(loadingTimeout)
|
||||
loadingTimeout = setTimeout(() => $pageLoading = false, DURATION.PAGE_IN)
|
||||
})
|
||||
</script>
|
||||
|
||||
<main class={name}
|
||||
in:fade={{ duration: DURATION.PAGE_IN, delay: DURATION.PAGE_DELAY }}
|
||||
<div class="page"
|
||||
in:fade={{ duration: DURATION.PAGE_IN, delay: DELAY.PAGE_LOADING }}
|
||||
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 />
|
||||
</main>
|
||||
</div>
|
||||
@@ -11,9 +11,7 @@
|
||||
export let url: string
|
||||
</script>
|
||||
|
||||
<a href={url} class="box-cta"
|
||||
data-sveltekit-prefetch={url.includes('http') ? true : undefined}
|
||||
>
|
||||
<a href={url} class="box-cta">
|
||||
<div class="icon">
|
||||
<Icon icon={icon} label={alt} />
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
<a
|
||||
href={url} class={classes}
|
||||
{target} {rel}
|
||||
data-sveltekit-prefetch={url && (isExternal || isProtocol) ? 'off' : ''}
|
||||
data-sveltekit-noscroll={isExternal || isProtocol ? 'off' : ''}
|
||||
{disabled}
|
||||
tabindex="0"
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
import { scale } from 'svelte/transition'
|
||||
import { quartOut } from 'svelte/easing'
|
||||
import { cartOpen, cartAmount } from '$utils/stores/shop'
|
||||
import { sendEvent } from '$utils/analytics'
|
||||
// Components
|
||||
import Icon from '$components/atoms/Icon.svelte'
|
||||
import ButtonCircle from '$components/atoms/ButtonCircle.svelte'
|
||||
|
||||
const openCart = () => {
|
||||
$cartOpen = true
|
||||
sendEvent('cartOpen')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<style lang="scss">
|
||||
:global(.scrolling-title) {
|
||||
.scrolling-title {
|
||||
display: inline-block;
|
||||
transform: translate3d(var(--parallax-x), 0, 0);
|
||||
transition: transform 1.2s var(--ease-quart);
|
||||
will-change: transform;
|
||||
@@ -25,8 +26,8 @@
|
||||
|
||||
// Define default values
|
||||
$: if (titleEl && !offsetStart && !offsetEnd) {
|
||||
offsetStart = titleEl.offsetTop - innerHeight * 0.75
|
||||
offsetEnd = titleEl.offsetTop + innerHeight * 0.25
|
||||
offsetStart = titleEl.offsetTop - innerHeight * (innerWidth < 768 ? 0.2 : 0.75)
|
||||
offsetEnd = titleEl.offsetTop + innerHeight * (innerWidth < 768 ? 0.5 : 0.5)
|
||||
}
|
||||
|
||||
// Check if title is larger than viewport to translate it
|
||||
@@ -34,7 +35,7 @@
|
||||
|
||||
// Calculate the parallax value
|
||||
$: 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
|
||||
}
|
||||
|
||||
@@ -44,7 +45,6 @@
|
||||
$$props.class
|
||||
].join(' ').trim()
|
||||
|
||||
|
||||
const revealOptions = animate ? {
|
||||
children: '.char',
|
||||
animation: { y: ['-105%', 0] },
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<script lang="ts">
|
||||
import SplitText from '$components/SplitText.svelte'
|
||||
import reveal from '$animations/reveal'
|
||||
import { DURATION } from '$utils/contants'
|
||||
import { DURATION } from '$utils/constants'
|
||||
|
||||
export let variant: string = 'lines'
|
||||
export let tag: string = 'h1'
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms'
|
||||
import { dev } from '$app/environment'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { quartOut } from 'svelte/easing'
|
||||
import { sendEvent } from '$utils/analytics'
|
||||
@@ -13,53 +15,41 @@
|
||||
export let past: boolean = false
|
||||
|
||||
let inputInFocus = false
|
||||
let formStatus: string = null
|
||||
let formStatus: { error: string, success: boolean, message: string } = null
|
||||
let formMessageTimeout: ReturnType<typeof setTimeout> | number
|
||||
const formMessages = {
|
||||
PENDING: `Almost there! Please confirm your email address through the email you'll receive soon.`,
|
||||
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.`,
|
||||
}
|
||||
|
||||
$: isSuccess = formStatus && formStatus.success
|
||||
|
||||
// Toggle input focus
|
||||
const toggleFocus = () => inputInFocus = !inputInFocus
|
||||
|
||||
// Handle form submission
|
||||
const handleForm = () => {
|
||||
return async ({ result, update }) => {
|
||||
formStatus = result.data
|
||||
|
||||
/**
|
||||
* Subscription form handling
|
||||
*/
|
||||
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 (dev) {
|
||||
console.log(result)
|
||||
}
|
||||
} else {
|
||||
formStatus = 'INVALID_EMAIL'
|
||||
}
|
||||
}
|
||||
|
||||
$: if (formStatus !== 'PENDING') {
|
||||
clearTimeout(formMessageTimeout)
|
||||
formMessageTimeout = setTimeout(() => formStatus = null, 3000)
|
||||
// If successful
|
||||
if (result.data.success) {
|
||||
sendEvent('newsletterSubscribe')
|
||||
update()
|
||||
} else {
|
||||
// Hide message for errors
|
||||
clearTimeout(formMessageTimeout)
|
||||
formMessageTimeout = requestAnimationFrame(() => setTimeout(() => formStatus = null, 4000))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="newsletter-form">
|
||||
{#if formStatus !== 'PENDING'}
|
||||
<form method="POST" on:submit|preventDefault={formSubmission}
|
||||
out:fly={{ y: -8, easing: quartOut, duration: 600 }}
|
||||
{#if !isSuccess}
|
||||
<form method="POST" action="?/subscribe"
|
||||
use:enhance={handleForm}
|
||||
out:fly|local={{ y: -8, easing: quartOut, duration: 600 }}
|
||||
>
|
||||
<div class="newsletter-form__email" class:is-focused={inputInFocus}>
|
||||
<input type="email" placeholder="Your email address" name="email" id="newsletter_email" required
|
||||
@@ -78,7 +68,7 @@
|
||||
|
||||
<div class="newsletter-form__bottom">
|
||||
{#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">
|
||||
<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>
|
||||
@@ -90,14 +80,14 @@
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if formStatus}
|
||||
{#if formStatus && formStatus.message}
|
||||
<div class="newsletter-form__message shadow-small"
|
||||
class:is-error={formStatus !== 'PENDING'}
|
||||
class:is-success={formStatus === 'PENDING'}
|
||||
in:fly={{ y: 8, easing: quartOut, duration: 600, delay: 600 }}
|
||||
out:fly={{ y: 8, easing: quartOut, duration: 600 }}
|
||||
class:is-error={!isSuccess}
|
||||
class:is-success={isSuccess}
|
||||
in:fly|local={{ y: 8, easing: quartOut, duration: 600, delay: isSuccess ? 600 : 0 }}
|
||||
out:fly|local={{ y: 8, easing: quartOut, duration: 600 }}
|
||||
>
|
||||
<p class="text-xsmall">{formMessages[formStatus]}</p>
|
||||
<p class="text-xsmall">{formStatus.message}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import { spring } from 'svelte/motion'
|
||||
import dayjs, { type Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { lerp } from '$utils/functions'
|
||||
import { PUBLIC_PREVIEW_COUNT } from '$env/static/public'
|
||||
import { seenLocations } from '$utils/stores'
|
||||
@@ -23,15 +23,24 @@
|
||||
|
||||
// Location date limit
|
||||
let isNew = false
|
||||
let dateUpdated: Dayjs
|
||||
const dateNowOffset = dayjs().subtract(settings.limit_new, 'day')
|
||||
const parsedSeenLocations = JSON.parse($seenLocations)
|
||||
|
||||
$: if (latestPhoto && $seenLocations) {
|
||||
dateUpdated = dayjs(latestPhoto.date_created)
|
||||
$: if (latestPhoto) {
|
||||
const dateUpdated = dayjs(latestPhoto.date_created)
|
||||
|
||||
// Detect if location has new content
|
||||
const seenLocation = JSON.parse($seenLocations)?.hasOwnProperty(location.id)
|
||||
isNew = dateUpdated.isAfter(dateNowOffset) && !seenLocation
|
||||
const seenLocationDate = dayjs(parsedSeenLocations[location.id])
|
||||
const isLocationSeen = parsedSeenLocations?.hasOwnProperty(location.id)
|
||||
|
||||
// Define if location is has new photos
|
||||
if (seenLocationDate && isLocationSeen) {
|
||||
// A more recent photo has been added (if has been seen and has a seen date)
|
||||
isNew = dateUpdated.isAfter(dateNowOffset) && dateUpdated.isAfter(seenLocationDate)
|
||||
} else {
|
||||
// The photo is after the offset
|
||||
isNew = dateUpdated.isAfter(dateNowOffset)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<div class="poster">
|
||||
{#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 })}
|
||||
>
|
||||
<Image
|
||||
|
||||
@@ -13,19 +13,10 @@
|
||||
export let text: string
|
||||
export let image: any = undefined
|
||||
export let video: any = undefined
|
||||
export let visible: boolean = false
|
||||
|
||||
let videoEl: HTMLVideoElement
|
||||
|
||||
const imageRatio = image ? image.width / image.height : undefined
|
||||
|
||||
// Toggle video playback if step is visible
|
||||
$: if (videoEl) {
|
||||
visible ? videoEl.play() : videoEl.pause()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<div class="step grid" style:--index={index}
|
||||
in:scaleFade|local={{ scale: [1.1, 1], opacity: [0, 1], x: [20, 0], delay: 0.2 }}
|
||||
out:scaleFade|local={{ scale: [1, 0.9], opacity: [1, 0], x: [0, -20] }}
|
||||
@@ -44,8 +35,8 @@
|
||||
ratio={imageRatio}
|
||||
alt={image.title}
|
||||
/>
|
||||
{:else if video}
|
||||
<video muted loop playsinline autoplay allow="autoplay" bind:this={videoEl}>
|
||||
{:else if video && video.mp4 && video.webm}
|
||||
<video muted loop playsinline autoplay allow="autoplay">
|
||||
<source type="video/mp4" src={getAssetUrlKey(video.mp4, 'step')} />
|
||||
<source type="video/webm" src={getAssetUrlKey(video.webm, 'step')} />
|
||||
<track kind="captions" />
|
||||
@@ -58,4 +49,3 @@
|
||||
{@html text}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -6,6 +6,7 @@
|
||||
import { page } from '$app/stores'
|
||||
import { getContext } from 'svelte'
|
||||
import reveal from '$animations/reveal'
|
||||
import { sendEvent } from '$utils/analytics'
|
||||
// Components
|
||||
import Icon from '$components/atoms/Icon.svelte'
|
||||
|
||||
@@ -20,6 +21,9 @@
|
||||
*/
|
||||
const toggleSwitcher = () => {
|
||||
isOpen = !isOpen
|
||||
|
||||
// Record opening event
|
||||
!isOpen && sendEvent('switcherOpen')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +60,7 @@
|
||||
</span>
|
||||
</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 }}
|
||||
<li class:is-active={$page.url.pathname === url}>
|
||||
<a href={url} on:click={toggleSwitcher} tabindex="0">
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { onMount } from 'svelte'
|
||||
import { fade, fly } from 'svelte/transition'
|
||||
import { quartOut } from 'svelte/easing'
|
||||
import { smoothScroll } from '$utils/stores'
|
||||
import { cartOpen, cartData, cartAmount, cartIsUpdating } from '$utils/stores/shop'
|
||||
import { initSwell, getCart, updateCartItem, removeCartItem } from '$utils/functions/shop'
|
||||
// Components
|
||||
@@ -13,6 +15,19 @@
|
||||
import Icon from '$components/atoms/Icon.svelte'
|
||||
import CartItem from '$components/molecules/CartItem.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
|
||||
@@ -117,7 +132,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<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}
|
||||
<div transition:fly={{ y: 8, duration: 600, easing: quartOut }}>
|
||||
<Button
|
||||
@@ -125,6 +140,7 @@
|
||||
text="Checkout"
|
||||
color="pink"
|
||||
size="small"
|
||||
on:click={() => sendEvent('cartCheckout', { props: { amount: $cartAmount }})}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
<footer class="footer">
|
||||
<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" />
|
||||
</a>
|
||||
|
||||
<nav class="footer__links">
|
||||
<ul data-sveltekit-prefetch data-sveltekit-noscroll>
|
||||
<ul data-sveltekit-noscroll>
|
||||
{#each footer_links as { title, slug }}
|
||||
<li>
|
||||
<a href="/{slug}" class="link-3d" tabindex="0">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { quartOut } from 'svelte/easing'
|
||||
import { Globe, type Marker } from '$modules/globe'
|
||||
import { getRandomItem, debounce } from '$utils/functions'
|
||||
import reveal from '$animations/reveal'
|
||||
import { revealSplit } from '$animations/transitions'
|
||||
// Components
|
||||
import SplitText from '$components/SplitText.svelte'
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
<ul class="globe__markers">
|
||||
{#each markers as { name, slug, country, lat, 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:mouseleave={() => hoveredMarker = null}
|
||||
>
|
||||
@@ -145,19 +145,11 @@
|
||||
|
||||
{#if hoveredMarker}
|
||||
<div class="globe__location"
|
||||
transition:fade={{ duration: 300, easing: quartOut }}
|
||||
use:reveal={{
|
||||
children: '.char',
|
||||
animation: { y: ['110%', 0] },
|
||||
options: {
|
||||
stagger: 0.04,
|
||||
duration: 1,
|
||||
threshold: 0,
|
||||
},
|
||||
}}
|
||||
in:revealSplit={{ duration: 1 }}
|
||||
out:fade={{ duration: 300, easing: quartOut }}
|
||||
>
|
||||
<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}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
class={'is-disabled'}
|
||||
on:click={() => {
|
||||
filterLocation(slug)
|
||||
sendEvent({ action: 'filterContinent' })
|
||||
sendEvent('filterContinent')
|
||||
}}
|
||||
>
|
||||
<svg width="12" height="12">
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
|
||||
<div class="newsletter newsletter--{theme} shadow-box-dark">
|
||||
<div class="newsletter__wrapper">
|
||||
<h3 class="title-medium">
|
||||
<h2 class="title-medium">
|
||||
<label for="newsletter_email">{newsletter_subtitle}</label>
|
||||
</h3>
|
||||
</h2>
|
||||
<p class="text-small">{newsletter_text}</p>
|
||||
|
||||
<EmailForm past={true} />
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import EmblaCarousel, { type EmblaCarouselType } from 'embla-carousel'
|
||||
// Components
|
||||
import Poster from '$components/molecules/Poster.svelte'
|
||||
import EmailForm from '$components/molecules/EmailForm.svelte'
|
||||
import { debounce } from '$utils/functions'
|
||||
|
||||
export let posters: any = []
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/pages/shop/intro";
|
||||
@import "../../style/pages/shop/banner";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -8,7 +8,7 @@
|
||||
import { stagger, timeline } from 'motion'
|
||||
import { smoothScroll } from '$utils/stores'
|
||||
import { cartOpen } from '$utils/stores/shop'
|
||||
import { DELAY } from '$utils/contants'
|
||||
import { DELAY } from '$utils/constants'
|
||||
import { quartOut } from '$animations/easings'
|
||||
// Components
|
||||
import Image from '$components/atoms/Image.svelte'
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
const { shop, shopLocations }: any = getContext('shop')
|
||||
|
||||
let innerWidth: number
|
||||
let navObserver: IntersectionObserver
|
||||
let introEl: HTMLElement, navChooseEl: HTMLElement
|
||||
let scrolledPastIntro = false
|
||||
@@ -50,7 +51,7 @@
|
||||
*/
|
||||
const animation = timeline([
|
||||
// Hero image
|
||||
['.shop-page__background', {
|
||||
['.background', {
|
||||
scale: [1.06, 1],
|
||||
opacity: [0, 1],
|
||||
z: 0,
|
||||
@@ -60,7 +61,7 @@
|
||||
}],
|
||||
|
||||
// Intro top elements
|
||||
['.shop-page__intro .top > *', {
|
||||
['.shop-banner .top > *', {
|
||||
y: [-100, 0],
|
||||
opacity: [0, 1],
|
||||
}, {
|
||||
@@ -69,7 +70,7 @@
|
||||
}],
|
||||
|
||||
// Hero title
|
||||
['.shop-page__title h1', {
|
||||
['.shop-banner .title h1', {
|
||||
y: [32, 0],
|
||||
opacity: [0, 1],
|
||||
}, {
|
||||
@@ -77,7 +78,7 @@
|
||||
}],
|
||||
|
||||
// Intro navbar
|
||||
['.shop-page__nav .container > *, .shop-page__intro .button-cart', {
|
||||
['.shop-banner .nav .container > *, .shop-banner .button-cart', {
|
||||
y: [100, 0],
|
||||
opacity: [0, 1],
|
||||
}, {
|
||||
@@ -104,7 +105,10 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<section class="shop-page__intro" bind:this={introEl}>
|
||||
<svelte:window bind:innerWidth />
|
||||
|
||||
|
||||
<section class="shop-banner" bind:this={introEl}>
|
||||
<div class="top container">
|
||||
<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">
|
||||
@@ -114,15 +118,15 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="shop-page__title">
|
||||
<div class="title">
|
||||
<h1 class="title-big-sans">Shop</h1>
|
||||
</div>
|
||||
|
||||
<nav class="shop-page__nav">
|
||||
<div class="nav">
|
||||
<div class="container">
|
||||
<p class="text-label">Choose a city</p>
|
||||
<nav>
|
||||
<ul data-sveltekit-noscroll data-sveltekit-prefetch bind:this={navChooseEl}>
|
||||
<ul bind:this={navChooseEl} data-sveltekit-noscroll>
|
||||
{#each shopLocations as { name, slug }}
|
||||
<li class:is-active={product && slug === product.location.slug}>
|
||||
<a href="/shop/poster-{slug}" on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}>
|
||||
@@ -133,12 +137,12 @@
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<ButtonCart />
|
||||
|
||||
<Image
|
||||
class="shop-page__background"
|
||||
class="background"
|
||||
id={shop.page_heroimage.id}
|
||||
alt={shop.page_heroimage.alt}
|
||||
sizeKey="hero"
|
||||
@@ -151,10 +155,12 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<nav class="shop-location"
|
||||
<nav class="shop-quicknav"
|
||||
class:is-visible={scrolledPastIntro}
|
||||
class:is-overlaid={$cartOpen}
|
||||
>
|
||||
<ShopLocationSwitcher />
|
||||
{#if innerWidth > 768}
|
||||
<ShopLocationSwitcher />
|
||||
{/if}
|
||||
<ButtonCart />
|
||||
</nav>
|
||||
@@ -62,7 +62,7 @@
|
||||
<div class="content">
|
||||
<div class="shop__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}
|
||||
<Image
|
||||
class={index === currentImageIndex ? 'is-visible' : null}
|
||||
@@ -80,7 +80,7 @@
|
||||
</div>
|
||||
|
||||
<div class="shop__content">
|
||||
<h3 class="title-medium">{title}</h3>
|
||||
<h2 class="title-medium">{title}</h2>
|
||||
<p class="text-small">{text}</p>
|
||||
{#if enabled}
|
||||
<Button {url} text={buttonText} color="pinklight" />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import { Renderer, Camera, Vec3, Orbit, Sphere, Transform, Program, Mesh, Texture } from 'ogl'
|
||||
import { map } from '$utils/functions'
|
||||
// Shaders
|
||||
import VERTEX_SHADER from '$modules/globe/vertex.glsl?raw'
|
||||
import FRAGMENT_SHADER from '$modules/globe/frag.glsl?raw'
|
||||
@@ -18,20 +17,20 @@ export class Globe {
|
||||
this.zoom = 1.3075
|
||||
|
||||
// Calculate the current sun position from a given location
|
||||
const locations = [
|
||||
{
|
||||
lat: -37.840935,
|
||||
lng: 144.946457,
|
||||
tz: 'Australia/Melbourne',
|
||||
},
|
||||
{
|
||||
lat: 48.856614,
|
||||
lng: 2.3522219,
|
||||
tz: 'Europe/Paris',
|
||||
}
|
||||
]
|
||||
const location = locations[1]
|
||||
const localDate = new Date(new Date().toLocaleString('en-US', { timeZone: location.tz }))
|
||||
// const locations = [
|
||||
// {
|
||||
// lat: -37.840935,
|
||||
// lng: 144.946457,
|
||||
// tz: 'Australia/Melbourne',
|
||||
// },
|
||||
// {
|
||||
// lat: 48.856614,
|
||||
// lng: 2.3522219,
|
||||
// tz: 'Europe/Paris',
|
||||
// }
|
||||
// ]
|
||||
// const location = locations[1]
|
||||
// const localDate = new Date(new Date().toLocaleString('en-US', { timeZone: location.tz }))
|
||||
|
||||
// Parameters
|
||||
this.params = {
|
||||
@@ -122,8 +121,7 @@ export class Globe {
|
||||
imgDark.src = this.options.mapFileDark
|
||||
|
||||
// Create light
|
||||
const dayTime = map(5, 0, 24, 0, 1, true)
|
||||
const lightD = degToRad(360 / dayTime)
|
||||
const lightD = degToRad(7 * 360 / 24)
|
||||
const sunPosition = new Vec3(
|
||||
Math.cos(lightD),
|
||||
Math.sin(lightD) * Math.sin(0),
|
||||
@@ -263,12 +261,14 @@ export class Globe {
|
||||
* Resize method
|
||||
*/
|
||||
resize () {
|
||||
this.width = this.el.offsetWidth
|
||||
this.height = this.el.offsetHeight
|
||||
this.renderer.setSize(this.width, this.height)
|
||||
this.camera.perspective({
|
||||
aspect: this.gl.canvas.width / this.gl.canvas.height
|
||||
})
|
||||
if (this.renderer) {
|
||||
this.width = this.el.offsetWidth
|
||||
this.height = this.el.offsetHeight
|
||||
this.renderer.setSize(this.width, this.height)
|
||||
this.camera.perspective({
|
||||
aspect: this.gl.canvas.width / this.gl.canvas.height
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,17 +25,19 @@
|
||||
/>
|
||||
|
||||
|
||||
<PageTransition name="shop-page">
|
||||
<ShopHeader />
|
||||
<PageTransition>
|
||||
<main class="shop-page">
|
||||
<ShopHeader />
|
||||
|
||||
<section class="shop-page__error">
|
||||
<div class="container grid">
|
||||
<div class="inner">
|
||||
<h2 class="title-big">Uh oh!</h2>
|
||||
<p class="text-medium">{errors[$page.status].message}</p>
|
||||
<section class="shop-page__error">
|
||||
<div class="container grid">
|
||||
<div class="inner">
|
||||
<h2 class="title-big">Uh oh!</h2>
|
||||
<p class="text-medium">{errors[$page.status].message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<PostersGrid {posters} />
|
||||
<PostersGrid {posters} />
|
||||
</main>
|
||||
</PageTransition>
|
||||
@@ -1,9 +1,9 @@
|
||||
import { error } from '@sveltejs/kit'
|
||||
import type { PageServerLoad } from './$types'
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
import { fetchAPI } from '$utils/api'
|
||||
import { fetchSwell } from '$utils/functions/shopServer'
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
export const load: LayoutServerLoad = async () => {
|
||||
try {
|
||||
// Get content from API
|
||||
const res = await fetchAPI(`query {
|
||||
@@ -1,5 +1,5 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/pages/shop";
|
||||
@import "../../../style/pages/shop";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -26,13 +26,15 @@
|
||||
/>
|
||||
|
||||
|
||||
<PageTransition name="shop-page">
|
||||
<ShopHeader {product} />
|
||||
<PageTransition>
|
||||
<main class="shop-page">
|
||||
<ShopHeader {product} />
|
||||
|
||||
<PosterLayout
|
||||
product={product}
|
||||
shopProduct={shopProduct}
|
||||
/>
|
||||
<PosterLayout
|
||||
product={product}
|
||||
shopProduct={shopProduct}
|
||||
/>
|
||||
|
||||
<PostersGrid {posters} />
|
||||
<PostersGrid {posters} />
|
||||
</main>
|
||||
</PageTransition>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types'
|
||||
import { getContext } from 'svelte'
|
||||
import { getAssetUrlKey } from '$utils/api'
|
||||
import { shopCurrentProductSlug } from '$utils/stores/shop'
|
||||
import { capitalizeFirstLetter } from '$utils/functions'
|
||||
// Components
|
||||
@@ -19,18 +20,20 @@
|
||||
|
||||
<Metas
|
||||
title="{data.product.location.name} {capitalizeFirstLetter(data.product.type)} – Houses Of"
|
||||
description=""
|
||||
image=""
|
||||
description={data.product.description}
|
||||
image={getAssetUrlKey(data.product.photos_product[2].directus_files_id.id, 'share-image')}
|
||||
/>
|
||||
|
||||
|
||||
<PageTransition name="shop-page">
|
||||
<ShopHeader product={data.product} />
|
||||
<PageTransition>
|
||||
<main class="shop-page">
|
||||
<ShopHeader product={data.product} />
|
||||
|
||||
<PosterLayout
|
||||
product={data.product}
|
||||
shopProduct={data.shopProduct}
|
||||
/>
|
||||
<PosterLayout
|
||||
product={data.product}
|
||||
shopProduct={data.shopProduct}
|
||||
/>
|
||||
|
||||
<PostersGrid {posters} />
|
||||
<PostersGrid {posters} />
|
||||
</main>
|
||||
</PageTransition>
|
||||
@@ -1,9 +1,13 @@
|
||||
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 { fetchAPI, photoFields } from '$utils/api'
|
||||
import subscribe from '$utils/forms/subscribe'
|
||||
|
||||
|
||||
/**
|
||||
* Page Data
|
||||
*/
|
||||
export const load: PageServerLoad = async ({ params, setHeaders }) => {
|
||||
try {
|
||||
const { location: slug } = params
|
||||
@@ -41,7 +45,8 @@ export const load: PageServerLoad = async ({ params, setHeaders }) => {
|
||||
|
||||
photos: photo (
|
||||
filter: {
|
||||
location: { slug: { _eq: "${slug}" }}
|
||||
location: { slug: { _eq: "${slug}" }},
|
||||
status: { _eq: "published" },
|
||||
},
|
||||
sort: "-date_created",
|
||||
limit: ${PUBLIC_LIST_AMOUNT},
|
||||
@@ -56,7 +61,12 @@ export const load: PageServerLoad = async ({ params, setHeaders }) => {
|
||||
}
|
||||
|
||||
# Shop product
|
||||
product (filter: { location: { slug: { _eq: "${slug}" }}}) {
|
||||
product (
|
||||
filter: {
|
||||
location: { slug: { _eq: "${slug}" }},
|
||||
status: { _eq: "published" },
|
||||
}
|
||||
) {
|
||||
photos_product {
|
||||
directus_files_id {
|
||||
id
|
||||
@@ -83,3 +93,12 @@ export const load: PageServerLoad = async ({ params, setHeaders }) => {
|
||||
throw error(500, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Form Data
|
||||
*/
|
||||
export const actions: Actions = {
|
||||
// Form newsletter subscription
|
||||
subscribe,
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<style lang="scss">
|
||||
@import "../../../style/pages/location";
|
||||
@import "../../../../style/pages/location";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -11,7 +11,7 @@
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { quartOut } from '$animations/easings'
|
||||
import { getAssetUrlKey } from '$utils/api'
|
||||
import { DELAY } from '$utils/contants'
|
||||
import { DELAY } from '$utils/constants'
|
||||
import { seenLocations } from '$utils/stores'
|
||||
import { photoFields } from '$utils/api'
|
||||
import { PUBLIC_LIST_INCREMENT } from '$env/static/public'
|
||||
@@ -210,156 +210,159 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window bind:scrollY />
|
||||
|
||||
<Metas
|
||||
title="Houses Of {location.name}"
|
||||
description="Discover {totalPhotos} beautiful homes from {location.name}, {location.country.name}"
|
||||
image={latestPhoto ? getAssetUrlKey(latestPhoto.image.id, 'share-image') : null}
|
||||
/>
|
||||
|
||||
<svelte:window bind:scrollY />
|
||||
|
||||
|
||||
<PageTransition name="location-page">
|
||||
<section class="location-page__intro grid" bind:this={introEl}>
|
||||
<h1 class="title" class:is-short={location.name.length <= 4}>
|
||||
<span class="housesof mask">
|
||||
<strong class="word">Houses</strong>
|
||||
<span class="of">of</span>
|
||||
</span>
|
||||
<strong class="city mask">
|
||||
<span class="word">{location.name}</span>
|
||||
</strong>
|
||||
</h1>
|
||||
<PageTransition>
|
||||
<main class="location-page">
|
||||
<section class="location-page__intro grid" bind:this={introEl}>
|
||||
<h1 class="title" class:is-short={location.name.length <= 4}>
|
||||
<span class="housesof mask">
|
||||
<strong class="word">Houses</strong>
|
||||
<span class="of">of</span>
|
||||
</span>
|
||||
<strong class="city mask">
|
||||
<span class="word">{location.name}</span>
|
||||
</strong>
|
||||
</h1>
|
||||
|
||||
<div class="location-page__description grid">
|
||||
<div class="wrap">
|
||||
<div class="text-medium">
|
||||
Houses of {location.name} {location.description ?? 'has no description yet'}
|
||||
</div>
|
||||
<div class="info">
|
||||
<p class="text-label">
|
||||
Photos by
|
||||
{#each location.credits as { credit_id: { name, website }}}
|
||||
{#if website}
|
||||
<a href={website} target="_blank" rel="noopener external">
|
||||
{name}
|
||||
</a>
|
||||
{:else}
|
||||
<span>{name}</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</p>
|
||||
{#if latestPhoto}
|
||||
·
|
||||
<p class="text-label" title={dayjs(latestPhoto.date_created).format('DD/MM/YYYY, hh:mm')}>
|
||||
Updated <time datetime={dayjs(latestPhoto.date_created).format('YYYY-MM-DD')}>
|
||||
{dayjs().to(dayjs(latestPhoto.date_created))}
|
||||
</time>
|
||||
<div class="location-page__description grid">
|
||||
<div class="wrap">
|
||||
<div class="text-medium">
|
||||
Houses of {location.name} {location.description ?? 'has no description yet'}
|
||||
</div>
|
||||
<div class="info">
|
||||
<p class="text-label">
|
||||
Photos by
|
||||
{#each location.credits as { credit_id: { name, website }}}
|
||||
{#if website}
|
||||
<a href={website} target="_blank" rel="noopener external">
|
||||
{name}
|
||||
</a>
|
||||
{:else}
|
||||
<span>{name}</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if latestPhoto}
|
||||
·
|
||||
<p class="text-label" title={dayjs(latestPhoto.date_created).format('DD/MM/YYYY, hh:mm')}>
|
||||
Updated <time datetime={dayjs(latestPhoto.date_created).format('YYYY-MM-DD')}>
|
||||
{dayjs().to(dayjs(latestPhoto.date_created))}
|
||||
</time>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="ctas">
|
||||
<Button url="/locations" text="Change location" class="shadow-small">
|
||||
<IconEarth />
|
||||
</Button>
|
||||
|
||||
{#if location.has_poster}
|
||||
<Button url="/shop/poster-{location.slug}" text="Buy the poster" color="pinklight" class="shadow-small">
|
||||
<!-- <IconEarth /> -->
|
||||
<div class="ctas">
|
||||
<Button url="/locations" text="Change location" class="shadow-small">
|
||||
<IconEarth />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if hasIllustration}
|
||||
<picture class="location-page__illustration" style:--parallax-y="{illustrationOffsetY}px">
|
||||
<source media="(min-width: 1200px)" srcset={getAssetUrlKey(location.illustration_desktop_2x.id, 'illustration-desktop-2x')}>
|
||||
<source media="(min-width: 768px)" srcset={getAssetUrlKey(location.illustration_desktop.id, 'illustration-desktop-1x')}>
|
||||
<img
|
||||
src={getAssetUrlKey(location.illustration_mobile.id, 'illustration-mobile')}
|
||||
width={320}
|
||||
height={824}
|
||||
alt="Illustration for {location.name}"
|
||||
decoding="async"
|
||||
/>
|
||||
</picture>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if photos.length}
|
||||
<section class="location-page__houses" bind:this={photosListEl} data-sveltekit-noscroll>
|
||||
{#each photos as { title, image: { id, title: alt, width, height }, slug, city, date_taken }, index}
|
||||
<House
|
||||
{title}
|
||||
photoId={id}
|
||||
photoAlt={alt}
|
||||
url="/{params.country}/{params.location}/{slug}"
|
||||
{city}
|
||||
location={location.name}
|
||||
ratio={width / height}
|
||||
date={date_taken}
|
||||
index={(totalPhotos - index < 10) ? '0' : ''}{totalPhotos - index}
|
||||
/>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<section class="location-page__next container">
|
||||
<Pagination
|
||||
ended={ended}
|
||||
current={currentPhotosAmount}
|
||||
total={totalPhotos}
|
||||
on:click={!ended && loadMorePhotos}
|
||||
>
|
||||
{#if !ended}
|
||||
<p class="more">See more photos</p>
|
||||
{:else}
|
||||
<p>You've seen it all!</p>
|
||||
{/if}
|
||||
</Pagination>
|
||||
|
||||
{#if ended}
|
||||
<div class="grid-modules">
|
||||
<div class="container grid">
|
||||
<div class="wrap">
|
||||
{#if location.has_poster}
|
||||
<ShopModule
|
||||
title="Poster available"
|
||||
text="Houses of {location.name} is available as a poster on our shop."
|
||||
images={product.photos_product}
|
||||
textBottom={null}
|
||||
buttonText="Buy"
|
||||
url="/shop/poster-{location.slug}"
|
||||
/>
|
||||
{:else}
|
||||
<ShopModule />
|
||||
{/if}
|
||||
<NewsletterModule theme="light" />
|
||||
</div>
|
||||
{#if location.has_poster}
|
||||
<Button url="/shop/poster-{location.slug}" text="Buy the poster" color="pinklight" class="shadow-small">
|
||||
<!-- <IconEarth /> -->
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if location.acknowledgement}
|
||||
<div class="acknowledgement">
|
||||
<Image
|
||||
class="flag"
|
||||
id={location.country.flag.id}
|
||||
sizeKey="square-small"
|
||||
width={32} height={32}
|
||||
alt="Flag of {location.country.name}"
|
||||
{#if hasIllustration}
|
||||
<picture class="location-page__illustration" style:--parallax-y="{illustrationOffsetY}px">
|
||||
<source media="(min-width: 1200px)" srcset={getAssetUrlKey(location.illustration_desktop_2x.id, 'illustration-desktop-2x')}>
|
||||
<source media="(min-width: 768px)" srcset={getAssetUrlKey(location.illustration_desktop.id, 'illustration-desktop-1x')}>
|
||||
<img
|
||||
src={getAssetUrlKey(location.illustration_mobile.id, 'illustration-mobile')}
|
||||
width={320}
|
||||
height={824}
|
||||
alt="Illustration for {location.name}"
|
||||
decoding="async"
|
||||
/>
|
||||
<p>{location.acknowledgement}</p>
|
||||
</div>
|
||||
</picture>
|
||||
{/if}
|
||||
</section>
|
||||
{:else}
|
||||
<div class="location-page__message">
|
||||
<p>
|
||||
No photos available for {location.name}.<br>
|
||||
Come back later!
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if photos.length}
|
||||
<section class="location-page__houses" bind:this={photosListEl} data-sveltekit-noscroll>
|
||||
{#each photos as { title, image: { id, title: alt, width, height }, slug, city, date_taken }, index}
|
||||
<House
|
||||
{title}
|
||||
photoId={id}
|
||||
photoAlt={alt}
|
||||
url="/{params.country}/{params.location}/{slug}"
|
||||
{city}
|
||||
location={location.name}
|
||||
ratio={width / height}
|
||||
date={date_taken}
|
||||
index={(totalPhotos - index < 10) ? '0' : ''}{totalPhotos - index}
|
||||
/>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<section class="location-page__next container">
|
||||
<Pagination
|
||||
ended={ended}
|
||||
current={currentPhotosAmount}
|
||||
total={totalPhotos}
|
||||
on:click={!ended && loadMorePhotos}
|
||||
>
|
||||
{#if !ended}
|
||||
<p class="more">See more photos</p>
|
||||
{:else}
|
||||
<p>You've seen it all!</p>
|
||||
{/if}
|
||||
</Pagination>
|
||||
|
||||
{#if ended}
|
||||
<div class="grid-modules">
|
||||
<div class="container grid">
|
||||
<div class="wrap">
|
||||
{#if location.has_poster}
|
||||
<ShopModule
|
||||
title="Poster available"
|
||||
text="Houses of {location.name} is available as a poster on our shop."
|
||||
images={product.photos_product}
|
||||
textBottom={null}
|
||||
buttonText="Buy"
|
||||
url="/shop/poster-{location.slug}"
|
||||
/>
|
||||
{:else}
|
||||
<ShopModule />
|
||||
{/if}
|
||||
<NewsletterModule theme="light" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if location.acknowledgement}
|
||||
<div class="acknowledgement">
|
||||
<Image
|
||||
class="flag"
|
||||
id={location.country.flag.id}
|
||||
sizeKey="square-small"
|
||||
width={32} height={32}
|
||||
alt="Flag of {location.country.name}"
|
||||
/>
|
||||
<p>{location.acknowledgement}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{:else}
|
||||
<div class="location-page__message">
|
||||
<p>
|
||||
No photos available for {location.name}.<br>
|
||||
Come back later!
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
</PageTransition>
|
||||
@@ -1,5 +1,5 @@
|
||||
<style lang="scss">
|
||||
@import "../../../../style/pages/viewer";
|
||||
@import "../../../../../style/pages/viewer";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -14,7 +14,7 @@
|
||||
import { stagger, timeline } from 'motion'
|
||||
import { getAssetUrlKey } from '$utils/api'
|
||||
import { previousPage } from '$utils/stores'
|
||||
import { DELAY } from '$utils/contants'
|
||||
import { DELAY } from '$utils/constants'
|
||||
import { throttle } from '$utils/functions'
|
||||
import { swipe } from '$utils/interactions/swipe'
|
||||
// Components
|
||||
@@ -291,10 +291,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
bind:innerWidth
|
||||
on:keydown={handleKeydown}
|
||||
/>
|
||||
<svelte:window bind:innerWidth on:keydown={handleKeydown} />
|
||||
|
||||
{#if currentPhoto}
|
||||
<Metas
|
||||
@@ -305,98 +302,100 @@
|
||||
{/if}
|
||||
|
||||
|
||||
<PageTransition name="photo-page">
|
||||
<div class="container grid">
|
||||
<p class="photo-page__notice text-label">Tap for fullscreen</p>
|
||||
<PageTransition>
|
||||
<main class="photo-page">
|
||||
<div class="container grid">
|
||||
<p class="photo-page__notice text-label">Tap for fullscreen</p>
|
||||
|
||||
<ButtonCircle
|
||||
tag="a"
|
||||
url={previousUrl}
|
||||
color="purple"
|
||||
class="close shadow-box-dark"
|
||||
label="Close"
|
||||
>
|
||||
<svg width="12" height="12">
|
||||
<use xlink:href="#cross">
|
||||
</svg>
|
||||
</ButtonCircle>
|
||||
<ButtonCircle
|
||||
tag="a"
|
||||
url={previousUrl}
|
||||
color="purple"
|
||||
class="close shadow-box-dark"
|
||||
label="Close"
|
||||
>
|
||||
<svg width="12" height="12">
|
||||
<use xlink:href="#cross">
|
||||
</svg>
|
||||
</ButtonCircle>
|
||||
|
||||
<div class="photo-page__carousel">
|
||||
<div class="photo-page__images" use:swipe on:swipe={handleSwipe} on:tap={toggleFullscreen}>
|
||||
{#each visiblePhotos as { id, image, title }, index (id)}
|
||||
<div class="photo-page__picture is-{currentIndex === 0 ? index + 1 : index}">
|
||||
<Image
|
||||
class="photo {image.width / image.height < 1.475 ? 'not-landscape' : ''}"
|
||||
id={image.id}
|
||||
alt={title}
|
||||
sizeKey="photo-list"
|
||||
sizes={{
|
||||
small: { width: 500 },
|
||||
medium: { width: 850 },
|
||||
large: { width: 1280 },
|
||||
}}
|
||||
ratio={1.5}
|
||||
/>
|
||||
<div class="photo-page__carousel">
|
||||
<div class="photo-page__images" use:swipe on:swipe={handleSwipe} on:tap={toggleFullscreen}>
|
||||
{#each visiblePhotos as { id, image, title }, index (id)}
|
||||
<div class="photo-page__picture is-{currentIndex === 0 ? index + 1 : index}">
|
||||
<Image
|
||||
class="photo {image.width / image.height < 1.475 ? 'not-landscape' : ''}"
|
||||
id={image.id}
|
||||
alt={title}
|
||||
sizeKey="photo-list"
|
||||
sizes={{
|
||||
small: { width: 500 },
|
||||
medium: { width: 850 },
|
||||
large: { width: 1280 },
|
||||
}}
|
||||
ratio={1.5}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="photo-page__controls">
|
||||
<ButtonCircle class="prev shadow-box-dark" label="Previous" disabled={!canGoNext} clone={true} on:click={goToPrevious}>
|
||||
<IconArrow color="pink" flip={true} />
|
||||
</ButtonCircle>
|
||||
<ButtonCircle class="next shadow-box-dark" label="Next" disabled={!canGoPrev} clone={true} on:click={goToNext}>
|
||||
<IconArrow color="pink" />
|
||||
</ButtonCircle>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="photo-page__controls">
|
||||
<ButtonCircle class="prev shadow-box-dark" label="Previous" disabled={!canGoNext} clone={true} on:click={goToPrevious}>
|
||||
<IconArrow color="pink" flip={true} />
|
||||
</ButtonCircle>
|
||||
<ButtonCircle class="next shadow-box-dark" label="Next" disabled={!canGoPrev} clone={true} on:click={goToNext}>
|
||||
<IconArrow color="pink" />
|
||||
</ButtonCircle>
|
||||
|
||||
<div class="photo-page__index title-index">
|
||||
<SplitText text="{(currentPhotoIndex < 10) ? '0' : ''}{currentPhotoIndex}" mode="chars" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="photo-page__info">
|
||||
<h1 class="title-medium">{currentPhoto.title}</h1>
|
||||
|
||||
<div class="photo-page__index title-index">
|
||||
<SplitText text="{(currentPhotoIndex < 10) ? '0' : ''}{currentPhotoIndex}" mode="chars" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="photo-page__info">
|
||||
<h1 class="title-medium">{currentPhoto.title}</h1>
|
||||
|
||||
<div class="detail text-info">
|
||||
<a href="/{location.country.slug}/{location.slug}" data-sveltekit-prefetch data-sveltekit-noscroll>
|
||||
<Icon class="icon" icon="map-pin" label="Map pin" />
|
||||
<span>
|
||||
{#if currentPhoto.city}
|
||||
{currentPhoto.city}, {location.name}, {location.country.name}
|
||||
{:else}
|
||||
{location.name}, {location.country.name}
|
||||
{/if}
|
||||
</span>
|
||||
</a>
|
||||
{#if currentPhoto.date_taken}
|
||||
<span class="sep">·</span>
|
||||
<time datetime={dayjs(currentPhoto.date_taken).format('YYYY-MM-DD')}>{dayjs(currentPhoto.date_taken).format('MMMM YYYY')}</time>
|
||||
{/if}
|
||||
<div class="detail text-info">
|
||||
<a href="/{location.country.slug}/{location.slug}" data-sveltekit-noscroll>
|
||||
<Icon class="icon" icon="map-pin" label="Map pin" />
|
||||
<span>
|
||||
{#if currentPhoto.city}
|
||||
{currentPhoto.city}, {location.name}, {location.country.name}
|
||||
{:else}
|
||||
{location.name}, {location.country.name}
|
||||
{/if}
|
||||
</span>
|
||||
</a>
|
||||
{#if currentPhoto.date_taken}
|
||||
<span class="sep">·</span>
|
||||
<time datetime={dayjs(currentPhoto.date_taken).format('YYYY-MM-DD')}>{dayjs(currentPhoto.date_taken).format('MMMM YYYY')}</time>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isFullscreen}
|
||||
<div class="photo-page__fullscreen" bind:this={fullscreenEl} on:click={toggleFullscreen}
|
||||
in:fade={{ easing: quartOut, duration: 1000 }}
|
||||
out:fade={{ easing: quartOut, duration: 1000, delay: 300 }}
|
||||
>
|
||||
<div class="inner" transition:scale={{ easing: quartOut, start: 1.1, duration: 1000 }}>
|
||||
<Image
|
||||
id={currentPhoto.image.id}
|
||||
sizeKey="photo-grid-large"
|
||||
width={1266}
|
||||
height={844}
|
||||
alt={currentPhoto.title}
|
||||
/>
|
||||
<ButtonCircle color="gray-medium" class="close">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="#fff" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.751 0c4.274 0 7.752 3.477 7.752 7.751 0 1.846-.65 3.543-1.73 4.875l3.99 3.991a.81.81 0 1 1-1.146 1.146l-3.99-3.991a7.714 7.714 0 0 1-4.876 1.73C3.477 15.503 0 12.027 0 7.753 0 3.476 3.477 0 7.751 0Zm0 1.62a6.138 6.138 0 0 0-6.13 6.131 6.138 6.138 0 0 0 6.13 6.132 6.138 6.138 0 0 0 6.131-6.132c0-3.38-2.75-6.13-6.13-6.13Zm2.38 5.321a.81.81 0 1 1 0 1.62h-4.76a.81.81 0 1 1 0-1.62h4.76Z" />
|
||||
</svg>
|
||||
</ButtonCircle>
|
||||
{#if isFullscreen}
|
||||
<div class="photo-page__fullscreen" bind:this={fullscreenEl} on:click={toggleFullscreen}
|
||||
in:fade={{ easing: quartOut, duration: 1000 }}
|
||||
out:fade={{ easing: quartOut, duration: 1000, delay: 300 }}
|
||||
>
|
||||
<div class="inner" transition:scale={{ easing: quartOut, start: 1.1, duration: 1000 }}>
|
||||
<Image
|
||||
id={currentPhoto.image.id}
|
||||
sizeKey="photo-grid-large"
|
||||
width={1266}
|
||||
height={844}
|
||||
alt={currentPhoto.title}
|
||||
/>
|
||||
<ButtonCircle color="gray-medium" class="close">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="#fff" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.751 0c4.274 0 7.752 3.477 7.752 7.751 0 1.846-.65 3.543-1.73 4.875l3.99 3.991a.81.81 0 1 1-1.146 1.146l-3.99-3.991a7.714 7.714 0 0 1-4.876 1.73C3.477 15.503 0 12.027 0 7.753 0 3.476 3.477 0 7.751 0Zm0 1.62a6.138 6.138 0 0 0-6.13 6.131 6.138 6.138 0 0 0 6.13 6.132 6.138 6.138 0 0 0 6.131-6.132c0-3.38-2.75-6.13-6.13-6.13Zm2.38 5.321a.81.81 0 1 1 0 1.62h-4.76a.81.81 0 1 1 0-1.62h4.76Z" />
|
||||
</svg>
|
||||
</ButtonCircle>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</main>
|
||||
</PageTransition>
|
||||
402
src/routes/(site)/about/+page.svelte
Normal file
402
src/routes/(site)/about/+page.svelte
Normal 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>
|
||||
149
src/routes/(site)/credits/+page.svelte
Normal file
149
src/routes/(site)/credits/+page.svelte
Normal 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>
|
||||
94
src/routes/(site)/feed/products.xml/+server.ts
Normal file
94
src/routes/(site)/feed/products.xml/+server.ts
Normal 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, & Visual Artwork',
|
||||
value: 'Home & Garden > Decor > Artwork > Posters, Prints, & 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>`
|
||||
}
|
||||
8
src/routes/(site)/locations/+page.server.ts
Normal file
8
src/routes/(site)/locations/+page.server.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Actions } from './$types'
|
||||
import subscribe from '$utils/forms/subscribe'
|
||||
|
||||
|
||||
export const actions: Actions = {
|
||||
// Form newsletter subscription
|
||||
subscribe,
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/pages/explore";
|
||||
@import "../../../style/pages/explore";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -20,21 +20,23 @@
|
||||
<Metas
|
||||
title="Locations – Houses Of"
|
||||
description={text}
|
||||
image=""
|
||||
/>
|
||||
|
||||
<PageTransition name="explore">
|
||||
<Heading {text} />
|
||||
|
||||
<section class="explore__locations">
|
||||
<InteractiveGlobe />
|
||||
<Locations {locations} />
|
||||
</section>
|
||||
<PageTransition>
|
||||
<main class="explore">
|
||||
<Heading {text} />
|
||||
|
||||
<section class="grid-modules is-spaced grid">
|
||||
<div class="wrap">
|
||||
<ShopModule />
|
||||
<NewsletterModule />
|
||||
</div>
|
||||
</section>
|
||||
<section class="explore__locations">
|
||||
<InteractiveGlobe />
|
||||
<Locations {locations} />
|
||||
</section>
|
||||
|
||||
<section class="grid-modules is-spaced grid">
|
||||
<div class="wrap">
|
||||
<ShopModule />
|
||||
<NewsletterModule />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</PageTransition>
|
||||
@@ -1,9 +1,13 @@
|
||||
import { error } from '@sveltejs/kit'
|
||||
import type { PageServerLoad } from './$types'
|
||||
import type { Actions, PageServerLoad } from './$types'
|
||||
import { fetchAPI } from '$utils/api'
|
||||
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 }) => {
|
||||
try {
|
||||
// Query parameters
|
||||
@@ -86,3 +90,12 @@ export const load: PageServerLoad = async ({ url, setHeaders }) => {
|
||||
throw error(500, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Form Data
|
||||
*/
|
||||
export const actions: Actions = {
|
||||
// Form newsletter subscription
|
||||
subscribe,
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/pages/photos";
|
||||
@import "../../../style/pages/photos";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -12,7 +12,7 @@
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { stagger, timeline } from 'motion'
|
||||
import { DELAY } from '$utils/contants'
|
||||
import { DELAY } from '$utils/constants'
|
||||
import { map, lerp, throttle } from '$utils/functions'
|
||||
import { getAssetUrlKey } from '$utils/api'
|
||||
import { quartOut } from '$animations/easings'
|
||||
@@ -336,162 +336,164 @@
|
||||
/>
|
||||
|
||||
|
||||
<PageTransition name="photos-page">
|
||||
<section class="photos-page__intro"
|
||||
class:is-passed={scrolledPastIntro}
|
||||
>
|
||||
<ScrollingTitle tag="h1" text="Houses">
|
||||
<SplitText text="Houses" mode="chars" />
|
||||
</ScrollingTitle>
|
||||
|
||||
<DiscoverText />
|
||||
|
||||
<div class="filters"
|
||||
class:is-over={filtersOver}
|
||||
class:is-transitioning={filtersTransitioning}
|
||||
class:is-visible={filtersVisible}
|
||||
<PageTransition>
|
||||
<main class="photos-page">
|
||||
<section class="photos-page__intro"
|
||||
class:is-passed={scrolledPastIntro}
|
||||
>
|
||||
<div class="filters__bar">
|
||||
<span class="text-label filters__label">Filter photos</span>
|
||||
<ul>
|
||||
<li>
|
||||
<Select
|
||||
name="country" id="filter_country"
|
||||
options={[
|
||||
{
|
||||
value: defaultCountry,
|
||||
name: 'Worldwide',
|
||||
default: true,
|
||||
selected: filterCountry === defaultCountry,
|
||||
},
|
||||
...countries.map(({ slug, name }) => ({
|
||||
value: slug,
|
||||
name,
|
||||
selected: filterCountry === slug,
|
||||
}))
|
||||
]}
|
||||
on:change={handleCountryChange}
|
||||
value={filterCountry}
|
||||
>
|
||||
{#if countryFlagId}
|
||||
<Image
|
||||
class="icon"
|
||||
id={countryFlagId}
|
||||
sizeKey="square-small"
|
||||
width={26} height={26}
|
||||
alt="{filterCountry} flag"
|
||||
/>
|
||||
{:else}
|
||||
<IconEarth class="icon" />
|
||||
{/if}
|
||||
</Select>
|
||||
</li>
|
||||
<li>
|
||||
<Select
|
||||
name="sort" id="filter_sort"
|
||||
options={[
|
||||
{
|
||||
value: 'latest',
|
||||
name: 'Latest photos',
|
||||
default: true,
|
||||
selected: filterSort === defaultSort
|
||||
},
|
||||
{
|
||||
value: 'oldest',
|
||||
name: 'Oldest photos',
|
||||
selected: filterSort === 'oldest'
|
||||
},
|
||||
]}
|
||||
on:change={handleSortChange}
|
||||
value={filterSort}
|
||||
>
|
||||
<svg class="icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-label="Sort icon">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.878 15.93h-4.172c-.638 0-1.153.516-1.153 1.154 0 .639.515 1.154 1.153 1.154h4.172c.638 0 1.153-.515 1.153-1.154a1.152 1.152 0 0 0-1.153-1.153Zm3.244-5.396h-7.405c-.639 0-1.154.515-1.154 1.153 0 .639.515 1.154 1.154 1.154h7.405c.639 0 1.154-.515 1.154-1.154a1.145 1.145 0 0 0-1.154-1.153Zm3.244-5.408h-10.65c-.638 0-1.153.515-1.153 1.154 0 .639.515 1.154 1.154 1.154h10.65c.638 0 1.153-.515 1.153-1.154 0-.639-.515-1.154-1.154-1.154ZM7.37 20.679V3.376c0-.145-.03-.289-.082-.433a1.189 1.189 0 0 0-.628-.618 1.197 1.197 0 0 0-.886 0 1.045 1.045 0 0 0-.36.237c-.01 0-.01 0-.021.01L.82 7.145a1.156 1.156 0 0 0 0 1.638 1.156 1.156 0 0 0 1.637 0l2.596-2.596v11.7l-2.596-2.595a1.156 1.156 0 0 0-1.637 0 1.156 1.156 0 0 0 0 1.638l4.573 4.573c.103.103.237.185.37.247.135.062.289.082.433.082h.02c.145 0 .3-.03.433-.093a1.14 1.14 0 0 0 .629-.628.987.987 0 0 0 .092-.432Z" />
|
||||
</svg>
|
||||
</Select>
|
||||
</li>
|
||||
</ul>
|
||||
<ScrollingTitle tag="h1" text="Houses">
|
||||
<SplitText text="Houses" mode="chars" />
|
||||
</ScrollingTitle>
|
||||
|
||||
<div class="filters__actions">
|
||||
{#if filtered}
|
||||
<button class="reset button-link"
|
||||
on:click={resetFiltered}
|
||||
transition:fly={{ y: 4, duration: 600, easing: quartOutSvelte }}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<DiscoverText />
|
||||
|
||||
<section class="photos-page__content" bind:this={photosContentEl} style:--margin-sides="{sideMargins}px">
|
||||
<div class="grid container">
|
||||
{#if photos}
|
||||
<div class="photos-page__grid" bind:this={photosGridEl} data-sveltekit-noscroll data-sveltekit-prefetch>
|
||||
{#each photos as { id, image, slug, location, title, city }, index (id)}
|
||||
<figure class="photo shadow-photo">
|
||||
<a href="/{location.country.slug}/{location.slug}/{slug}" tabindex="0">
|
||||
<Image
|
||||
id={image.id}
|
||||
sizeKey="photo-grid"
|
||||
sizes={{
|
||||
small: { width: 500 },
|
||||
medium: { width: 900 },
|
||||
large: { width: 1440 },
|
||||
}}
|
||||
ratio={1.5}
|
||||
alt={image.title}
|
||||
/>
|
||||
</a>
|
||||
<figcaption>
|
||||
<PostCard
|
||||
street={title}
|
||||
location={city ? `${city}, ${location.name}` : location.name}
|
||||
region={location.region}
|
||||
country={location.country.name}
|
||||
flagId={location.country.flag.id}
|
||||
size={isSmall(index) ? 'small' : null}
|
||||
/>
|
||||
</figcaption>
|
||||
</figure>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="filters"
|
||||
class:is-over={filtersOver}
|
||||
class:is-transitioning={filtersTransitioning}
|
||||
class:is-visible={filtersVisible}
|
||||
>
|
||||
<div class="filters__bar">
|
||||
<span class="text-label filters__label">Filter photos</span>
|
||||
<ul>
|
||||
<li>
|
||||
<Select
|
||||
name="country" id="filter_country"
|
||||
options={[
|
||||
{
|
||||
value: defaultCountry,
|
||||
name: 'Worldwide',
|
||||
default: true,
|
||||
selected: filterCountry === defaultCountry,
|
||||
},
|
||||
...countries.map(({ slug, name }) => ({
|
||||
value: slug,
|
||||
name,
|
||||
selected: filterCountry === slug,
|
||||
}))
|
||||
]}
|
||||
on:change={handleCountryChange}
|
||||
value={filterCountry}
|
||||
>
|
||||
{#if countryFlagId}
|
||||
<Image
|
||||
class="icon"
|
||||
id={countryFlagId}
|
||||
sizeKey="square-small"
|
||||
width={26} height={26}
|
||||
alt="{filterCountry} flag"
|
||||
/>
|
||||
{:else}
|
||||
<IconEarth class="icon" />
|
||||
{/if}
|
||||
</Select>
|
||||
</li>
|
||||
<li>
|
||||
<Select
|
||||
name="sort" id="filter_sort"
|
||||
options={[
|
||||
{
|
||||
value: 'latest',
|
||||
name: 'Latest',
|
||||
default: true,
|
||||
selected: filterSort === defaultSort
|
||||
},
|
||||
{
|
||||
value: 'oldest',
|
||||
name: 'Oldest',
|
||||
selected: filterSort === 'oldest'
|
||||
},
|
||||
]}
|
||||
on:change={handleSortChange}
|
||||
value={filterSort}
|
||||
>
|
||||
<svg class="icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-label="Sort icon">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.878 15.93h-4.172c-.638 0-1.153.516-1.153 1.154 0 .639.515 1.154 1.153 1.154h4.172c.638 0 1.153-.515 1.153-1.154a1.152 1.152 0 0 0-1.153-1.153Zm3.244-5.396h-7.405c-.639 0-1.154.515-1.154 1.153 0 .639.515 1.154 1.154 1.154h7.405c.639 0 1.154-.515 1.154-1.154a1.145 1.145 0 0 0-1.154-1.153Zm3.244-5.408h-10.65c-.638 0-1.153.515-1.153 1.154 0 .639.515 1.154 1.154 1.154h10.65c.638 0 1.153-.515 1.153-1.154 0-.639-.515-1.154-1.154-1.154ZM7.37 20.679V3.376c0-.145-.03-.289-.082-.433a1.189 1.189 0 0 0-.628-.618 1.197 1.197 0 0 0-.886 0 1.045 1.045 0 0 0-.36.237c-.01 0-.01 0-.021.01L.82 7.145a1.156 1.156 0 0 0 0 1.638 1.156 1.156 0 0 0 1.637 0l2.596-2.596v11.7l-2.596-2.595a1.156 1.156 0 0 0-1.637 0 1.156 1.156 0 0 0 0 1.638l4.573 4.573c.103.103.237.185.37.247.135.062.289.082.433.082h.02c.145 0 .3-.03.433-.093a1.14 1.14 0 0 0 .629-.628.987.987 0 0 0 .092-.432Z" />
|
||||
</svg>
|
||||
</Select>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="controls grid">
|
||||
<p class="controls__date" title={dayjs(latestPhoto.date_created).format('DD/MM/YYYY, hh:mm')}>
|
||||
Last updated: <time datetime={dayjs(latestPhoto.date_created).format('YYYY-MM-DD')}>{dayjs().to(dayjs(latestPhoto.date_created))}</time>
|
||||
</p>
|
||||
|
||||
<Button
|
||||
tag="button"
|
||||
text={!ended ? 'See more photos' : "You've seen it all!"}
|
||||
size="large" color="beige"
|
||||
on:click={loadMorePhotos}
|
||||
disabled={ended}
|
||||
/>
|
||||
|
||||
<div class="controls__count">
|
||||
<span class="current">{currentPhotosAmount}</span>
|
||||
<span>/</span>
|
||||
<span class="total">{totalPhotos}</span>
|
||||
<div class="filters__actions">
|
||||
{#if filtered}
|
||||
<button class="reset button-link"
|
||||
on:click={resetFiltered}
|
||||
transition:fly={{ y: 4, duration: 600, easing: quartOutSvelte }}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if !filteredCountryExists}
|
||||
<div class="photos-page__message">
|
||||
<p>
|
||||
<strong>{$page.url.searchParams.get('country').replace(/(^|\s)\S/g, letter => letter.toUpperCase())}</strong> is not available… yet 👀
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid-modules">
|
||||
<div class="wrap">
|
||||
<ShopModule />
|
||||
<NewsletterModule theme="light" />
|
||||
<section class="photos-page__content" bind:this={photosContentEl} style:--margin-sides="{sideMargins}px">
|
||||
<div class="grid container">
|
||||
{#if photos}
|
||||
<div class="photos-page__grid" bind:this={photosGridEl} data-sveltekit-noscroll>
|
||||
{#each photos as { id, image, slug, location, title, city }, index (id)}
|
||||
<figure class="photo shadow-photo">
|
||||
<a href="/{location.country.slug}/{location.slug}/{slug}" tabindex="0">
|
||||
<Image
|
||||
id={image.id}
|
||||
sizeKey="photo-grid"
|
||||
sizes={{
|
||||
small: { width: 500 },
|
||||
medium: { width: 900 },
|
||||
large: { width: 1440 },
|
||||
}}
|
||||
ratio={1.5}
|
||||
alt={image.title}
|
||||
/>
|
||||
</a>
|
||||
<figcaption>
|
||||
<PostCard
|
||||
street={title}
|
||||
location={city ? `${city}, ${location.name}` : location.name}
|
||||
region={location.region}
|
||||
country={location.country.name}
|
||||
flagId={location.country.flag.id}
|
||||
size={isSmall(index) ? 'small' : null}
|
||||
/>
|
||||
</figcaption>
|
||||
</figure>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="controls grid">
|
||||
<p class="controls__date" title={dayjs(latestPhoto.date_created).format('DD/MM/YYYY, hh:mm')}>
|
||||
Last updated: <time datetime={dayjs(latestPhoto.date_created).format('YYYY-MM-DD')}>{dayjs().to(dayjs(latestPhoto.date_created))}</time>
|
||||
</p>
|
||||
|
||||
<Button
|
||||
tag="button"
|
||||
text={!ended ? 'See more photos' : "You've seen it all!"}
|
||||
size="large" color="beige"
|
||||
on:click={loadMorePhotos}
|
||||
disabled={ended}
|
||||
/>
|
||||
|
||||
<div class="controls__count">
|
||||
<span class="current">{currentPhotosAmount}</span>
|
||||
<span>/</span>
|
||||
<span class="total">{totalPhotos}</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else if !filteredCountryExists}
|
||||
<div class="photos-page__message">
|
||||
<p>
|
||||
<strong>{$page.url.searchParams.get('country').replace(/(^|\s)\S/g, letter => letter.toUpperCase())}</strong> is not available… yet 👀
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid-modules">
|
||||
<div class="wrap">
|
||||
<ShopModule />
|
||||
<NewsletterModule theme="light" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</PageTransition>
|
||||
@@ -1,7 +1,12 @@
|
||||
import { error } from '@sveltejs/kit'
|
||||
import type { PageServerLoad } from './$types'
|
||||
import type { Actions, PageServerLoad } from './$types'
|
||||
import { fetchAPI } from '$utils/api'
|
||||
import subscribe from '$utils/forms/subscribe'
|
||||
|
||||
|
||||
/**
|
||||
* Page Data
|
||||
*/
|
||||
export const load: PageServerLoad = async ({ setHeaders }) => {
|
||||
try {
|
||||
const res = await fetchAPI(`query {
|
||||
@@ -36,3 +41,12 @@ export const load: PageServerLoad = async ({ setHeaders }) => {
|
||||
throw error(500, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Form Data
|
||||
*/
|
||||
export const actions: Actions = {
|
||||
// Form newsletter subscription
|
||||
subscribe,
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/pages/subscribe";
|
||||
@import "../../../style/pages/subscribe";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { PageData } from './$types'
|
||||
import { onMount } from 'svelte'
|
||||
import { stagger, timeline } from 'motion'
|
||||
import { DELAY } from '$utils/contants'
|
||||
import { DELAY } from '$utils/constants'
|
||||
import { quartOut } from '$animations/easings'
|
||||
// Components
|
||||
import Metas from '$components/Metas.svelte'
|
||||
@@ -65,32 +65,35 @@
|
||||
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">
|
||||
<div class="subscribe__top">
|
||||
<Heading
|
||||
text={data.newsletter_page_text}
|
||||
/>
|
||||
|
||||
<EmailForm />
|
||||
</div>
|
||||
<PageTransition>
|
||||
<main class="subscribe">
|
||||
<div class="subscribe__top">
|
||||
<Heading
|
||||
text={data.newsletter_page_text}
|
||||
/>
|
||||
|
||||
<section class="subscribe__issues">
|
||||
<h2 class="title-small">Latest Issue</h2>
|
||||
<div class="issue-container">
|
||||
<NewsletterIssue size="large" date={latestIssue.date_sent} {...latestIssue} />
|
||||
<EmailForm />
|
||||
</div>
|
||||
|
||||
{#if issues.length > 1}
|
||||
<h2 class="title-small">Past Issues</h2>
|
||||
<ul>
|
||||
{#each issues.slice(1) as { issue, title, date_sent: date, link, thumbnail }}
|
||||
<li class="issue-container">
|
||||
<NewsletterIssue {issue} {title} {link} {thumbnail} {date} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
<section class="subscribe__issues">
|
||||
<h2 class="title-small">Latest Issue</h2>
|
||||
<div class="issue-container">
|
||||
<NewsletterIssue size="large" date={latestIssue.date_sent} {...latestIssue} />
|
||||
</div>
|
||||
|
||||
<InteractiveGlobe type="cropped" />
|
||||
{#if issues.length > 1}
|
||||
<h2 class="title-small">Past Issues</h2>
|
||||
<ul>
|
||||
{#each issues.slice(1) as { issue, title, date_sent: date, link, thumbnail }}
|
||||
<li class="issue-container">
|
||||
<NewsletterIssue {issue} {title} {link} {thumbnail} {date} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<InteractiveGlobe type="cropped" />
|
||||
</main>
|
||||
</PageTransition>
|
||||
49
src/routes/(site)/terms/+page.svelte
Normal file
49
src/routes/(site)/terms/+page.svelte
Normal 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>
|
||||
@@ -34,49 +34,52 @@
|
||||
title="{errors[$page.status].title} – Houses Of"
|
||||
/>
|
||||
|
||||
<PageTransition name="page-error">
|
||||
<div class="page-error__top">
|
||||
<Heading
|
||||
text="{$page.error.message ?? errors[$page.status].message} <br>{defaultMessage}"
|
||||
/>
|
||||
|
||||
<ListCTAs>
|
||||
<li>
|
||||
<BoxCTA
|
||||
url="/photos"
|
||||
icon="photos"
|
||||
label="Browse all photos"
|
||||
alt="Photos"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BoxCTA
|
||||
url="/shop"
|
||||
icon="bag"
|
||||
label="Shop our products"
|
||||
alt="Shopping bag"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BoxCTA
|
||||
url="/about"
|
||||
icon="compass"
|
||||
label="Learn about the project"
|
||||
alt="Compass"
|
||||
/>
|
||||
</li>
|
||||
</ListCTAs>
|
||||
</div>
|
||||
<PageTransition>
|
||||
<main class="page-error">
|
||||
<div class="page-error__top">
|
||||
<Heading
|
||||
text="{$page.error.message ?? errors[$page.status].message} <br>{defaultMessage}"
|
||||
/>
|
||||
|
||||
<InteractiveGlobe />
|
||||
<Locations {locations} />
|
||||
<ListCTAs>
|
||||
<li>
|
||||
<BoxCTA
|
||||
url="/photos"
|
||||
icon="photos"
|
||||
label="Browse all photos"
|
||||
alt="Photos"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BoxCTA
|
||||
url="/shop"
|
||||
icon="bag"
|
||||
label="Shop our products"
|
||||
alt="Shopping bag"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BoxCTA
|
||||
url="/about"
|
||||
icon="compass"
|
||||
label="Learn about the project"
|
||||
alt="Compass"
|
||||
/>
|
||||
</li>
|
||||
</ListCTAs>
|
||||
</div>
|
||||
|
||||
<div class="grid-modules">
|
||||
<div class="container grid">
|
||||
<div class="wrap">
|
||||
<ShopModule />
|
||||
<NewsletterModule />
|
||||
<InteractiveGlobe />
|
||||
<Locations {locations} />
|
||||
|
||||
<div class="grid-modules">
|
||||
<div class="container grid">
|
||||
<div class="wrap">
|
||||
<ShopModule />
|
||||
<NewsletterModule />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</PageTransition>
|
||||
@@ -1,10 +1,10 @@
|
||||
import { error } from '@sveltejs/kit'
|
||||
import type { PageServerLoad } from './$types'
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
import { fetchAPI } from '$utils/api'
|
||||
import { PUBLIC_PREVIEW_COUNT } from '$env/static/public'
|
||||
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
export const load: LayoutServerLoad = async () => {
|
||||
try {
|
||||
const res = await fetchAPI(`query {
|
||||
locations: location (filter: { status: { _eq: "published" }}) {
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
import '../style/global.scss'
|
||||
|
||||
import { browser } from '$app/environment'
|
||||
import { navigating, page } from '$app/stores'
|
||||
import { page } from '$app/stores'
|
||||
import { beforeNavigate } from '$app/navigation'
|
||||
import { PUBLIC_ANALYTICS_DOMAIN } from '$env/static/public'
|
||||
import type { PageData } from './$types'
|
||||
import { onMount, setContext } from 'svelte'
|
||||
import { pageLoading, previousPage } from '$utils/stores'
|
||||
import { DURATION } from '$utils/contants'
|
||||
import '$utils/polyfills'
|
||||
import { PUBLIC_ANALYTICS_KEY, PUBLIC_ANALYTICS_URL } from '$env/static/public'
|
||||
// Components
|
||||
import SVGSprite from '$components/SVGSprite.svelte'
|
||||
import SmoothScroll from '$components/SmoothScroll.svelte'
|
||||
@@ -44,17 +43,9 @@
|
||||
$previousPage = from.url.pathname
|
||||
})
|
||||
|
||||
// Define page loading from navigating store
|
||||
navigating.subscribe((store: any) => {
|
||||
if (store) {
|
||||
$pageLoading = true
|
||||
// Define page loading
|
||||
$: browser && document.body.classList.toggle('is-loading', $pageLoading)
|
||||
|
||||
// Turn page loading when changing page
|
||||
setTimeout(() => {
|
||||
$pageLoading = false
|
||||
}, DURATION.PAGE_IN * 1.25)
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
// Avoid FOUC
|
||||
@@ -65,6 +56,8 @@
|
||||
<svelte:window bind:innerHeight />
|
||||
|
||||
<svelte:head>
|
||||
<link rel="canonical" href={$page.url.href} />
|
||||
|
||||
{#each fonts as font}
|
||||
<link rel="preload" href="/fonts/{font}.woff2" as="font" type="font/woff2" crossorigin="anonymous">
|
||||
{/each}
|
||||
@@ -79,16 +72,9 @@
|
||||
<Footer />
|
||||
{/if}
|
||||
|
||||
{#if $pageLoading}
|
||||
<div class="page-loading" />
|
||||
{/if}
|
||||
|
||||
<SVGSprite />
|
||||
<SmoothScroll />
|
||||
|
||||
{#if browser}
|
||||
<Analytics
|
||||
appKey={PUBLIC_ANALYTICS_KEY}
|
||||
url={PUBLIC_ANALYTICS_URL}
|
||||
/>
|
||||
<Analytics domain={PUBLIC_ANALYTICS_DOMAIN} />
|
||||
{/if}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { error } from '@sveltejs/kit'
|
||||
import type { PageServerLoad } from './$types'
|
||||
import type { Actions, PageServerLoad } from './$types'
|
||||
import { fetchAPI } from '$utils/api'
|
||||
import { getRandomItems } from '$utils/functions'
|
||||
import subscribe from '$utils/forms/subscribe'
|
||||
|
||||
|
||||
/**
|
||||
* Page Data
|
||||
*/
|
||||
export const load: PageServerLoad = async ({ setHeaders }) => {
|
||||
try {
|
||||
// Get total of published photos
|
||||
@@ -53,3 +58,12 @@ export const load: PageServerLoad = async ({ setHeaders }) => {
|
||||
throw error(500, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Form Data
|
||||
*/
|
||||
export const actions: Actions = {
|
||||
// Form newsletter subscription
|
||||
subscribe,
|
||||
}
|
||||
@@ -3,11 +3,11 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { navigating } from '$app/stores'
|
||||
import { navigating, page } from '$app/stores'
|
||||
import type { PageData } from './$types'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { timeline, stagger } from 'motion'
|
||||
import { DELAY } from '$utils/contants'
|
||||
import { DELAY } from '$utils/constants'
|
||||
import { smoothScroll } from '$utils/stores'
|
||||
import { getAssetUrlKey } from '$utils/api'
|
||||
import reveal from '$animations/reveal'
|
||||
@@ -81,87 +81,90 @@
|
||||
image={getAssetUrlKey(settings.seo_image.id, 'share-image')}
|
||||
/>
|
||||
|
||||
<PageTransition name="homepage">
|
||||
<section class="homepage__intro"
|
||||
use:reveal={{
|
||||
animation: { opacity: [0, 1] },
|
||||
options: {
|
||||
duration: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ScrollingTitle
|
||||
tag="h1"
|
||||
class="title-houses"
|
||||
label="Houses of the World"
|
||||
offsetStart={-300}
|
||||
offsetEnd={400}
|
||||
|
||||
<PageTransition>
|
||||
<main class="homepage">
|
||||
<section class="homepage__intro"
|
||||
use:reveal={{
|
||||
animation: { opacity: [0, 1] },
|
||||
options: {
|
||||
duration: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SplitText text="Houses" mode="chars" />
|
||||
</ScrollingTitle>
|
||||
<ScrollingTitle
|
||||
tag="h1"
|
||||
class="title-houses"
|
||||
label="Houses of the World"
|
||||
offsetStart={-300}
|
||||
offsetEnd={400}
|
||||
>
|
||||
<SplitText text="Houses" mode="chars" />
|
||||
</ScrollingTitle>
|
||||
|
||||
<div class="homepage__headline">
|
||||
<p class="text-medium">
|
||||
{settings.description}
|
||||
</p>
|
||||
<div class="homepage__headline">
|
||||
<p class="text-medium">
|
||||
{settings.description}
|
||||
</p>
|
||||
|
||||
<Button url="#locations" text="Explore locations" on:click={() => $smoothScroll.scrollTo('#locations', { duration: 2 })}>
|
||||
<IconEarth animate={true} />
|
||||
</Button>
|
||||
<Button url="#locations" text="Explore locations" on:click={() => $smoothScroll.scrollTo('#locations', { duration: 2 })}>
|
||||
<IconEarth animate={true} />
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="homepage__photos">
|
||||
<Collage {photos} />
|
||||
</section>
|
||||
|
||||
<div class="homepage__ctas">
|
||||
<DiscoverText />
|
||||
|
||||
<ListCTAs>
|
||||
<li>
|
||||
<BoxCTA
|
||||
url="/photos"
|
||||
icon="photos"
|
||||
label="Browse all photos"
|
||||
alt="Photos"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BoxCTA
|
||||
url="/shop"
|
||||
icon="bag"
|
||||
label="Shop our products"
|
||||
alt="Shopping bag"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BoxCTA
|
||||
url="/about"
|
||||
icon="compass"
|
||||
label="Learn about the project"
|
||||
alt="Compass"
|
||||
/>
|
||||
</li>
|
||||
</ListCTAs>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="homepage__photos">
|
||||
<Collage {photos} />
|
||||
</section>
|
||||
<section class="homepage__locations" id="locations">
|
||||
<InteractiveGlobe />
|
||||
|
||||
<div class="homepage__ctas">
|
||||
<DiscoverText />
|
||||
<ScrollingTitle tag="p" class="title-world mask">
|
||||
<SplitText text="World" mode="chars" />
|
||||
</ScrollingTitle>
|
||||
|
||||
<ListCTAs>
|
||||
<li>
|
||||
<BoxCTA
|
||||
url="/photos"
|
||||
icon="photos"
|
||||
label="Browse all photos"
|
||||
alt="Photos"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BoxCTA
|
||||
url="/shop"
|
||||
icon="bag"
|
||||
label="Shop our products"
|
||||
alt="Shopping bag"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BoxCTA
|
||||
url="/about"
|
||||
icon="compass"
|
||||
label="Learn about the project"
|
||||
alt="Compass"
|
||||
/>
|
||||
</li>
|
||||
</ListCTAs>
|
||||
</div>
|
||||
<Locations {locations} />
|
||||
</section>
|
||||
|
||||
<section class="homepage__locations" id="locations">
|
||||
<InteractiveGlobe />
|
||||
|
||||
<ScrollingTitle tag="p" class="title-world mask">
|
||||
<SplitText text="World" mode="chars" />
|
||||
</ScrollingTitle>
|
||||
|
||||
<Locations {locations} />
|
||||
</section>
|
||||
|
||||
<div class="grid-modules">
|
||||
<div class="container grid">
|
||||
<div class="wrap">
|
||||
<ShopModule />
|
||||
<NewsletterModule />
|
||||
<div class="grid-modules">
|
||||
<div class="container grid">
|
||||
<div class="wrap">
|
||||
<ShopModule />
|
||||
<NewsletterModule />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</PageTransition>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
93
src/routes/sitemap.xml/+server.ts
Normal file
93
src/routes/sitemap.xml/+server.ts
Normal 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>`
|
||||
}
|
||||
@@ -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
114
src/service-workers.ts
Normal 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)
|
||||
})()
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -32,6 +32,13 @@ body {
|
||||
cursor: default;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: none;
|
||||
|
||||
&.block-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
&.is-loading * {
|
||||
cursor: wait !important;
|
||||
}
|
||||
}
|
||||
*, *:before, *:after {
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateZ(0);
|
||||
width: var(--width);
|
||||
pointer-events: none;
|
||||
|
||||
@media (hover: hover) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
// Responsive square padding
|
||||
&:after {
|
||||
@@ -62,6 +67,7 @@
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding-top: 0.25em;
|
||||
overflow: hidden;
|
||||
transform: translateY(-50%) translateZ(0);
|
||||
pointer-events: none;
|
||||
@@ -74,16 +80,22 @@
|
||||
font-family: $font-serif;
|
||||
font-weight: 100;
|
||||
letter-spacing: -0.035em;
|
||||
line-height: 0.75;
|
||||
color: $color-secondary;
|
||||
font-size: clamp(#{rem(88px)}, 20vw, #{rem(320px)});
|
||||
}
|
||||
.country {
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
margin-top: 2em;
|
||||
font-size: rem(14px);
|
||||
color: $color-tertiary;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 500;
|
||||
|
||||
@include bp (md) {
|
||||
margin-top: 4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,27 +124,38 @@
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: -8px;
|
||||
left: -8px;
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
color: $color-secondary;
|
||||
pointer-events: auto;
|
||||
|
||||
@include bp (md) {
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
// Dot
|
||||
i {
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 32px;
|
||||
background: $color-secondary;
|
||||
transition: box-shadow 0.4s var(--ease-quart), transform 0.4s var(--ease-quart);
|
||||
transform-origin: 50% 50%;
|
||||
|
||||
@include bp (md) {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
// Name
|
||||
span {
|
||||
@@ -142,7 +165,11 @@
|
||||
// Hover: Grow marker outline
|
||||
&:hover {
|
||||
i {
|
||||
box-shadow: 0 0 0 10px rgba($color-tertiary, 0.25);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,16 +88,21 @@
|
||||
// Arrow
|
||||
&__arrow {
|
||||
$color-shadow: rgba(#000, 0.075);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
transform: translate3d(var(--x), var(--y), 0);
|
||||
transition: transform 0.6s var(--ease-quart), opacity 0.6s var(--ease-quart);
|
||||
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);
|
||||
display: none;
|
||||
|
||||
// Enable only on devices with hover
|
||||
@media (hover: hover) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
transform: translate3d(var(--x), var(--y), 0);
|
||||
transition: transform 0.6s var(--ease-quart), opacity 0.6s var(--ease-quart);
|
||||
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);
|
||||
}
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
&--checkout {
|
||||
@include bp (md) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -132,7 +133,8 @@
|
||||
color: $color-gray;
|
||||
|
||||
@include bp (sm) {
|
||||
font-size: rem(12px);
|
||||
margin-bottom: 0;
|
||||
font-size: rem(14px);
|
||||
line-height: 1.6;
|
||||
}
|
||||
@include bp (md) {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
grid-column: 4 / span var(--columns);
|
||||
}
|
||||
|
||||
h3 {
|
||||
h2 {
|
||||
color: $color-secondary;
|
||||
margin-bottom: 8px;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transform: scale3d(1.075, 1.075, 1.075);
|
||||
transform: scale(1.075);
|
||||
transition: opacity 0.8s, transform 1.6s var(--ease-quart);
|
||||
}
|
||||
:global(img) {
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
:global(.is-visible) {
|
||||
opacity: 1;
|
||||
transform: scale3d(1,1,1);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
:global(img) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
:global(.about) {
|
||||
.about {
|
||||
overflow: hidden;
|
||||
|
||||
:global(.picture) {
|
||||
overflow: hidden;
|
||||
background: $color-primary-tertiary20;
|
||||
@@ -9,9 +11,7 @@
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about {
|
||||
/*
|
||||
** Introduction
|
||||
*/
|
||||
|
||||
@@ -23,9 +23,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.page-error) {
|
||||
// Globe
|
||||
:global(.globe) {
|
||||
margin-top: 96px;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Explore Page
|
||||
:global(.explore) {
|
||||
.explore {
|
||||
overflow: hidden;
|
||||
}
|
||||
:global(.explore__locations) {
|
||||
@include bp (sm, max) {
|
||||
margin-top: 72px;
|
||||
|
||||
&__locations {
|
||||
@include bp (sm, max) {
|
||||
margin-top: 72px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
:global(.homepage) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Homepage
|
||||
.homepage {
|
||||
overflow: hidden;
|
||||
|
||||
// Intro Section
|
||||
&__intro {
|
||||
padding-bottom: calc(96px + 20vw);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
:global(.location-page) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
// Location Page
|
||||
.location-page {
|
||||
background: #fff;
|
||||
|
||||
// Intro
|
||||
&__intro {
|
||||
position: relative;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// Title
|
||||
:global(h1) {
|
||||
margin: -20px 0 48px;
|
||||
overflow: hidden;
|
||||
color: $color-secondary;
|
||||
line-height: 1;
|
||||
|
||||
@@ -175,14 +176,6 @@
|
||||
}
|
||||
}
|
||||
// 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 + 4),
|
||||
&:nth-child(10n + 5){
|
||||
@@ -302,7 +295,7 @@
|
||||
margin-right: 12px;
|
||||
color: #fff;
|
||||
border-radius: 100%;
|
||||
transition: color 0.3s;
|
||||
transition: color 0.3s;;
|
||||
|
||||
@include bp (sm) {
|
||||
width: 26px;
|
||||
@@ -313,6 +306,7 @@
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,14 +367,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
|
||||
@include bp (sm) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 20px;
|
||||
transform: translateY(-50%);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
// Reset link
|
||||
|
||||
@@ -1,57 +1,32 @@
|
||||
:global(.shop-page) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Nav
|
||||
:global(.shop-location) {
|
||||
--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;
|
||||
background: $color-cream;
|
||||
color: $color-text;
|
||||
text-align: center;
|
||||
|
||||
@include bp (sm) {
|
||||
padding: clamp(64px, 12vw, 160px) 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
:global(.inner) {
|
||||
grid-column: 1 / span 8;
|
||||
// Error
|
||||
:global(.shop-page__error) {
|
||||
padding: 64px 0;
|
||||
background: $color-cream;
|
||||
color: $color-text;
|
||||
text-align: center;
|
||||
|
||||
@include bp (sm) {
|
||||
grid-column: 3 / span 12;
|
||||
padding: clamp(64px, 12vw, 160px) 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
:global(h2) {
|
||||
margin-bottom: 8px;
|
||||
color: $color-secondary;
|
||||
|
||||
@include bp (sm) {
|
||||
margin-bottom: 16px;
|
||||
:global(.inner) {
|
||||
grid-column: 1 / span 8;
|
||||
|
||||
@include bp (sm) {
|
||||
grid-column: 3 / span 12;
|
||||
}
|
||||
}
|
||||
:global(h2) {
|
||||
margin-bottom: 8px;
|
||||
color: $color-secondary;
|
||||
|
||||
@include bp (sm) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,13 @@
|
||||
|
||||
// Past Issues
|
||||
&__issues {
|
||||
margin: 64px auto 0;
|
||||
margin: 64px auto 96px;
|
||||
padding: 0 20px;
|
||||
|
||||
@include bp (sm) {
|
||||
max-width: 800px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 156px;
|
||||
}
|
||||
|
||||
// Title
|
||||
@@ -63,12 +64,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Globe
|
||||
:global(.subscribe .globe) {
|
||||
margin-top: 96px;
|
||||
|
||||
@include bp (sm) {
|
||||
margin-top: 156px;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
:global(.photo-page) {
|
||||
.photo-page {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: var(--vh);
|
||||
@@ -16,420 +16,418 @@
|
||||
}
|
||||
}
|
||||
|
||||
.photo-page {
|
||||
// Carousel
|
||||
&__carousel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, 0, 0);
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
grid-row-gap: 20px;
|
||||
width: calc(100% - 40px);
|
||||
height: 100%;
|
||||
max-width: 720px;
|
||||
// Carousel
|
||||
&__carousel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, 0, 0);
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
grid-row-gap: 20px;
|
||||
width: calc(100% - 40px);
|
||||
height: 100%;
|
||||
max-width: 720px;
|
||||
position: relative;
|
||||
|
||||
@include bp (md) {
|
||||
position: relative;
|
||||
|
||||
@include bp (md) {
|
||||
position: relative;
|
||||
max-width: none;
|
||||
margin: auto 0;
|
||||
grid-column: 2 / span 17;
|
||||
grid-row-gap: 40px;
|
||||
transform: translate3d(-50%, 2.5%, 0);
|
||||
}
|
||||
@include bp (sd) {
|
||||
grid-column: 3 / span 16;
|
||||
}
|
||||
max-width: none;
|
||||
margin: auto 0;
|
||||
grid-column: 2 / span 17;
|
||||
grid-row-gap: 40px;
|
||||
transform: translate3d(-50%, 2.5%, 0);
|
||||
}
|
||||
|
||||
// Images
|
||||
&__images {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: auto auto 0;
|
||||
padding-top: 66.66%;
|
||||
touch-action: none;
|
||||
@include bp (sd) {
|
||||
grid-column: 3 / span 16;
|
||||
}
|
||||
}
|
||||
|
||||
&__picture {
|
||||
--opacity: 1;
|
||||
// Images
|
||||
&__images {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: auto auto 0;
|
||||
padding-top: 66.66%;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
&__picture {
|
||||
--opacity: 1;
|
||||
--scale: 0.6;
|
||||
--rotate: 0deg;
|
||||
--offset-x: 0%;
|
||||
--offset-y: 0%;
|
||||
position: absolute;
|
||||
z-index: 8;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translateZ(0);
|
||||
will-change: transform, opacity;
|
||||
|
||||
@include bp (md) {
|
||||
--scale: 0.6;
|
||||
--rotate: 0deg;
|
||||
--offset-x: 0%;
|
||||
--rotate: 5deg;
|
||||
--offset-x: 28.5%;
|
||||
--offset-y: 0%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
:global(.photo) {
|
||||
position: absolute;
|
||||
z-index: 8;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translateZ(0);
|
||||
will-change: transform, opacity;
|
||||
transform: translate3d(var(--offset-x), var(--offset-y), 0) scale(var(--scale)) rotate(var(--rotate));
|
||||
transition: opacity 1s var(--ease-quart), transform 1s var(--ease-quart);
|
||||
will-change: transform;
|
||||
box-shadow:
|
||||
0 12px 12px rgba(#000, 0.15),
|
||||
0 20px 20px rgba(#000, 0.15),
|
||||
0 48px 48px rgba(#000, 0.15);
|
||||
|
||||
@include bp (md) {
|
||||
--scale: 0.6;
|
||||
--rotate: 5deg;
|
||||
--offset-x: 28.5%;
|
||||
--offset-y: 0%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
:global(.photo) {
|
||||
:global(picture) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate3d(var(--offset-x), var(--offset-y), 0) scale(var(--scale)) rotate(var(--rotate));
|
||||
transition: opacity 1s var(--ease-quart), transform 1s var(--ease-quart);
|
||||
will-change: transform;
|
||||
box-shadow:
|
||||
0 12px 12px rgba(#000, 0.15),
|
||||
0 20px 20px rgba(#000, 0.15),
|
||||
0 48px 48px rgba(#000, 0.15);
|
||||
|
||||
:global(picture) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: $color-primary;
|
||||
cursor: default;
|
||||
}
|
||||
:global(img) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: var(--opacity);
|
||||
transform: translateZ(0);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
transition: opacity 1s var(--ease-quart);
|
||||
}
|
||||
overflow: hidden;
|
||||
background: $color-primary;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
// Ratio is not landscape
|
||||
:global(.not-landscape) {
|
||||
:global(img) {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
// Hidden photo over
|
||||
&.is-0 {
|
||||
--scale: 1.03;
|
||||
--rotate: 0deg;
|
||||
--offset-x: 0%;
|
||||
--offset-y: -7%;
|
||||
z-index: 9;
|
||||
:global(img) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: var(--opacity);
|
||||
transform: translateZ(0);
|
||||
pointer-events: none;
|
||||
|
||||
:global(.photo) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@include bp (md) {
|
||||
--scale: 1.075;
|
||||
--rotate: -1deg;
|
||||
--offset-x: -9%;
|
||||
--offset-y: 0%;
|
||||
}
|
||||
}
|
||||
// First visible photo
|
||||
&.is-1 {
|
||||
--scale: 1;
|
||||
--rotate: 0deg;
|
||||
--offset-y: 0%;
|
||||
|
||||
@include bp (md) {
|
||||
--offset-x: 0%;
|
||||
--offset-y: 0%;
|
||||
}
|
||||
}
|
||||
&.is-2 {
|
||||
--scale: 0.9;
|
||||
--opacity: 0.75;
|
||||
--offset-y: 12%;
|
||||
z-index: 7;
|
||||
|
||||
@include bp (md) {
|
||||
--scale: 0.9;
|
||||
--rotate: 1deg;
|
||||
--offset-x: 9.5%;
|
||||
--offset-y: 0%;
|
||||
}
|
||||
}
|
||||
&.is-3 {
|
||||
--scale: 0.83;
|
||||
--opacity: 0.55;
|
||||
--offset-y: 20%;
|
||||
z-index: 6;
|
||||
|
||||
@include bp (md) {
|
||||
--scale: 0.83;
|
||||
--rotate: 2deg;
|
||||
--offset-x: 16.25%;
|
||||
--offset-y: 0%;
|
||||
}
|
||||
}
|
||||
&.is-4 {
|
||||
--scale: 0.75;
|
||||
--opacity: 0.45;
|
||||
--offset-y: 27.5%;
|
||||
z-index: 5;
|
||||
|
||||
@include bp (md) {
|
||||
--scale: 0.75;
|
||||
--rotate: 3deg;
|
||||
--offset-x: 22%;
|
||||
--offset-y: 0%;
|
||||
}
|
||||
}
|
||||
&.is-5 {
|
||||
--scale: 0.68;
|
||||
--opacity: 0.25;
|
||||
--offset-y: 33%;
|
||||
z-index: 4;
|
||||
|
||||
@include bp (md) {
|
||||
--scale: 0.68;
|
||||
--rotate: 4deg;
|
||||
--offset-x: 27%;
|
||||
--offset-y: 0%;
|
||||
}
|
||||
}
|
||||
&.is-6 {
|
||||
--opacity: 0.25;
|
||||
z-index: 3;
|
||||
|
||||
:global(.photo) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
&.is-7 {
|
||||
:global(.photo) {
|
||||
opacity: 0;
|
||||
}
|
||||
user-select: none;
|
||||
transition: opacity 1s var(--ease-quart);
|
||||
}
|
||||
}
|
||||
|
||||
// Infos
|
||||
&__info {
|
||||
bottom: 0;
|
||||
margin-top: auto;
|
||||
margin-bottom: 40px;
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
|
||||
@include bp (md) {
|
||||
position: static;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
@include bp (lg) {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: $color-secondary;
|
||||
font-size: clamp(#{rem(18px)}, 5.5vw, #{rem(28px)});
|
||||
line-height: 1.1;
|
||||
|
||||
@include bp (md) {
|
||||
font-size: rem(32px);
|
||||
}
|
||||
}
|
||||
|
||||
// Details
|
||||
.detail {
|
||||
display: inline-block;
|
||||
align-items: center;
|
||||
color: rgba($color-tertiary, 0.7);
|
||||
line-height: 1.5;
|
||||
|
||||
@include bp (lg) {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: $color-tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
// Icon
|
||||
:global(.icon) {
|
||||
display: inline-block;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
margin-top: -5px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
// Separator
|
||||
.sep {
|
||||
display: inline-block;
|
||||
margin: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
// Ratio is not landscape
|
||||
:global(.not-landscape) {
|
||||
:global(img) {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
// Index
|
||||
&__index {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 50%;
|
||||
bottom: calc(91% + 1vw);
|
||||
display: block;
|
||||
line-height: 1;
|
||||
color: rgba($color-tertiary, 0.4);
|
||||
transform: translate3d(-50%, 0, 0);
|
||||
white-space: nowrap;
|
||||
// Hidden photo over
|
||||
&.is-0 {
|
||||
--scale: 1.03;
|
||||
--rotate: 0deg;
|
||||
--offset-x: 0%;
|
||||
--offset-y: -7%;
|
||||
z-index: 9;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
|
||||
@include bp (md, max) {
|
||||
font-size: clamp(#{rem(80px)}, 24vw, #{rem(120px)});
|
||||
:global(.photo) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@include bp (md) {
|
||||
top: 50%;
|
||||
left: auto;
|
||||
right: calc(-1 * min(30vw, 400px));
|
||||
width: 350px;
|
||||
text-align: center;
|
||||
bottom: auto;
|
||||
transform: translate3d(0, -50%, 0);
|
||||
--scale: 1.075;
|
||||
--rotate: -1deg;
|
||||
--offset-x: -9%;
|
||||
--offset-y: 0%;
|
||||
}
|
||||
@include bp (lg) {
|
||||
right: calc(-1 * min(25vw, 460px));
|
||||
}
|
||||
// First visible photo
|
||||
&.is-1 {
|
||||
--scale: 1;
|
||||
--rotate: 0deg;
|
||||
--offset-y: 0%;
|
||||
|
||||
@include bp (md) {
|
||||
--offset-x: 0%;
|
||||
--offset-y: 0%;
|
||||
}
|
||||
}
|
||||
&.is-2 {
|
||||
--scale: 0.9;
|
||||
--opacity: 0.75;
|
||||
--offset-y: 12%;
|
||||
z-index: 7;
|
||||
|
||||
@include bp (md) {
|
||||
--scale: 0.9;
|
||||
--rotate: 1deg;
|
||||
--offset-x: 9.5%;
|
||||
--offset-y: 0%;
|
||||
}
|
||||
}
|
||||
&.is-3 {
|
||||
--scale: 0.83;
|
||||
--opacity: 0.55;
|
||||
--offset-y: 20%;
|
||||
z-index: 6;
|
||||
|
||||
@include bp (md) {
|
||||
--scale: 0.83;
|
||||
--rotate: 2deg;
|
||||
--offset-x: 16.25%;
|
||||
--offset-y: 0%;
|
||||
}
|
||||
}
|
||||
&.is-4 {
|
||||
--scale: 0.75;
|
||||
--opacity: 0.45;
|
||||
--offset-y: 27.5%;
|
||||
z-index: 5;
|
||||
|
||||
@include bp (md) {
|
||||
--scale: 0.75;
|
||||
--rotate: 3deg;
|
||||
--offset-x: 22%;
|
||||
--offset-y: 0%;
|
||||
}
|
||||
}
|
||||
&.is-5 {
|
||||
--scale: 0.68;
|
||||
--opacity: 0.25;
|
||||
--offset-y: 33%;
|
||||
z-index: 4;
|
||||
|
||||
@include bp (md) {
|
||||
--scale: 0.68;
|
||||
--rotate: 4deg;
|
||||
--offset-x: 27%;
|
||||
--offset-y: 0%;
|
||||
}
|
||||
}
|
||||
&.is-6 {
|
||||
--opacity: 0.25;
|
||||
z-index: 3;
|
||||
|
||||
:global(.photo) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
&.is-7 {
|
||||
:global(.photo) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Infos
|
||||
&__info {
|
||||
bottom: 0;
|
||||
margin-top: auto;
|
||||
margin-bottom: 40px;
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
|
||||
@include bp (md) {
|
||||
position: static;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
@include bp (lg) {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: $color-secondary;
|
||||
font-size: clamp(#{rem(18px)}, 5.5vw, #{rem(28px)});
|
||||
line-height: 1.1;
|
||||
|
||||
@include bp (md) {
|
||||
font-size: rem(32px);
|
||||
}
|
||||
}
|
||||
|
||||
// Controls
|
||||
&__controls {
|
||||
display: none;
|
||||
// Details
|
||||
.detail {
|
||||
display: inline-block;
|
||||
align-items: center;
|
||||
color: rgba($color-tertiary, 0.7);
|
||||
line-height: 1.5;
|
||||
|
||||
@include bp (md) {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
left: -28px;
|
||||
right: -28px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
@include bp (lg) {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
:global(button) {
|
||||
pointer-events: auto;
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
|
||||
// Prev button
|
||||
&:first-child {
|
||||
& > :global(*:nth-child(2)) {
|
||||
transform: translate3d(100%, -50%, 0) rotate(180deg);
|
||||
}
|
||||
&:hover {
|
||||
color: $color-tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
// Hover
|
||||
&:not([disabled]):hover {
|
||||
& > :global(*:nth-child(1)) {
|
||||
transform: translate3d(-20%, 0, 0) rotate(180deg);
|
||||
}
|
||||
& > :global(*:nth-child(2)) {
|
||||
transform: translate3d(-50%, -50%, 0) rotate(180deg);
|
||||
}
|
||||
}
|
||||
// Icon
|
||||
:global(.icon) {
|
||||
display: inline-block;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
margin-top: -5px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
// Separator
|
||||
.sep {
|
||||
display: inline-block;
|
||||
margin: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Index
|
||||
&__index {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 50%;
|
||||
bottom: calc(91% + 1vw);
|
||||
display: block;
|
||||
line-height: 1;
|
||||
color: rgba($color-tertiary, 0.4);
|
||||
transform: translate3d(-50%, 0, 0);
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
|
||||
@include bp (md, max) {
|
||||
font-size: clamp(#{rem(80px)}, 24vw, #{rem(120px)});
|
||||
}
|
||||
@include bp (md) {
|
||||
top: 50%;
|
||||
left: auto;
|
||||
right: calc(-1 * min(30vw, 400px));
|
||||
width: 350px;
|
||||
text-align: center;
|
||||
bottom: auto;
|
||||
transform: translate3d(0, -50%, 0);
|
||||
}
|
||||
@include bp (lg) {
|
||||
right: calc(-1 * min(25vw, 460px));
|
||||
}
|
||||
}
|
||||
|
||||
// Controls
|
||||
&__controls {
|
||||
display: none;
|
||||
|
||||
@include bp (md) {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
left: -28px;
|
||||
right: -28px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:global(button) {
|
||||
pointer-events: auto;
|
||||
|
||||
// Prev button
|
||||
&:first-child {
|
||||
& > :global(*:nth-child(2)) {
|
||||
transform: translate3d(100%, -50%, 0) rotate(180deg);
|
||||
}
|
||||
|
||||
// Hover
|
||||
&:not([disabled]):hover {
|
||||
background-color: $color-secondary;
|
||||
color: #fff;
|
||||
|
||||
:global(svg:nth-child(2)) {
|
||||
color: #fff;
|
||||
& > :global(*:nth-child(1)) {
|
||||
transform: translate3d(-20%, 0, 0) rotate(180deg);
|
||||
}
|
||||
& > :global(*:nth-child(2)) {
|
||||
transform: translate3d(-50%, -50%, 0) rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fullscreen viewer
|
||||
&__fullscreen {
|
||||
position: absolute;
|
||||
z-index: 102;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $color-primary-darker;
|
||||
// Hover
|
||||
&:not([disabled]):hover {
|
||||
background-color: $color-secondary;
|
||||
color: #fff;
|
||||
|
||||
.inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// Photo
|
||||
:global(picture) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
cursor: pointer;
|
||||
|
||||
:global(img) {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
:global(svg:nth-child(2)) {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close
|
||||
:global(.close) {
|
||||
$color-shadow: rgba(#000, 0.15);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
box-shadow:
|
||||
0 6px 6px $color-shadow,
|
||||
0 12px 12px $color-shadow,
|
||||
0 24px 24px $color-shadow;
|
||||
// Fullscreen viewer
|
||||
&__fullscreen {
|
||||
position: absolute;
|
||||
z-index: 102;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $color-primary-darker;
|
||||
|
||||
.inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// Photo
|
||||
:global(picture) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
cursor: pointer;
|
||||
|
||||
:global(img) {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Notice
|
||||
&__notice {
|
||||
// Close
|
||||
:global(.close) {
|
||||
$color-shadow: rgba(#000, 0.15);
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 20px;
|
||||
line-height: 44px;
|
||||
color: rgba($color-tertiary, 0.5);
|
||||
z-index: 2;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
box-shadow:
|
||||
0 6px 6px $color-shadow,
|
||||
0 12px 12px $color-shadow,
|
||||
0 24px 24px $color-shadow;
|
||||
}
|
||||
}
|
||||
|
||||
@include bp (md) {
|
||||
display: none;
|
||||
}
|
||||
// Notice
|
||||
&__notice {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 20px;
|
||||
line-height: 44px;
|
||||
color: rgba($color-tertiary, 0.5);
|
||||
|
||||
@include bp (md) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
** Shop: Intro
|
||||
** Shop: Intro banner
|
||||
*/
|
||||
.shop-page__intro {
|
||||
.shop-banner {
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
height: 30vw;
|
||||
@@ -70,8 +70,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Site Title
|
||||
.shop-page__title {
|
||||
// Page title
|
||||
.title {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 42%;
|
||||
@@ -98,6 +98,83 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
.nav {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
||||
.container {
|
||||
padding: 0 0 24px;
|
||||
|
||||
@include bp (md) {
|
||||
display: grid;
|
||||
grid-template-columns: 15% auto 15%;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
padding: 0 32px 32px;
|
||||
}
|
||||
}
|
||||
|
||||
// Shop
|
||||
p {
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
@include bp (md) {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 50px;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
|
||||
@include bp (sm) {
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
font-size: rem(22px);
|
||||
font-family: $font-serif;
|
||||
color: $color-tertiary;
|
||||
margin: 0 10px;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: $color-secondary;
|
||||
}
|
||||
|
||||
@include bp (sm) {
|
||||
font-size: rem(24px);
|
||||
margin: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// Active
|
||||
.is-active {
|
||||
a {
|
||||
color: $color-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Background Image
|
||||
:global(picture) {
|
||||
position: relative;
|
||||
@@ -146,79 +223,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Intro: Navigation
|
||||
.shop-page__nav {
|
||||
position: absolute;
|
||||
|
||||
// Quick nav
|
||||
.shop-quicknav {
|
||||
--inset: 20px;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
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;
|
||||
|
||||
.container {
|
||||
padding: 0 0 24px;
|
||||
|
||||
@include bp (md) {
|
||||
display: grid;
|
||||
grid-template-columns: 15% auto 15%;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
padding: 0 32px 32px;
|
||||
}
|
||||
@include bp (sm) {
|
||||
--inset: 32px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// Shop
|
||||
p {
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
@include bp (md) {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 50px;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
|
||||
@include bp (sm) {
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
font-size: rem(22px);
|
||||
font-family: $font-serif;
|
||||
color: $color-tertiary;
|
||||
margin: 0 10px;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: $color-secondary;
|
||||
}
|
||||
|
||||
@include bp (sm) {
|
||||
font-size: rem(24px);
|
||||
margin: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// Active
|
||||
.is-active {
|
||||
a {
|
||||
color: $color-secondary;
|
||||
}
|
||||
}
|
||||
// Visible state
|
||||
&.is-visible {
|
||||
transform: translate3d(0,0,0);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,8 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// Send page
|
||||
export const sendPage = (path: string = '') => {
|
||||
if (typeof Countly !== 'undefined') {
|
||||
Countly.track_pageview(path)
|
||||
}
|
||||
}
|
||||
|
||||
// Send event
|
||||
export const sendEvent = ({ action, segments = {}, amount = 1 }) => {
|
||||
if (typeof Countly !== 'undefined') {
|
||||
Countly.add_event({
|
||||
key: action,
|
||||
count: amount,
|
||||
segmentation: {
|
||||
...segments
|
||||
}
|
||||
})
|
||||
export const sendEvent = (action: string, props?: any) => {
|
||||
if (typeof plausible !== 'undefined') {
|
||||
plausible(action, props)
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,4 @@ export const DELAY = {
|
||||
export const DURATION = {
|
||||
PAGE_IN: 400,
|
||||
PAGE_OUT: 400,
|
||||
PAGE_DELAY: 600,
|
||||
}
|
||||
49
src/utils/forms/subscribe.ts
Normal file
49
src/utils/forms/subscribe.ts
Normal 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],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import { sendEvent } from '$utils/analytics'
|
||||
|
||||
|
||||
/**
|
||||
* Throttle function
|
||||
*/
|
||||
@@ -128,7 +131,7 @@ export const scrollToTop = (delay?: number) => {
|
||||
if (delay && delay > 0) {
|
||||
setTimeout(scroll, delay)
|
||||
} else {
|
||||
scroll()
|
||||
return scroll()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +153,9 @@ export const mailtoClipboard = (node: HTMLElement) => {
|
||||
detail: { email: emailAddress }
|
||||
}))
|
||||
|
||||
// Record event in analytics
|
||||
sendEvent('emailCopy')
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import swell from 'swell-js'
|
||||
import { addNotification } from '$utils/functions/notifications'
|
||||
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'
|
||||
|
||||
|
||||
@@ -40,6 +41,13 @@ export const addToCart = async (product: any, quantity: number = 1) => {
|
||||
name: `${product.name} - x1`,
|
||||
image: product.images[0].file.url,
|
||||
})
|
||||
|
||||
// Send event
|
||||
sendEvent('addToCart', {
|
||||
props: {
|
||||
product: product.name,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ Twitter: https://twitter.com/Flayks
|
||||
Location: Toulouse, France (Project originally started in April 2019 in Brisbane, Australia)
|
||||
|
||||
/* 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
|
||||
|
||||
/* SITE */
|
||||
Original launch: April 22th, 2020
|
||||
Version 2 launch: September 2022
|
||||
Version 2 launch: September 27th, 2022
|
||||
Standards: HTML5, CSS3, Javascript
|
||||
Front-End: SvelteKit, Motion One, normalize.css
|
||||
Back-End: Vercel, Directus, Docker
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"short_name": "Houses Of",
|
||||
"name": "Houses Of - Beautiful houses around the world",
|
||||
"description": "",
|
||||
"name": "Houses Of the World",
|
||||
"description": "Houses Of is a project showcasing charismatic houses around the world.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/siteicon-192.png",
|
||||
|
||||
4
static/robots.txt
Normal file
4
static/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Sitemap: https://housesof.world/sitemap.xml
|
||||
@@ -27,6 +27,7 @@ const config = {
|
||||
$utils: 'src/utils',
|
||||
$style: 'src/style',
|
||||
},
|
||||
csrf: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"lib": ["WebWorker"],
|
||||
},
|
||||
"exclude": ["./src/modules/globe/**/*"],
|
||||
}
|
||||
Reference in New Issue
Block a user