// Firebase Configuration const firebaseConfig = { apiKey: "AIzaSyD-E58BdRwFCHDmsgRG9-PEWzMaQoheJ-0", authDomain: "roesstad-9fd48.firebaseapp.com", projectId: "roesstad-9fd48", storageBucket: "roesstad-9fd48.firebasestorage.app", messagingSenderId: "704119736902", appId: "1:704119736902:web:bc47f2b6a1383de0479847" }; // Initialize Firebase firebase.initializeApp(firebaseConfig); const auth = firebase.auth(); const db = firebase.firestore(); // DOM Elements const loginModal = document.getElementById('login-modal'); const loginForm = document.getElementById('login-form'); const loginError = document.getElementById('login-error'); const logoutBtn = document.getElementById('logoutBtn'); const darkModeToggle = document.getElementById('darkModeToggle'); const navBtns = document.querySelectorAll('.nav-btn'); const tabContents = document.querySelectorAll('.tab-content'); const memberModal = document.getElementById('member-modal'); const addMemberBtn = document.getElementById('addMemberBtn'); const closeModalBtns = document.querySelectorAll('.close-modal'); const memberForm = document.getElementById('member-form'); // State let currentUser = null; let familyData = []; // Global Tree State for Pan/Zoom let treeState = { scale: 1, panning: false, pointX: 0, pointY: 0, startX: 0, startY: 0 }; // --- Authentication --- auth.onAuthStateChanged(user => { if (user) { currentUser = user; loginModal.style.display = 'none'; logoutBtn.style.display = 'block'; loadFamilyData(); } else { currentUser = null; loginModal.style.display = 'flex'; logoutBtn.style.display = 'none'; } }); loginForm.addEventListener('submit', (e) => { e.preventDefault(); const email = document.getElementById('email').value; const password = document.getElementById('password').value; auth.signInWithEmailAndPassword(email, password) .catch(error => { loginError.textContent = "Feil e-post eller passord."; console.error(error); }); }); logoutBtn.addEventListener('click', () => { auth.signOut(); }); // --- UI Logic --- // Dark Mode darkModeToggle.addEventListener('click', () => { document.body.classList.toggle('dark-mode'); const isDark = document.body.classList.contains('dark-mode'); darkModeToggle.textContent = isDark ? '🌙' : '☀️'; }); // Geocoding document.getElementById('geocodeBtn').addEventListener('click', () => { const location = document.getElementById('location').value; if (!location) return; const geocoder = new google.maps.Geocoder(); geocoder.geocode({ 'address': location }, (results, status) => { if (status === 'OK') { const lat = results[0].geometry.location.lat(); const lng = results[0].geometry.location.lng(); document.getElementById('lat').value = lat.toFixed(6); document.getElementById('lng').value = lng.toFixed(6); } else { alert('Kunne ikke finne koordinater for dette stedet: ' + status); } }); }); // --- Data Handling --- function loadFamilyData() { console.log("Loading family data..."); db.collection('familyMembers').onSnapshot(snapshot => { console.log("Snapshot received. Docs:", snapshot.size); familyData = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); populateDropdowns(); // Refresh active view const activeTab = document.querySelector('.tab-content.active'); if (activeTab && activeTab.id === 'tree-tab') renderTree(); if (activeTab && activeTab.id === 'history-tab') renderHistory(); if (activeTab && activeTab.id === 'map-tab') initMap(); }, error => { console.error("Firestore Error:", error); const container = document.getElementById('family-cards-container'); if (container) { container.innerHTML = `
Kunne ikke hente data: ${error.message}
Sjekk konsollen for detaljer.
`; } }); } function populateDropdowns(excludeId = null) { const selects = ['motherId', 'fatherId', 'spouseId']; selects.forEach(id => { const select = document.getElementById(id); if (!select) return; select.innerHTML = ''; const sorted = [...familyData].sort((a, b) => (a.firstName || '').localeCompare(b.firstName || '')); sorted.forEach(m => { if (m.id === excludeId) return; const option = document.createElement('option'); option.value = m.id; option.textContent = `${m.firstName} ${m.lastName} (${new Date(m.birthDate).getFullYear()})`; select.appendChild(option); }); }); } // Add/Edit Member memberForm.addEventListener('submit', (e) => { e.preventDefault(); const id = document.getElementById('member-id').value; const data = { firstName: document.getElementById('firstName').value, lastName: document.getElementById('lastName').value, gender: document.querySelector('input[name="gender"]:checked').value, birthDate: document.getElementById('birthDate').value, deathDate: document.getElementById('deathDate').value, location: document.getElementById('location').value, lat: parseFloat(document.getElementById('lat').value) || null, lng: parseFloat(document.getElementById('lng').value) || null, motherId: document.getElementById('motherId').value || null, fatherId: document.getElementById('fatherId').value || null, spouseId: document.getElementById('spouseId').value || null, notes: document.getElementById('notes').value }; if (id) { db.collection('familyMembers').doc(id).update(data) .then(() => { memberModal.style.display = 'none'; memberForm.reset(); }); } else { db.collection('familyMembers').add(data) .then(() => { memberModal.style.display = 'none'; memberForm.reset(); }); } }); // Modal Actions addMemberBtn.addEventListener('click', () => { document.getElementById('modal-title').textContent = "Legg til familiemedlem"; memberForm.reset(); document.getElementById('member-id').value = ''; populateDropdowns(); memberModal.style.display = 'flex'; }); closeModalBtns.forEach(btn => { btn.addEventListener('click', () => { memberModal.style.display = 'none'; loginModal.style.display = 'none'; }); }); window.onclick = (e) => { if (e.target === memberModal) memberModal.style.display = 'none'; if (e.target === loginModal) loginModal.style.display = 'none'; }; const deleteBtn = document.getElementById('deleteBtn'); if (deleteBtn) { deleteBtn.addEventListener('click', () => { const id = document.getElementById('member-id').value; if (!id) return; if (confirm('Er du sikker på at du vil slette denne personen?')) { db.collection('familyMembers').doc(id).delete() .then(() => { memberModal.style.display = 'none'; }); } }); } // --- Feature: Family Tree (Static Layout) --- // Feature: Family Tree (Static Layout) --- function updateTransform() { const treeContainer = document.getElementById('tree-container'); if (treeContainer) { // Safety check for NaN if (isNaN(treeState.pointX)) treeState.pointX = 0; if (isNaN(treeState.pointY)) treeState.pointY = 0; if (isNaN(treeState.scale)) treeState.scale = 1; treeContainer.style.transform = `translate(${treeState.pointX}px, ${treeState.pointY}px) scale(${treeState.scale})`; console.log(`Transform updated: X=${treeState.pointX}, Y=${treeState.pointY}, Scale=${treeState.scale}`); } } function renderTree() { const container = document.getElementById('family-cards-container'); const treeTab = document.getElementById('tree-tab'); // Disable Native Scrolling, Enable Custom if (treeTab) { // Handled by CSS (overflow: hidden), but force styles if needed treeTab.style.overflow = 'hidden'; treeTab.style.cursor = 'grab'; } if (!container) return; container.innerHTML = ''; const svg = document.getElementById('relationship-lines'); if (svg) svg.innerHTML = ''; if (!familyData || familyData.length === 0) { container.innerHTML = '
Ingen familiemedlemmer funnet. Klikk på + for å legge til.
'; return; } try { const memberMap = new Map(familyData.map(m => [m.id, { ...m }])); const childrenMap = new Map(); const partnerAdjacency = new Map(); const addRelation = (id1, id2, type) => { if (!id1 || !id2) return; if (!partnerAdjacency.has(id1)) partnerAdjacency.set(id1, []); if (!partnerAdjacency.has(id2)) partnerAdjacency.set(id2, []); if (!partnerAdjacency.get(id1).find(r => r.id === id2)) { partnerAdjacency.get(id1).push({ id: id2, type }); } if (!partnerAdjacency.get(id2).find(r => r.id === id1)) { partnerAdjacency.get(id2).push({ id: id1, type }); } }; familyData.forEach(m => { if (m.spouseId) addRelation(m.id, m.spouseId, 'spouse'); if (m.motherId && m.fatherId) { // Parents match -> they are partners addRelation(m.motherId, m.fatherId, 'coparent'); [m.motherId, m.fatherId].forEach(pId => { if (!childrenMap.has(pId)) childrenMap.set(pId, []); childrenMap.get(pId).push(m.id); }); } else { // Single parent or partial [m.motherId, m.fatherId].forEach(pId => { if (pId && memberMap.has(pId)) { if (!childrenMap.has(pId)) childrenMap.set(pId, []); childrenMap.get(pId).push(m.id); } }); } }); const renderedSet = new Set(); const placedNodes = []; const CARD_WIDTH = 220; const GAP_X = 50; const UNIT_GAP = 100; const GAP_Y = 250; const createCard = (member, x, y) => { if (!member) return; if (renderedSet.has(member.id)) return; const card = document.createElement('div'); card.id = `card-${member.id}`; card.className = 'family-card'; card.style.left = `${x}px`; card.style.top = `${y}px`; const genderIcon = member.gender === 'male' ? '👨' : (member.gender === 'female' ? '👩' : '👤'); card.innerHTML = `
${genderIcon}
${member.firstName} ${member.lastName}
${new Date(member.birthDate).getFullYear()} - ${member.deathDate ? new Date(member.deathDate).getFullYear() : ''}
`; container.appendChild(card); card.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // Stop pan openEditModal(member); }); // Prevent drag start on card text/images card.addEventListener('mousedown', (e) => { e.stopPropagation(); }); placedNodes.push({ ...member, x, y, width: CARD_WIDTH, height: 100 }); renderedSet.add(member.id); }; const renderComponent = (rootId, startY) => { const component = new Set(); const queue = [rootId]; component.add(rootId); while (queue.length > 0) { const curr = queue.shift(); const partners = partnerAdjacency.get(curr) || []; partners.forEach(p => { if (!component.has(p.id)) { component.add(p.id); queue.push(p.id); } }); } const members = Array.from(component); let sortedIds = []; const pivot = members.find(mId => { const adj = partnerAdjacency.get(mId) || []; const hasSpouse = adj.some(a => a.type === 'spouse'); const hasEx = adj.some(a => a.type === 'coparent'); return hasSpouse && hasEx; }); if (pivot) { const adj = partnerAdjacency.get(pivot) || []; const exes = adj.filter(a => a.type === 'coparent').map(a => a.id); const spouses = adj.filter(a => a.type === 'spouse').map(a => a.id); sortedIds = [...new Set([...exes, pivot, ...spouses])]; } else { sortedIds = members.sort((a, b) => { const ma = memberMap.get(a); const mb = memberMap.get(b); return new Date(ma.birthDate) - new Date(mb.birthDate); }); } const gapWidths = []; for (let i = 0; i < sortedIds.length - 1; i++) { const p1 = sortedIds[i]; const p2 = sortedIds[i + 1]; const c1 = childrenMap.get(p1) || []; const c2 = childrenMap.get(p2) || []; const common = c1.filter(id => c2.includes(id)); let childrenBlockWidth = common.length * (CARD_WIDTH + GAP_X) - GAP_X; let gap = UNIT_GAP; if (childrenBlockWidth > CARD_WIDTH) { gap = Math.max(UNIT_GAP, childrenBlockWidth - CARD_WIDTH * 2 + 100); } gapWidths.push(gap); } // Initially layout from 0 let currentX = 0; sortedIds.forEach((id, index) => { const member = memberMap.get(id); createCard(member, currentX, startY); if (index < sortedIds.length - 1) { const nextId = sortedIds[index + 1]; const gap = gapWidths[index]; const c1 = childrenMap.get(id) || []; const c2 = childrenMap.get(nextId) || []; const common = c1.filter(kid => c2.includes(kid)); if (common.length > 0) { common.sort((a, b) => { const ma = memberMap.get(a); const mb = memberMap.get(b); return new Date(ma.birthDate) - new Date(mb.birthDate); }); const gapCenterX = currentX + CARD_WIDTH + gap / 2; let totalChildrenWidth = common.length * CARD_WIDTH + (common.length - 1) * GAP_X; let childStartX = gapCenterX - totalChildrenWidth / 2; common.forEach(childId => { createCard(memberMap.get(childId), childStartX, startY + GAP_Y); childStartX += CARD_WIDTH + GAP_X; }); } currentX += CARD_WIDTH + gap; } // --- Single / Orphaned Children --- const myChildren = childrenMap.get(id) || []; const unrenderedChildren = myChildren.filter(kidId => !renderedSet.has(kidId)); if (unrenderedChildren.length > 0) { unrenderedChildren.sort((a, b) => new Date(memberMap.get(a).birthDate) - new Date(memberMap.get(b).birthDate)); let childX = currentX - ((unrenderedChildren.length * CARD_WIDTH) / 2); if (index > 0) childX = Math.max(childX, currentX); unrenderedChildren.forEach(kidId => { createCard(memberMap.get(kidId), childX, startY + GAP_Y); childX += CARD_WIDTH + GAP_X; }); } }); return currentX; }; const sortedMembers = Array.from(memberMap.values()).sort((a, b) => new Date(a.birthDate) - new Date(b.birthDate)); let rootY = 100; sortedMembers.forEach(m => { if (!renderedSet.has(m.id)) { const isChild = (m.motherId && memberMap.has(m.motherId)) || (m.fatherId && memberMap.has(m.fatherId)); if (!isChild) { renderComponent(m.id, rootY); } } }); // 3. Infinite Canvas: Remove fixed limits // We do simple auto-center ONCE on load, but no forced container clipping if (placedNodes.length > 0) { let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; placedNodes.forEach(n => { if (n.x < minX) minX = n.x; if (n.x + n.width > maxX) maxX = n.x + n.width; if (n.y < minY) minY = n.y; if (n.y + n.height > maxY) maxY = n.y + n.height; }); // Center Viewport const treeWidth = maxX - minX; const treeHeight = maxY - minY; const screenW = window.innerWidth; const screenH = window.innerHeight; treeState.pointX = (screenW - treeWidth) / 2 - minX; treeState.pointY = (screenH - treeHeight) / 2 - minY; treeState.pointY = Math.max(50, treeState.pointY); // Top padding // Ensure not NaN if (isNaN(treeState.pointX)) treeState.pointX = 0; if (isNaN(treeState.pointY)) treeState.pointY = 0; console.log("Calculated Initial Center:", treeState); updateTransform(); } // Draw Lines const edges = []; const nodeLookup = new Map(placedNodes.map(n => [n.id, n])); placedNodes.forEach(node => { const partners = partnerAdjacency.get(node.id) || []; partners.forEach(rel => { if (node.id < rel.id && nodeLookup.has(rel.id)) { edges.push({ source: node, target: nodeLookup.get(rel.id), type: rel.type }); } }); }); drawStaticLines(placedNodes, edges); } catch (err) { console.error("Render Error:", err); container.innerHTML = `
En feil oppstod under visning av treet:
${err.message}
`; } } function drawStaticLines(nodes, edges) { const svg = document.getElementById('relationship-lines'); if (!svg) return; let minX = Infinity, maxX = -Infinity, maxY = 0; nodes.forEach(n => { if (n.x < minX) minX = n.x; if (n.x + n.width > maxX) maxX = n.x + n.width; if (n.y + n.height > maxY) maxY = n.y + n.height; }); // SVG needs to be big enough to cover everything // We can make it huge for infinite canvas feel svg.setAttribute('width', Math.max(5000, maxX + 1000)); svg.setAttribute('height', Math.max(5000, maxY + 1000)); let html = ''; const nodeLookup = new Map(nodes.map(n => [n.id, n])); // 1. Draw Partner Lines edges.forEach(edge => { if (edge.type === 'spouse' || edge.type === 'coparent') { const s = edge.source; const t = edge.target; const x1 = s.x + s.width / 2; const y1 = s.y + 50; const x2 = t.x + t.width / 2; const y2 = t.y + 50; const strokeDash = edge.type === 'coparent' ? 'stroke-dasharray="5,5"' : ''; html += ``; } }); // 2. Draw Parent-Child Lines (Bracket Style) const groups = new Map(); nodes.forEach(node => { if (node.motherId || node.fatherId) { const parents = [node.motherId, node.fatherId].filter(x => x).sort().join('|'); if (parents) { if (!groups.has(parents)) groups.set(parents, []); groups.get(parents).push(node); } } }); groups.forEach((children, key) => { const [p1Id, p2Id] = key.split('|'); const p1 = nodeLookup.get(p1Id); const p2 = nodeLookup.get(p2Id); let startX, startY; if (p1 && p2) { startX = (p1.x + p1.width / 2 + p2.x + p2.width / 2) / 2; startY = p1.y + 50; } else if (p1) { startX = p1.x + p1.width / 2; startY = p1.y + 100; } else if (p2) { startX = p2.x + p2.width / 2; startY = p2.y + 100; } else { return; } const dropHeight = 80; const bracketY = startY + dropHeight; html += ``; let minChildX = Infinity; let maxChildX = -Infinity; const childTopY = children[0].y; children.forEach(child => { const cx = child.x + child.width / 2; if (cx < minChildX) minChildX = cx; if (cx > maxChildX) maxChildX = cx; html += ``; }); if (children.length > 1) { html += ``; } else { const childX = children[0].x + children[0].width / 2; if (Math.abs(startX - childX) > 1) { html += ``; } } }); svg.innerHTML = html; } // --- Feature: Family Tree Navigation (Pan/Zoom) --- function setupPanZoom() { const treeTab = document.getElementById('tree-tab'); const treeContainer = document.getElementById('tree-container'); if (!treeTab || !treeContainer) { console.error("SetupPanZoom: Elements not found"); return; } console.log("SetupPanZoom: Initializing..."); // Force styles treeTab.style.overflow = 'hidden'; treeTab.style.cursor = 'grab'; treeTab.style.width = '100%'; treeTab.style.height = '100%'; treeTab.style.userSelect = 'none'; // Improve drag feel treeContainer.style.transformOrigin = '0 0'; // Zoom (Wheel) treeTab.addEventListener('wheel', (e) => { e.preventDefault(); // Safety against NaN if (isNaN(treeState.scale)) treeState.scale = 1; if (isNaN(treeState.pointX)) treeState.pointX = 0; if (isNaN(treeState.pointY)) treeState.pointY = 0; if (e.ctrlKey) { const xs = (e.clientX - treeState.pointX) / treeState.scale; const ys = (e.clientY - treeState.pointY) / treeState.scale; const delta = -e.deltaY; (delta > 0) ? (treeState.scale *= 1.1) : (treeState.scale /= 1.1); treeState.scale = Math.min(Math.max(0.1, treeState.scale), 5); treeState.pointX = e.clientX - xs * treeState.scale; treeState.pointY = e.clientY - ys * treeState.scale; } else { // Scroll Pan treeState.pointX -= e.deltaX; treeState.pointY -= e.deltaY; } updateTransform(); }); // Pan (Mouse) treeTab.addEventListener('mousedown', (e) => { // Allow clicks on buttons/inputs if (e.target.closest('button') || e.target.closest('input')) return; // Stop if clicking a card (handled in card click) if (e.target.closest('.family-card')) return; console.log("MouseDown on treeTab"); e.preventDefault(); treeState.startX = e.clientX - treeState.pointX; treeState.startY = e.clientY - treeState.pointY; treeState.panning = true; treeTab.style.cursor = 'grabbing'; }); // Attach to WINDOW/DOCUMENT to handle drags that go outside the element window.addEventListener('mousemove', (e) => { if (!treeState.panning) return; e.preventDefault(); treeState.pointX = e.clientX - treeState.startX; treeState.pointY = e.clientY - treeState.startY; updateTransform(); }); const stopPan = () => { if (treeState.panning) { console.log("MouseUp: Stopping pan"); treeState.panning = false; treeTab.style.cursor = 'grab'; } }; window.addEventListener('mouseup', stopPan); // Also stop on mouseleave if desired, but window mouseup usually catches it treeTab.addEventListener('mouseleave', stopPan); // Global click logger REMOVED } // Initialize PanZoom when DOM is ready document.addEventListener('DOMContentLoaded', () => { setupPanZoom(); }); // Tabs navBtns.forEach(btn => { btn.addEventListener('click', () => { navBtns.forEach(b => b.classList.remove('active')); tabContents.forEach(c => c.classList.remove('active')); btn.classList.add('active'); const tabId = btn.getAttribute('data-tab'); document.getElementById(tabId).classList.add('active'); if (tabId === 'tree-tab') { renderTree(); } else if (tabId === 'map-tab') { if (typeof google !== 'undefined' && typeof google.maps !== 'undefined') { initMap(); } else { console.warn("Google Maps API not loaded. Map will not render."); } } else if (tabId === 'history-tab') { renderHistory(); } }); });