thats-me/frontend/_src/pages/EntryDetailPage.vue
2026-04-22 12:57:10 +02:00

1224 lines
No EOL
29 KiB
Vue

<template>
<q-page class="entry-detail-page">
<div class="entry-container">
<div class="entry-header">
<!-- Image Slider -->
<div v-if="allImages.length > 0" class="image-slider-container">
<q-carousel
v-model="currentSlide"
transition-prev="slide-right"
transition-next="slide-left"
swipeable
animated
control-color="white"
navigation
arrows
class="image-carousel"
>
<q-carousel-slide
v-for="(image, index) in allImages"
:key="index"
:name="index"
class="carousel-slide"
>
<div class="image-container">
<q-img
:src="image.url"
class="slide-image"
fit="cover"
/>
<div v-if="image.caption" class="image-caption">
{{ image.caption }}
</div>
<div v-if="index === 0" class="key-image-badge">
<q-icon name="star" size="sm" />
</div>
</div>
</q-carousel-slide>
</q-carousel>
</div>
<!-- Entry Title and Meta -->
<div class="entry-meta">
<h1 class="entry-title">{{ entry.title }}</h1>
<div class="entry-subtitle" v-if="entry.subtitle">{{ entry.subtitle }}</div>
<div class="entry-date-location">
<div class="entry-date">
<q-icon name="event" size="sm" class="q-mr-sm" />
{{ formatDate(entry.date) }}
<span v-if="entry.time" class="entry-time">{{ entry.time }}</span>
</div>
<div v-if="entry.location" class="entry-location">
<q-icon name="place" size="sm" class="q-mr-sm" />
{{ entry.location }}
</div>
</div>
<!-- Emotional Level Indicator -->
<div class="emotional-level">
<div class="level-label">Emotional Level:</div>
<div class="level-indicator" :class="getEmotionalLevelClass(entry.level)">
{{ getEmotionalLevelText(entry.level) }}
</div>
</div>
</div>
</div>
<!-- Entry Description -->
<div v-if="entry.description" class="entry-description">
<p class="description-text">{{ entry.description }}</p>
</div>
<!-- Video Recordings -->
<div v-if="entry.videoRecordings && entry.videoRecordings.length > 0" class="video-section">
<div class="video-slider-container">
<q-carousel
v-model="currentVideoSlide"
transition-prev="slide-right"
transition-next="slide-left"
swipeable
animated
control-color="grey-8"
navigation
arrows
class="video-carousel"
>
<q-carousel-slide
v-for="(video, index) in entry.videoRecordings"
:key="index"
:name="index"
class="video-carousel-slide"
>
<div class="video-container">
<div class="video-thumbnail" @click="openVideoLightbox(video, index)">
<video
:src="video.url"
:poster="getVideoPoster(video)"
class="video-preview"
muted
preload="metadata"
/>
<div class="video-play-overlay">
<q-icon name="play_circle_filled" size="4rem" color="white" />
</div>
</div>
</div>
</q-carousel-slide>
</q-carousel>
</div>
</div>
<!-- Additional Images Gallery -->
<div v-if="additionalImagesGallery.length > 0" class="additional-images-section">
<div class="additional-images-grid">
<div
v-for="(image, index) in additionalImagesGallery"
:key="index"
class="image-tile"
@click="openImageLightbox(image, index)"
>
<q-img
:src="image.thumbnail"
class="tile-image"
fit="cover"
loading="lazy"
>
<template v-slot:error>
<div class="absolute-full flex flex-center bg-grey-3 text-grey-7">
<q-icon name="broken_image" size="24px" />
</div>
</template>
</q-img>
<!-- Favorite Star Badge -->
<div v-if="image.isFavorite" class="favorite-star-badge">
<q-icon name="star" size="20px" color="amber" />
</div>
<div class="image-tile-overlay">
<q-icon name="zoom_in" size="24px" color="white" />
</div>
</div>
</div>
</div>
<!-- Audio Recordings -->
<div v-if="entry.audioRecordings && entry.audioRecordings.length > 0" class="audio-section">
<div class="audio-list">
<div
v-for="(audio, index) in entry.audioRecordings"
:key="index"
class="audio-item"
>
<q-icon name="audiotrack" size="md" color="primary" class="q-mr-md" />
<div class="audio-info">
<div class="audio-name">{{ audio.name || `Recording ${index + 1}` }}</div>
<audio :src="audio.url" controls class="audio-player">
Your browser does not support the audio element.
</audio>
</div>
</div>
</div>
</div>
<!-- Related Persons -->
<div v-if="entry.relatedPersons && entry.relatedPersons.length > 0" class="persons-section">
<h3>Related Persons</h3>
<div class="persons-list">
<div
v-for="person in entry.relatedPersons"
:key="person.id"
class="person-item"
>
<div class="person-avatar">
<q-img
v-if="person.avatar"
:src="person.avatar"
class="avatar-image"
/>
<div v-else class="avatar-initials">
{{ getInitials(person.name) }}
</div>
</div>
<div class="person-info">
<div class="person-name">{{ person.name }}</div>
<div v-if="person.relation" class="person-relation">{{ person.relation }}</div>
</div>
</div>
</div>
</div>
<!-- Categories -->
<div v-if="entry.categories && entry.categories.length > 0" class="categories-section">
<h3>Categories</h3>
<div class="categories-list">
<q-chip
v-for="category in entry.categories"
:key="category.id"
:icon="category.icon"
color="primary"
text-color="white"
class="category-chip"
>
{{ category.name }}
</q-chip>
</div>
</div>
<!-- Tags -->
<div v-if="entry.tags && entry.tags.length > 0" class="tags-section">
<h3>Tags</h3>
<div class="tags-list">
<q-chip
v-for="tag in entry.tags"
:key="tag.id"
:icon="tag.icon"
color="secondary"
text-color="white"
class="tag-chip"
>
{{ tag.name }}
</q-chip>
</div>
</div>
<!-- Edit Entry Button -->
<div class="edit-entry-section">
<q-btn
color="primary"
icon="edit"
label="Edit Entry"
size="lg"
class="edit-entry-btn"
@click="editEntry"
/>
</div>
</div>
<!-- Video Lightbox Dialog -->
<q-dialog v-model="showVideoLightbox" maximized>
<q-card class="video-lightbox-card">
<q-card-section class="video-lightbox-header">
<q-btn icon="close" flat round dense v-close-popup @click="pauseCurrentVideo" class="close-btn" />
</q-card-section>
<q-card-section class="video-lightbox-content">
<video
v-if="currentVideo"
ref="lightboxVideo"
:src="currentVideo.url"
:poster="getVideoPoster(currentVideo)"
controls
class="lightbox-video"
@loadedmetadata="onVideoLoaded"
>
Your browser does not support the video element.
</video>
</q-card-section>
<q-card-actions v-if="entry.videoRecordings && entry.videoRecordings.length > 1" align="center">
<q-btn
icon="chevron_left"
@click="previousVideo"
:disable="currentVideoIndex === 0"
flat
round
/>
<span class="video-counter">
{{ currentVideoIndex + 1 }} / {{ entry.videoRecordings.length }}
</span>
<q-btn
icon="chevron_right"
@click="nextVideo"
:disable="currentVideoIndex === entry.videoRecordings.length - 1"
flat
round
/>
</q-card-actions>
</q-card>
</q-dialog>
<!-- Image Lightbox Dialog -->
<q-dialog v-model="showImageLightbox" maximized>
<q-card class="image-lightbox-card">
<q-card-section class="image-lightbox-header">
<q-btn icon="close" flat round dense v-close-popup class="close-btn" />
</q-card-section>
<q-card-section class="image-lightbox-content">
<q-img
v-if="currentLightboxImage"
:src="currentLightboxImage.fullSize"
class="lightbox-image"
fit="contain"
>
<template v-slot:error>
<div class="absolute-full flex flex-center bg-grey-8 text-white">
<div class="text-center">
<q-icon name="broken_image" size="48px" class="q-mb-md" />
<div>Image could not be loaded</div>
</div>
</div>
</template>
</q-img>
</q-card-section>
<q-card-actions v-if="additionalImagesGallery.length > 1" align="center" class="image-lightbox-actions">
<q-btn
icon="chevron_left"
@click="previousImage"
:disable="currentImageIndex === 0"
flat
round
color="white"
/>
<span class="image-counter">
{{ currentImageIndex + 1 }} / {{ additionalImagesGallery.length }}
</span>
<q-btn
icon="chevron_right"
@click="nextImage"
:disable="currentImageIndex === additionalImagesGallery.length - 1"
flat
round
color="white"
/>
</q-card-actions>
</q-card>
</q-dialog>
</q-page>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
export default {
name: 'EntryDetailPage',
props: {
entryData: {
type: Object,
default: null
}
},
setup(props) {
const route = useRoute()
// Carousel state
const currentSlide = ref(0)
const currentVideoSlide = ref(0)
// Video lightbox state
const showVideoLightbox = ref(false)
const currentVideoIndex = ref(0)
const lightboxVideo = ref(null)
// Image lightbox state
const showImageLightbox = ref(false)
const currentImageIndex = ref(0)
// Define available gallery images from thumbs folder
const galleryImageNames = [
'aaron-huber-RLs8LZcONCA-unsplash.jpg',
'andrew-bui-z7rzbFHXym0-unsplash.jpg',
'becca-tapert--A_Sx8GrRWg-unsplash.jpg',
'fuu-j-r2nJPbEYuSQ-unsplash.jpg',
'ian-dooley-hpTH5b6mo2s-unsplash.jpg',
'ishan-seefromthesky-TobZaa8ZwI4-unsplash.jpg',
'javier-allegue-barros-55bVEzGVnzY-unsplash.jpg',
'jay-antol-Xbf_4e7YDII-unsplash.jpg',
'jorgen-hendriksen-8qg-hy6VbYE-unsplash.jpg',
'mohamed-nohassi-odxB5oIG_iA-unsplash.jpg',
'saad-chaudhry-cYpqYxGeqts-unsplash.jpg',
'taryn-kaahanui-J5b23iaAHis-unsplash.jpg',
'tirza-van-dijk-hbwdmqcmP6k-unsplash.jpg',
]
// Mark specific images as favorites (indices 2, 5, and 9)
const favoriteImageIndices = [2, 5, 9]
// Sample entry data (for testing) - Updated with poster paths
const sampleEntry = {
id: 1,
title: "Beginn des neuen Abenteuers",
subtitle: "Ein wichtiger Meilenstein in meinem Leben",
date: "2024-10-01",
time: "14:30",
location: "München, Deutschland",
level: 2,
keyImage: "/images/familie2.png",
description: "Dies war ein wirklich wichtiger Tag für mich. Nach langer Planung und Vorbereitung konnte ich endlich mein neues Abenteuer beginnen. Es war aufregend und gleichzeitig etwas beängstigenden, aber ich wusste, dass es der richtige Schritt war. Die Erfahrungen, die ich an diesem Tag gemacht habe, werden mich noch lange begleiten.",
additionalImages: [
{ url: "/images/familie2.png", caption: "Familie beim Start" },
{ url: "/images/see.png", caption: "Der schöne Ort" },
{ url: "/images/feier.png", caption: "Kleiner Umtrunk danach" }
],
audioRecordings: [
{ name: "Gedanken zum Tag", url: "/audio/sample.mp3" },
{ name: "Gespräch mit Familie", url: "/audio/sample2.mp3" }
],
videoRecordings: [
{
name: "UHD Nature Video",
url: "/videos/3191901-uhd_3840_2160_25fps.mp4",
poster: "/videos/thumbs/3191901-uhd_3840_2160_25fps_thumb.jpg"
},
{
name: "HD Landscape Video",
url: "/videos/3326928-hd_1920_1080_24fps.mp4",
poster: "/videos/thumbs/3326928-hd_1920_1080_24fps_thumb.jpg"
}
],
relatedPersons: [
{ id: 1, name: "Maria Schmidt", relation: "Freundin", avatar: null },
{ id: 2, name: "Thomas Müller", relation: "Bruder", avatar: null }
],
categories: [
{ id: 1, name: "Familie", icon: "family_restroom" },
{ id: 2, name: "Abenteuer", icon: "explore" }
],
tags: [
{ id: 1, name: "Aufregend", icon: "emoji_emotions" },
{ id: 2, name: "Neuanfang", icon: "new_releases" },
{ id: 3, name: "Wichtig", icon: "star" }
]
}
// Use prop data or sample data
const entry = computed(() => props.entryData || sampleEntry)
// Combine key image and additional images for the slider
const allImages = computed(() => {
const images = []
// Add key image first if it exists
if (entry.value.keyImage) {
images.push({
url: entry.value.keyImage,
caption: 'Key Image'
})
}
// Add additional images
if (entry.value.additionalImages) {
images.push(...entry.value.additionalImages)
}
return images
})
// Additional images gallery - Updated to include favorite status
const additionalImagesGallery = computed(() => {
return galleryImageNames.map((imageName, index) => ({
thumbnail: `/images/thumbs/${imageName}`,
fullSize: `/images/gallery/${imageName}`,
name: imageName,
isFavorite: favoriteImageIndices.includes(index)
}))
})
// Current lightbox image
const currentLightboxImage = computed(() =>
additionalImagesGallery.value[currentImageIndex.value]
)
// Current video for lightbox
const currentVideo = computed(() =>
entry.value.videoRecordings?.[currentVideoIndex.value]
)
// Methods
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
const getEmotionalLevelClass = (level) => {
if (level >= 2) return 'level-very-positive'
if (level >= 1) return 'level-positive'
if (level >= -1) return 'level-neutral'
if (level >= -2) return 'level-negative'
return 'level-very-negative'
}
const getEmotionalLevelText = (level) => {
if (level >= 2) return 'Very Positive'
if (level >= 1) return 'Positive'
if (level >= -1) return 'Neutral'
if (level >= -2) return 'Negative'
return 'Very Negative'
}
const getInitials = (name) => {
return name
.split(' ')
.map(word => word.charAt(0).toUpperCase())
.join('')
.substring(0, 2)
}
// Video lightbox methods
const openVideoLightbox = (video, index) => {
currentVideoIndex.value = index
showVideoLightbox.value = true
}
const pauseCurrentVideo = () => {
if (lightboxVideo.value) {
lightboxVideo.value.pause()
}
}
const previousVideo = () => {
if (currentVideoIndex.value > 0) {
pauseCurrentVideo()
currentVideoIndex.value--
}
}
const nextVideo = () => {
if (currentVideoIndex.value < entry.value.videoRecordings.length - 1) {
pauseCurrentVideo()
currentVideoIndex.value++
}
}
const onVideoLoaded = () => {
// Video metadata loaded, can add additional logic here if needed
}
// Image lightbox methods
const openImageLightbox = (image, index) => {
currentImageIndex.value = index
showImageLightbox.value = true
}
const previousImage = () => {
if (currentImageIndex.value > 0) {
currentImageIndex.value--
}
}
const nextImage = () => {
if (currentImageIndex.value < additionalImagesGallery.value.length - 1) {
currentImageIndex.value++
}
}
const editEntry = () => {
// TODO: Navigate to edit page or open edit modal
console.log('Edit entry:', entry.value.id)
// Example: router.push(`/edit/${entry.value.id}`)
}
// New method to get video poster/thumbnail
const getVideoPoster = (video) => {
// If video has a poster property, use it
if (video.poster) {
return video.poster
}
// Otherwise, try to generate a thumbnail path based on video filename
if (video.url) {
const videoName = video.url.split('/').pop().split('.')[0]
return `/videos/thumbs/${videoName}_thumb.jpg`
}
// Fallback to a default poster image
return '/images/video-placeholder.png'
}
onMounted(() => {
// If accessing via route, you might want to load data based on route params
const entryId = route.params.id
if (entryId && !props.entryData) {
// TODO: Load entry data from API/store based on entryId
console.log('Loading entry with ID:', entryId)
}
})
return {
entry,
allImages,
additionalImagesGallery,
favoriteImageIndices,
currentSlide,
currentVideoSlide,
showVideoLightbox,
currentVideoIndex,
currentVideo,
lightboxVideo,
showImageLightbox,
currentImageIndex,
currentLightboxImage,
formatDate,
getEmotionalLevelClass,
getEmotionalLevelText,
getInitials,
getVideoPoster,
openVideoLightbox,
pauseCurrentVideo,
previousVideo,
nextVideo,
onVideoLoaded,
openImageLightbox,
previousImage,
nextImage,
editEntry
}
}
}
</script>
<style scoped>
.entry-detail-page {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
}
.entry-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.entry-header {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 24px;
}
.image-slider-container {
width: 100%;
aspect-ratio: 3/2;
overflow: hidden;
}
.image-carousel {
height: 100%;
width: 100%;
}
.image-carousel .q-carousel__arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
width: 40px;
height: 40px;
}
.image-carousel .q-carousel__prev-arrow {
left: 16px;
}
.image-carousel .q-carousel__next-arrow {
right: 16px;
}
.carousel-slide {
padding: 0;
}
.image-container {
position: relative;
width: 100%;
height: 100%;
}
.slide-image {
width: 100%;
height: 100%;
}
.image-caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
color: white;
padding: 20px 16px 16px;
font-size: 0.9rem;
font-weight: 500;
}
.key-image-badge {
position: absolute;
top: 12px;
left: 12px;
background: rgba(255, 215, 0, 0.9);
color: #2c3e50;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
}
.entry-meta {
padding: 24px;
}
.entry-title {
font-size: 2rem;
font-weight: 700;
color: #2c3e50;
margin: 0 0 8px 0;
line-height: 1.2;
}
.entry-subtitle {
font-size: 1.2rem;
color: #7f8c8d;
margin-bottom: 16px;
font-style: italic;
}
.entry-date-location {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.entry-date, .entry-location {
display: flex;
align-items: center;
color: #5a6c7d;
font-size: 1rem;
}
.entry-time {
margin-left: 8px;
font-weight: 500;
}
.emotional-level {
display: flex;
align-items: center;
gap: 12px;
}
.level-label {
font-weight: 500;
color: #2c3e50;
}
.level-indicator {
padding: 6px 12px;
border-radius: 20px;
font-weight: 600;
font-size: 0.9rem;
}
.level-very-positive {
background: #e8f5e8;
color: #27ae60;
}
.level-positive {
background: #f0f8e8;
color: #7cb342;
}
.level-neutral {
background: #f5f5f5;
color: #5a6c7d;
}
.level-negative {
background: #fff3e0;
color: #ef6c00;
}
.level-very-negative {
background: #ffebee;
color: #e53935;
}
.entry-description,
.audio-section,
.persons-section,
.categories-section,
.tags-section {
background: white;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
padding: 24px;
margin-bottom: 24px;
}
.entry-description h3,
.audio-section h3,
.video-section h3,
.persons-section h3,
.categories-section h3,
.tags-section h3 {
color: #2c3e50;
font-size: 1.4rem;
font-weight: 600;
margin: 0 0 16px 0;
border-bottom: 2px solid #3498db;
padding-bottom: 8px;
}
.description-text {
font-size: 1.1rem;
line-height: 1.6;
color: #34495e;
margin: 0;
}
.audio-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.audio-item {
display: flex;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-radius: 12px;
}
.audio-info {
flex: 1;
}
.audio-name {
font-weight: 500;
color: #2c3e50;
margin-bottom: 8px;
}
.audio-player {
width: 100%;
height: 40px;
}
/* Video Slider Styles */
.video-section {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 24px;
}
.video-slider-container {
width: 100%;
aspect-ratio: 16/9;
overflow: hidden;
}
.video-carousel {
height: 100%;
width: 100%;
}
.video-carousel .q-carousel__arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
width: 40px;
height: 40px;
color: white !important;
}
.video-carousel .q-carousel__prev-arrow {
left: 16px;
}
.video-carousel .q-carousel__next-arrow {
right: 16px;
}
.video-carousel .q-carousel__navigation {
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
}
.video-carousel .q-carousel__navigation .q-btn {
background: rgba(255, 255, 255, 0.8);
color: #424242 !important;
margin: 0 4px;
min-width: 12px;
min-height: 12px;
border-radius: 50%;
}
.video-carousel-slide {
padding: 0;
}
.video-container {
position: relative;
width: 100%;
height: 100%;
}
.video-thumbnail {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
overflow: hidden;
}
.video-preview {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-play-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
padding: 16px;
transition: all 0.3s ease;
}
.video-play-overlay:hover {
background: rgba(0, 0, 0, 0.8);
transform: translate(-50%, -50%) scale(1.1);
}
/* Video Lightbox Styles */
.video-lightbox-card {
background: #000;
}
.video-lightbox-header {
background: transparent;
color: white;
display: flex;
justify-content: flex-end;
align-items: flex-start;
padding: 16px 24px;
position: absolute;
top: 0;
right: 0;
z-index: 1001;
}
.close-btn {
background: rgba(0, 0, 0, 0.5);
color: white;
}
.video-lightbox-content {
background: #000;
display: flex;
align-items: center;
justify-content: center;
min-height: 60vh;
padding: 20px;
}
.lightbox-video {
max-width: 100%;
max-height: 80vh;
border-radius: 8px;
}
.video-counter {
color: white;
font-weight: 500;
margin: 0 16px;
}
.persons-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.person-item {
display: flex;
align-items: center;
background: #f8f9fa;
padding: 12px;
border-radius: 12px;
min-width: 200px;
}
.person-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
margin-right: 12px;
overflow: hidden;
background: linear-gradient(135deg, #3498db, #2980b9);
display: flex;
align-items: center;
justify-content: center;
}
.avatar-image {
width: 100%;
height: 100%;
}
.avatar-initials {
color: white;
font-weight: 600;
font-size: 16px;
}
.person-name {
font-weight: 500;
color: #2c3e50;
}
.person-relation {
font-size: 0.9rem;
color: #7f8c8d;
}
.categories-list, .tags-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.category-chip, .tag-chip {
font-size: 0.9rem;
}
.edit-entry-section {
display: flex;
justify-content: center;
margin-top: 32px;
margin-bottom: 16px;
}
.edit-entry-btn {
min-width: 200px;
padding: 12px 24px;
font-weight: 600;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
transition: all 0.3s ease;
}
.edit-entry-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
}
/* Additional Images Gallery Styles */
.additional-images-section {
background: white;
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
}
.additional-images-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
}
.image-tile {
position: relative;
aspect-ratio: 1;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
}
.image-tile:hover {
transform: translateY(-4px);
}
.tile-image {
width: 100%;
height: 100%;
border-radius: 12px;
}
.image-tile-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.image-tile:hover .image-tile-overlay {
opacity: 1;
}
/* Image Lightbox Styles */
.image-lightbox-card {
background: #000;
}
.image-lightbox-header {
background: transparent;
color: white;
display: flex;
justify-content: flex-end;
align-items: flex-start;
padding: 16px 24px;
position: absolute;
top: 0;
right: 0;
z-index: 1001;
}
.image-lightbox-content {
background: #000;
display: flex;
align-items: center;
justify-content: center;
min-height: 80vh;
padding: 20px;
}
.lightbox-image {
max-width: 100%;
max-height: 80vh;
border-radius: 8px;
}
.image-lightbox-actions {
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 16px;
}
.image-counter {
color: white;
font-weight: 500;
margin: 0 16px;
}
/* Responsive */
@media (max-width: 600px) {
.entry-container {
padding: 12px;
}
.entry-meta {
padding: 16px;
}
.entry-title {
font-size: 1.6rem;
}
.entry-date-location {
flex-direction: column;
}
.persons-list {
flex-direction: column;
}
.person-item {
min-width: auto;
}
.additional-images-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
}
.additional-images-section {
padding: 16px;
}
}
/* Favorite Star Badge */
.favorite-star-badge {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
transition: all 0.3s ease;
}
.image-tile:hover .favorite-star-badge {
background: rgba(255, 255, 255, 1);
transform: scale(1.1);
}
</style>