// 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 = []; // --- 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 ? '🌙' : '☀️'; }); // Tabs navBtns.forEach(btn => { btn.addEventListener('click', () => { // Remove active class from all navBtns.forEach(b => b.classList.remove('active')); tabContents.forEach(c => c.classList.remove('active')); // Add active to clicked btn.classList.add('active'); const tabId = btn.getAttribute('data-tab'); document.getElementById(`${tabId}-tab`).classList.add('active'); }); }); // Modals addMemberBtn.addEventListener('click', () => { document.getElementById('modal-title').textContent = "Legg til person"; memberForm.reset(); document.getElementById('member-id').value = ''; // Clear children list const childrenList = document.getElementById('children-list'); if (childrenList) childrenList.innerHTML = '
  • Ingen registrerte barn
  • '; populateDropdowns(); // Populate relationships memberModal.style.display = 'flex'; }); closeModalBtns.forEach(btn => { btn.addEventListener('click', () => { btn.closest('.modal').style.display = 'none'; }); }); window.addEventListener('click', (e) => { if (e.target === memberModal) { memberModal.style.display = 'none'; } }); // --- Data Handling --- function loadFamilyData() { console.log("Loading family data..."); db.collection('familyMembers').onSnapshot(snapshot => { familyData = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); renderTree(); renderHistory(); initMap(); // Re-init map markers }, error => { console.error("Error loading data:", error); }); } function populateDropdowns(excludeId = null) { const motherSelect = document.getElementById('motherId'); const fatherSelect = document.getElementById('fatherId'); const spouseSelect = document.getElementById('spouseId'); // Clear existing options [motherSelect, fatherSelect, spouseSelect].forEach(select => { if (select) select.innerHTML = ''; }); familyData.forEach(member => { if (member.id === excludeId) return; // Don't link to self const option = document.createElement('option'); option.value = member.id; option.textContent = `${member.firstName} ${member.lastName}`; // Add to appropriate dropdowns based on gender/logic // Spouse can be anyone for now if (spouseSelect) spouseSelect.appendChild(option.cloneNode(true)); if (member.gender === 'female') { if (motherSelect) motherSelect.appendChild(option.cloneNode(true)); } else if (member.gender === 'male') { if (fatherSelect) fatherSelect.appendChild(option.cloneNode(true)); } else { // If gender undefined, maybe add to both or neither? // For now, let's add to both to be safe if data is missing if (motherSelect) motherSelect.appendChild(option.cloneNode(true)); if (fatherSelect) fatherSelect.appendChild(option.cloneNode(true)); } }); } memberForm.addEventListener('submit', (e) => { e.preventDefault(); const id = document.getElementById('member-id').value; // Get gender value const genderInputs = document.getElementsByName('gender'); let gender = null; for (const input of genderInputs) { if (input.checked) { gender = input.value; break; } } const memberData = { firstName: document.getElementById('firstName').value, lastName: document.getElementById('lastName').value, gender: gender, birthDate: document.getElementById('birthDate').value, deathDate: document.getElementById('deathDate').value || null, location: document.getElementById('location').value, notes: document.getElementById('notes').value, // Relationships motherId: document.getElementById('motherId').value || null, fatherId: document.getElementById('fatherId').value || null, spouseId: document.getElementById('spouseId').value || null }; if (id) { db.collection('familyMembers').doc(id).update(memberData) .then(() => memberModal.style.display = 'none'); } else { db.collection('familyMembers').add(memberData) .then(() => memberModal.style.display = 'none'); } }); // --- Feature: Family Tree --- function renderTree() { const container = document.getElementById('family-cards-container'); container.innerHTML = ''; if (familyData.length === 0) return; // 1. Calculate Generations const memberMap = new Map(familyData.map(m => [m.id, m])); const generations = {}; const processed = new Set(); function getGeneration(id, depth = 0) { if (depth > 20) return 0; // Prevent infinite recursion const member = memberMap.get(id); if (!member) return 0; let parentGen = -1; if (member.motherId) parentGen = Math.max(parentGen, getGeneration(member.motherId, depth + 1)); if (member.fatherId) parentGen = Math.max(parentGen, getGeneration(member.fatherId, depth + 1)); return parentGen + 1; } familyData.forEach(member => { const gen = getGeneration(member.id); if (!generations[gen]) generations[gen] = []; generations[gen].push(member); }); // 2. Position Members const cardWidth = 220; const cardHeight = 100; // Approx const gapX = 40; const gapY = 150; const startY = 50; // Sort generations to ensure we process 0, 1, 2... const genKeys = Object.keys(generations).sort((a, b) => a - b); // Store positions for line drawing const positions = {}; // id -> {x, y, width, height} genKeys.forEach(genKey => { const members = generations[genKey]; // Sort members to try and keep spouses together (simple heuristic) members.sort((a, b) => { if (a.spouseId === b.id) return -1; return 0; }); // Center the row relative to the container // We'll use a fixed large width for the container to allow scrolling // Let's assume center is 1000px const rowWidth = members.length * (cardWidth + gapX) - gapX; let startX = Math.max(50, 1000 - rowWidth / 2); members.forEach((member, index) => { const x = startX + (index * (cardWidth + gapX)); const y = startY + (genKey * gapY); positions[member.id] = { x, y, width: cardWidth, height: cardHeight }; const card = document.createElement('div'); 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() : ''}
    `; card.addEventListener('click', () => openEditModal(member)); container.appendChild(card); }); }); // 3. Draw Lines drawLines(positions); } function drawLines(positions) { const svg = document.getElementById('relationship-lines'); svg.innerHTML = ''; // Set SVG size to match content const maxX = Math.max(...Object.values(positions).map(p => p.x + p.width)) + 100; const maxY = Math.max(...Object.values(positions).map(p => p.y + p.height)) + 100; svg.setAttribute('width', Math.max(2000, maxX)); // Min width 2000 for scrolling svg.setAttribute('height', Math.max(1000, maxY)); familyData.forEach(member => { const pos = positions[member.id]; if (!pos) return; // Parent Connections [member.motherId, member.fatherId].forEach(parentId => { if (parentId && positions[parentId]) { const parentPos = positions[parentId]; // Draw line from bottom of parent to top of child const x1 = parentPos.x + parentPos.width / 2; const parentBottom = parentPos.y + 120; // Approx bottom of card const x2 = pos.x + pos.width / 2; const y2 = pos.y; // Bezier curve const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); const d = `M ${x1} ${parentBottom} C ${x1} ${parentBottom + 50}, ${x2} ${y2 - 50}, ${x2} ${y2}`; path.setAttribute("d", d); path.setAttribute("stroke", "#88a090"); // Moss color path.setAttribute("stroke-width", "2"); path.setAttribute("fill", "none"); svg.appendChild(path); } }); // Spouse Connection if (member.spouseId && positions[member.spouseId]) { const spousePos = positions[member.spouseId]; // Only draw once (e.g., if id < spouseId) if (member.id < member.spouseId) { const x1 = pos.x + pos.width; const y1 = pos.y + 60; // Middle of card approx const x2 = spousePos.x; const y2 = spousePos.y + 60; const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); const d = `M ${x1} ${y1} L ${x2} ${y2}`; path.setAttribute("d", d); path.setAttribute("stroke", "#1a472a"); // Forest color path.setAttribute("stroke-width", "2"); path.setAttribute("stroke-dasharray", "5,5"); // Dashed for spouse path.setAttribute("fill", "none"); svg.appendChild(path); } } }); } function openEditModal(member) { document.getElementById('modal-title').textContent = "Rediger person"; document.getElementById('member-id').value = member.id; document.getElementById('firstName').value = member.firstName; document.getElementById('lastName').value = member.lastName; document.getElementById('birthDate').value = member.birthDate; document.getElementById('deathDate').value = member.deathDate || ''; document.getElementById('location').value = member.location || ''; document.getElementById('notes').value = member.notes || ''; // Set Gender const genderInputs = document.getElementsByName('gender'); for (const input of genderInputs) { if (input.value === member.gender) { input.checked = true; } } // Populate dropdowns first (excluding self) populateDropdowns(member.id); // Set Relationships document.getElementById('motherId').value = member.motherId || ''; document.getElementById('fatherId').value = member.fatherId || ''; document.getElementById('spouseId').value = member.spouseId || ''; // List Children const childrenList = document.getElementById('children-list'); if (childrenList) { childrenList.innerHTML = ''; const children = familyData.filter(m => m.motherId === member.id || m.fatherId === member.id); if (children.length === 0) { childrenList.innerHTML = '
  • Ingen registrerte barn
  • '; } else { children.forEach(child => { const li = document.createElement('li'); li.textContent = `${child.firstName} ${child.lastName}`; li.style.padding = '4px 0'; li.style.borderBottom = '1px solid rgba(0,0,0,0.05)'; childrenList.appendChild(li); }); } } // Add Child Button Handler const addChildBtn = document.getElementById('addChildBtn'); if (addChildBtn) { addChildBtn.onclick = () => { // Close current modal memberModal.style.display = 'none'; // Open new modal for child setTimeout(() => { document.getElementById('modal-title').textContent = "Legg til barn"; memberForm.reset(); document.getElementById('member-id').value = ''; populateDropdowns(); // Pre-select parent based on gender if (member.gender === 'female') { document.getElementById('motherId').value = member.id; } else if (member.gender === 'male') { document.getElementById('fatherId').value = member.id; } // Pre-fill last name document.getElementById('lastName').value = member.lastName; memberModal.style.display = 'flex'; }, 100); }; } memberModal.style.display = 'flex'; } // --- Feature: Map --- // Global function for Google Maps callback window.initMap = function () { const mapElement = document.getElementById('map'); if (!mapElement) return; // Default to Høvåg/Røsstad area const center = { lat: 58.16, lng: 8.25 }; const map = new google.maps.Map(mapElement, { zoom: 10, center: center, styles: [ /* Optional: Add custom map styles for dark mode here */] }); // Add markers for family members familyData.forEach(member => { if (member.lat && member.lng) { new google.maps.Marker({ position: { lat: member.lat, lng: member.lng }, map: map, title: `${member.firstName} ${member.lastName}` }); } }); } // --- Feature: History --- function renderHistory() { const list = document.getElementById('history-list'); list.innerHTML = ''; // Sort by birthdate (Oldest first) const sortedMembers = [...familyData].sort((a, b) => { return new Date(a.birthDate) - new Date(b.birthDate); }); sortedMembers.forEach(member => { const item = document.createElement('li'); item.className = 'history-item'; const year = new Date(member.birthDate).getFullYear(); item.innerHTML = ` ${year} - ${member.firstName} ${member.lastName} ble født. `; list.appendChild(item); }); }