From e1f5019006d4a6180cc2123d3458530a5e848e1a Mon Sep 17 00:00:00 2001 From: Paul Huliganga Date: Tue, 24 Mar 2026 23:20:42 -0400 Subject: [PATCH] feat(frontend): add edit-before-save flow for URL imports --- TODO.md | 2 +- frontend/src/pages/ImportUrlPage.tsx | 161 ++++++++++++++++++++++++--- frontend/src/services/api.ts | 4 +- frontend/src/types/recipe.ts | 17 ++- 4 files changed, 162 insertions(+), 22 deletions(-) diff --git a/TODO.md b/TODO.md index df73cb7..b48701c 100644 --- a/TODO.md +++ b/TODO.md @@ -33,7 +33,7 @@ MVP is functionally complete (core app + docs + tests). ### Phase 2: Import UI - [x] Add “Import from URL” UI page/form in frontend - [x] Show parsed preview (title, ingredients, steps, source URL) -- [ ] Allow edit-before-save flow, then save to existing create recipe API +- [x] Allow edit-before-save flow, then save to existing create recipe API - [ ] Add frontend error states (invalid URL, parse failure, timeout) ### Phase 3: Fallback Parsing + Hardening diff --git a/frontend/src/pages/ImportUrlPage.tsx b/frontend/src/pages/ImportUrlPage.tsx index 64e9aea..8c3c339 100644 --- a/frontend/src/pages/ImportUrlPage.tsx +++ b/frontend/src/pages/ImportUrlPage.tsx @@ -1,23 +1,44 @@ import { useState } from 'react'; import type { FormEvent } from 'react'; -import { importRecipeFromUrl } from '../services/api'; -import type { UrlImportResult } from '../types/recipe'; +import { Link, useNavigate } from 'react-router-dom'; +import { createRecipe, importRecipeFromUrl } from '../services/api'; +import type { RecipeDraft, UrlImportResult } from '../types/recipe'; + +function toTextBlock(items: string[]): string { + return items.join('\n'); +} + +function toList(text: string): string[] { + return text + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0); +} export function ImportUrlPage() { + const navigate = useNavigate(); + const [url, setUrl] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState(null); + const [draft, setDraft] = useState(null); + const [draftError, setDraftError] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const handleSubmit = async (event: FormEvent) => { event.preventDefault(); setLoading(true); setError(null); setResult(null); + setDraft(null); + setDraftError(null); try { const imported = await importRecipeFromUrl(url); setResult(imported); + setDraft(imported.draft_recipe); } catch (err) { const message = err instanceof Error ? err.message : 'Failed to import recipe URL'; setError(message); @@ -26,6 +47,48 @@ export function ImportUrlPage() { } }; + const handleSave = async (event: FormEvent) => { + event.preventDefault(); + if (!draft) { + setDraftError('No draft recipe to save.'); + return; + } + + const title = draft.title.trim(); + const ingredients = draft.ingredients.map((item) => item.trim()).filter(Boolean); + const instructions = draft.instructions.map((item) => item.trim()).filter(Boolean); + + if (!title) { + setDraftError('Title is required.'); + return; + } + if (ingredients.length === 0) { + setDraftError('At least one ingredient is required.'); + return; + } + if (instructions.length === 0) { + setDraftError('At least one instruction step is required.'); + return; + } + + setIsSaving(true); + setDraftError(null); + + try { + const created = await createRecipe({ + ...draft, + title, + ingredients, + instructions, + }); + navigate(`/recipe/${created.id}`); + } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to save recipe'; + setDraftError(message); + setIsSaving(false); + } + }; + return (

Import from URL

@@ -74,30 +137,92 @@ export function ImportUrlPage() {

JSON-LD blocks found: {result.json_ld_blocks.length}

- {result.draft_recipe ? ( - <> + {draft ? ( +
+

+ Review and edit before saving. +

+ + {draftError && ( +
+ {draftError} +
+ )} +
-

{result.draft_recipe.title}

+ + setDraft({ ...draft, title: event.target.value })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg" + />
-
Ingredients
-
    - {result.draft_recipe.ingredients.map((ingredient, index) => ( -
  • {ingredient}
  • - ))} -
+ +