Files
housesof/src/routes/[country]/[location]/index.svelte
Félix Péault fd6fc20b13 Finish to replace Anime with Motion One for page animations
Page intro animation and reveal that has now been simplified as Motion One manages an inView option (that uses IntersectionObserver)
2022-08-14 20:24:28 +02:00

355 lines
12 KiB
Svelte

<style lang="scss">
@import "../../../style/pages/location";
</style>
<script lang="ts">
import { page, navigating } from '$app/stores'
import { onMount } from 'svelte'
import { timeline } from 'motion'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime.js'
import { quartOut } from '$animations/easings'
import { fetchAPI, getAssetUrlKey } from '$utils/api'
import { DELAY, DURATION } from '$utils/contants'
import { photoFields } from '.'
// 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 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 location: any
export let photos: any[]
export let totalPhotos: number
export let product: any = undefined
dayjs.extend(relativeTime)
const { params } = $page
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(import.meta.env.VITE_LIST_INCREMENT)) {
// Increment the current page
currentPage++
}
// Increment the currently visible amount of photos
currentPhotosAmount += newPhotos.length
}
}
// [function] Load photos helper
const loadPhotos = async (page?: number) => {
const res = fetchAPI(`
query {
photos: photo (
filter: {
location: { slug: { _eq: "${params.location}" }},
status: { _eq: "published" },
},
sort: "-date_created",
limit: ${import.meta.env.VITE_LIST_INCREMENT},
page: ${page},
) {
${photoFields}
}
}
`)
const { data: { photos }} = await res
if (photos) {
// Return new photos
return photos
} else {
throw new Error('Error while loading new photos')
}
}
/**
* Add parallax on illustration when scrolling
*/
$: if (scrollY && scrollY < introEl.offsetHeight) {
illustrationOffsetY = scrollY * 0.1
}
onMount(() => {
// 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,
rootMargin: '0px 0px 0px'
})
// 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,
}],
// 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>
<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>
<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 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 /> -->
</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}>
{#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}
</PageTransition>