- New backend service: og-scraper.service.ts (extracts og:image, og:title, og:description) - Pinterest support via Twitterbot UA (gets original resolution from i.pinimg.com) - Works with Chefkoch, Allrecipes, blogs, any site with og:image meta tags - GET /api/og-preview?url= for preview - POST /api/recipes/:id/fetch-image to download + process with sharp - Frontend: 'Bild holen' button appears when source URL is filled - Auto-fills title & description from OG data if empty - Images processed to WebP, max 1200px wide
92 lines
2.7 KiB
TypeScript
92 lines
2.7 KiB
TypeScript
import { apiFetch } from './client'
|
|
import type { Recipe, PaginatedResponse } from './types'
|
|
|
|
interface RecipeListParams {
|
|
category?: string
|
|
favorite?: boolean
|
|
sort?: string
|
|
page?: number
|
|
limit?: number
|
|
}
|
|
|
|
export function fetchRecipes(params?: RecipeListParams) {
|
|
const sp = new URLSearchParams()
|
|
if (params?.category) sp.set('category', params.category)
|
|
if (params?.favorite) sp.set('favorite', 'true')
|
|
if (params?.sort) sp.set('sort', params.sort)
|
|
if (params?.page) sp.set('page', String(params.page))
|
|
if (params?.limit) sp.set('limit', String(params.limit))
|
|
const qs = sp.toString()
|
|
return apiFetch<PaginatedResponse<Recipe>>(`/recipes${qs ? `?${qs}` : ''}`)
|
|
}
|
|
|
|
export function fetchRecipe(slug: string) {
|
|
return apiFetch<Recipe>(`/recipes/${slug}`)
|
|
}
|
|
|
|
export function searchRecipes(q: string) {
|
|
return apiFetch<PaginatedResponse<Recipe>>(`/recipes/search?q=${encodeURIComponent(q)}`)
|
|
}
|
|
|
|
export function toggleFavorite(id: string) {
|
|
return apiFetch<Recipe>(`/recipes/${id}/favorite`, { method: 'PATCH' })
|
|
}
|
|
|
|
export interface RecipeFormData {
|
|
title: string
|
|
description?: string
|
|
category_id?: string
|
|
difficulty?: 'easy' | 'medium' | 'hard'
|
|
prep_time?: number
|
|
cook_time?: number
|
|
servings?: number
|
|
image_url?: string
|
|
source_url?: string
|
|
ingredients?: { amount?: number; unit?: string; name: string; group_name?: string; sort_order?: number }[]
|
|
steps?: { step_number: number; instruction: string; duration_minutes?: number }[]
|
|
}
|
|
|
|
export function createRecipe(data: RecipeFormData) {
|
|
return apiFetch<Recipe>('/recipes', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
})
|
|
}
|
|
|
|
export function updateRecipe(id: string, data: RecipeFormData) {
|
|
return apiFetch<Recipe>(`/recipes/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
})
|
|
}
|
|
|
|
export function deleteRecipe(id: string) {
|
|
return apiFetch<{ ok: boolean }>(`/recipes/${id}`, { method: 'DELETE' })
|
|
}
|
|
|
|
export function uploadRecipeImage(id: string, file: File) {
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
return fetch(`/api/recipes/${id}/image`, { method: 'POST', body: formData }).then(r => {
|
|
if (!r.ok) throw new Error('Upload failed')
|
|
return r.json() as Promise<{ image_url: string }>
|
|
})
|
|
}
|
|
|
|
export interface OgData {
|
|
image?: string
|
|
title?: string
|
|
description?: string
|
|
}
|
|
|
|
export function fetchOgPreview(url: string) {
|
|
return apiFetch<OgData>(`/og-preview?url=${encodeURIComponent(url)}`)
|
|
}
|
|
|
|
export function fetchImageFromUrl(recipeId: string, url: string) {
|
|
return apiFetch<{ ok: boolean; image_url: string; og_title?: string; og_description?: string }>(
|
|
`/recipes/${recipeId}/fetch-image`,
|
|
{ method: 'POST', body: JSON.stringify({ url }) }
|
|
)
|
|
}
|