🔥 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:
@@ -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">·</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">·</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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user