From 8c3f80a2035c99e7fd61afffab3d0436085e6deb Mon Sep 17 00:00:00 2001 From: Roottoot Date: Sun, 21 Sep 2025 23:12:54 +0200 Subject: [PATCH 1/2] Add and remove from cart added --- .../controller/DiscountsController.java | 11 ++- .../org/example/backend/security/AppUser.java | 6 +- .../example/backend/security/AppUserDto.java | 4 +- .../backend/security/AuthController.java | 2 +- .../backend/security/CartItemTransferDto.java | 5 ++ .../security/CustomOAuth2UserService.java | 2 + .../security/CustomOidcUserService.java | 2 + .../backend/service/DiscountsService.java | 42 +++++++++- .../backend/security/AuthControllerTest.java | 9 +- .../backend/service/DiscountsServiceTest.java | 84 +++++++++++++++++++ frontend/index.html | 6 +- frontend/package-lock.json | 34 +++++--- frontend/package.json | 1 + frontend/public/BestDiscounts.svg | 8 ++ frontend/public/vite.svg | 1 - frontend/src/App.css | 72 ++++++++++++++-- frontend/src/App.tsx | 21 ++--- frontend/src/assets/react.svg | 1 - frontend/src/components/Card.tsx | 13 ++- frontend/src/components/Dashboard.tsx | 31 ++++--- frontend/src/components/DashbordCard.tsx | 47 +++++++++++ frontend/src/components/Login.tsx | 10 ++- frontend/src/components/Navbar.tsx | 22 ++++- frontend/src/components/SearchBar.tsx | 9 +- frontend/src/components/UserPref.tsx | 43 ++++++++++ frontend/src/components/UserPrefCard.tsx | 39 +++++++++ frontend/src/type/UserInfoType.ts | 5 +- 27 files changed, 459 insertions(+), 71 deletions(-) create mode 100644 backend/src/main/java/org/example/backend/security/CartItemTransferDto.java create mode 100644 backend/src/test/java/org/example/backend/service/DiscountsServiceTest.java create mode 100644 frontend/public/BestDiscounts.svg delete mode 100644 frontend/public/vite.svg delete mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/DashbordCard.tsx create mode 100644 frontend/src/components/UserPref.tsx create mode 100644 frontend/src/components/UserPrefCard.tsx diff --git a/backend/src/main/java/org/example/backend/controller/DiscountsController.java b/backend/src/main/java/org/example/backend/controller/DiscountsController.java index a2e1448..02a2759 100644 --- a/backend/src/main/java/org/example/backend/controller/DiscountsController.java +++ b/backend/src/main/java/org/example/backend/controller/DiscountsController.java @@ -2,10 +2,9 @@ import lombok.RequiredArgsConstructor; import org.example.backend.model.Discount; +import org.example.backend.security.CartItemTransferDto; import org.example.backend.service.DiscountsService; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -20,4 +19,10 @@ public class DiscountsController { public List getAllDiscounts(){ return service.getAllDiscounts(); } + + @PostMapping("/changeCart") + public List changeCart(@RequestBody CartItemTransferDto cartItem){ + System.out.println("Received item from frontend:" + cartItem); + return service.changeCart(cartItem); + } } diff --git a/backend/src/main/java/org/example/backend/security/AppUser.java b/backend/src/main/java/org/example/backend/security/AppUser.java index b8f0bad..d93438e 100644 --- a/backend/src/main/java/org/example/backend/security/AppUser.java +++ b/backend/src/main/java/org/example/backend/security/AppUser.java @@ -1,7 +1,11 @@ package org.example.backend.security; import lombok.Builder; +import lombok.With; +import java.util.List; + +@With @Builder -public record AppUser(String id,String providerId,String provider,String userName, String avatarUrl) { +public record AppUser(String id, String providerId, String provider, String userName, String avatarUrl, List shoppingCart) { } diff --git a/backend/src/main/java/org/example/backend/security/AppUserDto.java b/backend/src/main/java/org/example/backend/security/AppUserDto.java index 95fa09d..2ac8e30 100644 --- a/backend/src/main/java/org/example/backend/security/AppUserDto.java +++ b/backend/src/main/java/org/example/backend/security/AppUserDto.java @@ -3,7 +3,9 @@ import lombok.Builder; import lombok.With; +import java.util.List; + @With @Builder -public record AppUserDto(String id,String userName, String avatarUrl) { +public record AppUserDto(String id, String userName, String avatarUrl, List shoppingCart) { } diff --git a/backend/src/main/java/org/example/backend/security/AuthController.java b/backend/src/main/java/org/example/backend/security/AuthController.java index 97a26e3..169fa3e 100644 --- a/backend/src/main/java/org/example/backend/security/AuthController.java +++ b/backend/src/main/java/org/example/backend/security/AuthController.java @@ -16,6 +16,6 @@ public AppUserDto getMe(@AuthenticationPrincipal AppUserPrincipal user){ return null; } AppUser appUser=user.getAppUser(); - return new AppUserDto(appUser.id(),appUser.userName(),appUser.avatarUrl()); + return new AppUserDto(appUser.id(),appUser.userName(),appUser.avatarUrl(), appUser.shoppingCart()); } } diff --git a/backend/src/main/java/org/example/backend/security/CartItemTransferDto.java b/backend/src/main/java/org/example/backend/security/CartItemTransferDto.java new file mode 100644 index 0000000..025ad6d --- /dev/null +++ b/backend/src/main/java/org/example/backend/security/CartItemTransferDto.java @@ -0,0 +1,5 @@ +package org.example.backend.security; +public record CartItemTransferDto(String userId,String productId,Boolean addTrueFalseDelete) {} +//if add item is true item is added to the cart +// else item is removed from the cart +//(reason) duplicate code on add and delete \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/security/CustomOAuth2UserService.java b/backend/src/main/java/org/example/backend/security/CustomOAuth2UserService.java index d9b6f72..5905425 100644 --- a/backend/src/main/java/org/example/backend/security/CustomOAuth2UserService.java +++ b/backend/src/main/java/org/example/backend/security/CustomOAuth2UserService.java @@ -8,6 +8,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.Map; @Service @@ -65,6 +66,7 @@ private AppUser createAppUser(OAuth2User oAuth2User, String uniqueId, String pro .providerId(providerId) .userName(name) .avatarUrl(avatarUrl) + .shoppingCart(new ArrayList<>()) .build(); return userRepo.save(newUser); diff --git a/backend/src/main/java/org/example/backend/security/CustomOidcUserService.java b/backend/src/main/java/org/example/backend/security/CustomOidcUserService.java index d3b3bf0..d3c76a3 100644 --- a/backend/src/main/java/org/example/backend/security/CustomOidcUserService.java +++ b/backend/src/main/java/org/example/backend/security/CustomOidcUserService.java @@ -8,6 +8,7 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.Map; @Service @@ -45,6 +46,7 @@ private AppUser createAppUser(OidcUser oidcUser, String uniqueId, String provide .providerId(providerId) .userName(name) .avatarUrl(avatarUrl) + .shoppingCart(new ArrayList<>()) .build(); System.out.println("Saved user:" + newUser); diff --git a/backend/src/main/java/org/example/backend/service/DiscountsService.java b/backend/src/main/java/org/example/backend/service/DiscountsService.java index 3e91ee6..5348a06 100644 --- a/backend/src/main/java/org/example/backend/service/DiscountsService.java +++ b/backend/src/main/java/org/example/backend/service/DiscountsService.java @@ -2,22 +2,56 @@ import org.example.backend.model.Discount; import org.example.backend.repository.DiscountRepository; +import org.example.backend.security.AppUser; +import org.example.backend.security.AppUserRepository; +import org.example.backend.security.CartItemTransferDto; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; +import java.util.NoSuchElementException; @Service public class DiscountsService { private final DiscountRepository repo; - - public DiscountsService(DiscountRepository repo) { + private final AppUserRepository userRepo; + public DiscountsService(DiscountRepository repo, AppUserRepository userRepo) { this.repo = repo; + this.userRepo = userRepo; } public List getAllDiscounts() { return repo.findAll(); } - public List findByStore(String Provider) { - return repo.findDiscountByProvider(Provider); + + public List changeCart(CartItemTransferDto cartItem) { + AppUser user = userRepo.findById(cartItem.userId()) + .orElseThrow(() -> + new NoSuchElementException("User with id " + cartItem.userId() + " not found")); + List currentCart = user.shoppingCart(); + System.out.println("Current cart: " + currentCart); + if (currentCart == null) { + currentCart = new ArrayList<>(); + } + List updatedCart = new ArrayList<>(currentCart); + + if(cartItem.addTrueFalseDelete()){ + updatedCart.add(cartItem.productId()); + }else{ + updatedCart.remove(cartItem.productId()); + } + System.out.println("Updated cart: " + updatedCart); + + AppUser updatedUser = new AppUser( + user.id(), + user.providerId(), + user.provider(), + user.userName(), + user.avatarUrl(), + updatedCart + ); + userRepo.save(updatedUser); + System.out.println("Updated user: " + updatedUser); + return updatedUser.shoppingCart(); } } diff --git a/backend/src/test/java/org/example/backend/security/AuthControllerTest.java b/backend/src/test/java/org/example/backend/security/AuthControllerTest.java index 293d28a..8e1e912 100644 --- a/backend/src/test/java/org/example/backend/security/AuthControllerTest.java +++ b/backend/src/test/java/org/example/backend/security/AuthControllerTest.java @@ -7,6 +7,9 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import java.util.ArrayList; + import static org.mockito.Mockito.mock; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Login; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin; @@ -30,7 +33,8 @@ void getMe_whenLoggedInWithOidc_returnsAppUserDto() throws Exception { "123456789", "google", "George", - "George.jpg")) + "George.jpg", + new ArrayList<>())) ))) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value("google:123456789")) @@ -48,7 +52,8 @@ void getMe_whenLoggedInWithOAuth2_returnsAppUserDto() throws Exception { "123456789", "github", "Gerry", - "Gerry.jpg")) + "Gerry.jpg", + new ArrayList<>())) ))) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value("github:123456789")) diff --git a/backend/src/test/java/org/example/backend/service/DiscountsServiceTest.java b/backend/src/test/java/org/example/backend/service/DiscountsServiceTest.java new file mode 100644 index 0000000..ae4597e --- /dev/null +++ b/backend/src/test/java/org/example/backend/service/DiscountsServiceTest.java @@ -0,0 +1,84 @@ +package org.example.backend.service; + +import org.example.backend.model.Discount; +import org.example.backend.repository.DiscountRepository; +import org.example.backend.security.AppUser; +import org.example.backend.security.AppUserRepository; +import org.example.backend.security.CartItemTransferDto; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class DiscountsServiceTest { + + private final DiscountRepository repo = mock(DiscountRepository.class); + private final AppUserRepository userRepo = mock(AppUserRepository.class); + private final DiscountsService service = new DiscountsService(repo, userRepo); + + @Test + void getAllDiscounts_whenDiscountsExist_thenReturnListOfDiscounts() { + // GIVEN + Discount discount1 = new Discount("1", "Discount 1", "Description 1", "10","aldi"); + Discount discount2 = new Discount("2", "Discount 2", "Description 2", "20","lidl"); + List expected = List.of(discount1, discount2); + when(repo.findAll()).thenReturn(expected); + + // WHEN + List actual = service.getAllDiscounts(); + + // THEN + assertEquals(expected, actual); + verify(repo).findAll(); + } + + @Test + void changeCart_whenAddTrue_shouldAddToCart() { + // GIVEN + CartItemTransferDto cartItem = new CartItemTransferDto("user1", "product1", true); + AppUser user = new AppUser("user1", "providerId", "provider", "username", "avatarUrl", List.of()); + when(userRepo.findById("user1")).thenReturn(Optional.of(user)); + + // WHEN + List actual = service.changeCart(cartItem); + + // THEN + List expected = List.of("product1"); + assertEquals(expected, actual); + verify(userRepo).findById("user1"); + verify(userRepo).save(any(AppUser.class)); + } + + @Test + void changeCart_whenAddFalse_shouldRemoveFromCart() { + // GIVEN + CartItemTransferDto cartItem = new CartItemTransferDto("user1", "product1", false); + AppUser user = new AppUser("user1", "providerId", "provider", "username", "avatarUrl", List.of("product1", "product2")); + when(userRepo.findById("user1")).thenReturn(Optional.of(user)); + + // WHEN + List actual = service.changeCart(cartItem); + + // THEN + List expected = List.of("product2"); + assertEquals(expected, actual); + verify(userRepo).findById("user1"); + verify(userRepo).save(any(AppUser.class)); + } + + @Test + void changeCart_whenUserNotFound_shouldThrowNoSuchElementException() { + // GIVEN + CartItemTransferDto cartItem = new CartItemTransferDto("user1", "product1", true); + when(userRepo.findById("user1")).thenReturn(Optional.empty()); + + // WHEN & THEN + assertThrows(NoSuchElementException.class, () -> service.changeCart(cartItem)); + verify(userRepo).findById("user1"); + verify(userRepo, never()).save(any(AppUser.class)); + } +} \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index e4b78ea..2c2b2c4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,12 +2,12 @@ - + - Vite + React + TS + Best Discounts
- + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3ec277f..2f105cb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.11.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-icons": "^5.5.0", "react-router-dom": "^7.8.2" }, "devDependencies": { @@ -1790,9 +1791,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -3179,6 +3180,15 @@ "react": "^19.1.1" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -3394,14 +3404,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -3560,9 +3570,9 @@ } }, "node_modules/vite": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz", - "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.6.tgz", + "integrity": "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3571,7 +3581,7 @@ "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", - "tinyglobby": "^0.2.14" + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" diff --git a/frontend/package.json b/frontend/package.json index 90e5315..27ca13d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "axios": "^1.11.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-icons": "^5.5.0", "react-router-dom": "^7.8.2" }, "devDependencies": { diff --git a/frontend/public/BestDiscounts.svg b/frontend/public/BestDiscounts.svg new file mode 100644 index 0000000..9fffdaf --- /dev/null +++ b/frontend/public/BestDiscounts.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css index f6b2381..53fe362 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -5,24 +5,67 @@ text-align: center; } .app-container { - margin: 2rem; - padding: 2rem; + padding-top: 3rem; max-width: 100%; overflow-x: hidden; } nav { transition: top 0.3s; } +.navbar-avatar-comp{ + position: relative; + cursor: pointer; +} +.item-number{ + position: absolute; + top: -5px; + right: -5px; + background: red; + color: white; + border-radius: 50%; + padding: 2px 7px; + font-size: 12px; +} + +.search-bar { + position: relative; + display: flex; + align-items: center; +} + +.search-icon { + position: absolute; + left: 10px; /* Adjust as needed */ + color: #888; /* Icon color */ +} + +.search-input { + padding-left: 35px; /* Make space for the icon */ + width: 100%; /* Or a specific width */ +} + +.back-button{ + display:flex; + flex-flow:row; + justify-content:flex-end; + font-size: 3rem; +} .login-buttons{ display: grid; grid-template-columns: 1fr; justify-items: stretch; row-gap: 1rem; margin: 2rem; - .button{ - padding: 2rem; - } } + +.login-buttons button { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 1rem; +} + .card { display: flex; flex-direction: column; @@ -39,6 +82,22 @@ nav { .price{ color: red; } + button{ + background: #f3d200; + margin-bottom: 2rem; + } + .addedToCart{ + background: #02a602; + } + .removeFromCart{ + background: red; + } +} +.card-info{ + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; } .card-image { position: relative; @@ -64,6 +123,7 @@ nav { justify-items: center; /* Center cards within their grid cell */ } + nav { display: flex; gap: 1rem; @@ -135,7 +195,7 @@ button:hover{ } .search-bar{ .search-input{ - padding: 0.8em 1.5em; + padding: 0.8rem 1rem; border-radius: 2rem; border: 1px solid transparent; font-size: 1em; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f78d523..23f0799 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,19 +8,22 @@ import {Navbar} from "./components/Navbar.tsx"; import type {userInfoType} from "./type/UserInfoType.ts"; import type {DiscountInfoType} from "./type/DiscountInfoType.ts"; import Login from "./components/Login.tsx"; +import UserPref from "./components/UserPref.tsx"; + export default function App() { const [user,setUser]=useState(null) const [discounts,setDiscounts]=useState([]) const [filteredDiscounts,setFilteredDiscounts]=useState([]) - + const [shoppingCart,setShoppingCart]=useState([]) const loadUser =()=>{ axios.get("/api/auth/me") .then(response=>{ console.log(response.data); setUser(response.data); DbData(); + setShoppingCart(response.data.shoppingCart); }).catch(()=>setUser(null)) } const DbData = () => { @@ -32,24 +35,22 @@ export default function App() { } useEffect(() => { - loadUser() + loadUser(); }, []); return ( <>
- - {!user&& } + + {!user? : }> - }/> + }/> + }/> - }/> - {/*}>*/} - {/* }/>*/} - {/**/} + {/*}/>*/} - + }
) diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/components/Card.tsx b/frontend/src/components/Card.tsx index d503616..e763340 100644 --- a/frontend/src/components/Card.tsx +++ b/frontend/src/components/Card.tsx @@ -1,8 +1,13 @@ -import type {DiscountInfoType} from "../type/DiscountInfoType.ts"; - -export default function Card(props:Readonly) { +type CardProps={ + id: string, + image: string, + name: string, + price: string, + provider: string, +} +export default function Card(props:Readonly) { return ( -
+
{props.name}/

{props.name}

diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index 2e70d39..6bd6000 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,15 +1,17 @@ import type {userInfoType} from "../type/UserInfoType.ts"; import Login from "./Login.tsx"; import type {DiscountInfoType} from "../type/DiscountInfoType.ts"; -import Card from "./Card.tsx"; +import DashbordCard from "./DashbordCard.tsx"; type DashboardProps = { user: userInfoType | null, discounts: DiscountInfoType[], - filteredDiscounts: DiscountInfoType[] + filteredDiscounts: DiscountInfoType[], + shoppingCart:string[], + setShoppingCart:(shoppingCart:string[])=>void } -export default function Dashboard({user,discounts,filteredDiscounts}: Readonly) { +export default function Dashboard({user, discounts, filteredDiscounts,shoppingCart,setShoppingCart}: Readonly) { return (
@@ -20,24 +22,31 @@ export default function Dashboard({user,discounts,filteredDiscounts}: ReadonlyThis weeks discounts:
- {filteredDiscounts.length>0 ? + {filteredDiscounts.length > 0 ? filteredDiscounts.map( - (discount: DiscountInfoType)=>( - ( + )): - discounts.length>0&& + provider={discount.provider} + user={user} + shoppingCart={shoppingCart} + setShoppingCart={setShoppingCart} + />)) : + discounts.length > 0 && discounts.map( - (discount: DiscountInfoType)=>( - ( + )) + provider={discount.provider} + user={user} + shoppingCart={shoppingCart} + setShoppingCart={setShoppingCart}/>)) }
: diff --git a/frontend/src/components/DashbordCard.tsx b/frontend/src/components/DashbordCard.tsx new file mode 100644 index 0000000..35c7bf8 --- /dev/null +++ b/frontend/src/components/DashbordCard.tsx @@ -0,0 +1,47 @@ +import Card from "./Card.tsx"; +import axios from "axios"; +import type {userInfoType} from "../type/UserInfoType.ts"; +import { FaCheck } from "react-icons/fa"; +import { MdAddShoppingCart } from "react-icons/md"; +type DashbordCardProps={ + id: string, + image: string, + name: string, + price: string, + provider: string, + user:userInfoType, + shoppingCart:string[] + setShoppingCart:(shoppingCart:string[])=>void +} +export default function DashbordCard(props:Readonly) { + // const [cartItems,setCartItems]=useState(props.user.shoppingCart); + function postAddToCart (addedElementForPreRender:string) { + // adding the element early to render the button faster + props.setShoppingCart([... props.shoppingCart,addedElementForPreRender]); + // console.table([props.user.id,props.id]); + axios.post("/api/changeCart",{ + userId: props.user.id, + productId: props.id, + addTrueFalseDelete:true + }) + .then(response => { + console.log(response.data); + props.setShoppingCart(response.data); + }).catch(e => { + console.error(e); + // setting the value back in case it fails + props.setShoppingCart(props.shoppingCart.filter(id => id !== addedElementForPreRender)); + }) + } + + return ( +
+ + {props.shoppingCart.includes(props.id)? + + : + + } +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index efe7927..81f71fd 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx @@ -1,3 +1,6 @@ +import { FcGoogle } from "react-icons/fc"; +import { FaGithub,FaFacebook } from "react-icons/fa"; + function loginWithGithub() { const host: string = window.location.host === "localhost:5173" ? "http://localhost:8080" : @@ -21,11 +24,12 @@ function loginWithFacebook() { export default function Login() { return (
+

Welcome to Best Discounts

Please login

- - - + + +
); diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index c9e77ee..5e74a98 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -2,6 +2,8 @@ import type {userInfoType} from "../type/UserInfoType.ts"; import type {DiscountInfoType} from "../type/DiscountInfoType.ts"; import {useEffect, useState} from "react"; import SearchBar from "./SearchBar.tsx"; +import {useNavigate} from "react-router-dom"; +import { MdLogout } from "react-icons/md"; interface NavbarProps { @@ -10,11 +12,13 @@ interface NavbarProps { discounts: DiscountInfoType[], setDiscounts: (discounts: DiscountInfoType[]) => void, setFilteredDiscounts:(filteredDiscounts: DiscountInfoType[]) => void + shoppingCart:string[] } -export function Navbar({user, setUser, discounts, setFilteredDiscounts}: NavbarProps) { -//scroll animation +export function Navbar({user, setUser, discounts, setFilteredDiscounts,shoppingCart}: NavbarProps) { + const nav=useNavigate(); + //scroll animation const [prevScrollPosition, setPrevScrollPosition] = useState(0); const [visible, setVisible] = useState(true); useEffect(() => { @@ -39,9 +43,19 @@ export function Navbar({user, setUser, discounts, setFilteredDiscounts}: NavbarP return (
); diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx index 4073728..771bdab 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/components/SearchBar.tsx @@ -1,5 +1,5 @@ import type {DiscountInfoType} from "../type/DiscountInfoType.ts"; - +import { FaSearch } from "react-icons/fa"; type SearchBarProps = { discounts: DiscountInfoType[], setFilteredDiscounts: (filteredDiscounts: DiscountInfoType[]) => void @@ -18,8 +18,11 @@ export default function SearchBar({discounts, setFilteredDiscounts}: Readonly {discounts.length > 0 && ( - filterDiscounts(e.target.value) }/> + <> + + filterDiscounts(e.target.value) }/> + )}
); diff --git a/frontend/src/components/UserPref.tsx b/frontend/src/components/UserPref.tsx new file mode 100644 index 0000000..a3d51d4 --- /dev/null +++ b/frontend/src/components/UserPref.tsx @@ -0,0 +1,43 @@ +import {IoArrowBack} from "react-icons/io5"; +import type {userInfoType} from "../type/UserInfoType.ts"; +import type {DiscountInfoType} from "../type/DiscountInfoType.ts"; +import {useNavigate} from "react-router-dom"; +import UserPrefCard from "./UserPrefCard.tsx"; + +type UserPrefProps = { + user: userInfoType, + discounts: DiscountInfoType[], + shoppingCart: string[] + setShoppingCart:(shoppingCart:string[])=>void +} + +export default function UserPref({user, discounts, shoppingCart,setShoppingCart}: Readonly) { + const nav = useNavigate(); + const userSavedDiscounts = discounts.filter(discount => shoppingCart.includes(discount.id)); + + return ( +
+ nav("/")}/> +
+

Hello {user.userName}

+ {user.userName}/ +

Your saved discounts:

+
+
+ {userSavedDiscounts.length > 0 ? + userSavedDiscounts.map( + (discount: DiscountInfoType) => ( + )) :

You have no saved discounts yet.

+ } +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/UserPrefCard.tsx b/frontend/src/components/UserPrefCard.tsx new file mode 100644 index 0000000..6778b9f --- /dev/null +++ b/frontend/src/components/UserPrefCard.tsx @@ -0,0 +1,39 @@ +import Card from "./Card.tsx"; +import axios from "axios"; +import type {userInfoType} from "../type/UserInfoType.ts"; +import { MdRemoveShoppingCart } from "react-icons/md"; + +type UserPrefCardProps={ + id: string, + image: string, + name: string, + price: string, + provider: string, + user:userInfoType + shoppingCart:string[] + setShoppingCart:(shoppingCart:string[])=>void +} +export default function UserPrefCard(props:Readonly) { + function removeFromCart(removedElementForPreRender:string) { + props.setShoppingCart(props.shoppingCart.filter(id => id !== removedElementForPreRender)); + // console.table([props.user.id,props.id]); + axios.post("/api/changeCart",{ + userId: props.user.id, + productId: props.id, + addTrueFalseDelete:false + }) + .then(response => { + console.log(response.data); + props.setShoppingCart(response.data); + }).catch(e => { + console.error(e); + props.setShoppingCart([... props.shoppingCart,removedElementForPreRender]); + }) + } + return ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/type/UserInfoType.ts b/frontend/src/type/UserInfoType.ts index 0edb414..14e8541 100644 --- a/frontend/src/type/UserInfoType.ts +++ b/frontend/src/type/UserInfoType.ts @@ -1,5 +1,8 @@ +// import type {DiscountInfoType} from "./DiscountInfoType.ts"; + export type userInfoType={ id:string, userName: string, - avatarUrl: string + avatarUrl: string, + shoppingCart:string[] } \ No newline at end of file From 3de1c5ef6c0dc132ccc95d91e082b99729d1b2ba Mon Sep 17 00:00:00 2001 From: Roottoot Date: Sun, 21 Sep 2025 23:32:53 +0200 Subject: [PATCH 2/2] added onKeyDown because: Visible, non-interactive elements with click handlers must have at least one keyboard listener. --- frontend/src/components/Navbar.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 5e74a98..5d0185b 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -32,6 +32,16 @@ export function Navbar({user, setUser, discounts, setFilteredDiscounts,shoppingC return () => window.removeEventListener("scroll", handleScroll); }, [prevScrollPosition]); + function navigateToUserPref() { + nav("/userPref"); + } + + function handleKeyDown(event: React.KeyboardEvent) { + if (event.key === 'Enter' || event.key === ' ') { + navigateToUserPref(); + } + } + function logout() { const host: string = window.location.host === "localhost:5173" ? "http://localhost:8080" : @@ -45,7 +55,7 @@ export function Navbar({user, setUser, discounts, setFilteredDiscounts,shoppingC