Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add restaurant detail page with front and backend #9

Merged
merged 13 commits into from
Jun 5, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,11 @@ public ExceptionResponse handleMethodArgumentNotValidException(MethodArgumentNot

return new ExceptionResponse(errors);
}

@ExceptionHandler(NoSuchRestaurantException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleNoSuchRestaurantException(NoSuchRestaurantException exception) {

return exception.getMessage();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.neuefische.team2.backend.exceptions;

import java.util.NoSuchElementException;

public class NoSuchRestaurantException extends NoSuchElementException {
public NoSuchRestaurantException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import com.neuefische.team2.backend.restaurant.domain.NewRestaurantDTO;
import com.neuefische.team2.backend.restaurant.domain.Restaurant;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

Expand All @@ -23,6 +27,11 @@ public List<Restaurant> getRestaurants() {
return restaurantService.getRestaurants();
}

@GetMapping("/{id}")
Restaurant getRestaurantById(@PathVariable @Valid String id) {
daniel-pohl marked this conversation as resolved.
Show resolved Hide resolved
return restaurantService.findRestaurantById(id);
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Restaurant addRestaurant(@RequestBody @Valid NewRestaurantDTO newRestaurantDTO) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.neuefische.team2.backend.restaurant;

import com.neuefische.team2.backend.exceptions.NoSuchRestaurantException;
import com.neuefische.team2.backend.restaurant.domain.Restaurant;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
Expand All @@ -18,6 +18,10 @@ public List<Restaurant> getRestaurants() {
return restaurantRepository.findAll();
}

public Restaurant findRestaurantById(String id) {
return restaurantRepository.findById(id).orElseThrow(() -> new NoSuchRestaurantException("Restaurant with id " + id + " not found"));
}

public Restaurant addRestaurant(Restaurant restaurant) {
return restaurantRepository.save(restaurant);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.neuefische.team2.backend.restaurant.domain;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Document("restaurants")
public record Restaurant(
@Id
String id,
String title,
String city
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,41 @@ void addRestaurant_whenTitleAndCityContainOnlySpaces_thenReturnException() throw
}
"""));
}
@Test
daniel-pohl marked this conversation as resolved.
Show resolved Hide resolved
void getRestaurantById_whenRestaurantExists_thenReturnRestaurant() throws Exception {
//GIVEN
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/api/restaurants")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"title": "The Mockingbird",
"city": "New York"
}
""")).andReturn();
ObjectMapper mapper = new ObjectMapper();
Restaurant restaurant = mapper.readValue(result.getResponse().getContentAsString(), Restaurant.class);

//WHEN
mockMvc.perform(MockMvcRequestBuilders.get("/api/restaurants/" + restaurant.id()))
//THEN
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().json("""
{
"title": "The Mockingbird",
"city": "New York"
}
"""))
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(restaurant.id()));
}

@Test
void getRestaurantById_whenRestaurantDoesNotExist_thenReturnNotFound() throws Exception {

mockMvc.perform(MockMvcRequestBuilders.get("/api/restaurants/999"))
.andExpect(MockMvcResultMatchers.status().isNotFound())
.andExpect(MockMvcResultMatchers.content().string("Restaurant with id 999 not found"));
}



}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.neuefische.team2.backend.restaurant;

import com.neuefische.team2.backend.exceptions.NoSuchRestaurantException;
import com.neuefische.team2.backend.restaurant.domain.Restaurant;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class RestaurantServiceTest {
Expand Down Expand Up @@ -42,6 +43,32 @@ void getRestaurants_whenOneRestaurantInDB_thenReturnListOfOne() {
List<Restaurant> expected = List.of(restaurant);
assertEquals(expected, actual);
}
@Test
void findRestaurantById_whenRestaurantExists_thenReturnRestaurant() {
//GIVEN
Restaurant restaurant = new Restaurant("1", "The Mockingbird", "New York");
when(mockRestaurantRepository.findById("1")).thenReturn(Optional.of(restaurant));

// WHEN
Restaurant actual = restaurantService.findRestaurantById("1");

//THEN
verify(mockRestaurantRepository).findById("1");

assertEquals(restaurant, actual);
}

@Test
void findRestaurantById_whenRestaurantDoesNotExist_thenThrowException() {
//GIVEN
when(mockRestaurantRepository.findById("1")).thenReturn(Optional.empty());

//THEN
assertThrowsExactly(NoSuchRestaurantException.class, () -> restaurantService.findRestaurantById("1"));

verify(mockRestaurantRepository).findById("1");
}


@Test
void addRestaurant_whenRestaurantToSave_thenReturnSavedRestaurantWithId() {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
--default-border-color: lightgrey;
--default-border-radius: 15px;
--header-height: 70px;
--default-color: #333333

--default-color: #333333;
--primary-color: #6200ea;
}

body {
Expand Down
13 changes: 6 additions & 7 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import RestaurantsPage from "./pages/RestaurantsPage.tsx";
import RestaurantDetailsPage from "./pages/RestaurantDetailsPage.tsx";
import {Route, Routes} from "react-router-dom";
import AddRestaurantsPage from "./pages/AddRestaurantsPage.tsx";
import './App.css'

function App() {

return (
<>
<Routes>
<Route path="/" element={<RestaurantsPage/>}/>
<Route path="/restaurants/add" element={<AddRestaurantsPage />}/>

</Routes>
</>
<Routes>
<Route path="/" element={<RestaurantsPage />}/>
<Route path="/restaurants/add" element={<AddRestaurantsPage />}/>
<Route path="/restaurants/:id" element={<RestaurantDetailsPage />}/>
</Routes>
)
}

Expand Down
21 changes: 21 additions & 0 deletions frontend/src/components/Button/Button.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import styled from "styled-components";
import {Link} from "react-router-dom";

export const StyledLink = styled(Link)`
display: inline-block;
padding: 10px 20px;
color: white;
background-color: var(--primary-color);
text-decoration: none;
border: none;
border-radius: 5px;
cursor: pointer;

&:hover {
background-color: color-mix(in srgb, var(--primary-color) 80%, white);
}

&:active {
background-color: color-mix(in srgb, var(--primary-color) 70%, black);
}
`;
15 changes: 15 additions & 0 deletions frontend/src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {ReactNode} from "react";
import {StyledLink} from "./Button.styled.ts";

type ButtonProps = {
children: ReactNode,
href: string,
}

export default function Button({children, href}: ButtonProps) {
return (
<StyledLink to={href}>
{children}
</StyledLink>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ import {render, screen} from '@testing-library/react';
import '@testing-library/jest-dom';
import RestaurantCard from "./RestaurantCard.tsx";
import {RestaurantType} from "../../model/Restaurant.ts";
import {MemoryRouter} from "react-router-dom";

test('RestaurantCard component displays title of the restaurant', () => {
const restaurant: RestaurantType = {id: "1", title: "Don Carlos", city: "Berlin"}
render(<RestaurantCard restaurant={restaurant}/>);
render(<MemoryRouter><RestaurantCard restaurant={restaurant}/></MemoryRouter>);
const restaurantTitle = screen.getByText(/Don Carlos/i);
expect(restaurantTitle).toBeInTheDocument();
});

test("RestaurantCard component displays the title of the restaurant as h2", () => {
const restaurant: RestaurantType = {id: "1", title: "Don Carlos", city: "Berlin"}
render(<RestaurantCard restaurant={restaurant}/>);
render(<MemoryRouter><RestaurantCard restaurant={restaurant}/></MemoryRouter>);
const restaurantTitle = screen.getByRole("heading", {
level: 2,
name: /don carlos/i
Expand All @@ -22,7 +23,7 @@ test("RestaurantCard component displays the title of the restaurant as h2", () =

test("RestaurantCard component displays the city", () => {
const restaurant: RestaurantType = {id: "1", title: "Don Carlos", city: "Berlin"}
render(<RestaurantCard restaurant={restaurant}/>);
render(<MemoryRouter><RestaurantCard restaurant={restaurant}/></MemoryRouter>);
const restaurantCity = screen.getByText(/Berlin/i);
expect(restaurantCity).toBeInTheDocument();
})
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ export const StyledDetailsContainer = styled.div`

export const StyledDetailsTitle = styled.h2`
margin-bottom: 5px;
`;
`;

5 changes: 4 additions & 1 deletion frontend/src/components/RestaurantCard/RestaurantCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import {
StyledDetailsContainer,
StyledDetailsTitle,
StyledFallbackImage,
StyledImageContainer
StyledImageContainer,
} from "./RestaurantCard.styled.ts";
import Button from "../Button/Button.tsx";

type RestaurantCardProps = {
restaurant: RestaurantType,
}

export default function RestaurantCard({restaurant}: RestaurantCardProps) {

return (
<StyledArticle>
<StyledImageContainer>
Expand All @@ -23,6 +25,7 @@ export default function RestaurantCard({restaurant}: RestaurantCardProps) {
<StyledDetailsContainer>
<StyledDetailsTitle>{restaurant.title}</StyledDetailsTitle>
<RestaurantCardDetail icon={<FaLocationDot/>} value={restaurant.city}/>
<Button href={`/restaurants/${restaurant.id}`}>Details</Button>
</StyledDetailsContainer>
</StyledArticle>
)
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/pages/RestaurantDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useEffect, useState } from "react";
import { useParams} from "react-router-dom";
import DefaultPageTemplate from "./templates/DefaultPageTemplate/DefaultPageTemplate.tsx";
import axios from "axios";
import { RestaurantType } from "../model/Restaurant.ts";

import Button from "../components/Button/Button.tsx";

export default function RestaurantDetailsPage() {
const { id } = useParams<{ id: string }>();
const [restaurant, setRestaurant] = useState<RestaurantType>();
const [error, setError] = useState<string | null>(null);

useEffect(() => {
axios.get(`/api/restaurants/${id}`)
.then(response => {
setRestaurant(response.data);
})
.catch(error => {
setError("There was an error fetching the restaurant details!");
console.error(error);
});
}, [id]);

if (error) {
return <DefaultPageTemplate>{error}</DefaultPageTemplate>;
}

if (!restaurant) {
return <DefaultPageTemplate>Loading...</DefaultPageTemplate>;
}

return (
<DefaultPageTemplate pageTitle={restaurant.title}>
<p>{restaurant.city}</p>
<Button href="/">Back</Button>
</DefaultPageTemplate>
);
}
Loading