diff --git a/admin.js b/admin.js index 150c3fe61..cd64a5c0e 100644 --- a/admin.js +++ b/admin.js @@ -1,24 +1,24 @@ -var L=Object.defineProperty;var r=(t,e)=>L(t,"name",{value:e,configurable:!0});var p={owner:"johanbrook",repo:"johanbrook.com",notesDir:"src/notes"},b=r(()=>location.hostname=="localhost","isLocal");var f=r(t=>t.kind=="err","isErr");var y="jb_tok",U="https://api.github.com",v=r(({url:t})=>{let e=r(()=>{location.href=t},"doAuth"),n=r(async(o,{method:c="GET",query:a,body:u}={})=>{let d=E();if(!d)return e(),Promise.resolve({});let h=a?"?"+new URLSearchParams(a).toString():"",g=await fetch(U+o+h,{method:c,headers:{accept:"application/vnd.github.v3+json",authorization:`token ${d}`},body:u?JSON.stringify(u):void 0}),S=await g.json();return g.ok?S:g.status==401?(e(),Promise.resolve({})):{kind:"err",msg:"Failed to request GitHub REST data",cause:new Error(`${c} ${g.status} ${o}: ${S.message||g.statusText}`)}},"request"),s=r(o=>!!o.error,"isAuthError");return{maybeLogin:r(()=>{E()||(location.href=t)},"maybeLogin"),fetchToken:r(async o=>{let c=location.pathname+location.search.replace(/\bcode=\w+/,"").replace(/\?$/,"");history.pushState({},"",c);let a=await fetch(t,{method:"POST",mode:"cors",headers:{"content-type":"application/json"},body:JSON.stringify({code:o})});if(!a.ok)return{kind:"err",msg:a.statusText};let u=await a.json();if(s(u))return{kind:"err",msg:u.error};try{localStorage.setItem(y,u.token)}catch{}return{kind:"token",tok:u.token}},"fetchToken"),createNote:r(async(o,c)=>{let a=new Date,{repo:u,owner:d,notesDir:h}=p,g=b()?"dev":"main",S=w(a),A=w(a,!0),k=`--- -date: ${S} -location: The web +var L=Object.defineProperty;var r=(t,e)=>L(t,"name",{value:e,configurable:!0});var p={owner:"johanbrook",repo:"johanbrook.com",notesDir:"src/notes"},b=r(()=>location.hostname=="localhost","isLocal");var g=r(t=>t.kind=="err","isErr");var y="jb_tok",U="https://api.github.com",v=r(({url:t})=>{let e=r(()=>{location.href=t},"doAuth"),o=r(async(n,{method:c="GET",query:a,body:u}={})=>{let d=E();if(!d)return e(),Promise.resolve({});let h=a?"?"+new URLSearchParams(a).toString():"",f=await fetch(U+n+h,{method:c,headers:{accept:"application/vnd.github.v3+json",authorization:`token ${d}`},body:u?JSON.stringify(u):void 0}),T=await f.json();return f.ok?T:f.status==401?(e(),Promise.resolve({})):{kind:"err",msg:"Failed to request GitHub REST data",cause:new Error(`${c} ${f.status} ${n}: ${T.message||f.statusText}`)}},"request"),s=r(n=>!!n.error,"isAuthError");return{maybeLogin:r(()=>{E()||(location.href=t)},"maybeLogin"),fetchToken:r(async n=>{let c=location.pathname+location.search.replace(/\bcode=\w+/,"").replace(/\?$/,"");history.pushState({},"",c);let a=await fetch(t,{method:"POST",mode:"cors",headers:{"content-type":"application/json"},body:JSON.stringify({code:n})});if(!a.ok)return{kind:"err",msg:a.statusText};let u=await a.json();if(s(u))return{kind:"err",msg:u.error};try{localStorage.setItem(y,u.token)}catch{}return{kind:"token",tok:u.token}},"fetchToken"),createNote:r(async(n,c)=>{let a=new Date,{repo:u,owner:d,notesDir:h}=p,f=b()?"dev":"main",T=w(a),A=w(a,!0),k=`--- +date: ${T} +location: My web interface `;c&&(k+="draft: true"),k+=` --- -${o} -`;let P=`${A}.md`,_=h+"/"+P,l=await n(`/repos/${d}/${u}/contents/${_}`,{method:"PUT",body:{message:"Add note from web app",content:R(k),branch:g}});return f(l)?l:!l.content?.name||!l.content?.html_url||!l.commit.html_url?{kind:"err",msg:"Unexpected response when creating a note"}:{commitUrl:l.commit.html_url,file:l.content.name,fileUrl:l.content.html_url}},"createNote")}},"mkGitHub"),E=r(()=>{try{return localStorage.getItem(y)}catch{return null}},"getStoredToken"),R=r(t=>btoa(encodeURIComponent(t).replace(/%([0-9A-F]{2})/g,(e,n)=>String.fromCharCode(parseInt(n,16)))),"base64"),w=r((t,e=!1)=>{let n=[t.getUTCFullYear(),t.getUTCMonth()+1,t.getUTCDate()].map(i=>String(i).padStart(2,"0")).join("-"),s=[t.getUTCHours(),t.getUTCMinutes(),e?null:t.getUTCSeconds()].filter(Boolean).map(i=>String(i).padStart(2,"0")).join(e?"-":":");return e?n+"-"+s:n+" "+s},"formatDate");var H=b()?"http://localhost:8788/github-oauth":"https://brookie.pages.dev/github-oauth",I=r((t,e)=>v(e),"mkService"),N=r(async()=>{let t=I("github",{url:H}),e=document.getElementById("root"),n=O(),s=D(n),i=C(t),m=r(async o=>{e.innerHTML=await i(o)},"html"),T=r(async o=>{console.log("update",o);let c=s(o);console.log("state",c),await m(c)},"tick");window.handleEvt=async(o,c)=>{switch(console.log("event",o,c),c.kind){case"note_input":{let a=o.target;window.submitNote.disabled=a.value.trim().length==0,a.parentElement.dataset.replicatedValue=a.value;break}case"submit_note":{o.preventDefault();let a=o.target,u=a.querySelector("textarea").value.trim(),d=a.querySelector("#draft-check").checked;if(!u)return;let h=await t.createNote(u,d);f(h)?await T({err:h}):(M(window.submitNote,"\u2728 posted! \u2728"),await T({createNote:h}));break}}},await T(n)},"runApp"),C=r(t=>async e=>{let n=new URL(location.href).searchParams.get("code");if(n){let s=await t.fetchToken(n);if(f(s))return $(s)}return t.maybeLogin(),e.err?$(e.err):` +${n} +`;let P=`${A}.md`,_=h+"/"+P,l=await o(`/repos/${d}/${u}/contents/${_}`,{method:"PUT",body:{message:"Add note from web app",content:R(k),branch:f}});return g(l)?l:!l.content?.name||!l.content?.html_url||!l.commit.html_url?{kind:"err",msg:"Unexpected response when creating a note"}:{commitUrl:l.commit.html_url,file:l.content.name,fileUrl:l.content.html_url}},"createNote")}},"mkGitHub"),E=r(()=>{try{return localStorage.getItem(y)}catch{return null}},"getStoredToken"),R=r(t=>btoa(encodeURIComponent(t).replace(/%([0-9A-F]{2})/g,(e,o)=>String.fromCharCode(parseInt(o,16)))),"base64"),w=r((t,e=!1)=>{let o=[t.getUTCFullYear(),t.getUTCMonth()+1,t.getUTCDate()].map(i=>String(i).padStart(2,"0")).join("-"),s=[t.getUTCHours(),t.getUTCMinutes(),e?null:t.getUTCSeconds()].filter(Boolean).map(i=>String(i).padStart(2,"0")).join(e?"-":":");return e?o+"-"+s:o+" "+s},"formatDate");var H=b()?"http://localhost:8788/github-oauth":"https://brookie.pages.dev/github-oauth",I=r((t,e)=>v(e),"mkService"),N=r(async()=>{let t=I("github",{url:H}),e=document.getElementById("root"),o=O(),s=D(o),i=C(t),m=r(async n=>{e.innerHTML=await i(n)},"html"),S=r(async n=>{console.log("update",n);let c=s(n);console.log("state",c),await m(c)},"tick");window.handleEvt=async(n,c)=>{switch(console.log("event",n,c),c.kind){case"note_input":{let a=n.target;window.submitNote.disabled=a.value.trim().length==0,a.parentElement.dataset.replicatedValue=a.value;break}case"submit_note":{n.preventDefault();let a=n.target,u=a.querySelector("textarea").value.trim(),d=a.querySelector("#draft-check").checked;if(!u)return;let h=await t.createNote(u,d);g(h)?await S({err:h}):(j(window.submitNote,"\u2728 posted! \u2728"),await S({createNote:h}));break}}},await S(o)},"runApp"),C=r(t=>async e=>{let o=new URL(location.href).searchParams.get("code");if(o){let s=await t.fetchToken(o);if(g(s))return $(s)}return t.maybeLogin(),e.err?$(e.err):`

${p.repo}

${p.owner}/${p.repo}

- ${j()} + ${M()} ${e.createNote?`

Note created in repo: ${e.createNote.file}

`:""}

View all notes

-
`},"App"),D=r(t=>{function*e(){let s={},i=t;for(;;){let m={...i,...s};i=m,s=yield m}}r(e,"gen");let n=e();return n.next(t),s=>n.next(s).value},"reducer"),x=r((t,e)=>`on${t}='handleEvt(event${e?", "+JSON.stringify(e):""})'`,"ev"),j=r(()=>` + `},"App"),D=r(t=>{function*e(){let s={},i=t;for(;;){let m={...i,...s};i=m,s=yield m}}r(e,"gen");let o=e();return o.next(t),s=>o.next(s).value},"reducer"),x=r((t,e)=>`on${t}='handleEvt(event${e?", "+JSON.stringify(e):""})'`,"ev"),M=r(()=>`
${t.msg}

${t.cause?`
${t.cause.message}
`:""} -`,"Error"),M=r((t,e)=>{let n=t instanceof HTMLInputElement?t.value:t.innerText,s=r((i,m)=>{t instanceof HTMLInputElement?(t.value=i,t.disabled=m):t.innerText=i},"set");s(e,!0),setTimeout(()=>{s(n,!1)},3e3)},"flash");var O=r(()=>{let t=(()=>{try{let e=localStorage.getItem("jb_state")??"{}";return JSON.parse(e)}catch{return{}}})();return{}},"initialState");N().catch(console.error); +`,"Error"),j=r((t,e)=>{let o=t instanceof HTMLInputElement?t.value:t.innerText,s=r((i,m)=>{t instanceof HTMLInputElement?(t.value=i,t.disabled=m):t.innerText=i},"set");s(e,!0),setTimeout(()=>{s(o,!1)},3e3)},"flash");var O=r(()=>{let t=(()=>{try{let e=localStorage.getItem("jb_state")??"{}";return JSON.parse(e)}catch{return{}}})();return{}},"initialState");N().catch(console.error); /*# sourceMappingURL=./admin.js.map */ \ No newline at end of file diff --git a/admin.js.map b/admin.js.map index 42067b54b..b9669bb71 100644 --- a/admin.js.map +++ b/admin.js.map @@ -1 +1 @@ -{"version":3,"sources":["/src/js/config.ts","/src/js/util.ts","/src/js/github.ts","/src/js/app.ts","/src/admin.ts"],"sourcesContent":["export interface Config {\n\towner: string;\n\trepo: string;\n\tnotesDir: string;\n}\n\nexport const config: Config = {\n\towner: 'johanbrook',\n\trepo: 'johanbrook.com',\n\tnotesDir: 'src/notes',\n};\n\nexport const isLocal = () => location.hostname == 'localhost';\n","export interface Err {\n\tkind: 'err';\n\tmsg: string;\n\tcause?: Error;\n}\n\nexport type Result = T | Err;\n\nexport const promiseResult = (p: Promise): Promise =>\n\tnew Promise((rs, rj) => p.then((v) => (isErr(v) ? rj(new Error(v.msg)) : rs(v))).catch(rj));\n\nexport const isErr = (t: unknown): t is Err => (t as Err).kind == 'err';\n","import { Endpoints } from '../../deps.ts';\nimport { config, isLocal } from './config.ts';\nimport type { Service } from './service.ts';\nimport { type Err, isErr } from './util.ts';\n\nconst STORAGE_KEY = 'jb_tok';\n\nconst API_ROOT = 'https://api.github.com';\n\nexport interface Args {\n\turl: string;\n}\n\nexport const mkGitHub = ({ url }: Args): Service => {\n\tconst doAuth = () => {\n\t\tlocation.href = url;\n\t};\n\n\tconst request = async >(\n\t\tresource: `/${string}`,\n\t\t{\n\t\t\tmethod = 'GET',\n\t\t\tquery,\n\t\t\tbody,\n\t\t}: {\n\t\t\tmethod?: 'GET' | 'POST' | 'PUT';\n\t\t\tquery?: Record;\n\t\t\tbody?: B;\n\t\t} = {},\n\t): Promise => {\n\t\tconst storedTok = getStoredToken();\n\n\t\tif (!storedTok) {\n\t\t\tdoAuth();\n\t\t\treturn Promise.resolve({} as T);\n\t\t}\n\n\t\tconst qs: string = query ? '?' + new URLSearchParams(query).toString() : '';\n\n\t\tconst res = await fetch(API_ROOT + resource + qs, {\n\t\t\tmethod,\n\t\t\theaders: {\n\t\t\t\taccept: 'application/vnd.github.v3+json',\n\t\t\t\tauthorization: `token ${storedTok}`,\n\t\t\t},\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tconst json = await res.json();\n\n\t\tif (!res.ok) {\n\t\t\tif (res.status == 401) {\n\t\t\t\tdoAuth();\n\t\t\t\treturn Promise.resolve({} as T);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tkind: 'err',\n\t\t\t\tmsg: 'Failed to request GitHub REST data',\n\t\t\t\tcause: new Error(\n\t\t\t\t\t`${method} ${res.status} ${resource}: ${json.message || res.statusText}`,\n\t\t\t\t),\n\t\t\t};\n\t\t}\n\n\t\treturn json;\n\t};\n\n\ttype AuthError = { error: string };\n\ttype AuthSuccess = { token: string };\n\n\tconst isAuthError = (err: Record): err is AuthError =>\n\t\t!!(err as AuthError).error;\n\n\tconst fetchToken: Service['fetchToken'] = async (code) => {\n\t\t// remove ?code=... from URL\n\t\tconst path = location.pathname +\n\t\t\tlocation.search.replace(/\\bcode=\\w+/, '').replace(/\\?$/, '');\n\t\thistory.pushState({}, '', path);\n\n\t\tconst res = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\tmode: 'cors',\n\t\t\theaders: {\n\t\t\t\t'content-type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({ code }),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\treturn {\n\t\t\t\tkind: 'err',\n\t\t\t\tmsg: res.statusText,\n\t\t\t};\n\t\t}\n\n\t\tconst result: AuthSuccess | AuthError = await res.json();\n\n\t\tif (isAuthError(result)) {\n\t\t\treturn {\n\t\t\t\tkind: 'err',\n\t\t\t\tmsg: result.error,\n\t\t\t};\n\t\t}\n\n\t\ttry {\n\t\t\tlocalStorage.setItem(STORAGE_KEY, result.token);\n\t\t} catch (_ex) {\n\t\t\t//\n\t\t}\n\n\t\treturn {\n\t\t\tkind: 'token',\n\t\t\ttok: result.token,\n\t\t};\n\t};\n\n\tconst createNote: Service['createNote'] = async (text, draft) => {\n\t\ttype Endpoint = Endpoints['PUT /repos/{owner}/{repo}/contents/{path}'];\n\n\t\tconst d = new Date();\n\t\tconst { repo, owner, notesDir } = config;\n\t\tconst branch = isLocal() ? 'dev' : 'main';\n\t\tconst date = formatDate(d);\n\t\tconst fileDate = formatDate(d, true);\n\n\t\tlet content = `---\ndate: ${date}\nlocation: The web\n`;\n\n\t\tif (draft) {\n\t\t\tcontent += 'draft: true';\n\t\t}\n\n\t\tcontent += `\\n---\\n${text}\\n`;\n\n\t\tconst fileName = `${fileDate}.md`;\n\t\tconst path = notesDir + '/' + fileName;\n\n\t\tconst res = await request<\n\t\t\tEndpoint['response']['data'],\n\t\t\tOmit\n\t\t>(`/repos/${owner}/${repo}/contents/${path}`, {\n\t\t\tmethod: 'PUT',\n\t\t\tbody: {\n\t\t\t\tmessage: 'Add note from web app',\n\t\t\t\tcontent: base64(content),\n\t\t\t\tbranch,\n\t\t\t},\n\t\t});\n\n\t\tif (isErr(res)) return res;\n\n\t\tif (\n\t\t\t!res.content?.name ||\n\t\t\t!res.content?.html_url ||\n\t\t\t!res.commit.html_url\n\t\t) {\n\t\t\treturn {\n\t\t\t\tkind: 'err',\n\t\t\t\tmsg: 'Unexpected response when creating a note',\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tcommitUrl: res.commit.html_url,\n\t\t\tfile: res.content.name,\n\t\t\tfileUrl: res.content.html_url,\n\t\t};\n\t};\n\n\tconst maybeLogin: Service['maybeLogin'] = () => {\n\t\tconst storedTok = getStoredToken();\n\n\t\tif (!storedTok) {\n\t\t\tlocation.href = url;\n\t\t}\n\t};\n\n\treturn {\n\t\tmaybeLogin,\n\t\tfetchToken,\n\t\tcreateNote,\n\t};\n};\n\nconst getStoredToken = (): string | null => {\n\ttry {\n\t\treturn localStorage.getItem(STORAGE_KEY);\n\t} catch (_ex) {\n\t\treturn null;\n\t}\n};\n\n// From https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings\n// To not mess up utf-8 chars in the string.\nconst base64 = (str: string) =>\n\tbtoa(\n\t\tencodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_match, p1) =>\n\t\t\tString.fromCharCode(parseInt(p1, 16))),\n\t);\n\n// => 'yyyy-MM-dd HH:mm:ss'\n// fileName: true => 'yyyy-MM-dd-HH-mm'\nconst formatDate = (date: Date, fileName = false): string => {\n\tconst datePart = [\n\t\tdate.getUTCFullYear(),\n\t\tdate.getUTCMonth() + 1,\n\t\tdate.getUTCDate(),\n\t]\n\t\t.map((n) => String(n).padStart(2, '0'))\n\t\t.join('-');\n\n\tconst timePart = [\n\t\tdate.getUTCHours(),\n\t\tdate.getUTCMinutes(),\n\t\tfileName ? null : date.getUTCSeconds(),\n\t]\n\t\t.filter(Boolean)\n\t\t.map((n) => String(n).padStart(2, '0'))\n\t\t.join(fileName ? '-' : ':');\n\n\tif (fileName) {\n\t\treturn datePart + '-' + timePart;\n\t}\n\n\treturn datePart + ' ' + timePart;\n};\n","import { config, isLocal } from './config.ts';\nimport { Args as GitHubArgs, mkGitHub } from './github.ts';\nimport type { CreateNoteResult, Service } from './service.ts';\nimport { Err, isErr } from './util.ts';\n\nconst WORKER_URL = isLocal()\n\t? 'http://localhost:8788/github-oauth'\n\t: 'https://brookie.pages.dev/github-oauth';\n\ninterface Services {\n\tgithub: GitHubArgs;\n}\n\nconst mkService = (\n\t_service: T,\n\targs: Services[T],\n): Service => {\n\treturn mkGitHub(args);\n};\n\ninterface State {\n\terr?: Err;\n\tcreateNote?: CreateNoteResult;\n}\n\nexport const runApp = async () => {\n\tconst svc = mkService('github', { url: WORKER_URL });\n\tconst root = document.getElementById('root')!;\n\n\tconst initial = initialState();\n\tconst setState = reducer(initial);\n\n\tconst renderApp = App(svc);\n\n\tconst html = async (s: State) => {\n\t\troot.innerHTML = await renderApp(s);\n\t};\n\n\tconst tick = async (update: Partial) => {\n\t\tconsole.log('update', update);\n\t\tconst state = setState(update);\n\t\tconsole.log('state', state);\n\n\t\t// Side effects\n\t\tawait html(state);\n\t};\n\n\t(window as any).handleEvt = async (evt: Event, action: Action) => {\n\t\tconsole.log('event', evt, action);\n\n\t\tswitch (action.kind) {\n\t\t\tcase 'note_input': {\n\t\t\t\tconst textarea = evt.target as HTMLTextAreaElement;\n\n\t\t\t\t((window as any).submitNote as HTMLInputElement).disabled =\n\t\t\t\t\ttextarea.value.trim().length == 0;\n\n\t\t\t\t// Autogrow\n\t\t\t\ttextarea.parentElement!.dataset.replicatedValue = textarea.value;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase 'submit_note': {\n\t\t\t\tevt.preventDefault();\n\t\t\t\tconst form = evt.target as HTMLFormElement;\n\n\t\t\t\tconst text = form\n\t\t\t\t\t.querySelector('textarea')!\n\t\t\t\t\t.value.trim();\n\n\t\t\t\tconst draft = form\n\t\t\t\t\t.querySelector('#draft-check')\n\t\t\t\t\t.checked;\n\n\t\t\t\tif (!text) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst res = await svc.createNote(text, draft);\n\n\t\t\t\tif (isErr(res)) {\n\t\t\t\t\tawait tick({ err: res });\n\t\t\t\t} else {\n\t\t\t\t\tflash(\n\t\t\t\t\t\t(window as any).submitNote as HTMLInputElement,\n\t\t\t\t\t\t'✨ posted! ✨',\n\t\t\t\t\t);\n\n\t\t\t\t\tawait tick({ createNote: res });\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t};\n\n\t// Initial render\n\tawait tick(initial);\n};\n\nconst App = (svc: Service) => {\n\treturn async (state: State) => {\n\t\tconst code = new URL(location.href).searchParams.get('code');\n\n\t\tif (code) {\n\t\t\tconst res = await svc.fetchToken(code);\n\n\t\t\tif (isErr(res)) {\n\t\t\t\treturn Error(res);\n\t\t\t}\n\t\t}\n\n\t\tsvc.maybeLogin();\n\n\t\tif (state.err) {\n\t\t\treturn Error(state.err);\n\t\t}\n\n\t\treturn /* html */ `\n
\n

${config.repo}

\n

\n ${config.owner}/${config.repo}\n

\n\n ${NewNote()}\n\n ${\n\t\t\tstate.createNote\n\t\t\t\t? `

Note created in repo: ${state.createNote.file}

`\n\t\t\t\t: ''\n\t\t}\n\n

\n View all notes\n

\n
`;\n\t};\n};\n\nconst reducer = (state: S) => {\n\tfunction* gen() {\n\t\tlet update: Partial = {};\n\t\tlet prev: S = state;\n\n\t\twhile (true) {\n\t\t\tconst newState = { ...prev, ...update };\n\t\t\tprev = newState;\n\t\t\tupdate = yield newState as S;\n\t\t}\n\t}\n\n\tconst r = gen();\n\tr.next(state); // Initial\n\n\treturn (update: Partial) => r.next(update).value as S;\n};\n\ninterface SubmitNote {\n\tkind: 'submit_note';\n}\n\ninterface NoteInput {\n\tkind: 'note_input';\n}\n\ninterface ToggleDraft {\n\tkind: 'toggle_draft';\n}\n\ntype Action = SubmitNote | NoteInput | ToggleDraft;\n\nconst ev = (e: T, action: Action) =>\n\t`on${e}='handleEvt(event${action ? ', ' + JSON.stringify(action) : ''})'`;\n\nconst NewNote = () => /* HTML */ `\n \n

New note

\n\n
\n \n
\n\n

\n \n

\n\n

\n \n

\n \n`;\n\nconst Error = (err: Err) => /* html */ `\n
\n

Error

\n

${err.msg}

\n ${err.cause ? `
${err.cause.message}
` : ''}\n
\n`;\n\nconst flash = (el: HTMLElement, msg: string) => {\n\tconst org = el instanceof HTMLInputElement ? el.value : el.innerText;\n\n\tconst set = (str: string, disable: boolean) => {\n\t\tif (el instanceof HTMLInputElement) {\n\t\t\tel.value = str;\n\t\t\tel.disabled = disable;\n\t\t} else {\n\t\t\tel.innerText = str;\n\t\t}\n\t};\n\n\tset(msg, true);\n\n\tsetTimeout(() => {\n\t\tset(org, false);\n\t}, 3000);\n};\n\ninterface Persisted {}\n\nconst persist = (p: Persisted) => {\n\ttry {\n\t\tlocalStorage.setItem('jb_state', JSON.stringify(p));\n\t} catch (_ex) {\n\t\t//\n\t}\n};\n\nconst initialState = (): State => {\n\tconst persisted = ((): Persisted => {\n\t\ttry {\n\t\t\tconst json = localStorage.getItem('jb_state') ?? '{}';\n\t\t\treturn JSON.parse(json);\n\t\t} catch (_ex) {\n\t\t\t//\n\t\t\treturn {};\n\t\t}\n\t})();\n\n\treturn {};\n};\n","import { runApp } from './js/app.ts';\n\nrunApp().catch(console.error);\n"],"mappings":"+EAMO,IAAMA,EAAiB,CAC7B,MAAO,aACP,KAAM,iBACN,SAAU,WACX,EAEaC,EAAUC,EAAA,IAAM,SAAS,UAAY,YAA3B,WCDhB,IAAMC,EAAQC,EAAC,GAA0B,EAAU,MAAQ,MAA7C,SCNrB,IAAMC,EAAc,SAEdC,EAAW,yBAMJC,EAAWC,EAAA,CAAC,CAAE,IAAAC,CAAI,IAAqB,CACnD,IAAMC,EAASF,EAAA,IAAM,CACpB,SAAS,KAAOC,CACjB,EAFe,UAITE,EAAUH,EAAA,MACfI,EACA,CACC,OAAAC,EAAS,MACT,MAAAC,EACA,KAAAC,CACD,EAII,CAAC,IACiB,CACtB,IAAMC,EAAYC,EAAe,EAEjC,GAAI,CAACD,EACJ,OAAAN,EAAO,EACA,QAAQ,QAAQ,CAAC,CAAM,EAG/B,IAAMQ,EAAaJ,EAAQ,IAAM,IAAI,gBAAgBA,CAAK,EAAE,SAAS,EAAI,GAEnEK,EAAM,MAAM,MAAMb,EAAWM,EAAWM,EAAI,CACjD,OAAAL,EACA,QAAS,CACR,OAAQ,iCACR,cAAe,SAASG,GACzB,EACA,KAAMD,EAAO,KAAK,UAAUA,CAAI,EAAI,MACrC,CAAC,EAEKK,EAAO,MAAMD,EAAI,KAAK,EAE5B,OAAKA,EAAI,GAeFC,EAdFD,EAAI,QAAU,KACjBT,EAAO,EACA,QAAQ,QAAQ,CAAC,CAAM,GAGxB,CACN,KAAM,MACN,IAAK,qCACL,MAAO,IAAI,MACV,GAAGG,KAAUM,EAAI,UAAUP,MAAaQ,EAAK,SAAWD,EAAI,YAC7D,CACD,CAIF,EAhDgB,WAqDVE,EAAcb,EAACc,GACpB,CAAC,CAAEA,EAAkB,MADF,eA6GpB,MAAO,CACN,WATyCd,EAAA,IAAM,CAC7BS,EAAe,IAGhC,SAAS,KAAOR,EAElB,EAN0C,cAUzC,WA5GyCD,EAAA,MAAOe,GAAS,CAEzD,IAAMC,EAAO,SAAS,SACrB,SAAS,OAAO,QAAQ,aAAc,EAAE,EAAE,QAAQ,MAAO,EAAE,EAC5D,QAAQ,UAAU,CAAC,EAAG,GAAIA,CAAI,EAE9B,IAAML,EAAM,MAAM,MAAMV,EAAK,CAC5B,OAAQ,OACR,KAAM,OACN,QAAS,CACR,eAAgB,kBACjB,EACA,KAAM,KAAK,UAAU,CAAE,KAAAc,CAAK,CAAC,CAC9B,CAAC,EAED,GAAI,CAACJ,EAAI,GACR,MAAO,CACN,KAAM,MACN,IAAKA,EAAI,UACV,EAGD,IAAMM,EAAkC,MAAMN,EAAI,KAAK,EAEvD,GAAIE,EAAYI,CAAM,EACrB,MAAO,CACN,KAAM,MACN,IAAKA,EAAO,KACb,EAGD,GAAI,CACH,aAAa,QAAQpB,EAAaoB,EAAO,KAAK,CAC/C,MAAE,CAEF,CAEA,MAAO,CACN,KAAM,QACN,IAAKA,EAAO,KACb,CACD,EAzC0C,cA6GzC,WAlEyCjB,EAAA,MAAOkB,EAAMC,IAAU,CAGhE,IAAMC,EAAI,IAAI,KACR,CAAE,KAAAC,EAAM,MAAAC,EAAO,SAAAC,CAAS,EAAIC,EAC5BC,EAASC,EAAQ,EAAI,MAAQ,OAC7BC,EAAOC,EAAWR,CAAC,EACnBS,EAAWD,EAAWR,EAAG,EAAI,EAE/BU,EAAU;AAAA,QACRH;AAAA;AAAA,EAIFR,IACHW,GAAW,eAGZA,GAAW;AAAA;AAAA,EAAUZ;AAAA,EAErB,IAAMa,EAAW,GAAGF,OACdb,EAAOO,EAAW,IAAMQ,EAExBpB,EAAM,MAAMR,EAGhB,UAAUmB,KAASD,cAAiBL,IAAQ,CAC7C,OAAQ,MACR,KAAM,CACL,QAAS,wBACT,QAASgB,EAAOF,CAAO,EACvB,OAAAL,CACD,CACD,CAAC,EAED,OAAIQ,EAAMtB,CAAG,EAAUA,EAGtB,CAACA,EAAI,SAAS,MACd,CAACA,EAAI,SAAS,UACd,CAACA,EAAI,OAAO,SAEL,CACN,KAAM,MACN,IAAK,0CACN,EAGM,CACN,UAAWA,EAAI,OAAO,SACtB,KAAMA,EAAI,QAAQ,KAClB,QAASA,EAAI,QAAQ,QACtB,CACD,EArD0C,aAmE1C,CACD,EA5KwB,YA8KlBF,EAAiBT,EAAA,IAAqB,CAC3C,GAAI,CACH,OAAO,aAAa,QAAQH,CAAW,CACxC,MAAE,CACD,OAAO,IACR,CACD,EANuB,kBAUjBmC,EAAShC,EAACkC,GACf,KACC,mBAAmBA,CAAG,EAAE,QAAQ,kBAAmB,CAACC,EAAQC,IAC3D,OAAO,aAAa,SAASA,EAAI,EAAE,CAAC,CAAC,CACvC,EAJc,UAQTR,EAAa5B,EAAA,CAAC2B,EAAYI,EAAW,KAAkB,CAC5D,IAAMM,EAAW,CAChBV,EAAK,eAAe,EACpBA,EAAK,YAAY,EAAI,EACrBA,EAAK,WAAW,CACjB,EACE,IAAKW,GAAM,OAAOA,CAAC,EAAE,SAAS,EAAG,GAAG,CAAC,EACrC,KAAK,GAAG,EAEJC,EAAW,CAChBZ,EAAK,YAAY,EACjBA,EAAK,cAAc,EACnBI,EAAW,KAAOJ,EAAK,cAAc,CACtC,EACE,OAAO,OAAO,EACd,IAAKW,GAAM,OAAOA,CAAC,EAAE,SAAS,EAAG,GAAG,CAAC,EACrC,KAAKP,EAAW,IAAM,GAAG,EAE3B,OAAIA,EACIM,EAAW,IAAME,EAGlBF,EAAW,IAAME,CACzB,EAvBmB,cCxMnB,IAAMC,EAAaC,EAAQ,EACxB,qCACA,yCAMGC,EAAYC,EAAA,CACjBC,EACAC,IAEOC,EAASD,CAAI,EAJH,aAYLE,EAASJ,EAAA,SAAY,CACjC,IAAMK,EAAMN,EAAU,SAAU,CAAE,IAAKF,CAAW,CAAC,EAC7CS,EAAO,SAAS,eAAe,MAAM,EAErCC,EAAUC,EAAa,EACvBC,EAAWC,EAAQH,CAAO,EAE1BI,EAAYC,EAAIP,CAAG,EAEnBQ,EAAOb,EAAA,MAAOc,GAAa,CAChCR,EAAK,UAAY,MAAMK,EAAUG,CAAC,CACnC,EAFa,QAIPC,EAAOf,EAAA,MAAOgB,GAA2B,CAC9C,QAAQ,IAAI,SAAUA,CAAM,EAC5B,IAAMC,EAAQR,EAASO,CAAM,EAC7B,QAAQ,IAAI,QAASC,CAAK,EAG1B,MAAMJ,EAAKI,CAAK,CACjB,EAPa,QASZ,OAAe,UAAY,MAAOC,EAAYC,IAAmB,CAGjE,OAFA,QAAQ,IAAI,QAASD,EAAKC,CAAM,EAExBA,EAAO,KAAM,CACpB,IAAK,aAAc,CAClB,IAAMC,EAAWF,EAAI,OAEnB,OAAe,WAAgC,SAChDE,EAAS,MAAM,KAAK,EAAE,QAAU,EAGjCA,EAAS,cAAe,QAAQ,gBAAkBA,EAAS,MAC3D,KACD,CAEA,IAAK,cAAe,CACnBF,EAAI,eAAe,EACnB,IAAMG,EAAOH,EAAI,OAEXI,EAAOD,EACX,cAAc,UAAU,EACxB,MAAM,KAAK,EAEPE,EAAQF,EACZ,cAAgC,cAAc,EAC9C,QAEF,GAAI,CAACC,EACJ,OAGD,IAAME,EAAM,MAAMnB,EAAI,WAAWiB,EAAMC,CAAK,EAExCE,EAAMD,CAAG,EACZ,MAAMT,EAAK,CAAE,IAAKS,CAAI,CAAC,GAEvBE,EACE,OAAe,WAChB,uBACD,EAEA,MAAMX,EAAK,CAAE,WAAYS,CAAI,CAAC,GAG/B,KACD,CACD,CACD,EAGA,MAAMT,EAAKR,CAAO,CACnB,EAzEsB,UA2EhBK,EAAMZ,EAACK,GACL,MAAOY,GAAiB,CAC9B,IAAMU,EAAO,IAAI,IAAI,SAAS,IAAI,EAAE,aAAa,IAAI,MAAM,EAE3D,GAAIA,EAAM,CACT,IAAMH,EAAM,MAAMnB,EAAI,WAAWsB,CAAI,EAErC,GAAIF,EAAMD,CAAG,EACZ,OAAOI,EAAMJ,CAAG,CAElB,CAIA,OAFAnB,EAAI,WAAW,EAEXY,EAAM,IACFW,EAAMX,EAAM,GAAG,EAGL;AAAA;AAAA,4CAEwBY,EAAO;AAAA;AAAA,kDAEDA,EAAO,SAASA,EAAO,SAASA,EAAO,SAASA,EAAO;AAAA;AAAA;AAAA,kBAGvFC,EAAQ;AAAA;AAAA,kBAGvBb,EAAM,WACH,qCAAqCA,EAAM,WAAW,YAAYA,EAAM,WAAW,eACnF;AAAA;AAAA;AAAA;AAAA;AAAA,uBAOL,EArCW,OAwCNP,EAAUV,EAAIiB,GAAa,CAChC,SAAUc,GAAM,CACf,IAAIf,EAAqB,CAAC,EACtBgB,EAAUf,EAEd,OAAa,CACZ,IAAMgB,EAAW,CAAE,GAAGD,EAAM,GAAGhB,CAAO,EACtCgB,EAAOC,EACPjB,EAAS,MAAMiB,CAChB,CACD,CATUjC,EAAA+B,EAAA,OAWV,IAAMG,EAAIH,EAAI,EACd,OAAAG,EAAE,KAAKjB,CAAK,EAEJD,GAAuBkB,EAAE,KAAKlB,CAAM,EAAE,KAC/C,EAhBgB,WAgCVmB,EAAKnC,EAAA,CAAmCoC,EAAMjB,IACnD,KAAKiB,qBAAqBjB,EAAS,KAAO,KAAK,UAAUA,CAAM,EAAI,OADzD,MAGLW,EAAU9B,EAAA,IAAiB;AAAA;AAAA,UAGhCmC,EAAG,SAAU,CACZ,KAAM,aACP,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAQgBA,EAAG,QAAS,CAAE,KAAM,YAAa,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAbpC,WAsCVP,EAAQ5B,EAACqC,GAAwB;AAAA;AAAA;AAAA,aAG1BA,EAAI;AAAA,UACPA,EAAI,MAAQ,QAAQA,EAAI,MAAM,gBAAkB;AAAA;AAAA,EAJ5C,SAQRX,EAAQ1B,EAAA,CAACsC,EAAiBC,IAAgB,CAC/C,IAAMC,EAAMF,aAAc,iBAAmBA,EAAG,MAAQA,EAAG,UAErDG,EAAMzC,EAAA,CAAC0C,EAAaC,IAAqB,CAC1CL,aAAc,kBACjBA,EAAG,MAAQI,EACXJ,EAAG,SAAWK,GAEdL,EAAG,UAAYI,CAEjB,EAPY,OASZD,EAAIF,EAAK,EAAI,EAEb,WAAW,IAAM,CAChBE,EAAID,EAAK,EAAK,CACf,EAAG,GAAI,CACR,EAjBc,SA6Bd,IAAMI,EAAeC,EAAA,IAAa,CACjC,IAAMC,GAAa,IAAiB,CACnC,GAAI,CACH,IAAMC,EAAO,aAAa,QAAQ,UAAU,GAAK,KACjD,OAAO,KAAK,MAAMA,CAAI,CACvB,MAAE,CAED,MAAO,CAAC,CACT,CACD,GAAG,EAEH,MAAO,CAAC,CACT,EAZqB,gBCxPrBC,EAAO,EAAE,MAAM,QAAQ,KAAK","names":["config","isLocal","__name","isErr","__name","STORAGE_KEY","API_ROOT","mkGitHub","__name","url","doAuth","request","resource","method","query","body","storedTok","getStoredToken","qs","res","json","isAuthError","err","code","path","result","text","draft","d","repo","owner","notesDir","config","branch","isLocal","date","formatDate","fileDate","content","fileName","base64","isErr","str","_match","p1","datePart","n","timePart","WORKER_URL","isLocal","mkService","__name","_service","args","mkGitHub","runApp","svc","root","initial","initialState","setState","reducer","renderApp","App","html","s","tick","update","state","evt","action","textarea","form","text","draft","res","isErr","flash","code","Error","config","NewNote","gen","prev","newState","r","ev","e","err","el","msg","org","set","str","disable","initialState","__name","persisted","json","runApp"],"sourceRoot":"file:///home/runner/work/johanbrook.com/johanbrook.com","file":"/admin.js.map"} \ No newline at end of file +{"version":3,"sources":["/src/js/config.ts","/src/js/util.ts","/src/js/github.ts","/src/js/app.ts","/src/admin.ts"],"sourcesContent":["export interface Config {\n\towner: string;\n\trepo: string;\n\tnotesDir: string;\n}\n\nexport const config: Config = {\n\towner: 'johanbrook',\n\trepo: 'johanbrook.com',\n\tnotesDir: 'src/notes',\n};\n\nexport const isLocal = () => location.hostname == 'localhost';\n","export interface Err {\n\tkind: 'err';\n\tmsg: string;\n\tcause?: Error;\n}\n\nexport type Result = T | Err;\n\nexport const promiseResult = (p: Promise): Promise =>\n\tnew Promise((rs, rj) => p.then((v) => (isErr(v) ? rj(new Error(v.msg)) : rs(v))).catch(rj));\n\nexport const isErr = (t: unknown): t is Err => (t as Err).kind == 'err';\n","import { Endpoints } from '../../deps.ts';\nimport { config, isLocal } from './config.ts';\nimport type { Service } from './service.ts';\nimport { type Err, isErr } from './util.ts';\n\nconst STORAGE_KEY = 'jb_tok';\n\nconst API_ROOT = 'https://api.github.com';\n\nexport interface Args {\n\turl: string;\n}\n\nexport const mkGitHub = ({ url }: Args): Service => {\n\tconst doAuth = () => {\n\t\tlocation.href = url;\n\t};\n\n\tconst request = async >(\n\t\tresource: `/${string}`,\n\t\t{\n\t\t\tmethod = 'GET',\n\t\t\tquery,\n\t\t\tbody,\n\t\t}: {\n\t\t\tmethod?: 'GET' | 'POST' | 'PUT';\n\t\t\tquery?: Record;\n\t\t\tbody?: B;\n\t\t} = {},\n\t): Promise => {\n\t\tconst storedTok = getStoredToken();\n\n\t\tif (!storedTok) {\n\t\t\tdoAuth();\n\t\t\treturn Promise.resolve({} as T);\n\t\t}\n\n\t\tconst qs: string = query ? '?' + new URLSearchParams(query).toString() : '';\n\n\t\tconst res = await fetch(API_ROOT + resource + qs, {\n\t\t\tmethod,\n\t\t\theaders: {\n\t\t\t\taccept: 'application/vnd.github.v3+json',\n\t\t\t\tauthorization: `token ${storedTok}`,\n\t\t\t},\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tconst json = await res.json();\n\n\t\tif (!res.ok) {\n\t\t\tif (res.status == 401) {\n\t\t\t\tdoAuth();\n\t\t\t\treturn Promise.resolve({} as T);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tkind: 'err',\n\t\t\t\tmsg: 'Failed to request GitHub REST data',\n\t\t\t\tcause: new Error(\n\t\t\t\t\t`${method} ${res.status} ${resource}: ${json.message || res.statusText}`,\n\t\t\t\t),\n\t\t\t};\n\t\t}\n\n\t\treturn json;\n\t};\n\n\ttype AuthError = { error: string };\n\ttype AuthSuccess = { token: string };\n\n\tconst isAuthError = (err: Record): err is AuthError =>\n\t\t!!(err as AuthError).error;\n\n\tconst fetchToken: Service['fetchToken'] = async (code) => {\n\t\t// remove ?code=... from URL\n\t\tconst path = location.pathname +\n\t\t\tlocation.search.replace(/\\bcode=\\w+/, '').replace(/\\?$/, '');\n\t\thistory.pushState({}, '', path);\n\n\t\tconst res = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\tmode: 'cors',\n\t\t\theaders: {\n\t\t\t\t'content-type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({ code }),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\treturn {\n\t\t\t\tkind: 'err',\n\t\t\t\tmsg: res.statusText,\n\t\t\t};\n\t\t}\n\n\t\tconst result: AuthSuccess | AuthError = await res.json();\n\n\t\tif (isAuthError(result)) {\n\t\t\treturn {\n\t\t\t\tkind: 'err',\n\t\t\t\tmsg: result.error,\n\t\t\t};\n\t\t}\n\n\t\ttry {\n\t\t\tlocalStorage.setItem(STORAGE_KEY, result.token);\n\t\t} catch (_ex) {\n\t\t\t//\n\t\t}\n\n\t\treturn {\n\t\t\tkind: 'token',\n\t\t\ttok: result.token,\n\t\t};\n\t};\n\n\tconst createNote: Service['createNote'] = async (text, draft) => {\n\t\ttype Endpoint = Endpoints['PUT /repos/{owner}/{repo}/contents/{path}'];\n\n\t\tconst d = new Date();\n\t\tconst { repo, owner, notesDir } = config;\n\t\tconst branch = isLocal() ? 'dev' : 'main';\n\t\tconst date = formatDate(d);\n\t\tconst fileDate = formatDate(d, true);\n\n\t\tlet content = `---\ndate: ${date}\nlocation: My web interface\n`;\n\n\t\tif (draft) {\n\t\t\tcontent += 'draft: true';\n\t\t}\n\n\t\tcontent += `\\n---\\n${text}\\n`;\n\n\t\tconst fileName = `${fileDate}.md`;\n\t\tconst path = notesDir + '/' + fileName;\n\n\t\tconst res = await request<\n\t\t\tEndpoint['response']['data'],\n\t\t\tOmit\n\t\t>(`/repos/${owner}/${repo}/contents/${path}`, {\n\t\t\tmethod: 'PUT',\n\t\t\tbody: {\n\t\t\t\tmessage: 'Add note from web app',\n\t\t\t\tcontent: base64(content),\n\t\t\t\tbranch,\n\t\t\t},\n\t\t});\n\n\t\tif (isErr(res)) return res;\n\n\t\tif (\n\t\t\t!res.content?.name ||\n\t\t\t!res.content?.html_url ||\n\t\t\t!res.commit.html_url\n\t\t) {\n\t\t\treturn {\n\t\t\t\tkind: 'err',\n\t\t\t\tmsg: 'Unexpected response when creating a note',\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tcommitUrl: res.commit.html_url,\n\t\t\tfile: res.content.name,\n\t\t\tfileUrl: res.content.html_url,\n\t\t};\n\t};\n\n\tconst maybeLogin: Service['maybeLogin'] = () => {\n\t\tconst storedTok = getStoredToken();\n\n\t\tif (!storedTok) {\n\t\t\tlocation.href = url;\n\t\t}\n\t};\n\n\treturn {\n\t\tmaybeLogin,\n\t\tfetchToken,\n\t\tcreateNote,\n\t};\n};\n\nconst getStoredToken = (): string | null => {\n\ttry {\n\t\treturn localStorage.getItem(STORAGE_KEY);\n\t} catch (_ex) {\n\t\treturn null;\n\t}\n};\n\n// From https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings\n// To not mess up utf-8 chars in the string.\nconst base64 = (str: string) =>\n\tbtoa(\n\t\tencodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_match, p1) =>\n\t\t\tString.fromCharCode(parseInt(p1, 16))),\n\t);\n\n// => 'yyyy-MM-dd HH:mm:ss'\n// fileName: true => 'yyyy-MM-dd-HH-mm'\nconst formatDate = (date: Date, fileName = false): string => {\n\tconst datePart = [\n\t\tdate.getUTCFullYear(),\n\t\tdate.getUTCMonth() + 1,\n\t\tdate.getUTCDate(),\n\t]\n\t\t.map((n) => String(n).padStart(2, '0'))\n\t\t.join('-');\n\n\tconst timePart = [\n\t\tdate.getUTCHours(),\n\t\tdate.getUTCMinutes(),\n\t\tfileName ? null : date.getUTCSeconds(),\n\t]\n\t\t.filter(Boolean)\n\t\t.map((n) => String(n).padStart(2, '0'))\n\t\t.join(fileName ? '-' : ':');\n\n\tif (fileName) {\n\t\treturn datePart + '-' + timePart;\n\t}\n\n\treturn datePart + ' ' + timePart;\n};\n","import { config, isLocal } from './config.ts';\nimport { Args as GitHubArgs, mkGitHub } from './github.ts';\nimport type { CreateNoteResult, Service } from './service.ts';\nimport { Err, isErr } from './util.ts';\n\nconst WORKER_URL = isLocal()\n\t? 'http://localhost:8788/github-oauth'\n\t: 'https://brookie.pages.dev/github-oauth';\n\ninterface Services {\n\tgithub: GitHubArgs;\n}\n\nconst mkService = (\n\t_service: T,\n\targs: Services[T],\n): Service => {\n\treturn mkGitHub(args);\n};\n\ninterface State {\n\terr?: Err;\n\tcreateNote?: CreateNoteResult;\n}\n\nexport const runApp = async () => {\n\tconst svc = mkService('github', { url: WORKER_URL });\n\tconst root = document.getElementById('root')!;\n\n\tconst initial = initialState();\n\tconst setState = reducer(initial);\n\n\tconst renderApp = App(svc);\n\n\tconst html = async (s: State) => {\n\t\troot.innerHTML = await renderApp(s);\n\t};\n\n\tconst tick = async (update: Partial) => {\n\t\tconsole.log('update', update);\n\t\tconst state = setState(update);\n\t\tconsole.log('state', state);\n\n\t\t// Side effects\n\t\tawait html(state);\n\t};\n\n\t(window as any).handleEvt = async (evt: Event, action: Action) => {\n\t\tconsole.log('event', evt, action);\n\n\t\tswitch (action.kind) {\n\t\t\tcase 'note_input': {\n\t\t\t\tconst textarea = evt.target as HTMLTextAreaElement;\n\n\t\t\t\t((window as any).submitNote as HTMLInputElement).disabled =\n\t\t\t\t\ttextarea.value.trim().length == 0;\n\n\t\t\t\t// Autogrow\n\t\t\t\ttextarea.parentElement!.dataset.replicatedValue = textarea.value;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase 'submit_note': {\n\t\t\t\tevt.preventDefault();\n\t\t\t\tconst form = evt.target as HTMLFormElement;\n\n\t\t\t\tconst text = form\n\t\t\t\t\t.querySelector('textarea')!\n\t\t\t\t\t.value.trim();\n\n\t\t\t\tconst draft = form\n\t\t\t\t\t.querySelector('#draft-check')\n\t\t\t\t\t.checked;\n\n\t\t\t\tif (!text) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst res = await svc.createNote(text, draft);\n\n\t\t\t\tif (isErr(res)) {\n\t\t\t\t\tawait tick({ err: res });\n\t\t\t\t} else {\n\t\t\t\t\tflash(\n\t\t\t\t\t\t(window as any).submitNote as HTMLInputElement,\n\t\t\t\t\t\t'✨ posted! ✨',\n\t\t\t\t\t);\n\n\t\t\t\t\tawait tick({ createNote: res });\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t};\n\n\t// Initial render\n\tawait tick(initial);\n};\n\nconst App = (svc: Service) => {\n\treturn async (state: State) => {\n\t\tconst code = new URL(location.href).searchParams.get('code');\n\n\t\tif (code) {\n\t\t\tconst res = await svc.fetchToken(code);\n\n\t\t\tif (isErr(res)) {\n\t\t\t\treturn Error(res);\n\t\t\t}\n\t\t}\n\n\t\tsvc.maybeLogin();\n\n\t\tif (state.err) {\n\t\t\treturn Error(state.err);\n\t\t}\n\n\t\treturn /* html */ `\n
\n

${config.repo}

\n

\n ${config.owner}/${config.repo}\n

\n\n ${NewNote()}\n\n ${\n\t\t\tstate.createNote\n\t\t\t\t? `

Note created in repo: ${state.createNote.file}

`\n\t\t\t\t: ''\n\t\t}\n\n

\n View all notes\n

\n
`;\n\t};\n};\n\nconst reducer = (state: S) => {\n\tfunction* gen() {\n\t\tlet update: Partial = {};\n\t\tlet prev: S = state;\n\n\t\twhile (true) {\n\t\t\tconst newState = { ...prev, ...update };\n\t\t\tprev = newState;\n\t\t\tupdate = yield newState as S;\n\t\t}\n\t}\n\n\tconst r = gen();\n\tr.next(state); // Initial\n\n\treturn (update: Partial) => r.next(update).value as S;\n};\n\ninterface SubmitNote {\n\tkind: 'submit_note';\n}\n\ninterface NoteInput {\n\tkind: 'note_input';\n}\n\ninterface ToggleDraft {\n\tkind: 'toggle_draft';\n}\n\ntype Action = SubmitNote | NoteInput | ToggleDraft;\n\nconst ev = (e: T, action: Action) =>\n\t`on${e}='handleEvt(event${action ? ', ' + JSON.stringify(action) : ''})'`;\n\nconst NewNote = () => /* HTML */ `\n \n

New note

\n\n
\n \n
\n\n

\n \n

\n\n

\n \n

\n \n`;\n\nconst Error = (err: Err) => /* html */ `\n
\n

Error

\n

${err.msg}

\n ${err.cause ? `
${err.cause.message}
` : ''}\n
\n`;\n\nconst flash = (el: HTMLElement, msg: string) => {\n\tconst org = el instanceof HTMLInputElement ? el.value : el.innerText;\n\n\tconst set = (str: string, disable: boolean) => {\n\t\tif (el instanceof HTMLInputElement) {\n\t\t\tel.value = str;\n\t\t\tel.disabled = disable;\n\t\t} else {\n\t\t\tel.innerText = str;\n\t\t}\n\t};\n\n\tset(msg, true);\n\n\tsetTimeout(() => {\n\t\tset(org, false);\n\t}, 3000);\n};\n\ninterface Persisted {}\n\nconst persist = (p: Persisted) => {\n\ttry {\n\t\tlocalStorage.setItem('jb_state', JSON.stringify(p));\n\t} catch (_ex) {\n\t\t//\n\t}\n};\n\nconst initialState = (): State => {\n\tconst persisted = ((): Persisted => {\n\t\ttry {\n\t\t\tconst json = localStorage.getItem('jb_state') ?? '{}';\n\t\t\treturn JSON.parse(json);\n\t\t} catch (_ex) {\n\t\t\t//\n\t\t\treturn {};\n\t\t}\n\t})();\n\n\treturn {};\n};\n","import { runApp } from './js/app.ts';\n\nrunApp().catch(console.error);\n"],"mappings":"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","names":["config","isLocal","__name","isErr","__name","STORAGE_KEY","API_ROOT","mkGitHub","__name","url","doAuth","request","resource","method","query","body","storedTok","getStoredToken","qs","res","json","isAuthError","err","code","path","result","text","draft","d","repo","owner","notesDir","config","branch","isLocal","date","formatDate","fileDate","content","fileName","base64","isErr","str","_match","p1","datePart","n","timePart","WORKER_URL","isLocal","mkService","__name","_service","args","mkGitHub","runApp","svc","root","initial","initialState","setState","reducer","renderApp","App","html","s","tick","update","state","evt","action","textarea","form","text","draft","res","isErr","flash","code","Error","config","NewNote","gen","prev","newState","r","ev","e","err","el","msg","org","set","str","disable","initialState","__name","persisted","json","runApp"],"sourceRoot":"file:///home/runner/work/johanbrook.com/johanbrook.com","file":"/admin.js.map"} \ No newline at end of file diff --git a/api.json b/api.json index 540861f09..93a6beb48 100644 --- a/api.json +++ b/api.json @@ -58,8 +58,8 @@ "cv": "https://johan.im/johanbrook-cv.pdf", "versions": { "johan": "4.0.0", - "build": "0c5b3d997d41a500aa506ba283e617afae4e75f6", - "builtAt": "2023-09-18T21:43:54.376Z" + "build": "3b82ccb88e9aa07de76c65a9d4113bf54da80434", + "builtAt": "2023-09-19T07:28:55.328Z" } } diff --git a/index.html b/index.html index 91527380a..195b3f61c 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ -Johan Brook

Welcome.

My name is Johan. I’m coding & designing, and I like working with product, user experience, and interface design. And other things too.

I'd like to write more (don't we all), and I consume a (un)healthy amount of music.

You can reach me via email or Mastodon.

On my mind

All notesFeed
  1. Latest: September 14th, 2023 — 14:29

    The album “Right Time” (1976) by Mighty Diamonds might be the best reggae album I’ve heard. Even better than the epic “Heart of the Congos” (1977) by The Congos.

Listening

Tracks I've listened to recently.

    Writings

    All writingsFeed
    1. Mastodon

      December 30th, 2022

    2. My favourite software

      February 23rd, 2022

    3. Setting up Sublime Text for Deno

      February 16th, 2022

    \ No newline at end of file +Want to read / Johan Brook

    Want to read.

    Updated on .
    TitleAuthorNotes
    Secret PilgrimJohn le Carré
    Smiley's PeopleJohn le Carré
    GomorraRoberto Saviano

    You can also check out what I've read so far.

    \ No newline at end of file diff --git a/writings/index.html b/writings/index.html index 7636525e5..1c6797286 100644 --- a/writings/index.html +++ b/writings/index.html @@ -1 +1 @@ -Writings / Johan Brook

    Writings.

    181 posts, from to .

    1. Mastodon
    2. My favourite software
    3. Setting up Sublime Text for Deno
    4. A whole new world with Deno
    5. Customising an iOS home screen web app in 2021
    6. An Even Better Spaghetti Bolognese
    7. Recently in books II
    8. Building a small, functional reactive app architecture
    9. Recent improvements in our git discipline
    10. Recently in books
    11. Like Knitting
    12. Euphoria
    13. Using Figma's API to sync colours with your CSS
    14. Using streams in React with Hooks
    15. New watch
    16. Styled components in a virtual DOM
    17. Better Remote Pair Programming
    18. My Spaghetti Bolognese
    19. I am not a creative person
    20. Balancing personal energy and greatness in product design
    21. My Personal Gym FAQ
    22. On interacting and learning from users in support
    23. The White Duck
    24. Conditionally Composable Functions
    25. On Socialising
    26. The Ocean
    27. Intimate remote work
    28. Shipped with fear
    29. What's new in Safari 9
    30. Pain points & Incentives
    31. What's happened so far
    32. Redesigned & Reimagined
    33. Writing contextual CSS
    34. "Master of many trades"
    35. Changing perspectives and doing the crazy thing
    36. A symbol for sex
    37. I don't like counting hours
    38. On Scala
    39. Programming and communication
    40. Tags and class names – on building flexible markup
    41. Object-oriented Programming and Modeling the Real World
    42. How Ryan Singer builds products
    43. On the importance of knowing how to program
    44. Signal vs. Noise
    45. "The Art of Quality"
    46. Spotimood
    47. "Learnable Programming"
    48. Sass 3.2 Placeholders and Object-Oriented CSS
    49. Responsive web design – the infinite grid and water
    50. Linus Torvalds on good programmers
    51. "Twitter is the Benjamin Button of startups"
    52. Good software is like a knot
    53. Jawbone's Jambox
    54. The Hawk catches the Sparrow
    55. Thoughts on freedom, creativity and the internet
    56. "Some things I've learnt about programming"
    57. Valve – the truly flat hierarchy company
    58. The Slow Web Movement
    59. 'Systemet' – A liquor store status web app
    60. On using your time
    61. CSS variables soon to land in WebKit
    62. Perfection doesn't exist
    63. Software is handcrafted
    64. Bdgt for iPhone
    65. A word about testing code
    66. Device independency
    67. I'm apparently a unicorn
    68. Sync is about safety
    69. "Stop solving problems you don't have"
    70. Coffee and free software
    71. Great articles about design, code, and life
    72. Adobe Shadow – Device preview and debugging
    73. Gridset – create advanced grid systems on the web
    74. About guys who fix stuff
    75. On the evolution of languages and frameworks
    76. I can't design in the browser either
    77. How I set up my web development environment on OS X Lion
    78. Java and beginner programming courses at universities
    79. Realigning Johanbrook.com
    80. Songs to code by
    81. Adding custom URL endpoints in Wordpress
    82. "Can you make the logo smaller?"
    83. Hiring developers 2012 style
    84. Advice for university students: "I'm not as smart as I thought I was"
    85. Measuring and sizing UIs, 2011-style
    86. Writing documentation for CSS – Knyle Style Sheets
    87. Timeless fashion
    88. The Anatomy of a Perfect Web Site
    89. PHP needs to die
    90. Subtle updates to Chrome 14's Web Inspector
    91. A Greenhorn's Freelance Advice
    92. Chocolat – The Heir of Textmate?
    93. Add delight to web forms (with code sample)
    94. "Javascript is Dead. Long Live Javascript!"
    95. Native style momentum scrolling to arrive in iOS 5
    96. Staying hungry and evolving with new technologies
    97. Zach Holman on code documentation
    98. Relative positioning and CSS Columns
    99. A fix for antialiasing issues in WebKit browsers
    100. Bringing Order to CSS
    101. Debugging CSS Media Queries
    102. New Safari downloads UI in Lion
    103. Backup everything – my backup setup
    104. Generalist specialists
    105. Redesigned
    106. Spotify revamps the Free and Open options
    107. Digital magazines and HTML
    108. Whiteboard theme for Wordpress
    109. Firefox 4 – a bitter taste
    110. Flow out of beta
    111. Universal wrapping paper
    112. Posters by Avraham Cornfeld
    113. Customizing the Wordpress Admin Bar
    114. Photos of National Geographic
    115. Minimalistic Oscar posters
    116. Take a step back
    117. OS X Lion-Mountain Lion wallpapers
    118. Quick internal linking tag in Wordpress
    119. Get it out there – Matt Mullenweg on shipping software
    120. Dustin Diaz's $script.js
    121. Homemade is best
    122. Streamlines – another kind of Twitter client
    123. Thomas Fuchs' web dev tools
    124. Send URLs from iOS to desktop browser
    125. Visualizing WebKit's hardware acceleration
    126. Maven Pro – a free sans serif font
    127. "The Shape of Design"
    128. Use the current color in CSS with the currentcolor keyword
    129. How 37signals handles customer service
    130. Rasmus Andersson on Kod
    131. Webkit to get CSS variables, mixins, nesting?
    132. Whiteboard accounting, or how to say no to yourself
    133. "Google, H.264 and video on the web"
    134. Google to release a WebM plugin for Safari and IE9
    135. A simpler CSS3 Gradient syntax
    136. After Hours
    137. Dyluni for Wordpress
    138. Eric Meyer: Reset Revisited
    139. RSS is not dying, it's being ignored
    140. Kod app is now open source
    141. The road to the 37signals homepage redesign
    142. "A note to people that hire ... "
    143. Trickle: another kind of Twitter client
    144. Native retweets to Tweetie for Mac with ReTweetie
    145. Yet Another Site Redesign
    146. What Matters
    147. Old hardware still rocks
    148. Make the invisible visible – a clever campaign from Amnesty
    149. Rolling Stones' "Gimme Shelter" deconstructed
    150. iOS Fonts
    151. Andy Clarke's Hardboiled Web Design talk from DIBI
    152. Retro airline bags
    153. Regular people and web standards
    154. A Simple Icon
    155. Why Google can't build Instagram
    156. Five record breaking torrent files
    157. Yet another mobile HTML5 framework ... by 37signals
    158. Minimal Skype 5 message style
    159. Tower – an upcoming Git client for Mac
    160. Jeremy Keith: The Design of HTML5
    161. Paris vs. New York – a graphic comparison
    162. Autocomplete in TextEdit
    163. Formalize CSS – consistent forms
    164. Linked List style posts in Wordpress
    165. Start a Facetime call from URI in browser
    166. A typographic poster
    167. Firesheep – why HTTPS is important
    168. Windows Phone 7 needs activation for Live
    169. Panic talks about the future: Coda 2
    170. The Beatles’ album covers redesigned
    171. Vertical Rhythm Wordpress themes
    172. Apple’s Java in OS X 10.6 is now deprecated
    173. Out with the old
    174. Anatomy of a Rails Rumble project
    175. Who's suing whom?
    176. Comparison between a plumber and a freelancing designer
    177. Bjarne Stroustrup on C++'s 25th anniversary
    178. Old book illustrations
    179. Interview with David Heinemeier Hansson
    180. A Markdown teleprompter
    181. Unofficial version of Notational Velocity adds goodies
    \ No newline at end of file +Writings / Johan Brook

    Writings.

    181 posts, from to .

    1. Mastodon
    2. My favourite software
    3. Setting up Sublime Text for Deno
    4. A whole new world with Deno
    5. Customising an iOS home screen web app in 2021
    6. An Even Better Spaghetti Bolognese
    7. Recently in books II
    8. Building a small, functional reactive app architecture
    9. Recent improvements in our git discipline
    10. Recently in books
    11. Like Knitting
    12. Euphoria
    13. Using Figma's API to sync colours with your CSS
    14. Using streams in React with Hooks
    15. New watch
    16. Styled components in a virtual DOM
    17. Better Remote Pair Programming
    18. My Spaghetti Bolognese
    19. I am not a creative person
    20. Balancing personal energy and greatness in product design
    21. My Personal Gym FAQ
    22. On interacting and learning from users in support
    23. The White Duck
    24. Conditionally Composable Functions
    25. On Socialising
    26. The Ocean
    27. Intimate remote work
    28. Shipped with fear
    29. What's new in Safari 9
    30. Pain points & Incentives
    31. What's happened so far
    32. Redesigned & Reimagined
    33. Writing contextual CSS
    34. "Master of many trades"
    35. Changing perspectives and doing the crazy thing
    36. A symbol for sex
    37. I don't like counting hours
    38. On Scala
    39. Programming and communication
    40. Tags and class names – on building flexible markup
    41. Object-oriented Programming and Modeling the Real World
    42. How Ryan Singer builds products
    43. On the importance of knowing how to program
    44. Signal vs. Noise
    45. "The Art of Quality"
    46. Spotimood
    47. "Learnable Programming"
    48. Sass 3.2 Placeholders and Object-Oriented CSS
    49. Responsive web design – the infinite grid and water
    50. Linus Torvalds on good programmers
    51. "Twitter is the Benjamin Button of startups"
    52. Good software is like a knot
    53. Jawbone's Jambox
    54. The Hawk catches the Sparrow
    55. Thoughts on freedom, creativity and the internet
    56. "Some things I've learnt about programming"
    57. Valve – the truly flat hierarchy company
    58. The Slow Web Movement
    59. 'Systemet' – A liquor store status web app
    60. On using your time
    61. CSS variables soon to land in WebKit
    62. Perfection doesn't exist
    63. Software is handcrafted
    64. Bdgt for iPhone
    65. A word about testing code
    66. Device independency
    67. I'm apparently a unicorn
    68. Sync is about safety
    69. "Stop solving problems you don't have"
    70. Coffee and free software
    71. Great articles about design, code, and life
    72. Adobe Shadow – Device preview and debugging
    73. Gridset – create advanced grid systems on the web
    74. About guys who fix stuff
    75. On the evolution of languages and frameworks
    76. I can't design in the browser either
    77. How I set up my web development environment on OS X Lion
    78. Java and beginner programming courses at universities
    79. Realigning Johanbrook.com
    80. Songs to code by
    81. Adding custom URL endpoints in Wordpress
    82. "Can you make the logo smaller?"
    83. Hiring developers 2012 style
    84. Advice for university students: "I'm not as smart as I thought I was"
    85. Measuring and sizing UIs, 2011-style
    86. Writing documentation for CSS – Knyle Style Sheets
    87. Timeless fashion
    88. The Anatomy of a Perfect Web Site
    89. PHP needs to die
    90. Subtle updates to Chrome 14's Web Inspector
    91. A Greenhorn's Freelance Advice
    92. Chocolat – The Heir of Textmate?
    93. Add delight to web forms (with code sample)
    94. "Javascript is Dead. Long Live Javascript!"
    95. Native style momentum scrolling to arrive in iOS 5
    96. Staying hungry and evolving with new technologies
    97. Zach Holman on code documentation
    98. Relative positioning and CSS Columns
    99. A fix for antialiasing issues in WebKit browsers
    100. Bringing Order to CSS
    101. Debugging CSS Media Queries
    102. New Safari downloads UI in Lion
    103. Backup everything – my backup setup
    104. Generalist specialists
    105. Redesigned
    106. Spotify revamps the Free and Open options
    107. Digital magazines and HTML
    108. Whiteboard theme for Wordpress
    109. Firefox 4 – a bitter taste
    110. Flow out of beta
    111. Universal wrapping paper
    112. Posters by Avraham Cornfeld
    113. Customizing the Wordpress Admin Bar
    114. Photos of National Geographic
    115. Minimalistic Oscar posters
    116. Take a step back
    117. OS X Lion-Mountain Lion wallpapers
    118. Quick internal linking tag in Wordpress
    119. Get it out there – Matt Mullenweg on shipping software
    120. Dustin Diaz's $script.js
    121. Homemade is best
    122. Send URLs from iOS to desktop browser
    123. Streamlines – another kind of Twitter client
    124. Thomas Fuchs' web dev tools
    125. Visualizing WebKit's hardware acceleration
    126. Maven Pro – a free sans serif font
    127. "The Shape of Design"
    128. Use the current color in CSS with the currentcolor keyword
    129. How 37signals handles customer service
    130. Rasmus Andersson on Kod
    131. Webkit to get CSS variables, mixins, nesting?
    132. Whiteboard accounting, or how to say no to yourself
    133. "Google, H.264 and video on the web"
    134. Google to release a WebM plugin for Safari and IE9
    135. A simpler CSS3 Gradient syntax
    136. After Hours
    137. Dyluni for Wordpress
    138. Eric Meyer: Reset Revisited
    139. RSS is not dying, it's being ignored
    140. Kod app is now open source
    141. "A note to people that hire ... "
    142. The road to the 37signals homepage redesign
    143. Trickle: another kind of Twitter client
    144. Native retweets to Tweetie for Mac with ReTweetie
    145. What Matters
    146. Yet Another Site Redesign
    147. Old hardware still rocks
    148. Rolling Stones' "Gimme Shelter" deconstructed
    149. Make the invisible visible – a clever campaign from Amnesty
    150. iOS Fonts
    151. Andy Clarke's Hardboiled Web Design talk from DIBI
    152. Retro airline bags
    153. Regular people and web standards
    154. A Simple Icon
    155. Why Google can't build Instagram
    156. Five record breaking torrent files
    157. Yet another mobile HTML5 framework ... by 37signals
    158. Minimal Skype 5 message style
    159. Tower – an upcoming Git client for Mac
    160. Jeremy Keith: The Design of HTML5
    161. Paris vs. New York – a graphic comparison
    162. Autocomplete in TextEdit
    163. Formalize CSS – consistent forms
    164. Linked List style posts in Wordpress
    165. Start a Facetime call from URI in browser
    166. Firesheep – why HTTPS is important
    167. A typographic poster
    168. Windows Phone 7 needs activation for Live
    169. Panic talks about the future: Coda 2
    170. Out with the old
    171. The Beatles’ album covers redesigned
    172. Vertical Rhythm Wordpress themes
    173. Apple’s Java in OS X 10.6 is now deprecated
    174. Anatomy of a Rails Rumble project
    175. Who's suing whom?
    176. Comparison between a plumber and a freelancing designer
    177. Bjarne Stroustrup on C++'s 25th anniversary
    178. Old book illustrations
    179. Unofficial version of Notational Velocity adds goodies
    180. Interview with David Heinemeier Hansson
    181. A Markdown teleprompter
    \ No newline at end of file