some basic statistics and invite tree, plus watch new data
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
---
|
||||
import Base from '../../../layouts/Base.astro';
|
||||
const baseUrl = import.meta.env.BASE_URL ?? '/';
|
||||
const labels = {
|
||||
community: 'Comunitat',
|
||||
members: 'membre',
|
||||
members_pl: 'membres',
|
||||
day: 'dia',
|
||||
days: 'dies',
|
||||
invited_by: 'convidat per',
|
||||
founder: 'fundador',
|
||||
};
|
||||
---
|
||||
<Base title="Sobre el projecte — BincioActivity" public={true}>
|
||||
<div class="max-w-2xl mx-auto">
|
||||
@@ -26,6 +35,12 @@ const baseUrl = import.meta.env.BASE_URL ?? '/';
|
||||
|
||||
<div class="space-y-8 text-sm text-zinc-400 leading-relaxed">
|
||||
|
||||
<section id="stats-section" style="display:none">
|
||||
<h2 class="text-base font-semibold text-white mb-3">Comunitat</h2>
|
||||
<p id="stats-summary" class="text-zinc-500 text-xs mb-4"></p>
|
||||
<div id="stats-tree" class="text-sm"></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-base font-semibold text-white mb-2">Què és això?</h2>
|
||||
<p>
|
||||
@@ -108,7 +123,84 @@ const baseUrl = import.meta.env.BASE_URL ?? '/';
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
|
||||
<script define:vars={{ labels }}>
|
||||
(async () => {
|
||||
let data;
|
||||
try {
|
||||
const r = await fetch('/api/stats');
|
||||
if (!r.ok) return;
|
||||
data = await r.json();
|
||||
} catch { return; }
|
||||
|
||||
if (!data.user_count) return;
|
||||
|
||||
const section = document.getElementById('stats-section');
|
||||
const summary = document.getElementById('stats-summary');
|
||||
const treeEl = document.getElementById('stats-tree');
|
||||
|
||||
const n = data.user_count;
|
||||
summary.textContent = `${n} ${n === 1 ? labels.members : labels.members_pl}`;
|
||||
section.style.display = '';
|
||||
|
||||
const byHandle = {};
|
||||
for (const m of data.members) byHandle[m.handle] = m;
|
||||
const children = {};
|
||||
const roots = [];
|
||||
for (const m of data.members) {
|
||||
if (m.invited_by && byHandle[m.invited_by]) {
|
||||
(children[m.invited_by] ??= []).push(m.handle);
|
||||
} else {
|
||||
roots.push(m.handle);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(days) {
|
||||
if (days < 1) return `< 1 ${labels.day}`;
|
||||
if (days === 1) return `1 ${labels.day}`;
|
||||
if (days < 30) return `${days} ${labels.days}`;
|
||||
const months = Math.floor(days / 30);
|
||||
return months === 1 ? `1 mo` : `${months} mo`;
|
||||
}
|
||||
|
||||
function renderNode(handle, depth) {
|
||||
const m = byHandle[handle];
|
||||
const indent = depth * 20;
|
||||
const isRoot = !m.invited_by;
|
||||
const sub = isRoot ? labels.founder : `${labels.invited_by} @${m.invited_by}`;
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex items-baseline gap-2 py-1.5 border-b border-zinc-800/50';
|
||||
row.style.paddingLeft = `${indent}px`;
|
||||
if (depth > 0) {
|
||||
const connector = document.createElement('span');
|
||||
connector.className = 'text-zinc-700 shrink-0';
|
||||
connector.textContent = '└';
|
||||
row.appendChild(connector);
|
||||
}
|
||||
const name = document.createElement('span');
|
||||
name.className = 'text-white font-medium';
|
||||
name.textContent = m.display_name || `@${handle}`;
|
||||
row.appendChild(name);
|
||||
const handle_el = document.createElement('span');
|
||||
handle_el.className = 'text-zinc-600 text-xs';
|
||||
handle_el.textContent = `@${handle}`;
|
||||
row.appendChild(handle_el);
|
||||
const spacer = document.createElement('span');
|
||||
spacer.className = 'flex-1';
|
||||
row.appendChild(spacer);
|
||||
const meta = document.createElement('span');
|
||||
meta.className = 'text-zinc-600 text-xs text-right shrink-0';
|
||||
meta.innerHTML = `${formatDuration(m.member_for_days)}<br><span class="text-zinc-700">${sub}</span>`;
|
||||
row.appendChild(meta);
|
||||
treeEl.appendChild(row);
|
||||
|
||||
for (const child of (children[handle] ?? [])) renderNode(child, depth + 1);
|
||||
}
|
||||
|
||||
for (const root of roots) renderNode(root, 0);
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
---
|
||||
import Base from '../../../layouts/Base.astro';
|
||||
const baseUrl = import.meta.env.BASE_URL ?? '/';
|
||||
const labels = {
|
||||
community: 'Comunidad',
|
||||
members: 'miembro',
|
||||
members_pl: 'miembros',
|
||||
day: 'día',
|
||||
days: 'días',
|
||||
invited_by: 'invitado por',
|
||||
founder: 'fundador',
|
||||
};
|
||||
---
|
||||
<Base title="Acerca de — BincioActivity" public={true}>
|
||||
<div class="max-w-2xl mx-auto">
|
||||
@@ -26,6 +35,12 @@ const baseUrl = import.meta.env.BASE_URL ?? '/';
|
||||
|
||||
<div class="space-y-8 text-sm text-zinc-400 leading-relaxed">
|
||||
|
||||
<section id="stats-section" style="display:none">
|
||||
<h2 class="text-base font-semibold text-white mb-3">Comunidad</h2>
|
||||
<p id="stats-summary" class="text-zinc-500 text-xs mb-4"></p>
|
||||
<div id="stats-tree" class="text-sm"></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-base font-semibold text-white mb-2">¿Qué es esto?</h2>
|
||||
<p>
|
||||
@@ -107,7 +122,84 @@ const baseUrl = import.meta.env.BASE_URL ?? '/';
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
|
||||
<script define:vars={{ labels }}>
|
||||
(async () => {
|
||||
let data;
|
||||
try {
|
||||
const r = await fetch('/api/stats');
|
||||
if (!r.ok) return;
|
||||
data = await r.json();
|
||||
} catch { return; }
|
||||
|
||||
if (!data.user_count) return;
|
||||
|
||||
const section = document.getElementById('stats-section');
|
||||
const summary = document.getElementById('stats-summary');
|
||||
const treeEl = document.getElementById('stats-tree');
|
||||
|
||||
const n = data.user_count;
|
||||
summary.textContent = `${n} ${n === 1 ? labels.members : labels.members_pl}`;
|
||||
section.style.display = '';
|
||||
|
||||
const byHandle = {};
|
||||
for (const m of data.members) byHandle[m.handle] = m;
|
||||
const children = {};
|
||||
const roots = [];
|
||||
for (const m of data.members) {
|
||||
if (m.invited_by && byHandle[m.invited_by]) {
|
||||
(children[m.invited_by] ??= []).push(m.handle);
|
||||
} else {
|
||||
roots.push(m.handle);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(days) {
|
||||
if (days < 1) return `< 1 ${labels.day}`;
|
||||
if (days === 1) return `1 ${labels.day}`;
|
||||
if (days < 30) return `${days} ${labels.days}`;
|
||||
const months = Math.floor(days / 30);
|
||||
return months === 1 ? `1 mo` : `${months} mo`;
|
||||
}
|
||||
|
||||
function renderNode(handle, depth) {
|
||||
const m = byHandle[handle];
|
||||
const indent = depth * 20;
|
||||
const isRoot = !m.invited_by;
|
||||
const sub = isRoot ? labels.founder : `${labels.invited_by} @${m.invited_by}`;
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex items-baseline gap-2 py-1.5 border-b border-zinc-800/50';
|
||||
row.style.paddingLeft = `${indent}px`;
|
||||
if (depth > 0) {
|
||||
const connector = document.createElement('span');
|
||||
connector.className = 'text-zinc-700 shrink-0';
|
||||
connector.textContent = '└';
|
||||
row.appendChild(connector);
|
||||
}
|
||||
const name = document.createElement('span');
|
||||
name.className = 'text-white font-medium';
|
||||
name.textContent = m.display_name || `@${handle}`;
|
||||
row.appendChild(name);
|
||||
const handle_el = document.createElement('span');
|
||||
handle_el.className = 'text-zinc-600 text-xs';
|
||||
handle_el.textContent = `@${handle}`;
|
||||
row.appendChild(handle_el);
|
||||
const spacer = document.createElement('span');
|
||||
spacer.className = 'flex-1';
|
||||
row.appendChild(spacer);
|
||||
const meta = document.createElement('span');
|
||||
meta.className = 'text-zinc-600 text-xs text-right shrink-0';
|
||||
meta.innerHTML = `${formatDuration(m.member_for_days)}<br><span class="text-zinc-700">${sub}</span>`;
|
||||
row.appendChild(meta);
|
||||
treeEl.appendChild(row);
|
||||
|
||||
for (const child of (children[handle] ?? [])) renderNode(child, depth + 1);
|
||||
}
|
||||
|
||||
for (const root of roots) renderNode(root, 0);
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
const baseUrl = import.meta.env.BASE_URL ?? '/';
|
||||
const labels = {
|
||||
community: 'Community',
|
||||
members: 'member',
|
||||
members_pl: 'members',
|
||||
day: 'day',
|
||||
days: 'days',
|
||||
invited_by: 'invited by',
|
||||
founder: 'founder',
|
||||
loading: 'Loading…',
|
||||
};
|
||||
---
|
||||
<Base title="About — BincioActivity" public={true}>
|
||||
<div class="max-w-2xl mx-auto">
|
||||
@@ -26,6 +36,13 @@ const baseUrl = import.meta.env.BASE_URL ?? '/';
|
||||
|
||||
<div class="space-y-8 text-sm text-zinc-400 leading-relaxed">
|
||||
|
||||
<!-- Community stats (shown only in multi-user mode) -->
|
||||
<section id="stats-section" style="display:none">
|
||||
<h2 class="text-base font-semibold text-white mb-3">Community</h2>
|
||||
<p id="stats-summary" class="text-zinc-500 text-xs mb-4"></p>
|
||||
<div id="stats-tree" class="text-sm"></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-base font-semibold text-white mb-2">What is this?</h2>
|
||||
<p>
|
||||
@@ -103,7 +120,94 @@ const baseUrl = import.meta.env.BASE_URL ?? '/';
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
|
||||
<script define:vars={{ labels }}>
|
||||
(async () => {
|
||||
let data;
|
||||
try {
|
||||
const r = await fetch('/api/stats');
|
||||
if (!r.ok) return; // single-user mode — no stats endpoint
|
||||
data = await r.json();
|
||||
} catch { return; }
|
||||
|
||||
if (!data.user_count) return;
|
||||
|
||||
const section = document.getElementById('stats-section');
|
||||
const summary = document.getElementById('stats-summary');
|
||||
const treeEl = document.getElementById('stats-tree');
|
||||
|
||||
const n = data.user_count;
|
||||
summary.textContent = `${n} ${n === 1 ? labels.members : labels.members_pl}`;
|
||||
section.style.display = '';
|
||||
|
||||
// Build adjacency map: handle → [children]
|
||||
const byHandle = {};
|
||||
for (const m of data.members) byHandle[m.handle] = m;
|
||||
const children = {};
|
||||
const roots = [];
|
||||
for (const m of data.members) {
|
||||
if (m.invited_by && byHandle[m.invited_by]) {
|
||||
(children[m.invited_by] ??= []).push(m.handle);
|
||||
} else {
|
||||
roots.push(m.handle);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(days) {
|
||||
if (days < 1) return `< 1 ${labels.day}`;
|
||||
if (days === 1) return `1 ${labels.day}`;
|
||||
if (days < 30) return `${days} ${labels.days}`;
|
||||
const months = Math.floor(days / 30);
|
||||
return months === 1 ? `1 mo` : `${months} mo`;
|
||||
}
|
||||
|
||||
function renderNode(handle, depth) {
|
||||
const m = byHandle[handle];
|
||||
const indent = depth * 20;
|
||||
const isRoot = !m.invited_by;
|
||||
const sub = isRoot
|
||||
? labels.founder
|
||||
: `${labels.invited_by} @${m.invited_by}`;
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex items-baseline gap-2 py-1.5 border-b border-zinc-800/50';
|
||||
row.style.paddingLeft = `${indent}px`;
|
||||
if (depth > 0) {
|
||||
const connector = document.createElement('span');
|
||||
connector.className = 'text-zinc-700 shrink-0';
|
||||
connector.textContent = '└';
|
||||
row.appendChild(connector);
|
||||
}
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'text-white font-medium';
|
||||
name.textContent = m.display_name || `@${handle}`;
|
||||
row.appendChild(name);
|
||||
|
||||
const handle_el = document.createElement('span');
|
||||
handle_el.className = 'text-zinc-600 text-xs';
|
||||
handle_el.textContent = `@${handle}`;
|
||||
row.appendChild(handle_el);
|
||||
|
||||
const spacer = document.createElement('span');
|
||||
spacer.className = 'flex-1';
|
||||
row.appendChild(spacer);
|
||||
|
||||
const meta = document.createElement('span');
|
||||
meta.className = 'text-zinc-600 text-xs text-right shrink-0';
|
||||
meta.innerHTML = `${formatDuration(m.member_for_days)}<br><span class="text-zinc-700">${sub}</span>`;
|
||||
row.appendChild(meta);
|
||||
|
||||
treeEl.appendChild(row);
|
||||
|
||||
for (const child of (children[handle] ?? [])) {
|
||||
renderNode(child, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of roots) renderNode(root, 0);
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
---
|
||||
import Base from '../../../layouts/Base.astro';
|
||||
const baseUrl = import.meta.env.BASE_URL ?? '/';
|
||||
const labels = {
|
||||
community: 'Comunità',
|
||||
members: 'membro',
|
||||
members_pl: 'membri',
|
||||
day: 'giorno',
|
||||
days: 'giorni',
|
||||
invited_by: 'invitato da',
|
||||
founder: 'fondatore',
|
||||
};
|
||||
---
|
||||
<Base title="Informazioni — BincioActivity" public={true}>
|
||||
<div class="max-w-2xl mx-auto">
|
||||
@@ -26,6 +35,12 @@ const baseUrl = import.meta.env.BASE_URL ?? '/';
|
||||
|
||||
<div class="space-y-8 text-sm text-zinc-400 leading-relaxed">
|
||||
|
||||
<section id="stats-section" style="display:none">
|
||||
<h2 class="text-base font-semibold text-white mb-3">Comunità</h2>
|
||||
<p id="stats-summary" class="text-zinc-500 text-xs mb-4"></p>
|
||||
<div id="stats-tree" class="text-sm"></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-base font-semibold text-white mb-2">Cos'è?</h2>
|
||||
<p>
|
||||
@@ -107,7 +122,84 @@ const baseUrl = import.meta.env.BASE_URL ?? '/';
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Base>
|
||||
|
||||
<script define:vars={{ labels }}>
|
||||
(async () => {
|
||||
let data;
|
||||
try {
|
||||
const r = await fetch('/api/stats');
|
||||
if (!r.ok) return;
|
||||
data = await r.json();
|
||||
} catch { return; }
|
||||
|
||||
if (!data.user_count) return;
|
||||
|
||||
const section = document.getElementById('stats-section');
|
||||
const summary = document.getElementById('stats-summary');
|
||||
const treeEl = document.getElementById('stats-tree');
|
||||
|
||||
const n = data.user_count;
|
||||
summary.textContent = `${n} ${n === 1 ? labels.members : labels.members_pl}`;
|
||||
section.style.display = '';
|
||||
|
||||
const byHandle = {};
|
||||
for (const m of data.members) byHandle[m.handle] = m;
|
||||
const children = {};
|
||||
const roots = [];
|
||||
for (const m of data.members) {
|
||||
if (m.invited_by && byHandle[m.invited_by]) {
|
||||
(children[m.invited_by] ??= []).push(m.handle);
|
||||
} else {
|
||||
roots.push(m.handle);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(days) {
|
||||
if (days < 1) return `< 1 ${labels.day}`;
|
||||
if (days === 1) return `1 ${labels.day}`;
|
||||
if (days < 30) return `${days} ${labels.days}`;
|
||||
const months = Math.floor(days / 30);
|
||||
return months === 1 ? `1 mo` : `${months} mo`;
|
||||
}
|
||||
|
||||
function renderNode(handle, depth) {
|
||||
const m = byHandle[handle];
|
||||
const indent = depth * 20;
|
||||
const isRoot = !m.invited_by;
|
||||
const sub = isRoot ? labels.founder : `${labels.invited_by} @${m.invited_by}`;
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex items-baseline gap-2 py-1.5 border-b border-zinc-800/50';
|
||||
row.style.paddingLeft = `${indent}px`;
|
||||
if (depth > 0) {
|
||||
const connector = document.createElement('span');
|
||||
connector.className = 'text-zinc-700 shrink-0';
|
||||
connector.textContent = '└';
|
||||
row.appendChild(connector);
|
||||
}
|
||||
const name = document.createElement('span');
|
||||
name.className = 'text-white font-medium';
|
||||
name.textContent = m.display_name || `@${handle}`;
|
||||
row.appendChild(name);
|
||||
const handle_el = document.createElement('span');
|
||||
handle_el.className = 'text-zinc-600 text-xs';
|
||||
handle_el.textContent = `@${handle}`;
|
||||
row.appendChild(handle_el);
|
||||
const spacer = document.createElement('span');
|
||||
spacer.className = 'flex-1';
|
||||
row.appendChild(spacer);
|
||||
const meta = document.createElement('span');
|
||||
meta.className = 'text-zinc-600 text-xs text-right shrink-0';
|
||||
meta.innerHTML = `${formatDuration(m.member_for_days)}<br><span class="text-zinc-700">${sub}</span>`;
|
||||
row.appendChild(meta);
|
||||
treeEl.appendChild(row);
|
||||
|
||||
for (const child of (children[handle] ?? [])) renderNode(child, depth + 1);
|
||||
}
|
||||
|
||||
for (const root of roots) renderNode(root, 0);
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user