diff --git a/index.html b/index.html new file mode 100644 index 0000000..03ed2ad --- /dev/null +++ b/index.html @@ -0,0 +1,118 @@ + + + + + + + Røsstad Familietre + + + + + + + + + +
+ +
+
+ +
+ +
+
+ +
+ + +
+
+
+ + +
+
+

Slektshistorie

+
    + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..e341610 --- /dev/null +++ b/script.js @@ -0,0 +1,243 @@ +// 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 = ''; + 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); + }); +} + +memberForm.addEventListener('submit', (e) => { + e.preventDefault(); + const id = document.getElementById('member-id').value; + + const memberData = { + firstName: document.getElementById('firstName').value, + lastName: document.getElementById('lastName').value, + birthDate: document.getElementById('birthDate').value, + deathDate: document.getElementById('deathDate').value || null, + location: document.getElementById('location').value, + notes: document.getElementById('notes').value, + // TODO: Handle lat/lng via Geocoding API or manual input later + // TODO: Handle relationships + }; + + 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 = ''; // Clear current + + // Simple grid layout for now (placeholder for complex tree algo) + // In a real tree, we'd calculate positions based on generations + + familyData.forEach((member, index) => { + const card = document.createElement('div'); + card.className = 'family-card'; + // Simple positioning for demo purposes + card.style.top = `${100 + (Math.floor(index / 5) * 150)}px`; + card.style.left = `${50 + ((index % 5) * 250)}px`; + + card.innerHTML = ` +
${member.firstName} ${member.lastName}
+
+ ${new Date(member.birthDate).getFullYear()} - ${member.deathDate ? new Date(member.deathDate).getFullYear() : ''} +
+ `; + + card.addEventListener('click', () => openEditModal(member)); + container.appendChild(card); + }); + + drawLines(); +} + +function drawLines() { + // Placeholder for SVG line drawing logic + const svg = document.getElementById('relationship-lines'); + svg.innerHTML = ''; + // Logic to draw lines between parent/child/spouse would go here +} + +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 || ''; + 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); + }); +} diff --git a/style.css b/style.css index 55e4f05..0ca4957 100644 --- a/style.css +++ b/style.css @@ -1,30 +1,369 @@ -/* --- RØSSTAD DESIGN SYSTEM --- */ -@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap'); - - :root { - /* --- PALETTE: Røsstad Green --- */ - --color-primary-dark: #0f2f24; - /* Deep Forest */ - --color-primary: #134e4a; - /* Røsstad Teal */ - --color-primary-light: #2dd4bf; - /* Bright Teal */ + /* Color Palette */ + --color-forest: #1a472a; + /* Hovedfarge - Dyp skogsgrønn */ + --color-sea: #0f2b3e; + /* Sekundærfarge - Mørk blågrønn/skifer */ + --color-moss: #88a090; + /* Aksentfarge - Lav/Mose */ + --color-light-bg: #f4f7f5; + /* Bakgrunn Light Mode - Tåke */ + --color-dark-bg: #121212; + /* Bakgrunn Dark Mode - Dyp natt */ + --color-text-light: #121212; + --color-text-dark: #f4f7f5; + --color-white: #ffffff; + --color-error: #e74c3c; - --color-accent: #d97706; - /* Gold/Bronze */ - --color-danger: #be123c; - /* Berry Red */ + /* Fonts */ + --font-main: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - --color-bg: #f0fdf4; - /* Soft Mist Green */ - --color-surface: #ffffff; - /* Pure White */ - --color-surface-glass: rgba(255, 255, 255, 0.85); + /* Spacing */ + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; - --color-text-main: #1e293b; - /* Slate 800 */ - --color-text-muted: #64748b; - /* Slate 500 */ + /* Transitions */ + --transition-speed: 0.3s; +} +/* Reset & Base Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: var(--font-main); + background-color: var(--color-light-bg); + color: var(--color-text-light); + transition: background-color var(--transition-speed), color var(--transition-speed); + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Dark Mode */ +body.dark-mode { + background-color: var(--color-dark-bg); + color: var(--color-text-dark); +} + +/* Navigation */ +.top-nav { + background-color: var(--color-forest); + color: var(--color-white); + padding: var(--spacing-md) var(--spacing-lg); + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + z-index: 100; +} + +.nav-brand { + font-size: 1.5rem; + font-weight: bold; + letter-spacing: 1px; +} + +.nav-tabs { + display: flex; + gap: var(--spacing-md); +} + +.nav-btn { + background: none; + border: none; + color: rgba(255, 255, 255, 0.7); + font-size: 1rem; + cursor: pointer; + padding: var(--spacing-sm) var(--spacing-md); + border-radius: 4px; + transition: all var(--transition-speed); +} + +.nav-btn:hover, +.nav-btn.active { + color: var(--color-white); + background-color: rgba(255, 255, 255, 0.1); +} + +.nav-controls { + display: flex; + gap: var(--spacing-md); + align-items: center; +} + +.icon-btn { + background: none; + border: none; + font-size: 1.2rem; + cursor: pointer; + color: var(--color-white); +} + +.text-btn { + background: none; + border: 1px solid rgba(255, 255, 255, 0.5); + color: var(--color-white); + padding: 4px 12px; + border-radius: 4px; + cursor: pointer; +} + +.text-btn:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +/* Main Content */ +#main-content { + flex: 1; + position: relative; + overflow: hidden; +} + +.tab-content { + display: none; + height: 100%; + width: 100%; +} + +.tab-content.active { + display: block; +} + +/* Tree View */ +#tree-container { + width: 100%; + height: 100%; + position: relative; + overflow: auto; + /* Allow scrolling if tree is large */ + background-image: radial-gradient(circle, #ddd 1px, transparent 1px); + /* Grid dots */ + background-size: 20px 20px; +} + +body.dark-mode #tree-container { + background-image: radial-gradient(circle, #333 1px, transparent 1px); +} + +#relationship-lines { + position: absolute; + top: 0; + left: 0; + width: 100%; + /* Will need to update dynamically via JS */ + height: 100%; + pointer-events: none; + /* Let clicks pass through to cards */ + z-index: 0; +} + +.family-card { + position: absolute; + background-color: var(--color-white); + color: var(--color-text-light); + padding: var(--spacing-md); + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 200px; + text-align: center; + cursor: pointer; + z-index: 1; + border: 2px solid transparent; + transition: transform 0.2s, box-shadow 0.2s; +} + +body.dark-mode .family-card { + background-color: var(--color-sea); + color: var(--color-white); + border: 1px solid var(--color-moss); +} + +.family-card:hover { + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2); + border-color: var(--color-forest); +} + +.card-name { + font-weight: bold; + font-size: 1.1rem; + margin-bottom: 4px; +} + +.card-dates { + font-size: 0.9rem; + color: #666; +} + +body.dark-mode .card-dates { + color: #aaa; +} + +/* Map View */ +#map { + width: 100%; + height: 100%; +} + +/* History View */ +.history-container { + max-width: 800px; + margin: 0 auto; + padding: var(--spacing-lg); + overflow-y: auto; + height: 100%; +} + +#history-list { + list-style: none; + border-left: 2px solid var(--color-moss); + padding-left: var(--spacing-lg); +} + +.history-item { + margin-bottom: var(--spacing-lg); + position: relative; +} + +.history-item::before { + content: ''; + position: absolute; + left: calc(-1 * var(--spacing-lg) - 5px); + top: 6px; + width: 10px; + height: 10px; + background-color: var(--color-forest); + border-radius: 50%; +} + +.history-year { + font-weight: bold; + color: var(--color-moss); +} + +/* Floating Action Button */ +.fab { + position: absolute; + bottom: 32px; + right: 32px; + width: 56px; + height: 56px; + border-radius: 50%; + background-color: var(--color-forest); + color: var(--color-white); + border: none; + font-size: 24px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s; + z-index: 10; +} + +.fab:hover { + transform: scale(1.1); +} + +/* Modals */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + backdrop-filter: blur(5px); +} + +.modal-content { + background-color: var(--color-white); + padding: var(--spacing-lg); + border-radius: 8px; + width: 90%; + max-width: 500px; + position: relative; +} + +body.dark-mode .modal-content { + background-color: var(--color-sea); + color: var(--color-white); + border: 1px solid var(--color-moss); +} + +.close-modal { + position: absolute; + top: var(--spacing-sm); + right: var(--spacing-md); + font-size: 1.5rem; + cursor: pointer; +} + +/* Forms */ +.form-group { + margin-bottom: var(--spacing-md); +} + +.form-row { + display: flex; + gap: var(--spacing-md); +} + +.form-row .form-group { + flex: 1; +} + +label { + display: block; + margin-bottom: 4px; + font-weight: 500; +} + +input, +textarea { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + font-family: inherit; +} + +body.dark-mode input, +body.dark-mode textarea { + background-color: rgba(0, 0, 0, 0.2); + border-color: var(--color-moss); + color: var(--color-white); +} + +.primary-btn { + background-color: var(--color-forest); + color: var(--color-white); + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + width: 100%; + font-size: 1rem; + margin-top: var(--spacing-md); +} + +.primary-btn:hover { + background-color: #143620; +} + +.error-msg { + color: var(--color-error); + margin-top: var(--spacing-sm); + font-size: 0.9rem; } \ No newline at end of file