Phot next/prev buttons follow you
This commit is contained in:
parent
00fc9b90cc
commit
d1c7a777ed
3 changed files with 378 additions and 79 deletions
|
|
@ -5,6 +5,7 @@
|
||||||
import { generateMetaTags, generateCreativeWorkJsonLd } from '$lib/utils/metadata'
|
import { generateMetaTags, generateCreativeWorkJsonLd } from '$lib/utils/metadata'
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
|
import { spring } from 'svelte/motion'
|
||||||
import type { PageData } from './$types'
|
import type { PageData } from './$types'
|
||||||
import ArrowLeft from '$icons/arrow-left.svg'
|
import ArrowLeft from '$icons/arrow-left.svg'
|
||||||
import ArrowRight from '$icons/arrow-right.svg'
|
import ArrowRight from '$icons/arrow-right.svg'
|
||||||
|
|
@ -16,6 +17,25 @@
|
||||||
const navigation = $derived(data.navigation)
|
const navigation = $derived(data.navigation)
|
||||||
const error = $derived(data.error)
|
const error = $derived(data.error)
|
||||||
|
|
||||||
|
// Hover tracking for arrow buttons
|
||||||
|
let isHoveringLeft = $state(false)
|
||||||
|
let isHoveringRight = $state(false)
|
||||||
|
|
||||||
|
// Spring stores for smooth button movement
|
||||||
|
const leftButtonCoords = spring({ x: 0, y: 0 }, {
|
||||||
|
stiffness: 0.3,
|
||||||
|
damping: 0.8
|
||||||
|
})
|
||||||
|
|
||||||
|
const rightButtonCoords = spring({ x: 0, y: 0 }, {
|
||||||
|
stiffness: 0.3,
|
||||||
|
damping: 0.8
|
||||||
|
})
|
||||||
|
|
||||||
|
// Default button positions (will be set once photo loads)
|
||||||
|
let defaultLeftX = 0
|
||||||
|
let defaultRightX = 0
|
||||||
|
|
||||||
|
|
||||||
const pageUrl = $derived($page.url.href)
|
const pageUrl = $derived($page.url.href)
|
||||||
|
|
||||||
|
|
@ -63,6 +83,122 @@
|
||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Set default button positions when component mounts
|
||||||
|
$effect(() => {
|
||||||
|
if (!photo) return
|
||||||
|
|
||||||
|
// Wait for DOM to update and image to load
|
||||||
|
const checkAndSetPositions = () => {
|
||||||
|
const pageContainer = document.querySelector('.photo-page') as HTMLElement
|
||||||
|
const photoImage = pageContainer?.querySelector('.photo-content-wrapper img') as HTMLElement
|
||||||
|
|
||||||
|
if (photoImage && photoImage.complete) {
|
||||||
|
const imageRect = photoImage.getBoundingClientRect()
|
||||||
|
const pageRect = pageContainer.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Calculate default positions relative to the image
|
||||||
|
// Add 24px (half button width) since we're using translate(-50%, -50%)
|
||||||
|
defaultLeftX = (imageRect.left - pageRect.left) - 24 - 16 // half button width + gap
|
||||||
|
defaultRightX = (imageRect.right - pageRect.left) + 24 + 16 // half button width + gap
|
||||||
|
|
||||||
|
// Set initial positions at the vertical center of the image
|
||||||
|
const centerY = (imageRect.top - pageRect.top) + (imageRect.height / 2)
|
||||||
|
leftButtonCoords.set({ x: defaultLeftX, y: centerY }, { hard: true })
|
||||||
|
rightButtonCoords.set({ x: defaultRightX, y: centerY }, { hard: true })
|
||||||
|
|
||||||
|
// Check if mouse is already in a hover zone
|
||||||
|
checkInitialMousePosition(pageContainer, imageRect, pageRect)
|
||||||
|
} else {
|
||||||
|
// If image not loaded yet, try again
|
||||||
|
setTimeout(checkAndSetPositions, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAndSetPositions()
|
||||||
|
})
|
||||||
|
|
||||||
|
// We'll just remove the initial check for now
|
||||||
|
function checkInitialMousePosition(pageContainer: HTMLElement, imageRect: DOMRect, pageRect: DOMRect) {
|
||||||
|
// This will be handled by the first mouse move
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse tracking for hover areas
|
||||||
|
function handleMouseMove(event: MouseEvent) {
|
||||||
|
const pageContainer = event.currentTarget as HTMLElement
|
||||||
|
const photoWrapper = pageContainer.querySelector('.photo-content-wrapper') as HTMLElement
|
||||||
|
|
||||||
|
if (!photoWrapper) return
|
||||||
|
|
||||||
|
// Get the actual image element inside PhotoView
|
||||||
|
const photoImage = photoWrapper.querySelector('img') as HTMLElement
|
||||||
|
if (!photoImage) return
|
||||||
|
|
||||||
|
const pageRect = pageContainer.getBoundingClientRect()
|
||||||
|
const photoRect = photoImage.getBoundingClientRect()
|
||||||
|
|
||||||
|
const x = event.clientX
|
||||||
|
const mouseX = event.clientX - pageRect.left
|
||||||
|
const mouseY = event.clientY - pageRect.top
|
||||||
|
|
||||||
|
// Check if mouse is in the left or right margin (outside the photo)
|
||||||
|
const wasHoveringLeft = isHoveringLeft
|
||||||
|
const wasHoveringRight = isHoveringRight
|
||||||
|
|
||||||
|
isHoveringLeft = x < photoRect.left
|
||||||
|
isHoveringRight = x > photoRect.right
|
||||||
|
|
||||||
|
// Calculate image center Y position
|
||||||
|
const imageCenterY = (photoRect.top - pageRect.top) + (photoRect.height / 2)
|
||||||
|
|
||||||
|
// Update button positions
|
||||||
|
if (isHoveringLeft) {
|
||||||
|
leftButtonCoords.set({ x: mouseX, y: mouseY })
|
||||||
|
} else if (wasHoveringLeft && !isHoveringLeft) {
|
||||||
|
// Reset left button to default
|
||||||
|
leftButtonCoords.set({ x: defaultLeftX, y: imageCenterY })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHoveringRight) {
|
||||||
|
rightButtonCoords.set({ x: mouseX, y: mouseY })
|
||||||
|
} else if (wasHoveringRight && !isHoveringRight) {
|
||||||
|
// Reset right button to default
|
||||||
|
rightButtonCoords.set({ x: defaultRightX, y: imageCenterY })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
isHoveringLeft = false
|
||||||
|
isHoveringRight = false
|
||||||
|
|
||||||
|
// Reset buttons to default positions
|
||||||
|
const pageContainer = document.querySelector('.photo-page') as HTMLElement
|
||||||
|
const photoImage = pageContainer?.querySelector('.photo-content-wrapper img') as HTMLElement
|
||||||
|
|
||||||
|
if (photoImage && pageContainer) {
|
||||||
|
const imageRect = photoImage.getBoundingClientRect()
|
||||||
|
const pageRect = pageContainer.getBoundingClientRect()
|
||||||
|
const centerY = (imageRect.top - pageRect.top) + (imageRect.height / 2)
|
||||||
|
|
||||||
|
leftButtonCoords.set({ x: defaultLeftX, y: centerY })
|
||||||
|
rightButtonCoords.set({ x: defaultRightX, y: centerY })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard navigation
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'ArrowLeft' && navigation?.prevPhoto) {
|
||||||
|
goto(`/photos/${album.slug}/${navigation.prevPhoto.id}`)
|
||||||
|
} else if (e.key === 'ArrowRight' && navigation?.nextPhoto) {
|
||||||
|
goto(`/photos/${album.slug}/${navigation.nextPhoto.id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up keyboard listener
|
||||||
|
$effect(() => {
|
||||||
|
window.addEventListener('keydown', handleKeydown)
|
||||||
|
return () => window.removeEventListener('keydown', handleKeydown)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -102,7 +238,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="photo-page">
|
<div
|
||||||
|
class="photo-page"
|
||||||
|
onmousemove={handleMouseMove}
|
||||||
|
onmouseleave={handleMouseLeave}
|
||||||
|
>
|
||||||
<div class="photo-content-wrapper">
|
<div class="photo-content-wrapper">
|
||||||
<PhotoView
|
<PhotoView
|
||||||
src={photo.url}
|
src={photo.url}
|
||||||
|
|
@ -110,31 +250,43 @@
|
||||||
title={photo.title}
|
title={photo.title}
|
||||||
id={photo.id}
|
id={photo.id}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Adjacent Photos Navigation -->
|
<!-- Adjacent Photos Navigation -->
|
||||||
<div class="adjacent-navigation">
|
<div class="adjacent-navigation">
|
||||||
{#if navigation.prevPhoto}
|
{#if navigation.prevPhoto}
|
||||||
<button
|
<button
|
||||||
class="nav-button prev"
|
class="nav-button prev"
|
||||||
onclick={() => goto(`/photos/${album.slug}/${navigation.prevPhoto.id}`)}
|
class:hovering={isHoveringLeft}
|
||||||
type="button"
|
style="
|
||||||
aria-label="Previous photo"
|
left: {$leftButtonCoords.x}px;
|
||||||
>
|
top: {$leftButtonCoords.y}px;
|
||||||
<ArrowLeft class="nav-icon" />
|
transform: translate(-50%, -50%);
|
||||||
</button>
|
"
|
||||||
{/if}
|
onclick={() => goto(`/photos/${album.slug}/${navigation.prevPhoto.id}`)}
|
||||||
|
type="button"
|
||||||
|
aria-label="Previous photo"
|
||||||
|
>
|
||||||
|
<ArrowLeft class="nav-icon" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if navigation.nextPhoto}
|
{#if navigation.nextPhoto}
|
||||||
<button
|
<button
|
||||||
class="nav-button next"
|
class="nav-button next"
|
||||||
onclick={() => goto(`/photos/${album.slug}/${navigation.nextPhoto.id}`)}
|
class:hovering={isHoveringRight}
|
||||||
type="button"
|
style="
|
||||||
aria-label="Next photo"
|
left: {$rightButtonCoords.x}px;
|
||||||
>
|
top: {$rightButtonCoords.y}px;
|
||||||
<ArrowRight class="nav-icon" />
|
transform: translate(-50%, -50%);
|
||||||
</button>
|
"
|
||||||
{/if}
|
onclick={() => goto(`/photos/${album.slug}/${navigation.nextPhoto.id}`)}
|
||||||
</div>
|
type="button"
|
||||||
|
aria-label="Next photo"
|
||||||
|
>
|
||||||
|
<ArrowRight class="nav-icon" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PhotoMetadata
|
<PhotoMetadata
|
||||||
|
|
@ -209,7 +361,7 @@
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjacent Navigation
|
// Adjacent Navigation
|
||||||
|
|
@ -217,8 +369,8 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: calc(-48px - #{$unit-2x});
|
left: 0;
|
||||||
right: calc(-48px - #{$unit-2x});
|
right: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -235,7 +387,7 @@
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
position: relative;
|
position: absolute;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: $grey-100;
|
background: $grey-100;
|
||||||
|
|
@ -244,13 +396,18 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s ease;
|
transition: background 0.2s ease, box-shadow 0.2s ease;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $grey-95;
|
background: $grey-95;
|
||||||
transform: scale(1.1);
|
}
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
|
||||||
|
&.hovering {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
|
|
@ -269,13 +426,5 @@
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.prev {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.next {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
|
import { spring } from 'svelte/motion'
|
||||||
import type { PageData } from './$types'
|
import type { PageData } from './$types'
|
||||||
import { isAlbum } from '$lib/types/photos'
|
import { isAlbum } from '$lib/types/photos'
|
||||||
import ArrowLeft from '$icons/arrow-left.svg'
|
import ArrowLeft from '$icons/arrow-left.svg'
|
||||||
|
|
@ -18,6 +19,25 @@
|
||||||
const photoItems = $derived(data.photoItems || [])
|
const photoItems = $derived(data.photoItems || [])
|
||||||
const currentPhotoId = $derived(data.currentPhotoId)
|
const currentPhotoId = $derived(data.currentPhotoId)
|
||||||
|
|
||||||
|
// Hover tracking for arrow buttons
|
||||||
|
let isHoveringLeft = $state(false)
|
||||||
|
let isHoveringRight = $state(false)
|
||||||
|
|
||||||
|
// Spring stores for smooth button movement
|
||||||
|
const leftButtonCoords = spring({ x: 0, y: 0 }, {
|
||||||
|
stiffness: 0.3,
|
||||||
|
damping: 0.8
|
||||||
|
})
|
||||||
|
|
||||||
|
const rightButtonCoords = spring({ x: 0, y: 0 }, {
|
||||||
|
stiffness: 0.3,
|
||||||
|
damping: 0.8
|
||||||
|
})
|
||||||
|
|
||||||
|
// Default button positions (will be set once photo loads)
|
||||||
|
let defaultLeftX = 0
|
||||||
|
let defaultRightX = 0
|
||||||
|
|
||||||
const pageUrl = $derived($page.url.href)
|
const pageUrl = $derived($page.url.href)
|
||||||
|
|
||||||
// Generate metadata
|
// Generate metadata
|
||||||
|
|
@ -95,6 +115,107 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set default button positions when component mounts
|
||||||
|
$effect(() => {
|
||||||
|
if (!photo) return
|
||||||
|
|
||||||
|
// Wait for DOM to update and image to load
|
||||||
|
const checkAndSetPositions = () => {
|
||||||
|
const pageContainer = document.querySelector('.photo-page') as HTMLElement
|
||||||
|
const photoImage = pageContainer?.querySelector('.photo-content-wrapper img') as HTMLElement
|
||||||
|
|
||||||
|
if (photoImage && photoImage.complete) {
|
||||||
|
const imageRect = photoImage.getBoundingClientRect()
|
||||||
|
const pageRect = pageContainer.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Calculate default positions relative to the image
|
||||||
|
// Add 24px (half button width) since we're using translate(-50%, -50%)
|
||||||
|
defaultLeftX = (imageRect.left - pageRect.left) - 24 - 16 // half button width + gap
|
||||||
|
defaultRightX = (imageRect.right - pageRect.left) + 24 + 16 // half button width + gap
|
||||||
|
|
||||||
|
// Set initial positions at the vertical center of the image
|
||||||
|
const centerY = (imageRect.top - pageRect.top) + (imageRect.height / 2)
|
||||||
|
leftButtonCoords.set({ x: defaultLeftX, y: centerY }, { hard: true })
|
||||||
|
rightButtonCoords.set({ x: defaultRightX, y: centerY }, { hard: true })
|
||||||
|
|
||||||
|
// Check if mouse is already in a hover zone
|
||||||
|
checkInitialMousePosition(pageContainer, imageRect, pageRect)
|
||||||
|
} else {
|
||||||
|
// If image not loaded yet, try again
|
||||||
|
setTimeout(checkAndSetPositions, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAndSetPositions()
|
||||||
|
})
|
||||||
|
|
||||||
|
// We'll just remove the initial check for now
|
||||||
|
function checkInitialMousePosition(pageContainer: HTMLElement, imageRect: DOMRect, pageRect: DOMRect) {
|
||||||
|
// This will be handled by the first mouse move
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse tracking for hover areas
|
||||||
|
function handleMouseMove(event: MouseEvent) {
|
||||||
|
const pageContainer = event.currentTarget as HTMLElement
|
||||||
|
const photoWrapper = pageContainer.querySelector('.photo-content-wrapper') as HTMLElement
|
||||||
|
|
||||||
|
if (!photoWrapper) return
|
||||||
|
|
||||||
|
// Get the actual image element inside PhotoView
|
||||||
|
const photoImage = photoWrapper.querySelector('img') as HTMLElement
|
||||||
|
if (!photoImage) return
|
||||||
|
|
||||||
|
const pageRect = pageContainer.getBoundingClientRect()
|
||||||
|
const photoRect = photoImage.getBoundingClientRect()
|
||||||
|
|
||||||
|
const x = event.clientX
|
||||||
|
const mouseX = event.clientX - pageRect.left
|
||||||
|
const mouseY = event.clientY - pageRect.top
|
||||||
|
|
||||||
|
// Check if mouse is in the left or right margin (outside the photo)
|
||||||
|
const wasHoveringLeft = isHoveringLeft
|
||||||
|
const wasHoveringRight = isHoveringRight
|
||||||
|
|
||||||
|
isHoveringLeft = x < photoRect.left
|
||||||
|
isHoveringRight = x > photoRect.right
|
||||||
|
|
||||||
|
// Calculate image center Y position
|
||||||
|
const imageCenterY = (photoRect.top - pageRect.top) + (photoRect.height / 2)
|
||||||
|
|
||||||
|
// Update button positions
|
||||||
|
if (isHoveringLeft) {
|
||||||
|
leftButtonCoords.set({ x: mouseX, y: mouseY })
|
||||||
|
} else if (wasHoveringLeft && !isHoveringLeft) {
|
||||||
|
// Reset left button to default
|
||||||
|
leftButtonCoords.set({ x: defaultLeftX, y: imageCenterY })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHoveringRight) {
|
||||||
|
rightButtonCoords.set({ x: mouseX, y: mouseY })
|
||||||
|
} else if (wasHoveringRight && !isHoveringRight) {
|
||||||
|
// Reset right button to default
|
||||||
|
rightButtonCoords.set({ x: defaultRightX, y: imageCenterY })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
isHoveringLeft = false
|
||||||
|
isHoveringRight = false
|
||||||
|
|
||||||
|
// Reset buttons to default positions
|
||||||
|
const pageContainer = document.querySelector('.photo-page') as HTMLElement
|
||||||
|
const photoImage = pageContainer?.querySelector('.photo-content-wrapper img') as HTMLElement
|
||||||
|
|
||||||
|
if (photoImage && pageContainer) {
|
||||||
|
const imageRect = photoImage.getBoundingClientRect()
|
||||||
|
const pageRect = pageContainer.getBoundingClientRect()
|
||||||
|
const centerY = (imageRect.top - pageRect.top) + (imageRect.height / 2)
|
||||||
|
|
||||||
|
leftButtonCoords.set({ x: defaultLeftX, y: centerY })
|
||||||
|
rightButtonCoords.set({ x: defaultRightX, y: centerY })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set up keyboard listener
|
// Set up keyboard listener
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
window.addEventListener('keydown', handleKeydown)
|
window.addEventListener('keydown', handleKeydown)
|
||||||
|
|
@ -134,7 +255,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if photo}
|
{:else if photo}
|
||||||
<div class="photo-page">
|
<div
|
||||||
|
class="photo-page"
|
||||||
|
onmousemove={handleMouseMove}
|
||||||
|
onmouseleave={handleMouseLeave}
|
||||||
|
>
|
||||||
<div class="photo-content-wrapper">
|
<div class="photo-content-wrapper">
|
||||||
<PhotoView
|
<PhotoView
|
||||||
src={photo.url}
|
src={photo.url}
|
||||||
|
|
@ -142,31 +267,43 @@
|
||||||
title={photo.title}
|
title={photo.title}
|
||||||
id={photo.id}
|
id={photo.id}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Adjacent Photos Navigation -->
|
<!-- Adjacent Photos Navigation -->
|
||||||
<div class="adjacent-navigation">
|
<div class="adjacent-navigation">
|
||||||
{#if adjacentPhotos().prev}
|
{#if adjacentPhotos().prev}
|
||||||
<button
|
<button
|
||||||
class="nav-button prev"
|
class="nav-button prev"
|
||||||
onclick={() => navigateToPhoto(adjacentPhotos().prev)}
|
class:hovering={isHoveringLeft}
|
||||||
type="button"
|
style="
|
||||||
aria-label="Previous photo"
|
left: {$leftButtonCoords.x}px;
|
||||||
>
|
top: {$leftButtonCoords.y}px;
|
||||||
<ArrowLeft class="nav-icon" />
|
transform: translate(-50%, -50%);
|
||||||
</button>
|
"
|
||||||
{/if}
|
onclick={() => navigateToPhoto(adjacentPhotos().prev)}
|
||||||
|
type="button"
|
||||||
|
aria-label="Previous photo"
|
||||||
|
>
|
||||||
|
<ArrowLeft class="nav-icon" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if adjacentPhotos().next}
|
{#if adjacentPhotos().next}
|
||||||
<button
|
<button
|
||||||
class="nav-button next"
|
class="nav-button next"
|
||||||
onclick={() => navigateToPhoto(adjacentPhotos().next)}
|
class:hovering={isHoveringRight}
|
||||||
type="button"
|
style="
|
||||||
aria-label="Next photo"
|
left: {$rightButtonCoords.x}px;
|
||||||
>
|
top: {$rightButtonCoords.y}px;
|
||||||
<ArrowRight class="nav-icon" />
|
transform: translate(-50%, -50%);
|
||||||
</button>
|
"
|
||||||
{/if}
|
onclick={() => navigateToPhoto(adjacentPhotos().next)}
|
||||||
</div>
|
type="button"
|
||||||
|
aria-label="Next photo"
|
||||||
|
>
|
||||||
|
<ArrowRight class="nav-icon" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PhotoMetadata
|
<PhotoMetadata
|
||||||
|
|
@ -240,6 +377,7 @@
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -248,8 +386,8 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: calc(-48px - #{$unit-2x});
|
left: 0;
|
||||||
right: calc(-48px - #{$unit-2x});
|
right: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -266,7 +404,7 @@
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
position: relative;
|
position: absolute;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: $grey-100;
|
background: $grey-100;
|
||||||
|
|
@ -275,13 +413,18 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s ease;
|
transition: background 0.2s ease, box-shadow 0.2s ease;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $grey-95;
|
background: $grey-95;
|
||||||
transform: scale(1.1);
|
}
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
|
||||||
|
&.hovering {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
|
|
@ -300,13 +443,5 @@
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.prev {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.next {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
15
test-db.ts
Normal file
15
test-db.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
async function testDb() {
|
||||||
|
try {
|
||||||
|
const count = await prisma.media.count()
|
||||||
|
console.log('Total media entries:', count)
|
||||||
|
await prisma.$disconnect()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Database error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testDb()
|
||||||
Loading…
Reference in a new issue