devcoach 2.3.1 → 2.4.0-next.575.g9c67058
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 +3 -2
- package/assets/references/sharing.md +2 -1
- package/assets/static/relative-time.js +78 -15
- package/assets/static/style.css +70 -0
- package/assets/static/table-resize.js +178 -0
- package/dist/{app-SVGKP5WG.js → app-RC2D4BDF.js} +233 -141
- package/dist/bin.js +1 -1
- package/dist/chunk-DGO6MIQO.js +15 -0
- package/dist/chunk-LSO57U2T.js +2 -0
- package/dist/chunk-TQI44CEB.js +75 -0
- package/dist/commands-YNDVJIEM.js +36 -0
- package/dist/{hooks-5YA5PLBW.js → hooks-5UZL55FA.js} +1 -1
- package/dist/server-NLJ7CK6T.js +9 -0
- package/package.json +1 -1
- package/dist/chunk-4PFP67XN.js +0 -2
- package/dist/chunk-JEFYE33I.js +0 -75
- package/dist/chunk-UHF6ARVR.js +0 -15
- package/dist/commands-ECVEHTRX.js +0 -36
- package/dist/server-SM63J4EC.js +0 -9
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-
|
|
2
|
+
import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-LSO57U2T.js';import {T as T$1,p,r,J as J$1,ba,fa,ca,D,H,E as E$1,I,da,F as F$1,G as G$1,x,ga,U,Y as Y$1,z as z$1,y as y$1,B as B$1,s,v,ea,u,A,K,n,aa,$,R,S,k,W,V as V$1,X,_,Z}from'./chunk-TQI44CEB.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 T(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function Le(t,e){return t+e.replace(/[^A-Za-z0-9_-]/g,o=>`_${(o.codePointAt(0)??0).toString(16)}`)}var Ge=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 Ee(t,e="",o=""){return html`<div class="relative ${e}" ${raw(o)} 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">${Ge}</button>
|
|
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
|
+
${t}
|
|
6
|
+
</div>
|
|
7
|
+
</div>`}function Re(t,e,o,r,s){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
|
+
<div class="absolute inset-0 bg-gray-900/50 dark:bg-black/60" @click="${e}"></div>
|
|
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
|
+
<div class="flex items-start gap-3">
|
|
11
|
+
<span class="w-9 h-9 rounded-full bg-rose-100 dark:bg-rose-900/40 text-rose-600 dark:text-rose-300 inline-flex items-center justify-center shrink-0" aria-hidden="true">🗑</span>
|
|
12
|
+
<div class="min-w-0 flex-1">
|
|
13
|
+
<h2 class="text-base font-semibold text-gray-900 dark:text-white">${o}</h2>
|
|
14
|
+
<div class="mt-1 text-sm text-gray-500 dark:text-gray-400 space-y-2">${r}</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="mt-5 flex justify-end gap-2">${s}</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>`}function E(t){let e=(o,r,s)=>html`<a href="${o}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition ${s?"text-gray-900 dark:text-white font-semibold":""}">${r}</a>`;return html`<!DOCTYPE html>
|
|
3
20
|
<html lang="en">
|
|
4
21
|
<head>
|
|
5
22
|
<meta charset="UTF-8" />
|
|
6
23
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>${
|
|
24
|
+
<title>${t.title}</title>
|
|
8
25
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
|
9
|
-
<meta name="devcoach-theme" content="${
|
|
26
|
+
<meta name="devcoach-theme" content="${t.uiTheme}" />
|
|
10
27
|
<script>
|
|
11
28
|
(function () {
|
|
12
29
|
var serverTheme = document.querySelector('meta[name="devcoach-theme"]').content;
|
|
@@ -21,7 +38,7 @@ import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-4
|
|
|
21
38
|
<script src="/static/vendor/htmx.min.js"></script>
|
|
22
39
|
<script src="/static/vendor/alpinejs.min.js" defer></script>
|
|
23
40
|
<link rel="stylesheet" href="/static/style.css" />
|
|
24
|
-
${
|
|
41
|
+
${t.head??""}
|
|
25
42
|
</head>
|
|
26
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">
|
|
27
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">
|
|
@@ -30,16 +47,15 @@ import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-4
|
|
|
30
47
|
<a href="/" class="font-extrabold text-lg tracking-tight text-gray-900 dark:text-gray-100 hover:opacity-80 transition"
|
|
31
48
|
>🎓 dev<span class="text-teal-600 dark:text-teal-300">coach</span></a
|
|
32
49
|
>
|
|
33
|
-
${e("/","Profile",
|
|
34
|
-
${e("/lessons","Lessons",
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<button id="theme-toggle" onclick="toggleTheme()"
|
|
38
|
-
class="
|
|
39
|
-
title="Toggle theme"></button>
|
|
50
|
+
${e("/knowledge","Profile",t.currentPath==="/knowledge")}
|
|
51
|
+
${e("/lessons","Lessons",t.currentPath.includes("/lessons"))}
|
|
52
|
+
<div class="ml-auto flex items-center gap-1.5">
|
|
53
|
+
<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>
|
|
54
|
+
<button id="theme-toggle" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme"
|
|
55
|
+
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>
|
|
40
56
|
</div>
|
|
41
57
|
</nav>
|
|
42
|
-
<main class="max-w-7xl mx-auto px-4 sm:px-6 py-8">${
|
|
58
|
+
<main class="max-w-7xl mx-auto px-4 sm:px-6 py-8">${t.body}</main>
|
|
43
59
|
<script>
|
|
44
60
|
function isDark() { return document.documentElement.classList.contains('dark'); }
|
|
45
61
|
function updateThemeIcon() { var b = document.getElementById('theme-toggle'); if (b) b.textContent = isDark() ? '☀️' : '🌙'; }
|
|
@@ -58,9 +74,9 @@ import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-4
|
|
|
58
74
|
})();
|
|
59
75
|
updateThemeIcon(); updateHljsTheme();
|
|
60
76
|
</script>
|
|
61
|
-
${
|
|
77
|
+
${t.scripts??""}
|
|
62
78
|
</body>
|
|
63
|
-
</html>`}function
|
|
79
|
+
</html>`}function Me(t){let e=r=>t.allGroups.filter(s=>s!=="Other").map(s=>html`<option value="${s}" ${s===r?"selected":""}>${s}</option>`),o=html`
|
|
64
80
|
<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); } }">
|
|
65
81
|
|
|
66
82
|
<div class="flex items-center justify-between mb-6">
|
|
@@ -110,21 +126,21 @@ import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-4
|
|
|
110
126
|
</div>
|
|
111
127
|
|
|
112
128
|
<div class="mb-6 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400">
|
|
113
|
-
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(
|
|
129
|
+
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.total_lessons??0)}</span> lessons total</span>
|
|
114
130
|
<span class="text-gray-300 dark:text-gray-600">·</span>
|
|
115
|
-
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(
|
|
131
|
+
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.lessons_today??0)}</span> / ${t.maxPerDay} today</span>
|
|
116
132
|
<span class="text-gray-300 dark:text-gray-600">·</span>
|
|
117
|
-
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(
|
|
133
|
+
<span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.lessons_this_week??0)}</span> this week</span>
|
|
118
134
|
<span class="text-gray-300 dark:text-gray-600">·</span>
|
|
119
|
-
${
|
|
135
|
+
${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>`}
|
|
120
136
|
</div>
|
|
121
137
|
|
|
122
|
-
${Object.entries(
|
|
138
|
+
${Object.entries(t.categorised).map(([r,s])=>html`
|
|
123
139
|
<section class="mb-6 group/section">
|
|
124
140
|
<div class="flex items-center gap-2 mb-2 border-b border-gray-200 dark:border-gray-800 pb-1">
|
|
125
141
|
<div class="flex items-center gap-1 flex-1 min-w-0">
|
|
126
|
-
<h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 shrink-0">${
|
|
127
|
-
${
|
|
142
|
+
<h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 shrink-0">${r}</h2>
|
|
143
|
+
${r!=="Other"?html`<form x-show="editMode" style="display:none" method="post" action="/groups/${encodeURIComponent(r)}/delete" hx-post="/groups/${encodeURIComponent(r)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove group "${r}"? Topics will move to Other.">
|
|
128
144
|
<button type="submit" class="text-xs text-gray-300 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 transition w-4 text-center opacity-0 group-hover/section:opacity-100" title="Delete group">×</button>
|
|
129
145
|
</form>`:""}
|
|
130
146
|
</div>
|
|
@@ -132,7 +148,7 @@ ${Object.entries(r.categorised).map(([t,s])=>html`
|
|
|
132
148
|
<button type="button" @click="open = !open" class="text-xs text-gray-300 dark:text-gray-600 hover:text-indigo-500 dark:hover:text-indigo-400 transition px-1">+ topic</button>
|
|
133
149
|
<div x-show="open" @click.outside="open = false" 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 p-3 w-64" style="display:none">
|
|
134
150
|
<form method="post" action="/knowledge" hx-post="/knowledge" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" class="space-y-2">
|
|
135
|
-
<input type="hidden" name="group" value="${
|
|
151
|
+
<input type="hidden" name="group" value="${r!=="Other"?r:""}" />
|
|
136
152
|
<input type="text" name="topic" placeholder="topic_id (e.g. rust_lifetimes)" required class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
|
137
153
|
<div class="flex items-center gap-2">
|
|
138
154
|
<label class="text-xs text-gray-400 dark:text-gray-500 shrink-0">Confidence</label>
|
|
@@ -144,15 +160,15 @@ ${Object.entries(r.categorised).map(([t,s])=>html`
|
|
|
144
160
|
</div>
|
|
145
161
|
</div>
|
|
146
162
|
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-0">
|
|
147
|
-
${s.map(n=>{let l=
|
|
163
|
+
${s.map(n=>{let l=Le("topic-row-",n.topic),g=n.confidence,c=Math.trunc(g/10*100),i=g>=7?"bg-green-500":g>=4?"bg-yellow-500":"bg-red-500",p=g>=7?"text-green-600 dark:text-green-400":g>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
|
|
148
164
|
<div id="${l}" class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
|
|
149
165
|
<div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative shrink-0 flex items-center">
|
|
150
166
|
<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>
|
|
151
167
|
<div x-show="open" @click.outside="open = false" 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 p-1 w-40" style="display:none">
|
|
152
168
|
<form method="post" action="/knowledge/${encodeURIComponent(n.topic)}/group" hx-post="/knowledge/${encodeURIComponent(n.topic)}/group" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML">
|
|
153
169
|
<select name="group" onchange="this.form.requestSubmit()" class="w-full text-xs bg-transparent text-gray-700 dark:text-gray-200 px-2 py-1 focus:outline-none">
|
|
154
|
-
<option value="Other" ${
|
|
155
|
-
${e(
|
|
170
|
+
<option value="Other" ${r==="Other"?"selected":""}>Other (ungrouped)</option>
|
|
171
|
+
${e(r)}
|
|
156
172
|
</select>
|
|
157
173
|
</form>
|
|
158
174
|
</div>
|
|
@@ -169,7 +185,7 @@ ${Object.entries(r.categorised).map(([t,s])=>html`
|
|
|
169
185
|
<button class="text-xs text-gray-300 dark:text-gray-600 hover:text-red-500 dark:hover:text-red-400 transition w-5 text-center leading-none">−</button>
|
|
170
186
|
</form>
|
|
171
187
|
<div class="w-20 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
|
|
172
|
-
<div class="${
|
|
188
|
+
<div class="${i} h-full rounded-full transition-all" style="width: ${c}%"></div>
|
|
173
189
|
</div>
|
|
174
190
|
<form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(n.topic)}" hx-post="/knowledge/${encodeURIComponent(n.topic)}" hx-target="#${l}" hx-select="#${l}" hx-swap="outerHTML" class="shrink-0 flex items-center">
|
|
175
191
|
<input type="hidden" name="delta" value="1" />
|
|
@@ -179,10 +195,10 @@ ${Object.entries(r.categorised).map(([t,s])=>html`
|
|
|
179
195
|
</div>`})}
|
|
180
196
|
</div>
|
|
181
197
|
</section>`)}
|
|
182
|
-
</div>`;return
|
|
183
|
-
<a href="${
|
|
184
|
-
${
|
|
185
|
-
</a></th>`},
|
|
198
|
+
</div>`;return E({title:"Profile \u2014 devcoach",currentPath:"/knowledge",uiTheme:t.uiTheme,body:o})}var Te={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},je={know:"\u2713 Known",dont_know:"\u2717 Don't know",none:"\u2014 No response"},Pe={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function y(t,e={}){let o={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&&(o.starred="1"),Object.assign(o,e);let r=new URLSearchParams;for(let[s,n]of Object.entries(o))n&&r.set(s,n);return `?${r.toString()}`}function Be(t){let e=t.s,o=!!(e.date_from||e.date_to),r=e.period!=="all"&&!o||o||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.imported||e.shared_by||e.search||e.feedback),s=o?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:Te[e.period]??"All time",n=e.feedback?je[e.feedback]??"Feedback":"Feedback",l=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",g=[["","","All lessons"],["0","","\u{1F464} My own"],["1","","\u{1F91D} Shared with me"],...t.allSharedBy.map(d=>["1",d,`\u{1F91D} from ${d}`])],c={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"},i=(d,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">${d}<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>`,p=(d,x,b="")=>{let w=e.sort===x,j=w&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap relative overflow-hidden text-ellipsis ${b}">
|
|
199
|
+
<a href="${y(e,{sort:x,order:j})}" class="hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${d}
|
|
200
|
+
${w?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>`}
|
|
201
|
+
</a></th>`},h=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>`,P=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
|
|
186
202
|
<style>
|
|
187
203
|
.dark .flatpickr-calendar { background:#1f2937; border-color:#374151; }
|
|
188
204
|
.dark .flatpickr-day { color:#e5e7eb; }
|
|
@@ -193,13 +209,16 @@ ${Object.entries(r.categorised).map(([t,s])=>html`
|
|
|
193
209
|
.dark .flatpickr-current-month, .dark .flatpickr-monthDropdown-months { color:#e5e7eb; background:#1f2937; }
|
|
194
210
|
.dark .flatpickr-weekday { color:#9ca3af; background:#1f2937; }
|
|
195
211
|
.dark .flatpickr-prev-month svg, .dark .flatpickr-next-month svg { fill:#9ca3af; }
|
|
196
|
-
</style>`,
|
|
212
|
+
</style>`,_=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}`,$=JSON.stringify(t.lessons.map(d=>d.id)),R=html`
|
|
213
|
+
<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()">
|
|
197
214
|
<form id="filter-form" method="get" action="/lessons">
|
|
198
215
|
<input type="hidden" name="period" id="h-period" value="${e.period}">
|
|
199
216
|
<input type="hidden" name="date_from" id="h-date-from" value="${e.date_from}">
|
|
200
217
|
<input type="hidden" name="date_to" id="h-date-to" value="${e.date_to}">
|
|
201
218
|
<input type="hidden" name="feedback" id="h-feedback" value="${e.feedback}">
|
|
202
219
|
<input type="hidden" name="starred" id="h-starred" value="${e.starred?"1":""}">
|
|
220
|
+
<input type="hidden" name="imported" id="h-imported" value="${e.imported}">
|
|
221
|
+
<input type="hidden" name="shared_by" id="h-shared-by" value="${e.shared_by}">
|
|
203
222
|
<input type="hidden" name="category" value="${e.category}">
|
|
204
223
|
<input type="hidden" name="level" id="h-level" value="${e.level}">
|
|
205
224
|
<input type="hidden" name="project" value="${e.project}">
|
|
@@ -213,20 +232,27 @@ ${Object.entries(r.categorised).map(([t,s])=>html`
|
|
|
213
232
|
<input type="text" name="search" value="${e.search}" placeholder="Search lessons…" autocomplete="off" class="w-full pl-9 pr-10 py-3 rounded-xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-800 dark:text-gray-100 placeholder-gray-400 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" />
|
|
214
233
|
${e.search?html`<button type="submit" name="search" value="" class="absolute inset-y-0 right-3 flex items-center text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 text-xl leading-none">×</button>`:""}
|
|
215
234
|
</div>
|
|
216
|
-
<p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${
|
|
235
|
+
<p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${_}</p>
|
|
217
236
|
</div>
|
|
218
237
|
|
|
219
238
|
<div class="flex flex-wrap items-center gap-2 mb-3">
|
|
220
239
|
<button type="button" onclick="var h=document.getElementById('h-starred'); h.value=h.value?'':'1'; document.getElementById('filter-form').submit()"
|
|
221
240
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.starred?"bg-yellow-400 text-yellow-900 border-yellow-400":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-yellow-400 hover:text-yellow-500"}">★ Starred</button>
|
|
222
241
|
|
|
242
|
+
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
243
|
+
<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.imported||e.shared_by?"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"}">${l}${h}</button>
|
|
244
|
+
<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-52 p-1 overflow-hidden" style="display:none">
|
|
245
|
+
${g.map(([d,x,b])=>{let w=e.imported===d&&e.shared_by===x;return html`<button type="button" data-imported="${d}" data-shared-by="${x}" onclick="var d=this.dataset; document.getElementById('h-imported').value=d.imported; document.getElementById('h-shared-by').value=d.sharedBy; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${w?"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"}"><span class="truncate">${b}</span>${w?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`})}
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
223
249
|
<div class="relative" x-data="periodPicker()" @keydown.escape="close()">
|
|
224
250
|
<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 ${o||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"}">
|
|
225
|
-
<span>📅</span><span x-text="label">${s}</span>${
|
|
251
|
+
<span>📅</span><span x-text="label">${s}</span>${h}
|
|
226
252
|
</button>
|
|
227
253
|
<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">
|
|
228
254
|
<div class="p-1">
|
|
229
|
-
${Object.entries(
|
|
255
|
+
${Object.entries(Te).map(([d,x])=>{let b=e.period===d&&!o;return html`<button type="button" @click="selectPreset('${d}', '${x}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${b?"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}${b?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
|
|
230
256
|
</div>
|
|
231
257
|
<div class="border-t border-gray-100 dark:border-gray-800 p-1">
|
|
232
258
|
<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 ${o?"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"}">
|
|
@@ -241,38 +267,41 @@ ${Object.entries(r.categorised).map(([t,s])=>html`
|
|
|
241
267
|
</div>
|
|
242
268
|
|
|
243
269
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
244
|
-
<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.feedback?"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"}">${n}${
|
|
270
|
+
<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.feedback?"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"}">${n}${h}</button>
|
|
245
271
|
<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-44 p-1 overflow-hidden" style="display:none">
|
|
246
|
-
${[["","All feedback"],["know","\u2713 Known"],["dont_know","\u2717 Don't know"],["none","\u2014 No response"]].map(([
|
|
272
|
+
${[["","All feedback"],["know","\u2713 Known"],["dont_know","\u2717 Don't know"],["none","\u2014 No response"]].map(([d,x])=>html`<button type="button" onclick="document.getElementById('h-feedback').value='${d}'; 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.feedback===d?"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.feedback===d?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
|
|
247
273
|
</div>
|
|
248
274
|
</div>
|
|
249
275
|
|
|
250
276
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
251
|
-
<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?`${
|
|
277
|
+
<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?`${Pe[e.level]??""} ${e.level}`:"Level"}${h}</button>
|
|
252
278
|
<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">
|
|
253
|
-
${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([
|
|
279
|
+
${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([d,x])=>html`<button type="button" onclick="document.getElementById('h-level').value='${d}'; 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===d?"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===d?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
|
|
254
280
|
</div>
|
|
255
281
|
</div>
|
|
256
282
|
|
|
257
|
-
${
|
|
258
|
-
<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${
|
|
283
|
+
${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">
|
|
284
|
+
<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${h}</button>
|
|
259
285
|
<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">
|
|
260
|
-
${
|
|
261
|
-
${
|
|
262
|
-
${
|
|
263
|
-
${
|
|
264
|
-
${
|
|
286
|
+
${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(d=>html`<option value="${d}" ${e.category===d?"selected":""}>${d}</option>`)}</select></div>`:""}
|
|
287
|
+
${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(d=>html`<option value="${d}" ${e.project===d?"selected":""}>${d}</option>`)}</select></div>`:""}
|
|
288
|
+
${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(d=>html`<option value="${d}" ${e.repository===d?"selected":""}>${d}</option>`)}</select></div>`:""}
|
|
289
|
+
${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(d=>html`<option value="${d}">`)}</datalist></div>`:""}
|
|
290
|
+
${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(d=>html`<option value="${d.slice(0,7)}">`)}</datalist></div>`:""}
|
|
265
291
|
<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>
|
|
266
292
|
</div>
|
|
267
293
|
</div>`:""}
|
|
268
294
|
|
|
269
|
-
${
|
|
295
|
+
${r?html`<a href="/lessons" class="ml-auto text-xs text-gray-400 hover:text-gray-700 dark:hover:text-white transition">Clear all</a>`:""}
|
|
270
296
|
|
|
271
|
-
|
|
297
|
+
${Ee(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>
|
|
298
|
+
<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>`,r?"":"ml-auto",'x-show="!selectMode"')}
|
|
299
|
+
<button type="button" x-show="selectMode" style="display:none" @click="leave()" class="${r?"":"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>
|
|
300
|
+
<div class="relative" x-data="{ open: ${String(t.importOpen)} }" @click.outside="open = false" @keydown.escape="open = false">
|
|
272
301
|
<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>
|
|
273
302
|
<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">
|
|
274
303
|
<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>
|
|
275
|
-
${
|
|
304
|
+
${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>`:""}
|
|
276
305
|
<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>
|
|
277
306
|
<div class="flex items-center gap-2">
|
|
278
307
|
<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>
|
|
@@ -282,80 +311,120 @@ ${Object.entries(r.categorised).map(([t,s])=>html`
|
|
|
282
311
|
</div>
|
|
283
312
|
</div>
|
|
284
313
|
|
|
285
|
-
${
|
|
286
|
-
${o?
|
|
287
|
-
${e.period!=="all"&&!o?
|
|
288
|
-
${e.feedback?
|
|
289
|
-
${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="${
|
|
290
|
-
${e.
|
|
291
|
-
${e.
|
|
292
|
-
${e.
|
|
293
|
-
${e.
|
|
294
|
-
${e.
|
|
295
|
-
${e.
|
|
296
|
-
${e.
|
|
314
|
+
${r?html`<div class="flex flex-wrap gap-1.5 mb-4">
|
|
315
|
+
${o?i(`\u{1F4C5} ${s}`,y(e,{date_from:"",date_to:""})):""}
|
|
316
|
+
${e.period!=="all"&&!o?i(`\u{1F550} ${s}`,y(e,{period:"all"})):""}
|
|
317
|
+
${e.feedback?i(je[e.feedback]??e.feedback,y(e,{feedback:""})):""}
|
|
318
|
+
${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="${y({...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>`:""}
|
|
319
|
+
${e.imported||e.shared_by?i(l,y(e,{imported:"",shared_by:""})):""}
|
|
320
|
+
${e.search?i(`\u{1F50D} "${e.search}"`,y(e,{search:""})):""}
|
|
321
|
+
${e.category?i(e.category,y(e,{category:""})):""}
|
|
322
|
+
${e.level?i(`${Pe[e.level]??""} ${e.level}`,y(e,{level:""})):""}
|
|
323
|
+
${e.project?i(`\u{1F4C1} ${e.project}`,y(e,{project:""})):""}
|
|
324
|
+
${e.repository?i(`\u2387 ${e.repository}`,y(e,{repository:""})):""}
|
|
325
|
+
${e.branch?i(`\u2387 ${e.branch}`,y(e,{branch:""})):""}
|
|
326
|
+
${e.commit?i(e.commit.slice(0,7),y(e,{commit:""})):""}
|
|
297
327
|
</div>`:""}
|
|
298
328
|
</form>
|
|
299
329
|
<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="hidden"><input type="hidden" name="from" value="lessons" /></form>
|
|
300
330
|
<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>
|
|
301
331
|
|
|
302
|
-
${
|
|
303
|
-
<table class="w-full text-sm">
|
|
332
|
+
${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">
|
|
333
|
+
<table class="w-full text-sm table-fixed" data-resizable="lessons">
|
|
334
|
+
<colgroup>
|
|
335
|
+
<col data-col="star" style="width:2.75rem" />
|
|
336
|
+
<col data-col="date" data-min="96" style="width:8rem" />
|
|
337
|
+
<col data-col="topic" data-min="88" class="hidden sm:table-column" style="width:11rem" />
|
|
338
|
+
<col data-col="title" data-flex data-min="220" />
|
|
339
|
+
<col data-col="level" data-min="88" style="width:5.5rem" />
|
|
340
|
+
<col data-col="categories" data-min="120" class="hidden lg:table-column" style="width:24%" />
|
|
341
|
+
<col data-col="feedback" data-min="112" class="hidden xl:table-column" style="width:7rem" />
|
|
342
|
+
<col data-col="share" style="width:2.5rem" />
|
|
343
|
+
</colgroup>
|
|
304
344
|
<thead>
|
|
305
345
|
<tr class="bg-gray-50 dark:bg-gray-800/60 border-b border-gray-200 dark:border-gray-800 text-left text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
|
|
306
|
-
<th class="
|
|
307
|
-
${
|
|
308
|
-
${
|
|
309
|
-
${
|
|
310
|
-
${
|
|
311
|
-
<th class="px-3 py-3 hidden lg:table-cell">Categories</th>
|
|
312
|
-
${
|
|
346
|
+
<th class="px-3 py-3"><input type="checkbox" tabindex="-1" aria-label="Select all on this page" class="dc-check" x-show="selectMode" style="display:none" :checked="selected.length > 0 && ${$}.every((id) => selected.includes(id))" @change="setAll(${$}, $event.target.checked)" /></th>
|
|
347
|
+
${p("Date","timestamp")}
|
|
348
|
+
${p("Topic","topic_id","hidden sm:table-cell")}
|
|
349
|
+
${p("Title","title")}
|
|
350
|
+
${p("Level","level")}
|
|
351
|
+
<th class="px-3 py-3 hidden lg:table-cell relative overflow-hidden text-ellipsis">Categories</th>
|
|
352
|
+
${p("Feedback","feedback","hidden xl:table-cell")}
|
|
313
353
|
<th class="px-2 py-3 w-8"><span class="sr-only">Share</span></th>
|
|
314
354
|
</tr>
|
|
315
355
|
</thead>
|
|
316
356
|
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
317
|
-
${
|
|
318
|
-
<td class="px-3 py-3"
|
|
319
|
-
<form method="post" action="/lessons/${encodeURIComponent(
|
|
320
|
-
<input type="hidden" name="starred" value="${
|
|
321
|
-
<input type="hidden" name="next" value="/lessons${
|
|
322
|
-
<button type="submit" title="${
|
|
357
|
+
${t.lessons.map(d=>{let x=Le("lesson-row-",d.id),b=d.timestamp.slice(0,10),w=d.timestamp.slice(0,16).replace("T"," ");return html`<tr id="${x}" data-id="${d.id}" data-title="${d.title}" data-href="/lessons/${encodeURIComponent(d.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'">
|
|
358
|
+
<td class="px-3 py-3">
|
|
359
|
+
<form x-show="!selectMode" method="post" action="/lessons/${encodeURIComponent(d.id)}/star" hx-post="/lessons/${encodeURIComponent(d.id)}/star" hx-target="#${x}" hx-select="#${x}" hx-swap="outerHTML">
|
|
360
|
+
<input type="hidden" name="starred" value="${d.starred?"0":"1"}" />
|
|
361
|
+
<input type="hidden" name="next" value="/lessons${y(e)}" />
|
|
362
|
+
<button type="submit" title="${d.starred?"Unstar":"Star"}" class="w-6 text-lg text-center leading-none transition ${d.starred?"text-yellow-400 hover:text-yellow-300":"text-gray-300 dark:text-gray-600 hover:text-yellow-400"}">${d.starred?"\u2605":"\u2606"}</button>
|
|
323
363
|
</form>
|
|
364
|
+
<input type="checkbox" tabindex="-1" aria-hidden="true" class="dc-check pointer-events-none" x-show="selectMode" style="display:none" :checked="selected.includes($el.closest('tr').dataset.id)" />
|
|
324
365
|
</td>
|
|
325
366
|
<td class="px-3 py-3 whitespace-nowrap tabular-nums relative group/date">
|
|
326
|
-
<span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${
|
|
327
|
-
<div class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${
|
|
367
|
+
<span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${d.timestamp}">${b}</span>
|
|
368
|
+
<div class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${w}</div>
|
|
328
369
|
</td>
|
|
329
|
-
<td class="px-3 py-3 hidden sm:table-cell"><span class="text-xs font-mono text-cyan-600 dark:text-cyan-400">${
|
|
330
|
-
<td class="px-3 py-3
|
|
331
|
-
<td class="px-3 py-3"
|
|
332
|
-
<td class="px-3 py-3 hidden lg:table-cell"
|
|
333
|
-
<td class="px-3 py-3 hidden xl:table-cell"
|
|
334
|
-
<td class="px-2 py-3 text-center"
|
|
370
|
+
<td class="px-3 py-3 hidden sm:table-cell"><span class="block truncate text-xs font-mono text-cyan-600 dark:text-cyan-400">${d.topic_id}</span>${d.imported?html`<span class="block text-[11px] text-gray-400 dark:text-gray-500 truncate max-w-[10rem]" title="This lesson was shared with you">🤝 ${d.shared_by??"anonymous"}</span>`:""}</td>
|
|
371
|
+
<td class="px-3 py-3"><a href="/lessons/${encodeURIComponent(d.id)}" class="font-semibold text-[15px] leading-snug text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${d.title}</a></td>
|
|
372
|
+
<td class="px-3 py-3"><a href="${y(e,{level:d.level})}" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${c[d.level]??""} hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${d.level}</a></td>
|
|
373
|
+
<td class="px-3 py-3 hidden lg:table-cell"><div class="flex flex-wrap gap-1">${d.categories.map(j=>html`<a href="${y(e,{category:j})}" class="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors">${j}</a>`)}</div></td>
|
|
374
|
+
<td class="px-3 py-3 hidden xl:table-cell">${d.feedback==="know"?html`<span class="text-xs text-teal-600 dark:text-teal-400 font-medium">✓ Known</span>`:d.feedback==="dont_know"?html`<span class="text-xs text-rose-500 dark:text-rose-400 font-medium">✗ Unknown</span>`:""}</td>
|
|
375
|
+
<td class="px-2 py-3 text-center"><button type="button" hx-get="/lessons/${encodeURIComponent(d.id)}/share?format=panel" hx-target="#share-modal-body" hx-swap="innerHTML" @click="openShare($el)" title="Share this lesson" aria-label="Share this lesson" class="inline-flex text-gray-400 dark:text-gray-500 hover:text-indigo-500 dark:hover:text-indigo-400 transition opacity-0 group-hover:opacity-100 focus-visible:opacity-100"><svg class="w-4 h-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 14 14 6M8 6h6v6"/></svg></button></td>
|
|
335
376
|
</tr>`})}
|
|
336
377
|
</tbody>
|
|
337
378
|
</table>
|
|
338
379
|
</div>
|
|
339
|
-
${
|
|
340
|
-
<p class="text-xs text-gray-400 dark:text-gray-500">Page ${
|
|
380
|
+
${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
|
|
381
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">Page ${t.page} of ${t.totalPages}</p>
|
|
341
382
|
<div class="flex items-center gap-1">
|
|
342
|
-
${
|
|
343
|
-
${
|
|
344
|
-
${
|
|
383
|
+
${t.page>1?html`<a href="${y(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>`}
|
|
384
|
+
${Fe(t.page,t.totalPages).map(d=>d===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:d===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">${d}</span>`:html`<a href="${y(e,{page:String(d)})}" 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">${d}</a>`)}
|
|
385
|
+
${t.page<t.totalPages?html`<a href="${y(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>`}
|
|
345
386
|
</div>
|
|
346
387
|
</div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
|
|
347
388
|
<p class="text-3xl mb-3">📭</p>
|
|
348
389
|
<p class="text-gray-500 dark:text-gray-400 text-sm">No lessons match the current filters.</p>
|
|
349
|
-
${
|
|
350
|
-
</div>`}
|
|
390
|
+
${r?html`<a href="/lessons" class="mt-2 text-indigo-500 hover:text-indigo-400 text-sm transition">Clear all filters</a>`:""}
|
|
391
|
+
</div>`}
|
|
392
|
+
<div x-show="selectMode" style="display:none" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0" class="fixed inset-x-0 bottom-0 z-50 border-t border-gray-200 dark:border-gray-800 bg-white/95 dark:bg-gray-900/95 backdrop-blur px-4 sm:px-6 py-3">
|
|
393
|
+
<div class="max-w-7xl mx-auto flex items-center gap-3">
|
|
394
|
+
<button type="button" @click="leave()" 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>
|
|
395
|
+
<div class="ml-auto flex items-center gap-3">
|
|
396
|
+
<p class="text-sm text-gray-700 dark:text-gray-200 tabular-nums"><span x-text="selected.length"></span> selected</p>
|
|
397
|
+
<button type="button" x-show="selected.length" style="display:none" @click="selected = []" class="text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-200 transition">Clear</button>
|
|
398
|
+
<button type="button" :disabled="!selected.length" @click="confirmOpen = true" 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 disabled:opacity-40 disabled:cursor-not-allowed">🗑 Delete selected</button>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
${Re("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">
|
|
403
|
+
<template x-for="t in selectedTitles().slice(0, 6)" :key="t"><li class="truncate">· <span x-text="t"></span></li></template>
|
|
404
|
+
</ul>
|
|
405
|
+
<p x-show="selected.length > 6" style="display:none" x-text="'…and ' + (selected.length - 6) + ' more'"></p>
|
|
406
|
+
<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>
|
|
407
|
+
<form method="post" action="/lessons/delete" class="contents">
|
|
408
|
+
<template x-for="id in selected" :key="id"><input type="hidden" name="id" :value="id" /></template>
|
|
409
|
+
<input type="hidden" name="next" value="/lessons${y(e,{page:String(t.page)})}" />
|
|
410
|
+
<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>
|
|
411
|
+
</form>`)}
|
|
412
|
+
<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">
|
|
413
|
+
<div @click.outside="shareOpen = false" :style="{ left: sharePos.left + 'px', top: sharePos.top != null ? sharePos.top + 'px' : 'auto', bottom: sharePos.bottom != null ? sharePos.bottom + 'px' : 'auto' }" role="dialog" aria-label="Share this lesson" class="absolute w-80 pointer-events-auto bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-4">
|
|
414
|
+
<button type="button" @click="shareOpen = false" aria-label="Close" class="absolute top-2 right-2 w-7 h-7 inline-flex items-center justify-center rounded-lg text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 transition">✕</button>
|
|
415
|
+
<div id="share-modal-body" class="space-y-3"><p class="text-xs text-gray-400 dark:text-gray-500">Loading…</p></div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
</div>`,k=html`<script src="/static/vendor/flatpickr.min.js"></script>
|
|
351
419
|
<script src="/static/relative-time.js"></script>
|
|
420
|
+
<script src="/static/table-resize.js"></script>
|
|
352
421
|
<script src="/static/share.js"></script>
|
|
353
422
|
<script>
|
|
354
423
|
function periodPicker() {
|
|
355
424
|
return {
|
|
356
425
|
open: false,
|
|
357
426
|
showCustom: ${raw(o?"true":"false")},
|
|
358
|
-
label: ${raw(
|
|
427
|
+
label: ${raw(T(s))},
|
|
359
428
|
fp: null,
|
|
360
429
|
init() {
|
|
361
430
|
const self = this;
|
|
@@ -363,7 +432,7 @@ function periodPicker() {
|
|
|
363
432
|
if (val && !this.fp) {
|
|
364
433
|
this.fp = flatpickr(this.$refs.fp, {
|
|
365
434
|
mode: 'range', dateFormat: 'Y-m-d', inline: false,
|
|
366
|
-
${raw(e.date_from?`defaultDate: [${
|
|
435
|
+
${raw(e.date_from?`defaultDate: [${T(e.date_from)}${e.date_to?`, ${T(e.date_to)}`:""}],`:"")}
|
|
367
436
|
onChange(dates) {
|
|
368
437
|
if (dates.length === 2) {
|
|
369
438
|
document.getElementById('h-date-from').value = self.fp.formatDate(dates[0], 'Y-m-d');
|
|
@@ -386,16 +455,29 @@ function periodPicker() {
|
|
|
386
455
|
},
|
|
387
456
|
};
|
|
388
457
|
}
|
|
389
|
-
</script>`;return
|
|
458
|
+
</script>`;return E({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:P,body:R,scripts:k})}function Fe(t,e){let o=[],r=0;for(let s=1;s<=e;s++)(s===1||s===e||s>=t-2&&s<=t+2)&&(r&&s-r>1&&o.push(0),o.push(s),r=s);return o}var G={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function F(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">
|
|
390
459
|
<div class="flex flex-wrap gap-2">
|
|
391
460
|
<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>
|
|
392
461
|
<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>
|
|
393
462
|
<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>
|
|
394
463
|
</div>
|
|
395
|
-
<p class="text-[11px] text-gray-400 dark:text-gray-500">${
|
|
396
|
-
</div>`}function
|
|
464
|
+
<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>
|
|
465
|
+
</div>`}function z(t,e,o=null){return html`
|
|
466
|
+
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Share this lesson</p>
|
|
467
|
+
${o?html`<p class="text-sm font-semibold text-gray-800 dark:text-gray-100 leading-snug">${o}</p>`:""}
|
|
468
|
+
<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">
|
|
469
|
+
<input type="hidden" name="persist" value="0" x-ref="persist" />
|
|
470
|
+
<div>
|
|
471
|
+
<label for="share-name" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Your name</label>
|
|
472
|
+
<input id="share-name" type="text" name="name" value="${e.name}" maxlength="80" placeholder="anonymous" autocomplete="off" @input="$refs.persist.value = '0'" @change="$refs.persist.value = '1'" 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" />
|
|
473
|
+
</div>
|
|
474
|
+
<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>
|
|
475
|
+
</form>
|
|
476
|
+
${F(e)}
|
|
477
|
+
`}function Oe(t){let e=t.lesson,o=t.share,r=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",s=e.repository_platform==="local",n=null;s&&e.repository?n=`vscode://file/${e.repository}`:e.repository_platform&&G[e.repository_platform]&&e.repository&&(n=`https://${G[e.repository_platform]}/${e.repository}`);let l=null;n&&e.commit_hash&&!s&&(l=e.repository_platform==="gitlab"?`${n}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${n}/commits/${e.commit_hash}`:`${n}/commit/${e.commit_hash}`);let g=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),c=e.timestamp.slice(0,10),i=e.timestamp.slice(0,16).replace("T"," "),p=`hx-post="/lessons/${encodeURIComponent(e.id)}/feedback" hx-target="#lesson-meta" hx-select="#lesson-meta" hx-swap="outerHTML" hx-select-oob="#lesson-feedback:outerHTML"`,h=html`
|
|
478
|
+
<div x-data="{ confirmOpen: false }" @keydown.escape.window="confirmOpen = false">
|
|
397
479
|
<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>
|
|
398
|
-
${
|
|
480
|
+
${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>`:""}
|
|
399
481
|
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
|
|
400
482
|
<div class="flex flex-wrap items-center gap-3 mb-2">
|
|
401
483
|
<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">
|
|
@@ -404,30 +486,22 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
404
486
|
<button type="submit" title="${e.starred?"Unstar":"Star"}" class="w-6 text-center text-xl leading-none transition ${e.starred?"text-yellow-400 hover:text-yellow-300":"text-gray-300 dark:text-gray-600 hover:text-yellow-400"}">${e.starred?"\u2605":"\u2606"}</button>
|
|
405
487
|
</form>
|
|
406
488
|
<h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.title}</h1>
|
|
407
|
-
<a href="/lessons?level=${e.level}" class="text-xs font-semibold px-2 py-0.5 rounded-full border ${
|
|
489
|
+
<a href="/lessons?level=${e.level}" class="text-xs font-semibold px-2 py-0.5 rounded-full border ${r} shrink-0 hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${e.level}</a>
|
|
490
|
+
${Ee(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")}
|
|
408
491
|
<div class="relative shrink-0" x-data="{ open: ${String(o.open)} }" @click.outside="open = false" @keydown.escape="open = false">
|
|
409
|
-
<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
|
|
492
|
+
<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>
|
|
410
493
|
<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">
|
|
411
|
-
|
|
412
|
-
<form method="post" action="/lessons/${encodeURIComponent(e.id)}/share" hx-post="/lessons/${encodeURIComponent(e.id)}/share" hx-target="#share-payloads" hx-swap="outerHTML" hx-trigger="input delay:300ms, change" onsubmit="return false" class="space-y-2.5">
|
|
413
|
-
<input type="hidden" name="persist" value="0" x-ref="persist" />
|
|
414
|
-
<div>
|
|
415
|
-
<label for="share-name" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Your name</label>
|
|
416
|
-
<input id="share-name" type="text" name="name" value="${o.name}" maxlength="80" placeholder="anonymous" autocomplete="off" @input="$refs.persist.value = '0'" @change="$refs.persist.value = '1'" 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" />
|
|
417
|
-
</div>
|
|
418
|
-
<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" ${o.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>
|
|
419
|
-
</form>
|
|
420
|
-
${A(o)}
|
|
494
|
+
${z(e.id,o)}
|
|
421
495
|
</div>
|
|
422
496
|
</div>
|
|
423
497
|
</div>
|
|
424
498
|
<div id="lesson-meta" class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
|
|
425
499
|
<span class="relative group/date cursor-default">🗓 <span data-ts="${e.timestamp}">${c}</span>
|
|
426
|
-
<span class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${
|
|
500
|
+
<span class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${i}</span>
|
|
427
501
|
</span>
|
|
428
502
|
<span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></span>
|
|
429
503
|
${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>`:""}
|
|
430
|
-
${e.categories.map(
|
|
504
|
+
${e.categories.map($=>html`<a href="/lessons?category=${encodeURIComponent($)}" 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">${$}</a>`)}
|
|
431
505
|
${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 know this</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">✗ I don't know this</span>`}
|
|
432
506
|
<form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(p)}><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>`:""}
|
|
433
507
|
</div>
|
|
@@ -439,7 +513,7 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
439
513
|
${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>`:""}
|
|
440
514
|
${g?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">
|
|
441
515
|
${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${n?html`<a href="${n}" ${s?"":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>`:""}
|
|
442
|
-
${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&&
|
|
516
|
+
${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&&G[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="" />`:""}${n?html`<a href="${n}" ${s?"":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>`:""}
|
|
443
517
|
${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>`:""}
|
|
444
518
|
${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${l?html`<a href="${l}" 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>`:""}
|
|
445
519
|
${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>`:""}
|
|
@@ -449,7 +523,15 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
449
523
|
<form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(p)}><input type="hidden" name="feedback" value="dont_know" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-red-100 dark:hover:bg-red-900 hover:text-red-700 dark:hover:text-white">✗ I don't know this</button></form>
|
|
450
524
|
</div>`}</div>
|
|
451
525
|
<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>
|
|
452
|
-
</div
|
|
526
|
+
</div>
|
|
527
|
+
${Re("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class="text-gray-700 dark:text-gray-200 font-medium">“${e.title}”</p>
|
|
528
|
+
<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>
|
|
529
|
+
<form method="post" action="/lessons/delete" class="contents">
|
|
530
|
+
<input type="hidden" name="id" value="${e.id}" />
|
|
531
|
+
<input type="hidden" name="next" value="/lessons" />
|
|
532
|
+
<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 lesson</button>
|
|
533
|
+
</form>`)}
|
|
534
|
+
</div>`,P=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,_=html`<script src="/static/vendor/highlight.min.js"></script>
|
|
453
535
|
<script src="/static/vendor/marked.min.js"></script>
|
|
454
536
|
<script src="/static/vendor/purify.min.js"></script>
|
|
455
537
|
<script src="/static/relative-time.js"></script>
|
|
@@ -463,9 +545,9 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
463
545
|
},
|
|
464
546
|
breaks: true, gfm: true,
|
|
465
547
|
});
|
|
466
|
-
document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(
|
|
467
|
-
document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(
|
|
468
|
-
</script>`;return
|
|
548
|
+
document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(T(e.summary))}));
|
|
549
|
+
document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(T(e.body))}));
|
|
550
|
+
</script>`;return E({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:P,body:h,scripts:_})}var ze={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 Ae(t){let e=t.preview,o=html`<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="space-y-3">
|
|
469
551
|
<input type="hidden" name="from" value="lessons" />
|
|
470
552
|
<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>
|
|
471
553
|
<div class="flex items-center gap-3">
|
|
@@ -473,14 +555,14 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
473
555
|
<button type="submit" class="ml-auto bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg px-4 py-2 text-sm transition">Import</button>
|
|
474
556
|
</div>
|
|
475
557
|
<p class="text-xs text-gray-400 dark:text-gray-500">You can also drop the file anywhere on this page.</p>
|
|
476
|
-
</form>`,
|
|
558
|
+
</form>`,r=html`
|
|
477
559
|
<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>
|
|
478
560
|
<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">
|
|
479
561
|
<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>
|
|
480
|
-
${
|
|
562
|
+
${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>`:""}
|
|
481
563
|
${e?html`<div class="flex flex-wrap items-center gap-3 mb-2">
|
|
482
564
|
<h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.lesson.title}</h1>
|
|
483
|
-
<span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${
|
|
565
|
+
<span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${ze[e.lesson.level]??""} shrink-0">${e.lesson.level}</span>
|
|
484
566
|
</div>
|
|
485
567
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
|
|
486
568
|
<span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.lesson.topic_id}</span></span>
|
|
@@ -498,7 +580,7 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
498
580
|
${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>`:""}
|
|
499
581
|
<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">
|
|
500
582
|
<input type="hidden" name="from" value="lessons" />
|
|
501
|
-
<input type="hidden" name="text" value="${
|
|
583
|
+
<input type="hidden" name="text" value="${t.code}" />
|
|
502
584
|
<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>
|
|
503
585
|
<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>
|
|
504
586
|
</form>`:o}
|
|
@@ -515,12 +597,12 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
|
|
|
515
597
|
},
|
|
516
598
|
breaks: true, gfm: true,
|
|
517
599
|
});
|
|
518
|
-
${e?raw(`document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${
|
|
519
|
-
document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${
|
|
520
|
-
</script>`;return
|
|
600
|
+
${e?raw(`document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${T(e.lesson.summary)}));
|
|
601
|
+
document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${T(e.lesson.body??"")}));`):""}
|
|
602
|
+
</script>`;return E({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:s,body:r,scripts:n})}var Ke=[[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"]];function Ne(t){let e=t.settings.min_gap_minutes,o=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],r=[["auto","Auto","\u{1F3E0}"],["lessons","Lessons","\u{1F4DA}"],["knowledge","Knowledge map","\u{1F9ED}"]],s=t.flash,n=html`
|
|
521
603
|
<h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
|
|
522
|
-
${
|
|
523
|
-
${
|
|
604
|
+
${s?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${s.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"}">
|
|
605
|
+
${s.imported} lesson${s.imported!==1?"s":""} imported.${s.skipped?` ${s.skipped} skipped (already in DB).`:""}${s.invalid?` ${s.invalid} rejected (failed validation).`:""}${s.groups?` ${s.groups} group${s.groups!==1?"s":""} added.`:""}${s.notebook?" Notebook restored.":""}
|
|
524
606
|
</div>`:""}
|
|
525
607
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
526
608
|
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
|
|
@@ -529,13 +611,13 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
|
|
|
529
611
|
<div class="grid grid-cols-2 gap-4">
|
|
530
612
|
<div>
|
|
531
613
|
<label for="max-per-day" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Max lessons per day</label>
|
|
532
|
-
<input id="max-per-day" type="number" name="max_per_day" min="1" max="20" value="${
|
|
614
|
+
<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" />
|
|
533
615
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Lessons in a 24h window.</p>
|
|
534
616
|
</div>
|
|
535
617
|
<div>
|
|
536
618
|
<label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
|
|
537
619
|
<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">
|
|
538
|
-
${
|
|
620
|
+
${Ke.map(([g,c])=>html`<option value="${g}" ${e===g?"selected":""}>${c}</option>`)}
|
|
539
621
|
</select>
|
|
540
622
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
|
|
541
623
|
</div>
|
|
@@ -543,33 +625,43 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
|
|
|
543
625
|
<div class="grid grid-cols-2 gap-4">
|
|
544
626
|
<div>
|
|
545
627
|
<label for="nudge-every" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Interactions between lessons</label>
|
|
546
|
-
<input id="nudge-every" type="number" name="nudge_every" min="0" max="1000" value="${
|
|
628
|
+
<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" />
|
|
547
629
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Cue at most once every N (0 = every turn).</p>
|
|
548
630
|
</div>
|
|
549
631
|
<div>
|
|
550
632
|
<label for="nudge-scope" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Count interactions</label>
|
|
551
633
|
<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">
|
|
552
|
-
<option value="session" ${
|
|
553
|
-
<option value="global" ${
|
|
634
|
+
<option value="session" ${t.settings.nudge_scope==="session"?"selected":""}>Per chat session</option>
|
|
635
|
+
<option value="global" ${t.settings.nudge_scope==="global"?"selected":""}>Globally</option>
|
|
554
636
|
</select>
|
|
555
637
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Per-chat or across all sessions.</p>
|
|
556
638
|
</div>
|
|
557
639
|
</div>
|
|
558
640
|
<div>
|
|
559
641
|
<label for="share-name" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Your name (for sharing)</label>
|
|
560
|
-
<input id="share-name" type="text" name="share_name" maxlength="80" value="${
|
|
642
|
+
<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" />
|
|
561
643
|
<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>
|
|
562
644
|
</div>
|
|
563
645
|
<fieldset class="border-0 p-0 m-0">
|
|
564
646
|
<legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">UI theme</legend>
|
|
565
647
|
<div class="flex gap-2">
|
|
566
|
-
${o.map(([
|
|
567
|
-
<input type="radio" name="ui_theme" value="${
|
|
568
|
-
<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">${
|
|
648
|
+
${o.map(([g,c,i])=>html`<label class="flex-1 cursor-pointer">
|
|
649
|
+
<input type="radio" name="ui_theme" value="${g}" ${t.settings.ui_theme===g?"checked":""} class="sr-only peer" />
|
|
650
|
+
<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">${i}</span>${c}</span>
|
|
569
651
|
</label>`)}
|
|
570
652
|
</div>
|
|
571
653
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">System follows your OS preference.</p>
|
|
572
654
|
</fieldset>
|
|
655
|
+
<fieldset class="border-0 p-0 m-0">
|
|
656
|
+
<legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Home page</legend>
|
|
657
|
+
<div class="flex gap-2">
|
|
658
|
+
${r.map(([g,c,i])=>html`<label class="flex-1 cursor-pointer">
|
|
659
|
+
<input type="radio" name="ui_home" value="${g}" ${t.settings.ui_home===g?"checked":""} class="sr-only peer" />
|
|
660
|
+
<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">${i}</span>${c}</span>
|
|
661
|
+
</label>`)}
|
|
662
|
+
</div>
|
|
663
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Auto opens Lessons once you have one, the knowledge map before.</p>
|
|
664
|
+
</fieldset>
|
|
573
665
|
<button type="submit" class="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded px-4 py-2 text-sm transition">Save settings</button>
|
|
574
666
|
</form>
|
|
575
667
|
</div>
|
|
@@ -617,13 +709,13 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
|
|
|
617
709
|
<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>
|
|
618
710
|
</div>
|
|
619
711
|
<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>
|
|
620
|
-
<a href="vscode://file/${
|
|
712
|
+
<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>
|
|
621
713
|
</div>
|
|
622
714
|
</div>
|
|
623
715
|
<div x-show="nbMode === 'preview'" id="notebook-preview-settings" class="markdown-body min-h-24"></div>
|
|
624
|
-
<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">${
|
|
716
|
+
<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>
|
|
625
717
|
</div>
|
|
626
|
-
</div>`,
|
|
718
|
+
</div>`,l=html`<script src="/static/vendor/marked.min.js"></script>
|
|
627
719
|
<script src="/static/vendor/purify.min.js"></script>
|
|
628
720
|
<script>
|
|
629
721
|
function updateLabel(inputId, labelId, submitId) {
|
|
@@ -632,8 +724,8 @@ function updateLabel(inputId, labelId, submitId) {
|
|
|
632
724
|
document.getElementById(submitId).disabled = !input.files.length;
|
|
633
725
|
}
|
|
634
726
|
marked.setOptions({ breaks: true, gfm: true });
|
|
635
|
-
document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(
|
|
636
|
-
</script>`;return j({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:r.uiTheme,body:s,scripts:n})}var D=25;function H(r,e="/lessons"){return r?.startsWith("/")&&!r.startsWith("//")?r:e}function m(r,e,o=""){let t=r[e];return typeof t=="string"?t:o}var q=dirname(fileURLToPath(import.meta.url)),Ee=[join(q,"../assets/static"),join(q,"../../assets/static")].find(r=>existsSync(r))??join(q,"../assets/static"),ze={".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"},Ke="https://ultimaphoenix.github.io",Re={"access-control-allow-origin":Ke,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function G(r){let e=r.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var F;function Ye(){return F===void 0&&(F=a$1()),F}function Me(r,e,o){let t=T({explicit:o.name??null,setting:e,gitUserName:Ye()}),s=o.includeContext??false,n=S$1(r,{includeContext:s,sharedBy:t}),l=U(n);return {payload:n,state:{id:r.id,open:o.open??false,name:t??"",includeContext:s,text:W(n,l),link:X(n,l)}}}function Je(){try{return o(r=>G$1(r).ui_theme)}catch{return "system"}}function Ve(r$1={}){let e=new Hono;return e.get("/static/*",o=>{let t=decodeURIComponent(o.req.path.slice(8)),s=join(Ee,t);if(!s.startsWith(Ee))return o.notFound();try{let n=readFileSync(s);return new Response(new Uint8Array(n),{headers:{"content-type":ze[extname(s)]??"application/octet-stream"}})}catch{return o.notFound()}}),e.get("/ping",o=>o.json({ok:true,version:Q},200,Re)),e.options("/ping",o=>o.body(null,204,Re)),e.post("/shutdown",o=>G(o)?o.text("Forbidden",403):(setTimeout(()=>r$1.onShutdown?.(),50),o.json({ok:true}))),e.get("/shutdown",o=>o.text("Method Not Allowed",405)),e.get("/",o$1=>{let{profile:t,stats:s,rateLimit:n,settings:l}=o(d=>({profile:$(d),stats:ca(d),rateLimit:_(d),settings:G$1(d)})),g=new Map;for(let d of t.groups)for(let p of d.topics)g.set(p,d.name);let c={};for(let d of t.groups)c[d.name]=[];for(let d of t.knowledge){let p=g.get(d.topic)??"Other",b=c[p]??[];b.push(d),c[p]=b;}return o$1.html(Ce({categorised:c,allGroups:t.groups.map(d=>d.name),stats:s,rateLimit:n,maxPerDay:l.max_per_day,uiTheme:l.ui_theme}))}),e.post("/knowledge",async o$1=>{let t=await o$1.req.parseBody(),s=m(t,"topic").trim();if(s){let n=Number.parseInt(m(t,"confidence","5"),10)||5,l=m(t,"group").trim();o(g=>{A$1(g,s,n),l&&l!=="Other"&&E(g,s,l);});}return o$1.redirect("/",303)}),e.post("/knowledge/:topic/delete",o$1=>(o(t=>B$1(t,o$1.req.param("topic"))),o$1.redirect("/",303))),e.post("/knowledge/:topic/group",async o$1=>{let t=m(await o$1.req.parseBody(),"group").trim(),s=o$1.req.param("topic");return o(n=>{t&&t!=="Other"?E(n,s,t):F$1(n,s);}),o$1.redirect("/",303)}),e.post("/knowledge/:topic",async o$1=>{let t=Number.parseInt(m(await o$1.req.parseBody(),"delta","0"),10)||0;return o(s=>aa(s,o$1.req.param("topic"),t)),o$1.redirect("/",303)}),e.post("/groups",async o$1=>{let t=m(await o$1.req.parseBody(),"group_name").trim();return t&&t!=="Other"&&o(s=>C(s,t)),o$1.redirect("/",303)}),e.post("/groups/:group_name/delete",o$1=>(o(t=>D$1(t,o$1.req.param("group_name"))),o$1.redirect("/",303))),e.get("/lessons/export",()=>{let o$1=o(t=>v(t));return new Response(JSON.stringify(o$1,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e.post("/lessons/import",async o$1=>{let t=await o$1.req.parseBody(),s=m(t,"from")==="lessons",n=d=>o$1.redirect(s?`/lessons?import=1&error=${d}`:"/settings?imported=0&skipped=0&invalid=1",303);if(G(o$1))return n("cross");let l=t.file,c$1=(l instanceof File&&l.size>0?await l.text():null)??m(t,"text").trim();try{c(c$1)&&!c$1.includes("#devcoach:lesson:")&&(c$1=await d(c$1));let d$1=o(p=>da(p,c$1));return d$1.kind==="lessons"||!d$1.lesson?o$1.redirect(`/settings?imported=${d$1.inserted}&skipped=${d$1.duplicated}&invalid=${d$1.invalid}`,303):o$1.redirect(`/lessons/${encodeURIComponent(d$1.lesson.id)}?imported=${d$1.inserted?"1":"dup"}`,303)}catch(d){if(d instanceof R)return n("invalid");throw d}}),e.get("/lessons/import",o=>{let t=(o.req.query("code")??"").trim(),s=null,n=null;if(t)try{s=V(t);}catch(l){n=l instanceof R?l.message:"That code could not be read.";}return o.html(je({code:t,preview:s,error:n,uiTheme:Je()}))}),e.get("/lessons",o$1=>{let t=o$1.req.query(),s=new Set(["junior","mid","senior"]),n=t.level&&s.has(t.level)?t.level:"",l=t.date_from||"",g=t.date_to||"",c=t.starred==="1",d=t.sort||"timestamp",p=t.order==="asc"?"asc":"desc",b=Math.max(1,Number.parseInt(t.page??"1",10)||1),_=t.period||"all",T={period:l||g?null:_!=="all"?_:null,category:t.category||null,level:n||null,project:t.project||null,repository:t.repository||null,branch:t.branch||null,commit:t.commit||null,starred:c?true:null,search:t.search||null,feedback:t.feedback||null,date_from:l||null,date_to:g||null},i=o(h=>{let I=q$1(h,T),K=Math.max(1,Math.ceil(I/D));return b=Math.min(b,K),{total:I,totalPages:K,lessons:r(h,{...T,sort:d,order:p,page:b,per_page:D}),allCategories:y(h),allProjects:w(h,"project"),allRepositories:w(h,"repository"),allBranches:w(h,"branch"),allCommits:w(h,"commit_hash"),theme:G$1(h).ui_theme}}),y$1={period:_,category:t.category||"",level:n,project:t.project||"",repository:t.repository||"",branch:t.branch||"",commit:t.commit||"",starred:c,search:t.search||"",feedback:t.feedback||"",date_from:l,date_to:g,sort:d,order:p},v=t.error==="invalid"||t.error==="cross"?t.error:null;return o$1.html(Te({importOpen:t.import==="1",importError:v,lessons:i.lessons,allCategories:i.allCategories,allProjects:i.allProjects,allRepositories:i.allRepositories,allBranches:i.allBranches,allCommits:i.allCommits,s:y$1,page:b,perPage:D,total:i.total,totalPages:i.totalPages,uiTheme:i.theme}))}),e.post("/lessons/:lesson_id/star",async o$1=>{let t$1=await o$1.req.parseBody();return o(s=>t(s,o$1.req.param("lesson_id"),t$1.starred==="1")),o$1.redirect(H(m(t$1,"next")||void 0),303)}),e.post("/lessons/:lesson_id/feedback",async o$1=>{let t=await o$1.req.parseBody(),s=m(t,"feedback"),n=s===""||s==="clear"?null:s;return o(l=>ba(l,o$1.req.param("lesson_id"),n)),o$1.redirect(H(m(t,"next")||void 0),303)}),e.on(["GET","POST"],"/lessons/:lesson_id/share",async o$1=>{if(G(o$1))return o$1.text("Forbidden",403);let t,s,n=false;if(o$1.req.method==="POST"){let p=await o$1.req.parseBody();t=m(p,"name"),s=m(p,"include_context")==="1",n=m(p,"persist")==="1";}else t=o$1.req.query("name"),s=o$1.req.query("include_context")==="1";let l=o(p=>{let b=x(p,o$1.req.param("lesson_id")??"");return b?(n&&t!==void 0&&H$1(p,"share_name",m$1(t)),{lesson:b,settingName:G$1(p).share_name}):null});if(!l)return o$1.text("Lesson not found",404);let{state:g,payload:c}=Me(l.lesson,l.settingName,{name:t,includeContext:s}),d=o$1.req.query("format");return d==="text"?o$1.text(g.text):d==="link"?o$1.text(g.link):d==="md"?new Response(Z(c),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${Y(c)}"`}}):o$1.html(A(g))}),e.get("/lessons/:lesson_id",o$1=>{let t=o(l=>{let g=x(l,o$1.req.param("lesson_id"));if(!g)return null;let c=G$1(l);return {lesson:g,settingName:c.share_name,uiTheme:c.ui_theme}});if(!t)return o$1.html("<h1>Lesson not found</h1>",404);let s=o$1.req.query(),n=s.imported==="1"?"new":s.imported==="dup"?"dup":null;return o$1.html(Pe({lesson:t.lesson,uiTheme:t.uiTheme,share:Me(t.lesson,t.settingName,{open:s.share==="1"}).state,imported:n}))}),e.get("/settings/export",()=>{let o$1=o(t=>O(t));return new Response(new Uint8Array(o$1),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e.post("/settings/import",async o$1=>{let t=(await o$1.req.parseBody()).file;if(!(t instanceof File))return o$1.redirect("/settings?imported=0&skipped=0&invalid=1",303);let s=new Uint8Array(await t.arrayBuffer()),n=o(l=>P(l,s));return o$1.redirect(`/settings?imported=${n.lessons}&skipped=${n.skipped}&invalid=${n.invalid}&groups=${n.groups}¬ebook=${n.learning_state}`,303)}),e.get("/settings/notebook/download",()=>{let o=existsSync(j$1)?readFileSync(j$1,"utf8"):"";return new Response(o,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e.post("/settings/notebook",async o=>{let t=await o.req.parseBody();return mkdirSync(dirname(j$1),{recursive:true}),writeFileSync(j$1,m(t,"content"),"utf8"),o.redirect(H(m(t,"next")||void 0,"/settings?notebook_saved=1"),303)}),e.post("/settings",async o$1=>{let t=await o$1.req.parseBody(),s=Number.parseInt(m(t,"max_per_day","2"),10)||2,n=Number.parseInt(m(t,"min_gap_minutes","240"),10),l=m(t,"ui_theme","system");["system","dark","light"].includes(l)||(l="system");let g=Math.max(0,Number.parseInt(m(t,"nudge_every","10"),10)||0),c=m(t,"nudge_scope","session");return c!=="session"&&c!=="global"&&(c="session"),o(d=>{H$1(d,"max_per_day",String(s)),H$1(d,"min_gap_minutes",String(Number.isNaN(n)?240:n)),H$1(d,"ui_theme",l),H$1(d,"nudge_every",String(g)),H$1(d,"nudge_scope",c),H$1(d,"share_name",m$1(m(t,"share_name")));}),o$1.redirect("/settings",303)}),e.get("/settings",o$1=>{let t=o(g=>G$1(g)),s=existsSync(j$1)?readFileSync(j$1,"utf8"):"",n=o$1.req.query(),l=n.imported!==void 0?{imported:Number(n.imported??0),skipped:Number(n.skipped??0),invalid:Number(n.invalid??0),groups:Number(n.groups??0),notebook:Number(n.notebook??0)}:null;return o$1.html(Le({settings:t,notebookContent:s,notebookPath:j$1,uiTheme:t.ui_theme,flash:l}))}),e}function We(r,e=2e3){let o=r;return new Promise(t=>{let s=setTimeout(()=>{o.closeAllConnections?.();},e),n=setInterval(()=>o.closeIdleConnections?.(),100);r.close(()=>{clearTimeout(s),clearInterval(n),t();}),o.closeIdleConnections?.();})}async function Ze(r,e=fetch){try{let o=await e(`http://127.0.0.1:${r}/ping`,{signal:AbortSignal.timeout(1500)});if(!o.ok)return null;let t=await o.json();return t.ok===!0?{version:String(t.version??"")}:null}catch{return null}}async function Qe(r,e,o=Ze){if(e.code==="EADDRINUSE"){let t=await o(r);if(t){let s=`http://localhost:${r}`,n=t.version?` (v${t.version})`:"";return {existingUrl:s,exitCode:0,message:`${a.yellow("devcoach UI is already running at")} ${a.cyan(c$1(s))}${n}
|
|
637
|
-
${a.dim("Open it there, start another with --port <n>, or stop it with: devcoach ui --stop")}`}}return {exitCode:1,message:`${a.red(`\u2717 Port ${
|
|
638
|
-
${a.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a.red(`\u2717 Port ${
|
|
639
|
-
export{
|
|
727
|
+
document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(T(t.notebookContent))}));
|
|
728
|
+
</script>`;return E({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:n,scripts:l})}var J=25;function M(t,e="/lessons"){return t?.startsWith("/")&&!t.startsWith("//")?t:e}function m(t,e,o=""){let r=t[e];return typeof r=="string"?r:o}var Y=dirname(fileURLToPath(import.meta.url)),Ue=[join(Y,"../assets/static"),join(Y,"../../assets/static")].find(t=>existsSync(t))??join(Y,"../assets/static"),Xe={".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"},et="https://ultimaphoenix.github.io",He={"access-control-allow-origin":et,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function B(t){let e=t.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var V;function tt(){return V===void 0&&(V=a$1()),V}function De(t,e,o){let r=W({explicit:o.name??null,setting:e,gitUserName:tt()}),s=o.includeContext??false,n=V$1(t,{includeContext:s,sharedBy:r}),l=X(n);return {payload:n,state:{id:t.id,open:o.open??false,name:r??"",includeContext:s,text:Z(n,l),link:_(n,l)}}}function rt(t,e){return t==="lessons"?"/lessons":t==="knowledge"?"/knowledge":e?"/lessons":"/knowledge"}function ot(){try{return p(t=>J$1(t).ui_theme)}catch{return "system"}}function at(t={}){let e=new Hono;return e.get("/static/*",o=>{let r=decodeURIComponent(o.req.path.slice(8)),s=join(Ue,r);if(!s.startsWith(Ue))return o.notFound();try{let n=readFileSync(s);return new Response(new Uint8Array(n),{headers:{"content-type":Xe[extname(s)]??"application/octet-stream","cache-control":"no-cache"}})}catch{return o.notFound()}}),e.get("/ping",o=>o.json({ok:true,version:T$1},200,He)),e.options("/ping",o=>o.body(null,204,He)),e.post("/shutdown",o=>B(o)?o.text("Forbidden",403):(setTimeout(()=>t.onShutdown?.(),50),o.json({ok:true}))),e.get("/shutdown",o=>o.text("Method Not Allowed",405)),e.get("/",o=>{let{home:r$1,hasLessons:s}=p(n=>({home:J$1(n).ui_home,hasLessons:r(n)>0}));return o.redirect(rt(r$1,s),302)}),e.get("/knowledge",o=>{let{profile:r,stats:s,rateLimit:n,settings:l}=p(i=>({profile:ca(i),stats:fa(i),rateLimit:ba(i),settings:J$1(i)})),g=new Map;for(let i of r.groups)for(let p of i.topics)g.set(p,i.name);let c={};for(let i of r.groups)c[i.name]=[];for(let i of r.knowledge){let p=g.get(i.topic)??"Other",h=c[p]??[];h.push(i),c[p]=h;}return o.html(Me({categorised:c,allGroups:r.groups.map(i=>i.name),stats:s,rateLimit:n,maxPerDay:l.max_per_day,uiTheme:l.ui_theme}))}),e.post("/knowledge",async o=>{let r=await o.req.parseBody(),s=m(r,"topic").trim();if(s){let n=Number.parseInt(m(r,"confidence","5"),10)||5,l=m(r,"group").trim();p(g=>{D(g,s,n),l&&l!=="Other"&&H(g,s,l);});}return o.redirect("/knowledge",303)}),e.post("/knowledge/:topic/delete",o=>(p(r=>E$1(r,o.req.param("topic"))),o.redirect("/knowledge",303))),e.post("/knowledge/:topic/group",async o=>{let r=m(await o.req.parseBody(),"group").trim(),s=o.req.param("topic");return p(n=>{r&&r!=="Other"?H(n,s,r):I(n,s);}),o.redirect("/knowledge",303)}),e.post("/knowledge/:topic",async o=>{let r=Number.parseInt(m(await o.req.parseBody(),"delta","0"),10)||0;return p(s=>da(s,o.req.param("topic"),r)),o.redirect("/knowledge",303)}),e.post("/groups",async o=>{let r=m(await o.req.parseBody(),"group_name").trim();return r&&r!=="Other"&&p(s=>F$1(s,r)),o.redirect("/knowledge",303)}),e.post("/groups/:group_name/delete",o=>(p(r=>G$1(r,o.req.param("group_name"))),o.redirect("/knowledge",303))),e.get("/lessons/export",()=>{let o=p(r=>x(r));return new Response(JSON.stringify(o,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e.post("/lessons/import",async o=>{let r=await o.req.parseBody(),s=m(r,"from")==="lessons",n=i=>o.redirect(s?`/lessons?import=1&error=${i}`:"/settings?imported=0&skipped=0&invalid=1",303);if(B(o))return n("cross");let l=r.file,c$1=(l instanceof File&&l.size>0?await l.text():null)??m(r,"text").trim();try{c(c$1)&&!c$1.includes("#devcoach:lesson:")&&(c$1=await d(c$1));let i=p(p=>ga(p,c$1));return i.kind==="lessons"||!i.lesson?o.redirect(`/settings?imported=${i.inserted}&skipped=${i.duplicated}&invalid=${i.invalid}`,303):o.redirect(`/lessons/${encodeURIComponent(i.lesson.id)}?imported=${i.inserted?"1":"dup"}`,303)}catch(i){if(i instanceof U)return n("invalid");throw i}}),e.get("/lessons/import",o=>{let r=(o.req.query("code")??"").trim(),s=null,n=null;if(r)try{s=Y$1(r);}catch(l){n=l instanceof U?l.message:"That code could not be read.";}return o.html(Ae({code:r,preview:s,error:n,uiTheme:ot()}))}),e.get("/lessons",o=>{let r$1=o.req.query(),s$1=new Set(["junior","mid","senior"]),n=r$1.level&&s$1.has(r$1.level)?r$1.level:"",l=r$1.date_from||"",g=r$1.date_to||"",c=r$1.starred==="1",i=r$1.imported==="1"?"1":r$1.imported==="0"?"0":"",p$1=r$1.shared_by||null,h=r$1.sort||"timestamp",P=r$1.order==="asc"?"asc":"desc",_=Math.max(1,Number.parseInt(r$1.page??"1",10)||1),$=r$1.period||"all",R={period:l||g?null:$!=="all"?$:null,category:r$1.category||null,level:n||null,project:r$1.project||null,repository:r$1.repository||null,branch:r$1.branch||null,commit:r$1.commit||null,starred:c?true:null,imported:p$1||i==="1"?true:i==="0"?false:null,shared_by:p$1,search:r$1.search||null,feedback:r$1.feedback||null,date_from:l||null,date_to:g||null},k=p(b=>{let w=r(b,R),j=Math.max(1,Math.ceil(w/J));return _=Math.min(_,j),{total:w,totalPages:j,lessons:s(b,{...R,sort:h,order:P,page:_,per_page:J}),allCategories:B$1(b),allProjects:y$1(b,"project"),allRepositories:y$1(b,"repository"),allBranches:y$1(b,"branch"),allCommits:y$1(b,"commit_hash"),allSharedBy:z$1(b),theme:J$1(b).ui_theme}}),d={period:$,category:r$1.category||"",level:n,project:r$1.project||"",repository:r$1.repository||"",branch:r$1.branch||"",commit:r$1.commit||"",starred:c,imported:p$1?"1":i,shared_by:p$1??"",search:r$1.search||"",feedback:r$1.feedback||"",date_from:l,date_to:g,sort:h,order:P},x=r$1.error==="invalid"||r$1.error==="cross"?r$1.error:null;return o.html(Be({importOpen:r$1.import==="1",importError:x,lessons:k.lessons,allCategories:k.allCategories,allProjects:k.allProjects,allRepositories:k.allRepositories,allBranches:k.allBranches,allCommits:k.allCommits,allSharedBy:k.allSharedBy,s:d,page:_,perPage:J,total:k.total,totalPages:k.totalPages,uiTheme:k.theme}))}),e.post("/lessons/:lesson_id/star",async o=>{let r=await o.req.parseBody();return p(s=>v(s,o.req.param("lesson_id"),r.starred==="1")),o.redirect(M(m(r,"next")||void 0),303)}),e.post("/lessons/:lesson_id/feedback",async o=>{let r=await o.req.parseBody(),s=m(r,"feedback"),n=s===""||s==="clear"?null:s;return p(l=>ea(l,o.req.param("lesson_id"),n)),o.redirect(M(m(r,"next")||void 0),303)}),e.post("/lessons/delete",async o=>{if(B(o))return o.text("Forbidden",403);let r=await o.req.parseBody({all:true}),s=r.id,n=(Array.isArray(s)?s:[s]).filter(l=>typeof l=="string"&&l!=="");return p(l=>u(l,n)),o.redirect(M(m(r,"next")||void 0),303)}),e.on(["GET","POST"],"/lessons/:lesson_id/share",async o=>{if(B(o))return o.text("Forbidden",403);let r,s,n$1=false;if(o.req.method==="POST"){let p=await o.req.parseBody();r=m(p,"name"),s=m(p,"include_context")==="1",n$1=m(p,"persist")==="1";}else r=o.req.query("name"),s=o.req.query("include_context")==="1";let l=p(p=>{let h=A(p,o.req.param("lesson_id")??"");return h?(n$1&&r!==void 0&&K(p,"share_name",n(r)),{lesson:h,settingName:J$1(p).share_name}):null});if(!l)return o.text("Lesson not found",404);let{state:g,payload:c}=De(l.lesson,l.settingName,{name:r,includeContext:s}),i=o.req.query("format");return i==="text"?o.text(g.text):i==="link"?o.text(g.link):i==="md"?new Response(aa(c),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${$(c)}"`}}):i==="panel"?o.html(z(l.lesson.id,g,l.lesson.title)):o.html(F(g))}),e.get("/lessons/:lesson_id",o=>{let r=p(l=>{let g=A(l,o.req.param("lesson_id"));if(!g)return null;let c=J$1(l);return {lesson:g,settingName:c.share_name,uiTheme:c.ui_theme}});if(!r)return o.html("<h1>Lesson not found</h1>",404);let s=o.req.query(),n=s.imported==="1"?"new":s.imported==="dup"?"dup":null;return o.html(Oe({lesson:r.lesson,uiTheme:r.uiTheme,share:De(r.lesson,r.settingName,{open:s.share==="1"}).state,imported:n}))}),e.get("/settings/export",()=>{let o=p(r=>R(r));return new Response(new Uint8Array(o),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e.post("/settings/import",async o=>{let r=(await o.req.parseBody()).file;if(!(r instanceof File))return o.redirect("/settings?imported=0&skipped=0&invalid=1",303);let s=new Uint8Array(await r.arrayBuffer()),n=p(l=>S(l,s));return o.redirect(`/settings?imported=${n.lessons}&skipped=${n.skipped}&invalid=${n.invalid}&groups=${n.groups}¬ebook=${n.learning_state}`,303)}),e.get("/settings/notebook/download",()=>{let o=existsSync(k)?readFileSync(k,"utf8"):"";return new Response(o,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e.post("/settings/notebook",async o=>{let r=await o.req.parseBody();return mkdirSync(dirname(k),{recursive:true}),writeFileSync(k,m(r,"content"),"utf8"),o.redirect(M(m(r,"next")||void 0,"/settings?notebook_saved=1"),303)}),e.post("/settings",async o=>{let r=await o.req.parseBody(),s=Number.parseInt(m(r,"max_per_day","2"),10)||2,n$1=Number.parseInt(m(r,"min_gap_minutes","240"),10),l=m(r,"ui_theme","system");["system","dark","light"].includes(l)||(l="system");let g=m(r,"ui_home","auto");["auto","lessons","knowledge"].includes(g)||(g="auto");let c=Math.max(0,Number.parseInt(m(r,"nudge_every","10"),10)||0),i=m(r,"nudge_scope","session");return i!=="session"&&i!=="global"&&(i="session"),p(p=>{K(p,"max_per_day",String(s)),K(p,"min_gap_minutes",String(Number.isNaN(n$1)?240:n$1)),K(p,"ui_theme",l),K(p,"ui_home",g),K(p,"nudge_every",String(c)),K(p,"nudge_scope",i),K(p,"share_name",n(m(r,"share_name")));}),o.redirect("/settings",303)}),e.get("/settings",o=>{let r=p(g=>J$1(g)),s=existsSync(k)?readFileSync(k,"utf8"):"",n=o.req.query(),l=n.imported!==void 0?{imported:Number(n.imported??0),skipped:Number(n.skipped??0),invalid:Number(n.invalid??0),groups:Number(n.groups??0),notebook:Number(n.notebook??0)}:null;return o.html(Ne({settings:r,notebookContent:s,notebookPath:k,uiTheme:r.ui_theme,flash:l}))}),e}function st(t,e=2e3){let o=t;return new Promise(r=>{let s=setTimeout(()=>{o.closeAllConnections?.();},e),n=setInterval(()=>o.closeIdleConnections?.(),100);t.close(()=>{clearTimeout(s),clearInterval(n),r();}),o.closeIdleConnections?.();})}async function nt(t,e=fetch){try{let o=await e(`http://127.0.0.1:${t}/ping`,{signal:AbortSignal.timeout(1500)});if(!o.ok)return null;let r=await o.json();return r.ok===!0?{version:String(r.version??"")}:null}catch{return null}}async function it(t,e,o=nt){if(e.code==="EADDRINUSE"){let r=await o(t);if(r){let s=`http://localhost:${t}`,n=r.version?` (v${r.version})`:"";return {existingUrl:s,exitCode:0,message:`${a.yellow("devcoach UI is already running at")} ${a.cyan(c$1(s))}${n}
|
|
729
|
+
${a.dim("Open it there, start another with --port <n>, or stop it with: devcoach ui --stop")}`}}return {exitCode:1,message:`${a.red(`\u2717 Port ${t} is already in use by another process`)}
|
|
730
|
+
${a.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a.red(`\u2717 Port ${t} is not allowed (permission denied) \u2014 use a port \u2265 1024`)}:{exitCode:1,message:a.red(`\u2717 Could not start the devcoach UI on port ${t}: ${e.message}`)}}function $t(t,e={}){let o=false,r=(c,i)=>{if(o){i&&process.exit(130);return}o=true,process.stdout.isTTY&&console.log(a.dim(`devcoach UI stopping (${c})\u2026`)),st(n).then(()=>{i&&process.exit(0);});},s=at({onShutdown:()=>r("stop requested",false)}),n=serve({fetch:s.fetch,port:t,hostname:"127.0.0.1"},c=>{let i=`http://localhost:${c.port}`;console.log(`devcoach UI running at ${a.cyan(c$1(i))}`),process.stdout.isTTY&&console.log(a.dim("Press Ctrl+C to stop")),e.onReady?.(i);}),l=["SIGINT","SIGTERM","SIGHUP"],g={SIGINT:()=>r("SIGINT",true),SIGTERM:()=>r("SIGTERM",true),SIGHUP:()=>r("SIGHUP",true)};if(e.handleSignals!==false)for(let c of l)process.on(c,g[c]);return n.once("error",c=>{for(let i of l)process.off(i,g[i]);it(t,c).then(i=>{if(e.onListenError){e.onListenError(i);return}console.error(i.message),process.exit(i.exitCode);});}),n}async function St(t,e=fetch){try{return (await e(`http://127.0.0.1:${t}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
|
|
731
|
+
export{at as createApp,it as explainListenError,st as gracefulShutdown,nt as pingUi,rt as resolveHomePath,$t as startUi,St as stopUi};
|