devcoach 2.5.0-next.591.g9f8f52c → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/SKILL.md +0 -8
- package/dist/{app-PK4MUP43.js → app-47V7ESOC.js} +99 -116
- package/dist/bin.js +1 -1
- package/dist/chunk-PLIU5NYQ.js +15 -0
- package/dist/chunk-RUBF2HCU.js +104 -0
- package/dist/chunk-UMYQG4J6.js +2 -0
- package/dist/commands-NVMZMCN3.js +36 -0
- package/dist/{hooks-3J5QMLOJ.js → hooks-WFR6QCFN.js} +1 -1
- package/dist/server-XJT543CR.js +9 -0
- package/package.json +1 -1
- package/dist/chunk-PONOJW7Q.js +0 -111
- package/dist/chunk-TTCCNGUU.js +0 -2
- package/dist/chunk-WUEC6V2Z.js +0 -15
- package/dist/commands-OXHDFRFC.js +0 -36
- package/dist/server-P2EJ6QA5.js +0 -9
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {a as a$
|
|
3
|
-
<button type="button" @click="open = !open" :aria-expanded="open" aria-label="More actions" title="More actions" class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-indigo-400 hover:text-gray-800 dark:hover:text-gray-100">${
|
|
2
|
+
import {a as a$1,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a,e,g,j as j$1,l,n as n$1}from'./chunk-UMYQG4J6.js';import {ha,u,w,N,sa,wa,ta,H,L as L$1,I,M,ua,J as J$1,K as K$1,B as B$1,xa,ia,ma,D,C,F,x,A,va,z,E,O,s,ra,qa,ea,ga,n,ka,ja,la,oa,na}from'./chunk-RUBF2HCU.js';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {dirname,join,extname}from'path';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';function j(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function Ge(t,e){return t+e.replace(/[^A-Za-z0-9_-]/g,g=>`_${(g.codePointAt(0)??0).toString(16)}`)}var rt=html`<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><circle cx="4" cy="10" r="1.75"/><circle cx="10" cy="10" r="1.75"/><circle cx="16" cy="10" r="1.75"/></svg>`;function J(t,e="",g=""){return html`<div class="relative ${e}" ${raw(g)} x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
3
|
+
<button type="button" @click="open = !open" :aria-expanded="open" aria-label="More actions" title="More actions" class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-indigo-400 hover:text-gray-800 dark:hover:text-gray-100">${rt}</button>
|
|
4
4
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-56 p-1" style="display:none">
|
|
5
|
-
${
|
|
5
|
+
${t}
|
|
6
6
|
</div>
|
|
7
|
-
</div>`}function
|
|
7
|
+
</div>`}function Y(t,e,g,d,r){return html`<div x-show="${t}" style="display:none" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="fixed inset-0 z-[80] flex items-center justify-center px-4">
|
|
8
8
|
<div class="absolute inset-0 bg-gray-900/50 dark:bg-black/60" @click="${e}"></div>
|
|
9
9
|
<div role="alertdialog" aria-modal="true" class="relative w-full max-w-md bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-xl p-5">
|
|
10
10
|
<div class="flex items-start gap-3">
|
|
@@ -14,16 +14,16 @@ import {a as a$2,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g
|
|
|
14
14
|
<div class="mt-1 text-sm text-gray-500 dark:text-gray-400 space-y-2">${d}</div>
|
|
15
15
|
</div>
|
|
16
16
|
</div>
|
|
17
|
-
<div class="mt-5 flex justify-end gap-2">${
|
|
17
|
+
<div class="mt-5 flex justify-end gap-2">${r}</div>
|
|
18
18
|
</div>
|
|
19
|
-
</div>`}function P(
|
|
19
|
+
</div>`}function P(t){let e=(g,d,r)=>html`<a href="${g}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition ${r?"text-gray-900 dark:text-white font-semibold":""}">${d}</a>`;return html`<!DOCTYPE html>
|
|
20
20
|
<html lang="en">
|
|
21
21
|
<head>
|
|
22
22
|
<meta charset="UTF-8" />
|
|
23
23
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
24
|
-
<title>${
|
|
24
|
+
<title>${t.title}</title>
|
|
25
25
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
|
26
|
-
<meta name="devcoach-theme" content="${
|
|
26
|
+
<meta name="devcoach-theme" content="${t.uiTheme}" />
|
|
27
27
|
<script>
|
|
28
28
|
(function () {
|
|
29
29
|
var serverTheme = document.querySelector('meta[name="devcoach-theme"]').content;
|
|
@@ -38,18 +38,7 @@ import {a as a$2,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g
|
|
|
38
38
|
<script src="/static/vendor/htmx.min.js"></script>
|
|
39
39
|
<script src="/static/vendor/alpinejs.min.js" defer></script>
|
|
40
40
|
<link rel="stylesheet" href="/static/style.css" />
|
|
41
|
-
${
|
|
42
|
-
<script>
|
|
43
|
-
// Older devcoach on a newer database → the dashboard is read-only: one banner on every page.
|
|
44
|
-
fetch('/ping').then(function (r) { return r.json(); }).then(function (p) {
|
|
45
|
-
if (!p || !p.compat) return;
|
|
46
|
-
var b = document.createElement('div');
|
|
47
|
-
b.setAttribute('role', 'status');
|
|
48
|
-
b.className = 'bg-amber-100 dark:bg-amber-900/40 text-amber-900 dark:text-amber-200 border-b border-amber-300 dark:border-amber-700 px-6 py-2 text-sm';
|
|
49
|
-
b.textContent = '⚠ Read-only: ' + p.compat.message;
|
|
50
|
-
document.body.prepend(b);
|
|
51
|
-
}).catch(function () {});
|
|
52
|
-
</script>
|
|
41
|
+
${t.head??""}
|
|
53
42
|
</head>
|
|
54
43
|
<body class="bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 min-h-screen font-mono transition-colors duration-200">
|
|
55
44
|
<nav class="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-6 py-3 flex items-center gap-6">
|
|
@@ -58,16 +47,16 @@ import {a as a$2,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g
|
|
|
58
47
|
<a href="/" class="font-extrabold text-lg tracking-tight text-gray-900 dark:text-gray-100 hover:opacity-80 transition"
|
|
59
48
|
>🎓 dev<span class="text-teal-600 dark:text-teal-300">coach</span></a
|
|
60
49
|
>
|
|
61
|
-
${e("/knowledge","Profile",
|
|
62
|
-
${e("/lessons","Lessons",
|
|
63
|
-
${e("/courses","Courses",
|
|
50
|
+
${e("/knowledge","Profile",t.currentPath==="/knowledge")}
|
|
51
|
+
${e("/lessons","Lessons",t.currentPath.includes("/lessons"))}
|
|
52
|
+
${e("/courses","Courses",t.currentPath.includes("/courses"))}
|
|
64
53
|
<div class="ml-auto flex items-center gap-1.5">
|
|
65
|
-
<a href="/settings" title="Settings" ${
|
|
54
|
+
<a href="/settings" title="Settings" ${t.currentPath==="/settings"?raw('aria-current="page"'):""} class="h-8 px-2.5 inline-flex items-center gap-1.5 rounded-lg border text-sm font-medium transition ${t.currentPath==="/settings"?"bg-white dark:bg-gray-900 border-indigo-400 text-gray-900 dark:text-white":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-indigo-400 hover:text-gray-800 dark:hover:text-gray-100"}"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.56V21a2 2 0 1 1-4 0v-.09a1.7 1.7 0 0 0-1.11-1.56 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.56-1.03H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.65 8.9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6h.09A1.7 1.7 0 0 0 10.1 3.1V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1.03 1.56 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9v.09a1.7 1.7 0 0 0 1.56 1.03H21a2 2 0 1 1 0 4h-.09A1.7 1.7 0 0 0 19.4 15Z"/></svg><span>Settings</span></a>
|
|
66
55
|
<button id="theme-toggle" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme"
|
|
67
56
|
class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-indigo-400 text-base leading-none"></button>
|
|
68
57
|
</div>
|
|
69
58
|
</nav>
|
|
70
|
-
<main class="${
|
|
59
|
+
<main class="${t.wide?"max-w-none":"max-w-7xl"} mx-auto px-4 sm:px-6 py-8">${t.body}</main>
|
|
71
60
|
<script>
|
|
72
61
|
function isDark() { return document.documentElement.classList.contains('dark'); }
|
|
73
62
|
function updateThemeIcon() { var b = document.getElementById('theme-toggle'); if (b) b.textContent = isDark() ? '☀️' : '🌙'; }
|
|
@@ -86,9 +75,9 @@ import {a as a$2,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g
|
|
|
86
75
|
})();
|
|
87
76
|
updateThemeIcon(); updateHljsTheme();
|
|
88
77
|
</script>
|
|
89
|
-
${
|
|
78
|
+
${t.scripts??""}
|
|
90
79
|
</body>
|
|
91
|
-
</html>`}function
|
|
80
|
+
</html>`}function ze(t){let e=d=>t.allGroups.filter(r=>r!=="Other").map(r=>html`<option value="${r}" ${r===d?"selected":""}>${r}</option>`),g=html`
|
|
92
81
|
<div id="knowledge-map" x-data="{ editMode: JSON.parse(localStorage.getItem('km-edit-mode') || 'false'), toggle() { this.editMode = !this.editMode; localStorage.setItem('km-edit-mode', this.editMode); } }">
|
|
93
82
|
|
|
94
83
|
<div class="flex items-center justify-between mb-6">
|
|
@@ -138,16 +127,16 @@ import {a as a$2,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g
|
|
|
138
127
|
</div>
|
|
139
128
|
|
|
140
129
|
<div class="mb-6 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400">
|
|
141
|
-
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(
|
|
130
|
+
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.total_lessons??0)}</span> lessons total</span>
|
|
142
131
|
<span class="text-gray-300 dark:text-gray-600">·</span>
|
|
143
|
-
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(
|
|
132
|
+
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.lessons_today??0)}</span> / ${t.maxPerDay} today</span>
|
|
144
133
|
<span class="text-gray-300 dark:text-gray-600">·</span>
|
|
145
|
-
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(
|
|
134
|
+
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.lessons_this_week??0)}</span> this week</span>
|
|
146
135
|
<span class="text-gray-300 dark:text-gray-600">·</span>
|
|
147
|
-
${
|
|
136
|
+
${t.rateLimit.allowed?html`<span class="text-green-600 dark:text-green-400 font-medium">Available now</span>`:html`<span class="text-yellow-600 dark:text-yellow-400">${t.rateLimit.reason}</span>`}
|
|
148
137
|
</div>
|
|
149
138
|
|
|
150
|
-
${Object.entries(
|
|
139
|
+
${Object.entries(t.categorised).map(([d,r])=>html`
|
|
151
140
|
<section class="mb-6 group/section">
|
|
152
141
|
<div class="flex items-center gap-2 mb-2 border-b border-gray-200 dark:border-gray-800 pb-1">
|
|
153
142
|
<div class="flex items-center gap-1 flex-1 min-w-0">
|
|
@@ -172,7 +161,7 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
|
|
|
172
161
|
</div>
|
|
173
162
|
</div>
|
|
174
163
|
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-0">
|
|
175
|
-
${
|
|
164
|
+
${r.map(o=>{let n=Ge("topic-row-",o.topic),i=o.confidence,s=Math.trunc(i/10*100),c=i>=7?"bg-green-500":i>=4?"bg-yellow-500":"bg-red-500",u=i>=7?"text-green-600 dark:text-green-400":i>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
|
|
176
165
|
<div id="${n}" class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
|
|
177
166
|
<div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative shrink-0 flex items-center">
|
|
178
167
|
<button type="button" @click="open = !open" class="text-xs text-gray-200 dark:text-gray-700 hover:text-indigo-500 dark:hover:text-indigo-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Move to group">⇄</button>
|
|
@@ -207,7 +196,7 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
|
|
|
207
196
|
</div>`})}
|
|
208
197
|
</div>
|
|
209
198
|
</section>`)}
|
|
210
|
-
</div>`;return P({title:"Profile \u2014 devcoach",currentPath:"/knowledge",uiTheme:
|
|
199
|
+
</div>`;return P({title:"Profile \u2014 devcoach",currentPath:"/knowledge",uiTheme:t.uiTheme,body:g})}var Ne={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},He={know:"\u2713 Knew it",understood:"\u{1F4A1} Understood",dont_know:"\u2717 Couldn't follow",none:"\u2014 No response"},qe={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function h(t,e={}){let g={period:t.period,category:t.category,level:t.level,project:t.project,repository:t.repository,branch:t.branch,commit:t.commit,feedback:t.feedback,imported:t.imported,shared_by:t.shared_by,search:t.search,date_from:t.date_from,date_to:t.date_to,sort:t.sort,order:t.order};t.starred&&(g.starred="1"),Object.assign(g,e);let d=new URLSearchParams;for(let[r,o]of Object.entries(g))o&&d.set(r,o);return `?${d.toString()}`}function Ke(t){let e=t.s,g=!!(e.date_from||e.date_to),d=e.period!=="all"&&!g||g||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.imported||e.shared_by||e.search||e.feedback),r=g?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:Ne[e.period]??"All time",o=e.feedback?He[e.feedback]??"Feedback":"Feedback",n=e.shared_by?`\u{1F91D} from ${e.shared_by}`:e.imported==="1"?"\u{1F91D} Shared with me":e.imported==="0"?"\u{1F464} My own":"\u{1F91D} Shared",i=[["","","All lessons"],["0","","\u{1F464} My own"],["1","","\u{1F91D} Shared with me"],...t.allSharedBy.map(l=>["1",l,`\u{1F91D} from ${l}`])],s={junior:"text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/20",mid:"text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20",senior:"text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-900/20"},c=(l,x)=>html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-indigo-50 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800">${l}<a href="${x}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-indigo-200 dark:hover:bg-indigo-700 transition">×</a></span>`,u=(l,x,$="")=>{let S=e.sort===x,k=S&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap relative overflow-hidden text-ellipsis ${$}">
|
|
211
200
|
<a href="${h(e,{sort:x,order:k})}" class="hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${l}
|
|
212
201
|
${S?html`<span class="text-indigo-400">${e.order==="asc"?"\u2191":"\u2193"}</span>`:html`<span class="text-gray-200 dark:text-gray-700 group-hover/sort:text-gray-400 dark:group-hover/sort:text-gray-500 transition">↕</span>`}
|
|
213
202
|
</a></th>`},p=html`<svg class="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>`,m=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
|
|
@@ -221,7 +210,7 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
|
|
|
221
210
|
.dark .flatpickr-current-month, .dark .flatpickr-monthDropdown-months { color:#e5e7eb; background:#1f2937; }
|
|
222
211
|
.dark .flatpickr-weekday { color:#9ca3af; background:#1f2937; }
|
|
223
212
|
.dark .flatpickr-prev-month svg, .dark .flatpickr-next-month svg { fill:#9ca3af; }
|
|
224
|
-
</style>`,v=
|
|
213
|
+
</style>`,v=t.total===0?"No lessons":t.totalPages===1?`${t.total} lesson${t.total!==1?"s":""}`:`${(t.page-1)*t.perPage+1}\u2013${Math.min(t.page*t.perPage,t.total)} of ${t.total}`,I=JSON.stringify(t.lessons.map(l=>l.id)),R=html`
|
|
225
214
|
<div :class="selectMode && 'dc-selecting'" x-data="{ selectMode: false, selected: [], confirmOpen: false, shareOpen: false, sharePos: { left: 0, top: null, bottom: null }, openShare(btn) { const r = btn.getBoundingClientRect(); const w = 320; const left = Math.max(8, Math.min(r.right - w, innerWidth - w - 8)); const below = innerHeight - r.bottom > 380; this.sharePos = { left, top: below ? r.bottom + 6 : null, bottom: below ? null : innerHeight - r.top + 6 }; this.shareOpen = true }, toggle(id) { const i = this.selected.indexOf(id); if (i < 0) this.selected.push(id); else this.selected.splice(i, 1) }, setAll(ids, on) { this.selected = on ? ids.slice() : [] }, selectedTitles() { const rows = Array.from(document.querySelectorAll('tr[data-id]')); return this.selected.map((id) => { const r = rows.find((x) => x.dataset.id === id); return r ? r.dataset.title : id }) }, leave() { this.selectMode = false; this.selected = []; this.shareOpen = false; this.confirmOpen = false } }" @keydown.escape.window="leave()">
|
|
226
215
|
<form id="filter-form" method="get" action="/lessons">
|
|
227
216
|
<input type="hidden" name="period" id="h-period" value="${e.period}">
|
|
@@ -260,11 +249,11 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
|
|
|
260
249
|
|
|
261
250
|
<div class="relative" x-data="periodPicker()" @keydown.escape="close()">
|
|
262
251
|
<button type="button" @click="toggle()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${g||e.period!=="all"?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">
|
|
263
|
-
<span>📅</span><span x-text="label">${
|
|
252
|
+
<span>📅</span><span x-text="label">${r}</span>${p}
|
|
264
253
|
</button>
|
|
265
254
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" @click.outside="close()" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 overflow-hidden" style="display:none">
|
|
266
255
|
<div class="p-1">
|
|
267
|
-
${Object.entries(
|
|
256
|
+
${Object.entries(Ne).map(([l,x])=>{let $=e.period===l&&!g;return html`<button type="button" @click="selectPreset('${l}', '${x}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${$?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${$?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
|
|
268
257
|
</div>
|
|
269
258
|
<div class="border-t border-gray-100 dark:border-gray-800 p-1">
|
|
270
259
|
<button type="button" @click="showCustom = !showCustom" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${g?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">
|
|
@@ -286,34 +275,34 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
|
|
|
286
275
|
</div>
|
|
287
276
|
|
|
288
277
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
289
|
-
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${e.level?`${
|
|
278
|
+
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${e.level?`${qe[e.level]??""} ${e.level}`:"Level"}${p}</button>
|
|
290
279
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-40 p-1 overflow-hidden" style="display:none">
|
|
291
280
|
${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([l,x])=>html`<button type="button" onclick="document.getElementById('h-level').value='${l}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.level===l?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${e.level===l?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
|
|
292
281
|
</div>
|
|
293
282
|
</div>
|
|
294
283
|
|
|
295
|
-
${
|
|
284
|
+
${t.allCategories.length||t.allProjects.length||t.allRepositories.length||t.allBranches.length||t.allCommits.length?html`<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
296
285
|
<button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.category||e.project||e.repository||e.branch||e.commit?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">Filters${p}</button>
|
|
297
286
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 p-3 space-y-2.5" style="display:none">
|
|
298
|
-
${
|
|
299
|
-
${
|
|
300
|
-
${
|
|
301
|
-
${
|
|
302
|
-
${
|
|
287
|
+
${t.allCategories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Category</label><select name="category" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All categories</option>${t.allCategories.map(l=>html`<option value="${l}" ${e.category===l?"selected":""}>${l}</option>`)}</select></div>`:""}
|
|
288
|
+
${t.allProjects.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Project</label><select name="project" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All projects</option>${t.allProjects.map(l=>html`<option value="${l}" ${e.project===l?"selected":""}>${l}</option>`)}</select></div>`:""}
|
|
289
|
+
${t.allRepositories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Repository</label><select name="repository" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All repositories</option>${t.allRepositories.map(l=>html`<option value="${l}" ${e.repository===l?"selected":""}>${l}</option>`)}</select></div>`:""}
|
|
290
|
+
${t.allBranches.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Branch</label><input type="text" name="branch" value="${e.branch}" placeholder="e.g. main" list="branch-list" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" /><datalist id="branch-list">${t.allBranches.map(l=>html`<option value="${l}">`)}</datalist></div>`:""}
|
|
291
|
+
${t.allCommits.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Commit</label><input type="text" name="commit" value="${e.commit}" placeholder="hash prefix…" list="commit-list" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500" /><datalist id="commit-list">${t.allCommits.map(l=>html`<option value="${l.slice(0,7)}">`)}</datalist></div>`:""}
|
|
303
292
|
<button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg px-3 py-1.5 text-xs font-medium transition">Apply</button>
|
|
304
293
|
</div>
|
|
305
294
|
</div>`:""}
|
|
306
295
|
|
|
307
296
|
${d?html`<a href="/lessons" class="ml-auto text-xs text-gray-400 hover:text-gray-700 dark:hover:text-white transition">Clear all</a>`:""}
|
|
308
297
|
|
|
309
|
-
${
|
|
298
|
+
${J(html`<button type="button" @click="selectMode = true; open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"><span class="w-4 h-4 inline-flex items-center justify-center shrink-0 text-[13px] leading-none" aria-hidden="true">🗑</span>Delete lessons…</button>
|
|
310
299
|
<button type="button" @click="window.resetLessonColumns && window.resetLessonColumns(); open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"><svg class="w-4 h-4 shrink-0 text-gray-400 dark:text-gray-500" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><rect x="2.5" y="4" width="15" height="12" rx="2"/><path d="M7.5 4v12M12.5 4v12"/></svg>Reset column widths</button>`,d?"":"ml-auto",'x-show="!selectMode"')}
|
|
311
300
|
<button type="button" x-show="selectMode" style="display:none" @click="leave()" class="${d?"":"ml-auto"} inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-rose-400 hover:text-rose-600 dark:hover:text-rose-400">✕ Cancel</button>
|
|
312
|
-
<div class="relative" x-data="{ open: ${String(
|
|
301
|
+
<div class="relative" x-data="{ open: ${String(t.importOpen)} }" @click.outside="open = false" @keydown.escape="open = false">
|
|
313
302
|
<button type="button" @click="open = !open" title="Import a lesson someone shared" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">+ Import</button>
|
|
314
303
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-80 p-3 space-y-2.5" style="display:none">
|
|
315
304
|
<p class="text-xs text-gray-500 dark:text-gray-400">Paste the lesson code, the link, a URL or the whole text — or drop a <code>.devcoach.md</code> anywhere on this page.</p>
|
|
316
|
-
${
|
|
305
|
+
${t.importError==="invalid"?html`<p class="text-xs text-rose-600 dark:text-rose-400">That doesn't look like a devcoach lesson — copy the whole text again.</p>`:t.importError==="cross"?html`<p class="text-xs text-rose-600 dark:text-rose-400">Imports only work from this dashboard — paste the lesson here.</p>`:""}
|
|
317
306
|
<textarea name="text" form="import-form" rows="4" placeholder="devcoach:lesson:1:…" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono" x-ref="importText" x-effect="if (open) $nextTick(() => $refs.importText.focus())"></textarea>
|
|
318
307
|
<div class="flex items-center gap-2">
|
|
319
308
|
<label class="cursor-pointer text-xs text-gray-500 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">📄 Choose file<input id="import-file" type="file" name="file" form="import-form" accept=".md,.json,.txt" class="hidden" onchange="document.getElementById('import-form').submit()" /></label>
|
|
@@ -324,14 +313,14 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
|
|
|
324
313
|
</div>
|
|
325
314
|
|
|
326
315
|
${d?html`<div class="flex flex-wrap gap-1.5 mb-4">
|
|
327
|
-
${g?c(`\u{1F4C5} ${
|
|
328
|
-
${e.period!=="all"&&!g?c(`\u{1F550} ${
|
|
329
|
-
${e.feedback?c(
|
|
316
|
+
${g?c(`\u{1F4C5} ${r}`,h(e,{date_from:"",date_to:""})):""}
|
|
317
|
+
${e.period!=="all"&&!g?c(`\u{1F550} ${r}`,h(e,{period:"all"})):""}
|
|
318
|
+
${e.feedback?c(He[e.feedback]??e.feedback,h(e,{feedback:""})):""}
|
|
330
319
|
${e.starred?html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300 border border-yellow-200 dark:border-yellow-800">★ Starred<a href="${h({...e,starred:false})}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-yellow-200 dark:hover:bg-yellow-700 transition">×</a></span>`:""}
|
|
331
320
|
${e.imported||e.shared_by?c(n,h(e,{imported:"",shared_by:""})):""}
|
|
332
321
|
${e.search?c(`\u{1F50D} "${e.search}"`,h(e,{search:""})):""}
|
|
333
322
|
${e.category?c(e.category,h(e,{category:""})):""}
|
|
334
|
-
${e.level?c(`${
|
|
323
|
+
${e.level?c(`${qe[e.level]??""} ${e.level}`,h(e,{level:""})):""}
|
|
335
324
|
${e.project?c(`\u{1F4C1} ${e.project}`,h(e,{project:""})):""}
|
|
336
325
|
${e.repository?c(`\u2387 ${e.repository}`,h(e,{repository:""})):""}
|
|
337
326
|
${e.branch?c(`\u2387 ${e.branch}`,h(e,{branch:""})):""}
|
|
@@ -341,7 +330,7 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
|
|
|
341
330
|
<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="hidden"><input type="hidden" name="from" value="lessons" /></form>
|
|
342
331
|
<div id="drop-hint" class="hidden fixed inset-0 z-[60] bg-indigo-500/10 border-4 border-dashed border-indigo-400 pointer-events-none items-center justify-center"><p class="bg-white dark:bg-gray-900 text-indigo-700 dark:text-indigo-300 font-semibold rounded-xl px-6 py-3 shadow-lg">Drop to import the lesson</p></div>
|
|
343
332
|
|
|
344
|
-
${
|
|
333
|
+
${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
|
|
345
334
|
<table class="w-full text-sm table-fixed" data-resizable="lessons">
|
|
346
335
|
<colgroup>
|
|
347
336
|
<col data-col="star" style="width:2.75rem" />
|
|
@@ -366,7 +355,7 @@ ${r.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
|
|
|
366
355
|
</tr>
|
|
367
356
|
</thead>
|
|
368
357
|
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
369
|
-
${
|
|
358
|
+
${t.lessons.map(l=>{let x=Ge("lesson-row-",l.id),$=l.timestamp.slice(0,10),S=l.timestamp.slice(0,16).replace("T"," ");return html`<tr id="${x}" data-id="${l.id}" data-title="${l.title}" data-href="/lessons/${encodeURIComponent(l.id)}" class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" role="link" @click="if (selectMode) toggle($el.dataset.id); else if (!$event.target.closest('a, button, input, label')) window.location = $el.dataset.href" @keydown.enter="if (selectMode) toggle($el.dataset.id); else if ($event.target === $el) window.location = $el.dataset.href" @keydown.space.prevent="selectMode && toggle($el.dataset.id)" :class="selected.includes($el.dataset.id) && 'bg-indigo-50 dark:bg-indigo-900/20'">
|
|
370
359
|
<td class="px-3 py-3">
|
|
371
360
|
<form x-show="!selectMode" method="post" action="/lessons/${encodeURIComponent(l.id)}/star" hx-post="/lessons/${encodeURIComponent(l.id)}/star" hx-target="#${x}" hx-select="#${x}" hx-swap="outerHTML">
|
|
372
361
|
<input type="hidden" name="starred" value="${l.starred?"0":"1"}" />
|
|
@@ -389,12 +378,12 @@ ${r.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
|
|
|
389
378
|
</tbody>
|
|
390
379
|
</table>
|
|
391
380
|
</div>
|
|
392
|
-
${
|
|
393
|
-
<p class="text-xs text-gray-400 dark:text-gray-500">Page ${
|
|
381
|
+
${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
|
|
382
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">Page ${t.page} of ${t.totalPages}</p>
|
|
394
383
|
<div class="flex items-center gap-1">
|
|
395
|
-
${
|
|
396
|
-
${
|
|
397
|
-
${
|
|
384
|
+
${t.page>1?html`<a href="${h(e,{page:String(t.page-1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">← Prev</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">← Prev</span>`}
|
|
385
|
+
${ot(t.page,t.totalPages).map(l=>l===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:l===t.page?html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-indigo-600 text-white border border-indigo-600">${l}</span>`:html`<a href="${h(e,{page:String(l)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">${l}</a>`)}
|
|
386
|
+
${t.page<t.totalPages?html`<a href="${h(e,{page:String(t.page+1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">Next →</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">Next →</span>`}
|
|
398
387
|
</div>
|
|
399
388
|
</div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
|
|
400
389
|
<p class="text-3xl mb-3">📭</p>
|
|
@@ -411,14 +400,14 @@ ${r.totalPages>1?html`<div class="flex items-center justify-between mt-4">
|
|
|
411
400
|
</div>
|
|
412
401
|
</div>
|
|
413
402
|
</div>
|
|
414
|
-
${
|
|
403
|
+
${Y("confirmOpen","confirmOpen = false",html`<span x-text="selected.length === 1 ? 'Delete this lesson?' : 'Delete ' + selected.length + ' lessons?'"></span>`,html`<ul class="max-h-40 overflow-y-auto space-y-1 text-gray-700 dark:text-gray-200">
|
|
415
404
|
<template x-for="t in selectedTitles().slice(0, 6)" :key="t"><li class="truncate">· <span x-text="t"></span></li></template>
|
|
416
405
|
</ul>
|
|
417
406
|
<p x-show="selected.length > 6" style="display:none" x-text="'…and ' + (selected.length - 6) + ' more'"></p>
|
|
418
407
|
<p>They will be removed from your log. This cannot be undone.</p>`,html`<button type="button" @click="confirmOpen = false" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">Cancel</button>
|
|
419
408
|
<form method="post" action="/lessons/delete" class="contents">
|
|
420
409
|
<template x-for="id in selected" :key="id"><input type="hidden" name="id" :value="id" /></template>
|
|
421
|
-
<input type="hidden" name="next" value="/lessons${h(e,{page:String(
|
|
410
|
+
<input type="hidden" name="next" value="/lessons${h(e,{page:String(t.page)})}" />
|
|
422
411
|
<button type="submit" :disabled="!selected.length" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-rose-600 text-white border-rose-600 hover:bg-rose-500" x-text="selected.length === 1 ? '🗑 Delete lesson' : '🗑 Delete ' + selected.length + ' lessons'">🗑 Delete</button>
|
|
423
412
|
</form>`)}
|
|
424
413
|
<div x-show="shareOpen" style="display:none" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="fixed inset-0 z-[70] pointer-events-none">
|
|
@@ -427,7 +416,7 @@ ${J("confirmOpen","confirmOpen = false",html`<span x-text="selected.length === 1
|
|
|
427
416
|
<div id="share-modal-body" class="space-y-3"><p class="text-xs text-gray-400 dark:text-gray-500">Loading…</p></div>
|
|
428
417
|
</div>
|
|
429
418
|
</div>
|
|
430
|
-
</div>`,
|
|
419
|
+
</div>`,E=html`<script src="/static/vendor/flatpickr.min.js"></script>
|
|
431
420
|
<script src="/static/relative-time.js"></script>
|
|
432
421
|
<script src="/static/table-resize.js"></script>
|
|
433
422
|
<script src="/static/share.js"></script>
|
|
@@ -436,7 +425,7 @@ function periodPicker() {
|
|
|
436
425
|
return {
|
|
437
426
|
open: false,
|
|
438
427
|
showCustom: ${raw(g?"true":"false")},
|
|
439
|
-
label: ${raw(j(
|
|
428
|
+
label: ${raw(j(r))},
|
|
440
429
|
fp: null,
|
|
441
430
|
init() {
|
|
442
431
|
const self = this;
|
|
@@ -467,17 +456,17 @@ function periodPicker() {
|
|
|
467
456
|
},
|
|
468
457
|
};
|
|
469
458
|
}
|
|
470
|
-
</script>`;return P({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:
|
|
459
|
+
</script>`;return P({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:m,body:R,scripts:E})}function ot(t,e){let g=[],d=0;for(let r=1;r<=e;r++)(r===1||r===e||r>=t-2&&r<=t+2)&&(d&&r-d>1&&g.push(0),g.push(r),d=r);return g}var K={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function V(t){let e=`/lessons/${encodeURIComponent(t.id)}/share?format=md&name=${encodeURIComponent(t.name)}&include_context=${t.includeContext?"1":"0"}`;return html`<div id="share-payloads" data-text="${t.text}" data-link="${t.link}" class="space-y-2">
|
|
471
460
|
<div class="flex flex-wrap gap-2">
|
|
472
461
|
<button type="button" onclick="copyShare('text', this)" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 text-gray-700 dark:text-gray-200 hover:text-indigo-700 dark:hover:text-indigo-300 transition">📋 Copy text</button>
|
|
473
462
|
<button type="button" onclick="copyShare('link', this)" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 text-gray-700 dark:text-gray-200 hover:text-indigo-700 dark:hover:text-indigo-300 transition">🔗 Copy link</button>
|
|
474
463
|
<a href="${e}" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 text-gray-700 dark:text-gray-200 hover:text-indigo-700 dark:hover:text-indigo-300 transition">⬇ Download .md</a>
|
|
475
464
|
</div>
|
|
476
|
-
<p class="text-[11px] text-gray-400 dark:text-gray-500">${
|
|
477
|
-
</div>`}function W(
|
|
465
|
+
<p class="text-[11px] text-gray-400 dark:text-gray-500">${t.name?html`Shared by <span class="text-gray-600 dark:text-gray-300">${t.name}</span>`:"Shared anonymously"} · ${t.includeContext?"includes project, branch and commit":"only the lesson travels \u2014 no paths, no project"}.</p>
|
|
466
|
+
</div>`}function W(t,e,g=null){return html`
|
|
478
467
|
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Share this lesson</p>
|
|
479
468
|
${g?html`<p class="text-sm font-semibold text-gray-800 dark:text-gray-100 leading-snug">${g}</p>`:""}
|
|
480
|
-
<form method="post" action="/lessons/${encodeURIComponent(
|
|
469
|
+
<form method="post" action="/lessons/${encodeURIComponent(t)}/share" hx-post="/lessons/${encodeURIComponent(t)}/share" hx-target="#share-payloads" hx-swap="outerHTML" hx-trigger="input delay:300ms, change" onsubmit="return false" class="space-y-2.5">
|
|
481
470
|
<input type="hidden" name="persist" value="0" x-ref="persist" />
|
|
482
471
|
<div>
|
|
483
472
|
<label for="share-name" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Your name</label>
|
|
@@ -486,10 +475,10 @@ function periodPicker() {
|
|
|
486
475
|
<label class="flex items-start gap-2 text-xs text-gray-600 dark:text-gray-300 cursor-pointer"><input type="checkbox" name="include_context" value="1" ${e.includeContext?"checked":""} class="mt-0.5" /><span>Include where it happened <span class="text-gray-400 dark:text-gray-500">(project, branch, commit — never local paths)</span></span></label>
|
|
487
476
|
</form>
|
|
488
477
|
${V(e)}
|
|
489
|
-
`}function
|
|
478
|
+
`}function Je(t){let e=t.lesson,g=t.share,d=e.level==="junior"?"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700":e.level==="mid"?"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700":"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700",r=e.repository_platform==="local",o=null;r&&e.repository?o=`vscode://file/${e.repository}`:e.repository_platform&&K[e.repository_platform]&&e.repository&&(o=`https://${K[e.repository_platform]}/${e.repository}`);let n=null;o&&e.commit_hash&&!r&&(n=e.repository_platform==="gitlab"?`${o}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${o}/commits/${e.commit_hash}`:`${o}/commit/${e.commit_hash}`);let i=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),s=e.timestamp.slice(0,10),c=e.timestamp.slice(0,16).replace("T"," "),u=`hx-post="/lessons/${encodeURIComponent(e.id)}/feedback" hx-target="#lesson-meta" hx-select="#lesson-meta" hx-swap="outerHTML" hx-select-oob="#lesson-feedback:outerHTML"`,p=html`
|
|
490
479
|
<div x-data="{ confirmOpen: false }" @keydown.escape.window="confirmOpen = false">
|
|
491
480
|
<div class="mb-4"><a href="/lessons" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition">← Back to lessons</a></div>
|
|
492
|
-
${
|
|
481
|
+
${t.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300">${t.imported==="new"?html`✓ Imported “${e.title}”${e.shared_by?html`, shared by ${e.shared_by}`:""}. It's in your log now — feedback works as usual and it never counts against your daily limit.`:html`“${e.title}” was already in your log — nothing changed.`}</div>`:""}
|
|
493
482
|
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
|
|
494
483
|
<div class="flex flex-wrap items-center gap-3 mb-2">
|
|
495
484
|
<form id="lesson-star" method="post" action="/lessons/${encodeURIComponent(e.id)}/star" hx-post="/lessons/${encodeURIComponent(e.id)}/star" hx-target="#lesson-star" hx-select="#lesson-star" hx-swap="outerHTML">
|
|
@@ -499,7 +488,7 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
499
488
|
</form>
|
|
500
489
|
<h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.title}</h1>
|
|
501
490
|
<a href="/lessons?level=${e.level}" class="text-xs font-semibold px-2 py-0.5 rounded-full border ${d} shrink-0 hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${e.level}</a>
|
|
502
|
-
${
|
|
491
|
+
${J(html`<button type="button" @click="confirmOpen = true; open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 hover:!bg-rose-50 dark:hover:!bg-rose-900/30 hover:text-rose-700 dark:hover:text-rose-300"><span class="w-4 h-4 inline-flex items-center justify-center shrink-0 text-[13px] leading-none" aria-hidden="true">🗑</span>Delete lesson…</button>`,"shrink-0")}
|
|
503
492
|
<div class="relative shrink-0" x-data="{ open: ${String(g.open)} }" @click.outside="open = false" @keydown.escape="open = false">
|
|
504
493
|
<button type="button" @click="open = !open" title="Share this lesson" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">↗ Share</button>
|
|
505
494
|
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-80 p-4 space-y-3" style="display:none">
|
|
@@ -513,7 +502,7 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
513
502
|
</span>
|
|
514
503
|
<span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></span>
|
|
515
504
|
${e.imported?html`<span title="This lesson was shared with you">🤝 shared by <span class="text-gray-700 dark:text-gray-200">${e.shared_by??"anonymous"}</span></span>`:""}
|
|
516
|
-
${
|
|
505
|
+
${t.course?html`<a href="/courses/${encodeURIComponent(t.course.id)}" title="${t.course.title}" class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700 hover:border-indigo-400 transition">🎓 Course · ${t.course.done}/${t.course.total}${t.course.status==="completed"?" \u2713":""}</a>`:""}
|
|
517
506
|
${e.categories.map(I=>html`<a href="/lessons?category=${encodeURIComponent(I)}" class="inline-block bg-gray-100 dark:bg-gray-800 hover:bg-indigo-50 dark:hover:bg-indigo-900/50 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 transition border border-gray-200 dark:border-gray-700 hover:border-indigo-400 dark:hover:border-indigo-600">${I}</a>`)}
|
|
518
507
|
${e.feedback?html`${e.feedback==="know"?html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 border border-green-200 dark:border-green-700">✓ I knew this</span>`:e.feedback==="understood"?html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700">💡 Understood</span>`:html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 border border-red-200 dark:border-red-700">✗ Couldn't follow</span>`}
|
|
519
508
|
<form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(u)}><input type="hidden" name="feedback" value="clear" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="text-xs text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400 transition">Clear</button></form>`:""}
|
|
@@ -525,8 +514,8 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
525
514
|
<div id="body-content" class="markdown-body"></div>
|
|
526
515
|
${e.task_context?html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 text-sm text-gray-500 dark:text-gray-400"><span class="text-gray-400 dark:text-gray-500">Context:</span> ${e.task_context}</div>`:""}
|
|
527
516
|
${i?html`<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800"><div class="flex flex-wrap gap-x-5 gap-y-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
|
|
528
|
-
${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${o?html`<a href="${o}" ${
|
|
529
|
-
${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&K[e.repository_platform]?html`<img src="/static/vendor/icons/${e.repository_platform}.svg" class="w-3 h-3 dark:invert opacity-60 shrink-0" alt="" />`:""}${o?html`<a href="${o}" ${
|
|
517
|
+
${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${o?html`<a href="${o}" ${r?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.project}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.project}</span>`}</span>`:""}
|
|
518
|
+
${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&K[e.repository_platform]?html`<img src="/static/vendor/icons/${e.repository_platform}.svg" class="w-3 h-3 dark:invert opacity-60 shrink-0" alt="" />`:""}${o?html`<a href="${o}" ${r?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.repository}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.repository}</span>`}</span>`:""}
|
|
530
519
|
${e.branch?html`<span><span class="text-gray-400 dark:text-gray-600">branch</span> <span class="text-indigo-600 dark:text-indigo-400">${e.branch}</span></span>`:""}
|
|
531
520
|
${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${n?html`<a href="${n}" target="_blank" rel="noopener" class="text-cyan-600 dark:text-cyan-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.commit_hash.slice(0,7)}</a>`:html`<span class="text-cyan-600 dark:text-cyan-400">${e.commit_hash.slice(0,7)}</span>`}</span>`:""}
|
|
532
521
|
${e.folder?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">folder</span><a href="vscode://file/${e.folder}" class="inline-flex items-center gap-1 text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition"><img src="/static/vendor/icons/vscode.svg" class="w-3.5 h-3.5 shrink-0" alt="" />${e.folder}</a></span>`:""}
|
|
@@ -538,7 +527,7 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
538
527
|
</div>`}</div>
|
|
539
528
|
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-300 dark:text-gray-600">ID: ${e.id}</div>
|
|
540
529
|
</div>
|
|
541
|
-
${
|
|
530
|
+
${Y("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class="text-gray-700 dark:text-gray-200 font-medium">“${e.title}”</p>
|
|
542
531
|
<p>${e.imported?html`It was shared with you${e.shared_by?html` by ${e.shared_by}`:""}; you can import it again later. `:""}It will be removed from your log. This cannot be undone.</p>`,html`<button type="button" @click="confirmOpen = false" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">Cancel</button>
|
|
543
532
|
<form method="post" action="/lessons/delete" class="contents">
|
|
544
533
|
<input type="hidden" name="id" value="${e.id}" />
|
|
@@ -561,7 +550,7 @@ ${J("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
|
|
|
561
550
|
});
|
|
562
551
|
document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(e.summary))}));
|
|
563
552
|
document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(e.body))}));
|
|
564
|
-
</script>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:
|
|
553
|
+
</script>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:m,body:p,scripts:v})}var at={junior:"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700",mid:"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700",senior:"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700"};function Ye(t){let e=t.preview,g=html`<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="space-y-3">
|
|
565
554
|
<input type="hidden" name="from" value="lessons" />
|
|
566
555
|
<textarea name="text" rows="5" placeholder="devcoach:lesson:1:… — or the link, a URL, or the whole card" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono"></textarea>
|
|
567
556
|
<div class="flex items-center gap-3">
|
|
@@ -573,10 +562,10 @@ ${J("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
|
|
|
573
562
|
<div class="mb-4"><a href="/lessons" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition">← Back to lessons</a></div>
|
|
574
563
|
<div class="${e?"max-w-5xl":"max-w-3xl"} mx-auto bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
|
|
575
564
|
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-4">${e?"Someone shared a lesson with you":"Import a shared lesson"}</p>
|
|
576
|
-
${
|
|
565
|
+
${t.error?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-rose-50 dark:bg-rose-900/20 border-rose-200 dark:border-rose-700 text-rose-700 dark:text-rose-300">${t.error}</div>`:""}
|
|
577
566
|
${e?html`<div class="flex flex-wrap items-center gap-3 mb-2">
|
|
578
567
|
<h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.lesson.title}</h1>
|
|
579
|
-
<span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${
|
|
568
|
+
<span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${at[e.lesson.level]??""} shrink-0">${e.lesson.level}</span>
|
|
580
569
|
</div>
|
|
581
570
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
|
|
582
571
|
<span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.lesson.topic_id}</span></span>
|
|
@@ -594,11 +583,11 @@ ${J("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
|
|
|
594
583
|
${e.lesson.task_context?html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 text-sm text-gray-500 dark:text-gray-400"><span class="text-gray-400 dark:text-gray-500">Context:</span> ${e.lesson.task_context}</div>`:""}
|
|
595
584
|
<form method="post" action="/lessons/import" class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 flex flex-wrap items-center gap-3">
|
|
596
585
|
<input type="hidden" name="from" value="lessons" />
|
|
597
|
-
<input type="hidden" name="text" value="${
|
|
586
|
+
<input type="hidden" name="text" value="${t.code}" />
|
|
598
587
|
<button type="submit" class="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg px-4 py-2 text-sm transition">+ Add to my lessons</button>
|
|
599
588
|
<p class="text-xs text-gray-400 dark:text-gray-500">Nothing is saved until you click. It joins your log like your own lessons and never counts against the daily limit.</p>
|
|
600
589
|
</form>`:g}
|
|
601
|
-
</div>`,
|
|
590
|
+
</div>`,r=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,o=html`<script src="/static/vendor/highlight.min.js"></script>
|
|
602
591
|
<script src="/static/vendor/marked.min.js"></script>
|
|
603
592
|
<script src="/static/vendor/purify.min.js"></script>
|
|
604
593
|
<script src="/static/share.js"></script>
|
|
@@ -613,12 +602,12 @@ ${J("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
|
|
|
613
602
|
});
|
|
614
603
|
${e?raw(`document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${j(e.lesson.summary)}));
|
|
615
604
|
document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${j(e.lesson.body??"")}));`):""}
|
|
616
|
-
</script>`;return P({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:
|
|
605
|
+
</script>`;return P({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:r,body:d,scripts:o})}var st=[[0,"No cooldown"],[15,"15 minutes"],[30,"30 minutes"],[60,"1 hour"],[90,"1 hour 30 min"],[120,"2 hours"],[180,"3 hours"],[240,"4 hours"],[360,"6 hours"],[480,"8 hours"],[720,"12 hours"],[1440,"24 hours"]],Fe={active:"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border-indigo-200 dark:border-indigo-700",completed:"bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-400 border-green-200 dark:border-green-700",abandoned:"bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700"},nt={todo:"\u25CB",done:"\u25CF",skipped:"\u25CC"};function Ve(t){return html`<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${Fe[t]??Fe.abandoned}">${t}</span>`}function We(t){let e=html`
|
|
617
606
|
<div class="flex items-center justify-between mb-6">
|
|
618
607
|
<h1 class="text-2xl font-bold text-indigo-600 dark:text-indigo-400">Courses</h1>
|
|
619
|
-
<p class="text-sm text-gray-400 dark:text-gray-500">${
|
|
608
|
+
<p class="text-sm text-gray-400 dark:text-gray-500">${t.courses.length} course${t.courses.length!==1?"s":""}</p>
|
|
620
609
|
</div>
|
|
621
|
-
${
|
|
610
|
+
${t.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
|
|
622
611
|
<table class="w-full text-sm table-fixed">
|
|
623
612
|
<colgroup><col style="width:46%" /><col style="width:22%" /><col style="width:14%" /><col style="width:8rem" /></colgroup>
|
|
624
613
|
<thead>
|
|
@@ -627,15 +616,15 @@ ${r.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
|
|
|
627
616
|
</tr>
|
|
628
617
|
</thead>
|
|
629
618
|
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
630
|
-
${
|
|
619
|
+
${t.courses.map(d=>{let r=d.steps.filter(n=>n.status!=="todo").length,o=d.steps.length?Math.round(r/d.steps.length*100):0;return html`<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors cursor-pointer" tabindex="0" role="link" data-href="/courses/${encodeURIComponent(d.id)}" @click="if (!$event.target.closest('a, button')) window.location = $el.dataset.href" @keydown.enter="window.location = $el.dataset.href">
|
|
631
620
|
<td class="px-4 py-3">
|
|
632
621
|
<a href="/courses/${encodeURIComponent(d.id)}" class="font-semibold text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${d.title}</a>
|
|
633
622
|
${d.lesson_id?html`<span class="block text-[11px] text-gray-400 dark:text-gray-500 truncate">from <a href="/lessons/${encodeURIComponent(d.lesson_id)}" class="hover:text-indigo-500 hover:underline">${d.lesson_title??d.lesson_id}</a></span>`:""}
|
|
634
623
|
</td>
|
|
635
624
|
<td class="px-3 py-3">
|
|
636
|
-
<div class="flex items-center gap-2"><div class="flex-1 h-1.5 rounded-full bg-gray-100 dark:bg-gray-800 overflow-hidden"><div class="h-full bg-indigo-500" style="width:${o}%"></div></div><span class="text-xs tabular-nums text-gray-500 dark:text-gray-400 shrink-0">${
|
|
625
|
+
<div class="flex items-center gap-2"><div class="flex-1 h-1.5 rounded-full bg-gray-100 dark:bg-gray-800 overflow-hidden"><div class="h-full bg-indigo-500" style="width:${o}%"></div></div><span class="text-xs tabular-nums text-gray-500 dark:text-gray-400 shrink-0">${r}/${d.steps.length}</span></div>
|
|
637
626
|
</td>
|
|
638
|
-
<td class="px-3 py-3">${
|
|
627
|
+
<td class="px-3 py-3">${Ve(d.status)}</td>
|
|
639
628
|
<td class="px-3 py-3 whitespace-nowrap text-gray-400 dark:text-gray-500 tabular-nums"><span data-ts="${d.updated_at}">${d.updated_at.slice(0,10)}</span></td>
|
|
640
629
|
</tr>`})}
|
|
641
630
|
</tbody>
|
|
@@ -644,27 +633,21 @@ ${r.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
|
|
|
644
633
|
<p class="text-3xl mb-3">🎓</p>
|
|
645
634
|
<p class="text-gray-700 dark:text-gray-200 font-medium">No courses yet.</p>
|
|
646
635
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-2 max-w-md mx-auto">A course starts in your agent: answer <span class="font-mono">n</span> (couldn't follow) under a lesson card and say yes to the offer, or run <span class="font-mono">/devcoach:course</span> on any lesson or concept. The AI first asks what you already know, then writes the course here.</p>
|
|
647
|
-
</div>`}`,g=html`<script src="/static/relative-time.js"></script>`;return P({title:"Courses \u2014 devcoach",currentPath:"/courses",uiTheme:
|
|
648
|
-
<link rel="stylesheet" href="/static/style.css" /></head>
|
|
649
|
-
<body style="font-family: ui-monospace, monospace; max-width: 40rem; margin: 4rem auto; padding: 0 1rem; line-height: 1.5">
|
|
650
|
-
<h1 style="font-size: 1.25rem">This devcoach is older than your database</h1>
|
|
651
|
-
<p>${r}</p>
|
|
652
|
-
<p><a href="javascript:history.back()">← Back</a></p>
|
|
653
|
-
</body></html>`}function tt(r){let e=r.course,g=e.steps.filter(s=>s.status!=="todo").length,d=e.steps.find(s=>s.position===r.selected)??e.steps[0],t=`/courses/${encodeURIComponent(e.id)}/index.html${d?`#${d.anchor}`:""}`,o=(s,c,u,p)=>html`<form method="post" action="/courses/${encodeURIComponent(e.id)}/steps/${s}" class="inline"><input type="hidden" name="status" value="${c}" /><input type="hidden" name="next" value="/courses/${encodeURIComponent(e.id)}?step=${s}" /><button type="submit" class="${p}">${u}</button></form>`,n="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400",i=html`
|
|
636
|
+
</div>`}`,g=html`<script src="/static/relative-time.js"></script>`;return P({title:"Courses \u2014 devcoach",currentPath:"/courses",uiTheme:t.uiTheme,body:e,scripts:g})}function Ze(t){let e=t.course,g=e.steps.filter(s=>s.status!=="todo").length,d=e.steps.find(s=>s.position===t.selected)??e.steps[0],r=`/courses/${encodeURIComponent(e.id)}/index.html${d?`#${d.anchor}`:""}`,o=(s,c,u,p)=>html`<form method="post" action="/courses/${encodeURIComponent(e.id)}/steps/${s}" class="inline"><input type="hidden" name="status" value="${c}" /><input type="hidden" name="next" value="/courses/${encodeURIComponent(e.id)}?step=${s}" /><button type="submit" class="${p}">${u}</button></form>`,n="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400",i=html`
|
|
654
637
|
<div x-data="{ confirmOpen: false }" @keydown.escape.window="confirmOpen = false">
|
|
655
638
|
<div class="sticky top-0 z-20 -mx-4 sm:-mx-6 -mt-8 mb-5 px-4 sm:px-6 h-12 flex items-center gap-3 bg-gray-50/95 dark:bg-gray-950/95 backdrop-blur border-b border-gray-200 dark:border-gray-800">
|
|
656
639
|
<a href="/courses" title="Back to courses" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition shrink-0">←</a>
|
|
657
640
|
<span class="text-sm font-semibold text-gray-900 dark:text-white truncate min-w-0">${e.title}</span>
|
|
658
|
-
${
|
|
641
|
+
${Ve(e.status)}
|
|
659
642
|
<span class="text-xs text-gray-500 dark:text-gray-400 shrink-0">${g}/${e.steps.length} steps</span>
|
|
660
|
-
${
|
|
643
|
+
${J(html`<button type="button" @click="confirmOpen = true; open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 hover:!bg-rose-50 dark:hover:!bg-rose-900/30 hover:text-rose-700 dark:hover:text-rose-300"><span class="w-4 h-4 inline-flex items-center justify-center shrink-0 text-[13px] leading-none" aria-hidden="true">🗑</span>Delete course…</button>`,"ml-auto shrink-0")}
|
|
661
644
|
</div>
|
|
662
645
|
<div class="grid grid-cols-1 lg:grid-cols-[15rem_minmax(0,1fr)] gap-6 lg:gap-8">
|
|
663
646
|
<aside id="course-steps" class="lg:sticky lg:top-16 self-start">
|
|
664
647
|
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 px-2 mb-2">Steps</p>
|
|
665
648
|
${e.steps.length?html`<ol class="space-y-0.5">${e.steps.map(s=>html`<li data-anchor="${s.anchor}" class="rounded-lg px-2 py-1.5 ${d&&s.position===d.position?"bg-indigo-50 dark:bg-indigo-900/30":""}">
|
|
666
649
|
<a href="/courses/${encodeURIComponent(e.id)}?step=${s.position}" data-anchor="${s.anchor}" class="flex items-start gap-2 text-sm">
|
|
667
|
-
<span class="${s.status==="done"?"text-green-500":s.status==="skipped"?"text-gray-400":"text-indigo-400"}">${
|
|
650
|
+
<span class="${s.status==="done"?"text-green-500":s.status==="skipped"?"text-gray-400":"text-indigo-400"}">${nt[s.status]??"\u25CB"}</span>
|
|
668
651
|
<span class="min-w-0 flex-1"><span class="block text-gray-800 dark:text-gray-100">${s.position}. ${s.title}</span><span class="block text-[11px] text-gray-400 dark:text-gray-500">${s.kind}${s.status!=="todo"?` \xB7 ${s.status}`:""}</span></span>
|
|
669
652
|
</a>
|
|
670
653
|
<div data-step-actions class="flex flex-wrap gap-1.5 mt-2 pl-6 ${d&&s.position===d.position?"":"hidden"}">
|
|
@@ -676,7 +659,7 @@ ${r.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
|
|
|
676
659
|
<summary class="cursor-pointer font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 select-none">About this course</summary>
|
|
677
660
|
<div class="mt-2 space-y-2">
|
|
678
661
|
<p>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></p>
|
|
679
|
-
${e.lesson_id?html`<p>from <a href="/lessons/${encodeURIComponent(e.lesson_id)}" class="text-gray-700 dark:text-gray-200 hover:text-indigo-500 hover:underline">${
|
|
662
|
+
${e.lesson_id?html`<p>from <a href="/lessons/${encodeURIComponent(e.lesson_id)}" class="text-gray-700 dark:text-gray-200 hover:text-indigo-500 hover:underline">${t.lessonTitle??e.lesson_id}</a></p>`:""}
|
|
680
663
|
${e.goal?html`<p class="text-gray-600 dark:text-gray-300">${e.goal}</p>`:""}
|
|
681
664
|
${e.prerequisites.length?html`<div class="flex flex-wrap items-center gap-1" title="The prerequisite chain explored before writing the course">
|
|
682
665
|
${e.prerequisites.map(s=>html`<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full border ${s.known?"bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-200 dark:border-green-800":"bg-rose-50 dark:bg-rose-900/20 text-rose-700 dark:text-rose-300 border-rose-200 dark:border-rose-800"}">${s.known?"\u2713":"\u2717"} ${s.concept}</span>`)}
|
|
@@ -685,19 +668,19 @@ ${r.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
|
|
|
685
668
|
</details>
|
|
686
669
|
</aside>
|
|
687
670
|
<div class="min-w-0">
|
|
688
|
-
${
|
|
671
|
+
${t.hasDocument?html`<iframe id="course-frame" src="${r}" title="${e.title}" sandbox="allow-scripts allow-forms" referrerpolicy="no-referrer" scrolling="no" class="w-full block min-h-[40vh]"></iframe>`:html`<div class="p-8 text-center text-sm text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl"><p class="text-3xl mb-3">✍️</p>The course document hasn't been written yet. Ask your agent to continue the course — it writes <span class="font-mono">index.html</span> in the course folder, and it appears here.</div>`}
|
|
689
672
|
</div>
|
|
690
673
|
</div>
|
|
691
|
-
${
|
|
674
|
+
${Y("confirmOpen","confirmOpen = false",html`Delete this course?`,html`<p class="text-gray-700 dark:text-gray-200 font-medium">“${e.title}”</p>
|
|
692
675
|
<p>The course, its steps and its document folder will be removed. This cannot be undone.${e.lesson_id?" The lesson it grew from stays.":""}</p>`,html`<button type="button" @click="confirmOpen = false" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">Cancel</button>
|
|
693
676
|
<form method="post" action="/courses/delete" class="contents">
|
|
694
677
|
<input type="hidden" name="id" value="${e.id}" />
|
|
695
678
|
<button type="submit" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-rose-600 text-white border-rose-600 hover:bg-rose-500">🗑 Delete course</button>
|
|
696
679
|
</form>`)}
|
|
697
|
-
</div>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/courses",uiTheme:
|
|
680
|
+
</div>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/courses",uiTheme:t.uiTheme,body:i,scripts:html`<script src="/static/course-viewer.js"></script>`,wide:true})}function Qe(t){let e=t.settings.min_gap_minutes,g=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],d=[["auto","Auto","\u{1F3E0}"],["lessons","Lessons","\u{1F4DA}"],["knowledge","Knowledge map","\u{1F9ED}"]],r=t.flash,o=html`
|
|
698
681
|
<h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
|
|
699
|
-
${
|
|
700
|
-
${
|
|
682
|
+
${r?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${r.invalid?"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-700 text-yellow-800 dark:text-yellow-300":"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300"}">
|
|
683
|
+
${r.imported} lesson${r.imported!==1?"s":""} imported.${r.skipped?` ${r.skipped} skipped (already in DB).`:""}${r.invalid?` ${r.invalid} rejected (failed validation).`:""}${r.groups?` ${r.groups} group${r.groups!==1?"s":""} added.`:""}${r.notebook?" Notebook restored.":""}
|
|
701
684
|
</div>`:""}
|
|
702
685
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
703
686
|
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
|
|
@@ -706,13 +689,13 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
|
|
|
706
689
|
<div class="grid grid-cols-2 gap-4">
|
|
707
690
|
<div>
|
|
708
691
|
<label for="max-per-day" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Max lessons per day</label>
|
|
709
|
-
<input id="max-per-day" type="number" name="max_per_day" min="1" max="20" value="${
|
|
692
|
+
<input id="max-per-day" type="number" name="max_per_day" min="1" max="20" value="${t.settings.max_per_day}" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500" />
|
|
710
693
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Lessons in a 24h window.</p>
|
|
711
694
|
</div>
|
|
712
695
|
<div>
|
|
713
696
|
<label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
|
|
714
697
|
<select id="min-gap-minutes" name="min_gap_minutes" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500">
|
|
715
|
-
${
|
|
698
|
+
${st.map(([i,s])=>html`<option value="${i}" ${e===i?"selected":""}>${s}</option>`)}
|
|
716
699
|
</select>
|
|
717
700
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
|
|
718
701
|
</div>
|
|
@@ -720,28 +703,28 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
|
|
|
720
703
|
<div class="grid grid-cols-2 gap-4">
|
|
721
704
|
<div>
|
|
722
705
|
<label for="nudge-every" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Interactions between lessons</label>
|
|
723
|
-
<input id="nudge-every" type="number" name="nudge_every" min="0" max="1000" value="${
|
|
706
|
+
<input id="nudge-every" type="number" name="nudge_every" min="0" max="1000" value="${t.settings.nudge_every}" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500" />
|
|
724
707
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Cue at most once every N (0 = every turn).</p>
|
|
725
708
|
</div>
|
|
726
709
|
<div>
|
|
727
710
|
<label for="nudge-scope" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Count interactions</label>
|
|
728
711
|
<select id="nudge-scope" name="nudge_scope" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500">
|
|
729
|
-
<option value="session" ${
|
|
730
|
-
<option value="global" ${
|
|
712
|
+
<option value="session" ${t.settings.nudge_scope==="session"?"selected":""}>Per chat session</option>
|
|
713
|
+
<option value="global" ${t.settings.nudge_scope==="global"?"selected":""}>Globally</option>
|
|
731
714
|
</select>
|
|
732
715
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Per-chat or across all sessions.</p>
|
|
733
716
|
</div>
|
|
734
717
|
</div>
|
|
735
718
|
<div>
|
|
736
719
|
<label for="share-name" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Your name (for sharing)</label>
|
|
737
|
-
<input id="share-name" type="text" name="share_name" maxlength="80" value="${
|
|
720
|
+
<input id="share-name" type="text" name="share_name" maxlength="80" value="${t.settings.share_name??""}" placeholder="git user.name when empty" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500" />
|
|
738
721
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Proposed as the sender when you share a lesson. Empty → your git name.</p>
|
|
739
722
|
</div>
|
|
740
723
|
<fieldset class="border-0 p-0 m-0">
|
|
741
724
|
<legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">UI theme</legend>
|
|
742
725
|
<div class="flex gap-2">
|
|
743
726
|
${g.map(([i,s,c])=>html`<label class="flex-1 cursor-pointer">
|
|
744
|
-
<input type="radio" name="ui_theme" value="${i}" ${
|
|
727
|
+
<input type="radio" name="ui_theme" value="${i}" ${t.settings.ui_theme===i?"checked":""} class="sr-only peer" />
|
|
745
728
|
<span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${c}</span>${s}</span>
|
|
746
729
|
</label>`)}
|
|
747
730
|
</div>
|
|
@@ -751,7 +734,7 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
|
|
|
751
734
|
<legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Home page</legend>
|
|
752
735
|
<div class="flex gap-2">
|
|
753
736
|
${d.map(([i,s,c])=>html`<label class="flex-1 cursor-pointer">
|
|
754
|
-
<input type="radio" name="ui_home" value="${i}" ${
|
|
737
|
+
<input type="radio" name="ui_home" value="${i}" ${t.settings.ui_home===i?"checked":""} class="sr-only peer" />
|
|
755
738
|
<span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${c}</span>${s}</span>
|
|
756
739
|
</label>`)}
|
|
757
740
|
</div>
|
|
@@ -804,11 +787,11 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
|
|
|
804
787
|
<button type="button" @click="nbMode = 'source'" :class="nbMode === 'source' ? 'bg-indigo-600 text-white' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'" class="px-3 py-1.5 transition border-l border-gray-200 dark:border-gray-700">Source</button>
|
|
805
788
|
</div>
|
|
806
789
|
<a href="/settings/notebook/download" class="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-3 py-1.5 text-xs transition">↓ Download</a>
|
|
807
|
-
<a href="vscode://file/${
|
|
790
|
+
<a href="vscode://file/${t.notebookPath}" title="Open in VS Code" class="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-3 py-1.5 text-xs transition"><img src="/static/vscode.svg" class="w-4 h-4" alt="VS Code" />Open in VS Code</a>
|
|
808
791
|
</div>
|
|
809
792
|
</div>
|
|
810
793
|
<div x-show="nbMode === 'preview'" id="notebook-preview-settings" class="markdown-body min-h-24"></div>
|
|
811
|
-
<pre x-show="nbMode === 'source'" style="display:none" class="text-xs font-mono bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded p-3 overflow-x-auto whitespace-pre-wrap text-gray-700 dark:text-gray-200 min-h-24">${
|
|
794
|
+
<pre x-show="nbMode === 'source'" style="display:none" class="text-xs font-mono bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded p-3 overflow-x-auto whitespace-pre-wrap text-gray-700 dark:text-gray-200 min-h-24">${t.notebookContent}</pre>
|
|
812
795
|
</div>
|
|
813
796
|
</div>`,n=html`<script src="/static/vendor/marked.min.js"></script>
|
|
814
797
|
<script src="/static/vendor/purify.min.js"></script>
|
|
@@ -819,10 +802,10 @@ function updateLabel(inputId, labelId, submitId) {
|
|
|
819
802
|
document.getElementById(submitId).disabled = !input.files.length;
|
|
820
803
|
}
|
|
821
804
|
marked.setOptions({ breaks: true, gfm: true });
|
|
822
|
-
document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(
|
|
823
|
-
</script>`;return P({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:
|
|
805
|
+
document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(t.notebookContent))}));
|
|
806
|
+
</script>`;return P({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:o,scripts:n})}var Z=25;function B(t,e="/lessons"){return t?.startsWith("/")&&!t.startsWith("//")?t:e}function y(t,e,g=""){let d=t[e];return typeof d=="string"?d:g}var Q=dirname(fileURLToPath(import.meta.url)),X=[join(Q,"../assets/static"),join(Q,"../../assets/static")].find(t=>existsSync(t))??join(Q,"../assets/static"),ut={".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".woff2":"font/woff2",".woff":"font/woff",".ico":"image/x-icon"},mt="https://ultimaphoenix.github.io",Xe={"access-control-allow-origin":mt,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function L(t){let e=t.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var ee;function xt(){return ee===void 0&&(ee=n$1()),ee}function et(t,e,g){let d=ka({explicit:g.name??null,setting:e,gitUserName:xt()}),r=g.includeContext??false,o=ja(t,{includeContext:r,sharedBy:d}),n=la(o);return {payload:o,state:{id:t.id,open:g.open??false,name:d??"",includeContext:r,text:na(o,n),link:oa(o,n)}}}function yt(t,e){return t==="lessons"?"/lessons":t==="knowledge"?"/knowledge":e?"/lessons":"/knowledge"}function bt(){try{return u(t=>N(t).ui_theme)}catch{return "system"}}function ht(t={}){let e$1=new Hono;e$1.get("/static/*",r=>{let o=decodeURIComponent(r.req.path.slice(8)),n=join(X,o);if(!n.startsWith(X))return r.notFound();try{let i=readFileSync(n);return new Response(new Uint8Array(i),{headers:{"content-type":ut[extname(n)]??"application/octet-stream","cache-control":"no-cache"}})}catch{return r.notFound()}}),e$1.get("/ping",r=>r.json({ok:true,version:ha},200,Xe)),e$1.options("/ping",r=>r.body(null,204,Xe)),e$1.post("/shutdown",r=>L(r)?r.text("Forbidden",403):(setTimeout(()=>t.onShutdown?.(),50),r.json({ok:true}))),e$1.get("/shutdown",r=>r.text("Method Not Allowed",405)),e$1.get("/",r=>{let{home:o,hasLessons:n}=u(i=>({home:N(i).ui_home,hasLessons:w(i)>0}));return r.redirect(yt(o,n),302)}),e$1.get("/knowledge",r=>{let{profile:o,stats:n,rateLimit:i,settings:s}=u(p=>({profile:ta(p),stats:wa(p),rateLimit:sa(p),settings:N(p)})),c=new Map;for(let p of o.groups)for(let m of p.topics)c.set(m,p.name);let u$1={};for(let p of o.groups)u$1[p.name]=[];for(let p of o.knowledge){let m=c.get(p.topic)??"Other",v=u$1[m]??[];v.push(p),u$1[m]=v;}return r.html(ze({categorised:u$1,allGroups:o.groups.map(p=>p.name),stats:n,rateLimit:i,maxPerDay:s.max_per_day,uiTheme:s.ui_theme}))}),e$1.post("/knowledge",async r=>{let o=await r.req.parseBody(),n=y(o,"topic").trim();if(n){let i=Number.parseInt(y(o,"confidence","5"),10)||5,s=y(o,"group").trim();u(c=>{H(c,n,i),s&&s!=="Other"&&L$1(c,n,s);});}return r.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic/delete",r=>(u(o=>I(o,r.req.param("topic"))),r.redirect("/knowledge",303))),e$1.post("/knowledge/:topic/group",async r=>{let o=y(await r.req.parseBody(),"group").trim(),n=r.req.param("topic");return u(i=>{o&&o!=="Other"?L$1(i,n,o):M(i,n);}),r.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic",async r=>{let o=Number.parseInt(y(await r.req.parseBody(),"delta","0"),10)||0;return u(n=>ua(n,r.req.param("topic"),o)),r.redirect("/knowledge",303)}),e$1.post("/groups",async r=>{let o=y(await r.req.parseBody(),"group_name").trim();return o&&o!=="Other"&&u(n=>J$1(n,o)),r.redirect("/knowledge",303)}),e$1.post("/groups/:group_name/delete",r=>(u(o=>K$1(o,r.req.param("group_name"))),r.redirect("/knowledge",303))),e$1.get("/lessons/export",()=>{let r=u(o=>B$1(o));return new Response(JSON.stringify(r,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e$1.post("/lessons/import",async r=>{let o=await r.req.parseBody(),n=y(o,"from")==="lessons",i=p=>r.redirect(n?`/lessons?import=1&error=${p}`:"/settings?imported=0&skipped=0&invalid=1",303);if(L(r))return i("cross");let s=o.file,u$1=(s instanceof File&&s.size>0?await s.text():null)??y(o,"text").trim();try{p(u$1)&&!u$1.includes("#devcoach:lesson:")&&(u$1=await q(u$1));let p$1=u(m=>xa(m,u$1));return p$1.kind==="lessons"||!p$1.lesson?r.redirect(`/settings?imported=${p$1.inserted}&skipped=${p$1.duplicated}&invalid=${p$1.invalid}`,303):r.redirect(`/lessons/${encodeURIComponent(p$1.lesson.id)}?imported=${p$1.inserted?"1":"dup"}`,303)}catch(p){if(p instanceof ia)return i("invalid");throw p}}),e$1.get("/lessons/import",r=>{let o=(r.req.query("code")??"").trim(),n=null,i=null;if(o)try{n=ma(o);}catch(s){i=s instanceof ia?s.message:"That code could not be read.";}return r.html(Ye({code:o,preview:n,error:i,uiTheme:bt()}))}),e$1.get("/lessons",r=>{let o=r.req.query(),n=new Set(["junior","mid","senior"]),i=o.level&&n.has(o.level)?o.level:"",s=o.date_from||"",c=o.date_to||"",u$1=o.starred==="1",p=o.imported==="1"?"1":o.imported==="0"?"0":"",m=o.shared_by||null,v=o.sort||"timestamp",I=o.order==="asc"?"asc":"desc",R=Math.max(1,Number.parseInt(o.page??"1",10)||1),E=o.period||"all",l={period:s||c?null:E!=="all"?E:null,category:o.category||null,level:i||null,project:o.project||null,repository:o.repository||null,branch:o.branch||null,commit:o.commit||null,starred:u$1?true:null,imported:m||p==="1"?true:p==="0"?false:null,shared_by:m,search:o.search||null,feedback:o.feedback||null,date_from:s||null,date_to:c||null},x$1=u(k=>{let re=w(k,l),oe=Math.max(1,Math.ceil(re/Z));return R=Math.min(R,oe),{total:re,totalPages:oe,lessons:x(k,{...l,sort:v,order:I,page:R,per_page:Z}),allCategories:F(k),allProjects:C(k,"project"),allRepositories:C(k,"repository"),allBranches:C(k,"branch"),allCommits:C(k,"commit_hash"),allSharedBy:D(k),theme:N(k).ui_theme}}),$={period:E,category:o.category||"",level:i,project:o.project||"",repository:o.repository||"",branch:o.branch||"",commit:o.commit||"",starred:u$1,imported:m?"1":p,shared_by:m??"",search:o.search||"",feedback:o.feedback||"",date_from:s,date_to:c,sort:v,order:I},S=o.error==="invalid"||o.error==="cross"?o.error:null;return r.html(Ke({importOpen:o.import==="1",importError:S,lessons:x$1.lessons,allCategories:x$1.allCategories,allProjects:x$1.allProjects,allRepositories:x$1.allRepositories,allBranches:x$1.allBranches,allCommits:x$1.allCommits,allSharedBy:x$1.allSharedBy,s:$,page:R,perPage:Z,total:x$1.total,totalPages:x$1.totalPages,uiTheme:x$1.theme}))}),e$1.post("/lessons/:lesson_id/star",async r=>{let o=await r.req.parseBody();return u(n=>A(n,r.req.param("lesson_id"),o.starred==="1")),r.redirect(B(y(o,"next")||void 0),303)}),e$1.post("/lessons/:lesson_id/feedback",async r=>{let o=await r.req.parseBody(),n=y(o,"feedback");if(!["","clear","know","understood","dont_know"].includes(n))return r.text("Unknown feedback value",400);let i=n===""||n==="clear"?null:n;return u(s=>va(s,r.req.param("lesson_id"),i)),r.redirect(B(y(o,"next")||void 0),303)}),e$1.post("/lessons/delete",async r=>{if(L(r))return r.text("Forbidden",403);let o=await r.req.parseBody({all:true}),n=o.id,i=(Array.isArray(n)?n:[n]).filter(s=>typeof s=="string"&&s!=="");return u(s=>z(s,i)),r.redirect(B(y(o,"next")||void 0),303)}),e$1.on(["GET","POST"],"/lessons/:lesson_id/share",async r=>{if(L(r))return r.text("Forbidden",403);let o,n,i=false;if(r.req.method==="POST"){let m=await r.req.parseBody();o=y(m,"name"),n=y(m,"include_context")==="1",i=y(m,"persist")==="1";}else o=r.req.query("name"),n=r.req.query("include_context")==="1";let s$1=u(m=>{let v=E(m,r.req.param("lesson_id")??"");return v?(i&&o!==void 0&&O(m,"share_name",s(o)),{lesson:v,settingName:N(m).share_name}):null});if(!s$1)return r.text("Lesson not found",404);let{state:c,payload:u$1}=et(s$1.lesson,s$1.settingName,{name:o,includeContext:n}),p=r.req.query("format");return p==="text"?r.text(c.text):p==="link"?r.text(c.link):p==="md"?new Response(ra(u$1),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${qa(u$1)}"`}}):p==="panel"?r.html(W(s$1.lesson.id,c,s$1.lesson.title)):r.html(V(c))}),e$1.get("/lessons/:lesson_id",r=>{let o=u(s=>{let c=E(s,r.req.param("lesson_id"));if(!c)return null;let u=N(s),p=h$1(s,{lesson_id:c.id})[0],m$1=p?{id:p.id,title:p.title,status:p.status,...m(p)}:null;return {lesson:c,course:m$1,settingName:u.share_name,uiTheme:u.ui_theme}});if(!o)return r.html("<h1>Lesson not found</h1>",404);let n=r.req.query(),i=n.imported==="1"?"new":n.imported==="dup"?"dup":null;return r.html(Je({lesson:o.lesson,uiTheme:o.uiTheme,share:et(o.lesson,o.settingName,{open:n.share==="1"}).state,imported:i,course:o.course}))});let g$1={"content-type":"text/html; charset=utf-8","content-security-policy":"sandbox allow-scripts allow-forms; default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline'; img-src data:; font-src data:; form-action 'none'; base-uri 'none'; frame-ancestors 'self'","referrer-policy":"no-referrer","x-content-type-options":"nosniff","cache-control":"no-cache"},d=(()=>{try{return `
|
|
824
807
|
<script>${readFileSync(join(X,"course-frame.js"),"utf8")}</script>
|
|
825
|
-
`}catch{return ""}})();return e$1.get("/courses",
|
|
826
|
-
${a$
|
|
827
|
-
${a$
|
|
828
|
-
export{
|
|
808
|
+
`}catch{return ""}})();return e$1.get("/courses",r=>{let o=u(n=>({courses:h$1(n).map(i=>({...i,lesson_title:i.lesson_id?E(n,i.lesson_id)?.title??null:null})),uiTheme:N(n).ui_theme}));return r.html(We(o))}),e$1.get("/courses/:course_id/index.html",r=>{let o=r.req.param("course_id");if(!a.test(o))return r.notFound();let n=e(o);if(!n)return r.notFound();try{let i=Buffer.concat([readFileSync(n),Buffer.from(d)]);return new Response(new Uint8Array(i),{headers:new Headers(g$1)})}catch{return r.notFound()}}),e$1.get("/courses/:course_id",r=>{let o=r.req.param("course_id"),n=u(c=>{let u=g(c,o);return u?{course:u,lessonTitle:u.lesson_id?E(c,u.lesson_id)?.title??null:null,uiTheme:N(c).ui_theme}:null});if(!n)return r.html("<h1>Course not found</h1>",404);let i=n.course.steps.find(c=>c.status==="todo")?.position??1,s=Number.parseInt(r.req.query("step")??"",10)||i;return r.html(Ze({...n,selected:s,hasDocument:e(o)!==null}))}),e$1.post("/courses/:course_id/steps/:position",async r=>{if(L(r))return r.text("Forbidden",403);let o=await r.req.parseBody(),n=y(o,"status");if(!["todo","done","skipped"].includes(n))return r.text("Unknown status",400);let i=Number.parseInt(r.req.param("position"),10),s=r.req.param("course_id");return u(u=>Number.isFinite(i)?j$1(u,s,i,n):null)?r.redirect(B(y(o,"next")||void 0,`/courses/${encodeURIComponent(s)}`),303):r.text("Step not found",404)}),e$1.post("/courses/delete",async r=>{if(L(r))return r.text("Forbidden",403);let o=await r.req.parseBody();return u(n=>l(n,y(o,"id"))),r.redirect("/courses",303)}),e$1.get("/settings/export",()=>{let r=u(o=>ea(o));return new Response(new Uint8Array(r),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e$1.post("/settings/import",async r=>{let o=(await r.req.parseBody()).file;if(!(o instanceof File))return r.redirect("/settings?imported=0&skipped=0&invalid=1",303);let n=new Uint8Array(await o.arrayBuffer()),i=u(s=>ga(s,n));return r.redirect(`/settings?imported=${i.lessons}&skipped=${i.skipped}&invalid=${i.invalid}&groups=${i.groups}¬ebook=${i.learning_state}`,303)}),e$1.get("/settings/notebook/download",()=>{let r=existsSync(n)?readFileSync(n,"utf8"):"";return new Response(r,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e$1.post("/settings/notebook",async r=>{let o=await r.req.parseBody();return mkdirSync(dirname(n),{recursive:true}),writeFileSync(n,y(o,"content"),"utf8"),r.redirect(B(y(o,"next")||void 0,"/settings?notebook_saved=1"),303)}),e$1.post("/settings",async r=>{let o=await r.req.parseBody(),n=Number.parseInt(y(o,"max_per_day","2"),10)||2,i=Number.parseInt(y(o,"min_gap_minutes","240"),10),s$1=y(o,"ui_theme","system");["system","dark","light"].includes(s$1)||(s$1="system");let c=y(o,"ui_home","auto");["auto","lessons","knowledge"].includes(c)||(c="auto");let u$1=Math.max(0,Number.parseInt(y(o,"nudge_every","10"),10)||0),p=y(o,"nudge_scope","session");return p!=="session"&&p!=="global"&&(p="session"),u(m=>{O(m,"max_per_day",String(n)),O(m,"min_gap_minutes",String(Number.isNaN(i)?240:i)),O(m,"ui_theme",s$1),O(m,"ui_home",c),O(m,"nudge_every",String(u$1)),O(m,"nudge_scope",p),O(m,"share_name",s(y(o,"share_name")));}),r.redirect("/settings",303)}),e$1.get("/settings",r=>{let o=u(c=>N(c)),n$1=existsSync(n)?readFileSync(n,"utf8"):"",i=r.req.query(),s=i.imported!==void 0?{imported:Number(i.imported??0),skipped:Number(i.skipped??0),invalid:Number(i.invalid??0),groups:Number(i.groups??0),notebook:Number(i.notebook??0)}:null;return r.html(Qe({settings:o,notebookContent:n$1,notebookPath:n,uiTheme:o.ui_theme,flash:s}))}),e$1}function ft(t,e=2e3){let g=t;return new Promise(d=>{let r=setTimeout(()=>{g.closeAllConnections?.();},e),o=setInterval(()=>g.closeIdleConnections?.(),100);t.close(()=>{clearTimeout(r),clearInterval(o),d();}),g.closeIdleConnections?.();})}async function kt(t,e=fetch){try{let g=await e(`http://127.0.0.1:${t}/ping`,{signal:AbortSignal.timeout(1500)});if(!g.ok)return null;let d=await g.json();return d.ok===!0?{version:String(d.version??"")}:null}catch{return null}}async function vt(t,e,g=kt){if(e.code==="EADDRINUSE"){let d=await g(t);if(d){let r=`http://localhost:${t}`,o=d.version?` (v${d.version})`:"";return {existingUrl:r,exitCode:0,message:`${a$1.yellow("devcoach UI is already running at")} ${a$1.cyan(c(r))}${o}
|
|
809
|
+
${a$1.dim("Open it there, start another with --port <n>, or stop it with: devcoach ui --stop")}`}}return {exitCode:1,message:`${a$1.red(`\u2717 Port ${t} is already in use by another process`)}
|
|
810
|
+
${a$1.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a$1.red(`\u2717 Port ${t} is not allowed (permission denied) \u2014 use a port \u2265 1024`)}:{exitCode:1,message:a$1.red(`\u2717 Could not start the devcoach UI on port ${t}: ${e.message}`)}}function At(t,e={}){let g=false,d=(s,c)=>{if(g){c&&process.exit(130);return}g=true,process.stdout.isTTY&&console.log(a$1.dim(`devcoach UI stopping (${s})\u2026`)),ft(o).then(()=>{c&&process.exit(0);});},r=ht({onShutdown:()=>d("stop requested",false)}),o=serve({fetch:r.fetch,port:t,hostname:"127.0.0.1"},s=>{let c$1=`http://localhost:${s.port}`;console.log(`devcoach UI running at ${a$1.cyan(c(c$1))}`),process.stdout.isTTY&&console.log(a$1.dim("Press Ctrl+C to stop")),e.onReady?.(c$1);}),n=["SIGINT","SIGTERM","SIGHUP"],i={SIGINT:()=>d("SIGINT",true),SIGTERM:()=>d("SIGTERM",true),SIGHUP:()=>d("SIGHUP",true)};if(e.handleSignals!==false)for(let s of n)process.on(s,i[s]);return o.once("error",s=>{for(let c of n)process.off(c,i[c]);vt(t,s).then(c=>{if(e.onListenError){e.onListenError(c);return}console.error(c.message),process.exit(c.exitCode);});}),o}async function Dt(t,e=fetch){try{return (await e(`http://127.0.0.1:${t}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
|
|
811
|
+
export{ht as createApp,vt as explainListenError,ft as gracefulShutdown,kt as pingUi,yt as resolveHomePath,At as startUi,Dt as stopUi};
|