Spaces:
Running
Running
<html lang="ro"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Cimitirul "Sf. Gheorghe"- Botești</title> | |
<link rel="icon" href="https://huggingface.co/spaces/vericudebuget/cimitir/resolve/main/Screenshot%202025-06-07%20223622.png" type="image/x-icon"> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Montserrat:wght@300;400;600;700&display=swap" rel="stylesheet"> | |
<style> | |
body { | |
font-family: 'Montserrat', sans-serif; | |
background-color: #111827; /* bg-slate-900 */ | |
color: #d1d5db; /* text-slate-300 */ | |
font-weight: 400; /* Default font weight */ | |
} | |
/* Use lighter font weight for main titles */ | |
h1, .card-title { | |
font-weight: 300; | |
} | |
.modal-content-scrollable::-webkit-scrollbar { | |
width: 8px; | |
} | |
.modal-content-scrollable::-webkit-scrollbar-track { | |
background: #374151; /* bg-slate-700 */ | |
border-radius: 10px; | |
} | |
.modal-content-scrollable::-webkit-scrollbar-thumb { | |
background: #4b5563; /* bg-slate-600 */ | |
border-radius: 10px; | |
} | |
.modal-content-scrollable::-webkit-scrollbar-thumb:hover { | |
background: #6b7280; /* bg-slate-500 */ | |
} | |
.placeholder-text-color::placeholder { | |
color: #9ca3af; | |
} | |
.btn-primary { | |
background-color: #0ea5e9; /* bg-sky-500 */ | |
color: white; | |
} | |
.btn-primary:hover { | |
background-color: #0284c7; /* hover:bg-sky-600 */ | |
} | |
.btn-secondary { | |
background-color: #4b5563; /* bg-slate-600 */ | |
color: white; | |
} | |
.btn-secondary:hover { | |
background-color: #6b7280; /* hover:bg-slate-500 */ | |
} | |
.btn-danger { | |
background-color: #dc2626; /* bg-red-600 */ | |
color: white; | |
} | |
.btn-danger:hover { | |
background-color: #b91c1c; /* hover:bg-red-700 */ | |
} | |
.input-style { | |
background-color: #374151; /* bg-slate-700 */ | |
border-color: #4b5563; /* border-slate-600 */ | |
color: #e5e7eb; /* text-slate-200 */ | |
} | |
.input-style:focus { | |
border-color: #0ea5e9; /* focus:border-sky-500 */ | |
} | |
/* Replace the existing .people-preview and related styles in the <style> section */ | |
.people-preview { | |
max-height: 120px; /* Limit height to roughly 2 people */ | |
overflow-y: auto; /* Enable scrolling for overflow */ | |
background-color: #2d3748; /* Slightly brighter bg-slate-800 */ | |
border: 1px solid #4b5563; /* Brighter border-slate-600 */ | |
border-radius: 6px; | |
padding: 8px; | |
padding-top: 10px; | |
margin-top: 8px; | |
display: flex; /* Use flexbox for vertical centering */ | |
flex-direction: column; /* Stack items vertically */ | |
justify-content: center; /* Center content vertically */ | |
text-align: left; /* Align text to the left */ | |
} | |
/* Slim scrollbar for people preview */ | |
.people-preview::-webkit-scrollbar { | |
width: 4px; /* Slim scrollbar */ | |
} | |
.people-preview::-webkit-scrollbar-track { | |
background: #374151; /* bg-slate-700 */ | |
border-radius: 10px; | |
} | |
.people-preview::-webkit-scrollbar-thumb { | |
background: #6b7280; /* bg-slate-500 */ | |
border-radius: 10px; | |
} | |
.people-preview::-webkit-scrollbar-thumb:hover { | |
background: #9ca3af; /* hover:bg-slate-400 */ | |
} | |
/* Style for single-person graves */ | |
.single-person { | |
margin-top: 4px; /* Minimal spacing below grave number */ | |
text-align: left; /* Align left for single person to avoid over-centering */ | |
} | |
</style> | |
</head> | |
<body class="bg-slate-900 text-slate-300 min-h-screen"> | |
<div class="container mx-auto p-4 md:p-8 max-w-6xl"> | |
<header class="text-center mb-8 md:mb-12"> | |
<h1 class="text-4xl md:text-5xl font-bold text-white"> | |
<span class="inline-block mr-2 text-sky-400">✟</span> | |
Cimitirul "Sf. Gheorghe"- Botești | |
</h1> | |
<p class="text-slate-400 mt-2 text-lg">..........................</p> | |
</header> | |
<main> | |
<div class="controls mb-8 p-4 bg-slate-800 rounded-lg shadow-lg flex flex-col sm:flex-row justify-between items-center gap-4"> | |
<button id="addGraveBtn" class="btn-primary w-full sm:w-auto py-2 px-4 rounded-md shadow-md hover:shadow-lg transition-shadow duration-200 flex items-center justify-center" aria-label="Adaugă un mormânt nou"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"> | |
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" /> | |
</svg> | |
Adaugă Mormânt Nou | |
</button> | |
<input type="text" id="searchInput" placeholder="Caută morminte (după număr, nume, etc...)" class="input-style placeholder-text-color w-full sm:w-1/2 lg:w-1/3 p-2 rounded-md border focus:ring-1 focus:outline-none" aria-label="Caută morminte"> | |
</div> | |
<div id="loadingIndicator" class="text-center py-4"></div> | |
<div id="gravesGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> | |
</div> | |
</main> | |
</div> | |
<div id="graveModal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50" style="display: none;" role="dialog" aria-modal="true"> | |
<div class="bg-slate-800 p-6 rounded-lg shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col"> | |
<div class="flex justify-between items-center mb-6"> | |
<h2 id="modalTitle" class="text-2xl font-semibold text-white card-title">Adaugă Mormânt Nou</h2> | |
<button id="closeGraveModalBtn" class="text-slate-400 hover:text-white text-2xl" aria-label="Închide modal mormânt">×</button> | |
</div> | |
<form id="graveForm" class="flex-grow overflow-y-auto modal-content-scrollable pr-2"> | |
<div class="mb-4"> | |
<label for="graveNumber" class="block text-sm font-medium text-slate-300 mb-1">Număr Mormânt</label> | |
<input type="text" id="graveNumber" class="input-style placeholder-text-color w-full p-2 rounded-md border focus:ring-1 focus:outline-none" placeholder="ex: G-001, Secțiunea A-12" aria-required="false"> | |
</div> | |
<div class="mb-4"> | |
<label for="graveComments" class="block text-sm font-medium text-slate-300 mb-1">Detalii (opțional)</label> | |
<textarea id="graveComments" rows="3" class="input-style placeholder-text-color w-full p-2 rounded-md border focus:ring-1 focus:outline-none" placeholder="Detalii despre acest mormânt..."></textarea> | |
</div> | |
<div class="mb-4"> | |
<label for="gravePhoto" class="block text-sm font-medium text-slate-300 mb-1">Fotografie Mormânt (opțional)</label> | |
<input type="file" id="gravePhoto" accept="image/*" class="w-full text-sm text-slate-400 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-sky-500 file:text-white hover:file:bg-sky-600" aria-label="Încarcă fotografie mormânt"> | |
<img id="gravePhotoPreview" src="" alt="Previzualizare fotografie mormânt" class="mt-3 rounded-md max-h-48 w-auto object-contain hidden"> | |
</div> | |
<div class="persons-section mt-6 pt-4 border-t border-slate-700"> | |
<div class="flex justify-between items-center mb-3"> | |
<h3 class="text-xl font-semibold text-white">Persoane în Acest Mormânt</h3> | |
<button type="button" id="addPersonToGraveBtn" class="btn-secondary text-sm py-1 px-3 rounded-md flex items-center" aria-label="Adaugă o persoană"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" /></svg> | |
Adaugă o persoană | |
</button> | |
</div> | |
<div id="personsListContainer"> | |
</div> | |
</div> | |
</form> | |
<div class="modal-actions mt-6 pt-4 border-t border-slate-700 flex justify-end gap-3"> | |
<button type="button" id="cancelGraveBtn" class="btn-secondary py-2 px-4 rounded-md" aria-label="Anulează modificările mormântului">Anulează</button> | |
<button type="submit" form="graveForm" class="btn-primary py-2 px-4 rounded-md" aria-label="Salvează mormântul">Salvează Mormânt</button> | |
</div> | |
</div> | |
</div> | |
<div id="personModal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-[60]" style="display: none;" role="dialog" aria-modal="true"> | |
<div class="bg-slate-800 p-6 rounded-lg shadow-2xl w-full max-w-lg max-h-[90vh] flex flex-col"> | |
<div class="flex justify-between items-center mb-6"> | |
<h2 id="personModalTitle" class="text-2xl font-semibold text-white card-title">Adaugă Persoană</h2> | |
<button id="closePersonModalBtn" class="text-slate-400 hover:text-white text-2xl" aria-label="Închide modal persoană">×</button> | |
</div> | |
<form id="personForm" class="flex-grow overflow-y-auto modal-content-scrollable pr-2"> | |
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4"> | |
<div> | |
<label for="personFirstName" class="block text-sm font-medium text-slate-300 mb-1">Prenume</label> | |
<input type="text" id="personFirstName" class="input-style placeholder-text-color w-full p-2 rounded-md border focus:ring-1 focus:outline-none" placeholder="ex: Ion"> | |
</div> | |
<div> | |
<label for="personLastName" class="block text-sm font-medium text-slate-300 mb-1">Nume de Familie</label> | |
<input type="text" id="personLastName" class="input-style placeholder-text-color w-full p-2 rounded-md border focus:ring-1 focus:outline-none" placeholder="ex: Popescu"> | |
</div> | |
</div> | |
<p class="text-xs text-slate-500 mb-4 -mt-2">Este necesar cel puțin un nume.</p> | |
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4"> | |
<div> | |
<label for="personBirthDate" class="block text-sm font-medium text-slate-300 mb-1">Data Nașterii</label> | |
<input type="date" id="personBirthDate" class="input-style w-full p-2 rounded-md border focus:ring-1 focus:outline-none"> | |
</div> | |
<div> | |
<label for="personDeathDate" class="block text-sm font-medium text-slate-300 mb-1">Data Decesului</label> | |
<input type="date" id="personDeathDate" class="input-style w-full p-2 rounded-md border focus:ring-1 focus:outline-none"> | |
</div> | |
</div> | |
<div class="mb-4"> | |
<label for="personNotes" class="block text-sm font-medium text-slate-300 mb-1">Informații (opțional)</label> | |
<textarea id="personNotes" rows="3" class="input-style placeholder-text-color w-full p-2 rounded-md border focus:ring-1 focus:outline-none" placeholder="..."></textarea> | |
</div> | |
</form> | |
<div class="modal-actions mt-6 pt-4 border-t border-slate-700 flex justify-end gap-3"> | |
<button type="button" id="cancelPersonBtn" class="btn-secondary py-2 px-4 rounded-md" aria-label="Anulează modificările persoanei">Anulează</button> | |
<button type="submit" form="personForm" class="btn-primary py-2 px-4 rounded-md" aria-label="Salvează persoana">Salvează Persoană</button> | |
</div> | |
</div> | |
</div> | |
<div id="alertModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4 z-[70]" style="display: none;" role="dialog" aria-modal="true"> | |
<div class="bg-slate-800 p-6 rounded-lg shadow-xl w-full max-w-sm text-center"> | |
<h3 class="text-lg font-medium text-white mb-4">Atenție!</h3> | |
<p id="alertMessage" class="text-slate-300 mb-6"></p> | |
<button id="alertOkBtn" class="btn-primary py-2 px-6 rounded-md w-full" aria-label="Confirmă alerta">OK</button> | |
</div> | |
</div> | |
<div id="confirmModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4 z-[70]" style="display: none;" role="dialog" aria-modal="true"> | |
<div class="bg-slate-800 p-6 rounded-lg shadow-xl w-full max-w-sm text-center"> | |
<h3 class="text-lg font-medium text-white mb-4">Confirmare</h3> | |
<p id="confirmMessage" class="text-slate-300 mb-6"></p> | |
<div class="flex justify-center gap-4"> | |
<button id="confirmNoBtn" class="btn-secondary py-2 px-6 rounded-md" aria-label="Anulează acțiunea">Nu</button> | |
<button id="confirmYesBtn" class="btn-primary py-2 px-6 rounded-md" aria-label="Confirmă acțiunea">Da</button> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// --- DOM ELEMENT SELECTORS --- | |
const gravesGrid = document.getElementById('gravesGrid'); | |
const searchInput = document.getElementById('searchInput'); | |
const addGraveBtn = document.getElementById('addGraveBtn'); | |
const loadingIndicator = document.getElementById('loadingIndicator'); | |
// Grave Modal Elements | |
const graveModal = document.getElementById('graveModal'); | |
const modalTitle = document.getElementById('modalTitle'); | |
const closeGraveModalBtn = document.getElementById('closeGraveModalBtn'); | |
const cancelGraveBtn = document.getElementById('cancelGraveBtn'); | |
const graveForm = document.getElementById('graveForm'); | |
const graveNumberInput = document.getElementById('graveNumber'); | |
const graveCommentsInput = document.getElementById('graveComments'); | |
const gravePhotoInput = document.getElementById('gravePhoto'); | |
const gravePhotoPreview = document.getElementById('gravePhotoPreview'); | |
const personsListContainer = document.getElementById('personsListContainer'); | |
const addPersonToGraveBtn = document.getElementById('addPersonToGraveBtn'); | |
// Person Modal Elements | |
const personModal = document.getElementById('personModal'); | |
const personModalTitle = document.getElementById('personModalTitle'); | |
const closePersonModalBtn = document.getElementById('closePersonModalBtn'); | |
const cancelPersonBtn = document.getElementById('cancelPersonBtn'); | |
const personForm = document.getElementById('personForm'); | |
const personFirstNameInput = document.getElementById('personFirstName'); | |
const personLastNameInput = document.getElementById('personLastName'); | |
const personBirthDateInput = document.getElementById('personBirthDate'); | |
const personDeathDateInput = document.getElementById('personDeathDate'); | |
const personNotesInput = document.getElementById('personNotes'); | |
// Custom Alert & Confirm Modals | |
const alertModal = document.getElementById('alertModal'); | |
const alertMessage = document.getElementById('alertMessage'); | |
const alertOkBtn = document.getElementById('alertOkBtn'); | |
const confirmModal = document.getElementById('confirmModal'); | |
const confirmMessage = document.getElementById('confirmMessage'); | |
const confirmYesBtn = document.getElementById('confirmYesBtn'); | |
const confirmNoBtn = document.getElementById('confirmNoBtn'); | |
// --- STATE MANAGEMENT --- | |
let graves = []; | |
let currentEditingGraveId = null; | |
let currentEditingPersonId = null; | |
let modalGraveData = null; | |
const PHOTO_MAX_SIZE_MB = 2; | |
// --- DATA PERSISTENCE --- | |
const loadGraves = () => { | |
try { | |
const storedGraves = localStorage.getItem('cemeteryData'); | |
graves = storedGraves ? JSON.parse(storedGraves) : []; | |
} catch (e) { | |
console.error("Error loading data from localStorage", e); | |
graves = []; | |
showAlert("A apărut o eroare la încărcarea datelor. Este posibil ca datele salvate anterior să fie corupte."); | |
} | |
}; | |
const saveGraves = () => { | |
try { | |
localStorage.setItem('cemeteryData', JSON.stringify(graves)); | |
} catch (e) { | |
console.error("Error saving data to localStorage", e); | |
showAlert("A apărut o eroare la salvarea datelor. Modificările recente s-ar putea să nu persiste."); | |
} | |
}; | |
// --- UTILITY & HELPER FUNCTIONS --- | |
const generateUniqueId = () => `id_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; | |
const formatDateForDisplay = (dateString) => { | |
if (!dateString) return "N/A"; | |
try { | |
const date = new Date(dateString); | |
if (isNaN(date.getTime())) return "N/A"; | |
return new Intl.DateTimeFormat('ro-RO', { year: 'numeric', month: 'long', day: 'numeric' }).format(date); | |
} catch { | |
return "N/A"; | |
} | |
}; | |
const formatDateForCard = (dateString) => { | |
if (!dateString) return "N/A"; | |
try { | |
const date = new Date(dateString); | |
if (isNaN(date.getTime())) return "N/A"; | |
return new Intl.DateTimeFormat('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(date); | |
} catch { | |
return "N/A"; | |
} | |
}; | |
const showAlert = (message) => { | |
alertMessage.textContent = message; | |
alertModal.style.display = 'flex'; | |
}; | |
const showConfirm = (message, onConfirm) => { | |
confirmMessage.textContent = message; | |
confirmModal.style.display = 'flex'; | |
confirmYesBtn.onclick = () => { | |
onConfirm(); | |
confirmModal.style.display = 'none'; | |
}; | |
}; | |
const generatePlaceholderImage = (persons) => { | |
const personNames = persons.map(p => `${p.firstName || ''} ${p.lastName || ''}`.trim()).filter(Boolean); | |
const displayNames = personNames.length > 0 ? personNames : ['Fără Persoane']; | |
// Dynamically adjust font size based on number of names | |
let fontSize = 24; | |
if (displayNames.length > 6) fontSize = 20; | |
if (displayNames.length > 9) fontSize = 16; | |
// Calculate line height and total height for equal spacing | |
const lineHeight = fontSize * 1.2; // Reduced multiplier for tighter spacing | |
const totalHeight = displayNames.length * lineHeight; | |
const startY = (300 - totalHeight) / 2; // Center vertically | |
// Create <tspan> elements with explicit y positions for equal spacing | |
const textElements = displayNames.map((name, index) => | |
`<tspan x="50%" y="${startY + index * lineHeight}">${name}</tspan>` | |
).join(''); | |
const svg = ` | |
<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg"> | |
<rect width="100%" height="100%" fill="#374151"/> | |
<text font-family="Georgia, serif" font-size="${fontSize}" fill="#f1f5f9" text-anchor="middle" dominant-baseline="hanging"> | |
${textElements} | |
</text> | |
</svg>`; | |
return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}`; | |
}; | |
const compressImage = (file, quality = 0.7) => { | |
return new Promise((resolve, reject) => { | |
const reader = new FileReader(); | |
reader.readAsDataURL(file); | |
reader.onload = (event) => { | |
const img = new Image(); | |
img.src = event.target.result; | |
img.onload = () => { | |
const canvas = document.createElement('canvas'); | |
const ctx = canvas.getContext('2d'); | |
const MAX_WIDTH = 1024; | |
const MAX_HEIGHT = 768; | |
let { width, height } = img; | |
if (width > height) { | |
if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } | |
} else { | |
if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } | |
} | |
canvas.width = width; | |
canvas.height = height; | |
ctx.drawImage(img, 0, 0, width, height); | |
resolve(canvas.toDataURL('image/jpeg', quality)); | |
}; | |
img.onerror = reject; | |
}; | |
reader.onerror = reject; | |
}); | |
}; | |
// --- RENDERING FUNCTIONS --- | |
const renderGravesGrid = (gravesToRender = graves) => { | |
gravesGrid.innerHTML = ''; | |
loadingIndicator.innerHTML = ''; | |
if (gravesToRender.length === 0) { | |
const isSearching = searchInput.value.trim() !== ''; | |
const title = isSearching ? "Niciun mormânt nu corespunde căutării" : "Niciun mormânt înregistrat"; | |
const subtitle = isSearching ? "Încercați alte cuvinte cheie sau ștergeți căutarea." : "Începeți prin a adăuga primul mormânt folosind butonul de mai sus."; | |
gravesGrid.innerHTML = `<div class="col-span-full text-center py-10 px-4 bg-slate-800 rounded-lg"><p class="text-xl font-semibold text-white">${title}</p><p class="text-slate-400 mt-2">${subtitle}</p></div>`; | |
return; | |
} | |
gravesToRender.forEach(grave => { | |
const card = document.createElement('div'); | |
card.className = "bg-slate-800 rounded-lg shadow-lg overflow-hidden flex flex-col transition-transform transform hover:scale-[1.02]"; | |
const photoSrc = grave.photo || generatePlaceholderImage(grave.persons); | |
let personsHtml = ''; | |
if (grave.persons.length === 1) { | |
// Single person: Place directly under grave number | |
const person = grave.persons[0]; | |
const personName = `${person.firstName || ''} ${person.lastName || ''}`.trim() || 'Persoană Fără Nume'; | |
personsHtml = ` | |
<div class="single-person"> | |
<p class="font-semibold text-slate-200">${personName}</p> | |
<p class="text-xs"> | |
<span class="text-sky-400">N:</span> ${formatDateForCard(person.birthDate)} | |
<span class="text-sky-400 ml-2">D:</span> ${formatDateForCard(person.deathDate)} | |
</p> | |
</div> | |
`; | |
} else { | |
// Multiple persons or none: Use scrollable container | |
personsHtml = '<div class="people-preview">'; | |
if (grave.persons.length > 0) { | |
grave.persons.forEach(person => { | |
const personName = `${person.firstName || ''} ${person.lastName || ''}`.trim() || 'Persoană Fără Nume'; | |
personsHtml += ` | |
<div> | |
<p class="text-l font-semibold text-slate-200">${personName}</p> | |
<p class="text-xs"> | |
<span class="text-sky-400">N:</span> ${formatDateForCard(person.birthDate)} | |
<span class="text-sky-400 ml-2">D:</span> ${formatDateForCard(person.deathDate)} | |
</p> | |
</div> | |
`; | |
}); | |
} else { | |
personsHtml += '<p class="text-slate-400 text-sm">Nicio persoană înregistrată.</p>'; | |
} | |
personsHtml += '</div>'; | |
} | |
card.innerHTML = ` | |
<img src="${photoSrc}" alt="Fotografie mormânt ${grave.number}" class="w-full h-48 object-cover"> | |
<div class="p-4 flex-grow flex flex-col"> | |
<div class="flex justify-between items-center mb-2"> | |
<h3 class="text-2xl text-white card-title">${grave.number || 'Mormânt Fără Număr'}</h3> | |
<div class="flex gap-3"> | |
<button class="edit-btn btn-secondary text-sm py-1 px-3 rounded-md" data-id="${grave.id}">Editează</button> | |
<button class="delete-btn btn-danger text-sm py-1 px-3 rounded-md" data-id="${grave.id}">Șterge</button> | |
</div> | |
</div> | |
${personsHtml} | |
<p class="text-slate-400 text-sm mt-2 flex-grow">${grave.comments || ' '}</p> | |
</div> | |
`; | |
gravesGrid.appendChild(card); | |
}); | |
document.querySelectorAll('.edit-btn').forEach(btn => btn.addEventListener('click', () => openGraveModal({ isEdit: true, graveId: btn.dataset.id }))); | |
document.querySelectorAll('.delete-btn').forEach(btn => btn.addEventListener('click', () => handleDeleteGrave(btn.dataset.id))); | |
}; | |
const renderPersonsInModal = (persons) => { | |
personsListContainer.innerHTML = ''; | |
if (persons.length === 0) { | |
personsListContainer.innerHTML = '<p class="text-slate-400 text-center py-3">Nicio persoană nu a fost adăugată.</p>'; | |
return; | |
} | |
persons.forEach(person => { | |
const personDiv = document.createElement('div'); | |
personDiv.className = 'p-3 mb-2 bg-slate-700 rounded-md flex justify-between items-start'; | |
const personName = `${person.firstName || ''} ${person.lastName || ''}`.trim() || 'Persoană Fără Nume'; | |
personDiv.innerHTML = ` | |
<div class="text-sm"> | |
<p class="font-bold text-slate-100">${personName}</p> | |
<p class="text-slate-300">Născut/ă: ${formatDateForDisplay(person.birthDate)}</p> | |
<p class="text-slate-300">Decedat/ă: ${formatDateForDisplay(person.deathDate)}</p> | |
${person.notes ? `<p class="text-slate-400 mt-1 italic">Note: ${person.notes}</p>` : ''} | |
</div> | |
<div class="flex-shrink-0 flex gap-2 ml-2"> | |
<button type="button" class="edit-person-btn text-sky-400 hover:text-sky-300" data-id="${person.id}" aria-label="Editează persoana"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" /><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" /></svg> | |
</button> | |
<button type="button" class="delete-person-btn text-red-500 hover:text-red-400" data-id="${person.id}" aria-label="Șterge persoana"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm4 0a1 1 0 012 0v6a1 1 0 11-2 0V8z" clip-rule="evenodd" /></svg> | |
</button> | |
</div> | |
`; | |
personsListContainer.appendChild(personDiv); | |
}); | |
document.querySelectorAll('.edit-person-btn').forEach(btn => btn.addEventListener('click', () => openPersonModal({ isEdit: true, personId: btn.dataset.id }))); | |
document.querySelectorAll('.delete-person-btn').forEach(btn => btn.addEventListener('click', () => handleDeletePerson(btn.dataset.id))); | |
}; | |
// --- MODAL MANAGEMENT --- | |
const openGraveModal = ({ isEdit = false, graveId = null }) => { | |
graveForm.reset(); | |
gravePhotoPreview.src = '#'; | |
gravePhotoPreview.classList.add('hidden'); | |
personsListContainer.innerHTML = ''; | |
currentEditingPersonId = null; | |
if (isEdit) { | |
const grave = graves.find(g => g.id === graveId); | |
if (!grave) { showAlert("Eroare: Mormântul nu a fost găsit pentru editare."); return; } | |
currentEditingGraveId = graveId; | |
modalGraveData = JSON.parse(JSON.stringify(grave)); | |
modalTitle.textContent = "Modifică Mormântul"; | |
graveNumberInput.value = modalGraveData.number; | |
graveCommentsInput.value = modalGraveData.comments; | |
if (modalGraveData.photo) { | |
gravePhotoPreview.src = modalGraveData.photo; | |
gravePhotoPreview.classList.remove('hidden'); | |
} | |
} else { | |
currentEditingGraveId = null; | |
const nextGraveNumber = graves.length > 0 ? Math.max(...graves.map(g => parseInt(g.number, 10) || 0).filter(Number.isFinite)) + 1 : 1; | |
modalGraveData = { id: generateUniqueId(), number: String(nextGraveNumber), comments: '', photo: null, persons: [] }; | |
modalTitle.textContent = "Adaugă Mormânt Nou"; | |
graveNumberInput.placeholder = `ex: ${nextGraveNumber} `; | |
} | |
renderPersonsInModal(modalGraveData.persons); | |
graveModal.style.display = 'flex'; | |
}; | |
const closeGraveModal = () => { graveModal.style.display = 'none'; modalGraveData = null; currentEditingGraveId = null; }; | |
const openPersonModal = ({ isEdit = false, personId = null }) => { | |
if (!modalGraveData) { showAlert("Eroare: Vă rugăm să deschideți sau să începeți crearea unui mormânt înainte de a adăuga persoane."); return; } | |
personForm.reset(); | |
if (isEdit) { | |
const person = modalGraveData.persons.find(p => p.id === personId); | |
if (!person) return; | |
currentEditingPersonId = personId; | |
personModalTitle.textContent = "Editează Persoană"; | |
personFirstNameInput.value = person.firstName; | |
personLastNameInput.value = person.lastName; | |
personBirthDateInput.value = person.birthDate; | |
personDeathDateInput.value = person.deathDate; | |
personNotesInput.value = person.notes; | |
} else { | |
currentEditingPersonId = null; | |
personModalTitle.textContent = "Adaugă Persoană"; | |
} | |
personModal.style.display = 'flex'; | |
}; | |
const closePersonModal = () => { personModal.style.display = 'none'; currentEditingPersonId = null; }; | |
// --- EVENT HANDLERS & LOGIC --- | |
const handleGraveFormSubmit = (e) => { | |
e.preventDefault(); | |
modalGraveData.number = graveNumberInput.value.trim() || modalGraveData.number; | |
modalGraveData.comments = graveCommentsInput.value.trim(); | |
if (currentEditingGraveId) { | |
const index = graves.findIndex(g => g.id === currentEditingGraveId); | |
if (index !== -1) graves[index] = modalGraveData; | |
else { showAlert("Eroare: Nu s-a putut găsi mormântul pentru actualizare."); return; } | |
} else { | |
graves.push(modalGraveData); | |
} | |
saveGraves(); | |
renderGravesGrid(); | |
closeGraveModal(); | |
}; | |
const handlePersonFormSubmit = (e) => { | |
e.preventDefault(); | |
if (!modalGraveData) { showAlert("Eroare: Nu există date active pentru mormânt."); return; } | |
const firstName = personFirstNameInput.value.trim(); | |
const lastName = personLastNameInput.value.trim(); | |
if (!firstName && !lastName) { showAlert("Este necesar cel puțin Prenume sau Nume de Familie."); return; } | |
const personData = { firstName, lastName, birthDate: personBirthDateInput.value, deathDate: personDeathDateInput.value, notes: personNotesInput.value.trim() }; | |
if (currentEditingPersonId) { | |
const index = modalGraveData.persons.findIndex(p => p.id === currentEditingPersonId); | |
if (index !== -1) modalGraveData.persons[index] = { ...modalGraveData.persons[index], ...personData }; | |
} else { | |
personData.id = generateUniqueId(); | |
modalGraveData.persons.push(personData); | |
} | |
renderPersonsInModal(modalGraveData.persons); | |
closePersonModal(); | |
}; | |
const handleDeleteGrave = (graveId) => { | |
showConfirm("Sigur doriți să ștergeți acest mormânt și toate persoanele asociate? Această acțiune nu poate fi anulată.", () => { | |
graves = graves.filter(g => g.id !== graveId); | |
saveGraves(); | |
handleSearchInput(); // Re-render based on current search | |
}); | |
}; | |
const handleDeletePerson = (personId) => { | |
if (!modalGraveData) return; | |
showConfirm("Sigur doriți să eliminați această persoană din detaliile mormântului curent?", () => { | |
modalGraveData.persons = modalGraveData.persons.filter(p => p.id !== personId); | |
renderPersonsInModal(modalGraveData.persons); | |
}); | |
}; | |
const handlePhotoChange = async (e) => { | |
const file = e.target.files[0]; | |
if (!file) return; | |
loadingIndicator.textContent = "Se procesează imaginea..."; | |
try { | |
let imageDataUrl; | |
if (file.size > PHOTO_MAX_SIZE_MB * 1024 * 1024) { | |
showAlert(`Imaginea este prea mare. Se va încerca comprimarea...`); | |
imageDataUrl = await compressImage(file); | |
} else { | |
imageDataUrl = await new Promise((resolve, reject) => { | |
const reader = new FileReader(); | |
reader.onload = () => resolve(reader.result); | |
reader.onerror = reject; | |
reader.readAsDataURL(file); | |
}); | |
} | |
modalGraveData.photo = imageDataUrl; | |
gravePhotoPreview.src = imageDataUrl; | |
gravePhotoPreview.classList.remove('hidden'); | |
} catch (error) { | |
console.error(error); | |
showAlert("Eroare la citirea fișierului. Vă rugăm să încercați să selectați din nou."); | |
} finally { | |
loadingIndicator.textContent = ""; | |
} | |
}; | |
const handleSearchInput = () => { | |
const query = searchInput.value.toLowerCase().trim(); | |
const filteredGraves = graves.filter(grave => { | |
const graveInfo = [grave.number, grave.comments].join(' ').toLowerCase(); | |
if (graveInfo.includes(query)) return true; | |
return grave.persons.some(person => { | |
const personInfo = [person.firstName, person.lastName, person.notes].join(' ').toLowerCase(); | |
return personInfo.includes(query); | |
}); | |
}); | |
renderGravesGrid(filteredGraves); | |
}; | |
// --- SETUP EVENT LISTENERS --- | |
const setupEventListeners = () => { | |
addGraveBtn.addEventListener('click', () => openGraveModal({ isEdit: false })); | |
closeGraveModalBtn.addEventListener('click', closeGraveModal); | |
cancelGraveBtn.addEventListener('click', closeGraveModal); | |
graveForm.addEventListener('submit', handleGraveFormSubmit); | |
gravePhotoInput.addEventListener('change', handlePhotoChange); | |
addPersonToGraveBtn.addEventListener('click', () => openPersonModal({ isEdit: false })); | |
closePersonModalBtn.addEventListener('click', closePersonModal); | |
cancelPersonBtn.addEventListener('click', closePersonModal); | |
personForm.addEventListener('submit', handlePersonFormSubmit); | |
alertOkBtn.addEventListener('click', () => alertModal.style.display = 'none'); | |
confirmNoBtn.addEventListener('click', () => confirmModal.style.display = 'none'); | |
searchInput.addEventListener('input', handleSearchInput); | |
window.addEventListener('keydown', (e) => { | |
if (e.key === 'Escape') { | |
if (personModal.style.display === 'flex') closePersonModal(); | |
else if (graveModal.style.display === 'flex') closeGraveModal(); | |
else if (alertModal.style.display === 'flex') alertModal.style.display = 'none'; | |
else if (confirmModal.style.display === 'flex') confirmModal.style.display = 'none'; | |
} | |
}); | |
}; | |
// --- INITIALIZATION --- | |
const initialize = () => { | |
loadingIndicator.textContent = "Se încarcă datele..."; | |
loadGraves(); | |
renderGravesGrid(); | |
setupEventListeners(); | |
}; | |
initialize(); | |
}); | |
</script> | |
<script> | |
(function(){ | |
const BACKUP_URL = 'https://api.apispreadsheets.com/data/VVWozHAy5BosLaA3/'; | |
const IMGBB_API_KEY = '9498bf8da3ec348c0512a97edd9666c0'; | |
const CEMETERY_KEY = 'cemeteryData'; | |
const LAST_KEY = 'lastBackup'; | |
const BACKUP_INTERVAL_MS = 5 * 60 * 60 * 1000; // 5 hours | |
const initialRaw = localStorage.getItem(CEMETERY_KEY); | |
let hasChanged = false; | |
// Override setItem to detect changes | |
const originalSetItem = localStorage.setItem; | |
localStorage.setItem = function(key, value) { | |
if (key === CEMETERY_KEY && value !== initialRaw) { | |
hasChanged = true; | |
} | |
originalSetItem.apply(this, arguments); | |
}; | |
async function uploadToImgbb(base64) { | |
const form = new FormData(); | |
form.append("key", IMGBB_API_KEY); | |
form.append("image", base64.split(',')[1]); // remove "data:image/...;base64," | |
const res = await fetch("https://api.imgbb.com/1/upload", { | |
method: "POST", | |
body: form | |
}); | |
if (!res.ok) throw new Error("ImgBB upload failed"); | |
const json = await res.json(); | |
return json.data.url; | |
} | |
async function prepareBackup() { | |
const raw = localStorage.getItem(CEMETERY_KEY); | |
if (!raw) return; | |
let parsed; | |
try { | |
parsed = JSON.parse(raw); | |
} catch (e) { | |
console.error("Invalid JSON in cemeteryData", e); | |
return; | |
} | |
// Deep clone to avoid changing localStorage | |
const cloned = JSON.parse(JSON.stringify(parsed)); | |
for (let entry of cloned) { | |
if (entry.photo && typeof entry.photo === "string" && entry.photo.startsWith("data:image/")) { | |
try { | |
const url = await uploadToImgbb(entry.photo); | |
entry.photo = url; | |
} catch (e) { | |
console.error("Error uploading photo to ImgBB:", e); | |
} | |
} | |
} | |
// Now send the modified clone (not touching localStorage) | |
const payload = JSON.stringify({ data: { data: JSON.stringify(cloned) } }); | |
localStorage.setItem(LAST_KEY, new Date().toISOString()); | |
if (navigator.sendBeacon) { | |
const blob = new Blob([payload], { type: 'application/json' }); | |
navigator.sendBeacon(BACKUP_URL, blob); | |
} else { | |
fetch(BACKUP_URL, { | |
method: "POST", | |
headers: { "Content-Type": "application/json" }, | |
body: payload | |
}).catch(err => console.error("Backup failed:", err)); | |
} | |
} | |
function shouldBackup() { | |
const last = localStorage.getItem(LAST_KEY); | |
if (!last) return true; | |
return Date.now() - new Date(last).getTime() > BACKUP_INTERVAL_MS; | |
} | |
// Auto-backup on load if time expired | |
window.addEventListener('load', () => { | |
if (shouldBackup()) { | |
prepareBackup(); | |
} | |
}); | |
// Backup on unload if data changed | |
window.addEventListener('beforeunload', (e) => { | |
const current = localStorage.getItem(CEMETERY_KEY); | |
if (current !== initialRaw || hasChanged) { | |
navigator.sendBeacon || e.preventDefault(); // fallback logic | |
prepareBackup(); | |
} | |
}); | |
})(); | |
</script> | |
</body> | |
</html> |