vericudebuget commited on
Commit
05b4eb3
·
verified ·
1 Parent(s): c8b70e7

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1 -805
index.html CHANGED
@@ -1,805 +1 @@
1
- <!DOCTYPE html>
2
- <html lang="ro">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Cimitirul "Sf. Gheorghe"- Botești</title>
7
- <link rel="icon" href="https://huggingface.co/spaces/vericudebuget/cimitir/resolve/main/Screenshot%202025-06-07%20223622.png" type="image/x-icon">
8
- <script src="https://cdn.tailwindcss.com"></script>
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Montserrat:wght@300;400;600;700&display=swap" rel="stylesheet">
10
- <style>
11
- body {
12
- font-family: 'Montserrat', sans-serif;
13
- background-color: #111827; /* bg-slate-900 */
14
- color: #d1d5db; /* text-slate-300 */
15
- font-weight: 400; /* Default font weight */
16
- }
17
- /* Use lighter font weight for main titles */
18
- h1, .card-title {
19
- font-weight: 300;
20
- }
21
- .modal-content-scrollable::-webkit-scrollbar {
22
- width: 8px;
23
- }
24
- .modal-content-scrollable::-webkit-scrollbar-track {
25
- background: #374151; /* bg-slate-700 */
26
- border-radius: 10px;
27
- }
28
- .modal-content-scrollable::-webkit-scrollbar-thumb {
29
- background: #4b5563; /* bg-slate-600 */
30
- border-radius: 10px;
31
- }
32
- .modal-content-scrollable::-webkit-scrollbar-thumb:hover {
33
- background: #6b7280; /* bg-slate-500 */
34
- }
35
- .placeholder-text-color::placeholder {
36
- color: #9ca3af;
37
- }
38
- .btn-primary {
39
- background-color: #0ea5e9; /* bg-sky-500 */
40
- color: white;
41
- }
42
- .btn-primary:hover {
43
- background-color: #0284c7; /* hover:bg-sky-600 */
44
- }
45
- .btn-secondary {
46
- background-color: #4b5563; /* bg-slate-600 */
47
- color: white;
48
- }
49
- .btn-secondary:hover {
50
- background-color: #6b7280; /* hover:bg-slate-500 */
51
- }
52
- .btn-danger {
53
- background-color: #dc2626; /* bg-red-600 */
54
- color: white;
55
- }
56
- .btn-danger:hover {
57
- background-color: #b91c1c; /* hover:bg-red-700 */
58
- }
59
- .input-style {
60
- background-color: #374151; /* bg-slate-700 */
61
- border-color: #4b5563; /* border-slate-600 */
62
- color: #e5e7eb; /* text-slate-200 */
63
- }
64
- .input-style:focus {
65
- border-color: #0ea5e9; /* focus:border-sky-500 */
66
- }
67
-
68
- /* Replace the existing .people-preview and related styles in the <style> section */
69
- .people-preview {
70
- max-height: 120px; /* Limit height to roughly 2 people */
71
- overflow-y: auto; /* Enable scrolling for overflow */
72
- background-color: #2d3748; /* Slightly brighter bg-slate-800 */
73
- border: 1px solid #4b5563; /* Brighter border-slate-600 */
74
- border-radius: 6px;
75
- padding: 8px;
76
- padding-top: 10px;
77
- margin-top: 8px;
78
- display: flex; /* Use flexbox for vertical centering */
79
- flex-direction: column; /* Stack items vertically */
80
- justify-content: center; /* Center content vertically */
81
- text-align: left; /* Align text to the left */
82
- }
83
-
84
- /* Slim scrollbar for people preview */
85
- .people-preview::-webkit-scrollbar {
86
- width: 4px; /* Slim scrollbar */
87
- }
88
- .people-preview::-webkit-scrollbar-track {
89
- background: #374151; /* bg-slate-700 */
90
- border-radius: 10px;
91
- }
92
- .people-preview::-webkit-scrollbar-thumb {
93
- background: #6b7280; /* bg-slate-500 */
94
- border-radius: 10px;
95
- }
96
- .people-preview::-webkit-scrollbar-thumb:hover {
97
- background: #9ca3af; /* hover:bg-slate-400 */
98
- }
99
-
100
- /* Style for single-person graves */
101
- .single-person {
102
- margin-top: 4px; /* Minimal spacing below grave number */
103
- text-align: left; /* Align left for single person to avoid over-centering */
104
- }
105
- </style>
106
- </head>
107
- <body class="bg-slate-900 text-slate-300 min-h-screen">
108
-
109
- <div class="container mx-auto p-4 md:p-8 max-w-6xl">
110
- <header class="text-center mb-8 md:mb-12">
111
- <h1 class="text-4xl md:text-5xl font-bold text-white">
112
- <span class="inline-block mr-2 text-sky-400">✟</span>
113
- Cimitirul "Sf. Gheorghe"- Botești
114
- </h1>
115
- <p class="text-slate-400 mt-2 text-lg">..........................</p>
116
- </header>
117
-
118
- <main>
119
- <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">
120
- <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">
121
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
122
- <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" />
123
- </svg>
124
- Adaugă Mormânt Nou
125
- </button>
126
- <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">
127
- </div>
128
-
129
- <div id="loadingIndicator" class="text-center py-4"></div>
130
-
131
- <div id="gravesGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
132
- </div>
133
- </main>
134
- </div>
135
-
136
- <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">
137
- <div class="bg-slate-800 p-6 rounded-lg shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col">
138
- <div class="flex justify-between items-center mb-6">
139
- <h2 id="modalTitle" class="text-2xl font-semibold text-white card-title">Adaugă Mormânt Nou</h2>
140
- <button id="closeGraveModalBtn" class="text-slate-400 hover:text-white text-2xl" aria-label="Închide modal mormânt">×</button>
141
- </div>
142
- <form id="graveForm" class="flex-grow overflow-y-auto modal-content-scrollable pr-2">
143
- <div class="mb-4">
144
- <label for="graveNumber" class="block text-sm font-medium text-slate-300 mb-1">Număr Mormânt</label>
145
- <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">
146
- </div>
147
- <div class="mb-4">
148
- <label for="graveComments" class="block text-sm font-medium text-slate-300 mb-1">Detalii (opțional)</label>
149
- <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>
150
- </div>
151
- <div class="mb-4">
152
- <label for="gravePhoto" class="block text-sm font-medium text-slate-300 mb-1">Fotografie Mormânt (opțional)</label>
153
- <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">
154
- <img id="gravePhotoPreview" src="" alt="Previzualizare fotografie mormânt" class="mt-3 rounded-md max-h-48 w-auto object-contain hidden">
155
- </div>
156
-
157
- <div class="persons-section mt-6 pt-4 border-t border-slate-700">
158
- <div class="flex justify-between items-center mb-3">
159
- <h3 class="text-xl font-semibold text-white">Persoane în Acest Mormânt</h3>
160
- <button type="button" id="addPersonToGraveBtn" class="btn-secondary text-sm py-1 px-3 rounded-md flex items-center" aria-label="Adaugă o persoană">
161
- <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>
162
- Adaugă o persoană
163
- </button>
164
- </div>
165
- <div id="personsListContainer">
166
- </div>
167
- </div>
168
- </form>
169
- <div class="modal-actions mt-6 pt-4 border-t border-slate-700 flex justify-end gap-3">
170
- <button type="button" id="cancelGraveBtn" class="btn-secondary py-2 px-4 rounded-md" aria-label="Anulează modificările mormântului">Anulează</button>
171
- <button type="submit" form="graveForm" class="btn-primary py-2 px-4 rounded-md" aria-label="Salvează mormântul">Salvează Mormânt</button>
172
- </div>
173
- </div>
174
- </div>
175
-
176
- <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">
177
- <div class="bg-slate-800 p-6 rounded-lg shadow-2xl w-full max-w-lg max-h-[90vh] flex flex-col">
178
- <div class="flex justify-between items-center mb-6">
179
- <h2 id="personModalTitle" class="text-2xl font-semibold text-white card-title">Adaugă Persoană</h2>
180
- <button id="closePersonModalBtn" class="text-slate-400 hover:text-white text-2xl" aria-label="Închide modal persoană">×</button>
181
- </div>
182
- <form id="personForm" class="flex-grow overflow-y-auto modal-content-scrollable pr-2">
183
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
184
- <div>
185
- <label for="personFirstName" class="block text-sm font-medium text-slate-300 mb-1">Prenume</label>
186
- <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">
187
- </div>
188
- <div>
189
- <label for="personLastName" class="block text-sm font-medium text-slate-300 mb-1">Nume de Familie</label>
190
- <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">
191
- </div>
192
- </div>
193
- <p class="text-xs text-slate-500 mb-4 -mt-2">Este necesar cel puțin un nume.</p>
194
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
195
- <div>
196
- <label for="personBirthDate" class="block text-sm font-medium text-slate-300 mb-1">Data Nașterii</label>
197
- <input type="date" id="personBirthDate" class="input-style w-full p-2 rounded-md border focus:ring-1 focus:outline-none">
198
- </div>
199
- <div>
200
- <label for="personDeathDate" class="block text-sm font-medium text-slate-300 mb-1">Data Decesului</label>
201
- <input type="date" id="personDeathDate" class="input-style w-full p-2 rounded-md border focus:ring-1 focus:outline-none">
202
- </div>
203
- </div>
204
-
205
- <div class="mb-4">
206
- <label for="personNotes" class="block text-sm font-medium text-slate-300 mb-1">Informații (opțional)</label>
207
- <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>
208
- </div>
209
- </form>
210
- <div class="modal-actions mt-6 pt-4 border-t border-slate-700 flex justify-end gap-3">
211
- <button type="button" id="cancelPersonBtn" class="btn-secondary py-2 px-4 rounded-md" aria-label="Anulează modificările persoanei">Anulează</button>
212
- <button type="submit" form="personForm" class="btn-primary py-2 px-4 rounded-md" aria-label="Salvează persoana">Salvează Persoană</button>
213
- </div>
214
- </div>
215
- </div>
216
-
217
- <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">
218
- <div class="bg-slate-800 p-6 rounded-lg shadow-xl w-full max-w-sm text-center">
219
- <h3 class="text-lg font-medium text-white mb-4">Atenție!</h3>
220
- <p id="alertMessage" class="text-slate-300 mb-6"></p>
221
- <button id="alertOkBtn" class="btn-primary py-2 px-6 rounded-md w-full" aria-label="Confirmă alerta">OK</button>
222
- </div>
223
- </div>
224
-
225
- <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">
226
- <div class="bg-slate-800 p-6 rounded-lg shadow-xl w-full max-w-sm text-center">
227
- <h3 class="text-lg font-medium text-white mb-4">Confirmare</h3>
228
- <p id="confirmMessage" class="text-slate-300 mb-6"></p>
229
- <div class="flex justify-center gap-4">
230
- <button id="confirmNoBtn" class="btn-secondary py-2 px-6 rounded-md" aria-label="Anulează acțiunea">Nu</button>
231
- <button id="confirmYesBtn" class="btn-primary py-2 px-6 rounded-md" aria-label="Confirmă acțiunea">Da</button>
232
- </div>
233
- </div>
234
- </div>
235
-
236
- <script>
237
- document.addEventListener('DOMContentLoaded', () => {
238
- // --- DOM ELEMENT SELECTORS ---
239
- const gravesGrid = document.getElementById('gravesGrid');
240
- const searchInput = document.getElementById('searchInput');
241
- const addGraveBtn = document.getElementById('addGraveBtn');
242
- const loadingIndicator = document.getElementById('loadingIndicator');
243
-
244
- // Grave Modal Elements
245
- const graveModal = document.getElementById('graveModal');
246
- const modalTitle = document.getElementById('modalTitle');
247
- const closeGraveModalBtn = document.getElementById('closeGraveModalBtn');
248
- const cancelGraveBtn = document.getElementById('cancelGraveBtn');
249
- const graveForm = document.getElementById('graveForm');
250
- const graveNumberInput = document.getElementById('graveNumber');
251
- const graveCommentsInput = document.getElementById('graveComments');
252
- const gravePhotoInput = document.getElementById('gravePhoto');
253
- const gravePhotoPreview = document.getElementById('gravePhotoPreview');
254
- const personsListContainer = document.getElementById('personsListContainer');
255
- const addPersonToGraveBtn = document.getElementById('addPersonToGraveBtn');
256
-
257
- // Person Modal Elements
258
- const personModal = document.getElementById('personModal');
259
- const personModalTitle = document.getElementById('personModalTitle');
260
- const closePersonModalBtn = document.getElementById('closePersonModalBtn');
261
- const cancelPersonBtn = document.getElementById('cancelPersonBtn');
262
- const personForm = document.getElementById('personForm');
263
- const personFirstNameInput = document.getElementById('personFirstName');
264
- const personLastNameInput = document.getElementById('personLastName');
265
- const personBirthDateInput = document.getElementById('personBirthDate');
266
- const personDeathDateInput = document.getElementById('personDeathDate');
267
- const personNotesInput = document.getElementById('personNotes');
268
-
269
- // Custom Alert & Confirm Modals
270
- const alertModal = document.getElementById('alertModal');
271
- const alertMessage = document.getElementById('alertMessage');
272
- const alertOkBtn = document.getElementById('alertOkBtn');
273
- const confirmModal = document.getElementById('confirmModal');
274
- const confirmMessage = document.getElementById('confirmMessage');
275
- const confirmYesBtn = document.getElementById('confirmYesBtn');
276
- const confirmNoBtn = document.getElementById('confirmNoBtn');
277
-
278
- // --- STATE MANAGEMENT ---
279
- let graves = [];
280
- let currentEditingGraveId = null;
281
- let currentEditingPersonId = null;
282
- let modalGraveData = null;
283
-
284
- const PHOTO_MAX_SIZE_MB = 2;
285
-
286
- // --- DATA PERSISTENCE ---
287
- const loadGraves = () => {
288
- try {
289
- const storedGraves = localStorage.getItem('cemeteryData');
290
- graves = storedGraves ? JSON.parse(storedGraves) : [];
291
- } catch (e) {
292
- console.error("Error loading data from localStorage", e);
293
- graves = [];
294
- showAlert("A apărut o eroare la încărcarea datelor. Este posibil ca datele salvate anterior să fie corupte.");
295
- }
296
- };
297
-
298
- const saveGraves = () => {
299
- try {
300
- localStorage.setItem('cemeteryData', JSON.stringify(graves));
301
- } catch (e) {
302
- console.error("Error saving data to localStorage", e);
303
- showAlert("A apărut o eroare la salvarea datelor. Modificările recente s-ar putea să nu persiste.");
304
- }
305
- };
306
-
307
- // --- UTILITY & HELPER FUNCTIONS ---
308
- const generateUniqueId = () => `id_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
309
-
310
- const formatDateForDisplay = (dateString) => {
311
- if (!dateString) return "N/A";
312
- try {
313
- const date = new Date(dateString);
314
- if (isNaN(date.getTime())) return "N/A";
315
- return new Intl.DateTimeFormat('ro-RO', { year: 'numeric', month: 'long', day: 'numeric' }).format(date);
316
- } catch {
317
- return "N/A";
318
- }
319
- };
320
-
321
- const formatDateForCard = (dateString) => {
322
- if (!dateString) return "N/A";
323
- try {
324
- const date = new Date(dateString);
325
- if (isNaN(date.getTime())) return "N/A";
326
- return new Intl.DateTimeFormat('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(date);
327
- } catch {
328
- return "N/A";
329
- }
330
- };
331
-
332
- const showAlert = (message) => {
333
- alertMessage.textContent = message;
334
- alertModal.style.display = 'flex';
335
- };
336
-
337
- const showConfirm = (message, onConfirm) => {
338
- confirmMessage.textContent = message;
339
- confirmModal.style.display = 'flex';
340
- confirmYesBtn.onclick = () => {
341
- onConfirm();
342
- confirmModal.style.display = 'none';
343
- };
344
- };
345
-
346
- const generatePlaceholderImage = (persons) => {
347
- const personNames = persons.map(p => `${p.firstName || ''} ${p.lastName || ''}`.trim()).filter(Boolean);
348
- const displayNames = personNames.length > 0 ? personNames : ['Fără Persoane'];
349
-
350
- // Dynamically adjust font size based on number of names
351
- let fontSize = 24;
352
- if (displayNames.length > 6) fontSize = 20;
353
- if (displayNames.length > 9) fontSize = 16;
354
-
355
- // Calculate line height and total height for equal spacing
356
- const lineHeight = fontSize * 1.2; // Reduced multiplier for tighter spacing
357
- const totalHeight = displayNames.length * lineHeight;
358
- const startY = (300 - totalHeight) / 2; // Center vertically
359
-
360
- // Create <tspan> elements with explicit y positions for equal spacing
361
- const textElements = displayNames.map((name, index) =>
362
- `<tspan x="50%" y="${startY + index * lineHeight}">${name}</tspan>`
363
- ).join('');
364
-
365
- const svg = `
366
- <svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
367
- <rect width="100%" height="100%" fill="#374151"/>
368
- <text font-family="Georgia, serif" font-size="${fontSize}" fill="#f1f5f9" text-anchor="middle" dominant-baseline="hanging">
369
- ${textElements}
370
- </text>
371
- </svg>`;
372
- return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}`;
373
- };
374
-
375
- const compressImage = (file, quality = 0.7) => {
376
- return new Promise((resolve, reject) => {
377
- const reader = new FileReader();
378
- reader.readAsDataURL(file);
379
- reader.onload = (event) => {
380
- const img = new Image();
381
- img.src = event.target.result;
382
- img.onload = () => {
383
- const canvas = document.createElement('canvas');
384
- const ctx = canvas.getContext('2d');
385
- const MAX_WIDTH = 1024;
386
- const MAX_HEIGHT = 768;
387
- let { width, height } = img;
388
- if (width > height) {
389
- if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; }
390
- } else {
391
- if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; }
392
- }
393
- canvas.width = width;
394
- canvas.height = height;
395
- ctx.drawImage(img, 0, 0, width, height);
396
- resolve(canvas.toDataURL('image/jpeg', quality));
397
- };
398
- img.onerror = reject;
399
- };
400
- reader.onerror = reject;
401
- });
402
- };
403
-
404
- // --- RENDERING FUNCTIONS ---
405
- const renderGravesGrid = (gravesToRender = graves) => {
406
- gravesGrid.innerHTML = '';
407
- loadingIndicator.innerHTML = '';
408
-
409
- if (gravesToRender.length === 0) {
410
- const isSearching = searchInput.value.trim() !== '';
411
- const title = isSearching ? "Niciun mormânt nu corespunde căutării" : "Niciun mormânt înregistrat";
412
- 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.";
413
- 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>`;
414
- return;
415
- }
416
-
417
- gravesToRender.forEach(grave => {
418
- const card = document.createElement('div');
419
- card.className = "bg-slate-800 rounded-lg shadow-lg overflow-hidden flex flex-col transition-transform transform hover:scale-[1.02]";
420
-
421
- const photoSrc = grave.photo || generatePlaceholderImage(grave.persons);
422
-
423
- let personsHtml = '';
424
- if (grave.persons.length === 1) {
425
- // Single person: Place directly under grave number
426
- const person = grave.persons[0];
427
- const personName = `${person.firstName || ''} ${person.lastName || ''}`.trim() || 'Persoană Fără Nume';
428
- personsHtml = `
429
- <div class="single-person">
430
- <p class="font-semibold text-slate-200">${personName}</p>
431
- <p class="text-xs">
432
- <span class="text-sky-400">N:</span> ${formatDateForCard(person.birthDate)}
433
- <span class="text-sky-400 ml-2">D:</span> ${formatDateForCard(person.deathDate)}
434
- </p>
435
- </div>
436
- `;
437
- } else {
438
- // Multiple persons or none: Use scrollable container
439
- personsHtml = '<div class="people-preview">';
440
- if (grave.persons.length > 0) {
441
- grave.persons.forEach(person => {
442
- const personName = `${person.firstName || ''} ${person.lastName || ''}`.trim() || 'Persoană Fără Nume';
443
- personsHtml += `
444
- <div>
445
- <p class="text-l font-semibold text-slate-200">${personName}</p>
446
- <p class="text-xs">
447
- <span class="text-sky-400">N:</span> ${formatDateForCard(person.birthDate)}
448
- <span class="text-sky-400 ml-2">D:</span> ${formatDateForCard(person.deathDate)}
449
- </p>
450
- </div>
451
- `;
452
- });
453
- } else {
454
- personsHtml += '<p class="text-slate-400 text-sm">Nicio persoană înregistrată.</p>';
455
- }
456
- personsHtml += '</div>';
457
- }
458
-
459
- card.innerHTML = `
460
- <img src="${photoSrc}" alt="Fotografie mormânt ${grave.number}" class="w-full h-48 object-cover">
461
- <div class="p-4 flex-grow flex flex-col">
462
- <div class="flex justify-between items-center mb-2">
463
- <h3 class="text-2xl text-white card-title">${grave.number || 'Mormânt Fără Număr'}</h3>
464
- <div class="flex gap-3">
465
- <button class="edit-btn btn-secondary text-sm py-1 px-3 rounded-md" data-id="${grave.id}">Editează</button>
466
- <button class="delete-btn btn-danger text-sm py-1 px-3 rounded-md" data-id="${grave.id}">Șterge</button>
467
- </div>
468
- </div>
469
- ${personsHtml}
470
- <p class="text-slate-400 text-sm mt-2 flex-grow">${grave.comments || ' '}</p>
471
- </div>
472
- `;
473
- gravesGrid.appendChild(card);
474
- });
475
-
476
- document.querySelectorAll('.edit-btn').forEach(btn => btn.addEventListener('click', () => openGraveModal({ isEdit: true, graveId: btn.dataset.id })));
477
- document.querySelectorAll('.delete-btn').forEach(btn => btn.addEventListener('click', () => handleDeleteGrave(btn.dataset.id)));
478
- };
479
-
480
- const renderPersonsInModal = (persons) => {
481
- personsListContainer.innerHTML = '';
482
- if (persons.length === 0) {
483
- personsListContainer.innerHTML = '<p class="text-slate-400 text-center py-3">Nicio persoană nu a fost adăugată.</p>';
484
- return;
485
- }
486
-
487
- persons.forEach(person => {
488
- const personDiv = document.createElement('div');
489
- personDiv.className = 'p-3 mb-2 bg-slate-700 rounded-md flex justify-between items-start';
490
- const personName = `${person.firstName || ''} ${person.lastName || ''}`.trim() || 'Persoană Fără Nume';
491
-
492
- personDiv.innerHTML = `
493
- <div class="text-sm">
494
- <p class="font-bold text-slate-100">${personName}</p>
495
- <p class="text-slate-300">Născut/ă: ${formatDateForDisplay(person.birthDate)}</p>
496
- <p class="text-slate-300">Decedat/ă: ${formatDateForDisplay(person.deathDate)}</p>
497
- ${person.notes ? `<p class="text-slate-400 mt-1 italic">Note: ${person.notes}</p>` : ''}
498
- </div>
499
- <div class="flex-shrink-0 flex gap-2 ml-2">
500
- <button type="button" class="edit-person-btn text-sky-400 hover:text-sky-300" data-id="${person.id}" aria-label="Editează persoana">
501
- <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>
502
- </button>
503
- <button type="button" class="delete-person-btn text-red-500 hover:text-red-400" data-id="${person.id}" aria-label="Șterge persoana">
504
- <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>
505
- </button>
506
- </div>
507
- `;
508
- personsListContainer.appendChild(personDiv);
509
- });
510
-
511
- document.querySelectorAll('.edit-person-btn').forEach(btn => btn.addEventListener('click', () => openPersonModal({ isEdit: true, personId: btn.dataset.id })));
512
- document.querySelectorAll('.delete-person-btn').forEach(btn => btn.addEventListener('click', () => handleDeletePerson(btn.dataset.id)));
513
- };
514
-
515
- // --- MODAL MANAGEMENT ---
516
- const openGraveModal = ({ isEdit = false, graveId = null }) => {
517
- graveForm.reset();
518
- gravePhotoPreview.src = '#';
519
- gravePhotoPreview.classList.add('hidden');
520
- personsListContainer.innerHTML = '';
521
- currentEditingPersonId = null;
522
-
523
- if (isEdit) {
524
- const grave = graves.find(g => g.id === graveId);
525
- if (!grave) { showAlert("Eroare: Mormântul nu a fost găsit pentru editare."); return; }
526
- currentEditingGraveId = graveId;
527
- modalGraveData = JSON.parse(JSON.stringify(grave));
528
- modalTitle.textContent = "Modifică Mormântul";
529
- graveNumberInput.value = modalGraveData.number;
530
- graveCommentsInput.value = modalGraveData.comments;
531
- if (modalGraveData.photo) {
532
- gravePhotoPreview.src = modalGraveData.photo;
533
- gravePhotoPreview.classList.remove('hidden');
534
- }
535
- } else {
536
- currentEditingGraveId = null;
537
- const nextGraveNumber = graves.length > 0 ? Math.max(...graves.map(g => parseInt(g.number, 10) || 0).filter(Number.isFinite)) + 1 : 1;
538
- modalGraveData = { id: generateUniqueId(), number: String(nextGraveNumber), comments: '', photo: null, persons: [] };
539
- modalTitle.textContent = "Adaugă Mormânt Nou";
540
- graveNumberInput.placeholder = `ex: ${nextGraveNumber} `;
541
- }
542
-
543
- renderPersonsInModal(modalGraveData.persons);
544
- graveModal.style.display = 'flex';
545
- };
546
-
547
- const closeGraveModal = () => { graveModal.style.display = 'none'; modalGraveData = null; currentEditingGraveId = null; };
548
-
549
- const openPersonModal = ({ isEdit = false, personId = null }) => {
550
- 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; }
551
- personForm.reset();
552
- if (isEdit) {
553
- const person = modalGraveData.persons.find(p => p.id === personId);
554
- if (!person) return;
555
- currentEditingPersonId = personId;
556
- personModalTitle.textContent = "Editează Persoană";
557
- personFirstNameInput.value = person.firstName;
558
- personLastNameInput.value = person.lastName;
559
- personBirthDateInput.value = person.birthDate;
560
- personDeathDateInput.value = person.deathDate;
561
- personNotesInput.value = person.notes;
562
- } else {
563
- currentEditingPersonId = null;
564
- personModalTitle.textContent = "Adaugă Persoană";
565
- }
566
- personModal.style.display = 'flex';
567
- };
568
-
569
- const closePersonModal = () => { personModal.style.display = 'none'; currentEditingPersonId = null; };
570
-
571
- // --- EVENT HANDLERS & LOGIC ---
572
- const handleGraveFormSubmit = (e) => {
573
- e.preventDefault();
574
- modalGraveData.number = graveNumberInput.value.trim() || modalGraveData.number;
575
- modalGraveData.comments = graveCommentsInput.value.trim();
576
-
577
- if (currentEditingGraveId) {
578
- const index = graves.findIndex(g => g.id === currentEditingGraveId);
579
- if (index !== -1) graves[index] = modalGraveData;
580
- else { showAlert("Eroare: Nu s-a putut găsi mormântul pentru actualizare."); return; }
581
- } else {
582
- graves.push(modalGraveData);
583
- }
584
-
585
- saveGraves();
586
- renderGravesGrid();
587
- closeGraveModal();
588
- };
589
-
590
- const handlePersonFormSubmit = (e) => {
591
- e.preventDefault();
592
- if (!modalGraveData) { showAlert("Eroare: Nu există date active pentru mormânt."); return; }
593
- const firstName = personFirstNameInput.value.trim();
594
- const lastName = personLastNameInput.value.trim();
595
- if (!firstName && !lastName) { showAlert("Este necesar cel puțin Prenume sau Nume de Familie."); return; }
596
-
597
- const personData = { firstName, lastName, birthDate: personBirthDateInput.value, deathDate: personDeathDateInput.value, notes: personNotesInput.value.trim() };
598
-
599
- if (currentEditingPersonId) {
600
- const index = modalGraveData.persons.findIndex(p => p.id === currentEditingPersonId);
601
- if (index !== -1) modalGraveData.persons[index] = { ...modalGraveData.persons[index], ...personData };
602
- } else {
603
- personData.id = generateUniqueId();
604
- modalGraveData.persons.push(personData);
605
- }
606
- renderPersonsInModal(modalGraveData.persons);
607
- closePersonModal();
608
- };
609
-
610
- const handleDeleteGrave = (graveId) => {
611
- showConfirm("Sigur doriți să ștergeți acest mormânt și toate persoanele asociate? Această acțiune nu poate fi anulată.", () => {
612
- graves = graves.filter(g => g.id !== graveId);
613
- saveGraves();
614
- handleSearchInput(); // Re-render based on current search
615
- });
616
- };
617
-
618
- const handleDeletePerson = (personId) => {
619
- if (!modalGraveData) return;
620
- showConfirm("Sigur doriți să eliminați această persoană din detaliile mormântului curent?", () => {
621
- modalGraveData.persons = modalGraveData.persons.filter(p => p.id !== personId);
622
- renderPersonsInModal(modalGraveData.persons);
623
- });
624
- };
625
-
626
- const handlePhotoChange = async (e) => {
627
- const file = e.target.files[0];
628
- if (!file) return;
629
- loadingIndicator.textContent = "Se procesează imaginea...";
630
- try {
631
- let imageDataUrl;
632
- if (file.size > PHOTO_MAX_SIZE_MB * 1024 * 1024) {
633
- showAlert(`Imaginea este prea mare. Se va încerca comprimarea...`);
634
- imageDataUrl = await compressImage(file);
635
- } else {
636
- imageDataUrl = await new Promise((resolve, reject) => {
637
- const reader = new FileReader();
638
- reader.onload = () => resolve(reader.result);
639
- reader.onerror = reject;
640
- reader.readAsDataURL(file);
641
- });
642
- }
643
- modalGraveData.photo = imageDataUrl;
644
- gravePhotoPreview.src = imageDataUrl;
645
- gravePhotoPreview.classList.remove('hidden');
646
- } catch (error) {
647
- console.error(error);
648
- showAlert("Eroare la citirea fișierului. Vă rugăm să încercați să selectați din nou.");
649
- } finally {
650
- loadingIndicator.textContent = "";
651
- }
652
- };
653
-
654
- const handleSearchInput = () => {
655
- const query = searchInput.value.toLowerCase().trim();
656
- const filteredGraves = graves.filter(grave => {
657
- const graveInfo = [grave.number, grave.comments].join(' ').toLowerCase();
658
- if (graveInfo.includes(query)) return true;
659
- return grave.persons.some(person => {
660
- const personInfo = [person.firstName, person.lastName, person.notes].join(' ').toLowerCase();
661
- return personInfo.includes(query);
662
- });
663
- });
664
- renderGravesGrid(filteredGraves);
665
- };
666
-
667
- // --- SETUP EVENT LISTENERS ---
668
- const setupEventListeners = () => {
669
- addGraveBtn.addEventListener('click', () => openGraveModal({ isEdit: false }));
670
- closeGraveModalBtn.addEventListener('click', closeGraveModal);
671
- cancelGraveBtn.addEventListener('click', closeGraveModal);
672
- graveForm.addEventListener('submit', handleGraveFormSubmit);
673
- gravePhotoInput.addEventListener('change', handlePhotoChange);
674
- addPersonToGraveBtn.addEventListener('click', () => openPersonModal({ isEdit: false }));
675
- closePersonModalBtn.addEventListener('click', closePersonModal);
676
- cancelPersonBtn.addEventListener('click', closePersonModal);
677
- personForm.addEventListener('submit', handlePersonFormSubmit);
678
- alertOkBtn.addEventListener('click', () => alertModal.style.display = 'none');
679
- confirmNoBtn.addEventListener('click', () => confirmModal.style.display = 'none');
680
- searchInput.addEventListener('input', handleSearchInput);
681
-
682
- window.addEventListener('keydown', (e) => {
683
- if (e.key === 'Escape') {
684
- if (personModal.style.display === 'flex') closePersonModal();
685
- else if (graveModal.style.display === 'flex') closeGraveModal();
686
- else if (alertModal.style.display === 'flex') alertModal.style.display = 'none';
687
- else if (confirmModal.style.display === 'flex') confirmModal.style.display = 'none';
688
- }
689
- });
690
- };
691
-
692
- // --- INITIALIZATION ---
693
- const initialize = () => {
694
- loadingIndicator.textContent = "Se încarcă datele...";
695
- loadGraves();
696
- renderGravesGrid();
697
- setupEventListeners();
698
- };
699
-
700
- initialize();
701
- });
702
- </script>
703
-
704
- <script>
705
- (function(){
706
- const BACKUP_URL = 'https://api.apispreadsheets.com/data/VVWozHAy5BosLaA3/';
707
- const IMGBB_API_KEY = '9498bf8da3ec348c0512a97edd9666c0';
708
- const CEMETERY_KEY = 'cemeteryData';
709
- const LAST_KEY = 'lastBackup';
710
- const BACKUP_INTERVAL_MS = 5 * 60 * 60 * 1000; // 5 hours
711
-
712
- const initialRaw = localStorage.getItem(CEMETERY_KEY);
713
- let hasChanged = false;
714
-
715
- // Override setItem to detect changes
716
- const originalSetItem = localStorage.setItem;
717
- localStorage.setItem = function(key, value) {
718
- if (key === CEMETERY_KEY && value !== initialRaw) {
719
- hasChanged = true;
720
- }
721
- originalSetItem.apply(this, arguments);
722
- };
723
-
724
- async function uploadToImgbb(base64) {
725
- const form = new FormData();
726
- form.append("key", IMGBB_API_KEY);
727
- form.append("image", base64.split(',')[1]); // remove "data:image/...;base64,"
728
- const res = await fetch("https://api.imgbb.com/1/upload", {
729
- method: "POST",
730
- body: form
731
- });
732
- if (!res.ok) throw new Error("ImgBB upload failed");
733
- const json = await res.json();
734
- return json.data.url;
735
- }
736
-
737
- async function prepareBackup() {
738
- const raw = localStorage.getItem(CEMETERY_KEY);
739
- if (!raw) return;
740
-
741
- let parsed;
742
- try {
743
- parsed = JSON.parse(raw);
744
- } catch (e) {
745
- console.error("Invalid JSON in cemeteryData", e);
746
- return;
747
- }
748
-
749
- // Deep clone to avoid changing localStorage
750
- const cloned = JSON.parse(JSON.stringify(parsed));
751
-
752
- for (let entry of cloned) {
753
- if (entry.photo && typeof entry.photo === "string" && entry.photo.startsWith("data:image/")) {
754
- try {
755
- const url = await uploadToImgbb(entry.photo);
756
- entry.photo = url;
757
- } catch (e) {
758
- console.error("Error uploading photo to ImgBB:", e);
759
- }
760
- }
761
- }
762
-
763
- // Now send the modified clone (not touching localStorage)
764
- const payload = JSON.stringify({ data: { data: JSON.stringify(cloned) } });
765
-
766
- localStorage.setItem(LAST_KEY, new Date().toISOString());
767
-
768
- if (navigator.sendBeacon) {
769
- const blob = new Blob([payload], { type: 'application/json' });
770
- navigator.sendBeacon(BACKUP_URL, blob);
771
- } else {
772
- fetch(BACKUP_URL, {
773
- method: "POST",
774
- headers: { "Content-Type": "application/json" },
775
- body: payload
776
- }).catch(err => console.error("Backup failed:", err));
777
- }
778
- }
779
-
780
- function shouldBackup() {
781
- const last = localStorage.getItem(LAST_KEY);
782
- if (!last) return true;
783
- return Date.now() - new Date(last).getTime() > BACKUP_INTERVAL_MS;
784
- }
785
-
786
- // Auto-backup on load if time expired
787
- window.addEventListener('load', () => {
788
- if (shouldBackup()) {
789
- prepareBackup();
790
- }
791
- });
792
-
793
- // Backup on unload if data changed
794
- window.addEventListener('beforeunload', (e) => {
795
- const current = localStorage.getItem(CEMETERY_KEY);
796
- if (current !== initialRaw || hasChanged) {
797
- navigator.sendBeacon || e.preventDefault(); // fallback logic
798
- prepareBackup();
799
- }
800
- });
801
- })();
802
- </script>
803
-
804
- </body>
805
- </html>
 
1
+ Atenție! Site-ul este închis pentru mentenanță. Se va redeschide peste câteva zile.