diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7a4e2b7..5923390 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,9 +2,10 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom" import RecipeDetailPage from "./components/recipes/RecipeDetailPage" import RecipeEditPage from "./components/recipes/RecipeEditPage" import RecipeListPage from "./components/recipes/RecipeListPage" -import { getRecipeAddUrlDefinition, getRecipeDetailsUrlDefinition, getRecipeEditUrlDefinition, getRootUrlDefinition } from "./routes" +import { getLoginUrl, getRecipeAddUrlDefinition, getRecipeDetailsUrlDefinition, getRecipeEditUrlDefinition, getRecipeListUrlDefinition, getRootUrlDefinition } from "./routes" import "./App.css" +import LoginPage from "./components/LoginPage" /** * Main application component. @@ -14,8 +15,10 @@ function App() { return ( + {/* Login page */} + }/> {/* Home page: list of recipes */} - } /> + } /> {/* Detail page: shows one recipe */} } /> diff --git a/frontend/src/api/dtos/LoginRequestDto.ts b/frontend/src/api/dtos/LoginRequestDto.ts new file mode 100644 index 0000000..a3da6ee --- /dev/null +++ b/frontend/src/api/dtos/LoginRequestDto.ts @@ -0,0 +1,7 @@ +/** + * Defines a login request + */ +export class LoginRequestDto { + userName?: string; + password?: string; +} \ No newline at end of file diff --git a/frontend/src/api/dtos/LoginResponseDto.ts b/frontend/src/api/dtos/LoginResponseDto.ts new file mode 100644 index 0000000..fdb5a39 --- /dev/null +++ b/frontend/src/api/dtos/LoginResponseDto.ts @@ -0,0 +1,10 @@ +import { UserDto } from "./UserDto.js"; + +/** + * Response to a successful login + */ +export class LoginResponseDto { + userData?: UserDto; + token?: string; + expiryDate? : Date; +} \ No newline at end of file diff --git a/frontend/src/api/dtos/RecipeDto.ts b/frontend/src/api/dtos/RecipeDto.ts new file mode 100644 index 0000000..b8d757f --- /dev/null +++ b/frontend/src/api/dtos/RecipeDto.ts @@ -0,0 +1,15 @@ + +import { AbstractDto } from "./AbstractDto.js"; +import { RecipeIngredientGroupDto } from "./RecipeIngredientGroupDto.js"; +import { RecipeInstructionStepDto } from "./RecipeInstructionStepDto.js"; +/** + * DTO describing a recipe + */ + +export class RecipeDto extends AbstractDto { + title!: string; + amount?: number + amountDescription?: string; + instructions!: RecipeInstructionStepDto[]; + ingredientGroups!: RecipeIngredientGroupDto[]; +} \ No newline at end of file diff --git a/frontend/src/api/dtos/RecipeIngredientDto.ts b/frontend/src/api/dtos/RecipeIngredientDto.ts new file mode 100644 index 0000000..3d7c91f --- /dev/null +++ b/frontend/src/api/dtos/RecipeIngredientDto.ts @@ -0,0 +1,10 @@ +import { AbstractDto } from "./AbstractDto.js"; + +export class RecipeIngredientDto extends AbstractDto{ + name!: string; + subtext?: string; + amount?: number; + unit?: string; + sortOrder!: number; + ingredientGroupId?: string; +} \ No newline at end of file diff --git a/frontend/src/api/dtos/RecipeIngredientGroupDto.ts b/frontend/src/api/dtos/RecipeIngredientGroupDto.ts new file mode 100644 index 0000000..324d3c5 --- /dev/null +++ b/frontend/src/api/dtos/RecipeIngredientGroupDto.ts @@ -0,0 +1,9 @@ +import { AbstractDto } from "./AbstractDto.js"; +import { RecipeIngredientDto } from "./RecipeIngredientDto.js"; + +export class RecipeIngredientGroupDto extends AbstractDto{ + title?: string; + sortOrder!: number; + recipeId?: string; + ingredients!: RecipeIngredientDto[]; +} \ No newline at end of file diff --git a/frontend/src/api/dtos/RecipeInstructionStepDto.ts b/frontend/src/api/dtos/RecipeInstructionStepDto.ts new file mode 100644 index 0000000..362e282 --- /dev/null +++ b/frontend/src/api/dtos/RecipeInstructionStepDto.ts @@ -0,0 +1,8 @@ +import { UUID } from "crypto"; +import { AbstractDto } from "./AbstractDto.js"; + +export class RecipeInstructionStepDto extends AbstractDto{ + text!: string; + sortOrder!: number; + recipeId?: UUID; +} \ No newline at end of file diff --git a/frontend/src/api/dtos/UserDto.ts b/frontend/src/api/dtos/UserDto.ts new file mode 100644 index 0000000..56fc6c2 --- /dev/null +++ b/frontend/src/api/dtos/UserDto.ts @@ -0,0 +1,9 @@ +import { AbstractDto } from "./AbstractDto.js"; + +export class UserDto extends AbstractDto { + firstName?: string; + lastName?: string; + userName!: string; + email!: string; + role?: string; +} \ No newline at end of file diff --git a/frontend/src/api/points/AuthPoint.ts b/frontend/src/api/points/AuthPoint.ts new file mode 100644 index 0000000..493f34c --- /dev/null +++ b/frontend/src/api/points/AuthPoint.ts @@ -0,0 +1,27 @@ +import type { Recipe } from "../../types/recipe" +import type { LoginRequestDto } from "../dtos/LoginRequestDto"; +import type { LoginResponseDto } from "../dtos/LoginResponseDto"; +import { get, postJson, putJson } 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 = `${BASE_URL}/auth` + + +/** + * Create new Recipe + * @param recipe Recipe to create + * @returns Saved recipe + */ +export async function login(requestDto: LoginRequestDto): Promise { + const res = await postJson(`${AUTH_URL}/login`, JSON.stringify(requestDto), false); + return res.json(); +} diff --git a/frontend/src/api/points/CompactRecipePoint.ts b/frontend/src/api/points/CompactRecipePoint.ts new file mode 100644 index 0000000..6579e4d --- /dev/null +++ b/frontend/src/api/points/CompactRecipePoint.ts @@ -0,0 +1,29 @@ +import type { Recipe } from "../../types/recipe" +import { get, postJson, putJson } 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 = `${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; + // 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/recipePoint.ts b/frontend/src/api/points/RecipePoint.ts similarity index 64% rename from frontend/src/api/recipePoint.ts rename to frontend/src/api/points/RecipePoint.ts index 44f5779..73bf935 100644 --- a/frontend/src/api/recipePoint.ts +++ b/frontend/src/api/points/RecipePoint.ts @@ -1,5 +1,5 @@ -import type { Recipe } from "../types/recipe" -import { get, postJson, putJson } from "./utils/requests"; +import type { Recipe } from "../../types/recipe" +import { get, postJson, putJson } from "../utils/requests"; /** @@ -23,21 +23,6 @@ export async function fetchRecipe(id: string): Promise { return res.json() } -/** - * 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; - // 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(); -} - /** * Create new Recipe * @param recipe Recipe to create diff --git a/frontend/src/api/utils/headers.ts b/frontend/src/api/utils/headers.ts index 4a2fdb9..2ca7bfa 100644 --- a/frontend/src/api/utils/headers.ts +++ b/frontend/src/api/utils/headers.ts @@ -1,3 +1,12 @@ + +const BASE_URL = import.meta.env.VITE_API_BASE; + +export function createBasicHeader() : Headers { + const headers = new Headers(); + //headers.set('Access-Control-Allow-Origin', '*'); + return headers; +} + export function setContentTypeHeaderJson(headers: Headers){ return headers.set("Content-Type", "application/json"); } diff --git a/frontend/src/api/utils/requests.ts b/frontend/src/api/utils/requests.ts index 2a3156f..7a335ee 100644 --- a/frontend/src/api/utils/requests.ts +++ b/frontend/src/api/utils/requests.ts @@ -1,7 +1,7 @@ -import { setAuthHeader, setContentTypeHeaderJson } from "./headers"; +import { createBasicHeader, setAuthHeader, setContentTypeHeaderJson } from "./headers"; export async function get(url: string) : Promise{ - const requestHeaders = new Headers(); + const requestHeaders = createBasicHeader(); setAuthHeader(requestHeaders); console.log("GET to " + url); const response = await fetch(url, { @@ -15,26 +15,35 @@ export async function get(url: string) : Promise{ } export async function postJson(url: string, requestBody: string, logBody = true) : Promise{ - console.log("POST to " + url + (logBody) ? "with body " + requestBody : ""); - return persistJson(url, requestBody, "POST"); + console.log("POST to " + url); + if(logBody){ + console.log("body: " + requestBody); + } + return await persistJson(url, requestBody, "POST"); } export async function putJson(url: string, requestBody: string, logBody = true) : Promise{ - console.log("PUT to " + url + (logBody) ? "with body " + requestBody : ""); + console.log("PUT to " + url); + if(logBody){ + console.log("body: " + requestBody); + } return persistJson(url, requestBody, "PUT"); } async function persistJson(url: string, requestBody: string, requestMethod: string) : Promise{ - const requestHeaders = new Headers(); + const requestHeaders = createBasicHeader(); setContentTypeHeaderJson(requestHeaders); setAuthHeader(requestHeaders); - const response = await fetch(url, { + const request = new Request(url, { method: requestMethod, headers: requestHeaders, body: requestBody, }); + console.log(request) + const response = await fetch(request); + console.log("response.ok", response.ok) if(!response.ok){ - throw new Error(requestMethod + " to " + url + "failed!") + throw new Error(requestMethod + " to " + url + " failed!") } return response; } diff --git a/frontend/src/components/LoginPage.tsx b/frontend/src/components/LoginPage.tsx new file mode 100644 index 0000000..eb23cdd --- /dev/null +++ b/frontend/src/components/LoginPage.tsx @@ -0,0 +1,55 @@ +import { useState } from "react"; +import Button from "./basics/Button"; +import type { LoginRequestDto } from "../api/dtos/LoginRequestDto"; +import type { LoginResponseDto } from "../api/dtos/LoginResponseDto"; +import { login } from "../api/points/AuthPoint"; +import { getRecipeListUrl } from "../routes"; +import { useNavigate } from "react-router-dom"; + +export default function LoginPage(){ + const [userName, setUserName] = useState(""); + const [password, setPassword] = useState(""); + const navigate = useNavigate(); + const executeLogin = async () =>{ + const dto : LoginRequestDto = { + userName: userName, + password: password + } + try{ + // @todo move to auth handler + console.log("trying to log in with " + dto.userName) + const loginResponse : LoginResponseDto = await login(dto); + localStorage.setItem("session", JSON.stringify(loginResponse)); + console.log("Successfully logged on with user " + loginResponse.userData?.userName) + navigate(getRecipeListUrl()); + } catch(err){ + // @todo show error in GUI + console.error(err); + } + } + return( +
+ { + setUserName(e.target.value) + }} + /> + {/* @todo Password mode!!! */} + { + setPassword(e.target.value) + }} + /> +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/recipes/RecipeDetailPage.tsx b/frontend/src/components/recipes/RecipeDetailPage.tsx index 83fdcc9..87bb183 100644 --- a/frontend/src/components/recipes/RecipeDetailPage.tsx +++ b/frontend/src/components/recipes/RecipeDetailPage.tsx @@ -1,7 +1,7 @@ -import { useParams, Link } from "react-router-dom" +import { useParams } from "react-router-dom" import type { Recipe } from "../../types/recipe" import { useEffect, useState } from "react" -import { fetchRecipe } from "../../api/recipePoint" +import { fetchRecipe } from "../../api/points/RecipePoint" import { getRecipeEditUrl, getRecipeListUrl } from "../../routes" import ButtonLink from "../basics/ButtonLink" diff --git a/frontend/src/components/recipes/RecipeEditPage.tsx b/frontend/src/components/recipes/RecipeEditPage.tsx index f1022b4..a06243d 100644 --- a/frontend/src/components/recipes/RecipeEditPage.tsx +++ b/frontend/src/components/recipes/RecipeEditPage.tsx @@ -2,8 +2,8 @@ import { useParams, useNavigate } from "react-router-dom" import { useEffect, useState } from "react" import type { Recipe } from "../../types/recipe" import RecipeEditor from "./RecipeEditor" -import { fetchRecipe, createRecipe, updateRecipe } from "../../api/recipePoint" -import { getRecipeDetailUrl, getRecipeListUrl, getRootUrl } from "../../routes" +import { fetchRecipe, createRecipe, updateRecipe } from "../../api/points/RecipePoint" +import { getRecipeDetailUrl, getRecipeListUrl } from "../../routes" export default function RecipeEditPage() { // Extract recipe ID from route params diff --git a/frontend/src/components/recipes/RecipeListPage.tsx b/frontend/src/components/recipes/RecipeListPage.tsx index cd08be9..5b2fb80 100644 --- a/frontend/src/components/recipes/RecipeListPage.tsx +++ b/frontend/src/components/recipes/RecipeListPage.tsx @@ -1,10 +1,9 @@ import { useEffect, useState } from "react" import RecipeListItem from "./RecipeListItem" import type { Recipe } from "../../types/recipe" -import { fetchRecipeList } from "../../api/recipePoint" +import { fetchRecipeList } from "../../api/points/CompactRecipePoint" import { useNavigate } from "react-router-dom" import { getRecipeAddUrl, getRecipeDetailUrl } from "../../routes" -import SearchField from "../basics/SearchField" import RecipeListToolbar from "./RecipeListToolbar" /** diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 5b31d55..ba3b241 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -6,10 +6,13 @@ export function getRootUrlDefinition() : string { return getRootUrl()} export function getRecipeDetailsUrlDefinition() : string {return getRecipeDetailUrl(":id")} export function getRecipeEditUrlDefinition() : string {return getRecipeEditUrl(":id")} export function getRecipeAddUrlDefinition() : string {return getRecipeAddUrl()} +export function getRecipeListUrlDefinition() : string {return getRecipeListUrl()} +export function getLoginUrlDefinition() : string {return getLoginUrl()} // URLs including id export function getRootUrl () : string { return "/"} -export function getRecipeListUrl() : string {return getRootUrl()} +export function getRecipeListUrl() : string {return "/recipe/list"} export function getRecipeDetailUrl(id: string) : string {return "/recipe/" + id + "/card"} export function getRecipeEditUrl(id: string) : string {return "/recipe/" + id + "/edit"} -export function getRecipeAddUrl() : string {return "/recipe/add"} \ No newline at end of file +export function getRecipeAddUrl() : string {return "/recipe/add"} +export function getLoginUrl() : string {return "/login"} \ No newline at end of file