app: Classes binder + Mind Dump — v0.5.0
Two new tabs in the Studio Notebook: - 📚 Classes: a binder for courses, workshops, books, and video series. Each class has a subject, source, link, status (taking / want / done), a what-it's-about note, photos, handout files, and dated pages of class notes that can be edited in place. - 💭 Mind Dump: idea capture filed by category (things to make — with a medium: clay / wood / silver / metal / mixed — marketing, packaging, display & booth, shop & site, other) with spark / trying / did it / parked status, sketch photos, one-tap 'make it a to-do', and filters. The Today jot box gains a 💭 idea chip; those land as 'unsorted' to file later. Schema: Course, CoursePage, Idea (additive; prisma db push at boot). Files tab now labels attachments that belong to a class. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -109,6 +109,10 @@ const sFile = (f) => ({ id: f.id, entity: f.entity, entity_id: f.entityId, store
|
||||
const sProduct = (p) => ({ id: p.id, name: p.name, kind: p.kind, category: p.category, price: p.price,
|
||||
cost: p.cost, qty: p.qty, status: p.status, notes: p.notes, created: fmtDate(p.created) });
|
||||
const sSale = (s) => ({ id: s.id, product_id: s.productId, qty: s.qty, price: s.price, cost: s.cost, created: fmtDate(s.created) });
|
||||
const sCourse = (c) => ({ id: c.id, title: c.title, subject: c.subject, source: c.source, url: c.url, status: c.status,
|
||||
notes: c.notes, created: fmtDate(c.created), page_count: c._count?.pages ?? 0 });
|
||||
const sPage = (p) => ({ id: p.id, course_id: p.courseId, title: p.title, text: p.text, created: fmtDate(p.created), updated: fmtDate(p.updated) });
|
||||
const sIdea = (i) => ({ id: i.id, text: i.text, category: i.category, medium: i.medium, status: i.status, created: fmtDate(i.created) });
|
||||
const sSupplier = (s) => ({ id: s.id, name: s.name, kind: s.kind, distance: s.distance, url: s.url,
|
||||
notes: s.notes, search_url: s.searchUrl, created: fmtDate(s.created) });
|
||||
|
||||
@@ -249,6 +253,10 @@ const routes = {
|
||||
for (const l of lines) await db.shopping.create({ data: { text: l } });
|
||||
return { ok: true };
|
||||
}
|
||||
if (kind === "idea") { // → mind dump, unsorted
|
||||
await db.idea.create({ data: { text: b.text } });
|
||||
return { ok: true };
|
||||
}
|
||||
if (kind === "suggestion") {
|
||||
try {
|
||||
const issue = await fileSuggestion(b.text, session?.email || "");
|
||||
@@ -405,11 +413,78 @@ const routes = {
|
||||
|
||||
"GET /api/files": async () => {
|
||||
const files = await db.fileAsset.findMany({ orderBy: { id: "desc" } });
|
||||
const productIds = [...new Set(files.filter((f) => f.entity === "product").map((f) => f.entityId))];
|
||||
const products = await db.product.findMany({ where: { id: { in: productIds } } });
|
||||
const idsOf = (entity) => [...new Set(files.filter((f) => f.entity === entity).map((f) => f.entityId))];
|
||||
const products = await db.product.findMany({ where: { id: { in: idsOf("product") } } });
|
||||
const courses = await db.course.findMany({ where: { id: { in: idsOf("course") } } });
|
||||
const nameOf = Object.fromEntries(products.map((p) => [p.id, p.name]));
|
||||
return files.map((f) => ({ ...sFile(f), product_name: f.entity === "product" ? nameOf[f.entityId] ?? null : null }));
|
||||
const courseOf = Object.fromEntries(courses.map((c) => [c.id, c.title]));
|
||||
return files.map((f) => ({ ...sFile(f), product_name: f.entity === "product" ? nameOf[f.entityId] ?? null : null,
|
||||
course_name: f.entity === "course" ? courseOf[f.entityId] ?? null : null }));
|
||||
},
|
||||
// ── Classes binder ─────────────────────────────────────────────
|
||||
"GET /api/courses": async () =>
|
||||
(await db.course.findMany({ orderBy: [{ status: "asc" }, { title: "asc" }], include: { _count: { select: { pages: true } } } })).map(sCourse),
|
||||
"GET /api/course": async (_b, q) => {
|
||||
const id = Number(q.get("id"));
|
||||
const c = await db.course.findUnique({ where: { id }, include: { pages: { orderBy: { id: "desc" } } } });
|
||||
if (!c) return null;
|
||||
return { ...sCourse(c), pages: c.pages.map(sPage),
|
||||
photos: (await db.photo.findMany({ where: { entity: "course", entityId: id } })).map(sPhoto),
|
||||
files: (await db.fileAsset.findMany({ where: { entity: "course", entityId: id }, orderBy: { id: "desc" } })).map(sFile) };
|
||||
},
|
||||
"POST /api/courses": async (b) => {
|
||||
const c = await db.course.create({ data: { title: b.title, subject: b.subject || "", source: b.source || "",
|
||||
url: b.url || "", status: b.status || "taking", notes: b.notes || "" } });
|
||||
return { id: c.id };
|
||||
},
|
||||
"POST /api/course/update": async (b) => {
|
||||
const data = {};
|
||||
for (const k of ["title", "subject", "source", "url", "status", "notes"]) if (typeof b[k] === "string") data[k] = b[k];
|
||||
await db.course.update({ where: { id: b.id }, data });
|
||||
return { ok: true };
|
||||
},
|
||||
"POST /api/course/delete": async (b) => {
|
||||
await db.course.delete({ where: { id: b.id } }); // pages cascade
|
||||
await db.photo.deleteMany({ where: { entity: "course", entityId: b.id } });
|
||||
for (const f of await db.fileAsset.findMany({ where: { entity: "course", entityId: b.id } })) {
|
||||
try { unlinkSync(join(FILES, f.stored)); } catch { /* already gone */ }
|
||||
}
|
||||
await db.fileAsset.deleteMany({ where: { entity: "course", entityId: b.id } });
|
||||
return { ok: true };
|
||||
},
|
||||
"POST /api/course/pages": async (b) => {
|
||||
const p = await db.coursePage.create({ data: { courseId: b.course_id, title: b.title || "", text: b.text || "" } });
|
||||
return { id: p.id };
|
||||
},
|
||||
"POST /api/course/pages/update": async (b) => {
|
||||
await db.coursePage.update({ where: { id: b.id }, data: {
|
||||
...(typeof b.title === "string" ? { title: b.title } : {}), ...(typeof b.text === "string" ? { text: b.text } : {}), updated: new Date() } });
|
||||
return { ok: true };
|
||||
},
|
||||
"POST /api/course/pages/delete": async (b) => { await db.coursePage.delete({ where: { id: b.id } }); return { ok: true }; },
|
||||
|
||||
// ── Mind dump ──────────────────────────────────────────────────
|
||||
"GET /api/ideas": async () => {
|
||||
const list = await db.idea.findMany({ orderBy: { id: "desc" } });
|
||||
const photos = await db.photo.findMany({ where: { entity: "idea" } });
|
||||
return list.map((i) => ({ ...sIdea(i), photos: photos.filter((p) => p.entityId === i.id).map(sPhoto) }));
|
||||
},
|
||||
"POST /api/ideas": async (b) => {
|
||||
const i = await db.idea.create({ data: { text: b.text, category: b.category || "", medium: b.medium || "", status: b.status || "spark" } });
|
||||
return { id: i.id };
|
||||
},
|
||||
"POST /api/ideas/update": async (b) => {
|
||||
const data = {};
|
||||
for (const k of ["text", "category", "medium", "status"]) if (typeof b[k] === "string") data[k] = b[k];
|
||||
await db.idea.update({ where: { id: b.id }, data });
|
||||
return { ok: true };
|
||||
},
|
||||
"POST /api/ideas/delete": async (b) => {
|
||||
await db.idea.delete({ where: { id: b.id } });
|
||||
await db.photo.deleteMany({ where: { entity: "idea", entityId: b.id } });
|
||||
return { ok: true };
|
||||
},
|
||||
|
||||
"POST /api/files/delete": async (b) => {
|
||||
const f = await db.fileAsset.findUnique({ where: { id: b.id } });
|
||||
if (f) {
|
||||
|
||||
Reference in New Issue
Block a user