This repository has no description
1export const LANDING_PAGE = `<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <title>Nanopub — AT Protocol Nanopublication Index</title>
7 <style>
8 :root { --bg: #0a0a0a; --surface: #141414; --border: #222; --text: #e0e0e0; --dim: #888; --accent: #6d9bc0; --accent2: #8fb8d4; --green: #6dc08b; }
9 * { margin: 0; padding: 0; box-sizing: border-box; }
10 body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; }
11 a { color: var(--accent); text-decoration: none; }
12 a:hover { color: var(--accent2); text-decoration: underline; }
13 .container { max-width: 960px; margin: 0 auto; padding: 2rem 1.5rem; }
14 header { margin-bottom: 2rem; }
15 header h1 { font-size: 1.8rem; font-weight: 600; margin-bottom: 0.3rem; }
16 header p { color: var(--dim); font-size: 0.95rem; }
17 .stats { display: flex; gap: 1.5rem; margin-bottom: 2rem; flex-wrap: wrap; }
18 .stat { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 1rem 1.5rem; min-width: 120px; }
19 .stat .num { font-size: 1.6rem; font-weight: 700; color: var(--accent); }
20 .stat .label { font-size: 0.8rem; color: var(--dim); text-transform: uppercase; letter-spacing: 0.05em; }
21 .nanopub-list { list-style: none; }
22 .nanopub-item { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 0.75rem; transition: border-color 0.15s; }
23 .nanopub-item:hover { border-color: var(--accent); }
24 .nanopub-label { font-weight: 500; margin-bottom: 0.25rem; }
25 .nanopub-meta { font-size: 0.8rem; color: var(--dim); display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; }
26 .nanopub-type { display: inline-block; background: var(--accent); color: var(--bg); font-size: 0.7rem; font-weight: 600; padding: 0.15rem 0.5rem; border-radius: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
27 .nanopub-type.retraction { background: #c06d6d; }
28 .nanopub-type.new-version { background: #6dc08b; }
29 .island-link { display: inline-block; background: var(--green); color: var(--bg); font-size: 0.7rem; font-weight: 600; padding: 0.15rem 0.5rem; border-radius: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
30 .island-link:hover { text-decoration: none; opacity: 0.85; }
31 .empty { color: var(--dim); font-style: italic; padding: 2rem; text-align: center; }
32 .loading { color: var(--dim); padding: 2rem; text-align: center; }
33 footer { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border); color: var(--dim); font-size: 0.8rem; }
34 </style>
35</head>
36<body>
37 <div class="container">
38 <header>
39 <h1>Nanopub</h1>
40 <p>AT Protocol nanopublication index — the smallest unit of publishable scientific knowledge</p>
41 </header>
42 <div class="stats" id="stats"></div>
43 <ul class="nanopub-list" id="list"><li class="loading">Loading...</li></ul>
44 <footer>
45 <p>Nanopub appview powered by <a href="https://github.com/nandithebull">Contrail</a> on AT Protocol</p>
46 </footer>
47 </div>
48 <script>
49 (async function() {
50 const statsEl = document.getElementById('stats');
51 const listEl = document.getElementById('list');
52
53 try {
54 const res = await fetch('/api/nanopubs?limit=100');
55 const { nanopubs } = await res.json();
56
57 // Stats
58 const total = nanopubs.length;
59 const assertions = nanopubs.filter(n => !n.nanopubType || n.nanopubType.includes('assertion')).length;
60 const retractions = nanopubs.filter(n => n.nanopubType && n.nanopubType.includes('retraction')).length;
61 const newVersions = nanopubs.filter(n => n.nanopubType && n.nanopubType.includes('newVersion')).length;
62 const withIsland = nanopubs.filter(n => n.islandUrl).length;
63 statsEl.innerHTML = [
64 { num: total, label: 'Total' },
65 { num: assertions, label: 'Assertions' },
66 { num: withIsland, label: 'Islands' },
67 { num: retractions, label: 'Retractions' },
68 ].map(s => '<div class="stat"><div class="num">' + s.num + '</div><div class="label">' + s.label + '</div></div>').join('');
69
70 if (!nanopubs.length) {
71 listEl.innerHTML = '<li class="empty">No nanopublications indexed yet</li>';
72 return;
73 }
74
75 listEl.innerHTML = nanopubs.map(n => {
76 const type = n.nanopubType ? n.nanopubType.split('#').pop() : 'assertion';
77 const typeClass = type === 'retraction' ? ' retraction' : type === 'newVersion' ? ' new-version' : '';
78 const label = n.assertion?.label || n.assertion?.subjectIri || 'Untitled';
79 const creators = (n.pubInfo?.creators || []).map(c => {
80 const m = c.match(/orcid\\.org\\/([\\d-]+)/);
81 return m ? m[1] : c.split('/').pop();
82 }).join(', ');
83 const published = n.pubInfo?.createdOn || n.createdAt || '';
84 return '<li class="nanopub-item">'
85 + '<a class="nanopub-label" href="/nanopub/' + n.rkey + '">' + escHtml(label) + '</a>'
86 + '<span class="nanopub-type' + typeClass + '">' + type + '</span>'
87 + (n.islandUrl ? ' <a class="island-link" href="' + escHtml(n.islandUrl) + '">island</a>' : '')
88 + '<div class="nanopub-meta">'
89 + (creators ? '<span>' + escHtml(creators) + '</span>' : '')
90 + (published ? '<span>' + published.substring(0, 10) + '</span>' : '')
91 + (n.sourceNanopubUri ? '<span><a href="' + escHtml(n.sourceNanopubUri) + '">source</a></span>' : '')
92 + '</div></li>';
93 }).join('');
94 } catch (err) {
95 listEl.innerHTML = '<li class="empty">Failed to load: ' + escHtml(err.message) + '</li>';
96 }
97
98 function escHtml(s) { if (!s) return ''; const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
99 })();
100 </script>
101</body>
102</html>`;