264 lines
12 KiB
HTML
264 lines
12 KiB
HTML
|
|
{% extends "base.html" %}
|
||
|
|
{% import "macros/ui.html" as m %}
|
||
|
|
|
||
|
|
{% block title %}Personal — {{ slug }} — Ontoref{% endblock title %}
|
||
|
|
{% block nav_personal %}active{% endblock nav_personal %}
|
||
|
|
{% block nav_group_domain %}active{% endblock nav_group_domain %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<div class="mb-6 flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<h1 class="text-2xl font-bold">Personal Ontology</h1>
|
||
|
|
<p class="text-base-content/50 text-sm mt-1">Content pipeline · Opportunities · CFP pipeline</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Tabs -->
|
||
|
|
<div role="tablist" class="tabs tabs-bordered mb-4">
|
||
|
|
<a role="tab" class="tab tab-active" onclick="switchTab('content', this)">Content</a>
|
||
|
|
<a role="tab" class="tab" onclick="switchTab('opportunities', this)">Opportunities</a>
|
||
|
|
<a role="tab" class="tab" onclick="switchTab('cfp', this)">CFP Pipeline</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Content tab -->
|
||
|
|
<div id="tab-content">
|
||
|
|
<div class="flex flex-wrap gap-2 mb-3">
|
||
|
|
<input id="content-filter" type="text" placeholder="Filter by title, kind…"
|
||
|
|
class="input input-sm input-bordered flex-1 min-w-48 font-mono" oninput="renderContent()">
|
||
|
|
<select id="content-status" class="select select-sm select-bordered" onchange="renderContent()">
|
||
|
|
<option value="">All statuses</option>
|
||
|
|
<option value="Idea">Idea</option>
|
||
|
|
<option value="Brief">Brief</option>
|
||
|
|
<option value="Draft">Draft</option>
|
||
|
|
<option value="Review">Review</option>
|
||
|
|
<option value="Published">Published</option>
|
||
|
|
<option value="Archived">Archived</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="overflow-x-auto">
|
||
|
|
<table class="table table-sm w-full bg-base-200 rounded-lg">
|
||
|
|
<thead>
|
||
|
|
<tr class="text-base-content/50 text-xs uppercase tracking-wider">
|
||
|
|
<th>ID</th><th>Title</th><th>Kind</th><th>Status</th><th>Audience</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="content-body"></tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Opportunities tab -->
|
||
|
|
<div id="tab-opportunities" class="hidden">
|
||
|
|
<div class="flex flex-wrap gap-2 mb-3">
|
||
|
|
<input id="opp-filter" type="text" placeholder="Filter by name, kind…"
|
||
|
|
class="input input-sm input-bordered flex-1 min-w-48 font-mono" oninput="renderOpportunities()">
|
||
|
|
<select id="opp-status" class="select select-sm select-bordered" onchange="renderOpportunities()">
|
||
|
|
<option value="">All statuses</option>
|
||
|
|
<option value="Watching">Watching</option>
|
||
|
|
<option value="Evaluating">Evaluating</option>
|
||
|
|
<option value="Active">Active</option>
|
||
|
|
<option value="Submitted">Submitted</option>
|
||
|
|
<option value="Closed">Closed</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="overflow-x-auto">
|
||
|
|
<table class="table table-sm w-full bg-base-200 rounded-lg">
|
||
|
|
<thead>
|
||
|
|
<tr class="text-base-content/50 text-xs uppercase tracking-wider">
|
||
|
|
<th>ID</th><th>Name</th><th>Kind</th><th>Status</th><th>Deadline</th><th>Links</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="opp-body"></tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- CFP tab -->
|
||
|
|
<div id="tab-cfp" class="hidden">
|
||
|
|
<div class="flex flex-wrap gap-2 mb-3">
|
||
|
|
<input id="cfp-filter" type="text" placeholder="Filter by name, note…"
|
||
|
|
class="input input-sm input-bordered flex-1 min-w-48 font-mono" oninput="renderCfp()">
|
||
|
|
<select id="cfp-stage" class="select select-sm select-bordered" onchange="renderCfp()">
|
||
|
|
<option value="">All stages</option>
|
||
|
|
<option value="Watching">Watching</option>
|
||
|
|
<option value="Evaluating">Evaluating</option>
|
||
|
|
<option value="Drafting">Drafting</option>
|
||
|
|
<option value="Submitted">Submitted</option>
|
||
|
|
<option value="Accepted">Accepted</option>
|
||
|
|
<option value="Declined">Declined</option>
|
||
|
|
<option value="Delivered">Delivered</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div id="cfp-cards" class="grid grid-cols-1 lg:grid-cols-2 gap-4"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Detail modal -->
|
||
|
|
<dialog id="detail-modal" class="modal">
|
||
|
|
<div class="modal-box w-full max-w-2xl">
|
||
|
|
<form method="dialog">
|
||
|
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-3 top-3">✕</button>
|
||
|
|
</form>
|
||
|
|
<div id="modal-content" class="text-sm space-y-3"></div>
|
||
|
|
</div>
|
||
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
||
|
|
</dialog>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
const CONTENTS = {{ contents_json | safe }};
|
||
|
|
const OPPORTUNITIES = {{ opportunities_json | safe }};
|
||
|
|
const CFP = {{ cfp_json | safe }};
|
||
|
|
|
||
|
|
const STATUS_BADGE = {
|
||
|
|
Idea: 'badge-ghost', Brief: 'badge-info', Draft: 'badge-warning',
|
||
|
|
Review: 'badge-warning', Published: 'badge-success', Rejected: 'badge-error',
|
||
|
|
Archived: 'badge-ghost',
|
||
|
|
Watching: 'badge-ghost', Evaluating: 'badge-info', Active: 'badge-warning',
|
||
|
|
Submitted: 'badge-info', Closed: 'badge-ghost',
|
||
|
|
Drafting: 'badge-warning', Accepted: 'badge-success', Declined: 'badge-error',
|
||
|
|
Delivered: 'badge-success',
|
||
|
|
};
|
||
|
|
|
||
|
|
function badge(s) {
|
||
|
|
const cls = STATUS_BADGE[s] ?? 'badge-ghost';
|
||
|
|
return `<span class="badge badge-xs font-mono ${cls}">${s}</span>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function linkChip(lk) {
|
||
|
|
return `<a href="${lk.url}" target="_blank" class="badge badge-xs badge-outline font-mono">${lk.kind}${lk.label ? ': ' + lk.label : ''}</a>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Content ──────────────────────────────────────────────────────────────────
|
||
|
|
let visibleContents = CONTENTS;
|
||
|
|
|
||
|
|
function renderContent() {
|
||
|
|
const text = document.getElementById('content-filter').value.toLowerCase();
|
||
|
|
const status = document.getElementById('content-status').value;
|
||
|
|
visibleContents = CONTENTS.filter(c => {
|
||
|
|
const tm = !text || c.id.toLowerCase().includes(text) || (c.title||'').toLowerCase().includes(text) || (c.kind||'').toLowerCase().includes(text);
|
||
|
|
const sm = !status || c.status === status;
|
||
|
|
return tm && sm;
|
||
|
|
});
|
||
|
|
const tbody = document.getElementById('content-body');
|
||
|
|
tbody.innerHTML = visibleContents.length === 0
|
||
|
|
? '<tr><td colspan="5" class="text-center text-base-content/30 py-6">No content items</td></tr>'
|
||
|
|
: visibleContents.map((c, i) => `
|
||
|
|
<tr class="hover cursor-pointer" onclick="showContentDetail(${i})">
|
||
|
|
<td class="font-mono text-xs text-primary">${c.id}</td>
|
||
|
|
<td class="text-sm">${c.title || '<span class="text-base-content/30">—</span>'}</td>
|
||
|
|
<td class="font-mono text-xs">${c.kind}</td>
|
||
|
|
<td>${badge(c.status)}</td>
|
||
|
|
<td class="text-xs text-base-content/60">${c.audience || ''}</td>
|
||
|
|
</tr>`).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function showContentDetail(i) {
|
||
|
|
const c = visibleContents[i];
|
||
|
|
if (!c) return;
|
||
|
|
const links = (c.links||[]).map(linkChip).join(' ');
|
||
|
|
document.getElementById('modal-content').innerHTML = `
|
||
|
|
<div class="flex items-baseline gap-2 flex-wrap">
|
||
|
|
<span class="font-mono font-bold text-primary">${c.id}</span>
|
||
|
|
${badge(c.status)} <span class="font-mono text-xs text-base-content/40">${c.kind}</span>
|
||
|
|
</div>
|
||
|
|
<h2 class="text-lg font-bold">${c.title || '—'}</h2>
|
||
|
|
<div class="flex gap-2 flex-wrap text-xs text-base-content/50">
|
||
|
|
${c.audience ? `<span>Audience: ${c.audience}</span>` : ''}
|
||
|
|
${(c.platforms||[]).length ? `<span>Platforms: ${c.platforms.join(', ')}</span>` : ''}
|
||
|
|
</div>
|
||
|
|
${links ? `<div class="flex flex-wrap gap-1">${links}</div>` : ''}
|
||
|
|
${c.note ? `<p class="text-base-content/70 text-sm leading-relaxed">${c.note}</p>` : ''}
|
||
|
|
${(c.linked_nodes||[]).length ? `<p class="text-xs text-base-content/40 font-mono">nodes: ${c.linked_nodes.join(', ')}</p>` : ''}
|
||
|
|
`;
|
||
|
|
document.getElementById('detail-modal').showModal();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Opportunities ─────────────────────────────────────────────────────────────
|
||
|
|
let visibleOpps = OPPORTUNITIES;
|
||
|
|
|
||
|
|
function renderOpportunities() {
|
||
|
|
const text = document.getElementById('opp-filter').value.toLowerCase();
|
||
|
|
const status = document.getElementById('opp-status').value;
|
||
|
|
visibleOpps = OPPORTUNITIES.filter(o => {
|
||
|
|
const tm = !text || o.id.toLowerCase().includes(text) || (o.name||'').toLowerCase().includes(text) || (o.kind||'').toLowerCase().includes(text);
|
||
|
|
const sm = !status || o.status === status;
|
||
|
|
return tm && sm;
|
||
|
|
});
|
||
|
|
const tbody = document.getElementById('opp-body');
|
||
|
|
tbody.innerHTML = visibleOpps.length === 0
|
||
|
|
? '<tr><td colspan="6" class="text-center text-base-content/30 py-6">No opportunities</td></tr>'
|
||
|
|
: visibleOpps.map((o, i) => `
|
||
|
|
<tr class="hover cursor-pointer" onclick="showOppDetail(${i})">
|
||
|
|
<td class="font-mono text-xs text-primary">${o.id}</td>
|
||
|
|
<td class="text-sm">${o.name}</td>
|
||
|
|
<td class="font-mono text-xs">${o.kind}</td>
|
||
|
|
<td>${badge(o.status)}</td>
|
||
|
|
<td class="font-mono text-xs text-base-content/50">${o.deadline || '—'}</td>
|
||
|
|
<td class="text-xs">${(o.links||[]).length ? `<span class="badge badge-xs badge-outline">${(o.links||[]).length}</span>` : '—'}</td>
|
||
|
|
</tr>`).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function showOppDetail(i) {
|
||
|
|
const o = visibleOpps[i];
|
||
|
|
if (!o) return;
|
||
|
|
const links = (o.links||[]).map(linkChip).join(' ');
|
||
|
|
document.getElementById('modal-content').innerHTML = `
|
||
|
|
<div class="flex items-baseline gap-2 flex-wrap">
|
||
|
|
<span class="font-mono font-bold text-primary">${o.id}</span>
|
||
|
|
${badge(o.status)} <span class="font-mono text-xs text-base-content/40">${o.kind}</span>
|
||
|
|
</div>
|
||
|
|
<h2 class="text-lg font-bold">${o.name}</h2>
|
||
|
|
${o.deadline ? `<p class="text-xs text-base-content/50 font-mono">Deadline: ${o.deadline}</p>` : ''}
|
||
|
|
${links ? `<div class="flex flex-wrap gap-1">${links}</div>` : ''}
|
||
|
|
${(o.fit_signals||[]).length ? `<p class="text-xs text-base-content/50">Fit: ${o.fit_signals.join(' · ')}</p>` : ''}
|
||
|
|
${o.note ? `<p class="text-base-content/70 text-sm leading-relaxed">${o.note}</p>` : ''}
|
||
|
|
${(o.linked_nodes||[]).length ? `<p class="text-xs text-base-content/40 font-mono">nodes: ${o.linked_nodes.join(', ')}</p>` : ''}
|
||
|
|
`;
|
||
|
|
document.getElementById('detail-modal').showModal();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── CFP ───────────────────────────────────────────────────────────────────────
|
||
|
|
function renderCfp() {
|
||
|
|
const text = document.getElementById('cfp-filter').value.toLowerCase();
|
||
|
|
const stage = document.getElementById('cfp-stage').value;
|
||
|
|
const items = CFP.filter(c => {
|
||
|
|
const tm = !text || c.id.toLowerCase().includes(text) || (c.name||'').toLowerCase().includes(text) || (c.note||'').toLowerCase().includes(text);
|
||
|
|
const sm = !stage || c.stage === stage;
|
||
|
|
return tm && sm;
|
||
|
|
});
|
||
|
|
const container = document.getElementById('cfp-cards');
|
||
|
|
container.innerHTML = items.length === 0
|
||
|
|
? '<p class="text-base-content/30 text-sm py-6 col-span-2">No CFP items</p>'
|
||
|
|
: items.map(c => {
|
||
|
|
const links = (c.links||[]).map(linkChip).join(' ');
|
||
|
|
return `
|
||
|
|
<div class="bg-base-200 rounded-lg p-4 space-y-2">
|
||
|
|
<div class="flex items-start justify-between gap-2">
|
||
|
|
<div>
|
||
|
|
<p class="font-semibold text-sm">${c.name}</p>
|
||
|
|
<p class="font-mono text-xs text-base-content/40">${c.id}</p>
|
||
|
|
</div>
|
||
|
|
${badge(c.stage)}
|
||
|
|
</div>
|
||
|
|
${c.deadline ? `<p class="text-xs text-base-content/50 font-mono">Deadline: ${c.deadline}</p>` : ''}
|
||
|
|
${links ? `<div class="flex flex-wrap gap-1">${links}</div>` : ''}
|
||
|
|
${c.next_action ? `<div class="border-t border-base-content/10 pt-2"><p class="text-xs text-base-content/50 uppercase tracking-wider mb-1">Next action</p><p class="text-xs text-base-content/70">${c.next_action}</p></div>` : ''}
|
||
|
|
${c.note ? `<p class="text-xs text-base-content/50 italic">${c.note}</p>` : ''}
|
||
|
|
</div>`;
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Tabs ──────────────────────────────────────────────────────────────────────
|
||
|
|
function switchTab(name, el) {
|
||
|
|
['content','opportunities','cfp'].forEach(t => {
|
||
|
|
document.getElementById('tab-' + t).classList.toggle('hidden', t !== name);
|
||
|
|
});
|
||
|
|
document.querySelectorAll('[role="tab"]').forEach(t => t.classList.remove('tab-active'));
|
||
|
|
el.classList.add('tab-active');
|
||
|
|
}
|
||
|
|
|
||
|
|
renderContent();
|
||
|
|
renderOpportunities();
|
||
|
|
renderCfp();
|
||
|
|
</script>
|
||
|
|
{% endblock content %}
|