1 <div id="image-viewer" class="image-viewer">
2 <img id="image-viewer-img" src="" alt="" />
3 </div>
4
5 <script>
6 function initImageViewer() {
7 const viewer = document.getElementById('image-viewer')
8 const viewerImg = document.getElementById('image-viewer-img') as HTMLImageElement
9
10 if (!viewer || !viewerImg || viewerImg.tagName !== 'IMG') return
11
12 // Display image in fullscreen viewer overlay
13 function showImage(src: string, alt?: string) {
14 if (viewerImg && src) {
15 viewerImg.src = src
16 viewerImg.alt = alt || ''
17 }
18 viewer?.classList.add('active')
19 document.body.classList.add('image-viewer-open')
20 document.body.style.overflow = 'hidden'
21 document.body.dataset.scrollY = window.scrollY.toString()
22 if (viewer) {
23 viewer.style.cursor = 'auto'
24 setTimeout(() => {
25 viewer.style.cursor = ''
26 }, 10)
27 }
28 }
29
30 // Hide the image viewer and restore page scroll
31 function hideImage() {
32 viewer?.classList.remove('active')
33 document.body.classList.remove('image-viewer-open')
34 document.body.style.overflow = ''
35 setTimeout(() => {
36 if (viewer && !viewer.classList.contains('active') && viewerImg) {
37 viewerImg.src = ''
38 viewerImg.alt = ''
39 viewer.style.cursor = 'auto'
40 setTimeout(() => {
41 viewer.style.cursor = ''
42 }, 10)
43 }
44 }, 300)
45 }
46
47 // Bind click events to images with data-preview="true" attribute
48 function bindImageClickEvents() {
49 const previewImages = document.querySelectorAll('img[data-preview="true"]')
50 previewImages.forEach((img) => {
51 const imgElement = img as HTMLImageElement
52 imgElement.style.cursor = 'zoom-in'
53 imgElement.addEventListener('click', (e) => {
54 e.preventDefault()
55 const target = e.target as HTMLImageElement
56 showImage(target.src, target.alt || '')
57 })
58 })
59 }
60
61 viewer?.addEventListener('click', hideImage)
62
63 // Prevent touch scroll in image viewer
64 viewer?.addEventListener(
65 'touchmove',
66 (e) => {
67 if (viewer.classList.contains('active')) {
68 e.preventDefault()
69 }
70 },
71 { passive: false }
72 )
73
74 document.addEventListener('keydown', (e) => {
75 if (e.key === 'Escape' && viewer.classList.contains('active')) {
76 hideImage()
77 }
78 })
79
80 bindImageClickEvents()
81
82 const observer = new MutationObserver(() => {
83 bindImageClickEvents()
84 })
85
86 observer.observe(document.body, {
87 childList: true,
88 subtree: true
89 })
90 }
91
92 if (document.readyState === 'loading') {
93 document.addEventListener('DOMContentLoaded', initImageViewer)
94 } else {
95 initImageViewer()
96 }
97
98 document.addEventListener('astro:page-load', initImageViewer)
99 </script>
100
101 <style>
102 .image-viewer {
103 position: fixed;
104 top: 0;
105 left: 0;
106 width: 100%;
107 height: 100%;
108 z-index: 9999;
109 display: flex;
110 align-items: center;
111 justify-content: center;
112 opacity: 0;
113 visibility: hidden;
114 transition: all 0.15s ease-in-out;
115 background: color-mix(in srgb, var(--bg) 90%, transparent);
116 cursor: zoom-out;
117 }
118
119 .image-viewer.active {
120 opacity: 1;
121 visibility: visible;
122 }
123
124 .image-viewer img {
125 min-width: 45rem;
126 max-width: 60vw;
127 max-height: 80vh;
128 object-fit: contain;
129 cursor: zoom-out;
130 }
131
132 @media (max-width: 768px) {
133 .image-viewer img {
134 min-width: 100vw;
135 }
136 }
137
138 body.image-viewer-open {
139 overflow: hidden;
140 }
141 </style>