🔥 Make the photo viewer completely working

Merci Grafikart 🙏
A whole bunch of headaches and challenges to load prev and next photos depending on the current one, while animating everything nicely.
Added lots of comments to document the logic and fetching.
This commit is contained in:
2021-11-11 23:32:41 +01:00
parent 7daf97ba1c
commit c0bd1ac516
2 changed files with 207 additions and 60 deletions

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import { browser } from '$app/env'
import { page } from '$app/stores'
import { getAssetUrlKey } from '$utils/helpers'
import { throttle } from '$utils/functions'
import dayjs from 'dayjs'
import advancedFormat from 'dayjs/plugin/advancedFormat.js'
// Components
@@ -13,52 +15,60 @@
export let photos: any[]
export let location: any
export let currentIndex: number
export let totalPhotos: number
export let countPhotos: number
export let limit: number
export let offset: number
dayjs.extend(advancedFormat)
// Define reactive last and first photos
$: isLast = currentIndex === totalPhotos - 1
$: isFirst = currentIndex === 0
enum directions { PREV, NEXT }
let globalOffset = offset
let isLoading = 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[photos.findIndex((photo: any) => photo.slug === $page.params.photo)]
// Reactive photos
$: displayedPhotos = [
...photos.filter((photo: any, index: number) => {
// Grab the 4 prev and next photos depending on the index
// console.log(index)
// console.log(index >= currentIndex - 4 && index < currentIndex + 4)
})
]
$: currentPhoto = photos[currentIndex]
// 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 = () => {
if (!isLast) {
currentIndex++
}
// TODO: Fetch new photos
}
const goToNext = throttle(() => canGoPrev && currentIndex++, 200)
// Fo to previous photo
const goToPrevious = () => {
if (!isFirst) {
currentIndex--
}
const goToPrevious = throttle(() => canGoNext && (currentIndex = Math.max(currentIndex - 1, 0)), 200)
// TODO: Fetch new photos
}
// Manage navigation with keyboard
// Enable navigation with keyboard
const handleKeydown = ({ key, defaultPrevented }: KeyboardEvent) => {
if (defaultPrevented) return
switch (key) {
case 'ArrowLeft': goToNext(); break;
case 'ArrowRight': goToPrevious(); break;
case 'ArrowLeft': goToPrevious(); break;
case 'ArrowRight': goToNext(); break;
default: return;
}
}
@@ -67,8 +77,76 @@
/**
* Load photos
*/
const loadPhotos = (index: number) => {
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
}
}
}
`)
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>
@@ -87,9 +165,9 @@
<div class="container grid">
<div class="viewer-photo__carousel">
<div class="viewer-photo__images">
{#each photos as photo}
{#each visiblePhotos as photo, index (photo.id)}
<Image
class="photo {photo.id === currentPhoto.id ? 'is-active' : ''}"
class="photo photo--{currentIndex === 0 ? index + 1 : index}"
id={photo.image.id}
alt={photo.title}
sizeKey="photo-list"
@@ -103,16 +181,16 @@
{/each}
<div class="viewer-photo__controls">
<ButtonCircle on:click={goToNext} disabled={isLast}>
<ButtonCircle on:click={goToPrevious} disabled={!canGoNext}>
<IconArrow color="pink" flip={true} />
</ButtonCircle>
<ButtonCircle on:click={goToPrevious} disabled={isFirst}>
<ButtonCircle on:click={goToNext} disabled={!canGoPrev}>
<IconArrow color="pink" />
</ButtonCircle>
</div>
<span class="viewer-photo__index title-index">
{currentIndex + 1}
{globalOffset + currentIndex + 1}
</span>
</div>
@@ -120,7 +198,7 @@
<h1 class="title-medium">{currentPhoto.title}</h1>
<div class="detail text-info">
<Icon class="icon" icon="map-pin" label="Map pin" /> <span>{location.name}, {location.country.name}</span> <span class="sep">&middot;</span> <time datetime={dayjs(currentPhoto.date_taken).format('YYYY-MM-DD')}>{dayjs(currentPhoto.date_taken).format('MMMM, Do YYYY')}</time>
<Icon class="icon" icon="map-pin" label="Map pin" /><span>{location.name}, {location.country.name}</span> <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>
@@ -132,12 +210,45 @@
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}" }}},
filter: {
location: { slug: { _eq: "${page.params.location}" }}
status: { _eq: "published" },
},
sort: "-date_created",
limit: 5,
limit: ${limit},
offset: ${offset},
) {
id
title
@@ -162,15 +273,16 @@
const totalPhotos = stuff.countTotalPhotosByLocation.find((total: any) => total.group.location === Number(location.id)).count.id
// Find photo's index
const currentPhotoIndex = data.photos.findIndex((photo: any) => photo.slug === page.params.photo)
const currentIndex = (totalPhotos - 1) - currentPhotoIndex
const currentIndex = data.photos.findIndex((photo: any) => photo.slug === page.params.photo)
return {
props: {
photos: data.photos,
location: data.location[0],
currentIndex,
totalPhotos,
countPhotos: totalPhotos,
limit,
offset,
}
}
}