diff --git a/backend/src/main/java/com/github/esgoet/backend/controllers/BookController.java b/backend/src/main/java/com/github/esgoet/backend/controllers/BookController.java index 9dee9be..0f72b10 100644 --- a/backend/src/main/java/com/github/esgoet/backend/controllers/BookController.java +++ b/backend/src/main/java/com/github/esgoet/backend/controllers/BookController.java @@ -1,5 +1,6 @@ package com.github.esgoet.backend.controllers; +import com.github.esgoet.backend.dto.NewBookDto; import com.github.esgoet.backend.models.Book; import com.github.esgoet.backend.services.BookService; import lombok.RequiredArgsConstructor; @@ -10,7 +11,6 @@ import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.NoSuchElementException; @RestController @RequestMapping("/api/books") @@ -34,4 +34,9 @@ public void deleteBook(@PathVariable String id) { public Book getBook(@PathVariable String id) { return bookService.getBook(id); } + + @PostMapping + public Book addABook(@RequestBody NewBookDto newBookDto) { + return bookService.saveBook(newBookDto); + } } diff --git a/backend/src/main/java/com/github/esgoet/backend/dto/NewBookDto.java b/backend/src/main/java/com/github/esgoet/backend/dto/NewBookDto.java new file mode 100644 index 0000000..1a462a2 --- /dev/null +++ b/backend/src/main/java/com/github/esgoet/backend/dto/NewBookDto.java @@ -0,0 +1,15 @@ +package com.github.esgoet.backend.dto; + +import com.github.esgoet.backend.models.Genre; +import lombok.With; + +import java.time.LocalDate; + +@With +public record NewBookDto( + String author, + String title, + Genre genre, + LocalDate publicationDate +) { +} diff --git a/backend/src/main/java/com/github/esgoet/backend/models/Book.java b/backend/src/main/java/com/github/esgoet/backend/models/Book.java index ff5d770..f5f1129 100644 --- a/backend/src/main/java/com/github/esgoet/backend/models/Book.java +++ b/backend/src/main/java/com/github/esgoet/backend/models/Book.java @@ -3,10 +3,15 @@ import lombok.With; import org.springframework.data.mongodb.core.mapping.Document; +import java.time.LocalDate; + @With @Document("books") public record Book( String id, String author, - String title) { + String title, + Genre genre, + LocalDate publicationDate +) { } diff --git a/backend/src/main/java/com/github/esgoet/backend/models/Genre.java b/backend/src/main/java/com/github/esgoet/backend/models/Genre.java new file mode 100644 index 0000000..dfb1ae5 --- /dev/null +++ b/backend/src/main/java/com/github/esgoet/backend/models/Genre.java @@ -0,0 +1,19 @@ +package com.github.esgoet.backend.models; + +public enum Genre { + NONE("none"), + FICTION("Fiction"), + MYSTERY("Mystery"), + THRILLER("Thriller"), + FANTASY("Fantasy"), + SCIENCE("Science"), + NON_FICTION("Non-fiction"), + HISTORY("History"), + NOVEL("Novel"); + + private final String genreValue; + + Genre(String genreValue) { this.genreValue = genreValue; } + + public String getGenre() { return genreValue; } +} diff --git a/backend/src/main/java/com/github/esgoet/backend/services/BookService.java b/backend/src/main/java/com/github/esgoet/backend/services/BookService.java index f41f02f..2f47cf1 100644 --- a/backend/src/main/java/com/github/esgoet/backend/services/BookService.java +++ b/backend/src/main/java/com/github/esgoet/backend/services/BookService.java @@ -1,12 +1,12 @@ package com.github.esgoet.backend.services; +import com.github.esgoet.backend.dto.NewBookDto; import com.github.esgoet.backend.models.Book; import com.github.esgoet.backend.models.BookNotFoundException; import com.github.esgoet.backend.repositories.BookRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; - import java.util.List; @@ -15,6 +15,7 @@ public class BookService { private final BookRepository bookRepository; + private final IdService idService; public List getAllBooks() { return bookRepository.findAll(); @@ -28,4 +29,15 @@ public Book getBook(String id) { return bookRepository.findById(id) .orElseThrow(() -> new BookNotFoundException("No book found with id: " + id)); } + + public Book saveBook(NewBookDto newBookDto) { + Book bookToSave = new Book( + idService.randomId(), + newBookDto.author(), + newBookDto.title(), + newBookDto.genre(), + newBookDto.publicationDate() + ); + return bookRepository.save(bookToSave); + } } diff --git a/backend/src/main/java/com/github/esgoet/backend/services/IdService.java b/backend/src/main/java/com/github/esgoet/backend/services/IdService.java new file mode 100644 index 0000000..7caf4db --- /dev/null +++ b/backend/src/main/java/com/github/esgoet/backend/services/IdService.java @@ -0,0 +1,15 @@ +package com.github.esgoet.backend.services; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class IdService { + + public String randomId() { + return UUID.randomUUID().toString(); + } +} diff --git a/backend/src/test/java/com/github/esgoet/backend/controllers/BookControllerIntegrationTest.java b/backend/src/test/java/com/github/esgoet/backend/controllers/BookControllerIntegrationTest.java index 1bf5bfb..db0864e 100644 --- a/backend/src/test/java/com/github/esgoet/backend/controllers/BookControllerIntegrationTest.java +++ b/backend/src/test/java/com/github/esgoet/backend/controllers/BookControllerIntegrationTest.java @@ -1,14 +1,19 @@ package com.github.esgoet.backend.controllers; import com.github.esgoet.backend.models.Book; +import com.github.esgoet.backend.models.Genre; import com.github.esgoet.backend.repositories.BookRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; +import java.time.LocalDate; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @@ -21,6 +26,8 @@ class BookControllerIntegrationTest { @Autowired BookRepository bookRepository; + private final LocalDate localDate = LocalDate.parse("2024-08-14"); + @Test public void getAllBooks_Test_When_DbEmpty_Then_returnEmptyArray() throws Exception { @@ -34,7 +41,7 @@ public void getAllBooks_Test_When_DbEmpty_Then_returnEmptyArray() throws Excepti @Test void getBook_Test_whenIdExists() throws Exception { //GIVEN - bookRepository.save(new Book("1","George Orwell", "1984")); + bookRepository.save(new Book("1","George Orwell", "1984", Genre.FANTASY, localDate)); //WHEN mockMvc.perform(get("/api/books/1")) //THEN @@ -43,11 +50,38 @@ void getBook_Test_whenIdExists() throws Exception { { "id": "1", "author": "George Orwell", - "title": "1984" + "title": "1984", + "genre": "FANTASY", + "publicationDate": "2024-08-14" } """)); } + @Test + @DirtiesContext + void addABookTest_whenNewTodoExists_thenReturnNewTodo() throws Exception { + // GIVEN + + // WHEN + mockMvc.perform(post("/api/books") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "author": "Tolstoy", + "title": "War and Peace", + "genre": "HISTORY", + "publicationDate": "1869-01-01" + } + """)) + // THEN + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").exists()) + .andExpect(jsonPath("$.author").value("Tolstoy")) + .andExpect(jsonPath("$.title").value("War and Peace")) + .andExpect(jsonPath("$.genre").value("HISTORY")) + .andExpect(jsonPath("$.publicationDate").value("1869-01-01")); + } + @DirtiesContext @Test void getBook_Test_whenIdDoesNotExists() throws Exception { @@ -67,7 +101,7 @@ void getBook_Test_whenIdDoesNotExists() throws Exception { @Test public void deleteBook() throws Exception { - bookRepository.save(new Book("1", "Simon", "HowToDeleteBooksFast")); + bookRepository.save(new Book("1", "Simon", "HowToDeleteBooksFast", Genre.SCIENCE,localDate)); mockMvc.perform(delete("/api/books/1")) .andExpect(status().isOk()); diff --git a/backend/src/test/java/com/github/esgoet/backend/services/BookServiceUnitTest.java b/backend/src/test/java/com/github/esgoet/backend/services/BookServiceUnitTest.java index 53314cf..fb4ed49 100644 --- a/backend/src/test/java/com/github/esgoet/backend/services/BookServiceUnitTest.java +++ b/backend/src/test/java/com/github/esgoet/backend/services/BookServiceUnitTest.java @@ -1,11 +1,12 @@ package com.github.esgoet.backend.services; +import com.github.esgoet.backend.dto.NewBookDto; import com.github.esgoet.backend.models.Book; import com.github.esgoet.backend.models.BookNotFoundException; +import com.github.esgoet.backend.models.Genre; import com.github.esgoet.backend.repositories.BookRepository; import org.junit.jupiter.api.Test; - - +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -17,18 +18,20 @@ class BookServiceUnitTest { private final BookRepository bookRepo = mock(BookRepository.class); - private final BookService bookService = new BookService(bookRepo); + private final IdService idService = mock(IdService.class); + private final BookService bookService = new BookService(bookRepo, idService); + private final LocalDate localDate = LocalDate.parse("2024-08-14"); @Test void getAllBooks_Test() { List allBooks = List.of( - new Book("1", "Simon", "Java for Dummies"), - new Book("2","Florian", "Java not for Dummies") + new Book("1", "Simon", "Java for Dummies", Genre.SCIENCE, localDate), + new Book("2","Florian", "Java not for Dummies", Genre.SCIENCE, localDate) ); List expectedBooks = List.of( - new Book("1", "Simon", "Java for Dummies"), - new Book("2","Florian", "Java not for Dummies") + new Book("1", "Simon", "Java for Dummies", Genre.SCIENCE, localDate), + new Book("2","Florian", "Java not for Dummies", Genre.SCIENCE, localDate) ); when(bookRepo.findAll()).thenReturn(allBooks); @@ -49,12 +52,12 @@ void getAllBooks_WhenEmpty_ReturnsEmptyList() { @Test void getBook_Test_whenBookExists_thenReturnBook() { //GIVEN - Book book = new Book("1", "George Orwell", "1984"); + Book book = new Book("1", "George Orwell", "1984", Genre.FANTASY, localDate); when(bookRepo.findById("1")).thenReturn(Optional.of(book)); //WHEN Book actual = bookService.getBook("1"); //THEN - Book expected = new Book("1", "George Orwell", "1984"); + Book expected = new Book("1", "George Orwell", "1984", Genre.FANTASY, localDate); verify(bookRepo).findById("1"); assertEquals(expected, actual); } @@ -69,7 +72,23 @@ void getBook_Test_whenBookDoesNotExists_thenThrow() { verify(bookRepo).findById("1"); } - + @Test + void addABookTest_whenNewBookAsInput_thenReturnNewBook() { + // GIVEN + NewBookDto newBookDto = new NewBookDto("J. K. Rowling", "Harry Potter", Genre.FANTASY, localDate); + Book bookToSave = new Book("1", newBookDto.author(), newBookDto.title(), newBookDto.genre(), newBookDto.publicationDate()); + when(bookRepo.save(bookToSave)).thenReturn(bookToSave); + when(idService.randomId()).thenReturn(bookToSave.id()); + + // WHEN + Book actual = bookService.saveBook(newBookDto); + + // THEN + Book expected = new Book("1", newBookDto.author(), newBookDto.title(), newBookDto.genre(), newBookDto.publicationDate()); + verify(bookRepo).save(bookToSave); + verify(idService).randomId(); + assertEquals(expected, actual); + } @Test void deleteBook_Test() { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1314af7..24ae13e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,7 @@ import {useEffect, useState} from "react"; import {Link, Route, Routes} from "react-router-dom"; import BookDetailsPage from "./pages/BookDetailsPage/bookDetailsPage/BookDetailsPage.tsx"; import BookGalleryPage from "./pages/BookGalleryPage/bookGalleryPage/BookGalleryPage.tsx"; +import AddBookForm from "./pages/BookGalleryPage/components/addBookButton/AddBookForm.tsx"; import Header from "./components/header/Header.tsx"; @@ -41,6 +42,7 @@ function App() { All Books }/> + }/> }/> diff --git a/frontend/src/pages/BookGalleryPage/bookGalleryPage/BookGalleryPage.tsx b/frontend/src/pages/BookGalleryPage/bookGalleryPage/BookGalleryPage.tsx index e63e769..b3c65d5 100644 --- a/frontend/src/pages/BookGalleryPage/bookGalleryPage/BookGalleryPage.tsx +++ b/frontend/src/pages/BookGalleryPage/bookGalleryPage/BookGalleryPage.tsx @@ -1,6 +1,7 @@ import {Book} from "../../../types/types.ts"; import BookGallery from "../components/bookGallery/BookGallery.tsx"; import "./BookGalleryPage.css"; +import {Link} from "react-router-dom"; type BookGalleryPageProps = { data: Book[] @@ -8,6 +9,7 @@ type BookGalleryPageProps = { export default function BookGalleryPage({data}: BookGalleryPageProps) { return ( <> + Add a Book ); diff --git a/frontend/src/pages/BookGalleryPage/components/addBookButton/AddBookForm.tsx b/frontend/src/pages/BookGalleryPage/components/addBookButton/AddBookForm.tsx new file mode 100644 index 0000000..85de4cc --- /dev/null +++ b/frontend/src/pages/BookGalleryPage/components/addBookButton/AddBookForm.tsx @@ -0,0 +1,87 @@ +import {ChangeEvent, FormEvent, useState} from "react"; +import {Genre, NewBook} from "../../../../types/types.ts"; +import axios from "axios"; +import {useNavigate} from "react-router-dom"; + +type FetchProps = { + fetchBooks: () => void; +} + +export default function AddBookForm({ fetchBooks }: FetchProps) { + + const [book, setBook] = useState({title: "", author: "", genre: "", publicationDate: ""}); + const navigate = useNavigate(); + + const genres: Genre = { + NONE: "None", + FICTION: "Fiction", + MYSTERY: "Mystery", + THRILLER: "Thriller", + FANTASY: "Fantasy", + SCIENCE: "Science", + NON_FICTION: "Non-fiction", + HISTORY: "History", + NOVEL: "Novel" + } + + function handleChange(event: ChangeEvent | ChangeEvent): void { + setBook({...book, [event.target.name]: event.target.value}) + } + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + + console.log(book); + console.log(event); + + axios.post("/api/books", { + title: book.title, + author: book.author, + genre: Object.keys(genres).find( + key => genres[key as keyof typeof genres] === book.genre), + publicationDate: book.publicationDate + }) + .then(() => fetchBooks()) + .then(response => console.log(response)) + .catch(error => console.log(error)) + + navigate("/books") + } + + return ( +
+ + + + + + + + + +
+ ) +} \ No newline at end of file diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts index 6f94988..4b3172b 100644 --- a/frontend/src/types/types.ts +++ b/frontend/src/types/types.ts @@ -3,3 +3,22 @@ export type Book = { author: string, title: string } + +export type NewBook = { + title: string, + author: string, + genre: string, + publicationDate: string +} + +export type Genre = { + NONE: string, + FICTION: string, + MYSTERY: string, + THRILLER: string, + FANTASY: string, + SCIENCE: string, + NON_FICTION: string, + HISTORY: string, + NOVEL: string +}