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 { 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>
|
||||
|
|
|
|||
|
|
@ -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
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