recipe-manager/frontend/src/services/api.ts

271 lines
6.9 KiB
TypeScript

/**
* 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<Recipe[]> {
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<Recipe[]> = 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<Recipe> {
const response = await fetch(`${API_BASE_URL}/recipes/${id}`);
if (!response.ok) {
throw new Error(`Failed to fetch recipe: ${response.statusText}`);
}
const result: ApiResponse<Recipe> = 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<Recipe> {
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<Recipe> = 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<Omit<Recipe, 'id' | 'created_at' | 'updated_at'>>): Promise<Recipe> {
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<Recipe> = 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<void> {
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<Tag[]> {
const response = await fetch(`${API_BASE_URL}/tags`);
if (!response.ok) {
throw new Error(`Failed to fetch tags: ${response.statusText}`);
}
const result: ApiResponse<Tag[]> = 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<Tag, 'id'>): Promise<Tag> {
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<Tag> = 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<Tag[]> {
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<Tag[]> = 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<void> {
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<void> {
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<void> {
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<UrlImportResult> {
const response = await fetch(`${API_BASE_URL}/import/url`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url }),
});
const result: ApiResponse<UrlImportResult> = 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;
}