diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index fe4653a..a7185f3 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -90,6 +90,8 @@ 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/dtos/RecipeDto.ts b/frontend/src/api/dtos/RecipeDto.ts index dbb9085..bc5bedc 100644 --- a/frontend/src/api/dtos/RecipeDto.ts +++ b/frontend/src/api/dtos/RecipeDto.ts @@ -1,8 +1,7 @@ -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 */ @@ -13,6 +12,4 @@ 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 deleted file mode 100644 index b20ba40..0000000 --- a/frontend/src/api/dtos/TagDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -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/AuthRestResource.ts b/frontend/src/api/endpoints/AuthRestResource.ts index 2e18229..603eb1c 100644 --- a/frontend/src/api/endpoints/AuthRestResource.ts +++ b/frontend/src/api/endpoints/AuthRestResource.ts @@ -1,11 +1,18 @@ import type {LoginRequest} from "../dtos/LoginRequest.ts"; import type {LoginResponse} from "../dtos/LoginResponse.ts"; -import {apiClient} from "../apiClient.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; /** * URL for handling recipes */ -const AUTH_URL = `/auth` +const AUTH_URL = `${BASE_URL}/auth` /** @@ -14,5 +21,6 @@ const AUTH_URL = `/auth` * @returns LoginResponse */ export async function login(loginRequest: LoginRequest): Promise { - return apiClient.post(`${AUTH_URL}/login`, loginRequest); + const res = await postJson(`${AUTH_URL}/login`, JSON.stringify(loginRequest), false); + return res.json(); } diff --git a/frontend/src/api/endpoints/CompactRecipeRestResource.ts b/frontend/src/api/endpoints/CompactRecipeRestResource.ts index dd374e1..45cf28b 100644 --- a/frontend/src/api/endpoints/CompactRecipeRestResource.ts +++ b/frontend/src/api/endpoints/CompactRecipeRestResource.ts @@ -1,22 +1,29 @@ -import type {RecipeModel} from "../../models/RecipeModel" -import {apiClient} from "../apiClient.ts"; +import type { RecipeModel } from "../../models/RecipeModel" +import { get } from "../utils/requests"; +/** + * 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 = "/compact-recipe" +const RECIPE_URL = `${BASE_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; - } - return apiClient.get(url); +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(); } diff --git a/frontend/src/api/endpoints/RecipeRestResource.ts b/frontend/src/api/endpoints/RecipeRestResource.ts index ea7a575..35b833e 100644 --- a/frontend/src/api/endpoints/RecipeRestResource.ts +++ b/frontend/src/api/endpoints/RecipeRestResource.ts @@ -1,10 +1,17 @@ -import type {RecipeDto} from "../dtos/RecipeDto"; -import {apiClient} from "../apiClient.ts"; +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; /** * URL for handling recipes */ -const RECIPE_URL = "/recipe" +const RECIPE_URL = `${BASE_URL}/recipe` /** * Load a single recipe @@ -12,7 +19,8 @@ const RECIPE_URL = "/recipe" * @returns A single recipe */ export async function fetchRecipe(id: string): Promise { - return apiClient.get(`${RECIPE_URL}/${id}`); + const res = await get(`${RECIPE_URL}/${id}`) + return res.json() } /** @@ -21,5 +29,6 @@ export async function fetchRecipe(id: string): Promise { * @returns Saved recipe */ export async function createOrUpdateRecipe(recipe: RecipeDto): Promise { - return apiClient.post(`${RECIPE_URL}/create-or-update`, recipe); + const res = await postJson(RECIPE_URL + "/create-or-update", JSON.stringify(recipe)); + return res.json(); } diff --git a/frontend/src/api/endpoints/TagRestResource.ts b/frontend/src/api/endpoints/TagRestResource.ts deleted file mode 100644 index 9ba5319..0000000 --- a/frontend/src/api/endpoints/TagRestResource.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {apiClient} from "../apiClient" -import type {TagDto} from "../dtos/TagDto" - -const TAG_URL = `/tag` - -/** - * Fetches all existing tags from the backend. - */ -export async function fetchAllTags(): Promise { - return apiClient.get(`${TAG_URL}/all`) -} - -/** - * 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_URL}/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_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 a305ad8..02d0695 100644 --- a/frontend/src/api/endpoints/UserRestResource.ts +++ b/frontend/src/api/endpoints/UserRestResource.ts @@ -5,24 +5,22 @@ 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_URL}/me`); + return apiClient.get("/user/me"); } export async function fetchAllUsers(): Promise { - return apiClient.get(`${USER_URL}/all`); + return apiClient.get("/user/all"); } export async function createUser(dto: CreateUserRequest): Promise { - return apiClient.post(`${USER_URL}/create`, dto); + return apiClient.post("/user/create", dto); } export async function updateUser(dto: UserDto): Promise { - return apiClient.post(`${USER_URL}/update`, dto); + return apiClient.post("/user/update", dto); } export async function changePassword(dto: ChangeUserPasswordRequest) { - return apiClient.post(`${USER_URL}/change-password`, dto); + return apiClient.post("/user/change-password", dto); } diff --git a/frontend/src/components/recipes/RecipeEditPage.tsx b/frontend/src/components/recipes/RecipeEditPage.tsx index f6fa145..6a9a5d8 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/endpoints/RecipeRestResource.ts" +import {createOrUpdateRecipe, fetchRecipe} from "../../api/points/RecipePoint" 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 bcb29ac..154bf6c 100644 --- a/frontend/src/components/recipes/RecipeEditor.tsx +++ b/frontend/src/components/recipes/RecipeEditor.tsx @@ -5,13 +5,11 @@ 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 @@ -25,30 +23,46 @@ 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}) } - const updateTagList = (tagList: TagModel[]) => { - setDraft({...draft, tagList}) - } - + /** + * Validate recipe + * @returns Information on the errors the validation encountered + */ 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 => !ingGrp.ingredientList || ingGrp.ingredientList.length === 0 + ingGrp => { + return !ingGrp.ingredientList || ingGrp.ingredientList.length === 0 + } ) if (isAnyIngredientListEmpty) { newErrors.ingredients = true @@ -56,19 +70,22 @@ 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"} @@ -82,7 +99,6 @@ export function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) { value={draft.title} onChange={e => setDraft({...draft, title: e.target.value})} /> - {/* Servings */}

Portionen

@@ -108,15 +124,7 @@ export function RecipeEditor({recipe, onSave, onCancel}: RecipeEditorProps) { }} />
- - {/* Tags */} -

Tags

- - - {/* Ingredient List */} + {/* Ingredient List - @todo better visualization of errors! */}
- {/* 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 deleted file mode 100644 index ef57aee..0000000 --- a/frontend/src/components/tags/TagListEditor.tsx +++ /dev/null @@ -1,161 +0,0 @@ -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 e11422d..397a880 100644 --- a/frontend/src/mappers/RecipeMapper.ts +++ b/frontend/src/mappers/RecipeMapper.ts @@ -2,7 +2,6 @@ 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 @@ -17,33 +16,36 @@ 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) + .sort((a, b) => a.sortOrder - b.sortOrder) // ensure correct order .map(step => ({ - text: step.text, - id: step.id, - internalId: step.id !== undefined ? step.id : crypto.randomUUID() - })), + 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() + }) + ), ingredientGroupList: dto.ingredientGroups - .sort((a, b) => a.sortOrder - b.sortOrder) + .sort((a, b) => a.sortOrder - b.sortOrder) // ensure groups are ordered .map(group => ({ id: group.id, title: group.title, ingredientList: group.ingredients - .sort((a, b) => a.sortOrder - b.sortOrder) + .sort((a, b) => a.sortOrder - b.sortOrder) // ensure ingredients are ordered .map(ing => ({ id: ing.id, - name: ing.name ?? "", + name: ing.name ?? "", // @todo ensure that name and amount are indeed present amount: ing.amount, unit: ing.unit, + //subtext: ing.subtext ?? undefined, })), })), - - tagList: (dto.tagList ?? []).map(mapTagDtoToModel), - - imageUrl: undefined, + imageUrl: undefined, // not part of DTO yet, placeholder } } @@ -52,6 +54,7 @@ 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, @@ -60,22 +63,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: groupIndex + 1, // sortOrder from list index ingredients: group.ingredientList.map((ing, ingIndex) => ({ id: ing.id, name: ing.name, amount: ing.amount, unit: ing.unit, - sortOrder: ingIndex + 1, + sortOrder: ingIndex + 1, // sortOrder from index + //subtext: ing.subtext ?? null, })), })) - const tagDtos = model.tagList.map(mapTagModelToDto) - return { id: model.id, title: model.title, @@ -83,6 +86,5 @@ 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 deleted file mode 100644 index f497eac..0000000 --- a/frontend/src/mappers/TagMapper.ts +++ /dev/null @@ -1,22 +0,0 @@ -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 dae6935..d212a64 100644 --- a/frontend/src/models/RecipeModel.ts +++ b/frontend/src/models/RecipeModel.ts @@ -1,12 +1,10 @@ -import type {IngredientGroupModel} from "./IngredientGroupModel" -import type {InstructionStepModel} from "./InstructionStepModel" -import type {ServingsModel} from "./ServingsModel" -import type {TagModel} from "./TagModel.ts"; +import type { IngredientGroupModel } from "./IngredientGroupModel" +import type { InstructionStepModel } from "./InstructionStepModel" +import type { ServingsModel } from "./ServingsModel" /** * 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,... @@ -15,27 +13,24 @@ import type {TagModel} from "./TagModel.ts"; * - 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 - - /** Tags categorising the recipe, e.g. "dessert", "vegetarian", "christmas" */ - tagList: TagModel[] + /** Optional image URL for the recipe */ + imageUrl?: string } diff --git a/frontend/src/models/TagModel.ts b/frontend/src/models/TagModel.ts deleted file mode 100644 index 674af1e..0000000 --- a/frontend/src/models/TagModel.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * 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