292 lines
9.1 KiB
HTML
292 lines
9.1 KiB
HTML
|
<!DOCTYPE html>
|
||
|
<html lang="en" data-theme="dark">
|
||
|
<head>
|
||
|
<meta charset="UTF-8">
|
||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
<title>Image Grid</title>
|
||
|
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
|
||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||
|
<style>
|
||
|
body, html {
|
||
|
margin: 0;
|
||
|
padding: 0;
|
||
|
min-height: 100vh;
|
||
|
}
|
||
|
.container {
|
||
|
max-width: 100% !important;
|
||
|
padding: 0.5rem !important;
|
||
|
margin: 0 !important;
|
||
|
}
|
||
|
.grid {
|
||
|
display: grid;
|
||
|
grid-template-columns: repeat(4, 1fr);
|
||
|
gap: 0.5rem;
|
||
|
}
|
||
|
.grid-item {
|
||
|
position: relative;
|
||
|
cursor: pointer;
|
||
|
overflow: hidden;
|
||
|
border-radius: 4px;
|
||
|
}
|
||
|
.grid img {
|
||
|
width: 100%;
|
||
|
height: calc(33.33vh - 1rem);
|
||
|
object-fit: cover;
|
||
|
margin: 0;
|
||
|
transition: transform 0.3s ease;
|
||
|
}
|
||
|
.grid-item:hover img {
|
||
|
transform: scale(1.05);
|
||
|
}
|
||
|
|
||
|
/* Modified image overlay styles */
|
||
|
.image-overlay {
|
||
|
position: absolute;
|
||
|
bottom: 0;
|
||
|
left: 0;
|
||
|
right: 0;
|
||
|
padding: 1rem;
|
||
|
text-align: center;
|
||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||
|
}
|
||
|
.image-overlay h3 {
|
||
|
margin: 0;
|
||
|
font-size: 1rem;
|
||
|
color: white;
|
||
|
margin-bottom: 0.5rem;
|
||
|
}
|
||
|
.image-overlay p {
|
||
|
margin: 0;
|
||
|
font-size: 0.8rem;
|
||
|
color: white;
|
||
|
line-height: 1.2;
|
||
|
}
|
||
|
|
||
|
/* Modal styles */
|
||
|
.modal {
|
||
|
position: fixed;
|
||
|
top: 0;
|
||
|
left: 0;
|
||
|
width: 100%;
|
||
|
height: 100%;
|
||
|
background-color: rgba(0, 0, 0, 0.95);
|
||
|
z-index: 1000;
|
||
|
opacity: 0;
|
||
|
visibility: hidden;
|
||
|
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||
|
}
|
||
|
.modal.active {
|
||
|
opacity: 1;
|
||
|
visibility: visible;
|
||
|
}
|
||
|
.modal-content {
|
||
|
position: absolute;
|
||
|
top: 50%;
|
||
|
left: 50%;
|
||
|
transform: translate(-50%, -50%);
|
||
|
width: 90%;
|
||
|
height: 90%;
|
||
|
display: flex;
|
||
|
justify-content: center;
|
||
|
align-items: center;
|
||
|
}
|
||
|
.modal img {
|
||
|
max-width: 90%;
|
||
|
max-height: 90vh;
|
||
|
object-fit: contain;
|
||
|
animation: zoomIn 0.3s ease;
|
||
|
}
|
||
|
.close-btn {
|
||
|
position: absolute;
|
||
|
top: 20px;
|
||
|
right: 30px;
|
||
|
color: #fff;
|
||
|
font-size: 40px;
|
||
|
font-weight: bold;
|
||
|
cursor: pointer;
|
||
|
z-index: 1002;
|
||
|
transition: transform 0.3s ease;
|
||
|
}
|
||
|
.close-btn:hover {
|
||
|
transform: scale(1.1);
|
||
|
}
|
||
|
|
||
|
/* Enhanced navigation buttons */
|
||
|
.nav-button {
|
||
|
position: fixed;
|
||
|
top: 50%;
|
||
|
transform: translateY(-50%);
|
||
|
background: rgba(255, 255, 255, 0.1);
|
||
|
border: none;
|
||
|
width: 60px;
|
||
|
height: 60px;
|
||
|
display: flex;
|
||
|
align-items: center;
|
||
|
justify-content: center;
|
||
|
color: white;
|
||
|
cursor: pointer;
|
||
|
border-radius: 50%;
|
||
|
z-index: 1002;
|
||
|
transition: all 0.3s ease;
|
||
|
}
|
||
|
|
||
|
.nav-button:hover {
|
||
|
background: rgba(255, 255, 255, 0.2);
|
||
|
transform: translateY(-50%) scale(1.1);
|
||
|
}
|
||
|
|
||
|
.nav-button.prev {
|
||
|
left: 30px;
|
||
|
}
|
||
|
|
||
|
.nav-button.next {
|
||
|
right: 30px;
|
||
|
}
|
||
|
|
||
|
.nav-button i {
|
||
|
font-size: 28px;
|
||
|
}
|
||
|
|
||
|
@keyframes zoomIn {
|
||
|
from {
|
||
|
opacity: 0;
|
||
|
transform: scale(0.3);
|
||
|
}
|
||
|
to {
|
||
|
opacity: 1;
|
||
|
transform: scale(1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@media (max-width: 768px) {
|
||
|
.grid {
|
||
|
grid-template-columns: repeat(2, 1fr);
|
||
|
}
|
||
|
.grid img {
|
||
|
height: calc(25vh - 1rem);
|
||
|
}
|
||
|
.nav-button.prev { left: 10px; }
|
||
|
.nav-button.next { right: 10px; }
|
||
|
.nav-button {
|
||
|
width: 50px;
|
||
|
height: 50px;
|
||
|
}
|
||
|
}
|
||
|
@media (max-width: 480px) {
|
||
|
.grid {
|
||
|
grid-template-columns: 1fr;
|
||
|
}
|
||
|
.grid img {
|
||
|
height: calc(20vh - 1rem);
|
||
|
}
|
||
|
}
|
||
|
</style>
|
||
|
</head>
|
||
|
<body>
|
||
|
<main class="container" x-data="imageGallery()">
|
||
|
<div class="grid">
|
||
|
<template x-for="image in displayedImages" :key="image.uniqueId">
|
||
|
<div class="grid-item" @click="openModal(image)">
|
||
|
<img :src="image.url" :alt="image.alt">
|
||
|
<template x-if="image.title && image.description">
|
||
|
<div class="image-overlay">
|
||
|
<h3 x-text="image.title"></h3>
|
||
|
<p x-text="image.description"></p>
|
||
|
</div>
|
||
|
</template>
|
||
|
</div>
|
||
|
</template>
|
||
|
</div>
|
||
|
|
||
|
<!-- Modal -->
|
||
|
<div id="imageModal"
|
||
|
class="modal"
|
||
|
:class="{ 'active': modalOpen }"
|
||
|
@click.self="closeModal()"
|
||
|
@keydown.window.escape="closeModal()"
|
||
|
@keydown.window.arrow-right="nextImage()"
|
||
|
@keydown.window.arrow-left="previousImage()">
|
||
|
|
||
|
<span class="close-btn" @click="closeModal()">×</span>
|
||
|
|
||
|
<div class="modal-content">
|
||
|
<template x-if="currentImage">
|
||
|
<img :src="currentImage.url"
|
||
|
:alt="currentImage.alt"
|
||
|
style="transition: opacity 0.3s ease"
|
||
|
x-transition:enter="transition ease-out duration-300"
|
||
|
x-transition:enter-start="opacity-0 transform scale-90"
|
||
|
x-transition:enter-end="opacity-100 transform scale-100">
|
||
|
</template>
|
||
|
</div>
|
||
|
|
||
|
<button class="nav-button prev" @click.stop="previousImage">
|
||
|
<i class="bi bi-chevron-left"></i>
|
||
|
</button>
|
||
|
<button class="nav-button next" @click.stop="nextImage">
|
||
|
<i class="bi bi-chevron-right"></i>
|
||
|
</button>
|
||
|
</div>
|
||
|
</main>
|
||
|
|
||
|
<script>
|
||
|
function imageGallery() {
|
||
|
return {
|
||
|
images: [],
|
||
|
displayedImages: [],
|
||
|
modalOpen: false,
|
||
|
currentImage: null,
|
||
|
currentIndex: 0,
|
||
|
|
||
|
async init() {
|
||
|
try {
|
||
|
const response = await fetch('data.json');
|
||
|
const data = await response.json();
|
||
|
this.images = data.images;
|
||
|
|
||
|
// Create array of 12 random images
|
||
|
let tempImages = [];
|
||
|
while (tempImages.length < 12) {
|
||
|
const randomImage = this.images[Math.floor(Math.random() * this.images.length)];
|
||
|
// Create a new object with unique ID to avoid duplicate keys
|
||
|
tempImages.push({
|
||
|
...randomImage,
|
||
|
uniqueId: randomImage.id + '_' + tempImages.length
|
||
|
});
|
||
|
}
|
||
|
this.displayedImages = tempImages;
|
||
|
} catch (error) {
|
||
|
console.error('Error loading images:', error);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
openModal(image) {
|
||
|
this.modalOpen = true;
|
||
|
this.currentImage = image;
|
||
|
this.currentIndex = this.displayedImages.findIndex(img => img.uniqueId === image.uniqueId);
|
||
|
document.body.style.overflow = 'hidden';
|
||
|
},
|
||
|
|
||
|
closeModal() {
|
||
|
this.modalOpen = false;
|
||
|
document.body.style.overflow = 'auto';
|
||
|
},
|
||
|
|
||
|
nextImage() {
|
||
|
if (!this.modalOpen) return;
|
||
|
this.currentIndex = (this.currentIndex + 1) % this.displayedImages.length;
|
||
|
this.currentImage = this.displayedImages[this.currentIndex];
|
||
|
},
|
||
|
|
||
|
previousImage() {
|
||
|
if (!this.modalOpen) return;
|
||
|
this.currentIndex = (this.currentIndex - 1 + this.displayedImages.length) % this.displayedImages.length;
|
||
|
this.currentImage = this.displayedImages[this.currentIndex];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
</script>
|
||
|
</body>
|
||
|
</html>
|