From 0dc3264a22a02b9c53266e83f2afa723b0938722 Mon Sep 17 00:00:00 2001 From: araemer Date: Sat, 21 Feb 2026 07:44:26 +0100 Subject: [PATCH 1/2] Renaming of rest resources --- frontend/src/api/dtos/RecipeDto.ts | 9 +- frontend/src/api/dtos/TagDto.ts | 8 + frontend/src/api/endpoints/TagRestResource.ts | 27 +++ .../src/components/recipes/RecipeEditPage.tsx | 2 +- .../src/components/recipes/RecipeEditor.tsx | 62 +++---- .../src/components/recipes/RecipeListPage.tsx | 2 +- frontend/src/components/tags/TagChip.tsx | 26 +++ .../src/components/tags/TagListEditor.tsx | 161 ++++++++++++++++++ frontend/src/mappers/RecipeMapper.ts | 42 +++-- frontend/src/mappers/TagMapper.ts | 22 +++ frontend/src/models/RecipeModel.ts | 37 ++-- frontend/src/models/TagModel.ts | 9 + 12 files changed, 328 insertions(+), 79 deletions(-) create mode 100644 frontend/src/api/dtos/TagDto.ts create mode 100644 frontend/src/api/endpoints/TagRestResource.ts create mode 100644 frontend/src/components/tags/TagChip.tsx create mode 100644 frontend/src/components/tags/TagListEditor.tsx create mode 100644 frontend/src/mappers/TagMapper.ts create mode 100644 frontend/src/models/TagModel.ts diff --git a/frontend/src/api/dtos/RecipeDto.ts b/frontend/src/api/dtos/RecipeDto.ts index bc5bedc..dbb9085 100644 --- a/frontend/src/api/dtos/RecipeDto.ts +++ b/frontend/src/api/dtos/RecipeDto.ts @@ -1,7 +1,8 @@ +import {AbstractDto} from "./AbstractDto.ts"; +import {RecipeIngredientGroupDto} from "./RecipeIngredientGroupDto.js"; +import {RecipeInstructionStepDto} from "./RecipeInstructionStepDto.js"; +import type {TagDto} from "./TagDto.ts"; -import { AbstractDto } from "./AbstractDto.ts"; -import { RecipeIngredientGroupDto } from "./RecipeIngredientGroupDto.js"; -import { RecipeInstructionStepDto } from "./RecipeInstructionStepDto.js"; /** * DTO describing a recipe */ @@ -12,4 +13,6 @@ export class RecipeDto extends AbstractDto { amountDescription?: string; instructions!: RecipeInstructionStepDto[]; ingredientGroups!: RecipeIngredientGroupDto[]; + /** Tags associated with this recipe */ + tagList?: TagDto[]; } \ No newline at end of file diff --git a/frontend/src/api/dtos/TagDto.ts b/frontend/src/api/dtos/TagDto.ts new file mode 100644 index 0000000..b20ba40 --- /dev/null +++ b/frontend/src/api/dtos/TagDto.ts @@ -0,0 +1,8 @@ +import {AbstractDto} from "./AbstractDto.ts"; + +/** + * DTO describing a tag + */ +export class TagDto extends AbstractDto { + description!: string; +} \ No newline at end of file diff --git a/frontend/src/api/endpoints/TagRestResource.ts b/frontend/src/api/endpoints/TagRestResource.ts new file mode 100644 index 0000000..6660368 --- /dev/null +++ b/frontend/src/api/endpoints/TagRestResource.ts @@ -0,0 +1,27 @@ +import {apiClient} from "../apiClient" +import type {TagDto} from "../dtos/TagDto" + +const TAG_ENDPOINT = "tags" + +/** + * Fetches all existing tags from the backend. + */ +export async function fetchAllTags(): Promise { + return apiClient.get(TAG_ENDPOINT) +} + +/** + * Creates a new tag or updates an existing one. + * @param tag The tag to create or update + */ +export async function createOrUpdateTag(tag: TagDto): Promise { + return apiClient.post(`${TAG_ENDPOINT}/create-or-update`, tag) +} + +/** + * Deletes a tag by its ID. + * @param id The ID of the tag to delete + */ +export async function deleteTag(id: string): Promise { + return apiClient.delete(`${TAG_ENDPOINT}/${id}`) +} \ No newline at end of file diff --git a/frontend/src/components/recipes/RecipeEditPage.tsx b/frontend/src/components/recipes/RecipeEditPage.tsx index 6a9a5d8..f6fa145 100644 --- a/frontend/src/components/recipes/RecipeEditPage.tsx +++ b/frontend/src/components/recipes/RecipeEditPage.tsx @@ -2,7 +2,7 @@ import {useNavigate, useParams} from "react-router-dom" import {useEffect, useState} from "react" import type {RecipeModel} from "../../models/RecipeModel" import {RecipeEditor} from "./RecipeEditor" -import {createOrUpdateRecipe, fetchRecipe} from "../../api/points/RecipePoint" +import {createOrUpdateRecipe, fetchRecipe} from "../../api/endpoints/RecipeRestResource.ts" import {getRecipeDetailUrl, getRecipeListUrl} from "../../routes" import {mapRecipeDtoToModel, mapRecipeModelToDto} from "../../mappers/RecipeMapper" import type {RecipeDto} from "../../api/dtos/RecipeDto" diff --git a/frontend/src/components/recipes/RecipeEditor.tsx b/frontend/src/components/recipes/RecipeEditor.tsx index 154bf6c..bcb29ac 100644 --- a/frontend/src/components/recipes/RecipeEditor.tsx +++ b/frontend/src/components/recipes/RecipeEditor.tsx @@ -5,11 +5,13 @@ import {IngredientGroupListEditor} from "./IngredientGroupListEditor" import Button from "../basics/Button" import {InstructionStepListEditor} from "./InstructionStepListEditor" import type {InstructionStepModel} from "../../models/InstructionStepModel" +import type {TagModel} from "../../models/TagModel" +import {TagListEditor} from "../tags/TagListEditor" import {ButtonType} from "../basics/BasicButtonDefinitions" -import ButtonGroupLayout from "../basics/ButtonGroupLayout.tsx"; -import ContentBody from "../basics/ContentBody.tsx"; -import PageContainer from "../basics/PageContainer.tsx"; -import PageContentLayout from "../basics/PageContentLayout.tsx"; +import ButtonGroupLayout from "../basics/ButtonGroupLayout.tsx" +import ContentBody from "../basics/ContentBody.tsx" +import PageContainer from "../basics/PageContainer.tsx" +import PageContentLayout from "../basics/PageContentLayout.tsx" type RecipeEditorProps = { recipe: RecipeModel @@ -23,46 +25,30 @@ export function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) { /** Error list */ const [errors, setErrors] = useState<{ title?: boolean; ingredients?: boolean }>({}) - /** - * Update ingredients - * @param ingredientGroupList updated ingredient groups and ingredients - */ const updateIngredientGroupList = (ingredientGroupList: IngredientGroupModel[]) => { setDraft({...draft, ingredientGroupList}) } - /** - * Update instruction steps - * @param instructionStepList updated instructions - */ const updateInstructionList = (instructionStepList: InstructionStepModel[]) => { setDraft({...draft, instructionStepList}) } - /** - * Validate recipe - * @returns Information on the errors the validation encountered - */ + const updateTagList = (tagList: TagModel[]) => { + setDraft({...draft, tagList}) + } + const validate = () => { const newErrors: { title?: boolean; ingredients?: boolean } = {} - // each recipe requires a title if (!draft.title.trim()) { newErrors.title = true } - /* there must be at least one ingredient group - * no group may contain an empty ingredient list - * @todo check whether all ingredients are valid - * @todo enhance visualization of ingredient errors - */ if (!draft.ingredientGroupList || draft.ingredientGroupList.length === 0) { newErrors.ingredients = true } else { const isAnyIngredientListEmpty = draft.ingredientGroupList.some( - ingGrp => { - return !ingGrp.ingredientList || ingGrp.ingredientList.length === 0 - } + ingGrp => !ingGrp.ingredientList || ingGrp.ingredientList.length === 0 ) if (isAnyIngredientListEmpty) { newErrors.ingredients = true @@ -70,22 +56,19 @@ export function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) { } setErrors(newErrors) - return Object.keys(newErrors).length === 0 } - /** Handles saving and ensures that the draft is only saved if valid */ + const handleSave = (draft: RecipeModel) => { if (validate()) { onSave(draft) } } - // ensure that there is a recipe and show error otherwise + if (!recipe) return
Oops, there's no recipe in RecipeEditor...
- // @todo add handling of images + return ( - /*Container spanning entire screen used to center content horizontally */ - {/* Container defining the maximum width of the content */}

{recipe.id ? "Rezept bearbeiten" : "Neues Rezept"} @@ -99,6 +82,7 @@ export function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) { value={draft.title} onChange={e => setDraft({...draft, title: e.target.value})} /> + {/* Servings */}

Portionen

@@ -124,7 +108,15 @@ export function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) { }} />
- {/* Ingredient List - @todo better visualization of errors! */} + + {/* Tags */} +

Tags

+ + + {/* Ingredient List */}
- {/* Instruction List*/} + {/* Instruction List */} - - {/* Save Button */} + + ) +} \ No newline at end of file diff --git a/frontend/src/components/tags/TagListEditor.tsx b/frontend/src/components/tags/TagListEditor.tsx new file mode 100644 index 0000000..ef57aee --- /dev/null +++ b/frontend/src/components/tags/TagListEditor.tsx @@ -0,0 +1,161 @@ +import {useEffect, useRef, useState} from "react" +import type {TagModel} from "../../models/TagModel" +import {TagChip} from "./TagChip" +import {createOrUpdateTag, fetchAllTags} from "../../api/endpoints/TagRestResource.ts" + +type TagListEditorProps = { + /** The tags currently assigned to the recipe */ + tagList: TagModel[] + /** Called whenever the tag list changes */ + onChange: (tagList: TagModel[]) => void +} + +/** + * Tag editor inspired by Jira's label picker. + * + * - Shows existing tags as chips with a × button to remove them. + * - An input field lets the user search through all available tags. + * - Matching tags are shown in a dropdown; the user can pick one or + * confirm the typed text as a brand-new tag with "Create ''" option. + */ +export function TagListEditor({tagList, onChange}: TagListEditorProps) { + const [inputValue, setInputValue] = useState("") + const [availableTags, setAvailableTags] = useState([]) + const [isDropdownOpen, setIsDropdownOpen] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const containerRef = useRef(null) + + // Load all available tags once on mount + useEffect(() => { + setIsLoading(true) + fetchAllTags() + .then(dtos => + setAvailableTags(dtos.map(dto => ({id: dto.id, description: dto.description}))) + ) + .catch(console.error) + .finally(() => setIsLoading(false)) + }, []) + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setIsDropdownOpen(false) + setInputValue("") + } + } + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + }, []) + + const trimmed = inputValue.trim().toLowerCase() + + /** Tags matching the current input that are not already selected */ + const filteredSuggestions = availableTags.filter( + tag => + tag.description.toLowerCase().includes(trimmed) && + !tagList.some(t => t.id === tag.id || t.description.toLowerCase() === tag.description.toLowerCase()) + ) + + /** Whether the typed text is a genuinely new tag (not in available list) */ + const isNewTag = + trimmed.length > 0 && + !availableTags.some(t => t.description.toLowerCase() === trimmed) + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value) + setIsDropdownOpen(true) + } + + const handleInputFocus = () => { + setIsDropdownOpen(true) + } + + const selectTag = (tag: TagModel) => { + onChange([...tagList, tag]) + setInputValue("") + setIsDropdownOpen(false) + } + + const createNewTag = async () => { + if (!trimmed) return + try { + // Persist to backend; backend returns the tag with its new ID + const created = await createOrUpdateTag({id: undefined as unknown as string, description: trimmed}) + const newTag: TagModel = {id: created.id, description: created.description} + // Add to local available list so it shows up in future searches + setAvailableTags(prev => [...prev, newTag]) + selectTag(newTag) + } catch (e) { + console.error("Failed to create tag", e) + } + } + + const removeTag = (tagToRemove: TagModel) => { + onChange(tagList.filter(t => t !== tagToRemove)) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault() + if (filteredSuggestions.length > 0 && !isNewTag) { + selectTag(filteredSuggestions[0]) + } else if (isNewTag) { + createNewTag() + } + } else if (e.key === "Escape") { + setIsDropdownOpen(false) + setInputValue("") + } + } + + const showDropdown = isDropdownOpen && (filteredSuggestions.length > 0 || isNewTag) + + return ( +
+ {/* Selected tags — only rendered when at least one tag is present */} + {tagList.length > 0 && ( +
+ {tagList.map((tag, index) => ( + + ))} +
+ )} + + {/* Input */} + + + {/* Dropdown */} + {showDropdown && ( +
    + {filteredSuggestions.map(tag => ( +
  • selectTag(tag)} + className="px-3 py-2 cursor-pointer hover:bg-blue-50" + > + {tag.description} +
  • + ))} + {isNewTag && ( +
  • + Create “{inputValue.trim()}” +
  • + )} +
+ )} +
+ ) +} \ No newline at end of file diff --git a/frontend/src/mappers/RecipeMapper.ts b/frontend/src/mappers/RecipeMapper.ts index 397a880..e11422d 100644 --- a/frontend/src/mappers/RecipeMapper.ts +++ b/frontend/src/mappers/RecipeMapper.ts @@ -2,6 +2,7 @@ import type {RecipeModel} from "../models/RecipeModel" import type {RecipeDto} from "../api/dtos/RecipeDto" import type {RecipeIngredientGroupDto} from "../api/dtos/RecipeIngredientGroupDto" import type {RecipeInstructionStepDto} from "../api/dtos/RecipeInstructionStepDto" +import {mapTagDtoToModel, mapTagModelToDto} from "./TagMapper" /** * Maps a RecipeDto (as returned by the backend) to the Recipe model @@ -16,36 +17,33 @@ export function mapRecipeDtoToModel(dto: RecipeDto): RecipeModel { amount: dto.amount ?? 1, unit: dto.amountDescription ?? "", }, - // join all instruction step texts into a single string for display + instructionStepList: dto.instructions - .sort((a, b) => a.sortOrder - b.sortOrder) // ensure correct order + .sort((a, b) => a.sortOrder - b.sortOrder) .map(step => ({ - text: step.text, - id: step.id, - /* When mapping a stepDTO, it should already contain a UUID. If - * If, however, for some reason, it does not, add a UUID as sorting - * steps in teh GUI requires a unique identifier for each item. - */ - internalId: step.id !== undefined ? step.id : crypto.randomUUID() - }) - ), + text: step.text, + id: step.id, + internalId: step.id !== undefined ? step.id : crypto.randomUUID() + })), ingredientGroupList: dto.ingredientGroups - .sort((a, b) => a.sortOrder - b.sortOrder) // ensure groups are ordered + .sort((a, b) => a.sortOrder - b.sortOrder) .map(group => ({ id: group.id, title: group.title, ingredientList: group.ingredients - .sort((a, b) => a.sortOrder - b.sortOrder) // ensure ingredients are ordered + .sort((a, b) => a.sortOrder - b.sortOrder) .map(ing => ({ id: ing.id, - name: ing.name ?? "", // @todo ensure that name and amount are indeed present + name: ing.name ?? "", amount: ing.amount, unit: ing.unit, - //subtext: ing.subtext ?? undefined, })), })), - imageUrl: undefined, // not part of DTO yet, placeholder + + tagList: (dto.tagList ?? []).map(mapTagDtoToModel), + + imageUrl: undefined, } } @@ -54,7 +52,6 @@ export function mapRecipeDtoToModel(dto: RecipeDto): RecipeModel { * for sending updates or creations to the backend. */ export function mapRecipeModelToDto(model: RecipeModel): RecipeDto { - // Map instructions const instructionDtos: RecipeInstructionStepDto[] = model.instructionStepList.map( (step, index) => ({ id: step.id, @@ -63,22 +60,22 @@ export function mapRecipeModelToDto(model: RecipeModel): RecipeDto { }) ) - // Map ingredients const ingredientGroupDtos: RecipeIngredientGroupDto[] = model.ingredientGroupList.map((group, groupIndex) => ({ id: group.id, title: group.title, - sortOrder: groupIndex + 1, // sortOrder from list index + sortOrder: groupIndex + 1, ingredients: group.ingredientList.map((ing, ingIndex) => ({ id: ing.id, name: ing.name, amount: ing.amount, unit: ing.unit, - sortOrder: ingIndex + 1, // sortOrder from index - //subtext: ing.subtext ?? null, + sortOrder: ingIndex + 1, })), })) + const tagDtos = model.tagList.map(mapTagModelToDto) + return { id: model.id, title: model.title, @@ -86,5 +83,6 @@ export function mapRecipeModelToDto(model: RecipeModel): RecipeDto { amountDescription: model.servings.unit, instructions: instructionDtos, ingredientGroups: ingredientGroupDtos, + tagList: tagDtos, } -} +} \ No newline at end of file diff --git a/frontend/src/mappers/TagMapper.ts b/frontend/src/mappers/TagMapper.ts new file mode 100644 index 0000000..f497eac --- /dev/null +++ b/frontend/src/mappers/TagMapper.ts @@ -0,0 +1,22 @@ +import type {TagModel} from "../models/TagModel" +import type {TagDto} from "../api/dtos/TagDto" + +/** + * Maps a TagDto (as returned by the backend) to the TagModel used in the frontend. + */ +export function mapTagDtoToModel(dto: TagDto): TagModel { + return { + id: dto.id, + description: dto.description, + } +} + +/** + * Maps a TagModel (as used in the frontend) back to a TagDto for sending to the backend. + */ +export function mapTagModelToDto(model: TagModel): TagDto { + return { + id: model.id!, + description: model.description, + } +} \ No newline at end of file diff --git a/frontend/src/models/RecipeModel.ts b/frontend/src/models/RecipeModel.ts index d212a64..dae6935 100644 --- a/frontend/src/models/RecipeModel.ts +++ b/frontend/src/models/RecipeModel.ts @@ -1,10 +1,12 @@ -import type { IngredientGroupModel } from "./IngredientGroupModel" -import type { InstructionStepModel } from "./InstructionStepModel" -import type { ServingsModel } from "./ServingsModel" +import type {IngredientGroupModel} from "./IngredientGroupModel" +import type {InstructionStepModel} from "./InstructionStepModel" +import type {ServingsModel} from "./ServingsModel" +import type {TagModel} from "./TagModel.ts"; /** * Represents a recipe object in the application. */ + /* * @todo ingredient groups! There may be serveral ingredient lists, each with a title. * e.g. for the dough, for the filling, for the icing,... @@ -13,24 +15,27 @@ import type { ServingsModel } from "./ServingsModel" * - add an IngredientGroupListEditor for handling IngredientGroups */ export interface RecipeModel { - /** Unique identifier for the recipe */ - id?: string + /** Unique identifier for the recipe */ + id?: string - /** Title of the recipe */ - title: string + /** Title of the recipe */ + title: string - /** List of ingredients groups containing the ingredients of the recipe */ - ingredientGroupList: IngredientGroupModel[] + /** List of ingredients groups containing the ingredients of the recipe */ + ingredientGroupList: IngredientGroupModel[] - /** Preparation instructions */ - instructionStepList: InstructionStepModel[] + /** Preparation instructions */ + instructionStepList: InstructionStepModel[] - /** Number of servings, e.g., for 4 person, 12 cupcakes, 2 glasses */ - servings: ServingsModel + /** Number of servings, e.g., for 4 person, 12 cupcakes, 2 glasses */ + servings: ServingsModel - /** Unit for the quantity */ + /** Unit for the quantity */ - /** Optional image URL for the recipe */ - imageUrl?: string + /** Optional image URL for the recipe */ + imageUrl?: string + + /** Tags categorising the recipe, e.g. "dessert", "vegetarian", "christmas" */ + tagList: TagModel[] } diff --git a/frontend/src/models/TagModel.ts b/frontend/src/models/TagModel.ts new file mode 100644 index 0000000..674af1e --- /dev/null +++ b/frontend/src/models/TagModel.ts @@ -0,0 +1,9 @@ +/** + * Represents a tag in the application. + */ +export interface TagModel { + /** Unique identifier for the tag */ + id?: string + /** Human-readable label, e.g. "dessert", "vegetarian", "christmas" */ + description: string +} \ No newline at end of file From 709cb23f3da4e877e516cdc4e87b1a7fa98409de Mon Sep 17 00:00:00 2001 From: araemer Date: Sun, 22 Feb 2026 13:15:48 +0100 Subject: [PATCH 2/2] Add the API Client to all rest resources --- frontend/src/api/apiClient.ts | 2 -- .../src/api/endpoints/AuthRestResource.ts | 14 +++------- .../endpoints/CompactRecipeRestResource.ts | 27 +++++++------------ .../src/api/endpoints/RecipeRestResource.ts | 19 ++++--------- frontend/src/api/endpoints/TagRestResource.ts | 8 +++--- .../src/api/endpoints/UserRestResource.ts | 12 +++++---- 6 files changed, 29 insertions(+), 53 deletions(-) diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index a7185f3..fe4653a 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -90,8 +90,6 @@ export const apiClient = { get: (endpoint: string) => apiRequest(endpoint, {method: "GET"}), post: (endpoint: string, body: object) => apiRequest(endpoint, {method: "POST", body: JSON.stringify(body)}), - put: (endpoint: string, body: object) => - apiRequest(endpoint, {method: "PUT", body: JSON.stringify(body)}), delete: (endpoint: string) => apiRequest(endpoint, {method: "DELETE"}), }; diff --git a/frontend/src/api/endpoints/AuthRestResource.ts b/frontend/src/api/endpoints/AuthRestResource.ts index 603eb1c..2e18229 100644 --- a/frontend/src/api/endpoints/AuthRestResource.ts +++ b/frontend/src/api/endpoints/AuthRestResource.ts @@ -1,18 +1,11 @@ import type {LoginRequest} from "../dtos/LoginRequest.ts"; import type {LoginResponse} from "../dtos/LoginResponse.ts"; -import {postJson} from "../utils/requests"; - - -/** - * Util for handling the recipe api - */ -// read base url from .env file -const BASE_URL = import.meta.env.VITE_API_BASE; +import {apiClient} from "../apiClient.ts"; /** * URL for handling recipes */ -const AUTH_URL = `${BASE_URL}/auth` +const AUTH_URL = `/auth` /** @@ -21,6 +14,5 @@ const AUTH_URL = `${BASE_URL}/auth` * @returns LoginResponse */ export async function login(loginRequest: LoginRequest): Promise { - const res = await postJson(`${AUTH_URL}/login`, JSON.stringify(loginRequest), false); - return res.json(); + return apiClient.post(`${AUTH_URL}/login`, loginRequest); } diff --git a/frontend/src/api/endpoints/CompactRecipeRestResource.ts b/frontend/src/api/endpoints/CompactRecipeRestResource.ts index 45cf28b..dd374e1 100644 --- a/frontend/src/api/endpoints/CompactRecipeRestResource.ts +++ b/frontend/src/api/endpoints/CompactRecipeRestResource.ts @@ -1,29 +1,22 @@ -import type { RecipeModel } from "../../models/RecipeModel" -import { get } from "../utils/requests"; +import type {RecipeModel} from "../../models/RecipeModel" +import {apiClient} from "../apiClient.ts"; -/** - * Util for handling the recipe api - */ -// read base url from .env file -const BASE_URL = import.meta.env.VITE_API_BASE; - /** * URL for handling recipes header data */ -const RECIPE_URL = `${BASE_URL}/compact-recipe` +const RECIPE_URL = "/compact-recipe" /** * Load list of all recipes * @param searchString Search string for filtering recipeList * @returns Array of recipe */ -export async function fetchRecipeList(searchString : string): Promise { - let url : string = RECIPE_URL; // add an s to the base URL as we want to load a list - // if there's a search string add it as query parameter - if(searchString && searchString !== ""){ - url +="?search=" + searchString; - } - const res = await get(url); - return res.json(); +export async function fetchRecipeList(searchString: string): Promise { + let url: string = RECIPE_URL; // add an s to the base URL as we want to load a list + // if there's a search string add it as query parameter + if (searchString && searchString !== "") { + url += "?search=" + searchString; + } + return apiClient.get(url); } diff --git a/frontend/src/api/endpoints/RecipeRestResource.ts b/frontend/src/api/endpoints/RecipeRestResource.ts index 35b833e..ea7a575 100644 --- a/frontend/src/api/endpoints/RecipeRestResource.ts +++ b/frontend/src/api/endpoints/RecipeRestResource.ts @@ -1,17 +1,10 @@ -import type { RecipeDto } from "../dtos/RecipeDto"; -import { get, postJson } from "../utils/requests"; - - -/** - * Util for handling the recipe api - */ -// read base url from .env file -const BASE_URL = import.meta.env.VITE_API_BASE; +import type {RecipeDto} from "../dtos/RecipeDto"; +import {apiClient} from "../apiClient.ts"; /** * URL for handling recipes */ -const RECIPE_URL = `${BASE_URL}/recipe` +const RECIPE_URL = "/recipe" /** * Load a single recipe @@ -19,8 +12,7 @@ const RECIPE_URL = `${BASE_URL}/recipe` * @returns A single recipe */ export async function fetchRecipe(id: string): Promise { - const res = await get(`${RECIPE_URL}/${id}`) - return res.json() + return apiClient.get(`${RECIPE_URL}/${id}`); } /** @@ -29,6 +21,5 @@ export async function fetchRecipe(id: string): Promise { * @returns Saved recipe */ export async function createOrUpdateRecipe(recipe: RecipeDto): Promise { - const res = await postJson(RECIPE_URL + "/create-or-update", JSON.stringify(recipe)); - return res.json(); + return apiClient.post(`${RECIPE_URL}/create-or-update`, recipe); } diff --git a/frontend/src/api/endpoints/TagRestResource.ts b/frontend/src/api/endpoints/TagRestResource.ts index 6660368..9ba5319 100644 --- a/frontend/src/api/endpoints/TagRestResource.ts +++ b/frontend/src/api/endpoints/TagRestResource.ts @@ -1,13 +1,13 @@ import {apiClient} from "../apiClient" import type {TagDto} from "../dtos/TagDto" -const TAG_ENDPOINT = "tags" +const TAG_URL = `/tag` /** * Fetches all existing tags from the backend. */ export async function fetchAllTags(): Promise { - return apiClient.get(TAG_ENDPOINT) + return apiClient.get(`${TAG_URL}/all`) } /** @@ -15,7 +15,7 @@ export async function fetchAllTags(): Promise { * @param tag The tag to create or update */ export async function createOrUpdateTag(tag: TagDto): Promise { - return apiClient.post(`${TAG_ENDPOINT}/create-or-update`, tag) + return apiClient.post(`${TAG_URL}/create-or-update`, tag) } /** @@ -23,5 +23,5 @@ export async function createOrUpdateTag(tag: TagDto): Promise { * @param id The ID of the tag to delete */ export async function deleteTag(id: string): Promise { - return apiClient.delete(`${TAG_ENDPOINT}/${id}`) + return apiClient.delete(`${TAG_URL}/${id}`) } \ No newline at end of file diff --git a/frontend/src/api/endpoints/UserRestResource.ts b/frontend/src/api/endpoints/UserRestResource.ts index 02d0695..a305ad8 100644 --- a/frontend/src/api/endpoints/UserRestResource.ts +++ b/frontend/src/api/endpoints/UserRestResource.ts @@ -5,22 +5,24 @@ import type {ChangeUserPasswordRequest} from "../dtos/ChangeUserPasswordRequest. import type {CreateUserResponse} from "../dtos/CreateUserResponse.ts"; import type {UserListResponse} from "../dtos/UserListResponse.ts"; +const USER_URL = "/user" + export async function fetchCurrentUser(): Promise { - return apiClient.get("/user/me"); + return apiClient.get(`${USER_URL}/me`); } export async function fetchAllUsers(): Promise { - return apiClient.get("/user/all"); + return apiClient.get(`${USER_URL}/all`); } export async function createUser(dto: CreateUserRequest): Promise { - return apiClient.post("/user/create", dto); + return apiClient.post(`${USER_URL}/create`, dto); } export async function updateUser(dto: UserDto): Promise { - return apiClient.post("/user/update", dto); + return apiClient.post(`${USER_URL}/update`, dto); } export async function changePassword(dto: ChangeUserPasswordRequest) { - return apiClient.post("/user/change-password", dto); + return apiClient.post(`${USER_URL}/change-password`, dto); }