devcoach 2.0.2 → 2.1.0-next.503.g8594664

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import {m,F,P,T,Q,z,D,A as A$1,E,R,B as B$1,C as C$1,t,u,o,v,x as x$1,p,r,S as S$1,w,N,O,j,G}from'./chunk-ECN4MNGM.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 C(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function ne(t,e){return t+e.replace(/[^A-Za-z0-9_-]/g,r=>`_${(r.codePointAt(0)??0).toString(16)}`)}function S(t){let e=(r,o,s)=>html`<a href="${r}" 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":""}">${o}</a>`;return html`<!DOCTYPE html>
2
+ import {c,d,a}from'./chunk-3RBMKXAA.js';import {Q,o,G,_,ca,$,A as A$1,E,B as B$1,F,aa,C,D as D$1,v,da,R,V,q,w as w$1,y as y$1,r,t,ba,x,H as H$1,m,Z,Y,O as O$1,P,j as j$1,T,S,U,X,W}from'./chunk-4KFSUF7S.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 w(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function we(t,e){return t+e.replace(/[^A-Za-z0-9_-]/g,r=>`_${(r.codePointAt(0)??0).toString(16)}`)}function j(t){let e=(r,a,s)=>html`<a href="${r}" 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":""}">${a}</a>`;return html`<!DOCTYPE html>
3
3
  <html lang="en">
4
4
  <head>
5
5
  <meta charset="UTF-8" />
@@ -60,7 +60,7 @@ import {m,F,P,T,Q,z,D,A as A$1,E,R,B as B$1,C as C$1,t,u,o,v,x as x$1,p,r,S as S
60
60
  </script>
61
61
  ${t.scripts??""}
62
62
  </body>
63
- </html>`}function ie(t){let e=o=>t.allGroups.filter(s=>s!=="Other").map(s=>html`<option value="${s}" ${s===o?"selected":""}>${s}</option>`),r=html`
63
+ </html>`}function $e(t){let e=a=>t.allGroups.filter(s=>s!=="Other").map(s=>html`<option value="${s}" ${s===a?"selected":""}>${s}</option>`),r=html`
64
64
  <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
65
 
66
66
  <div class="flex items-center justify-between mb-6">
@@ -119,12 +119,12 @@ import {m,F,P,T,Q,z,D,A as A$1,E,R,B as B$1,C as C$1,t,u,o,v,x as x$1,p,r,S as S
119
119
  ${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
120
  </div>
121
121
 
122
- ${Object.entries(t.categorised).map(([o,s])=>html`
122
+ ${Object.entries(t.categorised).map(([a,s])=>html`
123
123
  <section class="mb-6 group/section">
124
124
  <div class="flex items-center gap-2 mb-2 border-b border-gray-200 dark:border-gray-800 pb-1">
125
125
  <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">${o}</h2>
127
- ${o!=="Other"?html`<form x-show="editMode" style="display:none" method="post" action="/groups/${encodeURIComponent(o)}/delete" hx-post="/groups/${encodeURIComponent(o)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove group &quot;${o}&quot;? Topics will move to Other.">
126
+ <h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 shrink-0">${a}</h2>
127
+ ${a!=="Other"?html`<form x-show="editMode" style="display:none" method="post" action="/groups/${encodeURIComponent(a)}/delete" hx-post="/groups/${encodeURIComponent(a)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove group &quot;${a}&quot;? Topics will move to Other.">
128
128
  <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
129
  </form>`:""}
130
130
  </div>
@@ -132,7 +132,7 @@ ${Object.entries(t.categorised).map(([o,s])=>html`
132
132
  <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
133
  <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
134
  <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="${o!=="Other"?o:""}" />
135
+ <input type="hidden" name="group" value="${a!=="Other"?a:""}" />
136
136
  <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
137
  <div class="flex items-center gap-2">
138
138
  <label class="text-xs text-gray-400 dark:text-gray-500 shrink-0">Confidence</label>
@@ -144,45 +144,45 @@ ${Object.entries(t.categorised).map(([o,s])=>html`
144
144
  </div>
145
145
  </div>
146
146
  <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-0">
147
- ${s.map(i=>{let g=ne("topic-row-",i.topic),d=i.confidence,l=Math.trunc(d/10*100),m=d>=7?"bg-green-500":d>=4?"bg-yellow-500":"bg-red-500",b=d>=7?"text-green-600 dark:text-green-400":d>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
147
+ ${s.map(n=>{let g=we("topic-row-",n.topic),l=n.confidence,d=Math.trunc(l/10*100),c=l>=7?"bg-green-500":l>=4?"bg-yellow-500":"bg-red-500",x=l>=7?"text-green-600 dark:text-green-400":l>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
148
148
  <div id="${g}" class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
149
149
  <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative shrink-0 flex items-center">
150
150
  <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
151
  <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
- <form method="post" action="/knowledge/${encodeURIComponent(i.topic)}/group" hx-post="/knowledge/${encodeURIComponent(i.topic)}/group" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML">
152
+ <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
153
  <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" ${o==="Other"?"selected":""}>Other (ungrouped)</option>
155
- ${e(o)}
154
+ <option value="Other" ${a==="Other"?"selected":""}>Other (ungrouped)</option>
155
+ ${e(a)}
156
156
  </select>
157
157
  </form>
158
158
  </div>
159
159
  </div>
160
160
  <div class="flex items-center gap-1 flex-1 min-w-0">
161
- <a x-show="!editMode" href="/lessons?search=${encodeURIComponent(i.topic)}" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate hover:text-indigo-600 dark:hover:text-indigo-400 hover:underline transition" title="${i.topic}">${i.topic}</a>
162
- <span x-show="editMode" style="display:none" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate" title="${i.topic}">${i.topic}</span>
163
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(i.topic)}/delete" hx-post="/knowledge/${encodeURIComponent(i.topic)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove ${i.topic} from your knowledge map?" class="shrink-0 flex items-center">
161
+ <a x-show="!editMode" href="/lessons?search=${encodeURIComponent(n.topic)}" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate hover:text-indigo-600 dark:hover:text-indigo-400 hover:underline transition" title="${n.topic}">${n.topic}</a>
162
+ <span x-show="editMode" style="display:none" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate" title="${n.topic}">${n.topic}</span>
163
+ <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(n.topic)}/delete" hx-post="/knowledge/${encodeURIComponent(n.topic)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove ${n.topic} from your knowledge map?" class="shrink-0 flex items-center">
164
164
  <button type="submit" class="text-xs text-gray-200 dark:text-gray-700 hover:text-red-500 dark:hover:text-red-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Remove topic">×</button>
165
165
  </form>
166
166
  </div>
167
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(i.topic)}" hx-post="/knowledge/${encodeURIComponent(i.topic)}" hx-target="#${g}" hx-select="#${g}" hx-swap="outerHTML" class="shrink-0 flex items-center">
167
+ <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(n.topic)}" hx-post="/knowledge/${encodeURIComponent(n.topic)}" hx-target="#${g}" hx-select="#${g}" hx-swap="outerHTML" class="shrink-0 flex items-center">
168
168
  <input type="hidden" name="delta" value="-1" />
169
169
  <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
170
  </form>
171
171
  <div class="w-20 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
172
- <div class="${m} h-full rounded-full transition-all" style="width: ${l}%"></div>
172
+ <div class="${c} h-full rounded-full transition-all" style="width: ${d}%"></div>
173
173
  </div>
174
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(i.topic)}" hx-post="/knowledge/${encodeURIComponent(i.topic)}" hx-target="#${g}" hx-select="#${g}" hx-swap="outerHTML" class="shrink-0 flex items-center">
174
+ <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(n.topic)}" hx-post="/knowledge/${encodeURIComponent(n.topic)}" hx-target="#${g}" hx-select="#${g}" hx-swap="outerHTML" class="shrink-0 flex items-center">
175
175
  <input type="hidden" name="delta" value="1" />
176
176
  <button class="text-xs text-gray-300 dark:text-gray-600 hover:text-green-600 dark:hover:text-green-400 transition w-5 text-center leading-none">+</button>
177
177
  </form>
178
- <span class="text-xs font-bold ${b} w-5 text-right shrink-0">${d}</span>
178
+ <span class="text-xs font-bold ${x} w-5 text-right shrink-0">${l}</span>
179
179
  </div>`})}
180
180
  </div>
181
181
  </section>`)}
182
- </div>`;return S({title:"Profile \u2014 devcoach",currentPath:"/",uiTheme:t.uiTheme,body:r})}var oe={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},ae={know:"\u2713 Known",dont_know:"\u2717 Don't know",none:"\u2014 No response"},se={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function x(t,e={}){let r={period:t.period,category:t.category,level:t.level,project:t.project,repository:t.repository,branch:t.branch,commit:t.commit,feedback:t.feedback,search:t.search,date_from:t.date_from,date_to:t.date_to,sort:t.sort,order:t.order};t.starred&&(r.starred="1"),Object.assign(r,e);let o=new URLSearchParams;for(let[s,i]of Object.entries(r))i&&o.set(s,i);return `?${o.toString()}`}function de(t){let e=t.s,r=!!(e.date_from||e.date_to),o=e.period!=="all"&&!r||r||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.search||e.feedback),s=r?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:oe[e.period]??"All time",i=e.feedback?ae[e.feedback]??"Feedback":"Feedback",g={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"},d=(n,c)=>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">${n}<a href="${c}" 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>`,l=(n,c,f="")=>{let w=e.sort===c,j=w&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap ${f}">
183
- <a href="${x(e,{sort:c,order:j})}" class="inline-flex items-center gap-1 hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${n}
184
- ${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>`}
185
- </a></th>`},m=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>`,b=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
182
+ </div>`;return j({title:"Profile \u2014 devcoach",currentPath:"/",uiTheme:t.uiTheme,body:r})}var fe={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},ke={know:"\u2713 Known",dont_know:"\u2717 Don't know",none:"\u2014 No response"},ve={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function y(t,e={}){let r={period:t.period,category:t.category,level:t.level,project:t.project,repository:t.repository,branch:t.branch,commit:t.commit,feedback:t.feedback,search:t.search,date_from:t.date_from,date_to:t.date_to,sort:t.sort,order:t.order};t.starred&&(r.starred="1"),Object.assign(r,e);let a=new URLSearchParams;for(let[s,n]of Object.entries(r))n&&a.set(s,n);return `?${a.toString()}`}function _e(t){let e=t.s,r=!!(e.date_from||e.date_to),a=e.period!=="all"&&!r||r||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.search||e.feedback),s=r?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:fe[e.period]??"All time",n=e.feedback?ke[e.feedback]??"Feedback":"Feedback",g={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"},l=(i,u)=>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">${i}<a href="${u}" 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>`,d=(i,u,b="")=>{let _=e.sort===u,S=_&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap ${b}">
183
+ <a href="${y(e,{sort:u,order:S})}" class="inline-flex items-center gap-1 hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${i}
184
+ ${_?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>`}
185
+ </a></th>`},c=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>`,x=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
186
186
  <style>
187
187
  .dark .flatpickr-calendar { background:#1f2937; border-color:#374151; }
188
188
  .dark .flatpickr-day { color:#e5e7eb; }
@@ -193,7 +193,7 @@ ${Object.entries(t.categorised).map(([o,s])=>html`
193
193
  .dark .flatpickr-current-month, .dark .flatpickr-monthDropdown-months { color:#e5e7eb; background:#1f2937; }
194
194
  .dark .flatpickr-weekday { color:#9ca3af; background:#1f2937; }
195
195
  .dark .flatpickr-prev-month svg, .dark .flatpickr-next-month svg { fill:#9ca3af; }
196
- </style>`,v=t.total===0?"No lessons":t.totalPages===1?`${t.total} lesson${t.total!==1?"s":""}`:`${(t.page-1)*t.perPage+1}\u2013${Math.min(t.page*t.perPage,t.total)} of ${t.total}`,$=html`
196
+ </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}`,I=html`
197
197
  <form id="filter-form" method="get" action="/lessons">
198
198
  <input type="hidden" name="period" id="h-period" value="${e.period}">
199
199
  <input type="hidden" name="date_from" id="h-date-from" value="${e.date_from}">
@@ -213,7 +213,7 @@ ${Object.entries(t.categorised).map(([o,s])=>html`
213
213
  <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
214
  ${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
215
  </div>
216
- <p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${v}</p>
216
+ <p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${$}</p>
217
217
  </div>
218
218
 
219
219
  <div class="flex flex-wrap items-center gap-2 mb-3">
@@ -222,11 +222,11 @@ ${Object.entries(t.categorised).map(([o,s])=>html`
222
222
 
223
223
  <div class="relative" x-data="periodPicker()" @keydown.escape="close()">
224
224
  <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 ${r||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>${m}
225
+ <span>📅</span><span x-text="label">${s}</span>${c}
226
226
  </button>
227
227
  <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
228
  <div class="p-1">
229
- ${Object.entries(oe).map(([n,c])=>{let f=e.period===n&&!r;return html`<button type="button" @click="selectPreset('${n}', '${c}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${f?"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"}">${c}${f?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
229
+ ${Object.entries(fe).map(([i,u])=>{let b=e.period===i&&!r;return html`<button type="button" @click="selectPreset('${i}', '${u}')" 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"}">${u}${b?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
230
230
  </div>
231
231
  <div class="border-t border-gray-100 dark:border-gray-800 p-1">
232
232
  <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 ${r?"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,80 +241,97 @@ ${Object.entries(t.categorised).map(([o,s])=>html`
241
241
  </div>
242
242
 
243
243
  <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"}">${i}${m}</button>
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}${c}</button>
245
245
  <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(([n,c])=>html`<button type="button" onclick="document.getElementById('h-feedback').value='${n}'; 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===n?"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"}">${c}${e.feedback===n?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
246
+ ${[["","All feedback"],["know","\u2713 Known"],["dont_know","\u2717 Don't know"],["none","\u2014 No response"]].map(([i,u])=>html`<button type="button" onclick="document.getElementById('h-feedback').value='${i}'; 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===i?"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"}">${u}${e.feedback===i?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
247
247
  </div>
248
248
  </div>
249
249
 
250
250
  <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?`${se[e.level]??""} ${e.level}`:"Level"}${m}</button>
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?`${ve[e.level]??""} ${e.level}`:"Level"}${c}</button>
252
252
  <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(([n,c])=>html`<button type="button" onclick="document.getElementById('h-level').value='${n}'; 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===n?"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"}">${c}${e.level===n?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
253
+ ${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([i,u])=>html`<button type="button" onclick="document.getElementById('h-level').value='${i}'; 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===i?"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"}">${u}${e.level===i?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
254
254
  </div>
255
255
  </div>
256
256
 
257
257
  ${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">
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${m}</button>
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${c}</button>
259
259
  <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
- ${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(n=>html`<option value="${n}" ${e.category===n?"selected":""}>${n}</option>`)}</select></div>`:""}
261
- ${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(n=>html`<option value="${n}" ${e.project===n?"selected":""}>${n}</option>`)}</select></div>`:""}
262
- ${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(n=>html`<option value="${n}" ${e.repository===n?"selected":""}>${n}</option>`)}</select></div>`:""}
263
- ${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(n=>html`<option value="${n}">`)}</datalist></div>`:""}
264
- ${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(n=>html`<option value="${n.slice(0,7)}">`)}</datalist></div>`:""}
260
+ ${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(i=>html`<option value="${i}" ${e.category===i?"selected":""}>${i}</option>`)}</select></div>`:""}
261
+ ${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(i=>html`<option value="${i}" ${e.project===i?"selected":""}>${i}</option>`)}</select></div>`:""}
262
+ ${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(i=>html`<option value="${i}" ${e.repository===i?"selected":""}>${i}</option>`)}</select></div>`:""}
263
+ ${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(i=>html`<option value="${i}">`)}</datalist></div>`:""}
264
+ ${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(i=>html`<option value="${i.slice(0,7)}">`)}</datalist></div>`:""}
265
265
  <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
266
  </div>
267
267
  </div>`:""}
268
268
 
269
- ${o?html`<a href="/lessons" class="ml-auto text-xs text-gray-400 hover:text-gray-700 dark:hover:text-white transition">Clear all</a>`:""}
269
+ ${a?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
+
271
+ <div class="relative ${a?"":"ml-auto"}" x-data="{ open: ${String(t.importOpen)} }" @click.outside="open = false" @keydown.escape="open = false">
272
+ <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
+ <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
+ <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
+ ${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
+ <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
+ <div class="flex items-center gap-2">
278
+ <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>
279
+ <button type="submit" form="import-form" class="ml-auto bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg px-3 py-1.5 text-xs font-medium transition">Import</button>
280
+ </div>
281
+ </div>
282
+ </div>
270
283
  </div>
271
284
 
272
- ${o?html`<div class="flex flex-wrap gap-1.5 mb-4">
273
- ${r?d(`\u{1F4C5} ${s}`,x(e,{date_from:"",date_to:""})):""}
274
- ${e.period!=="all"&&!r?d(`\u{1F550} ${s}`,x(e,{period:"all"})):""}
275
- ${e.feedback?d(ae[e.feedback]??e.feedback,x(e,{feedback:""})):""}
276
- ${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="${x({...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>`:""}
277
- ${e.search?d(`\u{1F50D} "${e.search}"`,x(e,{search:""})):""}
278
- ${e.category?d(e.category,x(e,{category:""})):""}
279
- ${e.level?d(`${se[e.level]??""} ${e.level}`,x(e,{level:""})):""}
280
- ${e.project?d(`\u{1F4C1} ${e.project}`,x(e,{project:""})):""}
281
- ${e.repository?d(`\u2387 ${e.repository}`,x(e,{repository:""})):""}
282
- ${e.branch?d(`\u2387 ${e.branch}`,x(e,{branch:""})):""}
283
- ${e.commit?d(e.commit.slice(0,7),x(e,{commit:""})):""}
285
+ ${a?html`<div class="flex flex-wrap gap-1.5 mb-4">
286
+ ${r?l(`\u{1F4C5} ${s}`,y(e,{date_from:"",date_to:""})):""}
287
+ ${e.period!=="all"&&!r?l(`\u{1F550} ${s}`,y(e,{period:"all"})):""}
288
+ ${e.feedback?l(ke[e.feedback]??e.feedback,y(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="${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>`:""}
290
+ ${e.search?l(`\u{1F50D} "${e.search}"`,y(e,{search:""})):""}
291
+ ${e.category?l(e.category,y(e,{category:""})):""}
292
+ ${e.level?l(`${ve[e.level]??""} ${e.level}`,y(e,{level:""})):""}
293
+ ${e.project?l(`\u{1F4C1} ${e.project}`,y(e,{project:""})):""}
294
+ ${e.repository?l(`\u2387 ${e.repository}`,y(e,{repository:""})):""}
295
+ ${e.branch?l(`\u2387 ${e.branch}`,y(e,{branch:""})):""}
296
+ ${e.commit?l(e.commit.slice(0,7),y(e,{commit:""})):""}
284
297
  </div>`:""}
285
298
  </form>
299
+ <form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="hidden"><input type="hidden" name="from" value="lessons" /></form>
300
+ <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>
286
301
 
287
302
  ${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">
288
303
  <table class="w-full text-sm">
289
304
  <thead>
290
305
  <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">
291
306
  <th class="w-8 px-3 py-3"></th>
292
- ${l("Date","timestamp")}
293
- ${l("Topic","topic_id","hidden sm:table-cell")}
294
- ${l("Title","title")}
295
- ${l("Level","level")}
307
+ ${d("Date","timestamp")}
308
+ ${d("Topic","topic_id","hidden sm:table-cell")}
309
+ ${d("Title","title")}
310
+ ${d("Level","level")}
296
311
  <th class="px-3 py-3 hidden lg:table-cell">Categories</th>
297
- ${l("Feedback","feedback","hidden xl:table-cell")}
312
+ ${d("Feedback","feedback","hidden xl:table-cell")}
313
+ <th class="px-2 py-3 w-8"><span class="sr-only">Share</span></th>
298
314
  </tr>
299
315
  </thead>
300
316
  <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
301
- ${t.lessons.map(n=>{let c=ne("lesson-row-",n.id),f=n.timestamp.slice(0,10),w=n.timestamp.slice(0,16).replace("T"," ");return html`<tr id="${c}" class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" onclick="window.location='/lessons/${encodeURIComponent(n.id)}'" onkeydown="if(event.key==='Enter')window.location='/lessons/${encodeURIComponent(n.id)}'" role="link">
317
+ ${t.lessons.map(i=>{let u=we("lesson-row-",i.id),b=i.timestamp.slice(0,10),_=i.timestamp.slice(0,16).replace("T"," ");return html`<tr id="${u}" class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" onclick="window.location='/lessons/${encodeURIComponent(i.id)}'" onkeydown="if(event.key==='Enter')window.location='/lessons/${encodeURIComponent(i.id)}'" role="link">
302
318
  <td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">
303
- <form method="post" action="/lessons/${encodeURIComponent(n.id)}/star" hx-post="/lessons/${encodeURIComponent(n.id)}/star" hx-target="#${c}" hx-select="#${c}" hx-swap="outerHTML">
304
- <input type="hidden" name="starred" value="${n.starred?"0":"1"}" />
305
- <input type="hidden" name="next" value="/lessons${x(e)}" />
306
- <button type="submit" title="${n.starred?"Unstar":"Star"}" class="w-6 text-lg text-center leading-none transition ${n.starred?"text-yellow-400 hover:text-yellow-300":"text-gray-300 dark:text-gray-600 hover:text-yellow-400"}">${n.starred?"\u2605":"\u2606"}</button>
319
+ <form method="post" action="/lessons/${encodeURIComponent(i.id)}/star" hx-post="/lessons/${encodeURIComponent(i.id)}/star" hx-target="#${u}" hx-select="#${u}" hx-swap="outerHTML">
320
+ <input type="hidden" name="starred" value="${i.starred?"0":"1"}" />
321
+ <input type="hidden" name="next" value="/lessons${y(e)}" />
322
+ <button type="submit" title="${i.starred?"Unstar":"Star"}" class="w-6 text-lg text-center leading-none transition ${i.starred?"text-yellow-400 hover:text-yellow-300":"text-gray-300 dark:text-gray-600 hover:text-yellow-400"}">${i.starred?"\u2605":"\u2606"}</button>
307
323
  </form>
308
324
  </td>
309
325
  <td class="px-3 py-3 whitespace-nowrap tabular-nums relative group/date">
310
- <span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${n.timestamp}">${f}</span>
311
- <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>
326
+ <span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${i.timestamp}">${b}</span>
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">${_}</div>
312
328
  </td>
313
- <td class="px-3 py-3 hidden sm:table-cell"><span class="text-xs font-mono text-cyan-600 dark:text-cyan-400">${n.topic_id}</span></td>
314
- <td class="px-3 py-3 max-w-xs"><a href="/lessons/${encodeURIComponent(n.id)}" class="font-medium text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${n.title}</a></td>
315
- <td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><a href="${x(e,{level:n.level})}" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${g[n.level]??""} hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${n.level}</a></td>
316
- <td class="px-3 py-3 hidden lg:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><div class="flex flex-wrap gap-1">${n.categories.map(j=>html`<a href="${x(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>
317
- <td class="px-3 py-3 hidden xl:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">${n.feedback==="know"?html`<span class="text-xs text-teal-600 dark:text-teal-400 font-medium">✓ Known</span>`:n.feedback==="dont_know"?html`<span class="text-xs text-rose-500 dark:text-rose-400 font-medium">✗ Unknown</span>`:""}</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">${i.topic_id}</span></td>
330
+ <td class="px-3 py-3 max-w-xs"><a href="/lessons/${encodeURIComponent(i.id)}" class="font-medium text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${i.title}</a></td>
331
+ <td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><a href="${y(e,{level:i.level})}" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${g[i.level]??""} hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${i.level}</a></td>
332
+ <td class="px-3 py-3 hidden lg:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><div class="flex flex-wrap gap-1">${i.categories.map(S=>html`<a href="${y(e,{category:S})}" 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">${S}</a>`)}</div></td>
333
+ <td class="px-3 py-3 hidden xl:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">${i.feedback==="know"?html`<span class="text-xs text-teal-600 dark:text-teal-400 font-medium">✓ Known</span>`:i.feedback==="dont_know"?html`<span class="text-xs text-rose-500 dark:text-rose-400 font-medium">✗ Unknown</span>`:""}</td>
334
+ <td class="px-2 py-3 text-center" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><a href="/lessons/${encodeURIComponent(i.id)}?share=1" title="Share this lesson" class="text-gray-300 dark:text-gray-600 hover:text-indigo-500 dark:hover:text-indigo-400 transition text-base leading-none">↗</a></td>
318
335
  </tr>`})}
319
336
  </tbody>
320
337
  </table>
@@ -322,22 +339,23 @@ ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
322
339
  ${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
323
340
  <p class="text-xs text-gray-400 dark:text-gray-500">Page ${t.page} of ${t.totalPages}</p>
324
341
  <div class="flex items-center gap-1">
325
- ${t.page>1?html`<a href="${x(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>`}
326
- ${me(t.page,t.totalPages).map(n=>n===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:n===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">${n}</span>`:html`<a href="${x(e,{page:String(n)})}" 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">${n}</a>`)}
327
- ${t.page<t.totalPages?html`<a href="${x(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>`}
342
+ ${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>`}
343
+ ${Ee(t.page,t.totalPages).map(i=>i===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:i===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">${i}</span>`:html`<a href="${y(e,{page:String(i)})}" 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">${i}</a>`)}
344
+ ${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>`}
328
345
  </div>
329
346
  </div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
330
347
  <p class="text-3xl mb-3">📭</p>
331
348
  <p class="text-gray-500 dark:text-gray-400 text-sm">No lessons match the current filters.</p>
332
- ${o?html`<a href="/lessons" class="mt-2 text-indigo-500 hover:text-indigo-400 text-sm transition">Clear all filters</a>`:""}
333
- </div>`}`,u=html`<script src="/static/vendor/flatpickr.min.js"></script>
349
+ ${a?html`<a href="/lessons" class="mt-2 text-indigo-500 hover:text-indigo-400 text-sm transition">Clear all filters</a>`:""}
350
+ </div>`}`,h=html`<script src="/static/vendor/flatpickr.min.js"></script>
334
351
  <script src="/static/relative-time.js"></script>
352
+ <script src="/static/share.js"></script>
335
353
  <script>
336
354
  function periodPicker() {
337
355
  return {
338
356
  open: false,
339
357
  showCustom: ${raw(r?"true":"false")},
340
- label: ${raw(C(s))},
358
+ label: ${raw(w(s))},
341
359
  fp: null,
342
360
  init() {
343
361
  const self = this;
@@ -345,7 +363,7 @@ function periodPicker() {
345
363
  if (val && !this.fp) {
346
364
  this.fp = flatpickr(this.$refs.fp, {
347
365
  mode: 'range', dateFormat: 'Y-m-d', inline: false,
348
- ${raw(e.date_from?`defaultDate: [${C(e.date_from)}${e.date_to?`, ${C(e.date_to)}`:""}],`:"")}
366
+ ${raw(e.date_from?`defaultDate: [${w(e.date_from)}${e.date_to?`, ${w(e.date_to)}`:""}],`:"")}
349
367
  onChange(dates) {
350
368
  if (dates.length === 2) {
351
369
  document.getElementById('h-date-from').value = self.fp.formatDate(dates[0], 'Y-m-d');
@@ -368,8 +386,16 @@ function periodPicker() {
368
386
  },
369
387
  };
370
388
  }
371
- </script>`;return S({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:b,body:$,scripts:u})}function me(t,e){let r=[],o=0;for(let s=1;s<=e;s++)(s===1||s===e||s>=t-2&&s<=t+2)&&(o&&s-o>1&&r.push(0),r.push(s),o=s);return r}var L={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function le(t){let e=t.lesson,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",o=e.repository_platform==="local",s=null;o&&e.repository?s=`vscode://file/${e.repository}`:e.repository_platform&&L[e.repository_platform]&&e.repository&&(s=`https://${L[e.repository_platform]}/${e.repository}`);let i=null;s&&e.commit_hash&&!o&&(i=e.repository_platform==="gitlab"?`${s}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${s}/commits/${e.commit_hash}`:`${s}/commit/${e.commit_hash}`);let g=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),d=e.timestamp.slice(0,10),l=e.timestamp.slice(0,16).replace("T"," "),m=`hx-post="/lessons/${encodeURIComponent(e.id)}/feedback" hx-target="#lesson-meta" hx-select="#lesson-meta" hx-swap="outerHTML" hx-select-oob="#lesson-feedback:outerHTML"`,b=html`
389
+ </script>`;return j({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:x,body:I,scripts:h})}function Ee(t,e){let r=[],a=0;for(let s=1;s<=e;s++)(s===1||s===e||s>=t-2&&s<=t+2)&&(a&&s-a>1&&r.push(0),r.push(s),a=s);return r}var B={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function N(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
+ <div class="flex flex-wrap gap-2">
391
+ <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
+ <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
+ <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
+ </div>
395
+ <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>
396
+ </div>`}function Se(t){let e=t.lesson,r=t.share,a=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&&B[e.repository_platform]&&e.repository&&(n=`https://${B[e.repository_platform]}/${e.repository}`);let g=null;n&&e.commit_hash&&!s&&(g=e.repository_platform==="gitlab"?`${n}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${n}/commits/${e.commit_hash}`:`${n}/commit/${e.commit_hash}`);let l=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),d=e.timestamp.slice(0,10),c=e.timestamp.slice(0,16).replace("T"," "),x=`hx-post="/lessons/${encodeURIComponent(e.id)}/feedback" hx-target="#lesson-meta" hx-select="#lesson-meta" hx-swap="outerHTML" hx-select-oob="#lesson-feedback:outerHTML"`,$=html`
372
397
  <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
+ ${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>`:""}
373
399
  <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
374
400
  <div class="flex flex-wrap items-center gap-3 mb-2">
375
401
  <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">
@@ -378,16 +404,32 @@ function periodPicker() {
378
404
  <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>
379
405
  </form>
380
406
  <h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.title}</h1>
381
- <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>
407
+ <a href="/lessons?level=${e.level}" class="text-xs font-semibold px-2 py-0.5 rounded-full border ${a} shrink-0 hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${e.level}</a>
408
+ <div class="relative shrink-0" x-data="{ open: ${String(r.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 !text-xs !px-2.5 !py-1">↗ Share</button>
410
+ <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
+ <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Share this lesson</p>
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="${r.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" ${r.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
+ ${N(r)}
421
+ </div>
422
+ </div>
382
423
  </div>
383
424
  <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">
384
425
  <span class="relative group/date cursor-default">🗓 <span data-ts="${e.timestamp}">${d}</span>
385
- <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">${l}</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">${c}</span>
386
427
  </span>
387
428
  <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></span>
388
- ${e.categories.map(u=>html`<a href="/lessons?category=${encodeURIComponent(u)}" 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">${u}</a>`)}
429
+ ${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(i=>html`<a href="/lessons?category=${encodeURIComponent(i)}" class="inline-block bg-gray-100 dark:bg-gray-800 hover:bg-indigo-50 dark:hover:bg-indigo-900/50 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 transition border border-gray-200 dark:border-gray-700 hover:border-indigo-400 dark:hover:border-indigo-600">${i}</a>`)}
389
431
  ${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>`}
390
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(m)}><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>`:""}
432
+ <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(x)}><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>`:""}
391
433
  </div>
392
434
  <div class="my-5 pl-4 border-l-4 border-indigo-400 dark:border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 rounded-r-lg py-3 pr-4">
393
435
  <p class="text-xs font-semibold uppercase tracking-wide text-indigo-500 dark:text-indigo-400 mb-1">TL;DR</p>
@@ -395,21 +437,23 @@ function periodPicker() {
395
437
  </div>
396
438
  <div id="body-content" class="markdown-body"></div>
397
439
  ${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>`:""}
398
- ${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">
399
- ${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${s?html`<a href="${s}" ${o?"":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>`:""}
400
- ${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&&L[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="" />`:""}${s?html`<a href="${s}" ${o?"":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>`:""}
440
+ ${l?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
+ ${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&&B[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>`:""}
401
443
  ${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>`:""}
402
- ${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${i?html`<a href="${i}" 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>`:""}
444
+ ${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${g?html`<a href="${g}" 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>`:""}
403
445
  ${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>`:""}
404
446
  </div></div>`:""}
405
447
  <div id="lesson-feedback">${e.feedback?"":html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 flex flex-wrap gap-2">
406
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(m)}><input type="hidden" name="feedback" value="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-green-100 dark:hover:bg-green-800 hover:text-green-700 dark:hover:text-white">✓ I know this</button></form>
407
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(m)}><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>
448
+ <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(x)}><input type="hidden" name="feedback" value="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-green-100 dark:hover:bg-green-800 hover:text-green-700 dark:hover:text-white">✓ I know this</button></form>
449
+ <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(x)}><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>
408
450
  </div>`}</div>
409
451
  <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>
410
- </div>`,v=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,$=html`<script src="/static/vendor/highlight.min.js"></script>
452
+ </div>`,I=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,h=html`<script src="/static/vendor/highlight.min.js"></script>
411
453
  <script src="/static/vendor/marked.min.js"></script>
454
+ <script src="/static/vendor/purify.min.js"></script>
412
455
  <script src="/static/relative-time.js"></script>
456
+ <script src="/static/share.js"></script>
413
457
  <script>
414
458
  updateHljsTheme();
415
459
  marked.setOptions({
@@ -419,12 +463,64 @@ function periodPicker() {
419
463
  },
420
464
  breaks: true, gfm: true,
421
465
  });
422
- document.getElementById('summary-content').innerHTML = marked.parse(${raw(C(e.summary))});
423
- document.getElementById('body-content').innerHTML = marked.parse(${raw(C(e.body))});
424
- </script>`;return S({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:v,body:b,scripts:$})}var xe=[[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 ge(t){let e=t.settings.min_gap_minutes,r=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],o=t.flash,s=html`
466
+ document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(w(e.summary))}));
467
+ document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(w(e.body))}));
468
+ </script>`;return j({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:I,body:$,scripts:h})}var Me={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 Ce(t){let e=t.preview,r=html`<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="space-y-3">
469
+ <input type="hidden" name="from" value="lessons" />
470
+ <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
+ <div class="flex items-center gap-3">
472
+ <label class="cursor-pointer text-xs text-gray-500 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">📄 Choose a .devcoach.md file<input id="import-file" type="file" name="file" accept=".md,.json,.txt" class="hidden" onchange="document.getElementById('import-form').submit()" /></label>
473
+ <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
+ </div>
475
+ <p class="text-xs text-gray-400 dark:text-gray-500">You can also drop the file anywhere on this page.</p>
476
+ </form>`,a=html`
477
+ <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
+ <div class="max-w-3xl mx-auto bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
479
+ <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
+ ${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
+ ${e?html`<div class="flex flex-wrap items-center gap-3 mb-2">
482
+ <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 ${Me[e.lesson.level]??""} shrink-0">${e.lesson.level}</span>
484
+ </div>
485
+ <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
+ <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.lesson.topic_id}</span></span>
487
+ ${e.lesson.categories.map(g=>html`<span class="inline-block bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 border border-gray-200 dark:border-gray-700">${g}</span>`)}
488
+ <span>🤝 shared by <span class="text-gray-700 dark:text-gray-200">${e.shared_by??"anonymous"}</span> · ${e.shared_at.slice(0,10)}</span>
489
+ </div>
490
+ <div class="my-5 pl-4 border-l-4 border-indigo-400 dark:border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 rounded-r-lg py-3 pr-4">
491
+ <p class="text-xs font-semibold uppercase tracking-wide text-indigo-500 dark:text-indigo-400 mb-1">TL;DR</p>
492
+ <div id="summary-content" class="markdown-body text-sm text-indigo-900 dark:text-indigo-100"></div>
493
+ </div>
494
+ <details class="group" ${e.lesson.body?"":"hidden"}>
495
+ <summary class="cursor-pointer text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-500 select-none">Read the full lesson</summary>
496
+ <div id="body-content" class="markdown-body mt-3"></div>
497
+ </details>
498
+ ${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
+ <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
+ <input type="hidden" name="from" value="lessons" />
501
+ <input type="hidden" name="text" value="${t.code}" />
502
+ <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
+ <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
+ </form>`:r}
505
+ </div>`,s=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,n=html`<script src="/static/vendor/highlight.min.js"></script>
506
+ <script src="/static/vendor/marked.min.js"></script>
507
+ <script src="/static/vendor/purify.min.js"></script>
508
+ <script src="/static/share.js"></script>
509
+ <script>
510
+ updateHljsTheme();
511
+ marked.setOptions({
512
+ highlight: function(code, lang) {
513
+ const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext';
514
+ return hljs.highlight(code, { language }).value;
515
+ },
516
+ breaks: true, gfm: true,
517
+ });
518
+ ${e?raw(`document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${w(e.lesson.summary)}));
519
+ document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${w(e.lesson.body??"")}));`):""}
520
+ </script>`;return j({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:s,body:a,scripts:n})}var Be=[[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 Ie(t){let e=t.settings.min_gap_minutes,r=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],a=t.flash,s=html`
425
521
  <h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
426
- ${o?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${o.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"}">
427
- ${o.imported} lesson${o.imported!==1?"s":""} imported.${o.skipped?` ${o.skipped} skipped (already in DB).`:""}${o.invalid?` ${o.invalid} rejected (failed validation).`:""}${o.groups?` ${o.groups} group${o.groups!==1?"s":""} added.`:""}${o.notebook?" Notebook restored.":""}
522
+ ${a?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${a.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"}">
523
+ ${a.imported} lesson${a.imported!==1?"s":""} imported.${a.skipped?` ${a.skipped} skipped (already in DB).`:""}${a.invalid?` ${a.invalid} rejected (failed validation).`:""}${a.groups?` ${a.groups} group${a.groups!==1?"s":""} added.`:""}${a.notebook?" Notebook restored.":""}
428
524
  </div>`:""}
429
525
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
430
526
  <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
@@ -439,7 +535,7 @@ ${o?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${o.invalid?"bg-ye
439
535
  <div>
440
536
  <label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
441
537
  <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">
442
- ${xe.map(([g,d])=>html`<option value="${g}" ${e===g?"selected":""}>${d}</option>`)}
538
+ ${Be.map(([g,l])=>html`<option value="${g}" ${e===g?"selected":""}>${l}</option>`)}
443
539
  </select>
444
540
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
445
541
  </div>
@@ -459,12 +555,17 @@ ${o?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${o.invalid?"bg-ye
459
555
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Per-chat or across all sessions.</p>
460
556
  </div>
461
557
  </div>
558
+ <div>
559
+ <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="${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
+ <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
+ </div>
462
563
  <fieldset class="border-0 p-0 m-0">
463
564
  <legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">UI theme</legend>
464
565
  <div class="flex gap-2">
465
- ${r.map(([g,d,l])=>html`<label class="flex-1 cursor-pointer">
566
+ ${r.map(([g,l,d])=>html`<label class="flex-1 cursor-pointer">
466
567
  <input type="radio" name="ui_theme" value="${g}" ${t.settings.ui_theme===g?"checked":""} class="sr-only peer" />
467
- <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">${l}</span>${d}</span>
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">${d}</span>${l}</span>
468
569
  </label>`)}
469
570
  </div>
470
571
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">System follows your OS preference.</p>
@@ -522,7 +623,8 @@ ${o?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${o.invalid?"bg-ye
522
623
  <div x-show="nbMode === 'preview'" id="notebook-preview-settings" class="markdown-body min-h-24"></div>
523
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">${t.notebookContent}</pre>
524
625
  </div>
525
- </div>`,i=html`<script src="/static/vendor/marked.min.js"></script>
626
+ </div>`,n=html`<script src="/static/vendor/marked.min.js"></script>
627
+ <script src="/static/vendor/purify.min.js"></script>
526
628
  <script>
527
629
  function updateLabel(inputId, labelId, submitId) {
528
630
  var input = document.getElementById(inputId);
@@ -530,6 +632,6 @@ function updateLabel(inputId, labelId, submitId) {
530
632
  document.getElementById(submitId).disabled = !input.files.length;
531
633
  }
532
634
  marked.setOptions({ breaks: true, gfm: true });
533
- document.getElementById('notebook-preview-settings').innerHTML = marked.parse(${raw(C(t.notebookContent))});
534
- </script>`;return S({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:s,scripts:i})}var M=25;function B(t,e="/lessons"){return t?.startsWith("/")&&!t.startsWith("//")?t:e}function y(t,e,r=""){let o=t[e];return typeof o=="string"?o:r}var A=dirname(fileURLToPath(import.meta.url)),ce=[join(A,"../assets/static"),join(A,"../../assets/static")].find(t=>existsSync(t))??join(A,"../assets/static"),ve={".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"};function we(){try{return m(t=>F(t).ui_theme)}catch{return "system"}}function $e(){let t$1=new Hono;return t$1.get("/static/*",e=>{let r=decodeURIComponent(e.req.path.slice(8)),o=join(ce,r);if(!o.startsWith(ce))return e.notFound();try{let s=readFileSync(o);return new Response(new Uint8Array(s),{headers:{"content-type":ve[extname(o)]??"application/octet-stream"}})}catch{return e.notFound()}}),t$1.get("/",e=>{let{profile:r,stats:o,rateLimit:s,settings:i}=m(l=>({profile:Q(l),stats:T(l),rateLimit:P(l),settings:F(l)})),g=new Map;for(let l of r.groups)for(let m of l.topics)g.set(m,l.name);let d={};for(let l of r.groups)d[l.name]=[];for(let l of r.knowledge){let m=g.get(l.topic)??"Other",b=d[m]??[];b.push(l),d[m]=b;}return e.html(ie({categorised:d,allGroups:r.groups.map(l=>l.name),stats:o,rateLimit:s,maxPerDay:i.max_per_day,uiTheme:i.ui_theme}))}),t$1.post("/knowledge",async e=>{let r=await e.req.parseBody(),o=y(r,"topic").trim();if(o){let s=Number.parseInt(y(r,"confidence","5"),10)||5,i=y(r,"group").trim();m(g=>{z(g,o,s),i&&i!=="Other"&&D(g,o,i);});}return e.redirect("/",303)}),t$1.post("/knowledge/:topic/delete",e=>(m(r=>A$1(r,e.req.param("topic"))),e.redirect("/",303))),t$1.post("/knowledge/:topic/group",async e=>{let r=y(await e.req.parseBody(),"group").trim(),o=e.req.param("topic");return m(s=>{r&&r!=="Other"?D(s,o,r):E(s,o);}),e.redirect("/",303)}),t$1.post("/knowledge/:topic",async e=>{let r=Number.parseInt(y(await e.req.parseBody(),"delta","0"),10)||0;return m(o=>R(o,e.req.param("topic"),r)),e.redirect("/",303)}),t$1.post("/groups",async e=>{let r=y(await e.req.parseBody(),"group_name").trim();return r&&r!=="Other"&&m(o=>B$1(o,r)),e.redirect("/",303)}),t$1.post("/groups/:group_name/delete",e=>(m(r=>C$1(r,e.req.param("group_name"))),e.redirect("/",303))),t$1.get("/lessons/export",()=>{let e=m(r=>t(r));return new Response(JSON.stringify(e,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),t$1.post("/lessons/import",async e=>{let r=(await e.req.parseBody()).file,o;try{o=JSON.parse(r instanceof File?await r.text():String(r));}catch{return e.redirect("/settings?imported=0&skipped=0&invalid=1",303)}if(!Array.isArray(o))return e.redirect("/settings?imported=0&skipped=0&invalid=1",303);let s=m(i=>u(i,o));return e.redirect(`/settings?imported=${s.inserted}&skipped=${s.duplicated}&invalid=${s.invalid}`,303)}),t$1.get("/lessons",e=>{let r=e.req.query(),o$1=new Set(["junior","mid","senior"]),s=r.level&&o$1.has(r.level)?r.level:"",i=r.date_from||"",g=r.date_to||"",d=r.starred==="1",l=r.sort||"timestamp",m$1=r.order==="asc"?"asc":"desc",b=Math.max(1,Number.parseInt(r.page??"1",10)||1),v$1=r.period||"all",$={period:i||g?null:v$1!=="all"?v$1:null,category:r.category||null,level:s||null,project:r.project||null,repository:r.repository||null,branch:r.branch||null,commit:r.commit||null,starred:d?true:null,search:r.search||null,feedback:r.feedback||null,date_from:i||null,date_to:g||null},u=m(c=>{let f=o(c,$),w=Math.max(1,Math.ceil(f/M));return b=Math.min(b,w),{total:f,totalPages:w,lessons:p(c,{...$,sort:l,order:m$1,page:b,per_page:M}),allCategories:x$1(c),allProjects:v(c,"project"),allRepositories:v(c,"repository"),allBranches:v(c,"branch"),allCommits:v(c,"commit_hash"),theme:F(c).ui_theme}}),n={period:v$1,category:r.category||"",level:s,project:r.project||"",repository:r.repository||"",branch:r.branch||"",commit:r.commit||"",starred:d,search:r.search||"",feedback:r.feedback||"",date_from:i,date_to:g,sort:l,order:m$1};return e.html(de({lessons:u.lessons,allCategories:u.allCategories,allProjects:u.allProjects,allRepositories:u.allRepositories,allBranches:u.allBranches,allCommits:u.allCommits,s:n,page:b,perPage:M,total:u.total,totalPages:u.totalPages,uiTheme:u.theme}))}),t$1.post("/lessons/:lesson_id/star",async e=>{let r$1=await e.req.parseBody();return m(o=>r(o,e.req.param("lesson_id"),r$1.starred==="1")),e.redirect(B(y(r$1,"next")||void 0),303)}),t$1.post("/lessons/:lesson_id/feedback",async e=>{let r=await e.req.parseBody(),o=y(r,"feedback"),s=o===""||o==="clear"?null:o;return m(i=>S$1(i,e.req.param("lesson_id"),s)),e.redirect(B(y(r,"next")||void 0),303)}),t$1.get("/lessons/:lesson_id",e=>{let r=m(o=>w(o,e.req.param("lesson_id")));return r?e.html(le({lesson:r,uiTheme:we()})):e.html("<h1>Lesson not found</h1>",404)}),t$1.get("/settings/export",()=>{let e=m(r=>N(r));return new Response(new Uint8Array(e),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),t$1.post("/settings/import",async e=>{let r=(await e.req.parseBody()).file;if(!(r instanceof File))return e.redirect("/settings?imported=0&skipped=0&invalid=1",303);let o=new Uint8Array(await r.arrayBuffer()),s=m(i=>O(i,o));return e.redirect(`/settings?imported=${s.lessons}&skipped=${s.skipped}&invalid=${s.invalid}&groups=${s.groups}&notebook=${s.learning_state}`,303)}),t$1.get("/settings/notebook/download",()=>{let e=existsSync(j)?readFileSync(j,"utf8"):"";return new Response(e,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),t$1.post("/settings/notebook",async e=>{let r=await e.req.parseBody();return mkdirSync(dirname(j),{recursive:true}),writeFileSync(j,y(r,"content"),"utf8"),e.redirect(B(y(r,"next")||void 0,"/settings?notebook_saved=1"),303)}),t$1.post("/settings",async e=>{let r=await e.req.parseBody(),o=Number.parseInt(y(r,"max_per_day","2"),10)||2,s=Number.parseInt(y(r,"min_gap_minutes","240"),10),i=y(r,"ui_theme","system");["system","dark","light"].includes(i)||(i="system");let g=Math.max(0,Number.parseInt(y(r,"nudge_every","10"),10)||0),d=y(r,"nudge_scope","session");return d!=="session"&&d!=="global"&&(d="session"),m(l=>{G(l,"max_per_day",String(o)),G(l,"min_gap_minutes",String(Number.isNaN(s)?240:s)),G(l,"ui_theme",i),G(l,"nudge_every",String(g)),G(l,"nudge_scope",d);}),e.redirect("/settings",303)}),t$1.get("/settings",e=>{let r=m(g=>F(g)),o=existsSync(j)?readFileSync(j,"utf8"):"",s=e.req.query(),i=s.imported!==void 0?{imported:Number(s.imported??0),skipped:Number(s.skipped??0),invalid:Number(s.invalid??0),groups:Number(s.groups??0),notebook:Number(s.notebook??0)}:null;return e.html(ge({settings:r,notebookContent:o,notebookPath:j,uiTheme:r.ui_theme,flash:i}))}),t$1}function Me(t){let e=$e();serve({fetch:e.fetch,port:t,hostname:"127.0.0.1"},r=>{console.log(`devcoach UI running at http://localhost:${r.port}`);});}
535
- export{$e as createApp,Me as startUi};
635
+ document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(w(t.notebookContent))}));
636
+ </script>`;return j({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:s,scripts:n})}var A=25;function D(t,e="/lessons"){return t?.startsWith("/")&&!t.startsWith("//")?t:e}function p(t,e,r=""){let a=t[e];return typeof a=="string"?a:r}var O=dirname(fileURLToPath(import.meta.url)),Te=[join(O,"../assets/static"),join(O,"../../assets/static")].find(t=>existsSync(t))??join(O,"../assets/static"),qe={".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"},Fe="https://ultimaphoenix.github.io",je={"access-control-allow-origin":Fe,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function Pe(t){let e=t.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var H;function Ge(){return H===void 0&&(H=a()),H}function Le(t,e,r){let a=T({explicit:r.name??null,setting:e,gitUserName:Ge()}),s=r.includeContext??false,n=S(t,{includeContext:s,sharedBy:a}),g=U(n);return {payload:n,state:{id:t.id,open:r.open??false,name:a??"",includeContext:s,text:W(n,g),link:X(n,g)}}}function ze(){try{return o(t=>G(t).ui_theme)}catch{return "system"}}function Ke(){let t$1=new Hono;return t$1.get("/static/*",e=>{let r=decodeURIComponent(e.req.path.slice(8)),a=join(Te,r);if(!a.startsWith(Te))return e.notFound();try{let s=readFileSync(a);return new Response(new Uint8Array(s),{headers:{"content-type":qe[extname(a)]??"application/octet-stream"}})}catch{return e.notFound()}}),t$1.get("/ping",e=>e.json({ok:true,version:Q},200,je)),t$1.options("/ping",e=>e.body(null,204,je)),t$1.get("/",e=>{let{profile:r,stats:a,rateLimit:s,settings:n}=o(d=>({profile:$(d),stats:ca(d),rateLimit:_(d),settings:G(d)})),g=new Map;for(let d of r.groups)for(let c of d.topics)g.set(c,d.name);let l={};for(let d of r.groups)l[d.name]=[];for(let d of r.knowledge){let c=g.get(d.topic)??"Other",x=l[c]??[];x.push(d),l[c]=x;}return e.html($e({categorised:l,allGroups:r.groups.map(d=>d.name),stats:a,rateLimit:s,maxPerDay:n.max_per_day,uiTheme:n.ui_theme}))}),t$1.post("/knowledge",async e=>{let r=await e.req.parseBody(),a=p(r,"topic").trim();if(a){let s=Number.parseInt(p(r,"confidence","5"),10)||5,n=p(r,"group").trim();o(g=>{A$1(g,a,s),n&&n!=="Other"&&E(g,a,n);});}return e.redirect("/",303)}),t$1.post("/knowledge/:topic/delete",e=>(o(r=>B$1(r,e.req.param("topic"))),e.redirect("/",303))),t$1.post("/knowledge/:topic/group",async e=>{let r=p(await e.req.parseBody(),"group").trim(),a=e.req.param("topic");return o(s=>{r&&r!=="Other"?E(s,a,r):F(s,a);}),e.redirect("/",303)}),t$1.post("/knowledge/:topic",async e=>{let r=Number.parseInt(p(await e.req.parseBody(),"delta","0"),10)||0;return o(a=>aa(a,e.req.param("topic"),r)),e.redirect("/",303)}),t$1.post("/groups",async e=>{let r=p(await e.req.parseBody(),"group_name").trim();return r&&r!=="Other"&&o(a=>C(a,r)),e.redirect("/",303)}),t$1.post("/groups/:group_name/delete",e=>(o(r=>D$1(r,e.req.param("group_name"))),e.redirect("/",303))),t$1.get("/lessons/export",()=>{let e=o(r=>v(r));return new Response(JSON.stringify(e,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),t$1.post("/lessons/import",async e=>{let r=await e.req.parseBody(),a=p(r,"from")==="lessons",s=d=>e.redirect(a?`/lessons?import=1&error=${d}`:"/settings?imported=0&skipped=0&invalid=1",303);if(Pe(e))return s("cross");let n=r.file,l=(n instanceof File&&n.size>0?await n.text():null)??p(r,"text").trim();try{c(l)&&!l.includes("#devcoach:lesson:")&&(l=await d(l));let d$1=o(c=>da(c,l));return d$1.kind==="lessons"||!d$1.lesson?e.redirect(`/settings?imported=${d$1.inserted}&skipped=${d$1.duplicated}&invalid=${d$1.invalid}`,303):e.redirect(`/lessons/${encodeURIComponent(d$1.lesson.id)}?imported=${d$1.inserted?"1":"dup"}`,303)}catch(d){if(d instanceof R)return s("invalid");throw d}}),t$1.get("/lessons/import",e=>{let r=(e.req.query("code")??"").trim(),a=null,s=null;if(r)try{a=V(r);}catch(n){s=n instanceof R?n.message:"That code could not be read.";}return e.html(Ce({code:r,preview:a,error:s,uiTheme:ze()}))}),t$1.get("/lessons",e=>{let r$1=e.req.query(),a=new Set(["junior","mid","senior"]),s=r$1.level&&a.has(r$1.level)?r$1.level:"",n=r$1.date_from||"",g=r$1.date_to||"",l=r$1.starred==="1",d=r$1.sort||"timestamp",c=r$1.order==="asc"?"asc":"desc",x=Math.max(1,Number.parseInt(r$1.page??"1",10)||1),$=r$1.period||"all",I={period:n||g?null:$!=="all"?$:null,category:r$1.category||null,level:s||null,project:r$1.project||null,repository:r$1.repository||null,branch:r$1.branch||null,commit:r$1.commit||null,starred:l?true:null,search:r$1.search||null,feedback:r$1.feedback||null,date_from:n||null,date_to:g||null},h=o(b=>{let _=q(b,I),S=Math.max(1,Math.ceil(_/A));return x=Math.min(x,S),{total:_,totalPages:S,lessons:r(b,{...I,sort:d,order:c,page:x,per_page:A}),allCategories:y$1(b),allProjects:w$1(b,"project"),allRepositories:w$1(b,"repository"),allBranches:w$1(b,"branch"),allCommits:w$1(b,"commit_hash"),theme:G(b).ui_theme}}),i={period:$,category:r$1.category||"",level:s,project:r$1.project||"",repository:r$1.repository||"",branch:r$1.branch||"",commit:r$1.commit||"",starred:l,search:r$1.search||"",feedback:r$1.feedback||"",date_from:n,date_to:g,sort:d,order:c},u=r$1.error==="invalid"||r$1.error==="cross"?r$1.error:null;return e.html(_e({importOpen:r$1.import==="1",importError:u,lessons:h.lessons,allCategories:h.allCategories,allProjects:h.allProjects,allRepositories:h.allRepositories,allBranches:h.allBranches,allCommits:h.allCommits,s:i,page:x,perPage:A,total:h.total,totalPages:h.totalPages,uiTheme:h.theme}))}),t$1.post("/lessons/:lesson_id/star",async e=>{let r=await e.req.parseBody();return o(a=>t(a,e.req.param("lesson_id"),r.starred==="1")),e.redirect(D(p(r,"next")||void 0),303)}),t$1.post("/lessons/:lesson_id/feedback",async e=>{let r=await e.req.parseBody(),a=p(r,"feedback"),s=a===""||a==="clear"?null:a;return o(n=>ba(n,e.req.param("lesson_id"),s)),e.redirect(D(p(r,"next")||void 0),303)}),t$1.on(["GET","POST"],"/lessons/:lesson_id/share",async e=>{if(Pe(e))return e.text("Forbidden",403);let r,a,s=false;if(e.req.method==="POST"){let c=await e.req.parseBody();r=p(c,"name"),a=p(c,"include_context")==="1",s=p(c,"persist")==="1";}else r=e.req.query("name"),a=e.req.query("include_context")==="1";let n=o(c=>{let x$1=x(c,e.req.param("lesson_id")??"");return x$1?(s&&r!==void 0&&H$1(c,"share_name",m(r)),{lesson:x$1,settingName:G(c).share_name}):null});if(!n)return e.text("Lesson not found",404);let{state:g,payload:l}=Le(n.lesson,n.settingName,{name:r,includeContext:a}),d=e.req.query("format");return d==="text"?e.text(g.text):d==="link"?e.text(g.link):d==="md"?new Response(Z(l),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${Y(l)}"`}}):e.html(N(g))}),t$1.get("/lessons/:lesson_id",e=>{let r=o(n=>{let g=x(n,e.req.param("lesson_id"));if(!g)return null;let l=G(n);return {lesson:g,settingName:l.share_name,uiTheme:l.ui_theme}});if(!r)return e.html("<h1>Lesson not found</h1>",404);let a=e.req.query(),s=a.imported==="1"?"new":a.imported==="dup"?"dup":null;return e.html(Se({lesson:r.lesson,uiTheme:r.uiTheme,share:Le(r.lesson,r.settingName,{open:a.share==="1"}).state,imported:s}))}),t$1.get("/settings/export",()=>{let e=o(r=>O$1(r));return new Response(new Uint8Array(e),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),t$1.post("/settings/import",async e=>{let r=(await e.req.parseBody()).file;if(!(r instanceof File))return e.redirect("/settings?imported=0&skipped=0&invalid=1",303);let a=new Uint8Array(await r.arrayBuffer()),s=o(n=>P(n,a));return e.redirect(`/settings?imported=${s.lessons}&skipped=${s.skipped}&invalid=${s.invalid}&groups=${s.groups}&notebook=${s.learning_state}`,303)}),t$1.get("/settings/notebook/download",()=>{let e=existsSync(j$1)?readFileSync(j$1,"utf8"):"";return new Response(e,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),t$1.post("/settings/notebook",async e=>{let r=await e.req.parseBody();return mkdirSync(dirname(j$1),{recursive:true}),writeFileSync(j$1,p(r,"content"),"utf8"),e.redirect(D(p(r,"next")||void 0,"/settings?notebook_saved=1"),303)}),t$1.post("/settings",async e=>{let r=await e.req.parseBody(),a=Number.parseInt(p(r,"max_per_day","2"),10)||2,s=Number.parseInt(p(r,"min_gap_minutes","240"),10),n=p(r,"ui_theme","system");["system","dark","light"].includes(n)||(n="system");let g=Math.max(0,Number.parseInt(p(r,"nudge_every","10"),10)||0),l=p(r,"nudge_scope","session");return l!=="session"&&l!=="global"&&(l="session"),o(d=>{H$1(d,"max_per_day",String(a)),H$1(d,"min_gap_minutes",String(Number.isNaN(s)?240:s)),H$1(d,"ui_theme",n),H$1(d,"nudge_every",String(g)),H$1(d,"nudge_scope",l),H$1(d,"share_name",m(p(r,"share_name")));}),e.redirect("/settings",303)}),t$1.get("/settings",e=>{let r=o(g=>G(g)),a=existsSync(j$1)?readFileSync(j$1,"utf8"):"",s=e.req.query(),n=s.imported!==void 0?{imported:Number(s.imported??0),skipped:Number(s.skipped??0),invalid:Number(s.invalid??0),groups:Number(s.groups??0),notebook:Number(s.notebook??0)}:null;return e.html(Ie({settings:r,notebookContent:a,notebookPath:j$1,uiTheme:r.ui_theme,flash:n}))}),t$1}function it(t){let e=Ke();serve({fetch:e.fetch,port:t,hostname:"127.0.0.1"},r=>{console.log(`devcoach UI running at http://localhost:${r.port}`);});}
637
+ export{Ke as createApp,it as startUi};