Fine-tuning slideshow
This commit is contained in:
parent
7894750d2b
commit
0354b798d3
6 changed files with 419 additions and 252 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LinkCard from './LinkCard.svelte'
|
import LinkCard from './LinkCard.svelte'
|
||||||
|
import Slideshow from './Slideshow.svelte'
|
||||||
|
|
||||||
let { post }: { post: any } = $props()
|
let { post }: { post: any } = $props()
|
||||||
|
|
||||||
|
|
@ -127,19 +128,40 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if post.attachments && Array.isArray(post.attachments) && post.attachments.length > 0}
|
{#if post.album && post.album.photos && post.album.photos.length > 0}
|
||||||
<div class="post-attachments">
|
<!-- Album slideshow -->
|
||||||
<h3>Attachments</h3>
|
<div class="post-album">
|
||||||
<div class="attachments-grid">
|
<div class="album-header">
|
||||||
{#each post.attachments as attachment}
|
<h3>{post.album.title}</h3>
|
||||||
<div class="attachment-item">
|
{#if post.album.description}
|
||||||
<img src={attachment.url} alt={attachment.caption || 'Attachment'} loading="lazy" />
|
<p class="album-description">{post.album.description}</p>
|
||||||
{#if attachment.caption}
|
|
||||||
<p class="attachment-caption">{attachment.caption}</p>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
<Slideshow
|
||||||
|
items={post.album.photos.map(photo => ({
|
||||||
|
url: photo.url,
|
||||||
|
thumbnailUrl: photo.thumbnailUrl,
|
||||||
|
caption: photo.caption,
|
||||||
|
alt: photo.caption || post.album.title
|
||||||
|
}))}
|
||||||
|
alt={post.album.title}
|
||||||
|
aspectRatio="4/3"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if post.attachments && Array.isArray(post.attachments) && post.attachments.length > 0}
|
||||||
|
<!-- Regular attachments -->
|
||||||
|
<div class="post-attachments">
|
||||||
|
<h3>Photos</h3>
|
||||||
|
<Slideshow
|
||||||
|
items={post.attachments.map(attachment => ({
|
||||||
|
url: attachment.url,
|
||||||
|
thumbnailUrl: attachment.thumbnailUrl,
|
||||||
|
caption: attachment.caption,
|
||||||
|
alt: attachment.caption || 'Photo'
|
||||||
|
}))}
|
||||||
|
alt="Post photos"
|
||||||
|
aspectRatio="4/3"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
@ -228,6 +250,7 @@
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-album,
|
||||||
.post-attachments {
|
.post-attachments {
|
||||||
margin-bottom: $unit-4x;
|
margin-bottom: $unit-4x;
|
||||||
|
|
||||||
|
|
@ -237,26 +260,20 @@
|
||||||
margin: 0 0 $unit-2x;
|
margin: 0 0 $unit-2x;
|
||||||
color: $grey-20;
|
color: $grey-20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachments-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: $unit-2x;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-item {
|
.album-header {
|
||||||
img {
|
margin-bottom: $unit-3x;
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
h3 {
|
||||||
border-radius: $unit;
|
margin-bottom: $unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-caption {
|
.album-description {
|
||||||
margin: $unit 0 0;
|
margin: 0;
|
||||||
font-size: 0.875rem;
|
font-size: 0.9rem;
|
||||||
color: $grey-40;
|
color: $grey-40;
|
||||||
font-style: italic;
|
line-height: 1.5;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Lightbox from './Lightbox.svelte'
|
import Slideshow from './Slideshow.svelte'
|
||||||
import TiltCard from './TiltCard.svelte'
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
images = [],
|
images = [],
|
||||||
|
|
@ -10,191 +9,9 @@
|
||||||
alt?: string
|
alt?: string
|
||||||
} = $props()
|
} = $props()
|
||||||
|
|
||||||
let selectedIndex = $state(0)
|
// Convert string array to slideshow items
|
||||||
let lightboxOpen = $state(false)
|
const slideshowItems = $derived(images.map(url => ({ url, alt })))
|
||||||
let windowWidth = $state(0)
|
|
||||||
|
|
||||||
// Calculate columns based on breakpoints
|
|
||||||
const columnsPerRow = $derived(windowWidth <= 400 ? 3 : windowWidth <= 600 ? 4 : 6)
|
|
||||||
const totalSlots = $derived(Math.ceil(images.length / columnsPerRow) * columnsPerRow)
|
|
||||||
|
|
||||||
const selectImage = (index: number) => {
|
|
||||||
selectedIndex = index
|
|
||||||
}
|
|
||||||
|
|
||||||
const openLightbox = (index?: number) => {
|
|
||||||
if (index !== undefined) {
|
|
||||||
selectedIndex = index
|
|
||||||
}
|
|
||||||
lightboxOpen = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track window width for responsive columns
|
|
||||||
$effect(() => {
|
|
||||||
windowWidth = window.innerWidth
|
|
||||||
|
|
||||||
const handleResize = () => {
|
|
||||||
windowWidth = window.innerWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if images.length === 1}
|
<Slideshow items={slideshowItems} {alt} aspectRatio="4/3" />
|
||||||
<!-- Single image -->
|
|
||||||
<TiltCard>
|
|
||||||
<button class="single-image image-button" onclick={() => openLightbox()}>
|
|
||||||
<img src={images[0]} {alt} />
|
|
||||||
</button>
|
|
||||||
</TiltCard>
|
|
||||||
{:else if images.length > 1}
|
|
||||||
<!-- Slideshow -->
|
|
||||||
<div class="slideshow">
|
|
||||||
<TiltCard>
|
|
||||||
<button class="main-image image-button" onclick={() => openLightbox()}>
|
|
||||||
<img src={images[selectedIndex]} alt="{alt} {selectedIndex + 1}" />
|
|
||||||
</button>
|
|
||||||
</TiltCard>
|
|
||||||
<div class="thumbnails">
|
|
||||||
{#each Array(totalSlots) as _, index}
|
|
||||||
{#if index < images.length}
|
|
||||||
<button
|
|
||||||
class="thumbnail"
|
|
||||||
class:active={index === selectedIndex}
|
|
||||||
onclick={() => selectImage(index)}
|
|
||||||
aria-label="View image {index + 1}"
|
|
||||||
>
|
|
||||||
<img src={images[index]} alt="{alt} thumbnail {index + 1}" />
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<div class="thumbnail placeholder" aria-hidden="true"></div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Lightbox {images} bind:selectedIndex bind:isOpen={lightboxOpen} {alt} />
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.image-button {
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid $red-60;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-image,
|
|
||||||
.main-image {
|
|
||||||
aspect-ratio: 4 / 3;
|
|
||||||
border-radius: $image-corner-radius;
|
|
||||||
overflow: hidden;
|
|
||||||
// Force GPU acceleration and proper clipping
|
|
||||||
transform: translateZ(0);
|
|
||||||
-webkit-backface-visibility: hidden;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.slideshow {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $unit-2x;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnails {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(6, 1fr);
|
|
||||||
gap: $unit-2x;
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 400px) {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnail {
|
|
||||||
position: relative;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
border-radius: $image-corner-radius;
|
|
||||||
overflow: hidden;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
border-radius: $image-corner-radius;
|
|
||||||
border: 4px solid transparent;
|
|
||||||
z-index: 2;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: border-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 4px;
|
|
||||||
border-radius: calc($image-corner-radius - 4px);
|
|
||||||
border: 4px solid transparent;
|
|
||||||
z-index: 3;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: border-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
&::before {
|
|
||||||
border-color: $red-60;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-color: $grey-100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.placeholder {
|
|
||||||
background: $grey-90;
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
319
src/lib/components/Slideshow.svelte
Normal file
319
src/lib/components/Slideshow.svelte
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Lightbox from './Lightbox.svelte'
|
||||||
|
import TiltCard from './TiltCard.svelte'
|
||||||
|
|
||||||
|
interface SlideItem {
|
||||||
|
url: string
|
||||||
|
thumbnailUrl?: string
|
||||||
|
caption?: string
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
items = [],
|
||||||
|
alt = 'Image',
|
||||||
|
showThumbnails = true,
|
||||||
|
aspectRatio = '4/3',
|
||||||
|
maxThumbnails,
|
||||||
|
totalCount,
|
||||||
|
showMoreLink
|
||||||
|
}: {
|
||||||
|
items: SlideItem[]
|
||||||
|
alt?: string
|
||||||
|
showThumbnails?: boolean
|
||||||
|
aspectRatio?: string
|
||||||
|
maxThumbnails?: number
|
||||||
|
totalCount?: number
|
||||||
|
showMoreLink?: string
|
||||||
|
} = $props()
|
||||||
|
|
||||||
|
let selectedIndex = $state(0)
|
||||||
|
let lightboxOpen = $state(false)
|
||||||
|
let windowWidth = $state(0)
|
||||||
|
|
||||||
|
// Calculate columns based on breakpoints
|
||||||
|
const columnsPerRow = $derived(windowWidth <= 400 ? 3 : windowWidth <= 600 ? 4 : 6)
|
||||||
|
|
||||||
|
const showMoreThumbnail = $derived(maxThumbnails && totalCount && totalCount > maxThumbnails - 1)
|
||||||
|
|
||||||
|
// Determine how many thumbnails to show
|
||||||
|
const displayItems = $derived(
|
||||||
|
!maxThumbnails || !showMoreThumbnail
|
||||||
|
? items
|
||||||
|
: items.slice(0, maxThumbnails - 1) // Show actual thumbnails, leave last slot for "+N"
|
||||||
|
)
|
||||||
|
const remainingCount = $derived(
|
||||||
|
showMoreThumbnail ? (totalCount || items.length) - (maxThumbnails - 1) : 0
|
||||||
|
)
|
||||||
|
const totalSlots = $derived(
|
||||||
|
maxThumbnails
|
||||||
|
? maxThumbnails
|
||||||
|
: Math.ceil((displayItems.length + (showMoreThumbnail ? 1 : 0)) / columnsPerRow) * columnsPerRow
|
||||||
|
)
|
||||||
|
|
||||||
|
// Convert items to image URLs for lightbox
|
||||||
|
const lightboxImages = $derived(items.map(item => item.url))
|
||||||
|
|
||||||
|
const selectImage = (index: number) => {
|
||||||
|
selectedIndex = index
|
||||||
|
}
|
||||||
|
|
||||||
|
const openLightbox = (index?: number) => {
|
||||||
|
if (index !== undefined) {
|
||||||
|
selectedIndex = index
|
||||||
|
}
|
||||||
|
lightboxOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track window width for responsive columns
|
||||||
|
$effect(() => {
|
||||||
|
windowWidth = window.innerWidth
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
windowWidth = window.innerWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if items.length === 1}
|
||||||
|
<!-- Single image -->
|
||||||
|
<TiltCard>
|
||||||
|
<div class="single-image image-container" onclick={() => openLightbox()}>
|
||||||
|
<img src={items[0].url} alt={items[0].alt || alt} />
|
||||||
|
{#if items[0].caption}
|
||||||
|
<div class="image-caption">{items[0].caption}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</TiltCard>
|
||||||
|
{:else if items.length > 1}
|
||||||
|
<!-- Slideshow -->
|
||||||
|
<div class="slideshow">
|
||||||
|
<TiltCard>
|
||||||
|
<div class="main-image image-container" onclick={() => openLightbox()}>
|
||||||
|
<img
|
||||||
|
src={items[selectedIndex].url}
|
||||||
|
alt={items[selectedIndex].alt || `${alt} ${selectedIndex + 1}`}
|
||||||
|
/>
|
||||||
|
{#if items[selectedIndex].caption}
|
||||||
|
<div class="image-caption">{items[selectedIndex].caption}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</TiltCard>
|
||||||
|
|
||||||
|
{#if showThumbnails}
|
||||||
|
<div class="thumbnails">
|
||||||
|
{#each Array(totalSlots) as _, index}
|
||||||
|
{#if index < displayItems.length}
|
||||||
|
<button
|
||||||
|
class="thumbnail"
|
||||||
|
class:active={index === selectedIndex}
|
||||||
|
onclick={() => selectImage(index)}
|
||||||
|
aria-label="View image {index + 1}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={displayItems[index].thumbnailUrl || displayItems[index].url}
|
||||||
|
alt="{displayItems[index].alt || alt} thumbnail {index + 1}"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{:else if index === displayItems.length && showMoreThumbnail}
|
||||||
|
<a
|
||||||
|
href={showMoreLink}
|
||||||
|
class="thumbnail show-more"
|
||||||
|
aria-label="View all {totalCount || items.length} photos"
|
||||||
|
>
|
||||||
|
{#if items[displayItems.length]}
|
||||||
|
<img
|
||||||
|
src={items[displayItems.length].thumbnailUrl || items[displayItems.length].url}
|
||||||
|
alt="View all photos"
|
||||||
|
class="blurred-bg"
|
||||||
|
/>
|
||||||
|
{:else if items[items.length - 1]}
|
||||||
|
<img
|
||||||
|
src={items[items.length - 1].thumbnailUrl || items[items.length - 1].url}
|
||||||
|
alt="View all photos"
|
||||||
|
class="blurred-bg"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="show-more-overlay">
|
||||||
|
+{remainingCount}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<div class="thumbnail placeholder" aria-hidden="true"></div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Lightbox images={lightboxImages} bind:selectedIndex bind:isOpen={lightboxOpen} alt={alt} />
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.image-container {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid $red-60;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-image,
|
||||||
|
.main-image {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: v-bind(aspectRatio);
|
||||||
|
border-radius: $image-corner-radius;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
// Force GPU acceleration and proper clipping
|
||||||
|
transform: translateZ(0);
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-caption {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||||
|
color: white;
|
||||||
|
padding: $unit-3x $unit-2x $unit-2x;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideshow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnails {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: $unit-2x;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: $image-corner-radius;
|
||||||
|
overflow: hidden;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: $image-corner-radius;
|
||||||
|
border: 4px solid transparent;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 4px;
|
||||||
|
border-radius: calc($image-corner-radius - 4px);
|
||||||
|
border: 4px solid transparent;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
&::before {
|
||||||
|
border-color: $red-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border-color: $grey-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.placeholder {
|
||||||
|
background: $grey-90;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.show-more {
|
||||||
|
position: relative;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
.blurred-bg {
|
||||||
|
filter: blur(3px);
|
||||||
|
transform: scale(1.1); // Slightly scale to hide blur edges
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: $image-corner-radius;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.show-more-overlay {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import UniverseIcon from '$icons/universe.svg'
|
import UniverseIcon from '$icons/universe.svg'
|
||||||
|
import Slideshow from './Slideshow.svelte'
|
||||||
import type { UniverseItem } from '../../routes/api/universe/+server'
|
import type { UniverseItem } from '../../routes/api/universe/+server'
|
||||||
|
|
||||||
let { album }: { album: UniverseItem } = $props()
|
let { album }: { album: UniverseItem } = $props()
|
||||||
|
|
@ -12,6 +13,25 @@
|
||||||
year: 'numeric'
|
year: 'numeric'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert photos to slideshow items
|
||||||
|
const slideshowItems = $derived(
|
||||||
|
album.photos && album.photos.length > 0
|
||||||
|
? album.photos.map(photo => ({
|
||||||
|
url: photo.url,
|
||||||
|
thumbnailUrl: photo.thumbnailUrl,
|
||||||
|
caption: photo.caption,
|
||||||
|
alt: photo.caption || album.title
|
||||||
|
}))
|
||||||
|
: album.coverPhoto
|
||||||
|
? [{
|
||||||
|
url: album.coverPhoto.url,
|
||||||
|
thumbnailUrl: album.coverPhoto.thumbnailUrl,
|
||||||
|
caption: album.coverPhoto.caption,
|
||||||
|
alt: album.coverPhoto.caption || album.title
|
||||||
|
}]
|
||||||
|
: []
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<article class="universe-album-card">
|
<article class="universe-album-card">
|
||||||
|
|
@ -23,16 +43,17 @@
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if album.coverPhoto}
|
{#if slideshowItems.length > 0}
|
||||||
<div class="album-cover">
|
<div class="album-slideshow">
|
||||||
<img
|
<Slideshow
|
||||||
src={album.coverPhoto.thumbnailUrl || album.coverPhoto.url}
|
items={slideshowItems}
|
||||||
alt={album.coverPhoto.caption || album.title}
|
alt={album.title}
|
||||||
loading="lazy"
|
aspectRatio="3/2"
|
||||||
|
showThumbnails={slideshowItems.length > 1}
|
||||||
|
maxThumbnails={6}
|
||||||
|
totalCount={album.photosCount}
|
||||||
|
showMoreLink="/photos/{album.slug}"
|
||||||
/>
|
/>
|
||||||
<div class="photo-count-overlay">
|
|
||||||
{album.photosCount || 0} photo{(album.photosCount || 0) !== 1 ? 's' : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
@ -108,32 +129,10 @@
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.album-cover {
|
.album-slideshow {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
|
||||||
border-radius: $unit;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: $unit-3x;
|
margin-bottom: $unit-3x;
|
||||||
background: $grey-95;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo-count-overlay {
|
|
||||||
position: absolute;
|
|
||||||
bottom: $unit;
|
|
||||||
right: $unit;
|
|
||||||
background: rgba(0, 0, 0, 0.8);
|
|
||||||
color: white;
|
|
||||||
padding: $unit-half $unit-2x;
|
|
||||||
border-radius: 50px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.album-info {
|
.album-info {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,18 @@ export const GET: RequestHandler = async (event) => {
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
title: true,
|
title: true,
|
||||||
description: true
|
description: true,
|
||||||
|
photos: {
|
||||||
|
orderBy: { displayOrder: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
url: true,
|
||||||
|
thumbnailUrl: true,
|
||||||
|
caption: true,
|
||||||
|
width: true,
|
||||||
|
height: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
photo: {
|
photo: {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ export interface UniverseItem {
|
||||||
date?: string
|
date?: string
|
||||||
photosCount?: number
|
photosCount?: number
|
||||||
coverPhoto?: any
|
coverPhoto?: any
|
||||||
|
photos?: any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/universe - Get mixed feed of published posts and albums
|
// GET /api/universe - Get mixed feed of published posts and albums
|
||||||
|
|
@ -74,13 +75,15 @@ export const GET: RequestHandler = async (event) => {
|
||||||
select: { photos: true }
|
select: { photos: true }
|
||||||
},
|
},
|
||||||
photos: {
|
photos: {
|
||||||
take: 1,
|
take: 6, // Fetch enough for 5 thumbnails + 1 background
|
||||||
orderBy: { displayOrder: 'asc' },
|
orderBy: { displayOrder: 'asc' },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
url: true,
|
url: true,
|
||||||
thumbnailUrl: true,
|
thumbnailUrl: true,
|
||||||
caption: true
|
caption: true,
|
||||||
|
width: true,
|
||||||
|
height: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -114,7 +117,8 @@ export const GET: RequestHandler = async (event) => {
|
||||||
location: album.location || undefined,
|
location: album.location || undefined,
|
||||||
date: album.date?.toISOString(),
|
date: album.date?.toISOString(),
|
||||||
photosCount: album._count.photos,
|
photosCount: album._count.photos,
|
||||||
coverPhoto: album.photos[0] || null,
|
coverPhoto: album.photos[0] || null, // Keep for backward compatibility
|
||||||
|
photos: album.photos, // Add all photos for slideshow
|
||||||
publishedAt: album.createdAt.toISOString(), // Albums use createdAt as publishedAt
|
publishedAt: album.createdAt.toISOString(), // Albums use createdAt as publishedAt
|
||||||
createdAt: album.createdAt.toISOString()
|
createdAt: album.createdAt.toISOString()
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue