Files
housesof/src/routes/[country]/[location]/index.svelte

352 lines
12 KiB
Svelte

<script lang="ts">
import { browser } from '$app/env'
import { navigating, page } from '$app/stores'
import { onMount } from 'svelte'
import anime from 'animejs'
import type { AnimeTimelineInstance } from 'animejs'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime.js'
import { getAssetUrlKey } from '$utils/helpers'
import { fetchAPI } from '$utils/api'
import { DURATION } from '$utils/contants'
// Components
import Metas from '$components/Metas.svelte'
import PageTransition from '$components/PageTransition.svelte'
import Icon from '$components/atoms/Icon.svelte'
import Button from '$components/atoms/Button.svelte'
import IconEarth from '$components/atoms/IconEarth.svelte'
import Image from '$components/atoms/Image.svelte'
import Newsletter from '$components/organisms/Newsletter.svelte'
export let location: any
export let photos: any[]
export let totalPhotos: number
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},
) {
title
slug
city
image {
id
title
}
date_taken
date_created
}
}
`)
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
}
/**
* Transition: Anime timeline
*/
let timeline: AnimeTimelineInstance
if (browser) {
requestAnimationFrame(() => {
timeline = anime.timeline({
duration: 1600,
easing: 'easeOutQuart',
autoplay: false,
})
// Title word
timeline.add({
targets: '.location-page__intro .word',
translateY: ['110%', 0],
delay: anime.stagger(200)
}, 200 + ($navigating ? DURATION.PAGE_IN : 0))
// Illustration
timeline.add({
targets: '.location-page__illustration',
scale: [1.06, 1],
opacity: [0, 1],
duration: 2400,
}, 400 + ($navigating ? DURATION.PAGE_IN : 0))
// Title of
timeline.add({
targets: '.location-page__intro .of',
opacity: [0, 1],
duration: 1200,
}, 1050 + ($navigating ? DURATION.PAGE_IN : 0))
// Description
timeline.add({
targets: '.location-page__description',
translateY: ['10%', 0],
opacity: [0, 1],
}, 900 + ($navigating ? DURATION.PAGE_IN : 0))
})
}
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
mutationPhotos = new MutationObserver((mutationsList, observer) => {
// When adding new childs
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
// Observe new items
mutation.addedNodes.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))
// Transition in
requestAnimationFrame(() => {
timeline.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={getAssetUrlKey(latestPhoto.image.id, 'share-image')}
/>
<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}
</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}"
loading="lazy"
/>
</picture>
{/if}
</section>
{#if photos.length}
<section class="location-page__houses grid" bind:this={photosListEl}>
{#each photos as { title, image: { id, title: alt }, slug, city, date_taken }, index}
<div class="house grid">
<div class="house__info">
<h2 class="title-image">
{title}
</h2>
<p class="info text-info">
{#if city}<Icon class="icon" icon="map-pin" label="Map pin" /> {city} <span class="sep">·</span>{/if}
<time datetime={dayjs(date_taken).format('YYYY-MM-DD')}>
{dayjs(date_taken).format('MMMM YYYY')}
</time>
</p>
</div>
<figure class="house__photo">
<a href="/{params.country}/{params.location}/{slug}" sveltekit:noscroll tabindex="0">
<Image
id={id}
sizeKey="photo-list"
sizes={{
small: { width: 500 },
medium: { width: 850 },
large: { width: 1280 },
}}
ratio={1.5}
alt="{alt}"
class="photo shadow-photo"
/>
<span class="house__index title-index">
{(totalPhotos - index < 10) ? '0' : ''}{totalPhotos - index}
</span>
</a>
</figure>
</div>
{/each}
</section>
<section class="location-page__next">
<div class="container">
<div class="pagination" role="button"
on:click={!ended && loadMorePhotos} disabled={ended ? ended : undefined}
on:keydown={({ key, target }) => key === 'Enter' && target.click()}
tabindex="0"
>
<div class="pagination__progress">
<span class="current">{currentPhotosAmount}</span>
<span>/</span>
<span class="total">{totalPhotos}</span>
{#if !ended}
<p class="pagination__more">See more photos</p>
{:else}
<p class="pagination__message">You've seen it all!</p>
{/if}
</div>
</div>
{#if ended}
<Newsletter theme="light" />
{/if}
</div>
</section>
{:else}
<div class="location-page__message">
<p>
No photos available for {location.name}.<br>
Come back later!
</p>
</div>
{/if}
</PageTransition>