Stores the last location's page seeing date in localStorage to hide the Location's new label in list, on top of the date limit
365 lines
13 KiB
Svelte
365 lines
13 KiB
Svelte
<style lang="scss">
|
|
@import "../../../style/pages/location";
|
|
</style>
|
|
|
|
<script lang="ts">
|
|
import { page, navigating } from '$app/stores'
|
|
import type { PageData } from './$types'
|
|
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/contants'
|
|
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 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 data: PageData
|
|
|
|
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 {
|
|
throw 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>
|
|
|
|
<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}
|
|
·
|
|
<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} 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}
|
|
</PageTransition> |