Files
housesof/apps/website/src/routes/(site)/[country]/[location]/+page.svelte

364 lines
13 KiB
Svelte

<style lang="scss">
@import "../../../../style/pages/location";
</style>
<script lang="ts">
import { page, navigating } from '$app/stores'
import { onMount } from 'svelte'
import { stagger, timeline } from 'motion'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { quartOut } from '$animations/easings'
import { getAssetUrlKey } from '$utils/api'
import { DELAY } from '$utils/constants'
import { seenLocations } from '$utils/stores'
import { photoFields } from '$utils/api'
import { PUBLIC_LIST_INCREMENT } from '$env/static/public'
// Components
import Metas from '$components/Metas.svelte'
import Image from '$components/atoms/Image.svelte'
import Button from '$components/atoms/Button.svelte'
import IconEarth from '$components/atoms/IconEarth.svelte'
import House from '$components/molecules/House.svelte'
import Pagination from '$components/molecules/Pagination.svelte'
import NewsletterModule from '$components/organisms/NewsletterModule.svelte'
import ShopModule from '$components/organisms/ShopModule.svelte'
export let data
let { photos, totalPhotos }: { photos: any[], totalPhotos: number } = data
$: ({ photos, totalPhotos } = data)
const { location, product = undefined }: { location: any, totalPhotos: number, product: any } = data
const { params } = $page
dayjs.extend(relativeTime)
let introEl: HTMLElement
let photosListEl: HTMLElement
let scrollY: number
let observerPhotos: IntersectionObserver
let mutationPhotos: MutationObserver
let currentPage = 1
let ended: boolean
let currentPhotosAmount: number
let illustrationOffsetY = 0
$: latestPhoto = photos[0]
$: currentPhotosAmount = photos.length
$: ended = currentPhotosAmount === totalPhotos
$: hasIllustration = location.illustration_desktop && location.illustration_desktop_2x && location.illustration_mobile
/**
* Load photos
*/
// Load more photos from CTA
const loadMorePhotos = async () => {
// Append more photos from API
const newPhotos: any = await loadPhotos(currentPage + 1)
if (newPhotos) {
photos = [...photos, ...newPhotos]
// Define actions if the number of new photos is the expected ones
if (newPhotos.length === Number(PUBLIC_LIST_INCREMENT)) {
// Increment the current page
currentPage++
}
// Increment the currently visible amount of photos
currentPhotosAmount += newPhotos.length
}
}
// Load photos helper
const loadPhotos = async (page?: number) => {
const res = await fetch('/api/data', {
method: 'POST',
body: `query {
photos: photo (
filter: {
location: { slug: { _eq: "${params.location}" }},
status: { _eq: "published" },
},
sort: "-date_created",
limit: ${PUBLIC_LIST_INCREMENT},
page: ${page},
) {
${photoFields}
}
}`,
})
const { data: { photos } } = await res.json()
if (photos) {
// Return new photos
return photos
} else {
new Error('Error while loading new photos')
}
}
/**
* Add parallax on illustration when scrolling
*/
$: if (scrollY && scrollY < introEl.offsetHeight) {
illustrationOffsetY = scrollY * 0.1
}
onMount(() => {
// Define location's last seen state
$seenLocations = JSON.stringify({
// Add existing values
...JSON.parse($seenLocations),
// Add location ID with current time
[location.id]: new Date(),
})
// Photos IntersectionObserver
observerPhotos = new IntersectionObserver(entries => {
entries.forEach(({ isIntersecting, target }: IntersectionObserverEntry) => {
target.classList.toggle('is-visible', isIntersecting)
// Run effect once
isIntersecting && observerPhotos.unobserve(target)
})
}, { threshold: 0.3 })
// Photos MutationObserver
if (photos.length) {
mutationPhotos = new MutationObserver((mutationsList) => {
// When adding new childs
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
// Observe new items
Array.from(mutation.addedNodes)
.filter(item => item.nodeType === Node.ELEMENT_NODE)
.forEach((item: HTMLElement) => observerPhotos.observe(item))
}
}
})
mutationPhotos.observe(photosListEl, {
childList: true,
})
// Observe existing elements
const existingPhotos = photosListEl.querySelectorAll('.house')
existingPhotos.forEach(el => observerPhotos.observe(el))
}
/**
* Animations
*/
const animation = timeline([
// Title word
['.location-page__intro .word', {
y: ['110%', 0],
}, {
at: 0.2,
delay: stagger(0.4)
}],
// Illustration
['.location-page__illustration', {
scale: [1.06, 1],
opacity: [0, 1],
}, {
at: 0.4,
duration: 2.4,
}],
// Title of
['.location-page__intro .of', {
opacity: [0, 1],
}, {
at: 0.95,
duration: 1.2,
}],
// Description
['.location-page__description', {
y: ['10%', 0],
opacity: [0, 1],
}, {
at: 0.9,
duration: 1.2,
}]
], {
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
})
animation.stop()
// Run animation
requestAnimationFrame(animation.play)
// Destroy
return () => {
observerPhotos && observerPhotos.disconnect()
mutationPhotos && mutationPhotos.disconnect()
}
})
</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}
/>
<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}
&middot;
<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 size="medium" url="/locations" text="Change location" class="shadow-small">
<IconEarth />
</Button>
{#if location.has_poster}
<Button size="medium" url="/shop/poster-{location.slug}" text="Buy the poster" color="pinklight" 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>
</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>