Skip to content

Commit 0dea9a3

Browse files
committed
Improve image loading and add extra modal functionalities.
1 parent c5443cb commit 0dea9a3

6 files changed

Lines changed: 136 additions & 157 deletions

File tree

firebase.json

Lines changed: 0 additions & 19 deletions
This file was deleted.

next.config.mjs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
33
transpilePackages: ["framer-motion"],
4-
// output: "export",
54
trailingSlash: true,
65
images: {
7-
unoptimized: true,
8-
remotePatterns: []
6+
remotePatterns: [
7+
{
8+
protocol: "https",
9+
hostname: "**",
10+
},
11+
{
12+
protocol: "http",
13+
hostname: "localhost",
14+
port: "3000",
15+
},
16+
],
17+
formats: ["image/webp"],
18+
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
19+
minimumCacheTTL: 86400,
20+
unoptimized: false,
921
},
10-
trailingSlash: true,
11-
skipTrailingSlashRedirect: true
1222
};
1323

1424
export default nextConfig;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"build": "next build",
88
"export": "next export",
99
"start": "next start",
10-
"lint": "next lint"
10+
"lint": "next lint",
11+
"clean-build": "rd /s /q .next && npm run build && xcopy /E /I public\\images .next\\static\\images"
1112
},
1213
"dependencies": {
1314
"firebase": "^11.6.0",

src/app/multimedia/page.js

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
import Gallery from "@/components/Gallery";
2+
import { Suspense } from "react";
3+
import { images, videos } from "@/data/multimedia";
24

3-
const { images, videos } = require("@/data/multimedia");
5+
const GallerySkeleton = () => (
6+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
7+
{[...Array(8)].map((_, i) => (
8+
<div
9+
key={i}
10+
className="aspect-square bg-gradient-to-br from-primary-green/10 to-accent-yellow/5 animate-pulse rounded-lg"
11+
/>
12+
))}
13+
</div>
14+
);
415

516
export default function MultimediaPage() {
6-
const photoYears = Array.from(new Set(images.map((i) => i.year)))
7-
.sort()
8-
.reverse();
17+
const getYears = (data) =>
18+
[...new Set(data.map((item) => item.year))].sort().reverse();
19+
920
const videoYears = Array.from(
1021
new Set(videos.map((v) => new Date(v.date).getFullYear().toString()))
1122
)
@@ -16,49 +27,52 @@ export default function MultimediaPage() {
1627
<div className="container-py">
1728
<h1 className="section-title">Galería Multimedia</h1>
1829

19-
{/* Fotos por año */}
2030
<section id="photos">
2131
<h2 className="text-2xl font-bold mb-6 text-center">Fotos</h2>
22-
{photoYears.map((year) => (
23-
<div key={year} id={`photos-${year}`} className="mb-12">
24-
<h3 className="text-xl font-semibold mb-4 text-py-text">
25-
PyDay {year}
26-
</h3>
27-
<Gallery images={images.filter((img) => img.year === year)} />
32+
{getYears(images).map((year) => (
33+
<div key={year} className="mb-12">
34+
<h3 className="text-xl font-semibold mb-4">PyDay {year}</h3>
35+
<Suspense fallback={<GallerySkeleton />}>
36+
<Gallery images={images.filter((img) => img.year === year)} />
37+
</Suspense>
2838
</div>
2939
))}
3040
</section>
3141

32-
{/* Videos por año */}
3342
<section id="videos">
3443
<h2 className="text-2xl font-bold mb-6 text-center">Videos</h2>
3544
{videoYears.map((year) => (
36-
<div key={year} id={`videos-${year}`} className="mb-12">
37-
<h3 className="text-xl font-semibold mb-4 text-py-text">
38-
PyDay {year}
39-
</h3>
45+
<div key={year} className="mb-12">
46+
<h3 className="text-xl font-semibold mb-4">PyDay {year}</h3>
4047
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
4148
{videos
4249
.filter(
43-
(v) => new Date(v.date).getFullYear().toString() === year
50+
(v) => new Date(v.date).getFullYear() === parseInt(year)
4451
)
4552
.map((video) => (
4653
<div
4754
key={video.id}
48-
className="bg-black/30 backdrop-blur-sm rounded-lg overflow-hidden shadow-lg"
55+
className="bg-black/30 backdrop-blur-sm rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition-all"
4956
>
5057
<div className="aspect-video relative">
5158
<iframe
5259
src={video.url}
5360
title={video.title}
54-
frameBorder="0"
61+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
5562
allowFullScreen
5663
className="absolute inset-0 w-full h-full"
64+
loading="lazy"
5765
/>
5866
</div>
5967
<div className="p-4">
6068
<h4 className="text-lg font-semibold">{video.title}</h4>
61-
<p className="text-sm text-gray-300 mt-1">{video.date}</p>
69+
<p className="text-gray-300 text-sm mt-1">
70+
{new Date(video.date).toLocaleDateString("es-CL", {
71+
year: "numeric",
72+
month: "long",
73+
day: "numeric",
74+
})}
75+
</p>
6276
</div>
6377
</div>
6478
))}

src/components/Gallery.js

Lines changed: 85 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -4,82 +4,89 @@ import Image from "next/image";
44

55
export default function Gallery({ images }) {
66
const [activeImage, setActiveImage] = useState(null);
7-
const [imageDimensions, setImageDimensions] = useState({
8-
width: 0,
9-
height: 0,
10-
});
11-
const imageRef = useRef(null);
7+
const modalRef = useRef(null);
128

13-
const openModal = (image) => {
14-
setActiveImage(image);
15-
document.body.style.overflow = "hidden";
16-
};
9+
// Navegación por teclado
10+
const handleKeyNavigation = useCallback(
11+
(e) => {
12+
if (!activeImage) return;
13+
const currentIndex = images.findIndex(
14+
(img) => img.src === activeImage.src
15+
);
1716

18-
const closeModal = () => {
19-
setActiveImage(null);
20-
document.body.style.overflow = "auto";
21-
};
22-
23-
// Esta función calculará las dimensiones reales de la imagen renderizada
24-
const updateImageDimensions = useCallback(() => {
25-
if (imageRef.current && activeImage) {
26-
const img = imageRef.current;
27-
const rect = img.getBoundingClientRect();
28-
setImageDimensions({
29-
width: rect.width,
30-
height: rect.height,
31-
top: rect.top,
32-
left: rect.left,
33-
});
34-
}
35-
}, [activeImage]);
17+
if (e.key === "ArrowLeft" && currentIndex > 0) {
18+
setActiveImage(images[currentIndex - 1]);
19+
}
20+
if (e.key === "ArrowRight" && currentIndex < images.length - 1) {
21+
setActiveImage(images[currentIndex + 1]);
22+
}
23+
if (e.key === "Escape") {
24+
setActiveImage(null);
25+
document.body.style.overflow = "auto";
26+
}
27+
},
28+
[activeImage, images]
29+
);
3630

37-
// Actualizar dimensiones cuando cambia la imagen activa o en resize
3831
useEffect(() => {
39-
if (activeImage) {
40-
const timer = setTimeout(updateImageDimensions, 100);
41-
window.addEventListener("resize", updateImageDimensions);
32+
window.addEventListener("keydown", handleKeyNavigation);
33+
return () => window.removeEventListener("keydown", handleKeyNavigation);
34+
}, [handleKeyNavigation]);
4235

43-
return () => {
44-
clearTimeout(timer);
45-
window.removeEventListener("resize", updateImageDimensions);
46-
};
47-
}
48-
}, [activeImage, updateImageDimensions]);
36+
// Cargar placeholder dinámico
37+
const getBlurData = (width, height) =>
38+
`data:image/svg+xml;base64,${btoa(
39+
`<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" fill="#3D8B3720"/>`
40+
)}`;
4941

5042
return (
5143
<div>
5244
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
53-
{images.map((image, index) => (
54-
<div
55-
key={index}
56-
className="aspect-square relative rounded-lg overflow-hidden cursor-pointer group"
57-
onClick={() => openModal(image)}
58-
>
59-
<Image
60-
src={image.src}
61-
alt={image.alt || "Imagen de PyDay Chile"}
62-
fill
63-
className="object-cover transition-transform duration-300 group-hover:scale-110"
64-
/>
65-
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end p-4">
66-
<span className="text-white text-sm font-medium">
67-
{image.caption || `PyDay ${image.year || "2024"}`}
68-
</span>
45+
{images.map((image, index) => {
46+
const isPriority = index < 6;
47+
return (
48+
<div
49+
key={image.src}
50+
className="aspect-square relative rounded-lg overflow-hidden cursor-pointer group"
51+
onClick={() => {
52+
setActiveImage(image);
53+
document.body.style.overflow = "hidden";
54+
}}
55+
>
56+
<Image
57+
src={image.src}
58+
alt={image.alt || `Imagen ${index + 1} de PyDay Chile`}
59+
fill
60+
sizes="(max-width: 768px) 50vw, 33vw"
61+
className="object-cover transition-transform duration-300 group-hover:scale-105"
62+
loading={isPriority ? "eager" : "lazy"}
63+
priority={isPriority}
64+
placeholder="blur"
65+
blurDataURL={getBlurData(1600, 900)}
66+
/>
67+
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end p-3">
68+
<span className="text-white text-sm font-medium line-clamp-2">
69+
{image.caption}
70+
</span>
71+
</div>
6972
</div>
70-
</div>
71-
))}
73+
);
74+
})}
7275
</div>
7376

74-
{/* Modal */}
7577
{activeImage && (
7678
<div
79+
ref={modalRef}
7780
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4"
78-
onClick={closeModal}
81+
onClick={(e) => e.target === modalRef.current && setActiveImage(null)}
7982
>
8083
<button
81-
className="absolute top-4 right-4 text-white p-2 hover:bg-white/10 rounded-full transition z-50"
82-
onClick={closeModal}
84+
className="absolute top-4 right-4 text-white p-2 hover:bg-white/10 rounded-full z-50"
85+
onClick={() => {
86+
setActiveImage(null);
87+
document.body.style.overflow = "auto";
88+
}}
89+
aria-label="Cerrar modal"
8390
>
8491
<svg
8592
xmlns="http://www.w3.org/2000/svg"
@@ -97,47 +104,26 @@ export default function Gallery({ images }) {
97104
</svg>
98105
</button>
99106

100-
<div className="flex flex-col items-center justify-center relative max-w-3xl w-full">
101-
{/* Contenedor de la imagen */}
102-
<div className="relative flex items-center justify-center w-full h-[60vh] md:h-[70vh] lg:h-[80vh]">
103-
<div className="relative h-full flex items-center justify-center">
104-
<Image
105-
ref={imageRef}
106-
src={activeImage.src}
107-
alt={activeImage.alt || "Imagen de PyDay Chile"}
108-
width={1200}
109-
height={900}
110-
style={{
111-
maxHeight: "100%",
112-
width: "auto",
113-
maxWidth: "100%",
114-
objectFit: "contain",
115-
}}
116-
onLoad={updateImageDimensions}
117-
priority
118-
/>
107+
<div className="relative max-w-5xl w-full">
108+
<Image
109+
src={activeImage.src}
110+
alt={activeImage.alt}
111+
width={1600}
112+
height={900}
113+
className="max-h-[90vh] w-auto mx-auto object-contain"
114+
priority
115+
quality={90}
116+
/>
119117

120-
{/* Caption*/}
121-
{activeImage.caption && imageDimensions.width > 0 && (
122-
<div
123-
className="absolute bg-black/60 p-3 md:p-4 text-center"
124-
style={{
125-
bottom: 0,
126-
width: imageDimensions.width,
127-
maxWidth: "100%",
128-
}}
129-
>
130-
<p className="text-white text-sm md:text-base">
131-
{activeImage.caption}
132-
</p>
133-
{activeImage.location && (
134-
<p className="text-white text-xs md:text-sm opacity-70 mt-1">
135-
{activeImage.location}
136-
</p>
137-
)}
138-
</div>
139-
)}
140-
</div>
118+
<div className="absolute bottom-0 left-0 right-0 bg-black/70 p-4 text-center backdrop-blur-md">
119+
<p className="text-white text-lg font-medium">
120+
{activeImage.caption}
121+
</p>
122+
{activeImage.location && (
123+
<p className="text-gray-300 text-sm mt-1">
124+
{activeImage.location}
125+
</p>
126+
)}
141127
</div>
142128
</div>
143129
</div>

src/components/LazyVideoPlayerWrapper.js

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)