feat(frontend): add URL import error states
This commit is contained in:
parent
e1f5019006
commit
15ada9cb52
2
TODO.md
2
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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue