Merge branch 'main' of https://github.com/FunnyEivske/Roesstad
This commit is contained in:
commit
df79c7ca04
3 changed files with 177 additions and 155 deletions
|
|
@ -97,67 +97,184 @@ function setupTabs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- TREE RENDERER ---
|
// --- TREE RENDERER ---
|
||||||
|
// --- TREE RENDERER (GLOBAL) ---
|
||||||
function renderTree() {
|
function renderTree() {
|
||||||
const parentsRow = document.getElementById('row-parents');
|
const grid = document.getElementById('tree-grid');
|
||||||
const focusRow = document.getElementById('row-focus');
|
const svg = document.getElementById('tree-svg');
|
||||||
const childrenRow = document.getElementById('row-children');
|
if (!grid || !svg) return;
|
||||||
|
|
||||||
if (!parentsRow || !focusRow || !childrenRow) return;
|
grid.innerHTML = '';
|
||||||
|
svg.innerHTML = ''; // Clear lines
|
||||||
parentsRow.innerHTML = '';
|
|
||||||
focusRow.innerHTML = '';
|
|
||||||
childrenRow.innerHTML = '';
|
|
||||||
|
|
||||||
if (state.people.length === 0) {
|
if (state.people.length === 0) {
|
||||||
focusRow.innerHTML = `<div style="text-align:center; color:var(--color-text-muted);">
|
grid.innerHTML = `<div style="text-align:center; color:var(--color-text-muted); margin-top:4rem;">
|
||||||
<p>Ingen personer i treet ennå.</p>
|
<p>Ingen personer i treet ennå.</p>
|
||||||
<p>Klikk på <i class="ph-bold ph-plus-circle" style="color:var(--color-primary);"></i> nede til høyre for å starte!</p>
|
<p>Klikk på <i class="ph-bold ph-plus-circle" style="color:var(--color-primary);"></i> nede til høyre for å starte!</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusPerson = state.people.find(p => p.id === state.focusId);
|
const visited = new Set();
|
||||||
if (!focusPerson) return;
|
const roots = findRoots();
|
||||||
|
|
||||||
// 1. Parents
|
roots.forEach(root => {
|
||||||
const father = state.people.find(p => p.id === focusPerson.fatherId);
|
if (!visited.has(root.id)) {
|
||||||
const mother = state.people.find(p => p.id === focusPerson.motherId);
|
const treeNode = buildTreeNode(root, visited);
|
||||||
parentsRow.appendChild(createSlot(father, 'Far', 'add-parent-m'));
|
grid.appendChild(treeNode);
|
||||||
parentsRow.appendChild(createSlot(mother, 'Mor', 'add-parent-f'));
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 2. Partner (Left of Focus)
|
// Draw lines after layout is computed
|
||||||
const partner = state.people.find(p => p.id === focusPerson.partnerId);
|
setTimeout(drawConnections, 100);
|
||||||
if (partner) {
|
window.addEventListener('resize', drawConnections);
|
||||||
const pCard = createCard(partner);
|
}
|
||||||
pCard.onclick = () => setFocus(partner.id);
|
|
||||||
focusRow.appendChild(pCard);
|
|
||||||
|
|
||||||
// Heart Icon
|
function findRoots() {
|
||||||
const heart = document.createElement('div');
|
// A root is someone whose parents are NOT in the current list
|
||||||
heart.innerHTML = '<i class="ph-fill ph-heart" style="color:var(--color-female); font-size:1.5rem;"></i>';
|
return state.people.filter(p => {
|
||||||
focusRow.appendChild(heart);
|
const father = state.people.find(parent => parent.id === p.fatherId);
|
||||||
|
const mother = state.people.find(parent => parent.id === p.motherId);
|
||||||
|
return !father && !mother;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTreeNode(person, visited) {
|
||||||
|
visited.add(person.id);
|
||||||
|
|
||||||
|
// Container
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'node-container';
|
||||||
|
|
||||||
|
// 1. Couple Wrapper
|
||||||
|
const coupleWrapper = document.createElement('div');
|
||||||
|
coupleWrapper.className = 'couple-wrapper';
|
||||||
|
|
||||||
|
// Person Card
|
||||||
|
const pCard = createCard(person);
|
||||||
|
pCard.onclick = () => setFocus(person.id); // Keep focus logic for editing
|
||||||
|
coupleWrapper.appendChild(pCard);
|
||||||
|
|
||||||
|
// Partner Card
|
||||||
|
if (person.partnerId) {
|
||||||
|
const partner = state.people.find(p => p.id === person.partnerId);
|
||||||
|
if (partner && !visited.has(partner.id)) {
|
||||||
|
visited.add(partner.id);
|
||||||
|
const partCard = createCard(partner);
|
||||||
|
partCard.onclick = () => setFocus(partner.id);
|
||||||
|
coupleWrapper.appendChild(partCard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.appendChild(coupleWrapper);
|
||||||
|
|
||||||
|
// 2. Children
|
||||||
|
// Find children where this person (or partner) is a parent
|
||||||
|
const children = state.people.filter(p =>
|
||||||
|
p.fatherId === person.id || p.motherId === person.id ||
|
||||||
|
(person.partnerId && (p.fatherId === person.partnerId || p.motherId === person.partnerId))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (children.length > 0) {
|
||||||
|
const childrenContainer = document.createElement('div');
|
||||||
|
childrenContainer.className = 'children-container';
|
||||||
|
|
||||||
|
// Sort children by birth date
|
||||||
|
children.sort((a, b) => (a.birthDate || '9999') > (b.birthDate || '9999') ? 1 : -1);
|
||||||
|
|
||||||
|
children.forEach(child => {
|
||||||
|
if (!visited.has(child.id)) {
|
||||||
|
const childNode = buildTreeNode(child, visited);
|
||||||
|
childrenContainer.appendChild(childNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
container.appendChild(childrenContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Focus Person
|
return container;
|
||||||
focusRow.appendChild(createCard(focusPerson, true));
|
}
|
||||||
|
function drawConnections() {
|
||||||
|
const svg = document.getElementById('tree-svg');
|
||||||
|
const grid = document.getElementById('tree-grid');
|
||||||
|
if (!svg || !grid) return;
|
||||||
|
|
||||||
// 4. Children
|
svg.innerHTML = '';
|
||||||
const children = state.people.filter(p => p.fatherId === state.focusId || p.motherId === state.focusId);
|
const svgRect = svg.getBoundingClientRect();
|
||||||
children.forEach(child => {
|
|
||||||
const cCard = createCard(child);
|
// Map all visible cards by ID
|
||||||
cCard.onclick = () => setFocus(child.id);
|
const cardMap = new Map();
|
||||||
childrenRow.appendChild(cCard);
|
document.querySelectorAll('.card').forEach(card => {
|
||||||
|
if (card.dataset.id) cardMap.set(card.dataset.id, card);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const drawnPartners = new Set();
|
||||||
|
|
||||||
|
// Helper for coordinates
|
||||||
|
const getCenter = (el) => {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: rect.left + rect.width / 2 - svgRect.left,
|
||||||
|
y: rect.top + rect.height / 2 - svgRect.top,
|
||||||
|
bottom: rect.bottom - svgRect.top,
|
||||||
|
top: rect.top - svgRect.top
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
state.people.forEach(p => {
|
||||||
|
const pCard = cardMap.get(p.id);
|
||||||
|
if (!pCard) return;
|
||||||
|
|
||||||
|
const pCenter = getCenter(pCard);
|
||||||
|
|
||||||
|
// 1. Parent Connections
|
||||||
|
[p.fatherId, p.motherId].forEach(parentId => {
|
||||||
|
if (parentId) {
|
||||||
|
const parentCard = cardMap.get(parentId);
|
||||||
|
if (parentCard) {
|
||||||
|
const parCenter = getCenter(parentCard);
|
||||||
|
// Draw Curved Line
|
||||||
|
// From Parent Bottom to Child Top
|
||||||
|
const path = `M ${parCenter.x} ${parCenter.bottom}
|
||||||
|
C ${parCenter.x} ${parCenter.bottom + 50},
|
||||||
|
${pCenter.x} ${pCenter.top - 50},
|
||||||
|
${pCenter.x} ${pCenter.top}`;
|
||||||
|
createPath(svg, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Partner Connection
|
||||||
|
if (p.partnerId) {
|
||||||
|
const partnerCard = cardMap.get(p.partnerId);
|
||||||
|
if (partnerCard) {
|
||||||
|
// Avoid drawing twice
|
||||||
|
const pairId = [p.id, p.partnerId].sort().join('-');
|
||||||
|
if (!drawnPartners.has(pairId)) {
|
||||||
|
drawnPartners.add(pairId);
|
||||||
|
const partCenter = getCenter(partnerCard);
|
||||||
|
createPath(svg, `M ${pCenter.x} ${pCenter.y} L ${partCenter.x} ${partCenter.y}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPath(svg, d) {
|
||||||
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||||
|
path.setAttribute("d", d);
|
||||||
|
path.setAttribute("class", "connector");
|
||||||
|
svg.appendChild(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFocus(id) {
|
function setFocus(id) {
|
||||||
state.focusId = id;
|
state.focusId = id;
|
||||||
|
document.querySelectorAll('.card').forEach(c => c.classList.remove('focus'));
|
||||||
renderTree();
|
renderTree();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCard(person, isFocus = false) {
|
function createCard(person) {
|
||||||
|
const isFocus = (person.id === state.focusId);
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = `card ${isFocus ? 'focus' : ''} ${person.gender === 'F' ? 'female' : 'male'}`;
|
el.className = `card ${isFocus ? 'focus' : ''} ${person.gender === 'F' ? 'female' : 'male'}`;
|
||||||
|
el.dataset.id = person.id; // Crucial for connections
|
||||||
|
|
||||||
const year = person.birthDate ? new Date(person.birthDate).getFullYear() : '';
|
const year = person.birthDate ? new Date(person.birthDate).getFullYear() : '';
|
||||||
const dead = person.deathDate ? '✝ ' : '';
|
const dead = person.deathDate ? '✝ ' : '';
|
||||||
|
|
@ -170,6 +287,7 @@ function createCard(person, isFocus = false) {
|
||||||
${loc ? `<div class="card-location"><i class="ph-bold ph-map-pin"></i> ${loc}</div>` : ''}
|
${loc ? `<div class="card-location"><i class="ph-bold ph-map-pin"></i> ${loc}</div>` : ''}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Always show actions if focused
|
||||||
if (isFocus) {
|
if (isFocus) {
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'mini-actions';
|
actions.className = 'mini-actions';
|
||||||
|
|
@ -197,24 +315,11 @@ function createCard(person, isFocus = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSlot(person, label, action) {
|
function createSlot(person, label, action) {
|
||||||
const wrapper = document.createElement('div');
|
// Deprecated in global view, but kept if needed for other views
|
||||||
wrapper.className = 'slot';
|
return document.createElement('div');
|
||||||
wrapper.innerHTML = `<span class="slot-label">${label}</span>`;
|
|
||||||
|
|
||||||
if (person) {
|
|
||||||
const card = createCard(person);
|
|
||||||
card.onclick = () => setFocus(person.id);
|
|
||||||
wrapper.appendChild(card);
|
|
||||||
} else {
|
|
||||||
const btn = document.createElement('div');
|
|
||||||
btn.className = 'btn-slot';
|
|
||||||
btn.innerHTML = '<i class="ph-bold ph-plus"></i>';
|
|
||||||
btn.onclick = () => window.openModal(action);
|
|
||||||
wrapper.appendChild(btn);
|
|
||||||
}
|
|
||||||
return wrapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- MAP RENDERER ---
|
// --- MAP RENDERER ---
|
||||||
function renderMap() {
|
function renderMap() {
|
||||||
if (!state.map) {
|
if (!state.map) {
|
||||||
18
app.html
18
app.html
|
|
@ -49,24 +49,6 @@
|
||||||
<button id="btn-logout" class="btn-logout">Logg ut</button>
|
<button id="btn-logout" class="btn-logout">Logg ut</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- MAIN CONTENT -->
|
|
||||||
<main style="flex:1; position:relative; overflow:hidden;">
|
|
||||||
|
|
||||||
<!-- VIEW: TREE -->
|
|
||||||
<div id="view-tree" class="view-section">
|
|
||||||
<div id="tree-container">
|
|
||||||
<div id="tree-grid" class="tree-grid">
|
|
||||||
<div id="row-parents" class="tree-row"></div>
|
|
||||||
<div id="row-focus" class="tree-row"></div>
|
|
||||||
<div id="row-children" class="tree-row children"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- VIEW: MAP -->
|
|
||||||
<div id="view-map" class="view-section hidden">
|
|
||||||
<div id="map-container"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VIEW: TIMELINE -->
|
<!-- VIEW: TIMELINE -->
|
||||||
|
|
|
||||||
107
style.css
107
style.css
|
|
@ -1,5 +1,6 @@
|
||||||
/* --- RØSSTAD DESIGN SYSTEM --- */
|
/* --- RØSSTAD DESIGN SYSTEM --- */
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap');
|
||||||
|
@import url('tree.css');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* --- PALETTE: Røsstad Green --- */
|
/* --- PALETTE: Røsstad Green --- */
|
||||||
|
|
@ -26,70 +27,6 @@
|
||||||
--color-text-muted: #64748b;
|
--color-text-muted: #64748b;
|
||||||
/* Slate 500 */
|
/* Slate 500 */
|
||||||
|
|
||||||
--color-male: #0ea5e9;
|
|
||||||
/* Sky Blue */
|
|
||||||
--color-female: #be123c;
|
|
||||||
/* Rose */
|
|
||||||
|
|
||||||
/* --- SHADOWS & EFFECTS --- */
|
|
||||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
||||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
||||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
|
||||||
--blur-glass: blur(12px);
|
|
||||||
|
|
||||||
/* --- SPACING & RADIUS --- */
|
|
||||||
--radius-sm: 0.5rem;
|
|
||||||
--radius-md: 0.75rem;
|
|
||||||
--radius-lg: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- GLOBAL RESET --- */
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Outfit', sans-serif;
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
color: var(--color-text-main);
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
/* Subtle background pattern */
|
|
||||||
background-image:
|
|
||||||
radial-gradient(at 0% 0%, rgba(15, 47, 36, 0.05) 0px, transparent 50%),
|
|
||||||
radial-gradient(at 100% 100%, rgba(217, 119, 6, 0.05) 0px, transparent 50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- UTILITIES --- */
|
|
||||||
.hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full-width {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-center {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- LOGIN PAGE --- */
|
|
||||||
body.login-page {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: linear-gradient(135deg, var(--color-primary-dark) 0%, var(--color-primary) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
backdrop-filter: var(--blur-glass);
|
|
||||||
padding: 3rem;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
|
|
@ -448,21 +385,6 @@ body.login-page {
|
||||||
|
|
||||||
.btn-slot:hover {
|
.btn-slot:hover {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
color: var(--color-primary);
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Map & Timeline */
|
|
||||||
#map-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-wrapper {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 3rem 1rem;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -556,6 +478,7 @@ body.login-page {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
|
@ -567,17 +490,29 @@ body.login-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- FAB (Floating Action Button) --- */
|
/* --- FAB (Floating Action Button) --- */
|
||||||
.fab-btn {
|
.fab-btn {
|
||||||
position: fixed; bottom: 2rem; right: 2rem;
|
position: fixed;
|
||||||
width: 64px; height: 64px; border-radius: 50%;
|
bottom: 2rem;
|
||||||
background: var(--color-primary); color: white;
|
right: 2rem;
|
||||||
border: none; box-shadow: var(--shadow-lg);
|
width: 64px;
|
||||||
display: flex; justify-content: center; align-items: center;
|
height: 64px;
|
||||||
font-size: 2rem; cursor: pointer; z-index: 500;
|
border-radius: 50%;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 500;
|
||||||
transition: transform 0.2s, background 0.2s;
|
transition: transform 0.2s, background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fab-btn:hover {
|
.fab-btn:hover {
|
||||||
transform: scale(1.1) rotate(90deg);
|
transform: scale(1.1) rotate(90deg);
|
||||||
background: var(--color-primary-dark);
|
background: var(--color-primary-dark);
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue