Phot next/prev buttons follow you

This commit is contained in:
Justin Edmund 2025-06-13 07:39:24 -04:00
parent 00fc9b90cc
commit d1c7a777ed
3 changed files with 378 additions and 79 deletions

View file

@ -5,6 +5,7 @@
import { generateMetaTags, generateCreativeWorkJsonLd } from '$lib/utils/metadata'
import { page } from '$app/stores'
import { goto } from '$app/navigation'
import { spring } from 'svelte/motion'
import type { PageData } from './$types'
import ArrowLeft from '$icons/arrow-left.svg'
import ArrowRight from '$icons/arrow-right.svg'
@ -16,6 +17,25 @@
const navigation = $derived(data.navigation)
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)
@ -63,6 +83,122 @@
})
: 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>
<svelte:head>
@ -102,7 +238,11 @@
</div>
</div>
{:else}
<div class="photo-page">
<div
class="photo-page"
onmousemove={handleMouseMove}
onmouseleave={handleMouseLeave}
>
<div class="photo-content-wrapper">
<PhotoView
src={photo.url}
@ -110,12 +250,19 @@
title={photo.title}
id={photo.id}
/>
</div>
<!-- Adjacent Photos Navigation -->
<div class="adjacent-navigation">
{#if navigation.prevPhoto}
<button
class="nav-button prev"
class:hovering={isHoveringLeft}
style="
left: {$leftButtonCoords.x}px;
top: {$leftButtonCoords.y}px;
transform: translate(-50%, -50%);
"
onclick={() => goto(`/photos/${album.slug}/${navigation.prevPhoto.id}`)}
type="button"
aria-label="Previous photo"
@ -127,6 +274,12 @@
{#if navigation.nextPhoto}
<button
class="nav-button next"
class:hovering={isHoveringRight}
style="
left: {$rightButtonCoords.x}px;
top: {$rightButtonCoords.y}px;
transform: translate(-50%, -50%);
"
onclick={() => goto(`/photos/${album.slug}/${navigation.nextPhoto.id}`)}
type="button"
aria-label="Next photo"
@ -135,7 +288,6 @@
</button>
{/if}
</div>
</div>
<PhotoMetadata
title={photo.title}
@ -209,7 +361,7 @@
margin: 0 auto;
display: flex;
align-items: center;
box-sizing: border-box;
justify-content: center;
}
// Adjacent Navigation
@ -217,8 +369,8 @@
position: absolute;
top: 0;
bottom: 0;
left: calc(-48px - #{$unit-2x});
right: calc(-48px - #{$unit-2x});
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
@ -235,7 +387,7 @@
width: 48px;
height: 48px;
pointer-events: auto;
position: relative;
position: absolute;
border: none;
padding: 0;
background: $grey-100;
@ -244,14 +396,19 @@
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: background 0.2s ease, box-shadow 0.2s ease;
&:hover {
background: $grey-95;
transform: scale(1.1);
}
&.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 {
outline: none;
@ -269,13 +426,5 @@
stroke-linecap: round;
stroke-linejoin: round;
}
&.prev {
margin-right: auto;
}
&.next {
margin-left: auto;
}
}
</style>

View file

@ -6,6 +6,7 @@
import { page } from '$app/stores'
import { goto } from '$app/navigation'
import { onMount } from 'svelte'
import { spring } from 'svelte/motion'
import type { PageData } from './$types'
import { isAlbum } from '$lib/types/photos'
import ArrowLeft from '$icons/arrow-left.svg'
@ -18,6 +19,25 @@
const photoItems = $derived(data.photoItems || [])
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)
// 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
$effect(() => {
window.addEventListener('keydown', handleKeydown)
@ -134,7 +255,11 @@
</div>
</div>
{:else if photo}
<div class="photo-page">
<div
class="photo-page"
onmousemove={handleMouseMove}
onmouseleave={handleMouseLeave}
>
<div class="photo-content-wrapper">
<PhotoView
src={photo.url}
@ -142,12 +267,19 @@
title={photo.title}
id={photo.id}
/>
</div>
<!-- Adjacent Photos Navigation -->
<div class="adjacent-navigation">
{#if adjacentPhotos().prev}
<button
class="nav-button prev"
class:hovering={isHoveringLeft}
style="
left: {$leftButtonCoords.x}px;
top: {$leftButtonCoords.y}px;
transform: translate(-50%, -50%);
"
onclick={() => navigateToPhoto(adjacentPhotos().prev)}
type="button"
aria-label="Previous photo"
@ -159,6 +291,12 @@
{#if adjacentPhotos().next}
<button
class="nav-button next"
class:hovering={isHoveringRight}
style="
left: {$rightButtonCoords.x}px;
top: {$rightButtonCoords.y}px;
transform: translate(-50%, -50%);
"
onclick={() => navigateToPhoto(adjacentPhotos().next)}
type="button"
aria-label="Next photo"
@ -167,7 +305,6 @@
</button>
{/if}
</div>
</div>
<PhotoMetadata
title={photo.title}
@ -240,6 +377,7 @@
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
}
@ -248,8 +386,8 @@
position: absolute;
top: 0;
bottom: 0;
left: calc(-48px - #{$unit-2x});
right: calc(-48px - #{$unit-2x});
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
@ -266,7 +404,7 @@
width: 48px;
height: 48px;
pointer-events: auto;
position: relative;
position: absolute;
border: none;
padding: 0;
background: $grey-100;
@ -275,14 +413,19 @@
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: background 0.2s ease, box-shadow 0.2s ease;
&:hover {
background: $grey-95;
transform: scale(1.1);
}
&.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 {
outline: none;
@ -300,13 +443,5 @@
stroke-linecap: round;
stroke-linejoin: round;
}
&.prev {
margin-right: auto;
}
&.next {
margin-left: auto;
}
}
</style>

15
test-db.ts Normal file
View 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()