From 15ada9cb526db04b2c79e5cb9495508ad765ab29 Mon Sep 17 00:00:00 2001 From: Paul Huliganga Date: Tue, 24 Mar 2026 23:24:30 -0400 Subject: [PATCH] feat(frontend): add URL import error states --- TODO.md | 2 +- frontend/src/pages/ImportUrlPage.tsx | 58 ++++++++++++++++++++++++---- frontend/src/services/api.ts | 8 +++- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/TODO.md b/TODO.md index b48701c..537f102 100644 --- a/TODO.md +++ b/TODO.md @@ -34,7 +34,7 @@ MVP is functionally complete (core app + docs + tests). - [x] Add “Import from URL” UI page/form in frontend - [x] Show parsed preview (title, ingredients, steps, source URL) - [x] Allow edit-before-save flow, then save to existing create recipe API -- [ ] Add frontend error states (invalid URL, parse failure, timeout) +- [x] Add frontend error states (invalid URL, parse failure, timeout) ### Phase 3: Fallback Parsing + Hardening - [ ] Add heuristic fallback parser when Schema.org missing diff --git a/frontend/src/pages/ImportUrlPage.tsx b/frontend/src/pages/ImportUrlPage.tsx index 8c3c339..2fcc379 100644 --- a/frontend/src/pages/ImportUrlPage.tsx +++ b/frontend/src/pages/ImportUrlPage.tsx @@ -4,6 +4,8 @@ import { Link, useNavigate } from 'react-router-dom'; import { createRecipe, importRecipeFromUrl } from '../services/api'; import type { RecipeDraft, UrlImportResult } from '../types/recipe'; +type ImportErrorType = 'invalid-url' | 'parse-failure' | 'timeout' | 'generic'; + function toTextBlock(items: string[]): string { return items.join('\n'); } @@ -15,12 +17,36 @@ function toList(text: string): string[] { .filter((line) => line.length > 0); } +function getImportErrorDetails(message: string): { type: ImportErrorType; message: string } { + const normalized = message.toLowerCase(); + + if (normalized.includes('valid url')) { + return { + type: 'invalid-url', + message: 'Please enter a valid URL (including https://).', + }; + } + + if (normalized.includes('timed out')) { + return { + type: 'timeout', + message: 'The import request timed out. Please try again in a moment.', + }; + } + + return { + type: 'generic', + message, + }; +} + export function ImportUrlPage() { const navigate = useNavigate(); const [url, setUrl] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [errorType, setErrorType] = useState(null); const [result, setResult] = useState(null); const [draft, setDraft] = useState(null); @@ -31,6 +57,7 @@ export function ImportUrlPage() { event.preventDefault(); setLoading(true); setError(null); + setErrorType(null); setResult(null); setDraft(null); setDraftError(null); @@ -39,9 +66,16 @@ export function ImportUrlPage() { const imported = await importRecipeFromUrl(url); setResult(imported); setDraft(imported.draft_recipe); + + if (!imported.draft_recipe) { + setErrorType('parse-failure'); + setError('We could fetch this page, but could not find recipe fields to import.'); + } } catch (err) { const message = err instanceof Error ? err.message : 'Failed to import recipe URL'; - setError(message); + const details = getImportErrorDetails(message); + setErrorType(details.type); + setError(details.message); } finally { setLoading(false); } @@ -122,9 +156,21 @@ export function ImportUrlPage() { {error && ( -
-

- Error: {error} +

+

+ + {errorType === 'invalid-url' && 'Invalid URL:'} + {errorType === 'timeout' && 'Import timed out:'} + {errorType === 'parse-failure' && 'Parse failed:'} + {errorType === 'generic' && 'Error:'} + {' '} + {error}

)} @@ -139,9 +185,7 @@ export function ImportUrlPage() { {draft ? (
-

- Review and edit before saving. -

+

Review and edit before saving.

{draftError && (
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index f0ace3e..0709cde 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -253,11 +253,15 @@ export async function importRecipeFromUrl(url: string): Promise body: JSON.stringify({ url }), }); + const result: ApiResponse = await response.json(); + if (!response.ok) { - throw new Error(`Failed to import URL: ${response.statusText}`); + const errorMessage = typeof result.error === 'string' + ? result.error + : JSON.stringify(result.error ?? 'Failed to import URL'); + throw new Error(errorMessage); } - const result: ApiResponse = await response.json(); if (!result.success || !result.data) { throw new Error(result.error || 'Failed to import URL'); }