@@ -19,7 +19,7 @@ document.querySelectorAll("#nav button").forEach((b) =>
render ( ) ;
} ) ) ;
function render ( ) { ( { today , glazes , firings , pieces , files , sourcing , notes } ) [ current ] ( ) ; }
function render ( ) { ( { today , glazes , firings , pieces , files , sourcing , classes , ideas , notes } ) [ current ] ( ) ; }
// ── jot box (shared) ─────────────────────────────────────────────
const JOT _HINT = "⌨️ Enter saves · Shift+Enter = subtask line · Tab switches type · press / from anywhere to jot" ;
@@ -44,6 +44,7 @@ function wireCapture(after) {
await after ( ) ;
const j = $ ( "#jot" ) ;
if ( j ) { j . value = "" ; j . focus ( ) ; } // stay in the box — rapid-fire jotting
if ( kind === "idea" ) jotSaid ( "💭 dropped in the mind dump — file it there whenever" ) ;
if ( kind === "suggestion" ) jotSaid ( r . issue ? ` 💡 sent to Tony — suggestion # ${ r . issue } , thank you! `
: "💡 kept in your notes — it'll get passed along" ) ;
} ;
@@ -100,7 +101,7 @@ async function today() {
? ` <div class="todo"><span>🔥 <b>Firing # ${ s . activeFiring . id } </b> in progress — ${ esc ( s . activeFiring . type ) } ${ esc ( s . activeFiring . cone ) } </span></div>
<div class="mt"><button class="btn mini" id="goto-firing">go to firing</button></div> `
: ` <div class="todo"><span>Kiln idle ${ s . lastUnload ? ` — last unload ${ esc ( s . lastUnload . unloaded ? . slice ( 0 , 10 ) ) } ` : "" } </span></div> ` ;
view . innerHTML = captureHTML ( [ [ "note" , "✨ note" ] , [ "todo" , "✓ to-do" ] , [ "shopping" , "🧺 shopping" ] , [ "glaze idea" , "🧪 glaze idea" ] , [ "suggestion" , "💡 suggestion" ] ] ) +
view . innerHTML = captureHTML ( [ [ "note" , "✨ note" ] , [ "todo" , "✓ to-do" ] , [ "shopping" , "🧺 shopping" ] , [ "glaze idea" , "🧪 glaze idea" ] , [ "idea" , "💭 idea" ] , [ "suggestion" , "💡 suggestion" ] ] ) +
` <div class="grid g2">
<div class="card"><h3>To-do</h3> ${ todoRows } </div>
<div class="card"><h3>Shopping list 🧺</h3> ${ shopRows } </div>
@@ -434,14 +435,15 @@ function fileRow(f, showHome) {
<span class="meta"> ${ fmtSize ( f . size ) } · ${ esc ( f . created . slice ( 0 , 10 ) ) } </span></span>
<span> ${ f . tag ? ` <span class="pill butter"> ${ esc ( f . tag ) } </span> ` : "" }
${ showHome && f . product _name ? ` <span class="pill sage">🏺 ${ esc ( f . product _name ) } </span> ` : "" }
${ showHome && f . course _name ? ` <span class="pill sage">📚 ${ esc ( f . course _name ) } </span> ` : "" }
<button class="chip" data-fdel=" ${ f . id } " title="delete">🗑</button></span></div> ` ;
}
function uploadWidget ( entity , entityId , after ) {
function uploadWidget ( entity , entityId , after , tags = FILE _TAGS ) {
return {
html : ` <div class="capture mt"><div class="row1">
<button class="btn soft" id="file-pick">⬆️ Upload file(s)</button>
<input type="file" id="file-input" multiple hidden></div>
<div class="chips"><span class="hint">tag as…</span> ${ FILE _TAGS . map ( ( t , i ) => ` <span class="chip ${ i === 0 ? " on" : "" } " data-ftag=" ${ t } "> ${ t } </span> ` ) . join ( "" ) } </div></div> ` ,
<div class="chips"><span class="hint">tag as…</span> ${ tags . map ( ( t , i ) => ` <span class="chip ${ i === 0 ? " on" : "" } " data-ftag=" ${ t } "> ${ t } </span> ` ) . join ( "" ) } </div></div> ` ,
wire ( ) {
view . querySelectorAll ( "[data-ftag]" ) . forEach ( ( c ) =>
c . addEventListener ( "click" , ( ) => view . querySelectorAll ( "[data-ftag]" ) . forEach ( ( x ) => x . classList . toggle ( "on" , x === c ) ) ) ) ;
@@ -472,7 +474,7 @@ async function files() {
view . innerHTML = ` ${ up . html }
<div class="card mt"> ${ section ( "Studio files 🗂️" ) }
${ loose . map ( ( f ) => fileRow ( f , false ) ) . join ( "" ) || '<div class="empty">STLs for master molds, foam form templates, cut files… upload anything ⬆️</div>' } </div>
${ attached . length ? ` <div class="card mt"> ${ section ( "Attached to pieces 🏺 " ) } ${ attached . map ( ( f ) => fileRow ( f , true ) ) . join ( "" ) } </div> ` : "" } ` ;
${ attached . length ? ` <div class="card mt"> ${ section ( "Attached to pieces & classes " ) } ${ attached . map ( ( f ) => fileRow ( f , true ) ) . join ( "" ) } </div> ` : "" } ` ;
up . wire ( ) ;
wireFileDeletes ( files ) ;
}
@@ -623,6 +625,254 @@ async function notes() {
wireCapture ( notes ) ;
}
// ── Classes (the education binder) ──────────────────────────────
const SUBJECTS = [ "methods" , "skills" , "marketing" , "business" , "glaze chemistry" , "other" ] ;
const CLASS _STATUS = [ [ "taking" , "📖 taking now" ] , [ "want" , "🔖 want to take" ] , [ "done" , "✅ finished" ] ] ;
const CLASS _TAGS = [ "handout" , "slides" , "worksheet" , "reference" , "other" ] ;
const sectionH = ( label ) => ` <h3 style="font-size:12.5px;text-transform:uppercase;letter-spacing:0.1em;color:var(--blush-deep);font-weight:700;margin:22px 0 12px"> ${ label } </h3> ` ;
async function classes ( ) {
const list = await api ( "/api/courses" ) ;
const card = ( c ) => ` <div class="card clickable" data-c=" ${ c . id } ">
<h4> ${ esc ( c . title ) } </h4>
<div class="mt"> ${ c . subject ? ` <span class="pill butter"> ${ esc ( c . subject ) } </span> ` : "" } <span class="pill sage"> ${ c . page _count } page ${ c . page _count === 1 ? "" : "s" } </span></div>
${ c . source ? ` <div class="meta" style="margin-top:8px"> ${ esc ( c . source ) } </div> ` : "" } </div> ` ;
const group = ( key ) => list . filter ( ( c ) => ( c . status || "taking" ) === key ) ;
view . innerHTML = ` <div style="margin-bottom:14px"><button class="btn" id="new-class">+ New class</button>
<span class="meta" style="margin-left:10px">courses, workshops, books, video series — one binder tab each</span></div>
${ CLASS _STATUS . map ( ( [ k , label ] ) => {
const g = group ( k ) ;
if ( ! g . length && k !== "taking" ) return "" ;
return sectionH ( label ) + ` <div class="grid g3"> ${ g . map ( card ) . join ( "" ) ||
'<div class="empty">your binder starts here — add a class, a workshop, a book, or a video series 📚</div>' } </div> ` ;
} ).join("")} ` ;
$ ( "#new-class" ) . addEventListener ( "click" , newClassForm ) ;
view . querySelectorAll ( "[data-c]" ) . forEach ( ( c ) => c . addEventListener ( "click" , ( ) => classDetail ( Number ( c . dataset . c ) ) ) ) ;
}
function newClassForm ( ) {
view . innerHTML = ` <button class="back" id="back">← back to classes</button>
<div class="card"><h4>New class</h4>
<div class="meta" style="margin-bottom:14px">A title is enough — this can be a course, a workshop, a book, a YouTube series, anything you're learning from.</div>
<div class="field"><label>Title</label><input type="text" id="c-title" placeholder="e.g. Slipcasting basics — Sarah's workshop" style="width:100%"></div>
<div class="grid g2">
<div class="field"><label>Subject</label><input type="text" id="c-subject" list="subject-list" placeholder="methods, marketing, skills…" style="width:100%">
<datalist id="subject-list"> ${ SUBJECTS . map ( ( s ) => ` <option> ${ s } </option> ` ) . join ( "" ) } </datalist></div>
<div class="field"><label>Status</label><div class="chips"> ${ CLASS _STATUS . map ( ( [ k , l ] , i ) => ` <span class="chip ${ i === 0 ? " on" : "" } " data-cs=" ${ k } "> ${ l } </span> ` ) . join ( "" ) } </div></div>
<div class="field"><label>Teacher / where it's from</label><input type="text" id="c-source" placeholder="teacher, school, author, channel…" style="width:100%"></div>
<div class="field"><label>Link</label><input type="text" id="c-url" placeholder="https://…" style="width:100%"></div>
</div>
<div class="field"><label>What it's about</label><textarea id="c-notes" rows="2" style="width:100%" placeholder="why you're taking it, what you hope to get out of it…"></textarea></div>
<button class="btn" id="save-class">Save class</button></div> ` ;
view . querySelectorAll ( "[data-cs]" ) . forEach ( ( c ) =>
c . addEventListener ( "click" , ( ) => view . querySelectorAll ( "[data-cs]" ) . forEach ( ( x ) => x . classList . toggle ( "on" , x === c ) ) ) ) ;
$ ( "#back" ) . addEventListener ( "click" , classes ) ;
$ ( "#c-title" ) . focus ( ) ;
$ ( "#save-class" ) . addEventListener ( "click" , async ( ) => {
const title = $ ( "#c-title" ) . value . trim ( ) ;
if ( ! title ) return alert ( "A title is all it needs 🙂" ) ;
const r = await api ( "/api/courses" , { title , subject : $ ( "#c-subject" ) . value . trim ( ) , source : $ ( "#c-source" ) . value . trim ( ) ,
url : $ ( "#c-url" ) . value . trim ( ) , notes : $ ( "#c-notes" ) . value . trim ( ) , status : view . querySelector ( "[data-cs].on" ) ? . dataset . cs || "taking" } ) ;
classDetail ( r . id ) ;
} ) ;
}
async function classDetail ( id ) {
const c = await api ( ` /api/course?id= ${ id } ` ) ;
if ( ! c ) return classes ( ) ;
const up = uploadWidget ( "course" , id , ( ) => classDetail ( id ) , CLASS _TAGS ) ;
const page = ( p ) => ` <div class="card mt" data-page=" ${ p . id } ">
<div class="loadrow" style="border:none;padding:0"><h4> ${ esc ( p . title || "Page from " + p . created . slice ( 0 , 10 ) ) } </h4>
<span><span class="meta"> ${ esc ( p . created . slice ( 0 , 10 ) ) } </span>
<button class="chip" data-pedit=" ${ p . id } ">✏️ edit</button> <button class="chip" data-pdel=" ${ p . id } " title="delete page">🗑</button></span></div>
<p class="page-text" style="white-space:pre-wrap;font-size:14.5px;margin-top:8px"> ${ esc ( p . text ) } </p>
<div class="pageform" hidden style="margin-top:8px">
<input type="text" class="pe-title" value=" ${ esc ( p . title ) } " placeholder="page title (optional)" style="width:100%;margin-bottom:8px">
<textarea class="pe-text" rows="8" style="width:100%"> ${ esc ( p . text ) } </textarea>
<div class="mt"><button class="btn mini" data-psave=" ${ p . id } ">save</button> <button class="btn mini soft" data-pcancel=" ${ p . id } ">cancel</button></div></div></div> ` ;
view . innerHTML = ` <button class="back" id="back">← back to classes</button>
<div class="card">
<h4> ${ esc ( c . title ) } </h4>
<div class="mt"> ${ c . subject ? ` <span class="pill butter"> ${ esc ( c . subject ) } </span> ` : "" } ${ c . source ? ` <span class="meta"> ${ esc ( c . source ) } </span> ` : "" } ${ c . url ? ` <a href=" ${ esc ( c . url ) } " target="_blank" class="pill">open link ↗</a> ` : "" } </div>
<div class="chips mt"> ${ CLASS _STATUS . map ( ( [ k , l ] ) => ` <span class="chip ${ ( c . status || "taking" ) === k ? " on" : "" } " data-cs=" ${ k } "> ${ l } </span> ` ) . join ( "" ) } </div>
<div class="field mt"><label>What it's about</label>
<textarea id="c-notes" rows="2" style="width:100%" placeholder="why you're taking it, what you hope to get out of it…"> ${ esc ( c . notes || "" ) } </textarea>
<button class="btn mini soft mt" id="save-notes">save</button> <span class="meta" id="notes-said"></span></div>
<div class="mt"><h3>Photos</h3><div class="photos">
${ c . photos . map ( ( ph ) => ` <img src="/photos/ ${ ph . filename } " alt=""> ` ) . join ( "" ) }
<button class="ph-add" id="ph-add">+ </button><input type="file" id="ph-file" accept="image/*" hidden></div></div>
<div class="mt"><h3>Handouts & files</h3>
${ c . files . map ( ( f ) => fileRow ( f , false ) ) . join ( "" ) || '<div class="empty">nothing attached yet</div>' }
${ up . html } </div>
<div class="mt" style="text-align:right"><button class="chip" id="del-class">🗑 delete this class</button></div>
</div>
${ sectionH ( "New page ✍️" ) }
<div class="capture">
<input type="text" id="np-title" placeholder="page title (optional) — e.g. Week 2: pricing your work" style="width:100%;margin-bottom:8px">
<textarea id="np-text" rows="4" style="width:100%" placeholder="notes from class… write as much as you like, this is your binder"></textarea>
<div class="row1 mt"><button class="go" id="np-save">Add page</button><span class="meta" style="align-self:center">⌘/Ctrl+Enter also saves</span></div></div>
${ sectionH ( ` Pages ( ${ c . pages . length } ) ` ) }
${ c . pages . map ( page ) . join ( "" ) || '<div class="empty">no pages yet — your first class notes go above ✨</div>' } ` ;
$ ( "#back" ) . addEventListener ( "click" , classes ) ;
view . querySelectorAll ( "[data-cs]" ) . forEach ( ( ch ) =>
ch . addEventListener ( "click" , async ( ) => { await api ( "/api/course/update" , { id , status : ch . dataset . cs } ) ; classDetail ( id ) ; } ) ) ;
$ ( "#save-notes" ) . addEventListener ( "click" , async ( ) => {
await api ( "/api/course/update" , { id , notes : $ ( "#c-notes" ) . value } ) ;
$ ( "#notes-said" ) . textContent = "saved ✓" ;
setTimeout ( ( ) => { const s = $ ( "#notes-said" ) ; if ( s ) s . textContent = "" ; } , 3000 ) ;
} ) ;
$ ( "#ph-add" ) . addEventListener ( "click" , ( ) => $ ( "#ph-file" ) . click ( ) ) ;
$ ( "#ph-file" ) . addEventListener ( "change" , async ( e ) => {
const f = e . target . files [ 0 ] ;
if ( f ) { await fetch ( ` /api/photos?entity=course&id= ${ id } &name= ${ encodeURIComponent ( f . name ) } ` , { method : "POST" , body : f } ) ; classDetail ( id ) ; }
} ) ;
up . wire ( ) ;
wireFileDeletes ( ( ) => classDetail ( id ) ) ;
$ ( "#del-class" ) . addEventListener ( "click" , async ( ) => {
if ( confirm ( ` Delete " ${ c . title } " and all its pages? ` ) ) { await api ( "/api/course/delete" , { id } ) ; classes ( ) ; }
} ) ;
const addPage = async ( ) => {
const text = $ ( "#np-text" ) . value . trim ( ) ;
if ( ! text ) return $ ( "#np-text" ) . focus ( ) ;
await api ( "/api/course/pages" , { course _id : id , title : $ ( "#np-title" ) . value . trim ( ) , text } ) ;
classDetail ( id ) ;
} ;
$ ( "#np-save" ) . addEventListener ( "click" , addPage ) ;
$ ( "#np-text" ) . addEventListener ( "keydown" , ( e ) => { if ( e . key === "Enter" && ( e . metaKey || e . ctrlKey ) ) { e . preventDefault ( ) ; addPage ( ) ; } } ) ;
$ ( "#np-title" ) . addEventListener ( "keydown" , ( e ) => { if ( e . key === "Enter" ) { e . preventDefault ( ) ; $ ( "#np-text" ) . focus ( ) ; } } ) ;
const pageEl = ( pid ) => view . querySelector ( ` [data-page=" ${ pid } "] ` ) ;
const toggleEdit = ( pid , on ) => {
const el = pageEl ( pid ) ;
el . querySelector ( ".pageform" ) . hidden = ! on ;
el . querySelector ( ".page-text" ) . hidden = on ;
if ( on ) el . querySelector ( ".pe-text" ) . focus ( ) ;
} ;
view . querySelectorAll ( "[data-pedit]" ) . forEach ( ( b ) => b . addEventListener ( "click" , ( ) => toggleEdit ( Number ( b . dataset . pedit ) , true ) ) ) ;
view . querySelectorAll ( "[data-pcancel]" ) . forEach ( ( b ) => b . addEventListener ( "click" , ( ) => toggleEdit ( Number ( b . dataset . pcancel ) , false ) ) ) ;
view . querySelectorAll ( "[data-psave]" ) . forEach ( ( b ) =>
b . addEventListener ( "click" , async ( ) => {
const pid = Number ( b . dataset . psave ) , el = pageEl ( pid ) ;
await api ( "/api/course/pages/update" , { id : pid , title : el . querySelector ( ".pe-title" ) . value . trim ( ) , text : el . querySelector ( ".pe-text" ) . value } ) ;
classDetail ( id ) ;
} ) ) ;
view . querySelectorAll ( "[data-pdel]" ) . forEach ( ( b ) =>
b . addEventListener ( "click" , async ( ) => {
if ( confirm ( "Delete this page?" ) ) { await api ( "/api/course/pages/delete" , { id : Number ( b . dataset . pdel ) } ) ; classDetail ( id ) ; }
} ) ) ;
}
// ── Mind Dump (ideas, categorized) ───────────────────────────────
const IDEA _CATS = [ [ "make" , "🏺 things to make" ] , [ "marketing" , "📣 marketing" ] , [ "packaging" , "📦 packaging" ] , [ "display" , "🎪 display & booth" ] , [ "shop" , "🛍️ shop & site" ] , [ "other" , "💭 other" ] ] ;
const MEDIA = [ "clay" , "wood" , "silver" , "metal" , "mixed" ] ;
const IDEA _STATUS = [ [ "spark" , "✨ spark" ] , [ "trying" , "🔧 trying it" ] , [ "made" , "✅ did it" ] , [ "parked" , "💤 parked" ] ] ;
const IDEA _HINT = "⌨️ Enter saves · Shift+Enter = new line · Tab switches category · press / from anywhere" ;
let ideaFilter = "all" ;
const catLabel = ( k ) => ( IDEA _CATS . find ( ( [ c ] ) => c === k ) || [ k , "🗂️ unsorted" ] ) [ 1 ] ;
async function ideas ( ) {
const list = await api ( "/api/ideas" ) ;
const count = ( fn ) => list . filter ( fn ) . length ;
const live = ( i ) => i . status !== "parked" ;
const shown = list . filter ( ( i ) =>
ideaFilter === "all" ? live ( i )
: ideaFilter === "unsorted" ? ! i . category && live ( i )
: ideaFilter === "parked" ? i . status === "parked"
: i . category === ideaFilter && live ( i ) ) ;
const small = 'style="font-size:11.5px;padding:3px 9px"' ;
const card = ( i ) => ` <div class="card" data-idea=" ${ i . id } ">
<p style="font-size:14.5px;white-space:pre-wrap"> ${ esc ( i . text ) } </p>
<div class="mt"><span class="pill ${ i . category ? " butter" : "" } "> ${ catLabel ( i . category ) } </span> ${ i . medium ? ` <span class="pill sage"> ${ esc ( i . medium ) } </span> ` : "" } <span class="meta"> ${ esc ( i . created . slice ( 0 , 10 ) ) } </span></div>
${ i . photos . length ? ` <div class="photos"> ${ i . photos . map ( ( p ) => ` <img src="/photos/ ${ p . filename } " alt=""> ` ) . join ( "" ) } </div> ` : "" }
<div class="chips" style="margin-top:10px"> ${ IDEA _STATUS . map ( ( [ k , l ] ) => ` <span class="chip ${ i . status === k ? " on" : "" } " data-ist=" ${ k } " ${ small } > ${ l } </span> ` ) . join ( "" ) }
<span style="flex:1"></span><button class="chip" data-irefile ${ small } > ${ i . category ? "🗂️ re-file" : "🗂️ file it" } </button><button class="chip" data-itodo ${ small } >✓ to-do</button><button class="chip" data-iphoto ${ small } >📷</button><button class="chip" data-idel ${ small } >🗑</button></div>
<div class="refile" ${ i . category ? "hidden" : "" } >
<div class="chips"><span class="hint">file under</span> ${ IDEA _CATS . map ( ( [ k , l ] ) => ` <span class="chip ${ i . category === k ? " on" : "" } " data-icat=" ${ k } " ${ small } > ${ l } </span> ` ) . join ( "" ) } </div>
${ i . category === "make" ? ` <div class="chips"><span class="hint">medium</span> ${ MEDIA . map ( ( m ) => ` <span class="chip ${ i . medium === m ? " on" : "" } " data-imed=" ${ m } " ${ small } > ${ m } </span> ` ) . join ( "" ) } </div> ` : "" }
</div></div> ` ;
const filterChip = ( k , label , n ) => n || k === "all" ? ` <span class="chip ${ ideaFilter === k ? " on" : "" } " data-if=" ${ k } "> ${ label } ${ n ? ` <span style="opacity:.7"> ${ n } </span> ` : "" } </span> ` : "" ;
const filters = filterChip ( "all" , "everything" , count ( live ) )
+ filterChip ( "unsorted" , "🗂️ unsorted" , count ( ( i ) => ! i . category && live ( i ) ) )
+ IDEA _CATS . map ( ( [ k , l ] ) => filterChip ( k , l , count ( ( i ) => i . category === k && live ( i ) ) ) ) . join ( "" )
+ filterChip ( "parked" , "💤 parked" , count ( ( i ) => i . status === "parked" ) ) ;
let body ;
if ( ideaFilter === "all" ) {
const groups = [ [ "" , "🗂️ unsorted — file these when you have a minute" ] , ... IDEA _CATS ] . map ( ( [ k , l ] ) => [ k , l , shown . filter ( ( i ) => ( i . category || "" ) === k ) ] ) . filter ( ( [ , , g ] ) => g . length ) ;
body = groups . map ( ( [ , l , g ] ) => sectionH ( l ) + ` <div class="grid g2"> ${ g . map ( card ) . join ( "" ) } </div> ` ) . join ( "" )
|| '<div class="empty">empty head, full studio — dump the first idea above 💭</div>' ;
} else {
body = ` <div class="grid g2" style="margin-top:14px"> ${ shown . map ( card ) . join ( "" ) || '<div class="empty">nothing here yet</div>' } </div> ` ;
}
view . innerHTML = ` <div class="capture"><div class="row1">
<textarea id="jot" rows="1" style="flex:1;resize:none" placeholder="Dump it here… a piece to make, a booth idea, a packaging thought, anything"></textarea>
<button class="go" id="idea-save">Save</button></div>
<div class="chips"><span class="hint">file under…</span> ${ IDEA _CATS . map ( ( [ k , l ] , i ) => ` <span class="chip ${ i === 0 ? " on" : "" } " data-ncat=" ${ k } "> ${ l } </span> ` ) . join ( "" ) } </div>
<div class="chips" id="medium-row"><span class="hint">medium</span> ${ MEDIA . map ( ( m ) => ` <span class="chip" data-nmed=" ${ m } "> ${ m } </span> ` ) . join ( "" ) } </div>
<div class="chips"><span class="hint" id="jot-hint"> ${ IDEA _HINT } </span></div></div>
<div class="chips" style="margin:0 0 4px"> ${ filters } </div>
${ body }
<input type="file" id="idea-ph" accept="image/*" hidden> ` ;
// capture
const jot = $ ( "#jot" ) ;
const catChips = [ ... view . querySelectorAll ( "[data-ncat]" ) ] ;
const selectCat = ( c ) => { catChips . forEach ( ( x ) => x . classList . toggle ( "on" , x === c ) ) ; $ ( "#medium-row" ) . hidden = c . dataset . ncat !== "make" ; } ;
catChips . forEach ( ( c ) => c . addEventListener ( "click" , ( ) => selectCat ( c ) ) ) ;
view . querySelectorAll ( "[data-nmed]" ) . forEach ( ( c ) =>
c . addEventListener ( "click" , ( ) => view . querySelectorAll ( "[data-nmed]" ) . forEach ( ( x ) => x . classList . toggle ( "on" , x === c && ! x . classList . contains ( "on" ) ) ) ) ) ;
const save = async ( ) => {
const text = jot . value . trim ( ) ;
if ( ! text ) return ;
const category = view . querySelector ( "[data-ncat].on" ) ? . dataset . ncat || "make" ;
await api ( "/api/ideas" , { text , category , medium : category === "make" ? view . querySelector ( "[data-nmed].on" ) ? . dataset . nmed || "" : "" } ) ;
await ideas ( ) ;
const j = $ ( "#jot" ) ;
if ( j ) { j . value = "" ; j . focus ( ) ; }
jotSaid ( "💭 saved — it's in the pile" ) ;
} ;
$ ( "#idea-save" ) . addEventListener ( "click" , save ) ;
jot . addEventListener ( "keydown" , ( e ) => {
if ( e . key === "Enter" && ! e . shiftKey ) { e . preventDefault ( ) ; save ( ) ; }
else if ( e . key === "Tab" ) {
e . preventDefault ( ) ;
const i = catChips . findIndex ( ( c ) => c . classList . contains ( "on" ) ) ;
selectCat ( catChips [ ( i + ( e . shiftKey ? catChips . length - 1 : 1 ) ) % catChips . length ] ) ;
}
} ) ;
jot . addEventListener ( "input" , ( ) => setTimeout ( ( ) => { jot . style . height = "auto" ; jot . style . height = jot . scrollHeight + "px" ; } ) ) ;
// filters
view . querySelectorAll ( "[data-if]" ) . forEach ( ( c ) => c . addEventListener ( "click" , ( ) => { ideaFilter = c . dataset . if ; ideas ( ) ; } ) ) ;
// per-card actions
const idOf = ( el ) => Number ( el . closest ( "[data-idea]" ) . dataset . idea ) ;
view . querySelectorAll ( "[data-ist]" ) . forEach ( ( c ) =>
c . addEventListener ( "click" , async ( ) => { await api ( "/api/ideas/update" , { id : idOf ( c ) , status : c . dataset . ist } ) ; ideas ( ) ; } ) ) ;
view . querySelectorAll ( "[data-irefile]" ) . forEach ( ( b ) =>
b . addEventListener ( "click" , ( ) => { const r = b . closest ( "[data-idea]" ) . querySelector ( ".refile" ) ; r . hidden = ! r . hidden ; } ) ) ;
view . querySelectorAll ( "[data-icat]" ) . forEach ( ( c ) =>
c . addEventListener ( "click" , async ( ) => {
const cat = c . dataset . icat ;
await api ( "/api/ideas/update" , { id : idOf ( c ) , category : cat , ... ( cat === "make" ? { } : { medium : "" } ) } ) ;
ideas ( ) ;
} ) ) ;
view . querySelectorAll ( "[data-imed]" ) . forEach ( ( c ) =>
c . addEventListener ( "click" , async ( ) => {
await api ( "/api/ideas/update" , { id : idOf ( c ) , medium : c . classList . contains ( "on" ) ? "" : c . dataset . imed } ) ; ideas ( ) ;
} ) ) ;
view . querySelectorAll ( "[data-itodo]" ) . forEach ( ( b ) =>
b . addEventListener ( "click" , async ( ) => {
const text = b . closest ( "[data-idea]" ) . querySelector ( "p" ) . textContent ;
await api ( "/api/todos/add" , { text : text . split ( "\n" ) [ 0 ] . slice ( 0 , 200 ) } ) ;
b . textContent = "✓ on your to-do list" ; b . disabled = true ;
} ) ) ;
let photoTarget = null ;
view . querySelectorAll ( "[data-iphoto]" ) . forEach ( ( b ) =>
b . addEventListener ( "click" , ( ) => { photoTarget = idOf ( b ) ; $ ( "#idea-ph" ) . click ( ) ; } ) ) ;
$ ( "#idea-ph" ) . addEventListener ( "change" , async ( e ) => {
const f = e . target . files [ 0 ] ;
if ( f && photoTarget ) { await fetch ( ` /api/photos?entity=idea&id= ${ photoTarget } &name= ${ encodeURIComponent ( f . name ) } ` , { method : "POST" , body : f } ) ; ideas ( ) ; }
} ) ;
view . querySelectorAll ( "[data-idel]" ) . forEach ( ( b ) =>
b . addEventListener ( "click" , async ( ) => {
if ( confirm ( "Delete this idea?" ) ) { await api ( "/api/ideas/delete" , { id : idOf ( b ) } ) ; ideas ( ) ; }
} ) ) ;
}
document . getElementById ( "signout" ) ? . addEventListener ( "click" , async ( ) => {
await api ( "/api/logout" , { } ) ;
location . href = "/login" ;