diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..fe9b7e3 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,89 @@ +name: "Deploy App" + +on: + push: + branches: + - main + +jobs: + build-frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Build Frontend + working-directory: frontend + run: | + npm install + npm run build + + - uses: actions/upload-artifact@v4 + with: + name: frontend-build + path: frontend/dist/ + + build-backend: + runs-on: ubuntu-latest + needs: build-frontend + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: frontend-build + path: backend/src/main/resources/static + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '21' # must match the version in the pom.xml + distribution: 'temurin' + cache: 'maven' + + - name: Build with maven + run: mvn -B package --file backend/pom.xml + + - uses: actions/upload-artifact@v4 + with: + name: app.jar + path: backend/target/app.jar # must match the finalName in the pom.xml + + push-to-docker-hub: + runs-on: ubuntu-latest + needs: build-backend + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: app.jar + path: backend/target + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} # must match the name of the Dockerhub account + password: ${{ secrets.DOCKERHUB_PASSWORD }} # must match the password of the Dockerhub account + + - name: Build and push + uses: docker/build-push-action@v5 + with: + push: true + tags: ${{ secrets.DOCKERHUB_TAG }} # Example: username/project:latest + context: . + + deploy: + name: deploy-to-render + runs-on: ubuntu-latest + needs: push-to-docker-hub + environment: + name: Capstone Project # Capstone Project name + url: https://neuefische.de/ # Link to deployment + steps: + - name: Trigger Render.com Deployment + run: | + curl -X POST ${{ secrets.RENDER_DEPLOY }} #muss mit der url des Render Deployments übereinstimmen \ No newline at end of file diff --git a/backend/src/main/java/springweb/backend/Client.java b/backend/src/main/java/springweb/backend/Client.java new file mode 100644 index 0000000..626d221 --- /dev/null +++ b/backend/src/main/java/springweb/backend/Client.java @@ -0,0 +1,12 @@ +package springweb.backend; + +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.List; + +@Document("clients") +public record Client( + String id, + List shoppingList +) { +} diff --git a/backend/src/main/java/springweb/backend/ClientController.java b/backend/src/main/java/springweb/backend/ClientController.java new file mode 100644 index 0000000..fb55799 --- /dev/null +++ b/backend/src/main/java/springweb/backend/ClientController.java @@ -0,0 +1,54 @@ +package springweb.backend; + + +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.NoSuchElementException; + + +@RestController +@RequestMapping("/api/store/clients") +public class ClientController { + + private final ClientService clientService; + + public ClientController(ClientService clientService) { + this.clientService = clientService; + } + + @GetMapping + public List getAllClients() { + return clientService.getAllClients(); + } + + @GetMapping({"{id}"}) + public Client getClientByID(@PathVariable String id) { + return clientService.getClientById(id).orElseThrow(() -> new NoSuchElementException("Task not found")); + } + + @PostMapping("{id}/shoppingList") + public Client addGroceryProductToClient(@PathVariable String id, @RequestBody GroceryProduct groceryProduct) { + return clientService.addGroceryProductToClient(groceryProduct, id); + } + + @PostMapping + public Client addClient(@RequestBody Client client) { + return clientService.addClient(client); + } + + @PutMapping("/{id}") + public Client updateTask(@PathVariable String id, @RequestBody Client clientDto) { + return clientService.updateClient(id, clientDto); + } + + @DeleteMapping({"{id}"}) + public void deleteClientById(@PathVariable String id) { + clientService.deleteClientById(id); + } + + @DeleteMapping("{idClient}/shoppingList/{idProduct}") + public void deleteProductByIdFromClientById(@PathVariable String idClient, @PathVariable String idProduct) { + clientService.deleteProductByIdFromClientById(idClient,idProduct); + } +} diff --git a/backend/src/main/java/springweb/backend/ClientRepository.java b/backend/src/main/java/springweb/backend/ClientRepository.java new file mode 100644 index 0000000..89ec0ab --- /dev/null +++ b/backend/src/main/java/springweb/backend/ClientRepository.java @@ -0,0 +1,6 @@ +package springweb.backend; + +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface ClientRepository extends MongoRepository { +} diff --git a/backend/src/main/java/springweb/backend/ClientService.java b/backend/src/main/java/springweb/backend/ClientService.java new file mode 100644 index 0000000..43b1bb2 --- /dev/null +++ b/backend/src/main/java/springweb/backend/ClientService.java @@ -0,0 +1,93 @@ +package springweb.backend; + +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +public class ClientService { + private final ClientRepository clientRepository; + + public ClientService(ClientRepository clientRepository) { + this.clientRepository = clientRepository; + } + + public List getAllClients() { + return clientRepository.findAll(); + } + + public Optional getClientById(String id) { + return clientRepository.findById(id); + } + + public Client addGroceryProductToClient(GroceryProduct groceryProduct, String id) { + + if (clientRepository.existsById(id)) { + Client client = clientRepository.findById(id).get(); + client.shoppingList().add(groceryProduct); + return clientRepository.save(client); + } else { + throw new NoSuchElementException("No Client found with Id:" + id); + } + } + + public Client addClient(Client clientDto) { + Client client = new Client(clientDto.id(), clientDto.shoppingList()); + return clientRepository.save(client); + } + + public Client updateClient(String id, Client clientDto) { + if (clientRepository.existsById(id)) { + Client updatedClient = new Client(id, clientDto.shoppingList()); + return clientRepository.save(updatedClient); + } else { + throw new NoSuchElementException("No Client found with Id:" + id); + } + } + + public boolean deleteClientById(String id) { + if (clientRepository.existsById(id)) { + clientRepository.deleteById(id); + return true; + } else { + throw new NoSuchElementException("No Client found with Id:" + id); + } + } + + /* WIP + public List getAllGroceryProductsFromClient(String id) { + if (clientRepository.existsById(id)) { + return clientRepository.findById(id).get().shoppingList(); + } + throw new NoSuchElementException("No Client found with Id:" + id); + } + */ + + public void deleteProductByIdFromClientById(String idClient, String idProduct) { + if (clientRepository.existsById(idClient)) { + Client client = clientRepository.findById(idClient).get(); + boolean productExists = client.shoppingList() + .stream() + .anyMatch(product -> product.id().equals(idProduct)); + if (!client.shoppingList().isEmpty() && productExists) { + + List filteredList = client.shoppingList() + .stream() + .filter(product -> !product.id().equals(idProduct)) + .collect(Collectors.toList()); + + client.shoppingList().clear(); + client.shoppingList().addAll(filteredList); + + clientRepository.save(client); + } else { + throw new NoSuchElementException("No Product found with Id:" + idProduct); + } + } else { + throw new NoSuchElementException("No Client found with Id:" + idClient); + } + } +} diff --git a/backend/src/main/java/springweb/backend/GroceryController.java b/backend/src/main/java/springweb/backend/GroceryController.java index 3972ceb..78902c2 100644 --- a/backend/src/main/java/springweb/backend/GroceryController.java +++ b/backend/src/main/java/springweb/backend/GroceryController.java @@ -1,7 +1,6 @@ package springweb.backend; -import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -10,7 +9,7 @@ @RestController -@RequestMapping("/api/store") +@RequestMapping("/api/store/products") public class GroceryController { GroceryService groceryService; diff --git a/backend/src/main/java/springweb/backend/GroceryService.java b/backend/src/main/java/springweb/backend/GroceryService.java index 5926138..ffbdc5b 100644 --- a/backend/src/main/java/springweb/backend/GroceryService.java +++ b/backend/src/main/java/springweb/backend/GroceryService.java @@ -1,7 +1,5 @@ package springweb.backend; -import lombok.NoArgsConstructor; -import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; import java.util.List; @@ -18,4 +16,6 @@ public GroceryService(GroceryRepository groceryRepository) { public List getAllGroceryProducts() { return groceryRepository.findAll(); } + + } diff --git a/backend/src/test/java/springweb/backend/ClientServiceTest.java b/backend/src/test/java/springweb/backend/ClientServiceTest.java new file mode 100644 index 0000000..0db07e3 --- /dev/null +++ b/backend/src/test/java/springweb/backend/ClientServiceTest.java @@ -0,0 +1,58 @@ +package springweb.backend; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; + +import static com.mongodb.internal.connection.tlschannel.util.Util.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +public class ClientServiceTest { + + private final ClientRepository mockUserRepo = mock(ClientRepository.class); + + @Test + public void GetAllClients_Test() { + // GIVEN + GroceryProduct product = new GroceryProduct("1", "Fruit", "Apple", 0.99, "apple.jpg"); + List shoppingList1 = List.of(product); + Client expectedClient1 = new Client("123", shoppingList1); + + GroceryProduct product2 = new GroceryProduct("2", "Fruit", "Banana", 500, "banane.jpg"); + GroceryProduct product3 = new GroceryProduct("3", "Fruit", "Kiwi", 1.80, "ps5.jpg"); + List shoppingList2 = List.of(product2, product3); + Client expectedClient2 = new Client("456", shoppingList2); + + List clientList = List.of(expectedClient1, expectedClient2); + + when(mockUserRepo.findAll()).thenReturn(clientList); + ClientService clientService = new ClientService(mockUserRepo); + + // WHEN + List actual = clientService.getAllClients(); + + // THEN + verify(mockUserRepo).findAll(); + assertEquals(clientList, actual); + } + + @Test + public void GetClientById_Test() { + // GIVEN + GroceryProduct product = new GroceryProduct("1", "Fruit", "Apple", 0.99, "apple.jpg"); + List shoppingList = List.of(product); + Client expectedClient = new Client("123", shoppingList); + + when(mockUserRepo.findById("123")).thenReturn(Optional.of(expectedClient)); + ClientService clientService = new ClientService(mockUserRepo); + + // WHEN + Optional result = clientService.getClientById("123"); + + // THEN + verify(mockUserRepo).findById("123"); + assertTrue(result.isPresent()); + assertEquals(expectedClient, result.get()); + } +} \ No newline at end of file diff --git a/frontend/public/_GroceryDB.clients.json b/frontend/public/_GroceryDB.clients.json new file mode 100644 index 0000000..faa509d --- /dev/null +++ b/frontend/public/_GroceryDB.clients.json @@ -0,0 +1,40 @@ +[ + { + "_id": "1", + "shoppingList": [ + { + "_id": "1", + "category": "fruit", + "name": "apple", + "price": 1.5, + "image": "apple.jpg" + }, + { + "_id": "2", + "category": "fruit", + "name": "banana", + "price": 1, + "image": "banane.jpg" + } + ] + }, + { + "_id": "2", + "shoppingList": [ + { + "_id": "1", + "category": "fruit", + "name": "apple", + "price": 1.5, + "image": "apple.jpg" + }, + { + "_id": "2", + "category": "fruit", + "name": "banana", + "price": 1, + "image": "banane.jpg" + } + ] + } +] \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7a823f0..792bec4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import './App.css' import ProductView from "./components/ProductView.tsx"; +import ShoppingListView from "./components/ShoppingListView.tsx"; export default function App() { @@ -8,6 +9,7 @@ export default function App() { <>

Grocery

+ ) } diff --git a/frontend/src/Client.ts b/frontend/src/Client.ts new file mode 100644 index 0000000..4c4ed6c --- /dev/null +++ b/frontend/src/Client.ts @@ -0,0 +1,6 @@ +import {Product} from "./Product.ts"; + +export type Client ={ + id: string, + shoppingList: [Product], +} \ No newline at end of file diff --git a/frontend/src/components/ProductView.tsx b/frontend/src/components/ProductView.tsx index bec7273..170cbd7 100644 --- a/frontend/src/components/ProductView.tsx +++ b/frontend/src/components/ProductView.tsx @@ -11,7 +11,7 @@ export default function ProductView() { function fetchAllProducts() { axios({ method: "GET", - url: "api/store", + url: "api/store/products", }) .then((response) => { diff --git a/frontend/src/components/ShoppingListCard.css b/frontend/src/components/ShoppingListCard.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/ShoppingListCard.tsx b/frontend/src/components/ShoppingListCard.tsx new file mode 100644 index 0000000..f666f0b --- /dev/null +++ b/frontend/src/components/ShoppingListCard.tsx @@ -0,0 +1,15 @@ +import "./ShoppingListCard.css" +//import {Client} from "../Client.ts"; +import {Product} from "../Product.ts"; + +type Props = { + product: Product +} + +export function ShoppingListCard(props: Props) { + return ( +
+

{props.product.name}

+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/ShoppingListView.css b/frontend/src/components/ShoppingListView.css new file mode 100644 index 0000000..cbae032 --- /dev/null +++ b/frontend/src/components/ShoppingListView.css @@ -0,0 +1,11 @@ +.shoppingListView-container{ + font-family: Arial, sans-serif; + border: 10px solid hsl(100,10%, 80%); + border-radius: 50px; + background-color: pink; + width: 55% +} + +.shoppingList-container{ + +} \ No newline at end of file diff --git a/frontend/src/components/ShoppingListView.tsx b/frontend/src/components/ShoppingListView.tsx new file mode 100644 index 0000000..6ecdee8 --- /dev/null +++ b/frontend/src/components/ShoppingListView.tsx @@ -0,0 +1,34 @@ +import "./ShoppingListView.css" +import {useEffect, useState} from "react"; +import axios from "axios"; +import {Client} from "../Client.ts"; + +export default function ShoppingListView() { + + const [client, setClient] = useState() + + function fetchClients() { + axios({ + method: "GET", + url: "api/store/clients/1", + + }) + .then((response) => { + setClient(response.data) + }) + } + + useEffect(() => fetchClients(), []); + + if (!client) { + return "Lade..." + } + + return ( +
+

Shopping Cart

+

clients

+ {client.id} +
+ ); +}; \ No newline at end of file