feat: v1 release - serving calculator, notes UI, tags, random recipe, confetti, favorites section, PWA icons, category icons, animations

This commit is contained in:
clawd
2026-02-18 10:23:22 +00:00
parent 60ca01fb94
commit de567f93db
17 changed files with 476 additions and 39 deletions

View File

@@ -1,6 +1,22 @@
import { getDb } from '../db/connection.js';
import { ulid } from 'ulid';
function syncTags(db: any, recipeId: string, tags: string[]) {
db.prepare('DELETE FROM recipe_tags WHERE recipe_id = ?').run(recipeId);
for (const tagName of tags) {
const trimmed = tagName.trim();
if (!trimmed) continue;
const slug = trimmed.toLowerCase().replace(/ä/g,'ae').replace(/ö/g,'oe').replace(/ü/g,'ue').replace(/ß/g,'ss').replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
let tag = db.prepare('SELECT id FROM tags WHERE slug = ?').get(slug) as any;
if (!tag) {
const tagId = ulid();
db.prepare('INSERT INTO tags (id, name, slug) VALUES (?, ?, ?)').run(tagId, trimmed, slug);
tag = { id: tagId };
}
db.prepare('INSERT OR IGNORE INTO recipe_tags (recipe_id, tag_id) VALUES (?, ?)').run(recipeId, tag.id);
}
}
function slugify(text: string): string {
return text
.toLowerCase()
@@ -34,6 +50,7 @@ export interface CreateRecipeInput {
source_url?: string;
ingredients?: { amount?: number; unit?: string; name: string; group_name?: string; sort_order?: number }[];
steps?: { step_number: number; instruction: string; duration_minutes?: number }[];
tags?: string[];
}
function mapTimeFields(row: any) {
@@ -127,6 +144,10 @@ export function createRecipe(input: CreateRecipeInput) {
insertStep.run(ulid(), id, step.step_number, step.instruction, step.duration_minutes || null);
}
}
if (input.tags && input.tags.length > 0) {
syncTags(db, id, input.tags);
}
});
transaction();
@@ -152,6 +173,30 @@ export function updateRecipe(id: string, input: Partial<CreateRecipeInput>) {
input.source_url ?? existing.source_url, id
);
// Replace ingredients if provided
if (input.ingredients) {
db.prepare('DELETE FROM ingredients WHERE recipe_id = ?').run(id);
for (let i = 0; i < input.ingredients.length; i++) {
const ing = input.ingredients[i];
db.prepare('INSERT INTO ingredients (id, recipe_id, amount, unit, name, group_name, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)')
.run(ulid(), id, ing.amount || null, ing.unit || null, ing.name, ing.group_name || null, ing.sort_order ?? i);
}
}
// Replace steps if provided
if (input.steps) {
db.prepare('DELETE FROM steps WHERE recipe_id = ?').run(id);
for (const step of input.steps) {
db.prepare('INSERT INTO steps (id, recipe_id, step_number, instruction, duration_minutes) VALUES (?, ?, ?, ?, ?)')
.run(ulid(), id, step.step_number, step.instruction, step.duration_minutes || null);
}
}
// Sync tags if provided
if (input.tags) {
syncTags(db, id, input.tags);
}
return getRecipeBySlug(slug);
}
@@ -169,6 +214,13 @@ export function toggleFavorite(id: string) {
return { id, is_favorite: newVal };
}
export function getRandomRecipe() {
const db = getDb();
const recipe = db.prepare('SELECT slug FROM recipes ORDER BY RANDOM() LIMIT 1').get() as any;
if (!recipe) return null;
return getRecipeBySlug(recipe.slug);
}
export function searchRecipes(query: string) {
const db = getDb();
// Add * for prefix matching