From 37f4ce18743ea3f448dc77d32578402c168acf52 Mon Sep 17 00:00:00 2001
From: Anaru <98413596+FunnyEivske@users.noreply.github.com>
Date: Mon, 24 Nov 2025 17:34:35 +0100
Subject: [PATCH] First push
---
Røsstad.html | 88 ++++++++++++++++
Script.js | 288 +++++++++++++++++++++++++++++++++++++++++++++++++++
config.js | 12 +++
index.html | 27 +++++
login.js | 28 +++++
style.css | 219 +++++++++++++++++++++++++++++++++++++++
6 files changed, 662 insertions(+)
create mode 100644 Røsstad.html
create mode 100644 Script.js
create mode 100644 config.js
create mode 100644 index.html
create mode 100644 login.js
create mode 100644 style.css
diff --git a/Røsstad.html b/Røsstad.html
new file mode 100644
index 0000000..7a0c942
--- /dev/null
+++ b/Røsstad.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+ Røsstad Familietre
+
+
+
+
+
+
+
+
+
+
+ Røsstad Familietre
+
+
+
+ 100%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Script.js b/Script.js
new file mode 100644
index 0000000..bd91def
--- /dev/null
+++ b/Script.js
@@ -0,0 +1,288 @@
+import { initializeApp } from "[https://www.gstatic.com/firebasejs/12.6.0/firebase-app.js](https://www.gstatic.com/firebasejs/12.6.0/firebase-app.js)";
+import { getAuth, onAuthStateChanged, signOut } from "[https://www.gstatic.com/firebasejs/12.6.0/firebase-auth.js](https://www.gstatic.com/firebasejs/12.6.0/firebase-auth.js)";
+import { getFirestore, collection, addDoc, updateDoc, doc, onSnapshot, query } from "[https://www.gstatic.com/firebasejs/12.6.0/firebase-firestore.js](https://www.gstatic.com/firebasejs/12.6.0/firebase-firestore.js)";
+import { firebaseConfig } from "./config.js";
+
+// Start
+const app = initializeApp(firebaseConfig);
+const auth = getAuth(app);
+const db = getFirestore(app);
+
+// Variabler
+let currentUser = null;
+let people = [];
+let focusPersonId = null;
+let currentScale = 1.0;
+let modalAction = null;
+let modalMode = 'new';
+
+// --- SIKKERHETSSJEKK ---
+// Hvis brukeren ikke er logget inn, kast dem tilbake til index.html
+onAuthStateChanged(auth, (user) => {
+ if (user) {
+ currentUser = user;
+ startDataListener();
+ } else {
+ window.location.href = "index.html";
+ }
+});
+
+document.getElementById('btn-logout').addEventListener('click', () => {
+ signOut(auth).then(() => {
+ window.location.href = "index.html";
+ });
+});
+
+// --- DATA ---
+function startDataListener() {
+ const q = query(collection(db, 'familyMembers'));
+
+ onSnapshot(q, (snapshot) => {
+ people = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
+ if (!focusPersonId && people.length > 0) {
+ const me = people.find(p => p.name.toLowerCase().includes('eivind') || p.name.toLowerCase() === 'meg');
+ focusPersonId = me ? me.id : people[0].id;
+ }
+ renderTree();
+ });
+}
+
+// --- RENDERING ---
+function renderTree() {
+ const rowParents = document.getElementById('row-parents');
+ const rowFocus = document.getElementById('row-focus');
+ const rowChildren = document.getElementById('row-children');
+
+ // Tøm
+ rowParents.innerHTML = '';
+ rowFocus.innerHTML = '';
+ rowChildren.innerHTML = '';
+
+ if (people.length === 0) {
+ rowFocus.innerHTML = ``;
+ return;
+ }
+
+ const focusPerson = people.find(p => p.id === focusPersonId);
+ if (!focusPerson) return;
+
+ // A. Foreldre
+ const father = people.find(p => p.id === focusPerson.fatherId);
+ const mother = people.find(p => p.id === focusPerson.motherId);
+ rowParents.appendChild(createSlot(father, 'Far', 'add-parent-m'));
+ rowParents.appendChild(createSlot(mother, 'Mor', 'add-parent-f'));
+
+ // B. Partner
+ const partner = people.find(p => p.id === focusPerson.partnerId);
+ if (partner) {
+ const pCard = createCard(partner);
+ pCard.onclick = () => { focusPersonId = partner.id; renderTree(); };
+ rowFocus.appendChild(pCard);
+
+ // Hjerte
+ const heart = document.createElement('div');
+ heart.innerHTML = '';
+ rowFocus.appendChild(heart);
+ }
+
+ // C. Fokus Person
+ rowFocus.appendChild(createCard(focusPerson, true));
+
+ // D. Barn
+ const children = people.filter(p => p.fatherId === focusPersonId || p.motherId === focusPersonId);
+ children.forEach(child => {
+ const cCard = createCard(child);
+ cCard.onclick = () => { focusPersonId = child.id; renderTree(); };
+ rowChildren.appendChild(cCard);
+ });
+}
+
+// Hjelper: Lag Kort
+function createCard(person, isFocus = false) {
+ const el = document.createElement('div');
+ el.className = `card ${isFocus ? 'focus' : ''} ${person.gender === 'F' ? 'female' : 'male'}`;
+
+ const year = person.birthDate ? new Date(person.birthDate).getFullYear() : '????';
+ const dead = person.deathDate ? '✝ ' : '';
+
+ el.innerHTML = `
+
+ ${person.name}
+ ${dead}${year}
+ ${person.description ? 'Info' : ''}
+ `;
+
+ if (isFocus) {
+ const actions = document.createElement('div');
+ actions.className = 'focus-actions';
+
+ // Legg til barn
+ const btnChild = document.createElement('button');
+ btnChild.className = 'btn-mini-add add-child';
+ btnChild.innerHTML = '';
+ btnChild.onclick = (e) => { e.stopPropagation(); window.openModal('add-child'); };
+ actions.appendChild(btnChild);
+
+ // Legg til partner (hvis mangler)
+ if (!person.partnerId) {
+ const btnPartner = document.createElement('button');
+ btnPartner.className = 'btn-mini-add add-partner';
+ btnPartner.innerHTML = '';
+ btnPartner.onclick = (e) => { e.stopPropagation(); window.openModal('add-partner'); };
+ actions.appendChild(btnPartner);
+ }
+ el.appendChild(actions);
+ }
+ return el;
+}
+
+// Hjelper: Lag Slot (Kort eller Pluss-knapp)
+function createSlot(person, label, actionType) {
+ const wrapper = document.createElement('div');
+ wrapper.className = 'slot-container';
+ wrapper.innerHTML = `${label}`;
+
+ if (person) {
+ const card = createCard(person);
+ card.onclick = () => { focusPersonId = person.id; renderTree(); };
+ wrapper.appendChild(card);
+ } else {
+ const btn = document.createElement('div');
+ btn.className = 'btn-add-slot';
+ btn.innerHTML = '';
+ btn.onclick = () => window.openModal(actionType);
+ wrapper.appendChild(btn);
+ }
+ return wrapper;
+}
+
+// --- MODAL & LAGRING ---
+window.openModal = (action) => {
+ modalAction = action;
+ document.getElementById('modal-overlay').classList.remove('hidden');
+
+ // Nullstill skjema
+ document.getElementById('inp-name').value = '';
+ document.getElementById('inp-birth').value = '';
+ document.getElementById('inp-death').value = '';
+ document.getElementById('inp-desc').value = '';
+ document.getElementById('selected-existing-id').value = '';
+
+ const title = document.getElementById('modal-title');
+ const genderRadios = document.getElementsByName('gender');
+
+ const setGender = (val, disabled) => {
+ genderRadios.forEach(r => {
+ r.disabled = disabled;
+ if (val && r.value === val) r.checked = true;
+ });
+ };
+
+ if (action === 'add-parent-m') { title.textContent = "Legg til Far"; setGender('M', true); }
+ else if (action === 'add-parent-f') { title.textContent = "Legg til Mor"; setGender('F', true); }
+ else if (action === 'add-child') { title.textContent = "Legg til Barn"; setGender(null, false); }
+ else if (action === 'add-partner') { title.textContent = "Legg til Partner"; setGender(null, false); }
+ else { title.textContent = "Start Tre"; setGender(null, false); }
+
+ setMode('new');
+ renderExistingList();
+};
+
+document.getElementById('btn-close-modal').onclick = () => document.getElementById('modal-overlay').classList.add('hidden');
+
+document.getElementById('btn-save').onclick = async () => {
+ if (!currentUser) return;
+ const focusP = people.find(p => p.id === focusPersonId);
+ let targetId = null;
+
+ if (modalMode === 'new') {
+ const newPerson = {
+ name: document.getElementById('inp-name').value,
+ birthDate: document.getElementById('inp-birth').value,
+ deathDate: document.getElementById('inp-death').value,
+ description: document.getElementById('inp-desc').value,
+ gender: document.querySelector('input[name="gender"]:checked').value,
+ fatherId: null, motherId: null, partnerId: null
+ };
+
+ // Auto-link hvis vi legger til barn
+ if (modalAction === 'add-child' && focusP) {
+ if (focusP.gender === 'M') newPerson.fatherId = focusP.id;
+ else newPerson.motherId = focusP.id;
+ // Sjekk partner
+ if (focusP.partnerId) {
+ const part = people.find(p=>p.id === focusP.partnerId);
+ if(part) { if(part.gender==='M') newPerson.fatherId=part.id; else newPerson.motherId=part.id; }
+ }
+ }
+
+ const docRef = await addDoc(collection(db, 'familyMembers'), newPerson);
+ targetId = docRef.id;
+ if(modalAction === 'create-first') focusPersonId = targetId;
+
+ } else {
+ targetId = document.getElementById('selected-existing-id').value;
+ }
+
+ // Lagre relasjoner i databasen
+ if(focusP && targetId) {
+ const col = collection(db, 'familyMembers');
+ const updates = [];
+
+ if(modalAction === 'add-parent-m') updates.push(updateDoc(doc(col, focusPersonId), {fatherId: targetId}));
+ else if(modalAction === 'add-parent-f') updates.push(updateDoc(doc(col, focusPersonId), {motherId: targetId}));
+ else if(modalAction === 'add-partner') {
+ updates.push(updateDoc(doc(col, focusPersonId), {partnerId: targetId}));
+ updates.push(updateDoc(doc(col, targetId), {partnerId: focusPersonId}));
+ }
+ else if(modalAction === 'add-child' && modalMode === 'existing') {
+ const upd = {};
+ if(focusP.gender === 'M') upd.fatherId = focusP.id; else upd.motherId = focusP.id;
+ updates.push(updateDoc(doc(col, targetId), upd));
+ }
+ await Promise.all(updates);
+ }
+ document.getElementById('modal-overlay').classList.add('hidden');
+};
+
+function setMode(mode) {
+ modalMode = mode;
+ document.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
+ document.querySelector(`.toggle-btn[data-mode="${mode}"]`).classList.add('active');
+ document.getElementById('form-new').className = mode === 'new' ? '' : 'hidden';
+ document.getElementById('form-existing').className = mode === 'existing' ? '' : 'hidden';
+}
+
+document.querySelectorAll('.toggle-btn').forEach(btn => {
+ btn.onclick = () => setMode(btn.dataset.mode);
+});
+
+function renderExistingList() {
+ const list = document.getElementById('existing-list');
+ list.innerHTML = '';
+ // Filtrer bort fokuspersonen selv
+ const candidates = people.filter(p => p.id !== focusPersonId);
+ candidates.forEach(p => {
+ const d = document.createElement('div');
+ d.className = 'person-option';
+ d.innerHTML = `${p.name}`;
+ d.onclick = () => {
+ document.querySelectorAll('.person-option').forEach(x=>x.classList.remove('selected'));
+ d.classList.add('selected');
+ document.getElementById('selected-existing-id').value = p.id;
+ };
+ list.appendChild(d);
+ });
+}
+
+// Zoom Knapper
+document.getElementById('btn-zoom-in').onclick = () => {
+ currentScale += 0.1;
+ document.getElementById('tree-grid').style.transform = `scale(${currentScale})`;
+ document.getElementById('zoom-level').textContent = Math.round(currentScale * 100) + '%';
+};
+document.getElementById('btn-zoom-out').onclick = () => {
+ if (currentScale > 0.4) currentScale -= 0.1;
+ document.getElementById('tree-grid').style.transform = `scale(${currentScale})`;
+ document.getElementById('zoom-level').textContent = Math.round(currentScale * 100) + '%';
+};
diff --git a/config.js b/config.js
new file mode 100644
index 0000000..1f8bec7
--- /dev/null
+++ b/config.js
@@ -0,0 +1,12 @@
+// --- FIREBASE CONFIG ---
+// Prosjekt: roesstad-9fd48
+export 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"
+};
+
+
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..cfa3791
--- /dev/null
+++ b/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Logg Inn - Røsstad Arkivet
+
+
+
+
+
+
+
+
+
+
Røsstad Arkivet
+
Sikker pålogging kreves
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/login.js b/login.js
new file mode 100644
index 0000000..1c441b1
--- /dev/null
+++ b/login.js
@@ -0,0 +1,28 @@
+import { initializeApp } from "[https://www.gstatic.com/firebasejs/12.6.0/firebase-app.js](https://www.gstatic.com/firebasejs/12.6.0/firebase-app.js)";
+import { getAuth, signInAnonymously } from "[https://www.gstatic.com/firebasejs/12.6.0/firebase-auth.js](https://www.gstatic.com/firebasejs/12.6.0/firebase-auth.js)";
+import { firebaseConfig } from "./config.js";
+
+// Init
+const app = initializeApp(firebaseConfig);
+const auth = getAuth(app);
+
+// Håndter klikk
+document.getElementById('btn-login').addEventListener('click', async () => {
+ const btn = document.getElementById('btn-login');
+ const msg = document.getElementById('error-msg');
+
+ btn.textContent = "Autentiserer...";
+ msg.textContent = "";
+
+ try {
+ await signInAnonymously(auth);
+
+ // Suksess -> Gå til hovedsiden (Røsstad.html)
+ window.location.href = "Røsstad.html";
+
+ } catch (error) {
+ console.error(error);
+ msg.textContent = "Feil: " + error.message;
+ btn.textContent = "Prøv igjen";
+ }
+});
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..0bedb43
--- /dev/null
+++ b/style.css
@@ -0,0 +1,219 @@
+/* --- VARIABLER (Norsk Skog) --- */
+:root {
+ --forest-green: #1e5128; /* Mørk gran */
+ --leaf-green: #4e8d46; /* Lysere nåler */
+ --bg-color: #f0f4f1; /* Lys grågrønn bakgrunn */
+ --card-bg: #ffffff;
+ --text-main: #1c1c1c;
+ --text-muted: #666;
+ --border-color: #d1d5db;
+
+ --male-color: #2563eb; /* Blå */
+ --female-color: #db2777; /* Rosa */
+}
+
+body {
+ margin: 0;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background-color: var(--bg-color);
+ color: var(--text-main);
+ height: 100vh;
+ overflow: hidden;
+}
+
+/* --- HJELPEKLASSER --- */
+.hidden { display: none !important; }
+.full-width { width: 100%; }
+
+/* --- LOGIN SIDE STIL --- */
+body.login-page {
+ display: flex; justify-content: center; align-items: center;
+ background: linear-gradient(to bottom, #112d16, #0d1f10);
+}
+
+.login-box {
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ padding: 3rem;
+ border-radius: 1rem;
+ text-align: center;
+ color: white;
+ border: 1px solid rgba(255,255,255,0.2);
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
+ width: 100%;
+ max-width: 400px;
+}
+
+.logo-icon {
+ font-size: 4rem;
+ color: #4ade80;
+ margin-bottom: 1rem;
+ filter: drop-shadow(0 0 10px rgba(74, 222, 128, 0.3));
+}
+
+.error-msg {
+ margin-top: 1rem;
+ color: #f87171;
+ font-size: 0.9rem;
+ min-height: 1.2em;
+}
+
+/* --- HEADER (Hovedside) --- */
+header {
+ background: var(--forest-green);
+ color: white;
+ padding: 0.8rem 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
+}
+
+.brand {
+ font-size: 1.2rem;
+ font-weight: bold;
+ display: flex; align-items: center; gap: 0.5rem;
+}
+
+.controls {
+ display: flex; align-items: center; gap: 1rem;
+}
+
+.controls button {
+ background: none; border: none; color: #a7f3d0;
+ font-size: 1.5rem; cursor: pointer; transition: color 0.2s;
+}
+.controls button:hover { color: white; }
+
+/* --- KNAPPER --- */
+.btn-primary {
+ background-color: var(--leaf-green);
+ color: white; border: none; padding: 0.8rem 1.5rem;
+ border-radius: 0.5rem; font-weight: bold; cursor: pointer;
+ transition: background 0.2s;
+}
+.btn-primary:hover { background-color: var(--forest-green); }
+
+.btn-outline {
+ font-size: 0.9rem !important;
+ border: 1px solid rgba(255,255,255,0.3) !important;
+ padding: 0.3rem 0.8rem; border-radius: 4px;
+}
+
+/* --- TREET (GRID LAYOUT) --- */
+#app-container {
+ display: flex; height: 100vh; flex-direction: column;
+}
+
+#tree-view {
+ flex: 1; overflow: auto; position: relative;
+ display: flex; justify-content: center; align-items: center;
+ padding: 3rem;
+ background-image: radial-gradient(#d1d5db 1px, transparent 1px);
+ background-size: 24px 24px;
+}
+
+.tree-grid {
+ display: flex; flex-direction: column; align-items: center;
+ gap: 4rem; transition: transform 0.3s ease;
+}
+
+.row-parents, .row-focus, .row-children {
+ display: flex; justify-content: center; align-items: center;
+ position: relative;
+}
+.row-parents { gap: 3rem; }
+.row-focus { gap: 2rem; }
+.row-children { gap: 1.5rem; flex-wrap: wrap; }
+
+/* --- KORT --- */
+.card {
+ background: white; width: 180px; min-height: 110px;
+ border-radius: 12px; border: 2px solid var(--border-color);
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
+ padding: 1rem; display: flex; flex-direction: column; align-items: center;
+ position: relative; cursor: pointer; transition: all 0.2s;
+}
+.card:hover { transform: translateY(-3px); border-color: var(--leaf-green); }
+.card.focus {
+ border-color: var(--forest-green); background: #f0fdf4;
+ transform: scale(1.1); z-index: 10;
+ box-shadow: 0 0 0 4px rgba(78, 141, 70, 0.2);
+}
+
+.card h3 { margin: 0.5rem 0 0.2rem 0; font-size: 0.95rem; text-align: center; }
+.card p { margin: 0; font-size: 0.75rem; color: var(--text-muted); font-family: monospace; }
+.card .info-badge { font-size: 0.7rem; color: var(--forest-green); background: #dcfce7; padding: 2px 6px; border-radius: 10px; margin-top: 5px; }
+
+.card-icon { font-size: 1.5rem; padding: 8px; border-radius: 50%; }
+.male .card-icon { color: var(--male-color); background: #eff6ff; }
+.female .card-icon { color: var(--female-color); background: #fdf2f8; }
+
+/* --- PLUSS-KNAPPER --- */
+.slot-container {
+ display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
+}
+.slot-label {
+ font-size: 0.7rem; text-transform: uppercase; font-weight: bold; color: var(--text-muted);
+}
+.btn-add-slot {
+ width: 40px; height: 40px; border-radius: 50%;
+ border: 2px dashed #9ca3af; background: rgba(255,255,255,0.5);
+ color: #9ca3af; display: flex; justify-content: center; align-items: center;
+ cursor: pointer; font-size: 1.2rem; transition: all 0.2s;
+}
+.btn-add-slot:hover { border-color: var(--forest-green); color: var(--forest-green); background: white; }
+
+.focus-actions {
+ position: absolute; width: 100%; height: 100%; pointer-events: none;
+}
+.btn-mini-add {
+ pointer-events: auto; position: absolute;
+ width: 28px; height: 28px; background: var(--leaf-green);
+ color: white; border-radius: 50%; border: none;
+ display: flex; justify-content: center; align-items: center;
+ cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.2); z-index: 20;
+}
+.btn-mini-add:hover { background: var(--forest-green); transform: scale(1.15); }
+.add-child { bottom: -14px; left: 50%; transform: translateX(-50%); }
+.add-partner { top: 50%; right: -14px; transform: translateY(-50%); }
+
+/* --- MODAL --- */
+.modal-overlay {
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
+ background: rgba(0,0,0,0.6); backdrop-filter: blur(2px);
+ display: flex; justify-content: center; align-items: center; z-index: 3000;
+}
+.modal {
+ background: white; width: 90%; max-width: 500px;
+ border-radius: 1rem; padding: 1.5rem;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
+}
+.modal-header {
+ display: flex; justify-content: space-between; align-items: center;
+ border-bottom: 1px solid var(--border-color); padding-bottom: 1rem; margin-bottom: 1rem;
+}
+.toggle-group {
+ display: flex; background: #f3f4f6; padding: 4px; border-radius: 8px; margin-bottom: 1rem;
+}
+.toggle-btn {
+ flex: 1; padding: 8px; border: none; background: transparent;
+ cursor: pointer; border-radius: 6px; font-weight: 600; color: #6b7280;
+}
+.toggle-btn.active {
+ background: white; color: var(--forest-green); box-shadow: 0 1px 2px rgba(0,0,0,0.1);
+}
+.form-group { margin-bottom: 1rem; }
+.form-group label { display: block; font-size: 0.85rem; font-weight: bold; margin-bottom: 0.3rem; }
+.form-row { display: flex; gap: 1rem; }
+.form-row .form-group { flex: 1; }
+input, select, textarea {
+ width: 100%; padding: 0.6rem; border: 1px solid var(--border-color); border-radius: 0.5rem; font-size: 1rem;
+}
+.person-select-list {
+ max-height: 200px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 0.5rem;
+}
+.person-option {
+ padding: 0.75rem; border-bottom: 1px solid #f3f4f6; cursor: pointer; display: flex; justify-content: space-between;
+}
+.person-option.selected { background: #ecfdf5; border-left: 4px solid var(--forest-green); }