655 lines
26 KiB
JavaScript
655 lines
26 KiB
JavaScript
import { DatabaseSync } from "node:sqlite";
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import { getCardByNumber } from "./whack_cards.js";
|
|
|
|
export const VALID_STATUSES = ["spark", "exploring", "building", "shipped", "archived"];
|
|
export const VALID_CATEGORIES = ["project", "creative", "spark", "research"];
|
|
|
|
export function parseQuickCaptureInput(rawInput, defaultCategory = "spark") {
|
|
let text = String(rawInput || "").trim();
|
|
const tags = [];
|
|
let detectedCategory = null;
|
|
let boostExcitement = false;
|
|
|
|
// Extract #tags
|
|
text = text.replace(/(?:^|\s)#([a-zA-Z0-9_-]+)/g, (_, tag) => {
|
|
const clean = tag.toLowerCase();
|
|
if (!tags.includes(clean)) tags.push(clean);
|
|
return " ";
|
|
});
|
|
|
|
// Extract @category
|
|
text = text.replace(/(?:^|\s)@([a-zA-Z0-9_-]+)/g, (_, cat) => {
|
|
const lower = cat.toLowerCase();
|
|
if (["project", "product", "app", "saas", "startup"].includes(lower)) {
|
|
detectedCategory = "project";
|
|
} else if (["creative", "art", "tshirt", "merch", "design", "writing"].includes(lower)) {
|
|
detectedCategory = "creative";
|
|
if (["tshirt", "merch"].includes(lower) && !tags.includes(lower)) {
|
|
tags.push(lower);
|
|
}
|
|
} else if (["research", "book", "learn", "inspiration"].includes(lower)) {
|
|
detectedCategory = "research";
|
|
} else if (["spark", "thought", "quick"].includes(lower)) {
|
|
detectedCategory = "spark";
|
|
}
|
|
return " ";
|
|
});
|
|
|
|
// Extract !high or !!
|
|
if (/(?:^|\s)!(?:high|hot|!)/i.test(text)) {
|
|
boostExcitement = true;
|
|
text = text.replace(/(?:^|\s)!(?:high|hot|!)/gi, " ");
|
|
}
|
|
|
|
const cleanTitle = text.replace(/\s+/g, " ").trim() || "Untitled Spark";
|
|
const category = detectedCategory || (VALID_CATEGORIES.includes(defaultCategory) ? defaultCategory : "spark");
|
|
|
|
return {
|
|
title: cleanTitle,
|
|
category,
|
|
tags,
|
|
boostExcitement
|
|
};
|
|
}
|
|
|
|
export function computeCompositeScore(row) {
|
|
const excitement = Number(row.rating_excitement ?? 3);
|
|
const impact = Number(row.rating_impact ?? 3);
|
|
const ease = Number(row.rating_ease ?? 3);
|
|
|
|
// Weighted base 0..100
|
|
const base = ((excitement * 0.42 + impact * 0.35 + ease * 0.23) / 5) * 100;
|
|
const arenaBonus = Math.min(15, Number(row.arena_wins || 0) * 3);
|
|
const whackBonus = Math.min(6, (row.whack_history?.length || 0) * 2);
|
|
const mockupBonus = (row.mockups?.length || 0) > 0 ? 4 : 0;
|
|
|
|
return Math.min(100, Math.round(base + arenaBonus + whackBonus + mockupBonus));
|
|
}
|
|
|
|
export function createDatabase(dbFilePath) {
|
|
fs.mkdirSync(path.dirname(dbFilePath), { recursive: true });
|
|
const db = new DatabaseSync(dbFilePath);
|
|
|
|
db.exec(`
|
|
PRAGMA journal_mode = WAL;
|
|
CREATE TABLE IF NOT EXISTS ideas (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
elevator_pitch TEXT DEFAULT '',
|
|
category TEXT DEFAULT 'spark',
|
|
status TEXT DEFAULT 'spark',
|
|
notes_markdown TEXT DEFAULT '',
|
|
tags_json TEXT DEFAULT '[]',
|
|
checklist_json TEXT DEFAULT '[]',
|
|
pros_json TEXT DEFAULT '[]',
|
|
cons_json TEXT DEFAULT '[]',
|
|
links_json TEXT DEFAULT '[]',
|
|
whack_history_json TEXT DEFAULT '[]',
|
|
inspired_by_card INTEGER DEFAULT NULL,
|
|
mockups_json TEXT DEFAULT '[]',
|
|
cover_mockup_url TEXT DEFAULT '',
|
|
rating_excitement INTEGER DEFAULT 3,
|
|
rating_impact INTEGER DEFAULT 3,
|
|
rating_ease INTEGER DEFAULT 3,
|
|
arena_wins INTEGER DEFAULT 0,
|
|
arena_matches INTEGER DEFAULT 0,
|
|
pinned INTEGER DEFAULT 0,
|
|
last_reviewed_at TEXT DEFAULT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
`);
|
|
|
|
// 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;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function hydrateIdea(row) {
|
|
if (!row) return null;
|
|
const tags = safeParse(row.tags_json, []);
|
|
const checklist = safeParse(row.checklist_json, []);
|
|
const pros = safeParse(row.pros_json, []);
|
|
const cons = safeParse(row.cons_json, []);
|
|
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 = {
|
|
id: row.id,
|
|
title: row.title,
|
|
elevator_pitch: row.elevator_pitch || "",
|
|
category: row.category || "spark",
|
|
status: row.status || "spark",
|
|
notes_markdown: row.notes_markdown || "",
|
|
tags,
|
|
checklist,
|
|
pros,
|
|
cons,
|
|
links,
|
|
whack_history,
|
|
inspired_by_card: row.inspired_by_card || null,
|
|
inspired_card_detail: inspiredCard,
|
|
mockups,
|
|
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),
|
|
arena_wins: Number(row.arena_wins ?? 0),
|
|
arena_matches: Number(row.arena_matches ?? 0),
|
|
pinned: Boolean(row.pinned),
|
|
last_reviewed_at: row.last_reviewed_at,
|
|
created_at: row.created_at,
|
|
updated_at: row.updated_at
|
|
};
|
|
hydrated.composite_score = computeCompositeScore(hydrated);
|
|
return hydrated;
|
|
}
|
|
|
|
function listIdeas({ status, category, tag, search, hasMockup, sort = "score" } = {}) {
|
|
const rows = db.prepare(`SELECT * FROM ideas`).all();
|
|
let items = rows.map(hydrateIdea);
|
|
|
|
if (status && status !== "all") {
|
|
items = items.filter((i) => i.status === status);
|
|
}
|
|
if (category && category !== "all") {
|
|
items = items.filter((i) => i.category === category);
|
|
}
|
|
if (tag && tag !== "all") {
|
|
items = items.filter((i) => i.tags.includes(tag.toLowerCase()));
|
|
}
|
|
if (hasMockup === "true" || hasMockup === true) {
|
|
items = items.filter((i) => i.mockups.length > 0 || Boolean(i.cover_mockup_url));
|
|
}
|
|
if (search && search.trim()) {
|
|
const q = search.trim().toLowerCase();
|
|
items = items.filter(
|
|
(i) =>
|
|
i.title.toLowerCase().includes(q) ||
|
|
i.elevator_pitch.toLowerCase().includes(q) ||
|
|
i.notes_markdown.toLowerCase().includes(q) ||
|
|
i.tags.some((t) => t.toLowerCase().includes(q))
|
|
);
|
|
}
|
|
|
|
items.sort((a, b) => {
|
|
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
|
if (sort === "newest") return b.created_at.localeCompare(a.created_at) || b.id - a.id;
|
|
if (sort === "updated") return b.updated_at.localeCompare(a.updated_at) || b.id - a.id;
|
|
if (sort === "arena") return b.arena_wins - a.arena_wins || b.composite_score - a.composite_score;
|
|
if (sort === "excitement") return b.rating_excitement - a.rating_excitement || b.composite_score - a.composite_score;
|
|
return b.composite_score - a.composite_score || b.updated_at.localeCompare(a.updated_at);
|
|
});
|
|
|
|
return items;
|
|
}
|
|
|
|
function getIdea(id) {
|
|
const row = db.prepare(`SELECT * FROM ideas WHERE id = ?`).get(Number(id));
|
|
return hydrateIdea(row);
|
|
}
|
|
|
|
function createIdea(payload = {}) {
|
|
const now = new Date().toISOString();
|
|
const parsed = parseQuickCaptureInput(payload.title || "Untitled Spark", payload.category);
|
|
|
|
const finalTitle = payload.preserveRawTitle ? String(payload.title).trim() : parsed.title;
|
|
const finalCategory = VALID_CATEGORIES.includes(payload.category) ? payload.category : parsed.category;
|
|
const finalStatus = VALID_STATUSES.includes(payload.status) ? payload.status : "spark";
|
|
|
|
const mergedTags = Array.from(
|
|
new Set([...(Array.isArray(payload.tags) ? payload.tags.map((t) => String(t).toLowerCase()) : []), ...parsed.tags])
|
|
);
|
|
|
|
const excitement = payload.rating_excitement ?? (parsed.boostExcitement ? 5 : 3);
|
|
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 ||
|
|
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, attachments_json, cover_mockup_url,
|
|
rating_excitement, rating_impact, rating_ease,
|
|
arena_wins, arena_matches, pinned, last_reviewed_at, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
const result = stmt.run(
|
|
finalTitle,
|
|
payload.elevator_pitch || "",
|
|
finalCategory,
|
|
finalStatus,
|
|
payload.notes_markdown || "",
|
|
JSON.stringify(mergedTags),
|
|
JSON.stringify(Array.isArray(payload.checklist) ? payload.checklist : []),
|
|
JSON.stringify(Array.isArray(payload.pros) ? payload.pros : []),
|
|
JSON.stringify(Array.isArray(payload.cons) ? payload.cons : []),
|
|
JSON.stringify(Array.isArray(payload.links) ? payload.links : []),
|
|
JSON.stringify(whackHistory),
|
|
payload.inspired_by_card ? Number(payload.inspired_by_card) : null,
|
|
JSON.stringify(mockups),
|
|
JSON.stringify(attachments),
|
|
coverMockupUrl,
|
|
Number(excitement),
|
|
Number(impact),
|
|
Number(ease),
|
|
Number(payload.arena_wins || 0),
|
|
Number(payload.arena_matches || 0),
|
|
payload.pinned ? 1 : 0,
|
|
payload.last_reviewed_at || null,
|
|
now,
|
|
now
|
|
);
|
|
|
|
return getIdea(result.lastInsertRowid);
|
|
}
|
|
|
|
function updateIdea(id, patch = {}) {
|
|
const existing = getIdea(id);
|
|
if (!existing) return null;
|
|
|
|
const now = new Date().toISOString();
|
|
const updated = {
|
|
title: patch.title !== undefined ? String(patch.title).trim() || existing.title : existing.title,
|
|
elevator_pitch: patch.elevator_pitch !== undefined ? String(patch.elevator_pitch) : existing.elevator_pitch,
|
|
category: patch.category !== undefined && VALID_CATEGORIES.includes(patch.category) ? patch.category : existing.category,
|
|
status: patch.status !== undefined && VALID_STATUSES.includes(patch.status) ? patch.status : existing.status,
|
|
notes_markdown: patch.notes_markdown !== undefined ? String(patch.notes_markdown) : existing.notes_markdown,
|
|
tags: Array.isArray(patch.tags) ? patch.tags.map((t) => String(t).toLowerCase().trim()).filter(Boolean) : existing.tags,
|
|
checklist: Array.isArray(patch.checklist) ? patch.checklist : existing.checklist,
|
|
pros: Array.isArray(patch.pros) ? patch.pros : existing.pros,
|
|
cons: Array.isArray(patch.cons) ? patch.cons : existing.cons,
|
|
links: Array.isArray(patch.links) ? patch.links : existing.links,
|
|
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,
|
|
rating_ease: patch.rating_ease !== undefined ? Math.max(1, Math.min(5, Number(patch.rating_ease))) : existing.rating_ease,
|
|
arena_wins: patch.arena_wins !== undefined ? Number(patch.arena_wins) : existing.arena_wins,
|
|
arena_matches: patch.arena_matches !== undefined ? Number(patch.arena_matches) : existing.arena_matches,
|
|
pinned: patch.pinned !== undefined ? (patch.pinned ? 1 : 0) : (existing.pinned ? 1 : 0),
|
|
last_reviewed_at: patch.last_reviewed_at !== undefined ? patch.last_reviewed_at : existing.last_reviewed_at,
|
|
updated_at: now
|
|
};
|
|
|
|
db.prepare(`
|
|
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 = ?, attachments_json = ?, cover_mockup_url = ?,
|
|
rating_excitement = ?, rating_impact = ?, rating_ease = ?,
|
|
arena_wins = ?, arena_matches = ?, pinned = ?, last_reviewed_at = ?, updated_at = ?
|
|
WHERE id = ?
|
|
`).run(
|
|
updated.title,
|
|
updated.elevator_pitch,
|
|
updated.category,
|
|
updated.status,
|
|
updated.notes_markdown,
|
|
JSON.stringify(updated.tags),
|
|
JSON.stringify(updated.checklist),
|
|
JSON.stringify(updated.pros),
|
|
JSON.stringify(updated.cons),
|
|
JSON.stringify(updated.links),
|
|
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,
|
|
updated.rating_ease,
|
|
updated.arena_wins,
|
|
updated.arena_matches,
|
|
updated.pinned,
|
|
updated.last_reviewed_at,
|
|
updated.updated_at,
|
|
Number(id)
|
|
);
|
|
|
|
return getIdea(id);
|
|
}
|
|
|
|
function deleteIdea(id) {
|
|
const info = db.prepare(`DELETE FROM ideas WHERE id = ?`).run(Number(id));
|
|
return info.changes > 0;
|
|
}
|
|
|
|
function logWhackOnIdea(id, { cardNumber, reflection, addChecklistItem = false, promoteStatus = null }) {
|
|
const idea = getIdea(id);
|
|
if (!idea) return null;
|
|
const card = getCardByNumber(cardNumber);
|
|
if (!card) throw new Error(`Invalid Whack Pack card number: ${cardNumber}`);
|
|
|
|
const entry = {
|
|
id: `whack-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
cardNumber: card.number,
|
|
cardTitle: card.title,
|
|
suit: card.suit,
|
|
suitLabel: card.suitLabel,
|
|
promptQuestion: card.promptQuestion,
|
|
reflection: String(reflection || "").trim(),
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
|
|
const nextWhackHistory = [entry, ...idea.whack_history];
|
|
const nextChecklist = [...idea.checklist];
|
|
if (addChecklistItem && entry.reflection) {
|
|
nextChecklist.push({
|
|
id: `chk-${Date.now()}`,
|
|
text: `[Card #${card.number} ${card.title}] ${entry.reflection}`,
|
|
done: false
|
|
});
|
|
}
|
|
|
|
const patch = {
|
|
whack_history: nextWhackHistory,
|
|
checklist: nextChecklist,
|
|
last_reviewed_at: new Date().toISOString()
|
|
};
|
|
if (promoteStatus && VALID_STATUSES.includes(promoteStatus)) {
|
|
patch.status = promoteStatus;
|
|
}
|
|
|
|
return updateIdea(id, patch);
|
|
}
|
|
|
|
function addMockupToIdea(id, mockupEntry) {
|
|
const idea = getIdea(id);
|
|
if (!idea) return null;
|
|
|
|
const entry = {
|
|
id: mockupEntry.id || `mk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
title: mockupEntry.title || `${idea.title} Mockup`,
|
|
template: mockupEntry.template || "tshirt-front",
|
|
garmentColor: mockupEntry.garmentColor || "#F5E8D3",
|
|
garmentColorName: mockupEntry.garmentColorName || "Vintage Cream",
|
|
placement: mockupEntry.placement || "center-chest",
|
|
prompt: mockupEntry.prompt || "",
|
|
editInstruction: mockupEntry.editInstruction || "",
|
|
parentMockupId: mockupEntry.parentMockupId || null,
|
|
modelUsed: mockupEntry.modelUsed || "canvas-compositor",
|
|
imageUrl: mockupEntry.imageUrl,
|
|
topText: mockupEntry.topText || "",
|
|
bottomText: mockupEntry.bottomText || "",
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
|
|
const nextMockups = [entry, ...idea.mockups];
|
|
return updateIdea(id, {
|
|
mockups: nextMockups,
|
|
cover_mockup_url: entry.imageUrl
|
|
});
|
|
}
|
|
|
|
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);
|
|
if (!winner || !loser) return null;
|
|
|
|
const now = new Date().toISOString();
|
|
const updatedWinner = updateIdea(winner.id, {
|
|
arena_wins: winner.arena_wins + 1,
|
|
arena_matches: winner.arena_matches + 1,
|
|
last_reviewed_at: now
|
|
});
|
|
const updatedLoser = updateIdea(loser.id, {
|
|
arena_matches: loser.arena_matches + 1,
|
|
last_reviewed_at: now
|
|
});
|
|
|
|
return { winner: updatedWinner, loser: updatedLoser };
|
|
}
|
|
|
|
function getSetting(key, defaultValue = "") {
|
|
const row = db.prepare(`SELECT value FROM settings WHERE key = ?`).get(String(key));
|
|
return row ? row.value : defaultValue;
|
|
}
|
|
|
|
function setSetting(key, value) {
|
|
db.prepare(`
|
|
INSERT INTO settings (key, value) VALUES (?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
`).run(String(key), String(value ?? ""));
|
|
}
|
|
|
|
function seedIfEmpty() {
|
|
const countRow = db.prepare(`SELECT COUNT(*) AS count FROM ideas`).get();
|
|
if (countRow && countRow.count > 0) return;
|
|
|
|
createIdea({
|
|
title: "High Altitude Espresso Club — Vintage Screenprint Tee",
|
|
elevator_pitch: "Heavyweight cream crewneck t-shirt with a 3-color retro mountain & espresso emblem (5-inch left chest + full back print).",
|
|
category: "creative",
|
|
status: "building",
|
|
rating_excitement: 5,
|
|
rating_impact: 4,
|
|
rating_ease: 5,
|
|
pinned: true,
|
|
tags: ["tshirt", "merch", "screenprint", "apparel"],
|
|
notes_markdown: "### Garment & Ink Specs\n- **Blank**: Heavyweight 6.5oz combed ring-spun tee in **Vintage Cream (`#F5E8D3`)**.\n- **Ink Palette**: Espresso Brown (`#42200E`), Burnt Orange (`#BC4921`), Warm Gold (`#EFA51C`).\n- **Placement**: Test both **Left-Chest 5-inch emblem** and **Full-Front club crest** in the Nano Banana Mockup Studio.",
|
|
checklist: [
|
|
{ id: "c1", text: "Generate initial 3-color emblem mockup in Nano Banana Studio", done: true },
|
|
{ id: "c2", text: "Compare Left-Chest 5in placement vs Full-Front on Cream tee", done: true },
|
|
{ id: "c3", text: "Test Forest Green & Vintage Black colorway variations", done: false },
|
|
{ id: "c4", text: "Export 300 DPI print-ready PNG proof", done: false }
|
|
],
|
|
pros: [
|
|
"3 spot colors keep screen setup affordable",
|
|
"Cream fabric works as the 4th highlight tone without white underbase"
|
|
],
|
|
cons: [
|
|
"Small tagline text needs minimum 10pt stroke for clean screen mesh"
|
|
],
|
|
inspired_by_card: 14,
|
|
whack_history: [
|
|
{
|
|
id: "whack-seed-1",
|
|
cardNumber: 14,
|
|
cardTitle: "Look to the Past",
|
|
suit: "explorer",
|
|
suitLabel: "Explorer",
|
|
promptQuestion: "What vintage craft, historical design, or classic wisdom can you revive or remix here?",
|
|
reflection: "Modeled the typography and badge border after 1970s Alpine mountaineering club patches and classic Italian espresso tin labels.",
|
|
createdAt: new Date(Date.now() - 86400000 * 2).toISOString()
|
|
}
|
|
],
|
|
mockups: [
|
|
{
|
|
id: "mk-seed-espresso",
|
|
title: "Vintage Cream Tee — 3-Color Mountain Espresso Crest",
|
|
template: "tshirt-front",
|
|
garmentColor: "#F5E8D3",
|
|
garmentColorName: "Vintage Cream",
|
|
placement: "center-chest",
|
|
prompt: "Heavyweight vintage cream crewneck t-shirt featuring a 3-color retro screenprint emblem for HIGH ALTITUDE ESPRESSO CLUB - EST. 1984",
|
|
modelUsed: "gemini-nano-banana-2.1",
|
|
imageUrl: "/mockups/seed-tshirt-espresso.jpg",
|
|
createdAt: new Date(Date.now() - 3600000 * 12).toISOString()
|
|
}
|
|
],
|
|
cover_mockup_url: "/mockups/seed-tshirt-espresso.jpg",
|
|
arena_wins: 3,
|
|
arena_matches: 3
|
|
});
|
|
|
|
createIdea({
|
|
title: "Take a Whack At It — Die-Cut Studio Sticker & Desk Totem",
|
|
elevator_pitch: "Die-cut vinyl laptop & sketchbook sticker inspired by Card #49 (Warrior suit) to nudge daily shipping habits.",
|
|
category: "creative",
|
|
status: "exploring",
|
|
rating_excitement: 4,
|
|
rating_impact: 4,
|
|
rating_ease: 5,
|
|
tags: ["sticker", "merch", "whack-pack", "illustration"],
|
|
notes_markdown: "Inspired by Roger von Oech's **Warrior Card #49: Take a Whack at It**.\n\nCould be printed as a 3-inch matte vinyl sticker and included as a physical reward whenever a project in the vault moves to **Shipped**.",
|
|
checklist: [
|
|
{ id: "c21", text: "Mock up die-cut sticker contour with bold typography", done: true },
|
|
{ id: "c22", text: "Try a t-shirt pocket variation in the Mockup Studio", done: false }
|
|
],
|
|
pros: ["Instant tactile reminder to stop overthinking and build"],
|
|
cons: ["Needs high-contrast border for dark laptops"],
|
|
inspired_by_card: 49,
|
|
whack_history: [
|
|
{
|
|
id: "whack-seed-2",
|
|
cardNumber: 49,
|
|
cardTitle: "Take a Whack at It",
|
|
suit: "warrior",
|
|
suitLabel: "Warrior",
|
|
promptQuestion: "How can you take a concrete whack at your idea in the next 30 minutes?",
|
|
reflection: "Turned the lightbulb-boxer metaphor directly into a collectible studio sticker.",
|
|
createdAt: new Date(Date.now() - 86400000).toISOString()
|
|
}
|
|
],
|
|
mockups: [
|
|
{
|
|
id: "mk-seed-sticker",
|
|
title: "Die-Cut Vinyl Sticker — Lightbulb Boxer",
|
|
template: "sticker",
|
|
garmentColor: "#1C1B1A",
|
|
garmentColorName: "Studio Slate",
|
|
placement: "die-cut",
|
|
prompt: "Die-cut vinyl sticker of a glowing lightbulb wearing red boxing gloves with text TAKE A WHACK AT IT",
|
|
modelUsed: "gemini-nano-banana-2.1",
|
|
imageUrl: "/mockups/seed-sticker-whack.jpg",
|
|
createdAt: new Date(Date.now() - 3600000 * 6).toISOString()
|
|
}
|
|
],
|
|
cover_mockup_url: "/mockups/seed-sticker-whack.jpg",
|
|
arena_wins: 2,
|
|
arena_matches: 3
|
|
});
|
|
|
|
createIdea({
|
|
title: "E-Ink Kitchen Recipe & Brew Telemetry Frame",
|
|
elevator_pitch: "Low-power wall-mounted e-paper display that shows active fermentation stats by day and switches to dinner recipe cards at 5pm.",
|
|
category: "project",
|
|
status: "exploring",
|
|
rating_excitement: 5,
|
|
rating_impact: 5,
|
|
rating_ease: 3,
|
|
tags: ["hardware", "e-ink", "homelab", "brewing"],
|
|
notes_markdown: "Combine a 7.5-inch tri-color E-Ink panel with ESP32.\n- Morning/Afternoon: Ambient dashboard (brew tilt hydrometer gravity + cellar temp).\n- Evening: Pulls tonight's selected recipe card with step-by-step ingredients.",
|
|
checklist: [
|
|
{ id: "c31", text: "Design 800x480 1-bit wireframe layout", done: false },
|
|
{ id: "c32", text: "Test rotary encoder for scrolling recipe steps with floury hands", done: false }
|
|
],
|
|
pros: ["Zero screen glare in the kitchen", "Battery lasts months"],
|
|
cons: ["Slow refresh rate when switching pages"],
|
|
inspired_by_card: 25,
|
|
arena_wins: 1,
|
|
arena_matches: 2
|
|
});
|
|
|
|
createIdea({
|
|
title: "National Park Night-Sky Astrophotography Hoodie Series",
|
|
elevator_pitch: "Minimalist star-chart back print paired with elevation coordinates on the left chest for Maroon Bells & Mount Sneffels.",
|
|
category: "creative",
|
|
status: "spark",
|
|
rating_excitement: 4,
|
|
rating_impact: 4,
|
|
rating_ease: 4,
|
|
tags: ["tshirt", "apparel", "colorado", "astronomy"],
|
|
notes_markdown: "Use glow-in-the-dark or metallic silver puff ink on a deep midnight navy garment (`#1B2A4A`). Great candidate to test in the **Nano Banana Mockup Studio**!"
|
|
});
|
|
|
|
createIdea({
|
|
title: "Acoustic Vinyl Listening Log & Liner Notes Archive",
|
|
elevator_pitch: "Scan album jackets on the turntable and pair them with personal listening memories and pressing notes.",
|
|
category: "spark",
|
|
status: "spark",
|
|
rating_excitement: 3,
|
|
rating_impact: 3,
|
|
rating_ease: 4,
|
|
tags: ["music", "archive", "weekend-project"],
|
|
notes_markdown: "Quick spark—could use a Whack Pack card in the **Idea Collider** to find a unique twist."
|
|
});
|
|
}
|
|
|
|
seedIfEmpty();
|
|
|
|
return {
|
|
db,
|
|
listIdeas,
|
|
getIdea,
|
|
createIdea,
|
|
updateIdea,
|
|
deleteIdea,
|
|
logWhackOnIdea,
|
|
addMockupToIdea,
|
|
addAttachmentToIdea,
|
|
recordArenaMatch,
|
|
getSetting,
|
|
setSetting
|
|
};
|
|
}
|