Skip to content

Commit 7c802f4

Browse files
authored
Merge pull request #79 from pavanpej/fix-image
fix: add image viewer and fix width issue; update lighthouse URL list
2 parents 0647073 + 95b066a commit 7c802f4

File tree

5 files changed

+345
-5
lines changed

5 files changed

+345
-5
lines changed

lighthouserc.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
"https://pavanpej.com/experiences",
77
"https://pavanpej.com/experiences/road",
88
"https://pavanpej.com/experiences/usa",
9-
"https://pavanpej.com/music",
10-
"https://pavanpej.com/resume"
9+
"https://pavanpej.com/music"
1110
],
1211
"numberOfRuns": 3,
1312
"startServerCommand": "",

src/components/ImageViewer.astro

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
---
2+
const {
3+
src,
4+
alt,
5+
class: className = "",
6+
width = "400",
7+
height = "300",
8+
loading = "lazy",
9+
decoding = "async",
10+
} = Astro.props
11+
---
12+
13+
<div class="image-viewer-wrapper">
14+
<img
15+
src={src}
16+
alt={alt}
17+
class={`image-viewer-trigger cursor-pointer transition-transform hover:scale-105 ${className}`}
18+
width={width}
19+
height={height}
20+
loading={loading}
21+
decoding={decoding}
22+
data-src={src}
23+
data-alt={alt}
24+
/>
25+
</div>
26+
27+
<!-- Modal overlay (hidden by default) -->
28+
<div
29+
id="image-modal"
30+
class="image-modal hidden fixed inset-0 bg-rich-black bg-opacity-90 z-50 flex items-center justify-center p-4"
31+
role="dialog"
32+
aria-modal="true"
33+
aria-label="Image viewer"
34+
>
35+
<div class="image-modal-content relative max-w-full max-h-full">
36+
<!-- Close button -->
37+
<button
38+
class="image-modal-close absolute top-4 right-4 bg-rich-black bg-opacity-75 text-gray-200 hover:text-white rounded-full w-10 h-10 flex items-center justify-center transition-colors z-10 focus:outline-none"
39+
aria-label="Close image viewer"
40+
>
41+
<i class="fas fa-times text-lg" aria-hidden="true"></i>
42+
</button>
43+
44+
<!-- Navigation arrows -->
45+
<button
46+
id="prev-button"
47+
class="image-nav-button image-nav-prev fixed left-4 top-1/2 transform -translate-y-1/2 bg-rich-black bg-opacity-75 text-gray-200 hover:text-white rounded-full w-12 h-12 flex items-center justify-center transition-opacity z-10 focus:outline-none opacity-0 pointer-events-none"
48+
aria-label="Previous image"
49+
>
50+
<i class="fas fa-chevron-left text-lg" aria-hidden="true"></i>
51+
</button>
52+
53+
<button
54+
id="next-button"
55+
class="image-nav-button image-nav-next fixed right-4 top-1/2 transform -translate-y-1/2 bg-rich-black bg-opacity-75 text-gray-200 hover:text-white rounded-full w-12 h-12 flex items-center justify-center transition-opacity z-10 focus:outline-none opacity-0 pointer-events-none"
56+
aria-label="Next image"
57+
>
58+
<i class="fas fa-chevron-right text-lg" aria-hidden="true"></i>
59+
</button>
60+
61+
<!-- Image container -->
62+
<div class="image-modal-image-container">
63+
<img
64+
id="modal-image"
65+
class="max-w-full max-h-[90vh] object-contain rounded-lg shadow-2xl"
66+
alt=""
67+
src=""
68+
/>
69+
</div>
70+
71+
<!-- Image caption -->
72+
<div class="image-modal-caption mt-4 text-center">
73+
<p
74+
id="modal-caption"
75+
class="text-white text-sm bg-rich-black bg-opacity-75 rounded px-3 py-2 inline-block"
76+
>
77+
</p>
78+
</div>
79+
</div>
80+
</div>
81+
82+
<script is:inline>
83+
document.addEventListener("DOMContentLoaded", () => {
84+
const modal = document.getElementById("image-modal")
85+
const modalImage = document.getElementById("modal-image")
86+
const modalCaption = document.getElementById("modal-caption")
87+
const closeButton = modal && modal.querySelector(".image-modal-close")
88+
const prevButton = document.getElementById("prev-button")
89+
const nextButton = document.getElementById("next-button")
90+
91+
let currentImageIndex = 0
92+
let allImages = []
93+
94+
// Get all image viewer triggers on the page
95+
const updateImageList = () => {
96+
allImages = Array.from(document.querySelectorAll(".image-viewer-trigger"))
97+
}
98+
99+
// Show/hide navigation buttons based on image count
100+
const updateNavButtons = () => {
101+
if (allImages.length > 1) {
102+
if (prevButton) {
103+
prevButton.classList.remove("opacity-0", "pointer-events-none")
104+
prevButton.classList.add("opacity-100", "pointer-events-auto")
105+
}
106+
if (nextButton) {
107+
nextButton.classList.remove("opacity-0", "pointer-events-none")
108+
nextButton.classList.add("opacity-100", "pointer-events-auto")
109+
}
110+
} else {
111+
if (prevButton) {
112+
prevButton.classList.add("opacity-0", "pointer-events-none")
113+
prevButton.classList.remove("opacity-100", "pointer-events-auto")
114+
}
115+
if (nextButton) {
116+
nextButton.classList.add("opacity-0", "pointer-events-none")
117+
nextButton.classList.remove("opacity-100", "pointer-events-auto")
118+
}
119+
}
120+
}
121+
122+
// Load image by index
123+
const loadImageByIndex = index => {
124+
if (
125+
index < 0 ||
126+
index >= allImages.length ||
127+
!modalImage ||
128+
!modalCaption
129+
)
130+
return
131+
132+
const img = allImages[index]
133+
const src = img.getAttribute("data-src")
134+
const alt = img.getAttribute("data-alt")
135+
136+
if (src && alt) {
137+
modalImage.src = src
138+
modalImage.alt = alt
139+
modalCaption.textContent = alt
140+
currentImageIndex = index
141+
}
142+
}
143+
144+
// Navigate to previous image
145+
const showPrevImage = () => {
146+
const newIndex =
147+
currentImageIndex > 0 ? currentImageIndex - 1 : allImages.length - 1
148+
loadImageByIndex(newIndex)
149+
}
150+
151+
// Navigate to next image
152+
const showNextImage = () => {
153+
const newIndex =
154+
currentImageIndex < allImages.length - 1 ? currentImageIndex + 1 : 0
155+
loadImageByIndex(newIndex)
156+
}
157+
158+
// Handle image clicks
159+
document.addEventListener("click", e => {
160+
const { target } = e
161+
if (
162+
target &&
163+
target.classList &&
164+
target.classList.contains("image-viewer-trigger")
165+
) {
166+
e.preventDefault()
167+
168+
// Update image list and find current image index
169+
updateImageList()
170+
currentImageIndex = allImages.findIndex(img => img === target)
171+
172+
const src = target.getAttribute("data-src")
173+
const alt = target.getAttribute("data-alt")
174+
175+
if (src && alt && modal && modalImage && modalCaption) {
176+
modalImage.src = src
177+
modalImage.alt = alt
178+
modalCaption.textContent = alt
179+
modal.classList.remove("hidden")
180+
modal.classList.add("flex")
181+
182+
// Update navigation buttons
183+
updateNavButtons()
184+
185+
// Focus management
186+
if (closeButton && typeof closeButton.focus === "function") {
187+
closeButton.focus()
188+
}
189+
190+
// Prevent body scroll
191+
document.body.style.overflow = "hidden"
192+
}
193+
}
194+
})
195+
196+
// Close modal function
197+
const closeModal = () => {
198+
if (modal) {
199+
modal.classList.add("hidden")
200+
modal.classList.remove("flex")
201+
document.body.style.overflow = ""
202+
203+
// Return focus to the current trigger element
204+
if (
205+
allImages[currentImageIndex] &&
206+
typeof allImages[currentImageIndex].focus === "function"
207+
) {
208+
allImages[currentImageIndex].focus()
209+
}
210+
}
211+
}
212+
213+
// Navigation button clicks
214+
if (prevButton) {
215+
prevButton.addEventListener("click", e => {
216+
e.stopPropagation()
217+
showPrevImage()
218+
})
219+
}
220+
221+
if (nextButton) {
222+
nextButton.addEventListener("click", e => {
223+
e.stopPropagation()
224+
showNextImage()
225+
})
226+
}
227+
228+
// Close button click
229+
if (closeButton) {
230+
closeButton.addEventListener("click", closeModal)
231+
}
232+
233+
// Click outside to close
234+
if (modal) {
235+
modal.addEventListener("click", e => {
236+
if (e.target === modal) {
237+
closeModal()
238+
}
239+
})
240+
}
241+
242+
// Keyboard navigation
243+
document.addEventListener("keydown", e => {
244+
if (modal && !modal.classList.contains("hidden")) {
245+
if (e.key === "Escape") {
246+
closeModal()
247+
} else if (e.key === "ArrowLeft") {
248+
e.preventDefault()
249+
showPrevImage()
250+
} else if (e.key === "ArrowRight") {
251+
e.preventDefault()
252+
showNextImage()
253+
}
254+
}
255+
})
256+
})
257+
</script>

src/pages/experiences/road.astro

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
import MainLayout from "../../layouts/MainLayout.astro"
33
import Subheading from "../../components/Subheading.astro"
4+
import ImageViewer from "../../components/ImageViewer.astro"
45
import { roadTripData } from "../../data/road-trip-data"
56
67
const assetUrlPrefix = "/assets/experiences"
@@ -35,10 +36,10 @@ const assetUrlPrefix = "/assets/experiences"
3536
</p>
3637
<p class="max-w-md mb-4">{item.description}</p>
3738
{item.image && (
38-
<img
39+
<ImageViewer
3940
src={`${assetUrlPrefix}/${item.image}`}
4041
alt={`${item.title} - ${item.location}`}
41-
class="max-w-md rounded-lg shadow-lg"
42+
class="max-w-lg rounded-lg shadow-lg w-full sm:w-auto"
4243
loading="lazy"
4344
width="400"
4445
height="300"

src/styles/global.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* Import custom color properties */
22
@import './colors.css';
33
@import './map.css';
4+
@import './image-viewer.css';
45

56
@tailwind base;
67
@tailwind components;
@@ -28,4 +29,4 @@ ffe347 - minion yellow
2829
-webkit-line-clamp: 2;
2930
-webkit-box-orient: vertical;
3031
overflow: hidden;
31-
}
32+
}

src/styles/image-viewer.css

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/* Image viewer styles */
2+
.image-viewer-trigger {
3+
transition: transform 0.2s ease;
4+
}
5+
6+
.image-viewer-trigger:hover {
7+
transform: scale(1.02);
8+
}
9+
10+
.image-modal {
11+
backdrop-filter: blur(4px);
12+
animation: fadeIn 0.2s ease-out;
13+
}
14+
15+
.image-modal-content {
16+
animation: scaleIn 0.2s ease-out;
17+
}
18+
19+
/* Navigation buttons */
20+
.image-nav-button {
21+
transition: opacity 0.2s ease;
22+
}
23+
24+
.image-nav-button:hover {
25+
opacity: 1 !important;
26+
}
27+
28+
/* Image viewer animations */
29+
@keyframes fadeIn {
30+
from {
31+
opacity: 0;
32+
}
33+
34+
to {
35+
opacity: 1;
36+
}
37+
}
38+
39+
@keyframes scaleIn {
40+
from {
41+
opacity: 0;
42+
transform: scale(0.9);
43+
}
44+
45+
to {
46+
opacity: 1;
47+
transform: scale(1);
48+
}
49+
}
50+
51+
/* Mobile optimizations for image viewer */
52+
@media (max-width: 640px) {
53+
.image-modal-close {
54+
top: 1rem !important;
55+
right: 1rem !important;
56+
width: 2.5rem !important;
57+
height: 2.5rem !important;
58+
}
59+
60+
.image-nav-button {
61+
width: 2.5rem !important;
62+
height: 2.5rem !important;
63+
}
64+
65+
.image-nav-prev {
66+
left: 1rem !important;
67+
}
68+
69+
.image-nav-next {
70+
right: 1rem !important;
71+
}
72+
73+
.image-modal-caption {
74+
margin-top: 1rem !important;
75+
padding: 0 1rem !important;
76+
}
77+
78+
#modal-caption {
79+
font-size: 0.875rem !important;
80+
padding: 0.5rem 0.75rem !important;
81+
}
82+
}

0 commit comments

Comments
 (0)