feat(frontend): add URL import error states

This commit is contained in:
Paul Huliganga 2026-03-24 23:24:30 -04:00
parent e1f5019006
commit 15ada9cb52
3 changed files with 58 additions and 10 deletions

View File

@ -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

View File

@ -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<string | null>(null);
const [errorType, setErrorType] = useState<ImportErrorType | null>(null);
const [result, setResult] = useState<UrlImportResult | null>(null);
const [draft, setDraft] = useState<RecipeDraft | null>(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() {
</form>
{error && (
<div className="mt-4 bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-800">
<strong>Error:</strong> {error}
<div
className={`mt-4 border rounded-lg p-4 ${
errorType === 'parse-failure'
? 'bg-amber-50 border-amber-200'
: 'bg-red-50 border-red-200'
}`}
>
<p className={errorType === 'parse-failure' ? 'text-amber-800' : 'text-red-800'}>
<strong>
{errorType === 'invalid-url' && 'Invalid URL:'}
{errorType === 'timeout' && 'Import timed out:'}
{errorType === 'parse-failure' && 'Parse failed:'}
{errorType === 'generic' && 'Error:'}
</strong>{' '}
{error}
</p>
</div>
)}
@ -139,9 +185,7 @@ export function ImportUrlPage() {
{draft ? (
<form onSubmit={handleSave} className="space-y-4">
<p className="text-sm text-gray-600">
Review and edit before saving.
</p>
<p className="text-sm text-gray-600">Review and edit before saving.</p>
{draftError && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 text-red-800 text-sm">

View File

@ -253,11 +253,15 @@ export async function importRecipeFromUrl(url: string): Promise<UrlImportResult>
body: JSON.stringify({ url }),
});
const result: ApiResponse<UrlImportResult> = 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<UrlImportResult> = await response.json();
if (!result.success || !result.data) {
throw new Error(result.error || 'Failed to import URL');
}