feat(attachments): attach external notes, screenshots, photos, and image URLs to sparks
This commit is contained in:
1 parent
8a25771839
commit
4c5560a5a1
6 files changed
+888
-5
No files matched your search
+58
-5
@@ -107,6 +107,13 @@ export function createDatabase(dbFilePath) {
|
||||
);
|
||||
`);
|
||||
|
||||
// Ensure attachments_json column exists for existing databases
|
||||
try {
|
||||
db.exec(`ALTER TABLE ideas ADD COLUMN attachments_json TEXT DEFAULT '[]'`);
|
||||
} catch {
|
||||
// column already exists
|
||||
}
|
||||
|
||||
function safeParse(jsonStr, fallback = []) {
|
||||
try {
|
||||
return jsonStr ? JSON.parse(jsonStr) : fallback;
|
||||
@@ -124,6 +131,7 @@ export function createDatabase(dbFilePath) {
|
||||
const links = safeParse(row.links_json, []);
|
||||
const whack_history = safeParse(row.whack_history_json, []);
|
||||
const mockups = safeParse(row.mockups_json, []);
|
||||
const attachments = safeParse(row.attachments_json, []);
|
||||
const inspiredCard = row.inspired_by_card ? getCardByNumber(row.inspired_by_card) : null;
|
||||
|
||||
const hydrated = {
|
||||
@@ -142,7 +150,12 @@ export function createDatabase(dbFilePath) {
|
||||
inspired_by_card: row.inspired_by_card || null,
|
||||
inspired_card_detail: inspiredCard,
|
||||
mockups,
|
||||
cover_mockup_url: row.cover_mockup_url || (mockups[0]?.imageUrl || ""),
|
||||
attachments,
|
||||
cover_mockup_url:
|
||||
row.cover_mockup_url ||
|
||||
mockups[0]?.imageUrl ||
|
||||
attachments.find((a) => a.type === "image")?.imageUrl ||
|
||||
"",
|
||||
rating_excitement: Number(row.rating_excitement ?? 3),
|
||||
rating_impact: Number(row.rating_impact ?? 3),
|
||||
rating_ease: Number(row.rating_ease ?? 3),
|
||||
@@ -217,17 +230,22 @@ export function createDatabase(dbFilePath) {
|
||||
const impact = payload.rating_impact ?? 3;
|
||||
const ease = payload.rating_ease ?? 3;
|
||||
const mockups = Array.isArray(payload.mockups) ? payload.mockups : [];
|
||||
const attachments = Array.isArray(payload.attachments) ? payload.attachments : [];
|
||||
const whackHistory = Array.isArray(payload.whack_history) ? payload.whack_history : [];
|
||||
const coverMockupUrl = payload.cover_mockup_url || (mockups[0]?.imageUrl ?? "");
|
||||
const coverMockupUrl =
|
||||
payload.cover_mockup_url ||
|
||||
mockups[0]?.imageUrl ||
|
||||
attachments.find((a) => a.type === "image")?.imageUrl ||
|
||||
"";
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO ideas (
|
||||
title, elevator_pitch, category, status, notes_markdown,
|
||||
tags_json, checklist_json, pros_json, cons_json, links_json,
|
||||
whack_history_json, inspired_by_card, mockups_json, cover_mockup_url,
|
||||
whack_history_json, inspired_by_card, mockups_json, attachments_json, cover_mockup_url,
|
||||
rating_excitement, rating_impact, rating_ease,
|
||||
arena_wins, arena_matches, pinned, last_reviewed_at, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
@@ -244,6 +262,7 @@ export function createDatabase(dbFilePath) {
|
||||
JSON.stringify(whackHistory),
|
||||
payload.inspired_by_card ? Number(payload.inspired_by_card) : null,
|
||||
JSON.stringify(mockups),
|
||||
JSON.stringify(attachments),
|
||||
coverMockupUrl,
|
||||
Number(excitement),
|
||||
Number(impact),
|
||||
@@ -278,6 +297,7 @@ export function createDatabase(dbFilePath) {
|
||||
whack_history: Array.isArray(patch.whack_history) ? patch.whack_history : existing.whack_history,
|
||||
inspired_by_card: patch.inspired_by_card !== undefined ? (patch.inspired_by_card ? Number(patch.inspired_by_card) : null) : existing.inspired_by_card,
|
||||
mockups: Array.isArray(patch.mockups) ? patch.mockups : existing.mockups,
|
||||
attachments: Array.isArray(patch.attachments) ? patch.attachments : existing.attachments,
|
||||
cover_mockup_url: patch.cover_mockup_url !== undefined ? String(patch.cover_mockup_url) : existing.cover_mockup_url,
|
||||
rating_excitement: patch.rating_excitement !== undefined ? Math.max(1, Math.min(5, Number(patch.rating_excitement))) : existing.rating_excitement,
|
||||
rating_impact: patch.rating_impact !== undefined ? Math.max(1, Math.min(5, Number(patch.rating_impact))) : existing.rating_impact,
|
||||
@@ -293,7 +313,7 @@ export function createDatabase(dbFilePath) {
|
||||
UPDATE ideas SET
|
||||
title = ?, elevator_pitch = ?, category = ?, status = ?, notes_markdown = ?,
|
||||
tags_json = ?, checklist_json = ?, pros_json = ?, cons_json = ?, links_json = ?,
|
||||
whack_history_json = ?, inspired_by_card = ?, mockups_json = ?, cover_mockup_url = ?,
|
||||
whack_history_json = ?, inspired_by_card = ?, mockups_json = ?, attachments_json = ?, cover_mockup_url = ?,
|
||||
rating_excitement = ?, rating_impact = ?, rating_ease = ?,
|
||||
arena_wins = ?, arena_matches = ?, pinned = ?, last_reviewed_at = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
@@ -311,6 +331,7 @@ export function createDatabase(dbFilePath) {
|
||||
JSON.stringify(updated.whack_history),
|
||||
updated.inspired_by_card,
|
||||
JSON.stringify(updated.mockups),
|
||||
JSON.stringify(updated.attachments),
|
||||
updated.cover_mockup_url,
|
||||
updated.rating_excitement,
|
||||
updated.rating_impact,
|
||||
@@ -398,6 +419,37 @@ export function createDatabase(dbFilePath) {
|
||||
});
|
||||
}
|
||||
|
||||
function addAttachmentToIdea(id, attachmentEntry) {
|
||||
const idea = getIdea(id);
|
||||
if (!idea) return null;
|
||||
|
||||
const entry = {
|
||||
id: attachmentEntry.id || `att-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
||||
type: attachmentEntry.type || (attachmentEntry.imageUrl ? "image" : "note"),
|
||||
title: attachmentEntry.title || (attachmentEntry.imageUrl ? "Attached Image" : "External Note"),
|
||||
textContent: attachmentEntry.textContent || "",
|
||||
sourceUrl: attachmentEntry.sourceUrl || "",
|
||||
imageUrl: attachmentEntry.imageUrl || "",
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const nextAttachments = [entry, ...(idea.attachments || [])];
|
||||
const patch = {
|
||||
attachments: nextAttachments,
|
||||
last_reviewed_at: new Date().toISOString()
|
||||
};
|
||||
if (entry.type === "image" && entry.imageUrl && (!idea.cover_mockup_url || attachmentEntry.setAsCover)) {
|
||||
patch.cover_mockup_url = entry.imageUrl;
|
||||
}
|
||||
if (attachmentEntry.appendToNotes && entry.textContent) {
|
||||
const prefix = idea.notes_markdown ? `${idea.notes_markdown}\n\n` : "";
|
||||
const header = entry.title ? `### 📎 ${entry.title}${entry.sourceUrl ? ` (${entry.sourceUrl})` : ""}\n` : "";
|
||||
patch.notes_markdown = `${prefix}${header}${entry.textContent}`;
|
||||
}
|
||||
|
||||
return updateIdea(id, patch);
|
||||
}
|
||||
|
||||
function recordArenaMatch({ winnerId, loserId }) {
|
||||
const winner = getIdea(winnerId);
|
||||
const loser = getIdea(loserId);
|
||||
@@ -594,6 +646,7 @@ export function createDatabase(dbFilePath) {
|
||||
deleteIdea,
|
||||
logWhackOnIdea,
|
||||
addMockupToIdea,
|
||||
addAttachmentToIdea,
|
||||
recordArenaMatch,
|
||||
getSetting,
|
||||
setSetting
|
||||
|
||||
Reference in new issue
Block a user