feat: Initialize family tree web application with basic structure, comprehensive styling, and initial JavaScript.

This commit is contained in:
Eivind Røsstad Skeie 2025-11-26 00:18:03 +01:00
parent ac040fa8bc
commit 146eb6c02a
3 changed files with 724 additions and 24 deletions

118
index.html Normal file
View file

@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="no">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Røsstad Familietre</title>
<link rel="stylesheet" href="style.css">
<!-- Google Maps API -->
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBdC0kSFC6auWxO5bttoclLv48i3F44OPY&callback=initMap"
async defer></script>
</head>
<body class="dark-mode"> <!-- Default to dark mode as requested -->
<nav class="top-nav">
<div class="nav-brand">Røsstad Slekt</div>
<div class="nav-tabs">
<button class="nav-btn active" data-tab="tree">Familietre</button>
<button class="nav-btn" data-tab="map">Kart</button>
<button class="nav-btn" data-tab="history">Historie</button>
</div>
<div class="nav-controls">
<button id="darkModeToggle" class="icon-btn" title="Bytt lys/mørk modus">🌙</button>
<button id="logoutBtn" class="text-btn" style="display: none;">Logg ut</button>
</div>
</nav>
<main id="main-content">
<!-- Tab 1: Family Tree -->
<section id="tree-tab" class="tab-content active">
<div id="tree-container">
<svg id="relationship-lines"></svg>
<div id="family-cards-container">
<!-- Cards will be injected here -->
</div>
</div>
<button id="addMemberBtn" class="fab" title="Legg til person">+</button>
</section>
<!-- Tab 2: Map -->
<section id="map-tab" class="tab-content">
<div id="map"></div>
</section>
<!-- Tab 3: History -->
<section id="history-tab" class="tab-content">
<div class="history-container">
<h2>Slektshistorie</h2>
<ul id="history-list">
<!-- History items will be injected here -->
</ul>
</div>
</section>
</main>
<!-- Login Modal -->
<div id="login-modal" class="modal">
<div class="modal-content login-box">
<h2>Velkommen til Røsstad</h2>
<p>Vennligst logg inn for å se slektstreet.</p>
<form id="login-form">
<input type="email" id="email" placeholder="E-post" required>
<input type="password" id="password" placeholder="Passord" required>
<button type="submit" class="primary-btn">Logg inn</button>
</form>
<p id="login-error" class="error-msg"></p>
</div>
</div>
<!-- Add/Edit Member Modal -->
<div id="member-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close-modal">&times;</span>
<h2 id="modal-title">Legg til person</h2>
<form id="member-form">
<input type="hidden" id="member-id">
<div class="form-group">
<label>Fornavn</label>
<input type="text" id="firstName" required>
</div>
<div class="form-group">
<label>Etternavn</label>
<input type="text" id="lastName" required>
</div>
<div class="form-row">
<div class="form-group">
<label>Fødselsdato</label>
<input type="date" id="birthDate" required>
</div>
<div class="form-group">
<label>Dødsdato (valgfritt)</label>
<input type="date" id="deathDate">
</div>
</div>
<div class="form-group">
<label>Sted (for kart)</label>
<input type="text" id="location" placeholder="F.eks. Høvåg">
</div>
<div class="form-group">
<label>Notater</label>
<textarea id="notes" rows="4"></textarea>
</div>
<!-- Relationship fields would go here (simplified for now) -->
<button type="submit" class="primary-btn">Lagre</button>
</form>
</div>
</div>
<!-- Firebase SDKs -->
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-auth-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore-compat.js"></script>
<script src="script.js"></script>
</body>
</html>

243
script.js Normal file
View file

@ -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 = `
<div class="card-name">${member.firstName} ${member.lastName}</div>
<div class="card-dates">
${new Date(member.birthDate).getFullYear()} - ${member.deathDate ? new Date(member.deathDate).getFullYear() : ''}
</div>
`;
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 = `
<span class="history-year">${year}</span> -
<strong>${member.firstName} ${member.lastName}</strong> ble født.
`;
list.appendChild(item);
});
}

389
style.css
View file

@ -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);
--color-text-main: #1e293b;
/* Slate 800 */
--color-text-muted: #64748b;
/* Slate 500 */
/* Spacing */
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
/* 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;
}