This commit is contained in:
Fabio 2026-02-22 01:17:13 +01:00
parent 8c2608b79b
commit 4dec824b41
14 changed files with 765 additions and 39 deletions

17
css/base.css Normal file
View file

@ -0,0 +1,17 @@
body {
font-family: sans-serif;
margin: 0;
padding: 0;
background: #fafafa;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}

100
css/bottomSheet.css Normal file
View file

@ -0,0 +1,100 @@
/* ===============================
BOTTOM SHEET (menu )
=============================== */
.bottom-sheet {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
/* 🔥 Altezza dinamica: abbastanza grande per mostrare tutto */
height: 60vh;
max-height: 80vh;
background: rgba(255,255,255,0.95);
backdrop-filter: blur(6px);
border-top: 1px solid #ddd;
box-shadow: 0 -2px 10px rgba(0,0,0,0.15);
display: none;
flex-direction: column;
z-index: 9999;
overflow-y: auto; /* 🔥 Scroll verticale */
}
.bottom-sheet.open {
display: flex;
}
/* Header con la "maniglia" */
.sheet-header {
height: 16px;
display: flex;
justify-content: center;
align-items: center;
}
.sheet-header::before {
content: "";
width: 40px;
height: 4px;
background: #bbb;
border-radius: 4px;
}
/* Contenuto del menu (ordinamento, filtri, ecc.) */
.sheet-content {
padding: 20px;
}
/* Miniature (usate nel bottom sheet delle foto, non in quello delle opzioni) */
.sheet-gallery {
display: flex;
flex-direction: row;
overflow-x: auto;
padding: 10px;
gap: 10px;
}
.sheet-item {
width: 90px;
height: 90px;
border-radius: 10px;
overflow: hidden;
flex-shrink: 0;
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
background: #eee;
}
.sheet-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Pulsanti del menu ⋮ */
.sheet-btn {
width: 100%;
padding: 12px;
margin-bottom: 8px;
text-align: left;
background: #f5f5f5;
border: none;
border-radius: 8px;
font-size: 15px;
cursor: pointer;
}
.sheet-btn:hover {
background: #e8e8e8;
}
/* Titoli delle sezioni */
#optionsSheet h3 {
margin-top: 20px;
margin-bottom: 10px;
font-size: 16px;
color: #444;
}

46
css/gallery.css Normal file
View file

@ -0,0 +1,46 @@
.gallery {
display: block;
padding: 6px; /* più stretto */
}
.gallery-section-title {
font-size: 18px;
font-weight: 600;
margin: 18px 6px 6px; /* più compatto */
color: #444;
}
.gallery-section {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); /* leggermente più piccole */
gap: 6px; /* SPACING RIDOTTO */
padding: 0 6px;
}
.thumb {
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 8px; /* più compatto */
overflow: hidden;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.12); /* più leggero */
position: relative;
cursor: pointer;
}
.thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.play-icon {
position: absolute;
bottom: 6px;
right: 6px;
background: rgba(0,0,0,0.55);
color: white;
padding: 3px 5px;
border-radius: 4px;
font-size: 12px;
}

36
css/header.css Normal file
View file

@ -0,0 +1,36 @@
header {
padding: 10px 15px;
background: #333;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.top-buttons {
display: flex;
gap: 10px;
}
.icon-btn {
background: none;
border: none;
font-size: 22px;
padding: 6px 10px;
cursor: pointer;
border-radius: 6px;
}
.icon-btn:hover {
background: rgba(255,255,255,0.15);
}
.icon-btn {
background: none;
border: none;
font-size: 22px;
padding: 6px 10px;
cursor: pointer;
border-radius: 6px;
color: white; /* 🔥 questo mancava */
}

40
css/infoPanel.css Normal file
View file

@ -0,0 +1,40 @@
.info-panel {
position: fixed;
top: 0;
right: 0;
width: 320px;
height: 100%;
background: #fff;
padding: 16px;
box-shadow: -2px 0 6px rgba(0,0,0,0.25);
overflow-y: auto;
z-index: 2000;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.info-panel.open {
transform: translateX(0);
}
.info-panel h3 {
margin-top: 0;
}
.info-row {
margin-bottom: 10px;
}
.info-row b {
display: inline-block;
width: 110px;
}
.info-map {
width: 100%;
height: 250px;
margin-top: 15px;
border-radius: 6px;
overflow: hidden;
border: 1px solid #ccc;
}

58
css/map.css Normal file
View file

@ -0,0 +1,58 @@
.global-map {
display: none;
width: 100%;
height: calc(100vh - 60px);
z-index: 900;
}
.global-map.open {
display: block;
}
.photo-marker {
width: 48px;
height: 48px;
border-radius: 10px;
overflow: hidden;
position: relative;
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
background: #fff;
}
.photo-marker img {
width: 100%;
height: 100%;
object-fit: cover;
}
.photo-cluster {
width: 56px;
height: 56px;
position: relative;
border-radius: 12px;
overflow: visible;
}
.cluster-back {
position: absolute;
top: 6px;
left: 6px;
width: 48px;
height: 48px;
border-radius: 10px;
object-fit: cover;
opacity: 0.5;
filter: blur(1px);
transform: scale(0.95);
}
.cluster-front {
position: absolute;
top: 0;
left: 0;
width: 48px;
height: 48px;
border-radius: 10px;
object-fit: cover;
box-shadow: 0 2px 6px rgba(0,0,0,0.35);
}

70
css/modal.css Normal file
View file

@ -0,0 +1,70 @@
.modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.open {
display: flex;
}
.modal-content {
width: 90vw;
height: 90vh;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
#modalMediaContainer {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
#modalMediaContainer img,
#modalMediaContainer video {
width: 100%;
height: 100%;
object-fit: contain;
}
.modal-close {
position: absolute;
top: -10px;
right: -10px;
background: #fff;
border-radius: 50%;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-weight: bold;
border: 1px solid #ccc;
}
.modal-info-btn {
position: absolute;
bottom: -10px;
right: 40px;
background: #fff;
border-radius: 50%;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: 1px solid #ccc;
font-size: 16px;
z-index: 1500;
}

26
css/optionsSheet.css Normal file
View file

@ -0,0 +1,26 @@
#optionsSheet .sheet-content {
padding: 20px;
}
#optionsSheet h3 {
margin-top: 20px;
margin-bottom: 10px;
font-size: 16px;
color: #444;
}
.sheet-btn {
width: 100%;
padding: 12px;
margin-bottom: 8px;
text-align: left;
background: #f5f5f5;
border: none;
border-radius: 8px;
font-size: 15px;
cursor: pointer;
}
.sheet-btn:hover {
background: #e8e8e8;
}

24
css/utils.css Normal file
View file

@ -0,0 +1,24 @@
.hidden {
display: none !important;
}
.rounded {
border-radius: 10px;
}
.shadow-sm {
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
}
.shadow-md {
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
}
.fade-in {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

View file

@ -5,7 +5,16 @@
<title>Galleria Foto</title>
<!-- CSS -->
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/header.css">
<link rel="stylesheet" href="css/gallery.css">
<link rel="stylesheet" href="css/modal.css">
<link rel="stylesheet" href="css/infoPanel.css">
<link rel="stylesheet" href="css/bottomSheet.css">
<link rel="stylesheet" href="css/optionsSheet.css">
<link rel="stylesheet" href="css/map.css">
<link rel="stylesheet" href="css/utils.css">
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
@ -17,9 +26,17 @@
<body>
<!-- =============================== -->
<!-- BARRA SUPERIORE -->
<!-- =============================== -->
<header>
<h1>Galleria Foto</h1>
<button id="openMapBtn" class="map-btn">🗺️ Mappa</button>
<div class="top-buttons">
<button id="openMapBtn" class="icon-btn">🗺️</button>
<button id="optionsBtn" class="icon-btn"></button>
<button id="settingsBtn" class="icon-btn">⚙️</button>
</div>
</header>
<main>
@ -44,12 +61,38 @@
<!-- Pannello Info -->
<div id="infoPanel" class="info-panel"></div>
<!-- Bottom Sheet stile Google Photos Web -->
<!-- Bottom Sheet stile Google Photos Web (foto/cluster) -->
<div id="bottomSheet" class="bottom-sheet">
<div class="sheet-header"></div>
<div class="sheet-gallery" id="sheetGallery"></div>
</div>
<!-- =============================== -->
<!-- BOTTOM SHEET OPZIONI (ordinamento + raggruppamento + filtri) -->
<!-- =============================== -->
<div id="optionsSheet" class="bottom-sheet">
<div class="sheet-header"></div>
<div class="sheet-content">
<h3>Ordinamento</h3>
<button class="sheet-btn" data-sort="desc">Più recenti prima</button>
<button class="sheet-btn" data-sort="asc">Più vecchie prima</button>
<h3>Raggruppamento</h3>
<button class="sheet-btn" data-group="auto">Automatico (Oggi, Ieri…)</button>
<button class="sheet-btn" data-group="day">Giorno</button>
<button class="sheet-btn" data-group="month">Mese</button>
<button class="sheet-btn" data-group="year">Anno</button>
<h3>Filtri</h3>
<button class="sheet-btn" data-filter="folder">Per cartella</button>
<button class="sheet-btn" data-filter="location">Per luogo</button>
<button class="sheet-btn" data-filter="type">Per tipo</button>
</div>
</div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>

View file

@ -22,5 +22,8 @@ async function loadPhotos() {
return;
}
renderGallery(photosData);
// 🔥 IMPORTANTE:
// Ora la galleria deve essere costruita tramite refreshGallery(),
// che applica filtri, ordinamento e raggruppamento.
refreshGallery();
}

View file

@ -1,40 +1,149 @@
// ===============================
// RENDER GALLERIA
// ORDINAMENTO
// ===============================
function renderGallery(photos) {
const gallery = document.getElementById('gallery');
gallery.innerHTML = '';
function sortByDate(photos, direction = "desc") {
return photos.slice().sort((a, b) => {
const da = new Date(a.taken_at);
const db = new Date(b.taken_at);
photos.forEach(photo => {
const thumbDiv = document.createElement('div');
thumbDiv.className = 'thumb';
const img = document.createElement('img');
img.src = `https://prova.patachina.it/${photo.thub1}`;
img.alt = photo.name || '';
img.loading = "lazy";
thumbDiv.appendChild(img);
if (photo.mime_type.startsWith("video/")) {
const play = document.createElement("div");
play.className = "play-icon";
play.textContent = "▶";
thumbDiv.appendChild(play);
}
const preview = photo.thub2
? `https://prova.patachina.it/${photo.thub2}`
: `https://prova.patachina.it/${photo.thub1}`;
thumbDiv.addEventListener('click', () => {
openModal(
`https://prova.patachina.it/${photo.path}`,
preview,
photo
);
});
gallery.appendChild(thumbDiv);
return direction === "asc" ? da - db : db - da;
});
}
// ===============================
// FILTRI (base, estendibili)
// ===============================
function applyFilters(photos) {
if (!currentFilter) return photos;
switch (currentFilter) {
case "folder":
return photos.filter(p => p.folder); // da migliorare
case "location":
return photos.filter(p => p.gps && p.gps.lat);
case "type":
return photos.filter(p => p.mime_type.startsWith("image/"));
default:
return photos;
}
}
// ===============================
// RAGGRUPPAMENTO STILE GOOGLE PHOTOS
// ===============================
function groupByDate(photos, mode = "auto") {
const sections = [];
const now = new Date();
function getLabel(photo) {
const date = new Date(photo.taken_at);
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
if (mode === "day") return formatDay(date);
if (mode === "month") return formatMonth(date);
if (mode === "year") return date.getFullYear().toString();
// modalità AUTO (Google Photos)
if (diffDays === 0) return "Oggi";
if (diffDays === 1) return "Ieri";
if (diffDays <= 7) return "Questa settimana";
if (diffDays <= 14) return "La settimana scorsa";
if (diffDays <= 30) return "Questo mese";
if (diffDays <= 60) return "Mese scorso";
// stesso anno → raggruppa per mese
if (date.getFullYear() === now.getFullYear()) {
return formatMonth(date);
}
// anni precedenti → raggruppa per anno
return date.getFullYear().toString();
}
photos.forEach(photo => {
const label = getLabel(photo);
let section = sections.find(s => s.label === label);
if (!section) {
section = { label, photos: [] };
sections.push(section);
}
section.photos.push(photo);
});
return sections;
}
// ===============================
// FORMATTATORI
// ===============================
function formatDay(date) {
return date.toLocaleDateString("it-IT", {
weekday: "long",
day: "numeric",
month: "long"
});
}
function formatMonth(date) {
return date.toLocaleDateString("it-IT", {
month: "long",
year: "numeric"
});
}
// ===============================
// RENDER GALLERIA A SEZIONI
// ===============================
function renderGallery(sections) {
const gallery = document.getElementById("gallery");
gallery.innerHTML = "";
sections.forEach(section => {
// HEADER DELLA SEZIONE
const h = document.createElement("h2");
h.className = "gallery-section-title";
h.textContent = section.label;
gallery.appendChild(h);
// CONTENITORE DELLE FOTO
const container = document.createElement("div");
container.className = "gallery-section";
section.photos.forEach(photo => {
const thumbDiv = document.createElement("div");
thumbDiv.className = "thumb";
const img = document.createElement("img");
img.src = `https://prova.patachina.it/${photo.thub1}`;
img.alt = photo.name || "";
img.loading = "lazy";
thumbDiv.appendChild(img);
if (photo.mime_type.startsWith("video/")) {
const play = document.createElement("div");
play.className = "play-icon";
play.textContent = "▶";
thumbDiv.appendChild(play);
}
const preview = photo.thub2
? `https://prova.patachina.it/${photo.thub2}`
: `https://prova.patachina.it/${photo.thub1}`;
thumbDiv.addEventListener("click", () => {
openModal(
`https://prova.patachina.it/${photo.path}`,
preview,
photo
);
});
container.appendChild(thumbDiv);
});
gallery.appendChild(container);
});
}

View file

@ -2,4 +2,76 @@
// AVVIO
// ===============================
console.log("main.js avviato");
// Carica le foto iniziali
loadPhotos();
// ===============================
// VARIABILI GLOBALI PER LE OPZIONI
// ===============================
let currentSort = "desc"; // "desc" = più recenti prima
let currentGroup = "auto"; // auto = Oggi, Ieri, Settimana...
let currentFilter = null; // folder / location / type
// ===============================
// APERTURA / CHIUSURA OPTIONS SHEET
// ===============================
const optionsBtn = document.getElementById("optionsBtn");
const optionsSheet = document.getElementById("optionsSheet");
optionsBtn.addEventListener("click", () => {
optionsSheet.classList.add("open");
});
function closeOptionsSheet() {
optionsSheet.classList.remove("open");
}
// Chiudi se clicchi fuori
optionsSheet.addEventListener("click", (e) => {
if (e.target === optionsSheet) closeOptionsSheet();
});
// ===============================
// GESTIONE PULSANTI DEL BOTTOM SHEET OPZIONI
// ===============================
document.querySelectorAll("#optionsSheet .sheet-btn").forEach(btn => {
btn.addEventListener("click", () => {
if (btn.dataset.sort) {
currentSort = btn.dataset.sort;
}
if (btn.dataset.group) {
currentGroup = btn.dataset.group;
}
if (btn.dataset.filter) {
currentFilter = btn.dataset.filter;
}
closeOptionsSheet();
refreshGallery();
});
});
// ===============================
// FUNZIONE CENTRALE DI AGGIORNAMENTO GALLERIA
// ===============================
function refreshGallery() {
console.log("Aggiornamento galleria...");
let photos = [...photosData];
// 1) Filtri
photos = applyFilters(photos);
// 2) Ordinamento
photos = sortByDate(photos, currentSort);
// 3) Raggruppamento
const sections = groupByDate(photos, currentGroup);
// 4) Rendering
renderGallery(sections);
}

View file

@ -341,3 +341,85 @@ header {
height: 100%;
object-fit: cover;
}
.top-buttons {
display: flex;
gap: 10px;
}
.icon-btn {
background: none;
border: none;
font-size: 22px;
padding: 6px 10px;
cursor: pointer;
border-radius: 6px;
}
.icon-btn:hover {
background: rgba(0,0,0,0.1);
}
#optionsSheet .sheet-content {
padding: 20px;
}
#optionsSheet h3 {
margin-top: 20px;
margin-bottom: 10px;
font-size: 16px;
color: #444;
}
.sheet-btn {
width: 100%;
padding: 12px;
margin-bottom: 8px;
text-align: left;
background: #f5f5f5;
border: none;
border-radius: 8px;
font-size: 15px;
cursor: pointer;
}
.sheet-btn:hover {
background: #e8e8e8;
}
.gallery-section-title {
font-size: 20px;
font-weight: 600;
margin: 20px 10px 10px;
color: #444;
}
.gallery-section {
column-count: 3;
column-gap: 10px;
padding: 0 10px;
}
@media (max-width: 900px) {
.gallery-section { column-count: 2; }
}
@media (max-width: 600px) {
.gallery-section { column-count: 1; }
}
.thumb {
break-inside: avoid;
margin-bottom: 10px;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
.thumb img {
width: 100%;
display: block;
object-fit: cover;
}