Implement image gallery with dynamic loading and copy URL functionality; add styles for improved layout and user interaction
All checks were successful
CI / build (push) Successful in 30s

This commit is contained in:
2025-07-23 20:12:47 +00:00
parent 2fc8bde6eb
commit 0a566797de
7 changed files with 128 additions and 62 deletions

28
public/app.js Normal file
View File

@@ -0,0 +1,28 @@
fetch("/images")
.then(response => {
if (!response.ok) throw new Error("Failed to fetch images.");
return response.json();
})
.then(images => {
const gallery = document.getElementById("gallery");
images.forEach(img => {
const imgContainer = document.createElement("div");
imgContainer.className = "image-container";
imgContainer.innerHTML = `
<a href="${img.url}" target="_blank">
<img src="${img.url}" alt="${img.name}">
</a>
<button class="copy-btn" title="Copy URL">Copy URL</button>
`;
const copyBtn = imgContainer.querySelector('.copy-btn');
copyBtn.addEventListener('click', (e) => {
e.preventDefault();
navigator.clipboard.writeText(img.url).then(() => {
copyBtn.textContent = 'Copied!';
setTimeout(() => copyBtn.textContent = 'Copy URL', 1200);
});
});
gallery.appendChild(imgContainer);
});
})
.catch(error => console.error("Error loading images:", error));

View File

@@ -4,37 +4,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PictShare Gallery</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; }
#gallery { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }
.image-container { width: 150px; cursor: pointer; }
.image-container img { width: 100%; border-radius: 5px; }
</style>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>PictShare Gallery</h1>
<div id="gallery"></div>
<script>
fetch("/images")
.then(response => {
if (!response.ok) throw new Error("Failed to fetch images.");
return response.json();
})
.then(images => {
const gallery = document.getElementById("gallery");
images.forEach(img => {
const imgContainer = document.createElement("div");
imgContainer.className = "image-container";
imgContainer.innerHTML = `<a href="${img.url}" target="_blank">
<img src="${img.url}" alt="${img.name}">
</a>`;
gallery.appendChild(imgContainer);
});
})
.catch(error => console.error("Error loading images:", error));
</script>
<script src="/app.js"></script>
</body>
</html>

62
public/style.css Normal file
View File

@@ -0,0 +1,62 @@
body { font-family: Arial, sans-serif; text-align: center; margin: 0; padding: 0; }
h1 { margin-top: 1.5rem; }
#gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 22px;
justify-items: center;
align-items: start;
/* max-width: 1400px; */
margin: auto 10rem;
padding: 0 1rem;
}
.image-container {
width: 200px;
aspect-ratio: 1 / 1;
background: #fafafa;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
transition: transform 0.15s;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
.image-container:hover {
transform: scale(1.04);
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
}
.copy-btn {
position: absolute;
top: 8px;
right: 8px;
background: rgba(0,0,0,0.7);
color: #fff;
border: none;
border-radius: 4px;
padding: 4px 8px;
font-size: 0.9rem;
opacity: 0;
cursor: pointer;
transition: opacity 0.2s;
z-index: 2;
}
.image-container:hover .copy-btn {
opacity: 1;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px 8px 0 0;
display: block;
}

View File

@@ -1,9 +1,13 @@
const express = require("express");
const path = require("path");
const galleryRoutes = require("./galleryRoutes");
const app = express();
const PORT = process.env.PORT || 3000;
// Serve static files from public directory
app.use(express.static(path.join(__dirname, "../public")));
app.use(galleryRoutes);
module.exports = app;

View File

@@ -1,13 +1,14 @@
const express = require("express");
const getImages = require("./getImages");
const renderGallery = require("./renderGallery");
const router = express.Router();
// Route: Serve Image Gallery at `/`
// Serve static index.html for the root route
const path = require("path");
router.get("/", (req, res) => {
const images = getImages();
res.send(renderGallery(images));
res.sendFile(path.join(__dirname, "../public/index.html"));
});
// API Route: Return JSON List of Images
@@ -15,4 +16,5 @@ router.get("/images", (req, res) => {
res.json(getImages());
});
module.exports = router;

View File

@@ -1,30 +0,0 @@
module.exports = function renderGallery(images) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PictShare Gallery</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; }
#gallery { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }
.image-container { max-width: 150px; max-height: 150px; cursor: pointer; }
.image-container img { width: 100%; border-radius: 5px; }
</style>
</head>
<body>
<h1>PictShare Gallery</h1>
<div id="gallery">
${images
.map(img => `<a href="${img.url}" target="_blank">
<img src="${img.url}" alt="${img.name}" class="image-container">
</a>`)
.join("")}
</div>
</body>
</html>
`;
};

View File

@@ -1,3 +1,26 @@
0799fec84a8ace495e1cb631db19f944dadb330b;rkijhb.png
c8155c5afce62d096283fc3a7d5dc09e46714ffe;u71mru.png
443e983d9dd5ce40a42eb4be98a0facd8e080888;qojsy0.png
0799fec84a8ace495e1cb631db19f944dadb330b;000001.png
c8155c5afce62d096283fc3a7d5dc09e46714ffe;000002.png
443e983d9dd5ce40a42eb4be98a0facd8e080888;000003.png
e1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1;000004.png
f2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2;000005.png
03c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3;000006.png
14d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4;000007.png
25e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5;000008.png
36f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6;000009.png
4707070707070707070707070707070707070707;000010.png
5818181818181818181818181818181818181818;000011.png
6929292929292929292929292929292929292929;000012.png
7a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3;000013.png
8b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4;000014.png
9c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5;000015.png
ad6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6;000016.png
be7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7;000017.png
cf8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8;000018.png
d0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a;000019.png
e1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b;000020.png
f2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c;000021.png
03d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d;000022.png
14e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e;000023.png
25f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f;000024.png
3606060606060606060606060606060606060606;000025.png
1 0799fec84a8ace495e1cb631db19f944dadb330b rkijhb.png 000001.png
2 c8155c5afce62d096283fc3a7d5dc09e46714ffe u71mru.png 000002.png
3 443e983d9dd5ce40a42eb4be98a0facd8e080888 qojsy0.png 000003.png
4 e1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1 000004.png
5 f2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2 000005.png
6 03c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3 000006.png
7 14d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4 000007.png
8 25e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5 000008.png
9 36f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6 000009.png
10 4707070707070707070707070707070707070707 000010.png
11 5818181818181818181818181818181818181818 000011.png
12 6929292929292929292929292929292929292929 000012.png
13 7a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3 000013.png
14 8b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4 000014.png
15 9c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5 000015.png
16 ad6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6 000016.png
17 be7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7 000017.png
18 cf8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8 000018.png
19 d0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a 000019.png
20 e1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b 000020.png
21 f2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c 000021.png
22 03d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d 000022.png
23 14e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e 000023.png
24 25f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f 000024.png
25 3606060606060606060606060606060606060606 000025.png
26