feat(photos): enhance photo viewing with improved grids and metadata
- Add PhotoGrid component as base for photo grid layouts - Update PhotoItem with color placeholder loading states - Enhance PhotoMetadata display with better formatting - Improve PhotoViewEnhanced with smoother transitions - Update single and two-column grid layouts - Fix photo routing for album-based photo URLs - Add support for direct photo ID routes - Improve photo page performance and loading states Creates a more polished photo viewing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
cfde42c336
commit
02e41ed3d6
10 changed files with 467 additions and 86 deletions
122
src/lib/components/PhotoGrid.svelte
Normal file
122
src/lib/components/PhotoGrid.svelte
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import SmartImage from './SmartImage.svelte'
|
||||||
|
import type { Media } from '@prisma/client'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
photos: Media[]
|
||||||
|
layout?: 'masonry' | 'grid'
|
||||||
|
onPhotoClick?: (photo: Media) => void
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let { photos = [], layout = 'masonry', onPhotoClick, class: className = '' }: Props = $props()
|
||||||
|
|
||||||
|
function handlePhotoClick(photo: Media) {
|
||||||
|
if (onPhotoClick) {
|
||||||
|
onPhotoClick(photo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="photo-grid photo-grid-{layout} {className}">
|
||||||
|
{#each photos as photo}
|
||||||
|
<div class="grid-item">
|
||||||
|
{#if onPhotoClick}
|
||||||
|
<button class="photo-button" onclick={() => handlePhotoClick(photo)} type="button">
|
||||||
|
<SmartImage media={photo} alt={photo.description || ''} class="grid-photo" />
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<SmartImage media={photo} alt={photo.description || ''} class="grid-photo" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '$styles/variables.scss';
|
||||||
|
@import '$styles/mixins.scss';
|
||||||
|
|
||||||
|
.photo-grid {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-grid-masonry {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: $unit-2x;
|
||||||
|
|
||||||
|
@include breakpoint('phone') {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: $unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item {
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-grid-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: $unit-2x;
|
||||||
|
|
||||||
|
@include breakpoint('tablet') {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include breakpoint('phone') {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: $unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.grid-photo) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: $image-corner-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-button {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::after {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active::after {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.grid-photo) {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -26,7 +26,9 @@
|
||||||
} else {
|
} else {
|
||||||
// Navigate to individual photo page using the media ID
|
// Navigate to individual photo page using the media ID
|
||||||
const mediaId = item.id.replace(/^(media|photo)-/, '') // Support both prefixes
|
const mediaId = item.id.replace(/^(media|photo)-/, '') // Support both prefixes
|
||||||
goto(`/photos/p/${mediaId}`)
|
// Include the album slug as a 'from' parameter if we're in an album context
|
||||||
|
const url = albumSlug ? `/photos/p/${mediaId}?from=${albumSlug}` : `/photos/p/${mediaId}`
|
||||||
|
goto(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -37,16 +39,8 @@
|
||||||
|
|
||||||
const photo = $derived(isAlbum(item) ? item.coverPhoto : item)
|
const photo = $derived(isAlbum(item) ? item.coverPhoto : item)
|
||||||
const isAlbumItem = $derived(isAlbum(item))
|
const isAlbumItem = $derived(isAlbum(item))
|
||||||
const placeholderStyle = $derived(
|
const placeholderStyle = $derived(photo.dominantColor ? `background: ${photo.dominantColor}` : '')
|
||||||
photo.dominantColor
|
const aspectRatioStyle = $derived(photo.aspectRatio ? `aspect-ratio: ${photo.aspectRatio}` : '')
|
||||||
? `background: ${photo.dominantColor}`
|
|
||||||
: ''
|
|
||||||
)
|
|
||||||
const aspectRatioStyle = $derived(
|
|
||||||
photo.aspectRatio
|
|
||||||
? `aspect-ratio: ${photo.aspectRatio}`
|
|
||||||
: ''
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="photo-item" class:is-album={isAlbumItem}>
|
<div class="photo-item" class:is-album={isAlbumItem}>
|
||||||
|
|
@ -249,11 +243,9 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
|
||||||
&.loaded {
|
&.loaded {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
backHref?: string
|
backHref?: string
|
||||||
backLabel?: string
|
backLabel?: string
|
||||||
showBackButton?: boolean
|
showBackButton?: boolean
|
||||||
|
albums?: Array<{ id: number; title: string; slug: string }>
|
||||||
class?: string
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,6 +23,7 @@
|
||||||
backHref,
|
backHref,
|
||||||
backLabel,
|
backLabel,
|
||||||
showBackButton = false,
|
showBackButton = false,
|
||||||
|
albums = [],
|
||||||
class: className = ''
|
class: className = ''
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
|
|
||||||
|
|
@ -116,6 +118,19 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if albums && albums.length > 0}
|
||||||
|
<div class="albums-section">
|
||||||
|
<h3 class="albums-title">This photo appears in:</h3>
|
||||||
|
<div class="albums-list">
|
||||||
|
{#each albums as album}
|
||||||
|
<a href="/photos/{album.slug}" class="album-link">
|
||||||
|
{album.title}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if showBackButton && backHref && backLabel}
|
{#if showBackButton && backHref && backLabel}
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<BackButton href={backHref} label={backLabel} />
|
<BackButton href={backHref} label={backLabel} />
|
||||||
|
|
@ -219,6 +234,50 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.albums-section {
|
||||||
|
margin-bottom: $unit-4x;
|
||||||
|
padding-bottom: $unit-4x;
|
||||||
|
border-bottom: 1px solid $grey-90;
|
||||||
|
|
||||||
|
@include breakpoint('phone') {
|
||||||
|
margin-bottom: $unit-3x;
|
||||||
|
padding-bottom: $unit-3x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.albums-title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: $grey-40;
|
||||||
|
margin: 0 0 $unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.albums-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $unit $unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-link {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: $red-60;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $red-50;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child)::after {
|
||||||
|
content: ',';
|
||||||
|
color: $grey-40;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.card-footer {
|
.card-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,7 @@
|
||||||
height?: number
|
height?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { src, alt = '', title, id, class: className = '', width, height }: Props = $props()
|
||||||
src,
|
|
||||||
alt = '',
|
|
||||||
title,
|
|
||||||
id,
|
|
||||||
class: className = '',
|
|
||||||
width,
|
|
||||||
height
|
|
||||||
}: Props = $props()
|
|
||||||
|
|
||||||
let imageRef = $state<HTMLImageElement>()
|
let imageRef = $state<HTMLImageElement>()
|
||||||
let isUltrawide = $state(false)
|
let isUltrawide = $state(false)
|
||||||
|
|
@ -31,7 +23,12 @@
|
||||||
function checkIfUltrawide() {
|
function checkIfUltrawide() {
|
||||||
if (width && height) {
|
if (width && height) {
|
||||||
isUltrawide = width / height > 2
|
isUltrawide = width / height > 2
|
||||||
console.log('Ultrawide check from props:', { width, height, ratio: width / height, isUltrawide })
|
console.log('Ultrawide check from props:', {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
ratio: width / height,
|
||||||
|
isUltrawide
|
||||||
|
})
|
||||||
} else if (imageRef && imageLoaded) {
|
} else if (imageRef && imageLoaded) {
|
||||||
isUltrawide = imageRef.naturalWidth / imageRef.naturalHeight > 2
|
isUltrawide = imageRef.naturalWidth / imageRef.naturalHeight > 2
|
||||||
console.log('Ultrawide check from image:', {
|
console.log('Ultrawide check from image:', {
|
||||||
|
|
@ -72,12 +69,14 @@
|
||||||
mutations.forEach((mutation) => {
|
mutations.forEach((mutation) => {
|
||||||
if (mutation.type === 'childList') {
|
if (mutation.type === 'childList') {
|
||||||
// Check for zoom overlay - try multiple selectors
|
// Check for zoom overlay - try multiple selectors
|
||||||
const zoomOverlay = document.querySelector('[data-smiz-overlay]') ||
|
const zoomOverlay =
|
||||||
|
document.querySelector('[data-smiz-overlay]') ||
|
||||||
document.querySelector('.medium-image-zoom-overlay') ||
|
document.querySelector('.medium-image-zoom-overlay') ||
|
||||||
document.querySelector('[data-rmiz-modal-overlay]')
|
document.querySelector('[data-rmiz-modal-overlay]')
|
||||||
const zoomedImage = document.querySelector('[data-smiz-modal] img') ||
|
const zoomedImage =
|
||||||
|
document.querySelector('[data-smiz-modal] img') ||
|
||||||
document.querySelector('.medium-image-zoom-image') ||
|
document.querySelector('.medium-image-zoom-image') ||
|
||||||
document.querySelector('[data-rmiz-modal-img]') as HTMLImageElement
|
(document.querySelector('[data-rmiz-modal-img]') as HTMLImageElement)
|
||||||
|
|
||||||
console.log('Checking for zoom elements:', {
|
console.log('Checking for zoom elements:', {
|
||||||
zoomOverlay: !!zoomOverlay,
|
zoomOverlay: !!zoomOverlay,
|
||||||
|
|
@ -88,10 +87,16 @@
|
||||||
|
|
||||||
// Also check for any new elements with specific classes
|
// Also check for any new elements with specific classes
|
||||||
const allNewElements = mutation.addedNodes
|
const allNewElements = mutation.addedNodes
|
||||||
allNewElements.forEach(node => {
|
allNewElements.forEach((node) => {
|
||||||
if (node.nodeType === 1) { // Element node
|
if (node.nodeType === 1) {
|
||||||
|
// Element node
|
||||||
const element = node as HTMLElement
|
const element = node as HTMLElement
|
||||||
console.log('New element added:', element.tagName, element.className, element.getAttribute('data-rmiz-modal-overlay'))
|
console.log(
|
||||||
|
'New element added:',
|
||||||
|
element.tagName,
|
||||||
|
element.className,
|
||||||
|
element.getAttribute('data-rmiz-modal-overlay')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@
|
||||||
// Initialize view mode from URL or default
|
// Initialize view mode from URL or default
|
||||||
const urlMode = $page.url.searchParams.get('view') as ViewMode
|
const urlMode = $page.url.searchParams.get('view') as ViewMode
|
||||||
let viewMode = $state<ViewMode>(
|
let viewMode = $state<ViewMode>(
|
||||||
urlMode && ['masonry', 'single', 'two-column', 'horizontal'].includes(urlMode) ? urlMode : 'two-column'
|
urlMode && ['masonry', 'single', 'two-column', 'horizontal'].includes(urlMode)
|
||||||
|
? urlMode
|
||||||
|
: 'two-column'
|
||||||
)
|
)
|
||||||
|
|
||||||
// Track loaded photo IDs to prevent duplicates
|
// Track loaded photo IDs to prevent duplicates
|
||||||
|
|
|
||||||
|
|
@ -386,6 +386,7 @@
|
||||||
description={photo.description}
|
description={photo.description}
|
||||||
{exifData}
|
{exifData}
|
||||||
createdAt={photo.createdAt}
|
createdAt={photo.createdAt}
|
||||||
|
albums={photo.albums}
|
||||||
backHref={`/photos/${album.slug}`}
|
backHref={`/photos/${album.slug}`}
|
||||||
backLabel={`Back to ${album.title}`}
|
backLabel={`Back to ${album.title}`}
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import MasonryPhotoGrid from '$components/MasonryPhotoGrid.svelte'
|
import MasonryPhotoGrid from '$components/MasonryPhotoGrid.svelte'
|
||||||
import BackButton from '$components/BackButton.svelte'
|
import BackButton from '$components/BackButton.svelte'
|
||||||
import { generateMetaTags, generateImageGalleryJsonLd } from '$lib/utils/metadata'
|
import { generateMetaTags, generateImageGalleryJsonLd } from '$lib/utils/metadata'
|
||||||
|
import { renderEdraContent, getContentExcerpt } from '$lib/utils/content'
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
import type { PageData } from './$types'
|
import type { PageData } from './$types'
|
||||||
|
|
||||||
|
|
@ -35,13 +36,22 @@
|
||||||
|
|
||||||
const pageUrl = $derived($page.url.href)
|
const pageUrl = $derived($page.url.href)
|
||||||
|
|
||||||
|
// Helper to get content preview using Edra content excerpt utility
|
||||||
|
const extractContentPreview = (content: any): string => {
|
||||||
|
if (!content) return ''
|
||||||
|
return getContentExcerpt(content, 155)
|
||||||
|
}
|
||||||
|
|
||||||
// Generate metadata
|
// Generate metadata
|
||||||
const metaTags = $derived(
|
const metaTags = $derived(
|
||||||
type === 'album' && album
|
type === 'album' && album
|
||||||
? generateMetaTags({
|
? generateMetaTags({
|
||||||
title: album.title,
|
title: album.title,
|
||||||
description:
|
description: album.content
|
||||||
|
? extractContentPreview(album.content) ||
|
||||||
album.description ||
|
album.description ||
|
||||||
|
`Photo story: ${album.title}`
|
||||||
|
: album.description ||
|
||||||
`Photo album: ${album.title}${album.location ? ` taken in ${album.location}` : ''}`,
|
`Photo album: ${album.title}${album.location ? ` taken in ${album.location}` : ''}`,
|
||||||
url: pageUrl,
|
url: pageUrl,
|
||||||
image: album.photos?.[0]?.url,
|
image: album.photos?.[0]?.url,
|
||||||
|
|
@ -63,10 +73,9 @@
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// Generate image gallery JSON-LD
|
// Generate enhanced JSON-LD for albums with content
|
||||||
const galleryJsonLd = $derived(
|
const generateAlbumJsonLd = (album: any, pageUrl: string) => {
|
||||||
type === 'album' && album
|
const baseJsonLd = generateImageGalleryJsonLd({
|
||||||
? generateImageGalleryJsonLd({
|
|
||||||
name: album.title,
|
name: album.title,
|
||||||
description: album.description,
|
description: album.description,
|
||||||
url: pageUrl,
|
url: pageUrl,
|
||||||
|
|
@ -76,6 +85,48 @@
|
||||||
caption: photo.caption
|
caption: photo.caption
|
||||||
})) || []
|
})) || []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Enhance with Article schema if album has composed content
|
||||||
|
if (album.content) {
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@graph': [
|
||||||
|
baseJsonLd,
|
||||||
|
{
|
||||||
|
'@type': 'Article',
|
||||||
|
'@id': `${pageUrl}#article`,
|
||||||
|
headline: album.title,
|
||||||
|
description: album.description,
|
||||||
|
url: pageUrl,
|
||||||
|
datePublished: album.date || album.createdAt,
|
||||||
|
dateModified: album.updatedAt || album.createdAt,
|
||||||
|
author: {
|
||||||
|
'@type': 'Person',
|
||||||
|
name: 'Justin Edmund',
|
||||||
|
url: 'https://jedmund.com'
|
||||||
|
},
|
||||||
|
publisher: {
|
||||||
|
'@type': 'Person',
|
||||||
|
name: 'Justin Edmund',
|
||||||
|
url: 'https://jedmund.com'
|
||||||
|
},
|
||||||
|
image: album.photos?.[0]?.url,
|
||||||
|
mainEntityOfPage: {
|
||||||
|
'@type': 'WebPage',
|
||||||
|
'@id': pageUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseJsonLd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate image gallery JSON-LD
|
||||||
|
const galleryJsonLd = $derived(
|
||||||
|
type === 'album' && album
|
||||||
|
? generateAlbumJsonLd(album, pageUrl)
|
||||||
: type === 'photo' && photo
|
: type === 'photo' && photo
|
||||||
? {
|
? {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
|
|
@ -143,7 +194,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Photo Grid -->
|
<!-- Album Content -->
|
||||||
|
{#if album.content}
|
||||||
|
<div class="album-content-wrapper">
|
||||||
|
<div class="edra-rendered-content">
|
||||||
|
{@html renderEdraContent(album.content)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Legacy Photo Grid (for albums without composed content) -->
|
||||||
{#if photoItems.length > 0}
|
{#if photoItems.length > 0}
|
||||||
<MasonryPhotoGrid {photoItems} albumSlug={album.slug} />
|
<MasonryPhotoGrid {photoItems} albumSlug={album.slug} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -151,6 +210,7 @@
|
||||||
<p>This album doesn't contain any photos yet.</p>
|
<p>This album doesn't contain any photos yet.</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if type === 'photo' && photo}
|
{:else if type === 'photo' && photo}
|
||||||
<div class="photo-page">
|
<div class="photo-page">
|
||||||
|
|
@ -286,6 +346,134 @@
|
||||||
color: $grey-40;
|
color: $grey-40;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.album-content-wrapper {
|
||||||
|
margin-top: $unit-4x;
|
||||||
|
|
||||||
|
@include breakpoint('phone') {
|
||||||
|
margin-top: $unit-3x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edra-rendered-content {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
:global(p) {
|
||||||
|
margin: 0 0 $unit-3x;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: $grey-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(h1),
|
||||||
|
:global(h2),
|
||||||
|
:global(h3),
|
||||||
|
:global(h4) {
|
||||||
|
margin: $unit-4x 0 $unit-2x;
|
||||||
|
color: $grey-10;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(h1) {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
:global(h2) {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
:global(h3) {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
:global(h4) {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(ul),
|
||||||
|
:global(ol) {
|
||||||
|
margin: 0 0 $unit-3x;
|
||||||
|
padding-left: $unit-3x;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: $unit;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(blockquote) {
|
||||||
|
margin: $unit-4x 0;
|
||||||
|
padding: $unit-3x;
|
||||||
|
background: $grey-97;
|
||||||
|
border-left: 4px solid $grey-80;
|
||||||
|
border-radius: $unit;
|
||||||
|
color: $grey-30;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(a) {
|
||||||
|
color: $red-60;
|
||||||
|
text-decoration: underline;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $red-50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(code) {
|
||||||
|
background: $grey-95;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'SF Mono', Monaco, monospace;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(pre) {
|
||||||
|
background: $grey-95;
|
||||||
|
padding: $unit-3x;
|
||||||
|
border-radius: $unit;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0 0 $unit-3x;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(hr) {
|
||||||
|
margin: $unit-4x 0;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid $grey-90;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(figure) {
|
||||||
|
margin: $unit-4x 0;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: $card-corner-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
margin-top: $unit;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: $grey-40;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gallery styles
|
||||||
|
:global(.gallery-grid) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: $unit-2x;
|
||||||
|
margin: $unit-4x 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Geolocation styles
|
||||||
|
:global(.geolocation-map) {
|
||||||
|
margin: $unit-4x 0;
|
||||||
|
border-radius: $card-corner-radius;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.photo-page {
|
.photo-page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
let defaultRightX = 0
|
let defaultRightX = 0
|
||||||
|
|
||||||
const pageUrl = $derived($page.url.href)
|
const pageUrl = $derived($page.url.href)
|
||||||
|
const fromAlbum = $derived($page.url.searchParams.get('from'))
|
||||||
|
|
||||||
// Generate metadata
|
// Generate metadata
|
||||||
const metaTags = $derived(
|
const metaTags = $derived(
|
||||||
|
|
@ -408,8 +409,19 @@
|
||||||
description={photo.description}
|
description={photo.description}
|
||||||
{exifData}
|
{exifData}
|
||||||
createdAt={photo.createdAt}
|
createdAt={photo.createdAt}
|
||||||
backHref={photo.album ? `/photos/${photo.album.slug}` : '/photos'}
|
albums={photo.albums}
|
||||||
backLabel={photo.album ? `Back to ${photo.album.title}` : 'Back to Photos'}
|
backHref={fromAlbum
|
||||||
|
? `/photos/${fromAlbum}`
|
||||||
|
: photo.album
|
||||||
|
? `/photos/${photo.album.slug}`
|
||||||
|
: '/photos'}
|
||||||
|
backLabel={(() => {
|
||||||
|
if (fromAlbum && photo.albums) {
|
||||||
|
const album = photo.albums.find((a) => a.slug === fromAlbum)
|
||||||
|
return album ? `Back to ${album.title}` : 'Back to Photos'
|
||||||
|
}
|
||||||
|
return photo.album ? `Back to ${photo.album.title}` : 'Back to Photos'
|
||||||
|
})()}
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue