feat: v1 release - serving calculator, notes UI, tags, random recipe, confetti, favorites section, PWA icons, category icons, animations
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user