feat: Introduce main application UI with family tree, map, and timeline views, alongside updated authentication and documentation.

This commit is contained in:
Eivind Røsstad Skeie 2025-11-25 12:06:20 +01:00
parent bde8f2b693
commit 6e2ed796c0
5 changed files with 149 additions and 30 deletions

View file

@ -1 +1,26 @@
https://funnyeivske.github.io/Roesstad/
# Røsstad Familietre
**Nettsiden finner du her:** [https://funnyeivske.github.io/Roesstad/](https://funnyeivske.github.io/Roesstad/)
## Om Nettsiden
Dette er et digitalt samlingspunkt for Røsstad-familietreet. Nettsiden er designet for å bevare og visualisere familiens historie på en moderne og interaktiv måte.
## Hvordan den fungerer
### 1. Familietre (Hovedvisning)
Her ser du selve slektstreet.
- **Navigering:** Du kan dra i bakgrunnen for å flytte deg rundt, og zoome ut/inn.
- **Personkort:** Klikk på en person for å sette dem i fokus.
- **Legg til:** Bruk den store grønne knappen nede til høyre for å legge til nye familiemedlemmer.
### 2. Kart
Se hvor familien har sine røtter.
- Når du legger til en person, kan du skrive inn et sted (f.eks. "Grimstad").
- Personen vil da dukke opp som en markør på kartet.
### 3. Tidslinje
En kronologisk oversikt over familiens historie.
- Her listes alle fødsler og dødsfall opp automatisk basert på informasjonen i treet.
---
*Laget med ❤️ for Røsstad-familien.*

View file

@ -21,23 +21,35 @@ const state = {
map: null
};
// --- IMMEDIATE SETUP (UI Handlers) ---
// This ensures buttons work even if Auth is slow
setupTabs();
setupZoom();
setupModal();
setupHelp();
setupFAB();
// --- AUTHENTICATION ---
onAuthStateChanged(auth, (user) => {
if (user) {
state.user = user;
initApp();
startDataListener();
} else {
// Redirect to login if not authenticated
// Note: In local development with file://, this might trigger unexpectedly.
// For now, we assume index.html is the login page.
if (!window.location.pathname.endsWith('index.html') && !window.location.pathname.endsWith('/')) {
window.location.href = "index.html";
}
}
});
document.getElementById('btn-logout')?.addEventListener('click', () => {
signOut(auth).then(() => window.location.href = "index.html");
});
// --- APP LOGIC ---
function initApp() {
// Start Data Listener
// --- DATA LISTENER ---
function startDataListener() {
const q = query(collection(db, 'familyMembers'));
onSnapshot(q, (snapshot) => {
state.people = snapshot.docs.map(d => ({ id: d.id, ...d.data() }));
@ -50,11 +62,6 @@ function initApp() {
renderAll();
});
// Setup Navigation
setupTabs();
setupZoom();
setupModal();
}
function renderAll() {
@ -74,7 +81,8 @@ function setupTabs() {
// Show View
document.querySelectorAll('.view-section').forEach(v => v.classList.add('hidden'));
const viewId = `view-${btn.dataset.view}`;
document.getElementById(viewId).classList.remove('hidden');
const viewEl = document.getElementById(viewId);
if (viewEl) viewEl.classList.remove('hidden');
// Special handling for Map resize
if (btn.dataset.view === 'map' && state.map) {
@ -97,7 +105,10 @@ function renderTree() {
childrenRow.innerHTML = '';
if (state.people.length === 0) {
focusRow.innerHTML = `<button class="btn-primary" onclick="window.openModal('create-first')">Start familietreet</button>`;
focusRow.innerHTML = `<div style="text-align:center; color:var(--color-text-muted);">
<p>Ingen personer i treet ennå.</p>
<p>Klikk på <i class="ph-bold ph-plus-circle" style="color:var(--color-primary);"></i> nede til høyre for å starte!</p>
</div>`;
return;
}
@ -203,6 +214,9 @@ function createSlot(person, label, action) {
// --- MAP RENDERER ---
function renderMap() {
if (!state.map) {
const mapContainer = document.getElementById('map-container');
if (!mapContainer) return;
state.map = L.map('map-container').setView([65.0, 13.0], 5);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; OpenStreetMap &copy; CARTO',
@ -274,18 +288,21 @@ function renderTimeline() {
function setupModal() {
window.openModal = (action) => {
state.modal.action = action;
document.getElementById('modal-overlay').classList.remove('hidden');
const overlay = document.getElementById('modal-overlay');
if (overlay) overlay.classList.remove('hidden');
// Reset Form
document.querySelectorAll('.input-field').forEach(i => i.value = '');
// Set Title
const title = document.getElementById('modal-title');
if (title) {
if (action === 'add-parent-m') title.textContent = "Legg til Far";
else if (action === 'add-parent-f') title.textContent = "Legg til Mor";
else if (action === 'add-child') title.textContent = "Legg til Barn";
else if (action === 'add-partner') title.textContent = "Legg til Partner";
else title.textContent = "Start Tre";
else title.textContent = "Legg til Person";
}
// Default Mode
window.setModalMode('new');
@ -300,11 +317,13 @@ function setupModal() {
document.getElementById('form-existing').classList.toggle('hidden', mode !== 'existing');
};
document.getElementById('btn-close-modal').onclick = () => {
const closeBtn = document.getElementById('btn-close-modal');
if (closeBtn) closeBtn.onclick = () => {
document.getElementById('modal-overlay').classList.add('hidden');
};
document.getElementById('btn-save').onclick = async () => {
const saveBtn = document.getElementById('btn-save');
if (saveBtn) saveBtn.onclick = async () => {
const focusP = state.people.find(p => p.id === state.focusId);
let targetId = null;
@ -335,14 +354,18 @@ function setupModal() {
const docRef = await addDoc(collection(db, 'familyMembers'), newPerson);
targetId = docRef.id;
if (state.modal.action === 'create-first') state.focusId = targetId;
// If creating first person or using generic add
if (state.modal.action === 'create-first' || !state.focusId) {
state.focusId = targetId;
}
} else {
targetId = document.getElementById('selected-existing-id').value;
}
// Link Relations
if (focusP && targetId) {
if (focusP && targetId && state.modal.action !== 'create-first' && state.modal.action !== 'add-generic') {
const col = collection(db, 'familyMembers');
const updates = [];
@ -365,6 +388,7 @@ function setupModal() {
function renderExistingList() {
const list = document.getElementById('existing-list');
if (!list) return;
list.innerHTML = '';
state.people.filter(p => p.id !== state.focusId).forEach(p => {
const div = document.createElement('div');
@ -385,17 +409,42 @@ function setupZoom() {
const grid = document.getElementById('tree-grid');
const display = document.getElementById('zoom-display');
document.getElementById('btn-zoom-in').onclick = () => {
const btnIn = document.getElementById('btn-zoom-in');
if (btnIn) btnIn.onclick = () => {
state.scale += 0.1;
updateZoom();
};
document.getElementById('btn-zoom-out').onclick = () => {
const btnOut = document.getElementById('btn-zoom-out');
if (btnOut) btnOut.onclick = () => {
if (state.scale > 0.4) state.scale -= 0.1;
updateZoom();
};
function updateZoom() {
grid.style.transform = `scale(${state.scale})`;
display.textContent = Math.round(state.scale * 100) + '%';
if (grid) grid.style.transform = `scale(${state.scale})`;
if (display) display.textContent = Math.round(state.scale * 100) + '%';
}
}
// --- HELP ---
function setupHelp() {
const btn = document.getElementById('btn-help');
const modal = document.getElementById('modal-help');
const close = document.getElementById('btn-close-help');
if (btn && modal) {
btn.onclick = () => modal.classList.remove('hidden');
}
if (close && modal) {
close.onclick = () => modal.classList.add('hidden');
}
}
// --- FAB ---
function setupFAB() {
const fab = document.getElementById('fab-add');
if (fab) {
fab.onclick = () => window.openModal('add-generic');
}
}

View file

@ -36,6 +36,9 @@
<!-- CONTROLS -->
<div class="header-controls">
<button id="btn-help" class="icon-btn" title="Hjelp"><i class="ph ph-question"></i></button>
<div style="width:1px; height:24px; background:#cbd5e1; margin:0 0.5rem;"></div>
<button id="btn-zoom-out" class="icon-btn" title="Zoom ut"><i class="ph ph-minus-circle"></i></button>
<span id="zoom-display"
style="font-size:0.9rem; font-weight:600; width:40px; text-align:center;">100%</span>
@ -73,10 +76,15 @@
</div>
</div>
<!-- FLOATING ACTION BUTTON (FAB) -->
<button id="fab-add" class="fab-btn" title="Legg til person">
<i class="ph-bold ph-plus"></i>
</button>
</main>
</div>
<!-- MODAL -->
<!-- MODAL: ADD PERSON -->
<div id="modal-overlay" class="modal-overlay hidden">
<div class="modal-content">
<div class="modal-header">
@ -142,6 +150,29 @@
</div>
</div>
<!-- MODAL: HELP -->
<div id="modal-help" class="modal-overlay hidden">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Hjelp & Instruksjoner</h3>
<button id="btn-close-help" class="icon-btn"><i class="ph ph-x"></i></button>
</div>
<div style="line-height:1.6; color:var(--color-text-main);">
<p><strong>Velkommen til Røsstad Familietre!</strong></p>
<ul style="padding-left:1.2rem; margin-bottom:1.5rem;">
<li><strong>Legg til person:</strong> Klikk på den store grønne pluss-knappen nede til høyre, eller
bruk de små knappene på kortene.</li>
<li><strong>Navigasjon:</strong> Bruk fanene øverst for å bytte mellom Tre, Kart og Tidslinje.</li>
<li><strong>Zoom:</strong> Bruk pluss/minus knappene oppe til høyre for å zoome i treet.</li>
<li><strong>Flytt treet:</strong> Du kan dra og slippe treet for å bevege deg rundt.</li>
</ul>
<p><em>Tips: Legg til et sted (f.eks. "Grimstad") for å se personen på kartet!</em></p>
</div>
<button class="btn-primary"
onclick="document.getElementById('modal-help').classList.add('hidden')">Forstått</button>
</div>
</div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>

View file

@ -31,7 +31,7 @@ if (form) {
try {
await signInWithEmailAndPassword(auth, email, password);
// Redirect on success
window.location.href = "Røsstad.html";
window.location.href = "app.html";
} catch (error) {
console.error("Login error:", error);
handleError(error);

View file

@ -567,3 +567,17 @@ body.login-page {
display: flex;
gap: 1rem;
}
/* --- FAB (Floating Action Button) --- */
.fab-btn {
position: fixed; bottom: 2rem; right: 2rem;
width: 64px; height: 64px; border-radius: 50%;
background: var(--color-primary); color: white;
border: none; box-shadow: var(--shadow-lg);
display: flex; justify-content: center; align-items: center;
font-size: 2rem; cursor: pointer; z-index: 500;
transition: transform 0.2s, background 0.2s;
}
.fab-btn:hover {
transform: scale(1.1) rotate(90deg);
background: var(--color-primary-dark);
}