/** * API client for Recipe Manager backend */ import type { Recipe, RecipeDraft, Tag, ApiResponse, UrlImportResult } from '../types/recipe'; // Use relative URL - nginx will proxy to backend in production // For local development (npm run dev), configure vite.config.ts proxy const API_BASE_URL = '/api'; /** * Fetch recipes with optional filters */ export async function fetchRecipes(params?: { search?: string; offset?: number; limit?: number; }): Promise { const url = new URL(`${API_BASE_URL}/recipes`, window.location.origin); if (params?.search) { url.searchParams.set('search', params.search); } if (params?.offset !== undefined) { url.searchParams.set('offset', params.offset.toString()); } if (params?.limit !== undefined) { url.searchParams.set('limit', params.limit.toString()); } const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`Failed to fetch recipes: ${response.statusText}`); } const result: ApiResponse = await response.json(); if (!result.success || !result.data) { throw new Error(result.error || 'Failed to fetch recipes'); } return result.data; } /** * Fetch a single recipe by ID */ export async function fetchRecipe(id: number): Promise { const response = await fetch(`${API_BASE_URL}/recipes/${id}`); if (!response.ok) { throw new Error(`Failed to fetch recipe: ${response.statusText}`); } const result: ApiResponse = await response.json(); if (!result.success || !result.data) { throw new Error(result.error || 'Failed to fetch recipe'); } return result.data; } /** * Create a new recipe */ export async function createRecipe(recipe: RecipeDraft): Promise { const response = await fetch(`${API_BASE_URL}/recipes`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(recipe), }); if (!response.ok) { throw new Error(`Failed to create recipe: ${response.statusText}`); } const result: ApiResponse = await response.json(); if (!result.success || !result.data) { throw new Error(result.error || 'Failed to create recipe'); } return result.data; } /** * Update a recipe */ export async function updateRecipe(id: number, updates: Partial>): Promise { const response = await fetch(`${API_BASE_URL}/recipes/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(updates), }); if (!response.ok) { throw new Error(`Failed to update recipe: ${response.statusText}`); } const result: ApiResponse = await response.json(); if (!result.success || !result.data) { throw new Error(result.error || 'Failed to update recipe'); } return result.data; } /** * Delete a recipe */ export async function deleteRecipe(id: number): Promise { const response = await fetch(`${API_BASE_URL}/recipes/${id}`, { method: 'DELETE', }); if (!response.ok) { throw new Error(`Failed to delete recipe: ${response.statusText}`); } const result: ApiResponse<{ deleted: number }> = await response.json(); if (!result.success) { throw new Error(result.error || 'Failed to delete recipe'); } } /** * Fetch all tags */ export async function fetchTags(): Promise { const response = await fetch(`${API_BASE_URL}/tags`); if (!response.ok) { throw new Error(`Failed to fetch tags: ${response.statusText}`); } const result: ApiResponse = await response.json(); if (!result.success || !result.data) { throw new Error(result.error || 'Failed to fetch tags'); } return result.data; } /** * Create a new tag */ export async function createTag(tag: Omit): Promise { const response = await fetch(`${API_BASE_URL}/tags`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(tag), }); if (!response.ok) { throw new Error(`Failed to create tag: ${response.statusText}`); } const result: ApiResponse = await response.json(); if (!result.success || !result.data) { throw new Error(result.error || 'Failed to create tag'); } return result.data; } /** * Fetch tags for a specific recipe */ export async function fetchRecipeTags(recipeId: number): Promise { const response = await fetch(`${API_BASE_URL}/tags/recipes/${recipeId}/tags`); if (!response.ok) { throw new Error(`Failed to fetch recipe tags: ${response.statusText}`); } const result: ApiResponse = await response.json(); if (!result.success || !result.data) { throw new Error(result.error || 'Failed to fetch recipe tags'); } return result.data; } /** * Assign a tag to a recipe */ export async function assignTagToRecipe(recipeId: number, tagId: number): Promise { const response = await fetch(`${API_BASE_URL}/tags/recipes/${recipeId}/tags`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ tag_id: tagId }), }); if (!response.ok) { throw new Error(`Failed to assign tag: ${response.statusText}`); } const result: ApiResponse<{ assigned: boolean }> = await response.json(); if (!result.success) { throw new Error(result.error || 'Failed to assign tag'); } } /** * Remove a tag from a recipe */ export async function removeTagFromRecipe(recipeId: number, tagId: number): Promise { const response = await fetch(`${API_BASE_URL}/tags/recipes/${recipeId}/tags/${tagId}`, { method: 'DELETE', }); if (!response.ok) { throw new Error(`Failed to remove tag: ${response.statusText}`); } const result: ApiResponse<{ removed: boolean }> = await response.json(); if (!result.success) { throw new Error(result.error || 'Failed to remove tag'); } } /** * Delete a tag */ export async function deleteTag(id: number): Promise { const response = await fetch(`${API_BASE_URL}/tags/${id}`, { method: 'DELETE', }); if (!response.ok) { throw new Error(`Failed to delete tag: ${response.statusText}`); } const result: ApiResponse<{ id: number }> = await response.json(); if (!result.success) { throw new Error(result.error || 'Failed to delete tag'); } } /** * Import recipe data from URL */ export async function importRecipeFromUrl(url: string): Promise { const response = await fetch(`${API_BASE_URL}/import/url`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url }), }); const result: ApiResponse = await response.json(); if (!response.ok) { const errorMessage = typeof result.error === 'string' ? result.error : JSON.stringify(result.error ?? 'Failed to import URL'); throw new Error(errorMessage); } if (!result.success || !result.data) { throw new Error(result.error || 'Failed to import URL'); } return result.data; }