271 lines
6.9 KiB
TypeScript
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;
|
|
}
|