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

388 lines
13 KiB
Svelte

<script lang="ts">
import { browser } from '$app/env'
import { page } from '$app/stores'
import { tick } from 'svelte'
import { scale } from 'svelte/transition'
import { quartOut } from 'svelte/easing'
import { getAssetUrlKey } from '$utils/helpers'
import { throttle } from '$utils/functions'
import { swipe } from '$utils/interactions/swipe'
import dayjs from 'dayjs'
import advancedFormat from 'dayjs/plugin/advancedFormat.js'
// Components
import Metas from '$components/Metas.svelte'
import Image from '$components/atoms/Image.svelte'
import Icon from '$components/atoms/Icon.svelte'
import IconArrow from '$components/atoms/IconArrow.svelte'
import ButtonCircle from '$components/atoms/ButtonCircle.svelte'
export let photos: any[]
export let location: any
export let currentIndex: number
export let countPhotos: number
export let limit: number
export let offset: number
dayjs.extend(advancedFormat)
enum directions { PREV, NEXT }
let innerWidth: number
let fullscreenEl: HTMLElement
let globalOffset = offset
let isLoading = false
let isFullscreen = false
let hasNext = offset + limit < countPhotos
let hasPrev = offset > 0
// Define if we can navigate depending on loading state, existing photos and index being first or last
$: canGoPrev = !isLoading && (hasNext || currentIndex !== photos.length - 1)
$: canGoNext = !isLoading && (hasPrev || currentIndex !== 0)
// Define current photo
$: currentPhoto = photos[currentIndex]
$: currentPhotoIndex = globalOffset + currentIndex + 1
// Take 7 photos in the global photos array (5 for current, 1 before first and 1 after last)
// Start one index before the current image since the first one will be invisible
$: sliceStart = Math.max(currentIndex - 1, 0)
$: visiblePhotos = photos.slice(sliceStart, sliceStart + 7)
// Load previous photos
$: if (currentIndex === 0 && hasPrev) {
loadPhotos(photos[0].id)
}
// Load next photos
$: if (currentIndex === photos.length - 5 && hasNext) {
loadPhotos(photos[photos.length - 1].id, directions.NEXT)
}
// Change URL to current photo slug
$: if (browser && currentPhoto) {
window.history.replaceState(null, '', $page.path.replace($page.params.photo, currentPhoto.slug))
}
/**
* Photo navigation
*/
// Go to next photo
const goToNext = throttle(() => {
canGoPrev && currentIndex++
}, 200)
// Fo to previous photo
const goToPrevious = throttle(() => {
canGoNext && (currentIndex = Math.max(currentIndex - 1, 0))
}, 200)
// Enable navigation with keyboard
const handleKeydown = ({ key, defaultPrevented }: KeyboardEvent) => {
if (defaultPrevented) return
switch (key) {
case 'ArrowLeft': goToPrevious(); break;
case 'ArrowRight': goToNext(); break;
default: return;
}
}
// Enable swipe gestures
const handleSwipe = ({ detail }: CustomEvent<string>) => {
// Swipe up and down on mobile/small screens
if (innerWidth < 992) {
switch (detail) {
case '-y': goToNext(); break;
case 'y': goToPrevious(); break;
}
}
// Swipe left and right on larger screens
else {
switch (detail) {
case '-x': goToNext(); break;
case 'x': goToPrevious(); break;
}
}
}
/**
* Fullscreen for mobile
*/
const toggleFullscreen = async () => {
if (innerWidth < 992) {
isFullscreen = !isFullscreen
// Scroll at middle of photo
if (isFullscreen) {
// Wait for fullscreen children to be mounted
await tick()
const picture = fullscreenEl.querySelector('picture')
const image = fullscreenEl.querySelector('img')
picture.scrollTo((image.offsetWidth - innerWidth) / 2, 0)
}
}
}
/**
* Load photos
*/
const loadPhotos = async (id: number, direction: directions = directions.PREV) => {
// Block loading new photos if already loading
if (isLoading) return
// Set state to loading
isLoading = true
// Load new prev or next photos
const isPrev = direction === directions.PREV
const res = await fetchAPI(`
query {
photos: photo (
filter: {
location: { slug: { _eq: "${location.slug}" }},
id: { _${isPrev ? 'gt' : 'lt'}: ${id} },
status: { _eq: "published" },
},
sort: "${isPrev ? '' : '-'}id",
limit: ${limit},
) {
id
title
slug
date_taken
image {
id
title
}
city
}
}
`)
const { data: { photos: newPhotos }} = res
// Not loading anymore
isLoading = false
if (newPhotos) {
// Direction: Previous
if (direction === directions.PREV) {
// Append new photos
photos = [
...newPhotos.reverse(), // Reverse array from isPrev sort
...photos
]
// Increment current index by new amount of photos
currentIndex += newPhotos.length
// Decrement global offset by new amount of photos
globalOffset -= newPhotos.length
// No more prev photos available
if (newPhotos.length === 0) {
hasPrev = false
}
}
// Direction: Next
else {
// Append new photos
photos = [
...photos,
...newPhotos
]
// No more next photos available
if (newPhotos.length === 0) {
hasNext = false
}
}
} else {
throw new Error('Error while loading new photos')
}
}
</script>
<svelte:window
bind:innerWidth
on:keydown={handleKeydown}
/>
{#if currentPhoto}
<Metas
title="{currentPhoto.title} - Houses Of {location.name}"
description=""
image={getAssetUrlKey(currentPhoto.image.id, 'share')}
/>
{/if}
<main class="viewer-photo">
<div class="container grid">
<p class="viewer-photo__notice text-label">Tap for fullscreen</p>
<ButtonCircle
tag="a"
url="/{location.country.slug}/{location.slug}"
color="purple"
class="viewer-photo__close shadow-box-dark"
>
<svg width="12" height="12">
<use xlink:href="#cross">
</svg>
</ButtonCircle>
<div class="viewer-photo__carousel">
<div class="viewer-photo__images" use:swipe on:swipe={handleSwipe} on:tap={toggleFullscreen}>
{#each visiblePhotos as photo, index (photo.id)}
<Image
class="photo photo--{currentIndex === 0 ? index + 1 : index}"
id={photo.image.id}
alt={photo.title}
sizeKey="photo-list"
sizes={{
small: { width: 500 },
medium: { width: 850 },
large: { width: 1280 },
}}
ratio={1.5}
/>
{/each}
<div class="viewer-photo__controls">
<ButtonCircle class="shadow-box-dark" disabled={!canGoNext} clone={true} on:click={goToPrevious}>
<IconArrow color="pink" flip={true} />
</ButtonCircle>
<ButtonCircle class="shadow-box-dark" disabled={!canGoPrev} clone={true} on:click={goToNext}>
<IconArrow color="pink" />
</ButtonCircle>
</div>
<span class="viewer-photo__index title-index">
{(currentPhotoIndex < 10) ? '0' : ''}{currentPhotoIndex}
</span>
</div>
<div class="viewer-photo__info">
<h1 class="title-medium">{currentPhoto.title}</h1>
<div class="detail text-info">
<a href="/{location.country.slug}/{location.slug}" sveltekit:prefetch sveltekit:noscroll>
<Icon class="icon" icon="map-pin" label="Map pin" />
<span>
{#if currentPhoto.city}
{currentPhoto.city}, {location.name}, {location.country.name}
{:else}
{location.name}, {location.country.name}
{/if}
</span>
</a>
<span class="sep">&middot;</span> <time datetime={dayjs(currentPhoto.date_taken).format('YYYY-MM-DD')}>{dayjs(currentPhoto.date_taken).format('MMMM, Do YYYY')}</time>
</div>
</div>
</div>
</div>
{#if isFullscreen}
<div class="viewer-photo__fullscreen" bind:this={fullscreenEl} on:click={toggleFullscreen}>
<div class="inner" transition:scale={{ easing: quartOut, start: 1.1, duration: 1000 }}>
<Image
id={currentPhoto.image.id}
sizeKey="photo-grid-large"
width={1266}
height={844}
alt={currentPhoto.title}
/>
<ButtonCircle color="gray-medium" class="close">
<svg width="18" height="18" viewBox="0 0 18 18" fill="#fff" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.751 0c4.274 0 7.752 3.477 7.752 7.751 0 1.846-.65 3.543-1.73 4.875l3.99 3.991a.81.81 0 1 1-1.146 1.146l-3.99-3.991a7.714 7.714 0 0 1-4.876 1.73C3.477 15.503 0 12.027 0 7.753 0 3.476 3.477 0 7.751 0Zm0 1.62a6.138 6.138 0 0 0-6.13 6.131 6.138 6.138 0 0 0 6.13 6.132 6.138 6.138 0 0 0 6.131-6.132c0-3.38-2.75-6.13-6.13-6.13Zm2.38 5.321a.81.81 0 1 1 0 1.62h-4.76a.81.81 0 1 1 0-1.62h4.76Z" />
</svg>
</ButtonCircle>
</div>
</div>
{/if}
</main>
<script context="module" lang="ts">
import { fetchAPI } from '$utils/api'
export async function load ({ page, fetch, session, stuff }) {
// Get the first photo ID
const firstPhoto = await fetchAPI(`
query {
photo: photo (search: "${page.params.photo}") {
id
}
}
`)
const firstPhotoId = firstPhoto?.data?.photo[0]?.id
// TODO: use same request for both queries (photo.id)
const photosBeforeFirst = await fetchAPI(`
query {
count: photo_aggregated (
filter: {
id: { _gt: ${firstPhotoId} },
location: { slug: { _eq: "${page.params.location}" }},
status: { _eq: "published" },
},
sort: "-id",
) {
count {
id
}
}
}
`)
// Define offset from the current count
const offset = Math.max(photosBeforeFirst?.data?.count[0]?.count.id - 5, 0)
const limit = 10
const res = await fetchAPI(`
query {
photos: photo (
filter: {
location: { slug: { _eq: "${page.params.location}" }}
status: { _eq: "published" },
},
sort: "-date_created",
limit: ${limit},
offset: ${offset},
) {
id
title
slug
date_taken
image {
id
title
}
city
}
location (filter: { slug: { _eq: "${page.params.location}" }}) {
name
slug
country {
name
slug
}
}
}
`)
const { data } = res
const location = stuff.locations.find((location: any) => location.slug === page.params.location)
const totalPhotos = stuff.countTotalPhotosByLocation.find((total: any) => total.group.location === Number(location.id)).count.id
// Find photo's index
const currentIndex = data.photos.findIndex((photo: any) => photo.slug === page.params.photo)
return {
props: {
photos: data.photos,
location: data.location[0],
currentIndex,
countPhotos: totalPhotos,
limit,
offset,
}
}
}
</script>