Defines if the photo is under 3/2 by calculating ratio from width and height
314 lines
11 KiB
Svelte
314 lines
11 KiB
Svelte
<script lang="ts">
|
|
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'
|
|
import { photoFields } from '.'
|
|
// Components
|
|
import Metas from '$components/Metas.svelte'
|
|
import PageTransition from '$components/PageTransition.svelte'
|
|
import Button from '$components/atoms/Button.svelte'
|
|
import IconEarth from '$components/atoms/IconEarth.svelte'
|
|
import House from '$components/molecules/House.svelte'
|
|
import NewsletterModule from '$components/organisms/NewsletterModule.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},
|
|
) {
|
|
${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
|
|
mutationPhotos = new MutationObserver((mutationsList, observer) => {
|
|
// 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
|
|
*/
|
|
// Transition in
|
|
const timeline: AnimeTimelineInstance = 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))
|
|
|
|
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 ?? '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>
|
|
</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, width, height }, slug, city, date_taken }, index}
|
|
<House
|
|
{title}
|
|
photoId={id}
|
|
photoAlt={alt}
|
|
url="/{params.country}/{params.location}/{slug}"
|
|
{city}
|
|
ratio={width / height}
|
|
date={date_taken}
|
|
index={(totalPhotos - index < 10) ? '0' : ''}{totalPhotos - index}
|
|
/>
|
|
{/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}
|
|
<NewsletterModule 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> |