From 316d43c6872bf0db73fac086b5ace98ef58f8f83 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 24 Mar 2026 13:30:26 +0100 Subject: [PATCH 01/33] feat[utils]: Add ParameterValidator class increasing SoC and SRP --- .../group5/app/utils/ParameterValidator.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/main/java/edu/group5/app/utils/ParameterValidator.java diff --git a/src/main/java/edu/group5/app/utils/ParameterValidator.java b/src/main/java/edu/group5/app/utils/ParameterValidator.java new file mode 100644 index 0000000..1181e11 --- /dev/null +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -0,0 +1,72 @@ +package edu.group5.app.utils; + +import java.math.BigDecimal; +/** + * ParameterValidator is a utility class that provides static methods for validating various types of parameters. + * It includes methods for checking strings, integers, objects, and BigDecimal values to ensure they meet specific + * criteria such as not being null, not being blank, or being positive. + * + */ +public final class ParameterValidator { + + /** + * Validates that a string parameter is not null and not blank. + * @param stringArg the string parameter to validate + * @param variableName the name of the variable being validated, used in exception messages + * @throws IllegalArgumentException if the string is null or blank + */ + public static final void stringChecker(String stringArg, String variableName) throws IllegalArgumentException { + nullCheck(stringArg, variableName); + if (stringArg.isBlank()) { + throw new IllegalArgumentException(String.format("%s can't be blank", variableName)); + } + } + + /** + * Validates that an integer parameter is not null and is a positive integer. + * @param intArg the integer parameter to validate + * @param variableName the name of the variable being validated, used in exception messages + * @throws IllegalArgumentException if the integer is null or not a positive integer + */ + public static final void intChecker(int intArg, String variableName) throws IllegalArgumentException { + nullCheck(intArg, variableName); + if (intArg <= 0) { + throw new IllegalArgumentException(String.format("%s must be a positive integer", variableName)); + } + } + + /** + * Validates that an object parameter is not null. + * @param objectArg the object parameter to validate + * @param variableName the name of the variable being validated, used in exception messages + * @throws IllegalArgumentException if the object is null + */ + public static final void objectChecker(Object objectArg, String variableName) throws IllegalArgumentException { + nullCheck(objectArg, variableName); + } + + /** + * Validates that a BigDecimal parameter is not null and is greater than zero. + * @param bigDecimalArg the BigDecimal parameter to validate + * @param variableName the name of the variable being validated, used in exception messages + * @throws IllegalArgumentException if the BigDecimal is null or not greater than zero + */ + public static final void bigDecimalChecker(BigDecimal bigDecimalArg, String variableName) throws IllegalArgumentException { + nullCheck(bigDecimalArg, variableName); + if (bigDecimalArg.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException(String.format("%s must be larger than 0", variableName)); + } + } + + /** + * Helper method to check if a variable is null and throw an IllegalArgumentException with a formatted message if it is. + * @param variable the variable to check for null + * @param variableName the name of the variable being checked, used in the exception message + * @throws IllegalArgumentException if the variable is null + */ + private static final void nullCheck(Object variable, String variableName) throws IllegalArgumentException { + if (variable == null) { + throw new IllegalArgumentException(String.format("%s can't be null", variableName)); + } + } +} From 218a32f63920d0b73cc51c117ef8af169ada50b3 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 24 Mar 2026 14:51:39 +0100 Subject: [PATCH 02/33] update&test[utils]: Add positive and negative JUnit tests to ParameterValidator class, ensuring full test coverage --- .../group5/app/utils/ParameterValidator.java | 1 - .../app/utils/ParameterValidatorTest.java | 63 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/test/java/edu/group5/app/utils/ParameterValidatorTest.java diff --git a/src/main/java/edu/group5/app/utils/ParameterValidator.java b/src/main/java/edu/group5/app/utils/ParameterValidator.java index 1181e11..1a44e46 100644 --- a/src/main/java/edu/group5/app/utils/ParameterValidator.java +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -29,7 +29,6 @@ public static final void stringChecker(String stringArg, String variableName) th * @throws IllegalArgumentException if the integer is null or not a positive integer */ public static final void intChecker(int intArg, String variableName) throws IllegalArgumentException { - nullCheck(intArg, variableName); if (intArg <= 0) { throw new IllegalArgumentException(String.format("%s must be a positive integer", variableName)); } diff --git a/src/test/java/edu/group5/app/utils/ParameterValidatorTest.java b/src/test/java/edu/group5/app/utils/ParameterValidatorTest.java new file mode 100644 index 0000000..bf8ac0c --- /dev/null +++ b/src/test/java/edu/group5/app/utils/ParameterValidatorTest.java @@ -0,0 +1,63 @@ +package edu.group5.app.utils; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ParameterValidatorTest { + + @Test + void testValidatorDoesNotThrowWithValidParameters() { + assertDoesNotThrow(() -> ParameterValidator.stringChecker("valid", "validString")); + assertDoesNotThrow(() -> ParameterValidator.intChecker(1, "positiveInt")); + assertDoesNotThrow(() -> ParameterValidator.objectChecker(new Object(), "validObject")); + assertDoesNotThrow(() -> ParameterValidator.bigDecimalChecker(java.math.BigDecimal.valueOf(1), "positiveBigDecimal")); + } + + @Test + void testValidatorThrowsWithStringChecker() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.stringChecker(null, "nullString"); + }); + IllegalArgumentException exception2 = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.stringChecker("", "emptyString"); + }); + IllegalArgumentException exception3 = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.stringChecker(" ", "blankString"); + }); + assertEquals("nullString can't be null", exception.getMessage()); + assertEquals("emptyString can't be blank", exception2.getMessage()); + assertEquals("blankString can't be blank", exception3.getMessage()); + } + + @Test + void testValidatorThrowsWithIntChecker() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.intChecker(-1, "negativeInt"); + }); + assertEquals("negativeInt must be a positive integer", exception.getMessage()); + } + + @Test + void testValidatorThrowsWithObjectChecker() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.objectChecker(null, "nullObject"); + }); + assertEquals("nullObject can't be null", exception.getMessage()); + } + + @Test + void testValidatorThrowsWithBigDecimalChecker() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.bigDecimalChecker(null, "nullBigDecimal"); + }); + IllegalArgumentException exception2 = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.bigDecimalChecker(java.math.BigDecimal.valueOf(-1), "negativeBigDecimal"); + }); + IllegalArgumentException exception3 = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.bigDecimalChecker(java.math.BigDecimal.ZERO, "zeroBigDecimal"); + }); + assertEquals("nullBigDecimal can't be null", exception.getMessage()); + assertEquals("negativeBigDecimal must be larger than 0", exception2.getMessage()); + assertEquals("zeroBigDecimal must be larger than 0", exception3.getMessage()); + } +} From 2f11b6852366b89c4756bfc34217b28fb092fb21 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Sun, 5 Apr 2026 14:38:52 +0200 Subject: [PATCH 03/33] update: update speed of rendering OrgCards and Org image with ParallelStream --- .../organization/OrganizationService.java | 33 ++++++------- .../app/view/causespage/CausesPageView.java | 49 ++++++++++++------- .../app/view/causespage/OrganizationCard.java | 43 ++++++++++++++-- .../OrganizationPageView.java | 2 + 4 files changed, 88 insertions(+), 39 deletions(-) diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java index 9785040..8ebd625 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -3,6 +3,7 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; +import java.util.stream.Collectors; import java.util.HashMap; import java.util.Map; @@ -90,7 +91,9 @@ public String fetchLogoUrl(String pageUrl) { } try { - Document doc = Jsoup.connect(pageUrl).get(); + Document doc = Jsoup.connect(pageUrl) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .timeout(5000).get(); Element img = doc.selectFirst("div.logo img"); if (img != null) { @@ -116,23 +119,17 @@ public String fetchLogoUrl(String pageUrl) { */ public Map getTrustedOrganizationsWithLogos() { Map original = getTrustedOrganizations(); - Map trustedOrgsWithLogos = new HashMap<>(); - - for (Organization org : original.values()) { - String logoUrl = fetchLogoUrl(org.websiteUrl()); - - Organization newOrg = new Organization( - org.orgNumber(), - org.name(), - org.trusted(), - org.websiteUrl(), - org.isPreApproved(), - org.description(), - logoUrl - ); - trustedOrgsWithLogos.put(newOrg.orgNumber(), newOrg); - } - return trustedOrgsWithLogos; + return original.values().parallelStream() + .map(org -> new Organization( + org.orgNumber(), + org.name(), + org.trusted(), + org.websiteUrl(), + org.isPreApproved(), + org.description(), + fetchLogoUrl(org.websiteUrl()) + )) + .collect(Collectors.toMap(Organization::orgNumber, org -> org)); } /** diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 1cd6e83..5cfc197 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -41,11 +41,41 @@ private ScrollPane createBody() { vBox.setStyle("-fx-padding: 10;"); vBox.setSpacing(10); vBox.setMaxWidth(Double.MAX_VALUE); + + // Load organizations INSTANTLY from cache + allOrganizations = orgController.getTrustedOrgs(); + vBox.getChildren().addAll( createSearchSection(), createOrganizationSection(null) ); body.setContent(vBox); + + // Build a map of org ID -> card for quick lookup + Map cardMap = new HashMap<>(); + for (var node : organizationGrid.getChildren()) { + if (node instanceof OrganizationCard card) { + cardMap.put(card.getOrganization().orgNumber(), card); + } + } + + // Fetch logos and update existing cards (don't rebuild grid) + orgController.getOrganizationsWithLogosAsync() + .thenAccept(orgs -> {this.allOrganizations = orgs; + Platform.runLater(() -> { + for (var entry : orgs.entrySet()) { + OrganizationCard card = cardMap.get(entry.getKey()); + if (card != null && entry.getValue().logoUrl() != null) { + card.updateLogo(entry.getValue().logoUrl()); + } + } + Organization currentOrg = appState.getCurrentOrganization(); + if (currentOrg != null && orgs.containsKey(currentOrg.orgNumber())) { + appState.setCurrentOrganization(orgs.get(currentOrg.orgNumber())); + } + }); + }); + return body; } @@ -76,25 +106,8 @@ private GridPane createOrganizationSection(String searchTerm) { organizationGrid = grid; } - if (allOrganizations == null) { - allOrganizations = orgController.getTrustedOrgs(); - - //Show loading text while organizations and logos are fetched - grid.add(new javafx.scene.control.Label("Loading..."), 0, 0); - - //Fetch trusted organizations with logos asynchronously (runs in background) - orgController.getOrganizationsWithLogosAsync() - .thenAccept(orgs -> { - this.allOrganizations = orgs; - - // Update UI when data is ready - Platform.runLater(() -> updateOrganizationGrid("")); - }); - return grid; - } - Map organizations = new HashMap<>(); - if (searchTerm != null) { + if (searchTerm != null && !searchTerm.isEmpty()) { // Filter organizations by search term organizations = filterOrganizations(searchTerm); } else { diff --git a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java index 31b25ce..2114d17 100644 --- a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java +++ b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java @@ -14,6 +14,8 @@ public class OrganizationCard extends VBox { private final AppState appState; private final Organization organization; private final NavigationController nav; + private StackPane imageContainer; + private String currentLogoUrl; public OrganizationCard(AppState appstate, NavigationController nav, Organization org, String img) { this.appState = appstate; @@ -22,14 +24,15 @@ public OrganizationCard(AppState appstate, NavigationController nav, Organizatio setId("mainContainer"); getStylesheets().add(getClass().getResource("/browsepage/browse_org.css").toExternalForm()); + imageContainer = createImageContainer(img); getChildren().addAll( - imageContainer(img), + imageContainer, orgName(org.name()), checkMarkContainer() ); setOnMouseClicked(e -> { - appstate.setCurrentOrganization(organization); + appstate.setCurrentOrganization(getOrganizationWithCurrentLogo()); nav.showOrganizationPage(); }); @@ -38,7 +41,41 @@ public OrganizationCard(AppState appstate, NavigationController nav, Organizatio setAlignment(Pos.CENTER); } - private StackPane imageContainer(String img) { + public Organization getOrganization() { + return organization; + } + + public void updateLogo(String logoUrl) { + this.currentLogoUrl = logoUrl; + if (imageContainer == null) return; + imageContainer.getChildren().clear(); + if (logoUrl != null && !logoUrl.isBlank()) { + ImageView logo = new ImageView(new Image(logoUrl, true)); + logo.setId("logo"); + logo.setSmooth(true); + logo.setPreserveRatio(true); + logo.setFitHeight(80); + logo.setFitWidth(80); + imageContainer.getChildren().add(logo); + } + } + + private Organization getOrganizationWithCurrentLogo() { + if (currentLogoUrl == null) { + return organization; + } + return new Organization( + organization.orgNumber(), + organization.name(), + organization.trusted(), + organization.websiteUrl(), + organization.isPreApproved(), + organization.description(), + currentLogoUrl + ); + } + + private StackPane createImageContainer(String img) { StackPane imageContainer = new StackPane(); imageContainer.setId("imageContainer"); diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index da8d1df..1ac2fa6 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -68,6 +68,8 @@ private StackPane createImageContainer() { logo.setId("logo"); logo.setSmooth(true); logo.setPreserveRatio(true); + logo.setFitHeight(120); + logo.setFitWidth(120); imageContainer.getChildren().add(logo); } else { StackPane placeholder = new StackPane(); From ed93b95e8d25392a1267d6a7097e55bcf8c60884 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Sun, 5 Apr 2026 14:45:16 +0200 Subject: [PATCH 04/33] update&perf[App]: Update and infcreased performance of org.logos rendering by adding it into init in App --- src/main/java/edu/group5/app/App.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index 22b3baa..c83fe44 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -71,6 +71,9 @@ public void init() { UserService userService = new UserService(this.userRepository); DonationService donationService = new DonationService(this.donationRepository, organizationRepository); OrganizationService organizationService = new OrganizationService(organizationRepository); + + // Pre-load logos in background so they're ready when user views causes page + organizationService.getTrustedOrganizationsWithLogosAsync(); this.root = new BorderPane(); this.appState = new AppState(); From c5c3ce9af9764659eabeec75b0eda13b5a7f5f46 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Wed, 8 Apr 2026 11:54:07 +0200 Subject: [PATCH 05/33] fix[App]: remove preloading redundancy --- src/main/java/edu/group5/app/App.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index c83fe44..fa48178 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -71,10 +71,6 @@ public void init() { UserService userService = new UserService(this.userRepository); DonationService donationService = new DonationService(this.donationRepository, organizationRepository); OrganizationService organizationService = new OrganizationService(organizationRepository); - - // Pre-load logos in background so they're ready when user views causes page - organizationService.getTrustedOrganizationsWithLogosAsync(); - this.root = new BorderPane(); this.appState = new AppState(); this.nav = new NavigationController(root, appState, userService, donationService, organizationService); From a2a087b70b55435d5293f890b17bf1c00b1b84f0 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Wed, 8 Apr 2026 15:17:55 +0200 Subject: [PATCH 06/33] feat&update[OrganizationPage]: Update description to be description fetched from the API --- src/main/java/edu/group5/app/App.java | 8 +- .../organization/OrganizationRepository.java | 26 +++-- .../organization/OrganizationScraper.java | 107 ++++++++++++++++++ .../organization/OrganizationService.java | 56 +++------ .../OrganizationPageView.java | 24 +++- .../organizationpage/organizationpage.css | 44 ++++--- 6 files changed, 192 insertions(+), 73 deletions(-) create mode 100644 src/main/java/edu/group5/app/model/organization/OrganizationScraper.java diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index fa48178..b59c6ef 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -7,6 +7,7 @@ import edu.group5.app.model.donation.DonationRepository; import edu.group5.app.model.donation.DonationService; import edu.group5.app.model.organization.OrganizationRepository; +import edu.group5.app.model.organization.OrganizationScraper; import edu.group5.app.model.organization.OrganizationService; import edu.group5.app.model.user.UserRepository; import edu.group5.app.model.user.UserService; @@ -62,15 +63,16 @@ public void init() { System.err.println("Failed to load organization data: " + e.getMessage()); } - // Create repositories with fetched data + // Create scraper and repositories with fetched data + OrganizationScraper orgScraper = new OrganizationScraper(); this.userRepository = new UserRepository(userData); this.donationRepository = new DonationRepository(donationData); - OrganizationRepository organizationRepository = new OrganizationRepository(organizationData); + OrganizationRepository organizationRepository = new OrganizationRepository(organizationData, orgScraper); // Create services (backend wiring) UserService userService = new UserService(this.userRepository); DonationService donationService = new DonationService(this.donationRepository, organizationRepository); - OrganizationService organizationService = new OrganizationService(organizationRepository); + OrganizationService organizationService = new OrganizationService(organizationRepository, orgScraper); this.root = new BorderPane(); this.appState = new AppState(); this.nav = new NavigationController(root, appState, userService, donationService, organizationService); diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java index cc0a6b1..f31efdc 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java @@ -11,25 +11,33 @@ * Repository class for managing Organization entities. It provides methods to retrieve trusted organizations, * find organizations by their organization number or name, and initializes the repository with input data. * The repository uses a HashMap to store Organization objects for efficient retrieval based on their organization number. - * Handles the business logic associated with organizations + * Delegates web scraping to OrganizationScraper for separation of concerns. */ public class OrganizationRepository extends Repository { private final HashMap grandMap; + private final OrganizationScraper scraper; /** - * Initializes the repository with the given input data, c - * onverting it into Organization objects and storing them in a map for efficient retrieval. - * The input is expected to be an array of objects, where each object contains + * Initializes the repository with the given input data and scraper. + * Converts input into Organization objects and stores them in a map for efficient retrieval. + * The input is expected to be an array of objects, where each object contains * the necessary information to create an Organization. + * * @param input the input data used to populate the repository, must not be null - * @throws IllegalArgumentException if the input is null + * @param scraper the OrganizationScraper to use for fetching web data, must not be null + * @throws IllegalArgumentException if input or scraper is null */ - public OrganizationRepository(Object[] input) { - super(new HashMap<>()); - grandMap = new HashMap<>(); + public OrganizationRepository(Object[] input, OrganizationScraper scraper) { + super(new HashMap<>()); + this.grandMap = new HashMap<>(); if (input == null) { throw new IllegalArgumentException("The input cannot be null"); } + if (scraper == null) { + throw new IllegalArgumentException("The scraper cannot be null"); + } + this.scraper = scraper; + ObjectMapper mapper = new ObjectMapper(); for (Object obj : input) { @@ -42,7 +50,7 @@ public OrganizationRepository(Object[] input) { boolean trusted = "approved".equalsIgnoreCase((String) contentMap.get("status")); String websiteURL = (String) contentMap.get("url"); boolean isPreApproved = Boolean.TRUE.equals(contentMap.get("is_pre_approved")); - String description = "Information about " + name; + String description = scraper.fetchDescription(websiteURL) != null ? scraper.fetchDescription(websiteURL) : "Information about " + name; Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description, null); grandMap.put(org.orgNumber(), org); diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java new file mode 100644 index 0000000..edb41fd --- /dev/null +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -0,0 +1,107 @@ +package edu.group5.app.model.organization; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import java.util.stream.Collectors; + +import java.util.HashMap; +import java.util.Map; + +/** + * Handles web scraping of organization information from Innsamlingskontrollen. + * Responsible for fetching logos and descriptions from organization pages. + * All results are cached to avoid redundant network requests. + */ +public class OrganizationScraper { + private final Map logoCache = new HashMap<>(); + private final Map descriptionCache = new HashMap<>(); + + /** + * Fetches the description for the given URL by scraping all text content + * inside {@code
}. Results are cached. + * + *

Strategy:

+ *
    + *
  1. Tries to get all <p> tags (skipping the first one) and concatenates them
  2. + *
  3. If no paragraphs found, gets all text content from the section
  4. + *
  5. Returns null if section not found or is empty
  6. + *
+ * + * @param pageUrl the URL for the organization's page; may be null or blank + * @return the description text, or null if not found or pageUrl is invalid + */ + public String fetchDescription(String pageUrl) { + if (pageUrl == null || pageUrl.isBlank()) { + return null; + } + + if (descriptionCache.containsKey(pageUrl)) { + return descriptionCache.get(pageUrl); + } + + try { + Document doc = Jsoup.connect(pageUrl) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .timeout(5000).get(); + + Element section = doc.selectFirst("section.information"); + if (section != null) { + // Try to get all

tags (skip first one if multiple exist) + String description = section.select("p").stream() + .skip(1) // Skip first paragraph (usually a heading) + .map(Element::text) + .filter(text -> !text.isBlank()) + .map(String::trim) + .collect(Collectors.joining("\n\n")); + + // Fallback: if no paragraphs after first, get all text from section + if (description.isBlank()) { + description = section.text().trim(); + } + + // Only cache and return if we found something meaningful + if (!description.isBlank()) { + descriptionCache.put(pageUrl, description); + return description; + } + } + } catch (Exception e) { + System.out.println("Could not get description for: " + pageUrl); + } + return null; + } + + /** + * Fetches the logo URL for the given page by scraping the {@code div.logo img} + * element. Results are cached so each URL is only fetched once. + * + * @param pageUrl the URL for the organization's page; may be null or blank + * @return the absolute logo URL, or null if not found or pageUrl is invalid + */ + public String fetchLogoUrl(String pageUrl) { + if (pageUrl == null || pageUrl.isBlank()) { + return null; + } + + if (logoCache.containsKey(pageUrl)) { + return logoCache.get(pageUrl); + } + + try { + Document doc = Jsoup.connect(pageUrl) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .timeout(5000).get(); + Element img = doc.selectFirst("div.logo img"); + + if (img != null) { + String logoUrl = img.absUrl("src"); + logoCache.put(pageUrl, logoUrl); + return logoUrl; + } + } catch (Exception e) { + System.out.println("Could not get logo for: " + pageUrl); + } + return null; + } +} diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java index 8ebd625..170b9a5 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -1,8 +1,5 @@ package edu.group5.app.model.organization; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; import java.util.stream.Collectors; import java.util.HashMap; @@ -14,26 +11,29 @@ * It interacts with the OrganizationRepository to retrieve organization information * and contains business logic associated with organization management. * - *

It provides fetching logo URLs by web scraping each organization's page on - * Innsamlingskontrollen.

+ *

It provides fetching logo URLs by delegating to OrganizationScraper.

* * Fetched logo URLs are cached to avoid redundant network requests. */ public class OrganizationService { private OrganizationRepository organizationRepository; - - private final Map logoCache = new HashMap<>(); + private OrganizationScraper scraper; /** - * Constructs an OrganizationService with the given OrganizationRepository. + * Constructs an OrganizationService with the given OrganizationRepository and scraper. * @param organizationRepository the OrganizationRepository to use for managing organization data; must not be null - * @throws IllegalArgumentException if organizationRepository is null + * @param scraper the OrganizationScraper to use for fetching web data; must not be null + * @throws IllegalArgumentException if organizationRepository or scraper is null */ - public OrganizationService(OrganizationRepository organizationRepository) { + public OrganizationService(OrganizationRepository organizationRepository, OrganizationScraper scraper) { if (organizationRepository == null) { throw new IllegalArgumentException("OrganizationRepository cannot be null"); } + if (scraper == null) { + throw new IllegalArgumentException("OrganizationScraper cannot be null"); + } this.organizationRepository = organizationRepository; + this.scraper = scraper; } /** @@ -81,39 +81,13 @@ public Organization findByOrgName(String name) { * @return the absolute logo URL, or null if not found or pageUrl is invalid */ - public String fetchLogoUrl(String pageUrl) { - if (pageUrl == null || pageUrl.isBlank()) { - return null; - } - - if (logoCache.containsKey(pageUrl)) { - return logoCache.get(pageUrl); - } - - try { - Document doc = Jsoup.connect(pageUrl) - .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") - .timeout(5000).get(); - Element img = doc.selectFirst("div.logo img"); - - if (img != null) { - String logoUrl = img.absUrl("src"); - logoCache.put(pageUrl, logoUrl); - return logoUrl; - } - } catch (Exception e) { - System.out.println("Could not get logo for: " + pageUrl); - } - return null; - } - /** - * Fetches all trusted organizations with their logo URLs. + * Fetches all trusted organizations with their logo URLs and descriptions. * *

- * For each trusted organization, attempts to get its logo using - * {@link #fetchLogoUrl(String)}. Creates a new Organization - * object including the logo URL. + * For each trusted organization, attempts to get its logo using the scraper. + * Creates a new Organization object including the logo URL (description is + * already fetched during repository initialization). *

* @return a map of trusted organizations keyed by organization number, with logos included */ @@ -127,7 +101,7 @@ public Map getTrustedOrganizationsWithLogos() { org.websiteUrl(), org.isPreApproved(), org.description(), - fetchLogoUrl(org.websiteUrl()) + scraper.fetchLogoUrl(org.websiteUrl()) )) .collect(Collectors.toMap(Organization::orgNumber, org -> org)); } diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 1ac2fa6..2d10328 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -8,6 +8,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextArea; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; @@ -68,8 +69,8 @@ private StackPane createImageContainer() { logo.setId("logo"); logo.setSmooth(true); logo.setPreserveRatio(true); - logo.setFitHeight(120); - logo.setFitWidth(120); + logo.setFitHeight(350); + logo.setFitWidth(350); imageContainer.getChildren().add(logo); } else { StackPane placeholder = new StackPane(); @@ -90,14 +91,27 @@ private VBox createOrgInfoSection() { orgInfoSection.setSpacing(50); VBox orgNameAndDescription = new VBox(); + orgNameAndDescription.setSpacing(5); Label orgName = new Label(org != null ? org.name() : "Unknown Organization"); orgName.setId("orgName"); - Text description = new Text(org != null ? org.description() : "No description available"); - description.setId("description"); + VBox descriptionBox = new VBox(); + descriptionBox.setSpacing(15); + descriptionBox.setId("description-container"); + + if (org != null && org.description() != null) { + String[] paragraphs = org.description().split("\n\n"); + for (String para : paragraphs) { + if (!para.isBlank()) { + Label paragraph = new Label(para.trim()); + paragraph.setWrapText(true); + descriptionBox.getChildren().add(paragraph); + } + } + } - orgNameAndDescription.getChildren().addAll(orgName, description); + orgNameAndDescription.getChildren().addAll(orgName, descriptionBox); Button donateBtn = new Button("Donate"); donateBtn.setId("donate-button"); diff --git a/src/main/resources/organizationpage/organizationpage.css b/src/main/resources/organizationpage/organizationpage.css index a7276b5..e5263f2 100644 --- a/src/main/resources/organizationpage/organizationpage.css +++ b/src/main/resources/organizationpage/organizationpage.css @@ -1,31 +1,45 @@ #main-container { - -fx-padding: 50px + -fx-padding: 50px } #logo { - -fx-min-height: 50%; + -fx-min-height: 80%; } #orgName { - -fx-font-weight: bold; - -fx-font-size: 20pt; + -fx-font-weight: bold; + -fx-font-size: 28pt; + -fx-padding: 0 0 30 0; } -#description { - -fx-font-size: 10pt; +#description-container { + -fx-padding: 30 0 30 0; + -fx-spacing: 25; +} + +#description-paragraph { + -fx-font-size: 16; + -fx-text-fill: #222; + -fx-font-family: "Segoe UI", Arial, sans-serif; + -fx-padding: 15 50 15 50; + -fx-line-spacing: 6; + -fx-text-alignment: Left; + -fx-wrap-text: true; + } #donate-button { - -fx-pref-height: 55px; - -fx-background-color: #e03030; - -fx-text-fill: white; - -fx-font-size: 22px; - -fx-font-weight: bold; - -fx-background-radius: 8; - -fx-cursor: hand; - -fx-padding: 0 40 0 40; + -fx-pref-height: 55px; + -fx-background-color: #e03030; + -fx-text-fill: white; + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-padding: 0 40 0 40; + -fx-margin-top: 30; } #donate-button:hover { - -fx-background-color: #c02020; + -fx-background-color: #c02020; } \ No newline at end of file From 456a1d63d22253ab1d051ca091222be0c74b4960 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Wed, 8 Apr 2026 15:32:34 +0200 Subject: [PATCH 07/33] update&test[Organization]: Update JUnit tests with new features regarding OrganizationPage --- .../model/donation/DonationServiceTest.java | 11 ++-- .../OrganizationRepositoryTest.java | 26 +++++++- .../organization/OrganizationScraperTest.java | 66 +++++++++++++++++++ .../organization/OrganizationServiceTest.java | 57 ++++++++++------ 4 files changed, 134 insertions(+), 26 deletions(-) create mode 100644 src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java diff --git a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java index 80d37bf..3ed1aec 100644 --- a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java +++ b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java @@ -1,6 +1,7 @@ package edu.group5.app.model.donation; import edu.group5.app.model.organization.OrganizationRepository; +import edu.group5.app.model.organization.OrganizationScraper; import edu.group5.app.model.user.Customer; import org.junit.jupiter.api.BeforeEach; @@ -21,18 +22,20 @@ class DonationServiceTest { private OrganizationRepository organizationRepository; private DonationService donationService; private Customer customer; + private OrganizationScraper scraper; @BeforeEach void setUp() { + scraper = new OrganizationScraper(); HashMap orgMap = new HashMap<>(); - orgMap.put("org_number", "101"); + orgMap.put("org_number", "101"); orgMap.put("name", "CharityOrg"); - orgMap.put("status", "approved"); + orgMap.put("status", "approved"); orgMap.put("url", "https://charity.org"); - orgMap.put("is_pre_approved", true); + orgMap.put("is_pre_approved", true); Object[] orgInput = new Object[]{ orgMap }; - organizationRepository = new OrganizationRepository(orgInput); + organizationRepository = new OrganizationRepository(orgInput, scraper); donationRepository = new DonationRepository(new ArrayList<>()); diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java index f821e35..8906395 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java @@ -10,9 +10,11 @@ class OrganizationRepositoryTest { private OrganizationRepository repository; + private OrganizationScraper scraper; @BeforeEach void setUp() { + scraper = new OrganizationScraper(); Object[] content = new Object[] { Map.of( "org_number", "1", @@ -43,13 +45,13 @@ void setUp() { "is_pre_approved", true ) }; - repository = new OrganizationRepository(content); + repository = new OrganizationRepository(content, scraper); } private void constructorTest(Object[] input, String expectedMessage) { IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, - () -> new OrganizationRepository(input) + () -> new OrganizationRepository(input, scraper) ); assertEquals(expectedMessage, exception.getMessage()); } @@ -58,6 +60,24 @@ void constructor_ThrowsWhenContentIsNull() { constructorTest(null, "The input cannot be null"); } + @Test + void constructor_ThrowsWhenScraperIsNull() { + Object[] content = new Object[] { + Map.of( + "org_number", "1", + "name", "Org", + "status", "approved", + "url", "org.com", + "is_pre_approved", true + ) + }; + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new OrganizationRepository(content, null) + ); + assertEquals("The scraper cannot be null", exception.getMessage()); + } + @Test void getTrustedOrganizations_OnlyReturnsTrustedOrganizations() { Map trusted = repository.getTrustedOrganizations(); @@ -128,7 +148,7 @@ void testExportAllOrganizations() { @Test void testExportAllOrganizationsThrowsWhenRepositoryIsEmpty() { - OrganizationRepository emptyRepo = new OrganizationRepository(new Object[0]); + OrganizationRepository emptyRepo = new OrganizationRepository(new Object[0], scraper); IllegalStateException exception = assertThrows( IllegalStateException.class, () -> emptyRepo.export() ); diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java new file mode 100644 index 0000000..c6c318b --- /dev/null +++ b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java @@ -0,0 +1,66 @@ +package edu.group5.app.model.organization; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class OrganizationScraperTest { + + private OrganizationScraper scraper; + + @BeforeEach + void setUp() { + scraper = new OrganizationScraper(); + } + + @Test + void fetchDescription_ReturnsNullWhenUrlIsNull() { + assertNull(scraper.fetchDescription(null)); + } + + @Test + void fetchDescription_ReturnsNullWhenUrlIsBlank() { + assertNull(scraper.fetchDescription("")); + } + + @Test + void fetchDescription_ReturnsNullWhenUrlIsInvalid() { + String result = scraper.fetchDescription("https://invalid-url-that-does-not-exist-xyz123.com"); + assertNull(result); + } + + @Test + void fetchDescription_CachesResultOnSecondCall() { + // Mock URLs won't work, but cache still works with null returns + scraper.fetchDescription("https://example.com"); + scraper.fetchDescription("https://example.com"); + // If no exception thrown, cache works + assertTrue(true); + } + + @Test + void fetchLogoUrl_ReturnsNullWhenUrlIsNull() { + assertNull(scraper.fetchLogoUrl(null)); + } + + @Test + void fetchLogoUrl_ReturnsNullWhenUrlIsBlank() { + assertNull(scraper.fetchLogoUrl("")); + } + + @Test + void fetchLogoUrl_ReturnsNullWhenUrlIsInvalid() { + String result = scraper.fetchLogoUrl("https://invalid-url-that-does-not-exist-xyz123.com"); + assertNull(result); + } + + @Test + void fetchLogoUrl_CachesResultOnSecondCall() { + // Mock URLs won't work, but cache still works with null returns + scraper.fetchLogoUrl("https://example.com"); + scraper.fetchLogoUrl("https://example.com"); + // If no exception thrown, cache works + assertTrue(true); + } +} diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java index 0920e67..2d76391 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java @@ -6,14 +6,17 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; public class OrganizationServiceTest { private OrganizationRepository repo; private OrganizationService service; + private OrganizationScraper scraper; private Object[] content; @BeforeEach public void setUp() { + scraper = new OrganizationScraper(); Map orgMap = new HashMap<>(); orgMap.put("org_number", "1"); orgMap.put("name", "Misjonsalliansen"); @@ -22,22 +25,52 @@ public void setUp() { orgMap.put("is_pre_approved", false); content = new Object[]{orgMap}; - repo = new OrganizationRepository(content); - service = new OrganizationService(repo); + repo = new OrganizationRepository(content, scraper); + service = new OrganizationService(repo, scraper); } @Test - void constructor_throwsIfNull() { + void constructor_throwsIfRepositoryIsNull() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, - () -> new OrganizationService(null)); + () -> new OrganizationService(null, scraper)); assertEquals("OrganizationRepository cannot be null", ex.getMessage()); } + @Test + void constructor_throwsIfScraperIsNull() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new OrganizationService(repo, null)); + assertEquals("OrganizationScraper cannot be null", ex.getMessage()); + } + @Test void testGetOrganizationRepository() { assertEquals(repo, service.getOrganizationRepository()); } + @Test + void testGetTrustedOrganizationsWithLogos() { + Map orgsWithLogos = service.getTrustedOrganizationsWithLogos(); + assertNotNull(orgsWithLogos); + assertTrue(orgsWithLogos.containsKey(1)); + Organization org = orgsWithLogos.get(1); + assertEquals(1, org.orgNumber()); + assertEquals("Misjonsalliansen", org.name()); + assertNotNull(org); + } + + @Test + void testGetTrustedOrganizationsWithLogosAsync() throws Exception { + CompletableFuture> futureOrgs = + service.getTrustedOrganizationsWithLogosAsync(); + + assertNotNull(futureOrgs); + Map orgsWithLogos = futureOrgs.get(); + assertNotNull(orgsWithLogos); + assertTrue(orgsWithLogos.containsKey(1)); + assertEquals("Misjonsalliansen", orgsWithLogos.get(1).name()); + } + @Test void testGetTrustedOrganizations() { Map trustedOrgs = service.getTrustedOrganizations(); @@ -67,19 +100,5 @@ void testFindByOrgName() { assertEquals(1, org.orgNumber()); assertEquals("Misjonsalliansen", org.name()); } - - @Test - void fetchLogoUrlReturnsNullWhenUrlIsNull() { - assertNull(service.fetchLogoUrl(null)); - } - @Test - void fetchLogoUrlReturnsNullWhenUrlIsBlank() { - assertNull(service.fetchLogoUrl("")); - } - @Test - void fetchLogoUrlCachesResultOnSecondCall() { - String result1 = service.fetchLogoUrl("https://"); - String result2 = service.fetchLogoUrl("https://"); - assertEquals(result1, result2); - } } + From fbc4ed1c0a1fc5b09d65b5d16a9b6f236f440b5c Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 9 Apr 2026 09:29:06 +0200 Subject: [PATCH 08/33] update[Organization]: Update Hashmap of Organizations to be displayed in alphabetical order for more manuverable causesPage --- .../app/model/organization/OrganizationService.java | 11 ++++++++--- .../group5/app/view/causespage/CausesPageView.java | 7 ++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java index 170b9a5..e19c273 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -2,7 +2,8 @@ import java.util.stream.Collectors; -import java.util.HashMap; +import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -49,7 +50,10 @@ public OrganizationRepository getOrganizationRepository() { * @return a map of trusted organizations by organization number */ public Map getTrustedOrganizations() { - return organizationRepository.getTrustedOrganizations(); + return organizationRepository.getTrustedOrganizations().values().stream() + .sorted(Comparator.comparing(Organization::name)) + .collect(Collectors.toMap(Organization::orgNumber, + org -> org, (e1, e2) -> e1, LinkedHashMap::new)); } /** @@ -103,7 +107,8 @@ public Map getTrustedOrganizationsWithLogos() { org.description(), scraper.fetchLogoUrl(org.websiteUrl()) )) - .collect(Collectors.toMap(Organization::orgNumber, org -> org)); + .sorted(Comparator.comparing(Organization::name)) + .collect(Collectors.toMap(Organization::orgNumber, org -> org, (e1, e2) -> e1, LinkedHashMap::new)); } /** diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 5cfc197..36c056f 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -10,8 +10,10 @@ import javafx.scene.layout.*; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; +import java.util.Comparator; public class CausesPageView extends BorderPane { private final AppState appState; @@ -158,9 +160,12 @@ private Map filterOrganizations(String searchTerm) { String lowerSearchTerm = searchTerm.toLowerCase(); return allOrganizations.values().stream() .filter(org -> org.name().toLowerCase().contains(lowerSearchTerm)) + .sorted(Comparator.comparing(Organization::name)) .collect(Collectors.toMap( Organization::orgNumber, - org -> org + org -> org, + (e1, e2) -> e1, + LinkedHashMap::new )); } From fe434610cb84f684fe52e85faf4e3fb5509b3168 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 9 Apr 2026 10:39:10 +0200 Subject: [PATCH 09/33] update[CausesPage]: Update CausesPage to have search bare fixed at the top ensuring user friendly UX --- .../app/view/causespage/CausesPageView.java | 13 +++--- src/main/resources/loginpage/login.css | 42 ++++++++++++------- src/main/resources/loginpage/signin.css | 42 ++++++++++++------- 3 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 36c056f..a2f3374 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -32,7 +32,10 @@ public CausesPageView(AppState appState, NavigationController nav, OrganizationC setCenter(createBody()); } - private ScrollPane createBody() { + private BorderPane createBody() { + BorderPane bodyRoot = new BorderPane(); + bodyRoot.setTop(createSearchSection()); + ScrollPane body = new ScrollPane(); body.setId("body"); body.setFitToWidth(true); @@ -47,11 +50,9 @@ private ScrollPane createBody() { // Load organizations INSTANTLY from cache allOrganizations = orgController.getTrustedOrgs(); - vBox.getChildren().addAll( - createSearchSection(), - createOrganizationSection(null) - ); + vBox.getChildren().add(createOrganizationSection(null)); body.setContent(vBox); + bodyRoot.setCenter(body); // Build a map of org ID -> card for quick lookup Map cardMap = new HashMap<>(); @@ -78,7 +79,7 @@ private ScrollPane createBody() { }); }); - return body; + return bodyRoot; } private HBox createSearchSection() { diff --git a/src/main/resources/loginpage/login.css b/src/main/resources/loginpage/login.css index dc758a1..f0ffc41 100644 --- a/src/main/resources/loginpage/login.css +++ b/src/main/resources/loginpage/login.css @@ -1,25 +1,35 @@ #image-section { - -fx-background-image: url("/loginpage/login-image.jpg"); - -fx-background-size: 200%; - -fx-background-position: left center; - -fx-background-repeat: no-repeat; - -fx-pref-width: 50%; + -fx-background-image: url("/loginpage/login-image.jpg"); + -fx-background-size: 200%; + -fx-background-position: left center; + -fx-background-repeat: no-repeat; + -fx-pref-width: 50%; } + #login-btn { - -fx-background-color: #000000; - -fx-text-fill: white; - -fx-pref-height: 35px; + -fx-background-color: #000000; + -fx-text-fill: white; + -fx-pref-height: 35px; } #register-btn { - -fx-background-color: #000000; - -fx-text-fill: white; - -fx-pref-height: 35px; + -fx-background-color: #000000; + -fx-text-fill: white; + -fx-pref-height: 35px; } + #login-box { - -fx-border-color: #ccc; - -fx-border-radius: 8px; - -fx-border-width: 1px; - -fx-padding: 24px; - -fx-max-width: 340px; + -fx-border-color: #ccc; + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-padding: 24px; + -fx-max-width: 340px; +} + +#login-btn:hover { + -fx-cursor: hand; +} + +#register-btn:hover { + -fx-cursor: hand; } \ No newline at end of file diff --git a/src/main/resources/loginpage/signin.css b/src/main/resources/loginpage/signin.css index 4ab0276..d14af06 100644 --- a/src/main/resources/loginpage/signin.css +++ b/src/main/resources/loginpage/signin.css @@ -1,25 +1,35 @@ #image-section { - -fx-background-image: url("/loginpage/signin-image.png"); - -fx-background-size: auto; - -fx-background-position: right center; - -fx-background-repeat: no-repeat; - -fx-pref-width: 50%; + -fx-background-image: url("/loginpage/signin-image.png"); + -fx-background-size: auto; + -fx-background-position: right center; + -fx-background-repeat: no-repeat; + -fx-pref-width: 50%; } + #login-btn { - -fx-background-color: #000000; - -fx-text-fill: white; - -fx-pref-height: 35px; + -fx-background-color: #000000; + -fx-text-fill: white; + -fx-pref-height: 35px; } #register-btn { - -fx-background-color: #000000; - -fx-text-fill: white; - -fx-pref-height: 35px; + -fx-background-color: #000000; + -fx-text-fill: white; + -fx-pref-height: 35px; } + #login-box { - -fx-border-color: #ccc; - -fx-border-radius: 8px; - -fx-border-width: 1px; - -fx-padding: 24px; - -fx-max-width: 340px; + -fx-border-color: #ccc; + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-padding: 24px; + -fx-max-width: 340px; +} + +#login-btn:hover { + -fx-cursor: hand; +} + +#register-btn:hover { + -fx-cursor: hand; } \ No newline at end of file From 1960fced52e4bde825bc224758ae3ad86afddebd Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:44:59 +0200 Subject: [PATCH 10/33] style[DbWrapper]: add javadocs and split lines --- .../group5/app/model/wrapper/DbWrapper.java | 87 +++++++++++++++++-- 1 file changed, 82 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java index 073fd81..2357e87 100644 --- a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -15,6 +15,9 @@ import java.util.logging.Level; import java.util.logging.Logger; +/** + * A class for wrapping the database. + */ public class DbWrapper { protected Connection connection; private static final String CONNECTION_TYPE = "jdbc:h2:"; @@ -24,6 +27,11 @@ public class DbWrapper { private List donations; private Logger logger = Logger.getLogger(DbWrapper.class.getName()); + /** + * The constructor, which constructs a String for connecting to the database. + * + * @param test Whether to construct the connection String for testing (in-memory) or not. + */ public DbWrapper(boolean test) { if (test) { this.connectionString = CONNECTION_TYPE + "mem:test;" + DB_SCRIPT + "test_init.sql'"; @@ -33,6 +41,11 @@ public DbWrapper(boolean test) { this.logger.info("connectionString constructed"); } + /** + * Connects to the database, and returns the result, logging failures. + * + * @return True if successful, false if not. + */ public boolean connect() { try { this.connection = DriverManager.getConnection(this.connectionString); @@ -49,7 +62,13 @@ public boolean connect() { } } + /** + * Disconnects the database connection, logging failures. + * + * @return True if successful, false if not. + */ public boolean disconnect() { + // We are not interested in whether it fails to close, as we check its closed status later. try{ this.connection.close(); } catch (Exception e) {}; try { return this.connection.isClosed(); @@ -59,12 +78,25 @@ public boolean disconnect() { } } + /** + * Closes queries and results. + * + * @param results The ResultSet to close, can be null. + * @param ps The PreparedStatement to close, can be null. + */ private void close(ResultSet results, PreparedStatement ps) { + // This method can take null arguments, so an exception is expected. try { results.close(); } catch (Exception e) {} try { ps.close(); } catch (Exception e) {} this.logger.info("results and ps closed"); } + /** + * Gets all users from the database. + * + * @return The users from the database returned as a List of Object arrays, where each Object + * array represents a user and a row in the users table in the database. + */ public List importUsers() { PreparedStatement ps = null; ResultSet results = null; @@ -93,7 +125,17 @@ public List importUsers() { return this.users; } - public int exportUsers(List data) { + /** + * Puts new users into the database. + * + * @param data The new users to put into the database. Each Object array in the List is a new + * user to add as a row. + * @return The number of rows affected in the transaction. + * @throws IllegalArgumentException This exception is thrown when data is null, its rows are not + * of length 6, any of the rows are null, any of the rows are duplicates or existing rows in + * the database, or any of the values in the rows can't be cast to the correct data-types. + */ + public int exportUsers(List data) throws IllegalArgumentException { this.importUsers(); if (data == null) { @@ -102,12 +144,14 @@ public int exportUsers(List data) { if (data.isEmpty()) { return 0; } + // TODO: change to check length for every row. if (data.get(0).length != 6) { throw new IllegalArgumentException("data's arrays must have a length of 6"); } if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { throw new IllegalArgumentException("One or more rows in data contains null values"); } + // TODO: change how existing rows are checked. if (this.users.size() > 0) { if ((int) data.getLast()[0] <= (int) this.users.getLast()[0]) { throw new IllegalArgumentException("data can't contain existing rows"); @@ -123,7 +167,13 @@ public int exportUsers(List data) { int rowsAffected = 0; try { ps = this.connection.prepareStatement( - "INSERT INTO users (user_id, role, first_name, last_name, email, password_hash) VALUES (?, ?, ?, ?, ?, ?)"); + """ + INSERT INTO users + (user_id, role, first_name, last_name, email, password_hash) + VALUES + (?, ?, ?, ?, ?, ?) + """ + ); for (Object[] row : data) { try { ps.setInt(1, (int) row[0]); @@ -144,10 +194,21 @@ public int exportUsers(List data) { return rowsAffected; } + /** + * Imports all donations. + * + * @return A List of Object arrays for each donation in the database. + */ public List fetchAllDonations() { return this.importDonations(0, true); } + /** + * Imports the donations of a specific user based on a given user_id. + * + * @param user_id The id of the user to get the donations of. + * @return A List of Object arrays for each donation in the database. + */ public List importDonations(int user_id) { return this.importDonations(user_id, false); } @@ -155,7 +216,7 @@ public List importDonations(int user_id) { private List importDonations(int user_id, boolean all) { PreparedStatement ps = null; ResultSet results = null; - try{ + try { if (all) { ps = this.connection.prepareStatement("SELECT * FROM donations"); } else { @@ -185,7 +246,17 @@ private List importDonations(int user_id, boolean all) { return this.donations; } - public int exportDonations(List data) { + /** + * Puts new donations into the database. + * + * @param data The new donation to put into the database. Each Object array in the List is a new + * donations to add as a row. + * @return The number of rows affected in the transaction. + * @throws IllegalArgumentException This exception is thrown when data is null, its rows are not + * of length 6, any of the rows are null, any of the rows are duplicates or existing rows in + * the database, or any of the values in the rows can't be cast to the correct data-types. + */ + public int exportDonations(List data) throws IllegalArgumentException { this.fetchAllDonations(); if (data == null) { @@ -212,7 +283,13 @@ public int exportDonations(List data) { int rowsAffected = 0; try { ps = this.connection.prepareStatement( - "INSERT INTO donations (donation_id, user_id, organization_id, amount, dating, payment_method) VALUES (?, (SELECT user_id FROM users WHERE user_id = ?), ?, ?, ?, ?)"); + """ + INSERT INTO donations + (donation_id, user_id, organization_id, amount, dating, payment_method) + VALUES + (?, (SELECT user_id FROM users WHERE user_id = ?), ?, ?, ?, ?) + """ + ); for (Object[] row : data) { try { for (int i = 0; i < 3; i++) { From bdd4854b66e2513da5a85db9c4bd66f1499c5297 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:08:13 +0200 Subject: [PATCH 11/33] style[DbWrapper]: add whitespaces after try --- src/main/java/edu/group5/app/model/wrapper/DbWrapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java index 2357e87..f453cc3 100644 --- a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -69,7 +69,7 @@ public boolean connect() { */ public boolean disconnect() { // We are not interested in whether it fails to close, as we check its closed status later. - try{ this.connection.close(); } catch (Exception e) {}; + try { this.connection.close(); } catch (Exception e) {}; try { return this.connection.isClosed(); } catch (Exception e) { @@ -100,7 +100,7 @@ private void close(ResultSet results, PreparedStatement ps) { public List importUsers() { PreparedStatement ps = null; ResultSet results = null; - try{ + try { ps = this.connection.prepareStatement("SELECT * FROM users"); results = ps.executeQuery(); List data = new ArrayList(); From c3b05558c529e463293699a442933407ff7ee316 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:20:20 +0200 Subject: [PATCH 12/33] style[ParameterValidator]: make code conform to google checks --- .../group5/app/utils/ParameterValidator.java | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/main/java/edu/group5/app/utils/ParameterValidator.java b/src/main/java/edu/group5/app/utils/ParameterValidator.java index 1a44e46..996bf6e 100644 --- a/src/main/java/edu/group5/app/utils/ParameterValidator.java +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -1,21 +1,24 @@ package edu.group5.app.utils; import java.math.BigDecimal; + /** - * ParameterValidator is a utility class that provides static methods for validating various types of parameters. - * It includes methods for checking strings, integers, objects, and BigDecimal values to ensure they meet specific - * criteria such as not being null, not being blank, or being positive. - * + * ParameterValidator is a utility class that provides static methods for validating various types + * of parameters. + * It includes methods for checking strings, integers, objects, and BigDecimal values to ensure + * they meet specific criteria such as not being null, not being blank, or being positive. */ public final class ParameterValidator { /** * Validates that a string parameter is not null and not blank. + * * @param stringArg the string parameter to validate * @param variableName the name of the variable being validated, used in exception messages * @throws IllegalArgumentException if the string is null or blank */ - public static final void stringChecker(String stringArg, String variableName) throws IllegalArgumentException { + public static final void stringChecker(String stringArg, String variableName) + throws IllegalArgumentException { nullCheck(stringArg, variableName); if (stringArg.isBlank()) { throw new IllegalArgumentException(String.format("%s can't be blank", variableName)); @@ -24,33 +27,41 @@ public static final void stringChecker(String stringArg, String variableName) th /** * Validates that an integer parameter is not null and is a positive integer. + * * @param intArg the integer parameter to validate * @param variableName the name of the variable being validated, used in exception messages * @throws IllegalArgumentException if the integer is null or not a positive integer */ - public static final void intChecker(int intArg, String variableName) throws IllegalArgumentException { + public static final void intChecker(int intArg, String variableName) + throws IllegalArgumentException { if (intArg <= 0) { - throw new IllegalArgumentException(String.format("%s must be a positive integer", variableName)); + throw new IllegalArgumentException( + String.format("%s must be a positive integer", variableName) + ); } } /** * Validates that an object parameter is not null. + * * @param objectArg the object parameter to validate * @param variableName the name of the variable being validated, used in exception messages * @throws IllegalArgumentException if the object is null */ - public static final void objectChecker(Object objectArg, String variableName) throws IllegalArgumentException { + public static final void objectChecker(Object objectArg, String variableName) + throws IllegalArgumentException { nullCheck(objectArg, variableName); } /** * Validates that a BigDecimal parameter is not null and is greater than zero. + * * @param bigDecimalArg the BigDecimal parameter to validate * @param variableName the name of the variable being validated, used in exception messages * @throws IllegalArgumentException if the BigDecimal is null or not greater than zero */ - public static final void bigDecimalChecker(BigDecimal bigDecimalArg, String variableName) throws IllegalArgumentException { + public static final void bigDecimalChecker(BigDecimal bigDecimalArg, String variableName) + throws IllegalArgumentException { nullCheck(bigDecimalArg, variableName); if (bigDecimalArg.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException(String.format("%s must be larger than 0", variableName)); @@ -58,12 +69,15 @@ public static final void bigDecimalChecker(BigDecimal bigDecimalArg, String vari } /** - * Helper method to check if a variable is null and throw an IllegalArgumentException with a formatted message if it is. + * Helper method to check if a variable is null and throw an IllegalArgumentException with a + * formatted message if it is. + * * @param variable the variable to check for null * @param variableName the name of the variable being checked, used in the exception message * @throws IllegalArgumentException if the variable is null */ - private static final void nullCheck(Object variable, String variableName) throws IllegalArgumentException { + private static final void nullCheck(Object variable, String variableName) + throws IllegalArgumentException { if (variable == null) { throw new IllegalArgumentException(String.format("%s can't be null", variableName)); } From e1cda6f1a4da7299e06254e3ce5f572182814001 Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Thu, 9 Apr 2026 11:24:42 +0200 Subject: [PATCH 13/33] feat: added javadoc to some pages, and changed signin to signup --- .../group5/app/control/LoginController.java | 4 +- .../app/control/NavigationController.java | 6 +-- .../view/donationpage/DonationPageView.java | 8 ++++ .../donationpage/PaymentCompletePageView.java | 7 +++ .../app/view/homepage/HomePageView.java | 8 ++++ .../app/view/loginpage/LoginHeader.java | 4 ++ .../app/view/loginpage/LoginPageView.java | 12 ++++- ...ignInPageView.java => SignUpPageView.java} | 42 +++++++++++------- .../{signin-image.png => signup-image.png} | Bin .../loginpage/{signin.css => signup.css} | 2 +- 10 files changed, 68 insertions(+), 25 deletions(-) rename src/main/java/edu/group5/app/view/loginpage/{SignInPageView.java => SignUpPageView.java} (75%) rename src/main/resources/loginpage/{signin-image.png => signup-image.png} (100%) rename src/main/resources/loginpage/{signin.css => signup.css} (89%) diff --git a/src/main/java/edu/group5/app/control/LoginController.java b/src/main/java/edu/group5/app/control/LoginController.java index cdd5b5f..649a1c4 100644 --- a/src/main/java/edu/group5/app/control/LoginController.java +++ b/src/main/java/edu/group5/app/control/LoginController.java @@ -4,7 +4,7 @@ import edu.group5.app.model.user.User; import edu.group5.app.model.user.UserService; import edu.group5.app.view.loginpage.LoginPageView; -import edu.group5.app.view.loginpage.SignInPageView; +import edu.group5.app.view.loginpage.SignUpPageView; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class LoginController { @@ -18,7 +18,7 @@ public LoginController(AppState appState, NavigationController nav, UserService this.userService = userService; } - public void handleSignIn(SignInPageView view, String firstName, String lastName, String email, char[] passwordChars) { + public void handleSignUp(SignUpPageView view, String firstName, String lastName, String email, char[] passwordChars) { if (firstName == null || firstName.trim().isEmpty() || lastName == null || lastName.trim().isEmpty() || email == null || email.trim().isEmpty() || diff --git a/src/main/java/edu/group5/app/control/NavigationController.java b/src/main/java/edu/group5/app/control/NavigationController.java index ddab7e2..1b794d5 100644 --- a/src/main/java/edu/group5/app/control/NavigationController.java +++ b/src/main/java/edu/group5/app/control/NavigationController.java @@ -11,7 +11,7 @@ import edu.group5.app.view.homepage.HomePageView; import edu.group5.app.view.loginpage.LoginHeader; import edu.group5.app.view.loginpage.LoginPageView; -import edu.group5.app.view.loginpage.SignInPageView; +import edu.group5.app.view.loginpage.SignUpPageView; import edu.group5.app.view.organizationpage.OrganizationPageView; import edu.group5.app.view.userpage.UserPageView; import javafx.scene.layout.BorderPane; @@ -49,9 +49,9 @@ public void showLoginPage() { root.setCenter(new LoginPageView(appState, this, loginController)); } - public void showSignInPage() { + public void showSignUpPage() { root.setTop(loginHeader); - root.setCenter(new SignInPageView(appState, this, loginController)); + root.setCenter(new SignUpPageView(appState, this, loginController)); } public void showPaymentCompletePage() { diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 63d0493..f9681fc 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -18,6 +18,14 @@ import java.math.BigDecimal; import java.util.*; +/** + * A view for the Donation Page. + * In the donation page a user can donate a chosen amount + * to the organization they have chosen to donate to. + * + *

The donation page consists of payment amount buttons, + * payment method buttons, donation button, and a back to organization page button.

+ */ public class DonationPageView extends BorderPane { private final AppState appState; private final NavigationController nav; diff --git a/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java b/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java index fcefe97..e20cfee 100644 --- a/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java @@ -11,6 +11,13 @@ import java.util.Objects; +/** + * A view for the payment complete page. + * When a user have donated an amount, this page opens up. + * + *

The page consist of an image that says "Tank You For The Donation", + * and a "back to home" button at the bottom center.

+ */ public class PaymentCompletePageView extends BorderPane { private final NavigationController nav; diff --git a/src/main/java/edu/group5/app/view/homepage/HomePageView.java b/src/main/java/edu/group5/app/view/homepage/HomePageView.java index b40299a..c24b89b 100644 --- a/src/main/java/edu/group5/app/view/homepage/HomePageView.java +++ b/src/main/java/edu/group5/app/view/homepage/HomePageView.java @@ -10,6 +10,14 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; +/** + * A view for the homepage. + * In the home page a user can navigate to pages in the heading, + * and they can press the "donate to a cause" button or the "about us" button. + * + *

The homepage includes a heading, a "donate to a cause" button, + * and an about us button. The page also has a charity image at the bottom.

+ */ public class HomePageView extends BorderPane { private final AppState appState; private final NavigationController nav; diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java index ad6a412..b5f4296 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java @@ -6,6 +6,10 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; +/** + * A header for the login page and for the SignIn page. + *

The header includes a logo of the Help Me Help app.

+ */ public class LoginHeader extends BorderPane { public LoginHeader() { diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java index af972e0..3fb9bb7 100644 --- a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java @@ -12,6 +12,14 @@ import java.util.Objects; +/** + * A view for the login page. + * A user can login with email and password. + * If the user does not have an account they can + * press the register button to the SignUp page. + *

This page involves a {@code LoginHeader}, an image at the right, + * a login box, an email box, a login button, and a register button.

+ */ public class LoginPageView extends BorderPane { private final AppState appState; private final NavigationController nav; @@ -106,9 +114,9 @@ private Button getLoginBtn() { } public Button getRegisterBtn() { - Button registerBtn = new Button("Don't have an account? Sign In"); + Button registerBtn = new Button("Don't have an account? Sign Up"); registerBtn.setMaxWidth(300); - registerBtn.setOnMouseClicked(e -> nav.showSignInPage()); + registerBtn.setOnMouseClicked(e -> nav.showSignUpPage()); registerBtn.setId("register-btn"); return registerBtn; } diff --git a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java b/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java similarity index 75% rename from src/main/java/edu/group5/app/view/loginpage/SignInPageView.java rename to src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java index 946dae4..160f929 100644 --- a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java +++ b/src/main/java/edu/group5/app/view/loginpage/SignUpPageView.java @@ -12,8 +12,16 @@ import java.util.Objects; - -public class SignInPageView extends BorderPane { +/** + * A view for the SignUp page. + * In this page a user can create an account by writing in first and last name, + * and by adding email and password. If the user already have an account, + * they can press the back to login button to login. + * + *

This view contains a first name field, a surname field, a email field, + * a password field, a sign up button, and an back to login button.

+ */ +public class SignUpPageView extends BorderPane { private final AppState appState; private final NavigationController nav; private final LoginController loginController; @@ -24,7 +32,7 @@ public class SignInPageView extends BorderPane { private PasswordField passwordField; private Label errorLabel; - public SignInPageView(AppState appState, NavigationController nav, LoginController loginController) { + public SignUpPageView(AppState appState, NavigationController nav, LoginController loginController) { this.appState = appState; this.nav = nav; this.loginController = loginController; @@ -36,7 +44,7 @@ public SignInPageView(AppState appState, NavigationController nav, LoginControll content.getChildren().addAll(getOuterSection(), getImageSection()); String css = Objects.requireNonNull( - getClass().getResource("/loginpage/signin.css")).toExternalForm(); + getClass().getResource("/loginpage/signup.css")).toExternalForm(); content.getStylesheets().add(css); setCenter(content); @@ -68,16 +76,16 @@ private VBox getOuterSection() { VBox outerSection = new VBox(12); outerSection.setAlignment(Pos.CENTER); HBox.setHgrow(outerSection, Priority.ALWAYS); - outerSection.getChildren().addAll(getSignInBox(), getBackToLoginBtn()); + outerSection.getChildren().addAll(getSignUpBox(), getBackToLoginBtn()); return outerSection; } - private VBox getSignInBox() { - VBox signInSection = new VBox(12); - signInSection.setAlignment(Pos.CENTER); - signInSection.setId("login-box"); - signInSection.getChildren().addAll(getErrorLabel(), getNameRow(), getEmailBox(), getPasswordBox(), getSignInBtn()); - return signInSection; + private VBox getSignUpBox() { + VBox signUpSection = new VBox(12); + signUpSection.setAlignment(Pos.CENTER); + signUpSection.setId("login-box"); + signUpSection.getChildren().addAll(getErrorLabel(), getNameRow(), getEmailBox(), getPasswordBox(), getSignUpBtn()); + return signUpSection; } private Label getErrorLabel() { @@ -126,18 +134,18 @@ private VBox getPasswordBox() { return passwordBox; } - private Button getSignInBtn() { - Button signInBtn = new Button("Sign In"); - signInBtn.setMaxWidth(300); - signInBtn.setId("login-btn"); - signInBtn.setOnMouseClicked(e -> loginController.handleSignIn( + private Button getSignUpBtn() { + Button signUpBtn = new Button("Sign Up"); + signUpBtn.setMaxWidth(300); + signUpBtn.setId("login-btn"); + signUpBtn.setOnMouseClicked(e -> loginController.handleSignUp( this, getFirstName(), getLastName(), getEmail(), getPassword() )); - return signInBtn; + return signUpBtn; } public Button getBackToLoginBtn() { diff --git a/src/main/resources/loginpage/signin-image.png b/src/main/resources/loginpage/signup-image.png similarity index 100% rename from src/main/resources/loginpage/signin-image.png rename to src/main/resources/loginpage/signup-image.png diff --git a/src/main/resources/loginpage/signin.css b/src/main/resources/loginpage/signup.css similarity index 89% rename from src/main/resources/loginpage/signin.css rename to src/main/resources/loginpage/signup.css index 4ab0276..93ad133 100644 --- a/src/main/resources/loginpage/signin.css +++ b/src/main/resources/loginpage/signup.css @@ -1,5 +1,5 @@ #image-section { - -fx-background-image: url("/loginpage/signin-image.png"); + -fx-background-image: url("/loginpage/signup-image.png"); -fx-background-size: auto; -fx-background-position: right center; -fx-background-repeat: no-repeat; From ab64cda1599c09cccfca940bae3bf9565feeac04 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:47:07 +0200 Subject: [PATCH 14/33] refactor[DbWrapper&ParameterValidator]: move parameter validation in exports to ParameterValidator class --- .../group5/app/model/wrapper/DbWrapper.java | 45 +++---------------- .../group5/app/utils/ParameterValidator.java | 43 ++++++++++++++++++ 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java index f453cc3..c61bbc8 100644 --- a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -1,5 +1,6 @@ package edu.group5.app.model.wrapper; +import java.lang.reflect.Parameter; import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; @@ -15,6 +16,8 @@ import java.util.logging.Level; import java.util.logging.Logger; +import edu.group5.app.utils.ParameterValidator; + /** * A class for wrapping the database. */ @@ -137,30 +140,11 @@ public List importUsers() { */ public int exportUsers(List data) throws IllegalArgumentException { this.importUsers(); - - if (data == null) { - throw new IllegalArgumentException("data can't be null"); - } + if (data.isEmpty()) { return 0; } - // TODO: change to check length for every row. - if (data.get(0).length != 6) { - throw new IllegalArgumentException("data's arrays must have a length of 6"); - } - if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { - throw new IllegalArgumentException("One or more rows in data contains null values"); - } - // TODO: change how existing rows are checked. - if (this.users.size() > 0) { - if ((int) data.getLast()[0] <= (int) this.users.getLast()[0]) { - throw new IllegalArgumentException("data can't contain existing rows"); - } - } - Set ids = new HashSet<>(); - if (data.stream().anyMatch(i -> !ids.add(i[0]))) { - throw new IllegalArgumentException("data can't contain duplicate rows"); - } + ParameterValidator.exportChecker(data, "data", this.users); PreparedStatement ps = null; @@ -258,26 +242,11 @@ private List importDonations(int user_id, boolean all) { */ public int exportDonations(List data) throws IllegalArgumentException { this.fetchAllDonations(); - - if (data == null) { - throw new IllegalArgumentException("data can't be null"); - } + if (data.isEmpty()) { return 0; } - if (data.get(0).length != 6) { - throw new IllegalArgumentException("data's arrays must have a length of 6"); - } - if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { - throw new IllegalArgumentException("One or more rows in data contains null values"); - } - if (this.donations.size() > 0 && (int) data.getLast()[0] <= (int) this.donations.getLast()[0]) { - throw new IllegalArgumentException("data can't contain existing rows"); - } - Set ids = new HashSet<>(); - if (data.stream().anyMatch(i -> !ids.add(i[0]))) { - throw new IllegalArgumentException("data can't contain duplicate rows"); - } + ParameterValidator.exportChecker(data, "data", this.donations); PreparedStatement ps = null; int rowsAffected = 0; diff --git a/src/main/java/edu/group5/app/utils/ParameterValidator.java b/src/main/java/edu/group5/app/utils/ParameterValidator.java index 996bf6e..36a0d14 100644 --- a/src/main/java/edu/group5/app/utils/ParameterValidator.java +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -1,6 +1,10 @@ package edu.group5.app.utils; import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; /** * ParameterValidator is a utility class that provides static methods for validating various types @@ -82,4 +86,43 @@ private static final void nullCheck(Object variable, String variableName) throw new IllegalArgumentException(String.format("%s can't be null", variableName)); } } + + /** + * A method for checking if the data to be exported is valid. + * + * @param data The data to export. + * @param dataName The name of the variable of the data to export. + * @param oldData The existing data to compare to for checking row existence. + * @throws IllegalArgumentException This exception is thrown when data is null, its rows are not + * of length 6, any of the rows are null, any of the rows are duplicates or existing rows in + * the database, or any of the values in the rows can't be cast to the correct data-types. + */ + public static final void exportChecker( + List data, String dataName, List oldData + ) throws IllegalArgumentException { + if (data == null) { + throw new IllegalArgumentException(String.format("%s can't be null", dataName)); + } + // TODO: change to check length for every row. + if (data.get(0).length != 6) { + throw new IllegalArgumentException( + String.format("%s's arrays must have a length of 6", dataName) + ); + } + if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { + throw new IllegalArgumentException( + String.format("One or more rows in %s contains null values", dataName) + ); + } + // TODO: change how existing rows are checked. + if (oldData.size() > 0) { + if ((int) data.getLast()[0] <= (int) oldData.getLast()[0]) { + throw new IllegalArgumentException("data can't contain existing rows"); + } + } + Set ids = new HashSet<>(); + if (data.stream().anyMatch(i -> !ids.add(i[0]))) { + throw new IllegalArgumentException("data can't contain duplicate rows"); + } + } } From 85d6951e2c4c6697864d7516fd3f56ee781684f4 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 9 Apr 2026 12:01:58 +0200 Subject: [PATCH 15/33] update[UserPage]: Update Userpage with more visual appealing Javafx display of user account --- .../app/view/userpage/UserPageView.java | 78 +++++++++++---- src/main/resources/userpage/userpage.css | 94 +++++++++++++++---- 2 files changed, 133 insertions(+), 39 deletions(-) diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index c5d886d..ee5d980 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -12,12 +12,16 @@ import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.text.Text; +import java.text.SimpleDateFormat; import java.util.*; @@ -78,41 +82,46 @@ private VBox createCausesSection() { Text title = new Text("YOUR SUPPORTED CAUSES"); title.getStyleClass().add("section-title"); - VBox causesBox = new VBox(10); - causesBox.getStyleClass().add("section-box"); - causesBox.setPadding(new Insets(10)); - - User currentUser = appState.getCurrentUser(); + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + scrollPane.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); + + FlowPane causesFlow = new FlowPane(10, 10); + causesFlow.setStyle("-fx-padding: 10;"); + causesFlow.getStyleClass().add("section-box"); Set uniqueOrgs = donationController.getUniqueOrgs(); if (uniqueOrgs.isEmpty()) { Label noCauses = new Label("No causes supported yet"); noCauses.setStyle("-fx-text-fill: #999;"); - causesBox.getChildren().add(noCauses); + causesFlow.getChildren().add(noCauses); } else { for (int orgId : uniqueOrgs) { Organization org = organizationController.getOrgById(orgId); if (org != null) { - Label causeLabel = new Label("• " + org.name()); - causesBox.getChildren().add(causeLabel); + causesFlow.getChildren().add(createCauseChip(org)); // SRP: delegate to helper } } } - return new VBox(10, title, causesBox); + scrollPane.setContent(causesFlow); + return new VBox(10, title, scrollPane); } private VBox createDonationsSection() { Text title = new Text("PREVIOUS DONATIONS"); title.getStyleClass().add("section-title"); - VBox donationsBox = new VBox(10); - donationsBox.getStyleClass().add("section-box"); + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + scrollPane.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); + + VBox donationsBox = new VBox(12); + donationsBox.getStyleClass().add("donation-list"); donationsBox.setPadding(new Insets(10)); User currentUser = appState.getCurrentUser(); - Map userDonations = donationController.getUserDonations(currentUser.getUserId()); if (userDonations.isEmpty()) { @@ -121,17 +130,46 @@ private VBox createDonationsSection() { donationsBox.getChildren().add(noDonations); } else { for (Donation donation : userDonations.values()) { - Organization org = organizationController.getOrgById(donation.organizationId()); - String orgName = (org != null) ? org.name() : "Unknown Organization"; - - Label donationLabel = new Label( - orgName + " • " + donation.amount() + " kr" + " • " + donation.date() - ); - donationsBox.getChildren().add(donationLabel); + donationsBox.getChildren().add(createDonationCard(donation)); // SRP: delegate to helper } } - return new VBox(10, title, donationsBox); + scrollPane.setContent(donationsBox); + return new VBox(10, title, scrollPane); } + + private Label createCauseChip(Organization org) { + Label chip = new Label(org.name()); + chip.getStyleClass().add("cause-chip"); + return chip; + } + + private HBox createDonationCard(Donation donation) { + Organization org = organizationController.getOrgById(donation.organizationId()); + String orgName = (org != null) ? org.name() : "Unknown Organization"; + + HBox card = new HBox(20); + card.getStyleClass().add("donation-card"); + card.setPadding(new Insets(12, 15, 12, 15)); + card.setAlignment(Pos.CENTER_LEFT); + + Text orgText = new Text(orgName); + orgText.getStyleClass().add("donation-org-name"); + + VBox details = new VBox(4); + details.setAlignment(Pos.CENTER_RIGHT); + Label amountLabel = + new Label(String.format("%.2f", donation.amount()) + " kr"); + amountLabel.getStyleClass().add("donation-amount"); + Label dateLabel = new Label( + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(donation.date())); + dateLabel.getStyleClass().add("donation-date"); + details.getChildren().addAll(amountLabel, dateLabel); + + card.getChildren().addAll(orgText, details); + HBox.setHgrow(orgText, Priority.ALWAYS); // Org name takes available space + + return card; + } } diff --git a/src/main/resources/userpage/userpage.css b/src/main/resources/userpage/userpage.css index 8401a77..5037280 100644 --- a/src/main/resources/userpage/userpage.css +++ b/src/main/resources/userpage/userpage.css @@ -1,31 +1,87 @@ #profile-name { - -fx-font-size: 28px; - -fx-font-weight: bold; + -fx-font-size: 28px; + -fx-font-weight: bold; } + .profile-info { - -fx-font-size: 16px; - -fx-text-fill: #444; + -fx-font-size: 16px; + -fx-text-fill: #444; } + .section-title { - -fx-font-size: 14px; - -fx-font-weight: bold; - -fx-fill: #888; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-fill: #888; } + .section-box { - -fx-background-color: #ddd; - -fx-pref-height: 120px; - -fx-pref-width: 700px; - -fx-background-radius: 6; + -fx-background-color: #ddd; + -fx-pref-height: 200px; + -fx-pref-width: 700px; + -fx-background-radius: 6; } + .logout-button { - -fx-background-color: #e03030; - -fx-text-fill: white; - -fx-font-size: 14px; - -fx-font-weight: bold; - -fx-padding: 8 20; - -fx-background-radius: 4; - -fx-cursor: hand; + -fx-background-color: #e03030; + -fx-text-fill: white; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-padding: 8 20; + -fx-background-radius: 4; + -fx-cursor: hand; } + .logout-button:hover { - -fx-background-color: #c02020; + -fx-background-color: #c02020; +} + +.cause-chip { + -fx-background-color: #4CAF50; + -fx-text-fill: white; + -fx-padding: 6 12; + -fx-background-radius: 20; + -fx-font-size: 13px; +} + +.cause-chip:hover { + -fx-background-color: #45a049; + -fx-cursor: hand; +} + +.donation-card { + -fx-background-color: #f5f5f5; + -fx-border-color: #ddd; + -fx-border-radius: 6; + -fx-background-radius: 6; +} + +.donation-card:hover { + -fx-background-color: #efefef; + -fx-border-color: #bbb; +} + +.donation-org-name { + -fx-font-size: 15px; + -fx-font-weight: bold; + -fx-fill: #333; +} + +.donation-amount { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: #2196F3; +} + +.donation-date { + -fx-font-size: 12px; + -fx-text-fill: #999; +} + +.donation-list { + -fx-pref-height: 400px; +} + +/* ScrollPane styling */ +.scroll-pane { + -fx-control-inner-background: #fafafa; } \ No newline at end of file From 2f72f759b3a656979fcecf969a81ac23f2ee3145 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:42:28 +0200 Subject: [PATCH 16/33] refactor[OrgApiWrapper]: put off parameter validation to ParameterValidator --- .../java/edu/group5/app/model/wrapper/OrgApiWrapper.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java b/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java index 910b110..2a493e4 100644 --- a/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/OrgApiWrapper.java @@ -1,5 +1,6 @@ package edu.group5.app.model.wrapper; +import edu.group5.app.utils.ParameterValidator; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -24,11 +25,7 @@ public class OrgApiWrapper extends Wrapper { * @param urlString A string of the URL that's being connected to. */ public OrgApiWrapper(String urlString) { - if (urlString == null) { - throw new IllegalArgumentException("url can't be null"); - } else if (urlString.isBlank()) { - throw new IllegalArgumentException("url can't be blank"); - } + ParameterValidator.stringChecker(urlString, "url"); try { URI uri = URI.create(urlString); this.client = HttpClient.newHttpClient(); From 281d68dbba5e843e2e69de88b1638c7618660bd3 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:43:55 +0200 Subject: [PATCH 17/33] chore[utils] remove placeholder util classes --- src/main/java/edu/group5/app/utils/Utilities.java | 5 ----- src/test/java/edu/group5/app/utils/UtilitiesTest.java | 5 ----- 2 files changed, 10 deletions(-) delete mode 100644 src/main/java/edu/group5/app/utils/Utilities.java delete mode 100644 src/test/java/edu/group5/app/utils/UtilitiesTest.java diff --git a/src/main/java/edu/group5/app/utils/Utilities.java b/src/main/java/edu/group5/app/utils/Utilities.java deleted file mode 100644 index ce21d22..0000000 --- a/src/main/java/edu/group5/app/utils/Utilities.java +++ /dev/null @@ -1,5 +0,0 @@ -package edu.group5.app.utils; - -public class Utilities { - -} diff --git a/src/test/java/edu/group5/app/utils/UtilitiesTest.java b/src/test/java/edu/group5/app/utils/UtilitiesTest.java deleted file mode 100644 index 88aa0c9..0000000 --- a/src/test/java/edu/group5/app/utils/UtilitiesTest.java +++ /dev/null @@ -1,5 +0,0 @@ -package edu.group5.app.utils; - -public class UtilitiesTest { - -} From 1a5616e521fbe63361f93797d0217249ce555046 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:45:41 +0200 Subject: [PATCH 18/33] fix[DbWrapper&ParameterValidator]: fix checks so empty data exports skip data validation --- .../group5/app/model/wrapper/DbWrapper.java | 18 ++----- .../group5/app/utils/ParameterValidator.java | 47 +++++++++---------- 2 files changed, 26 insertions(+), 39 deletions(-) diff --git a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java index c61bbc8..f357ae1 100644 --- a/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/model/wrapper/DbWrapper.java @@ -1,6 +1,6 @@ package edu.group5.app.model.wrapper; -import java.lang.reflect.Parameter; +import edu.group5.app.utils.ParameterValidator; import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; @@ -9,15 +9,10 @@ import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; -import edu.group5.app.utils.ParameterValidator; - /** * A class for wrapping the database. */ @@ -141,11 +136,7 @@ public List importUsers() { public int exportUsers(List data) throws IllegalArgumentException { this.importUsers(); - if (data.isEmpty()) { - return 0; - } - ParameterValidator.exportChecker(data, "data", this.users); - + ParameterValidator.exportChecker(data, "data", this.users, 6); PreparedStatement ps = null; int rowsAffected = 0; @@ -243,10 +234,7 @@ private List importDonations(int user_id, boolean all) { public int exportDonations(List data) throws IllegalArgumentException { this.fetchAllDonations(); - if (data.isEmpty()) { - return 0; - } - ParameterValidator.exportChecker(data, "data", this.donations); + ParameterValidator.exportChecker(data, "data", this.donations, 6); PreparedStatement ps = null; int rowsAffected = 0; diff --git a/src/main/java/edu/group5/app/utils/ParameterValidator.java b/src/main/java/edu/group5/app/utils/ParameterValidator.java index 36a0d14..3bc3e5a 100644 --- a/src/main/java/edu/group5/app/utils/ParameterValidator.java +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -98,31 +98,30 @@ private static final void nullCheck(Object variable, String variableName) * the database, or any of the values in the rows can't be cast to the correct data-types. */ public static final void exportChecker( - List data, String dataName, List oldData + List data, String dataName, List oldData, int rowLength ) throws IllegalArgumentException { - if (data == null) { - throw new IllegalArgumentException(String.format("%s can't be null", dataName)); - } - // TODO: change to check length for every row. - if (data.get(0).length != 6) { - throw new IllegalArgumentException( - String.format("%s's arrays must have a length of 6", dataName) - ); - } - if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { - throw new IllegalArgumentException( - String.format("One or more rows in %s contains null values", dataName) - ); - } - // TODO: change how existing rows are checked. - if (oldData.size() > 0) { - if ((int) data.getLast()[0] <= (int) oldData.getLast()[0]) { - throw new IllegalArgumentException("data can't contain existing rows"); + objectChecker(data, dataName); + if (!data.isEmpty()) { + if (data.stream().anyMatch(i -> i.length != rowLength)) { + throw new IllegalArgumentException( + String.format("%s's arrays must have a length of 6", dataName) + ); } - } - Set ids = new HashSet<>(); - if (data.stream().anyMatch(i -> !ids.add(i[0]))) { - throw new IllegalArgumentException("data can't contain duplicate rows"); - } + if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { + throw new IllegalArgumentException( + String.format("One or more rows in %s contains null values", dataName) + ); + } + + Set ids = new HashSet<>(); + if (data.stream().anyMatch(i -> !ids.add(i[0]))) { + throw new IllegalArgumentException("data can't contain duplicate rows"); + } + if (oldData.size() > 0) { + if (oldData.stream().anyMatch(i -> !ids.add(i[0]))) { + throw new IllegalArgumentException("data can't contain existing rows"); + } + } + } } } From ac865375627a2193382b03d6b3cfc1641df2aeab Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:51:28 +0200 Subject: [PATCH 19/33] style[test/wrapper]: order imports and split too long lines --- .../model/wrapper/DbWrapperDonationsTest.java | 32 ++++++++++++------- .../app/model/wrapper/DbWrapperUserTest.java | 22 ++++--------- .../app/model/wrapper/OrgApiWrapperTest.java | 2 -- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java b/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java index cf76092..1db7a90 100644 --- a/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/DbWrapperDonationsTest.java @@ -12,12 +12,12 @@ import java.util.Arrays; import java.util.Date; import java.util.List; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import edu.group5.app.model.wrapper.DbWrapper; - +/** + * A test class for interactions with the donations table using the DbWrapper. + */ public class DbWrapperDonationsTest { private Object[] johnDonation; private List users; @@ -54,13 +54,15 @@ void init() { users.add(row); } - this.johnDonation = new Object[] { 1, 1, 39, new BigDecimal(20.02), new Timestamp(new Date().getTime()), - "Paypal" }; + this.johnDonation = new Object[] { + 1, 1, 39, new BigDecimal(20.02), new Timestamp(new Date().getTime()), "Paypal" + }; this.donations = new ArrayList(); this.donations.add(this.johnDonation); - this.janeDonation = new Object[] { 2, 2, 39, new BigDecimal(20.00), new Timestamp(new Date().getTime()), - "Visa debit card" }; + this.janeDonation = new Object[] { + 2, 2, 39, new BigDecimal(20.00), new Timestamp(new Date().getTime()), "Visa debit card" + }; this.donations2 = new ArrayList(); this.donations2.add(this.johnDonation); this.donations2.add(this.janeDonation); @@ -104,7 +106,12 @@ public void importDonationsIsOnlyExportDonationsTest() { assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); assertTrue(this.db.exportDonations(this.donations) == 1); assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 1); - assertTrue(donationEquals(this.donations.get(0), this.db.importDonations((int) this.users.get(0)[0]).get(0))); + assertTrue( + donationEquals( + this.donations.get(0), + this.db.importDonations((int) this.users.get(0)[0]).get(0) + ) + ); assertTrue(this.db.disconnect()); }); } @@ -157,9 +164,12 @@ public void addingSameDonationTwiceThrowsExpectedException2() { assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); assertTrue(this.db.importDonations((int) this.users.get(1)[0]).size() == 0); assertEquals(2, this.db.exportDonations(this.donations2)); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - this.db.exportDonations(this.donations); - }); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> { + this.db.exportDonations(this.donations); + } + ); assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 1); assertTrue(this.db.importDonations((int) this.users.get(1)[0]).size() == 1); assertTrue(this.db.disconnect()); diff --git a/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java b/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java index 7aca3c0..00107de 100644 --- a/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/DbWrapperUserTest.java @@ -2,27 +2,18 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.math.BigDecimal; -import java.util.Date; -import java.sql.SQLException; -import java.sql.Time; -import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.List; - -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import edu.group5.app.model.wrapper.DbWrapper; -import javafx.util.converter.BigDecimalStringConverter; - +/** + * A class for testing interactions with the users table of the database using DbWrapper. + */ public class DbWrapperUserTest { private Object[] johnDoe; private Object[] janeDoe; @@ -54,8 +45,9 @@ void init() { this.users3 = new ArrayList(); this.users3.add(this.janeDoe); - this.repeatingJoeJoe = new Object[] { 3, "Customer", "Repeating", "JoeJoe", "repeatingjjoe@email.com", - "passwordpassword" }; + this.repeatingJoeJoe = new Object[] { + 3, "Customer", "Repeating", "JoeJoe", "repeatingjjoe@email.com", "passwordpassword" + }; this.repeatedUsers = new ArrayList(); this.repeatedUsers.add(this.repeatingJoeJoe); this.repeatedUsers.add(this.repeatingJoeJoe); @@ -187,6 +179,4 @@ public void addingUserListWithNullInRowThrowsExpectedException() { assertTrue(this.db.disconnect()); assertEquals("One or more rows in data contains null values", exception.getMessage()); } - - } diff --git a/src/test/java/edu/group5/app/model/wrapper/OrgApiWrapperTest.java b/src/test/java/edu/group5/app/model/wrapper/OrgApiWrapperTest.java index 0a643f2..0165456 100644 --- a/src/test/java/edu/group5/app/model/wrapper/OrgApiWrapperTest.java +++ b/src/test/java/edu/group5/app/model/wrapper/OrgApiWrapperTest.java @@ -9,8 +9,6 @@ import java.lang.IllegalArgumentException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - -import edu.group5.app.model.wrapper.OrgApiWrapper; import tools.jackson.core.exc.StreamReadException; /** From 9d915ba679db416a032af138cf3e05ff37fb9c2c Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 9 Apr 2026 14:35:12 +0200 Subject: [PATCH 20/33] Update[UserPage]: Update Visual on UserPage to increase greater UX and CI --- .../app/view/userpage/UserPageView.java | 73 +++++++++++++++---- src/main/resources/userpage/userpage.css | 3 +- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index ee5d980..8dae614 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -13,6 +13,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; @@ -113,8 +114,18 @@ private VBox createDonationsSection() { Text title = new Text("PREVIOUS DONATIONS"); title.getStyleClass().add("section-title"); + HBox searchBox = new HBox(10); + searchBox.setStyle("-fx-padding: 10;"); + TextField searchField = new TextField(); + searchField.setPromptText("Search by organization name..."); + searchField.setPrefWidth(300); + searchBox.getChildren().add(searchField); + ScrollPane scrollPane = new ScrollPane(); - scrollPane.setFitToWidth(true); + scrollPane.setFitToWidth(false); + scrollPane.setPrefWidth(500); + scrollPane.setMaxWidth(500); + scrollPane.setPrefHeight(400); scrollPane.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); VBox donationsBox = new VBox(12); @@ -122,7 +133,40 @@ private VBox createDonationsSection() { donationsBox.setPadding(new Insets(10)); User currentUser = appState.getCurrentUser(); - Map userDonations = donationController.getUserDonations(currentUser.getUserId()); + Map userDonations = + donationController.getUserDonations(currentUser.getUserId()); + + // Filter donations based on search + searchField.textProperty().addListener((obs, oldVal, newVal) -> { + donationsBox.getChildren().clear(); + + if (userDonations.isEmpty()) { + Label noDonations = new Label("No donations yet"); + noDonations.setStyle("-fx-text-fill: #999;"); + donationsBox.getChildren().add(noDonations); + return; + } + + String searchTerm = newVal.toLowerCase().trim(); + boolean found = false; + + for (Donation donation : userDonations.values()) { + Organization org = organizationController.getOrgById(donation.organizationId()); + String orgName = (org != null) ? org.name() : "Unknown Organization"; + + // Filter by search term + if (searchTerm.isEmpty() || orgName.toLowerCase().contains(searchTerm)) { + donationsBox.getChildren().add(createDonationCard(donation)); + found = true; + } + } + + if (!found && !searchTerm.isEmpty()) { + Label noResults = new Label("No donations found for \"" + newVal + "\""); + noResults.setStyle("-fx-text-fill: #999;"); + donationsBox.getChildren().add(noResults); + } + }); if (userDonations.isEmpty()) { Label noDonations = new Label("No donations yet"); @@ -130,12 +174,12 @@ private VBox createDonationsSection() { donationsBox.getChildren().add(noDonations); } else { for (Donation donation : userDonations.values()) { - donationsBox.getChildren().add(createDonationCard(donation)); // SRP: delegate to helper + donationsBox.getChildren().add(createDonationCard(donation)); } } scrollPane.setContent(donationsBox); - return new VBox(10, title, scrollPane); + return new VBox(10, title, searchBox, scrollPane); } @@ -145,31 +189,34 @@ private Label createCauseChip(Organization org) { return chip; } - private HBox createDonationCard(Donation donation) { + private BorderPane createDonationCard(Donation donation) { Organization org = organizationController.getOrgById(donation.organizationId()); String orgName = (org != null) ? org.name() : "Unknown Organization"; - HBox card = new HBox(20); + // Use BorderPane to fix columns: LEFT | SPACE | RIGHT + BorderPane card = new BorderPane(); card.getStyleClass().add("donation-card"); card.setPadding(new Insets(12, 15, 12, 15)); - card.setAlignment(Pos.CENTER_LEFT); + // LEFT: Organization name Text orgText = new Text(orgName); orgText.getStyleClass().add("donation-org-name"); - + card.setLeft(orgText); + + // RIGHT: Amount and date (stacked vertically) VBox details = new VBox(4); details.setAlignment(Pos.CENTER_RIGHT); - Label amountLabel = - new Label(String.format("%.2f", donation.amount()) + " kr"); + + Label amountLabel = new Label(String.format("%.2f", donation.amount()) + " kr"); amountLabel.getStyleClass().add("donation-amount"); + Label dateLabel = new Label( new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(donation.date())); dateLabel.getStyleClass().add("donation-date"); + details.getChildren().addAll(amountLabel, dateLabel); + card.setRight(details); - card.getChildren().addAll(orgText, details); - HBox.setHgrow(orgText, Priority.ALWAYS); // Org name takes available space - return card; } } diff --git a/src/main/resources/userpage/userpage.css b/src/main/resources/userpage/userpage.css index 5037280..8d68841 100644 --- a/src/main/resources/userpage/userpage.css +++ b/src/main/resources/userpage/userpage.css @@ -16,8 +16,7 @@ .section-box { -fx-background-color: #ddd; - -fx-pref-height: 200px; - -fx-pref-width: 700px; + -fx-pref-height: 150px; -fx-background-radius: 6; } From cfef39ae705932765ef8918092cf270d019f3da6 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 9 Apr 2026 15:16:32 +0200 Subject: [PATCH 21/33] fix[]UserPage: fix sizing of donation section --- .../app/view/userpage/UserPageView.java | 4 +- src/main/resources/donationpage/donation.css | 73 ++++++++++--------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index 8dae614..08600f9 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -123,8 +123,8 @@ private VBox createDonationsSection() { ScrollPane scrollPane = new ScrollPane(); scrollPane.setFitToWidth(false); - scrollPane.setPrefWidth(500); - scrollPane.setMaxWidth(500); + scrollPane.setPrefWidth(650); + scrollPane.setMaxWidth(650); scrollPane.setPrefHeight(400); scrollPane.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); diff --git a/src/main/resources/donationpage/donation.css b/src/main/resources/donationpage/donation.css index 32433df..f690337 100644 --- a/src/main/resources/donationpage/donation.css +++ b/src/main/resources/donationpage/donation.css @@ -1,53 +1,58 @@ .donation-button { - -fx-background-color: white; - -fx-border-color: #ccc; - -fx-border-width: 2px; - -fx-border-radius: 8; - -fx-background-radius: 8; - -fx-cursor: hand; - -fx-font-size: 18px; - -fx-font-weight: bold; + -fx-background-color: white; + -fx-border-color: #ccc; + -fx-border-width: 2px; + -fx-border-radius: 8; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-font-size: 18px; + -fx-font-weight: bold; } .donation-button:hover { - -fx-border-color: #f0f0f0; + -fx-border-color: #f0f0f0; } .donation-button-selected { - -fx-background-color: #111; - -fx-text-fill: white; - -fx-border-color: #111; + -fx-background-color: #111; + -fx-text-fill: white; + -fx-border-color: #111; } + .donation-title { - -fx-font-size: 18px; - -fx-font-weight: bold; - -fx-fill: #111; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-fill: #111; } + .donation-amount { - -fx-font-size: 18px; - -fx-fill: #111; + -fx-font-size: 18px; + -fx-fill: #111; } + .donation-input { - -fx-font-size:16px; - -fx-pref-width: 140px; - -fx-background-color: transparent; - -fx-border-color: transparent transparent #333 transparent; - -fx-alignment: center; + -fx-font-size: 16px; + -fx-pref-width: 140px; + -fx-background-color: transparent; + -fx-border-color: transparent transparent #333 transparent; + -fx-alignment: center; } + .donation-input:focused { - -fx-border-color: transparent transparent #4a90d9 transparent; + -fx-border-color: transparent transparent #4a90d9 transparent; } + .donate-button { - -fx-pref-height: 55px; - -fx-background-color: #e03030; - -fx-text-fill: white; - -fx-font-size: 22px; - -fx-font-weight: bold; - -fx-background-radius: 8; - -fx-cursor: hand; - -fx-padding: 0 40 0 40; -} -.donate-button:hover { - -fx-background-color: #c02020; + -fx-pref-height: 55px; + -fx-background-color: #e03030; + -fx-text-fill: white; + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-padding: 0 40 0 40; } +.donate-button:hover { + -fx-background-color: #c02020; +} \ No newline at end of file From bd56a1d22e6df099479c23fc82ddc4b09adaab8c Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Thu, 9 Apr 2026 22:54:26 +0200 Subject: [PATCH 22/33] feat: added javadoc to more pages --- src/main/java/edu/group5/app/view/Header.java | 8 ++++++++ .../app/view/causespage/CausesPageView.java | 17 +++++++++++++++++ .../app/view/causespage/OrganizationCard.java | 10 ++++++++++ .../organizationpage/OrganizationPageView.java | 9 +++++++++ 4 files changed, 44 insertions(+) diff --git a/src/main/java/edu/group5/app/view/Header.java b/src/main/java/edu/group5/app/view/Header.java index 35472e1..6bc6133 100644 --- a/src/main/java/edu/group5/app/view/Header.java +++ b/src/main/java/edu/group5/app/view/Header.java @@ -7,6 +7,14 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.*; +/** + * A main header for the app. + * + *

The header consists of a logo button to homepage, + * a navigation bar with buttons to home page, causes page, + * and about us page. The header also has a profile button + * in the upper right corner.

+ */ public class Header extends BorderPane { private final NavigationController controller; diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 1cd6e83..fbf438a 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -13,6 +13,23 @@ import java.util.Map; import java.util.stream.Collectors; +/** + * A view for the causes page. + * + *

This page allows users to browse and search + * for organizations they may want to donate to. + * Organizations are displayed in a grid layout, + * with four organizations per row. Each organization + * is represented as a clickable card containing its name and logo, + * which navigates to the organization's detail page.

+ * + *

The page includes a search field that filters the + * displayed organizations based on user input.

+ * + *

Logos are fetched asynchronously, and a loading indicator + * is shown while the data is being retrieved. If a logo cannot be loaded, + * a fallback "no image" is displayed.

+ */ public class CausesPageView extends BorderPane { private final AppState appState; private final NavigationController nav; diff --git a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java index 31b25ce..e42ebd5 100644 --- a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java +++ b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java @@ -10,6 +10,16 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; +/** + * OrganizationCard represent a single organization card + * in the causes page. + * + *

The card displays the organization's logo, name, and verification + * checkmark. If no logo is available, a fallback text ("No image") is shown.

+ * + *

The card is clickable. When pressed it navigates + * to the organization's detail page.

+ */ public class OrganizationCard extends VBox { private final AppState appState; private final Organization organization; diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 684a1a6..77ea7ab 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -19,6 +19,15 @@ import java.util.Objects; +/** + * A view for displaying information about a selected organization. + * + *

The page shows the organization's logo, name, and description. + * If no logo is available, a fallback "No image" is displayed.

+ * + *

The page also includes a donate button that navigates to the + * donation page, and a back button to return to the causes page.

+ */ public class OrganizationPageView extends BorderPane { private final AppState appState; private final NavigationController nav; From 427f4e5068ddd602c14cf946b4e4df3a59e0beb1 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Fri, 10 Apr 2026 09:27:43 +0200 Subject: [PATCH 23/33] Step 3: Upgrade Spring Framework Dependency - Compile: SUCCESS Updated spring-core dependency from 6.1.10 to 6.2.0 in pom.xml. Verified main and test source compilation succeeds. Known limitation: full test suite deferred to Final Validation. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 75290fc..5bbba22 100644 --- a/pom.xml +++ b/pom.xml @@ -55,7 +55,7 @@ org.springframework spring-core - 6.1.10 + 6.2.0 org.slf4j From 441155e907935da5de272185e228c669ee9492bc Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Fri, 10 Apr 2026 14:35:42 +0200 Subject: [PATCH 24/33] update[OrganizationPage]: Update OrganizationPage's description to fetch and display the description in a more neatly manner --- .../organization/OrganizationRepository.java | 3 +- .../organization/OrganizationScraper.java | 17 +++++++--- .../OrganizationPageView.java | 21 +++++++++--- .../organizationpage/organizationpage.css | 34 +++++++++++++++---- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java index f31efdc..61155b7 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java @@ -50,7 +50,8 @@ public OrganizationRepository(Object[] input, OrganizationScraper scraper) { boolean trusted = "approved".equalsIgnoreCase((String) contentMap.get("status")); String websiteURL = (String) contentMap.get("url"); boolean isPreApproved = Boolean.TRUE.equals(contentMap.get("is_pre_approved")); - String description = scraper.fetchDescription(websiteURL) != null ? scraper.fetchDescription(websiteURL) : "Information about " + name; + String description = scraper.fetchDescription(websiteURL); + description = description != null ? description : "Information about " + name; Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description, null); grandMap.put(org.orgNumber(), org); diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java index edb41fd..c2c7f3b 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -3,6 +3,9 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; +import org.jsoup.nodes.TextNode; +import org.jsoup.select.Elements; + import java.util.stream.Collectors; import java.util.HashMap; @@ -47,18 +50,22 @@ public String fetchDescription(String pageUrl) { Element section = doc.selectFirst("section.information"); if (section != null) { - // Try to get all

tags (skip first one if multiple exist) - String description = section.select("p").stream() - .skip(1) // Skip first paragraph (usually a heading) + section.select("div.extra-info").remove(); + section.select("a.read-more").remove(); + + // Extract all

tags and

elements as separate paragraphs + String description = section.select("p, div").stream() + .filter(el -> !el.hasClass("extra-info") && !el.hasClass("logo")) .map(Element::text) + .map(text -> text.replace("Les mer", "").trim()) .filter(text -> !text.isBlank()) - .map(String::trim) .collect(Collectors.joining("\n\n")); - // Fallback: if no paragraphs after first, get all text from section + // Fallback: if no paragraphs found, get all text from section if (description.isBlank()) { description = section.text().trim(); } + description = description.replace("Les mer", "").trim(); // Only cache and return if we found something meaningful if (!description.isBlank()) { diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 2d10328..f9dbd22 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -99,19 +99,30 @@ private VBox createOrgInfoSection() { VBox descriptionBox = new VBox(); descriptionBox.setSpacing(15); descriptionBox.setId("description-container"); + descriptionBox.setMaxWidth(750); if (org != null && org.description() != null) { - String[] paragraphs = org.description().split("\n\n"); - for (String para : paragraphs) { - if (!para.isBlank()) { - Label paragraph = new Label(para.trim()); + String[] rawParagraphs = org.description().split("\n{2,}"); + + for (String para : rawParagraphs) { + String cleaned = para.trim(); + if (!cleaned.isBlank()) { + Label paragraph = new Label(cleaned); + paragraph.setId("description-paragraph"); paragraph.setWrapText(true); descriptionBox.getChildren().add(paragraph); } } } - orgNameAndDescription.getChildren().addAll(orgName, descriptionBox); + ScrollPane descriptionScroll = new ScrollPane(descriptionBox); + descriptionScroll.setId("description-scroll"); + descriptionScroll.setFitToWidth(true); + descriptionScroll.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); + descriptionScroll.setPrefHeight(400); + descriptionScroll.setMaxHeight(400); + + orgNameAndDescription.getChildren().addAll(orgName, descriptionScroll); Button donateBtn = new Button("Donate"); donateBtn.setId("donate-button"); diff --git a/src/main/resources/organizationpage/organizationpage.css b/src/main/resources/organizationpage/organizationpage.css index e5263f2..17d2b0c 100644 --- a/src/main/resources/organizationpage/organizationpage.css +++ b/src/main/resources/organizationpage/organizationpage.css @@ -13,19 +13,21 @@ } #description-container { - -fx-padding: 30 0 30 0; - -fx-spacing: 25; + -fx-padding: 30; + -fx-spacing: 22; + -fx-max-width: 750; + -fx-background-color: #f8f9fa; + -fx-border-radius: 8; } #description-paragraph { - -fx-font-size: 16; - -fx-text-fill: #222; + -fx-font-size: 18; + -fx-text-fill: #333; -fx-font-family: "Segoe UI", Arial, sans-serif; - -fx-padding: 15 50 15 50; - -fx-line-spacing: 6; + -fx-line-spacing: 13; -fx-text-alignment: Left; -fx-wrap-text: true; - + -fx-padding: 10 0 10 0; } #donate-button { @@ -42,4 +44,22 @@ #donate-button:hover { -fx-background-color: #c02020; +} + +#description-scroll { + -fx-control-inner-background: #f8f9fa; + -fx-padding: 0; +} + +#description-scroll .scroll-bar:vertical { + -fx-pref-width: 8; +} + +#description-scroll .scroll-bar:vertical .thumb { + -fx-background-radius: 4; + -fx-background-color: #cccccc; +} + +#description-scroll .scroll-bar:vertical .thumb:hover { + -fx-background-color: #999999; } \ No newline at end of file From 4edd823e950aa8a0ba9a22d7414303d54940f321 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Fri, 10 Apr 2026 22:14:35 +0200 Subject: [PATCH 25/33] fix&update[OrganizationPage]: Fix duplication and update paragraph spacing on the description fetched from API --- .../group5/app/model/organization/OrganizationScraper.java | 1 + .../app/view/organizationpage/OrganizationPageView.java | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java index c2c7f3b..6a8c230 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -55,6 +55,7 @@ public String fetchDescription(String pageUrl) { // Extract all

tags and

elements as separate paragraphs String description = section.select("p, div").stream() + .filter(el -> el.tagName().equals("p") || el.select("p").isEmpty()) .filter(el -> !el.hasClass("extra-info") && !el.hasClass("logo")) .map(Element::text) .map(text -> text.replace("Les mer", "").trim()) diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index f9dbd22..bbc1666 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -61,7 +61,7 @@ private StackPane createImageContainer() { imageContainer.setId("imageContainer"); imageContainer.setPrefHeight(120); imageContainer.setPrefWidth(120); - imageContainer.setMaxWidth(Double.MAX_VALUE); + imageContainer.setMaxWidth(120); Organization org = appState.getCurrentOrganization(); if (org != null && org.logoUrl() != null && !org.logoUrl().isBlank()) { @@ -121,6 +121,8 @@ private VBox createOrgInfoSection() { descriptionScroll.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); descriptionScroll.setPrefHeight(400); descriptionScroll.setMaxHeight(400); + descriptionScroll.setPrefWidth(750); + descriptionScroll.setMinWidth(750); orgNameAndDescription.getChildren().addAll(orgName, descriptionScroll); From 5767aaab5cc6485f317899ee0c8205e5c6b60c94 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Sun, 12 Apr 2026 18:33:17 +0200 Subject: [PATCH 26/33] update[DonationPage]: Refactor donation selection, add confirmation dialog, and improve custom amount UI --- .../app/control/DonationController.java | 54 +++++++-- .../view/donationpage/DonationPageView.java | 104 +++++++++++------- src/main/resources/donationpage/donation.css | 38 +++++++ 3 files changed, 152 insertions(+), 44 deletions(-) diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 97a1c9d..907caa3 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -12,6 +12,9 @@ import java.util.Map; import java.util.Set; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; + public class DonationController { private final AppState appState; private final NavigationController nav; @@ -38,26 +41,55 @@ public Set getUniqueOrgs() { return uniqueOrgs; } - public void handleDonate() { - // Get session data from MainController + public void requestDonationConfirmation() { + // Get session data User currentUser = appState.getCurrentUser(); Organization currentOrg = appState.getCurrentOrganization(); BigDecimal amount = appState.getCurrentDonationAmount(); + // Validate before showing dialog if (currentUser == null) { - System.err.println("Error: No user logged in"); + showError("Error: No user logged in"); return; } - if (!(currentUser instanceof Customer customer)) { - System.err.println("Error: Only customers can donate"); + if (!(currentUser instanceof Customer)) { + showError("Error: Only customers can donate"); return; } if (currentOrg == null) { - System.err.println("Error: No organization selected"); + showError("Error: No organization selected"); return; } if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { - System.err.println("Error: Invalid donation amount"); + showError("Please select a donation amount first"); + return; + } + + // Show confirmation dialog + Alert confirmDialog = new Alert(Alert.AlertType.CONFIRMATION); + confirmDialog.setTitle("Confirm Donation"); + confirmDialog.setHeaderText("Confirm Your Donation"); + confirmDialog.setContentText( + "Organization: " + currentOrg.name() + "\n" + + "Amount: " + amount + " kr\n\n" + + "Are you sure you want to proceed?" + ); + + // If user clicks OK, process donation + if (confirmDialog.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { + handleDonate(); + } + // If Cancel, dialog just closes and nothing happens + } + + private void handleDonate() { + // This now only handles the actual donation processing + User currentUser = appState.getCurrentUser(); + Organization currentOrg = appState.getCurrentOrganization(); + BigDecimal amount = appState.getCurrentDonationAmount(); + + if (!(currentUser instanceof Customer customer)) { + System.err.println("Error: Only customers can donate"); return; } @@ -81,4 +113,12 @@ public void handleDonate() { // Navigate to payment complete nav.showPaymentCompletePage(); } + + private void showError(String message) { + Alert errorAlert = new Alert(Alert.AlertType.WARNING); + errorAlert.setTitle("Donation Error"); + errorAlert.setHeaderText("Cannot Process Donation"); + errorAlert.setContentText(message); + errorAlert.showAndWait(); + } } diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 5087267..49a9a73 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -5,8 +5,8 @@ import edu.group5.app.model.AppState; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.Button; import javafx.scene.control.TextField; +import javafx.scene.control.Button; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.TilePane; @@ -16,18 +16,14 @@ import javafx.scene.Node; import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; public class DonationPageView extends BorderPane { private final AppState appState; private final NavigationController nav; private final DonationController donationController; - private final List allDonationElements = new ArrayList<>(); - private final Map elementAmounts = new HashMap<>(); + private Node currentlySelected = null; + private TextField customAmountField; public DonationPageView(AppState appState, NavigationController nav, DonationController donationController) { this.appState = appState; @@ -38,6 +34,12 @@ public DonationPageView(AppState appState, NavigationController nav, DonationCon VBox content = new VBox(); content.getChildren().addAll(createDonationGrid(), createDonateSection()); + + content.setOnMouseClicked(e -> { + if (e.getTarget() == content) { + clearSelection(); + } + }); setCenter(content); } @@ -75,14 +77,12 @@ public Button createDonationButton(String title, String amount) { button.getStyleClass().add("donation-button"); BigDecimal parsedAmount = parseAmount(amount); - elementAmounts.put(button, parsedAmount); + button.setUserData(parsedAmount); - button.setOnAction(e -> { - selectDonationElement(button); - }); - allDonationElements.add(button); + button.setOnAction(e -> selectDonation(button)); return button; } + private VBox createCustomButton() { Text titleText = new Text("Custom Donation"); titleText.getStyleClass().add("donation-title"); @@ -93,8 +93,10 @@ private VBox createCustomButton() { Text krText = new Text("kr"); krText.getStyleClass().add("donation-amount"); - TextField amountField = new TextField(); + this.customAmountField = new TextField(); + TextField amountField = customAmountField; amountField.getStyleClass().add("donation-input"); + amountField.setPromptText("Enter amount"); amountRow.getChildren().addAll(amountField, krText); @@ -103,48 +105,76 @@ private VBox createCustomButton() { box.getStyleClass().add("donation-button"); box.setOnMouseClicked(e -> { - try { - BigDecimal amount = new BigDecimal(amountField.getText().trim()); - elementAmounts.put(box, amount); - selectDonationElement(box); - } catch (NumberFormatException exception) { - System.err.println("Invalid custom donation amount: " + amountField.getText()); - } + selectDonation(box); + amountField.requestFocus(); + }); + + amountField.setOnMouseClicked(e -> + { + selectDonation(box); + amountField.requestFocus(); }); - allDonationElements.add(box); + // NEW: On text field input - update the amount in real-time + amountField.textProperty().addListener((obs, oldVal, newVal) -> { + if (!newVal.trim().isEmpty()) { + try { + BigDecimal amount = new BigDecimal(newVal.trim()); + box.setUserData(amount); + updateDonationAmount(amount); + } catch (NumberFormatException ignored) { + // User is still typing, silently ignore + } + } else { + box.setUserData(null); + if (currentlySelected == box) { + updateDonationAmount(null); + } + } + }); return box; } + private HBox createDonateSection() { Button donateBtn = new Button("Donate"); donateBtn.getStyleClass().add("donate-button"); - donateBtn.setOnAction(e -> donationController.handleDonate()); + donateBtn.setOnAction(e -> donationController.requestDonationConfirmation()); - HBox section = new HBox(donateBtn); + Button clearBtn = new Button("Clear"); + clearBtn.getStyleClass().add("clear-button"); + clearBtn.setOnAction(e -> clearSelection()); + + HBox section = new HBox(20, clearBtn, donateBtn); section.setAlignment(Pos.CENTER); section.setPadding(new Insets(20, 0, 30, 0)); return section; } - private void selectDonationElement(Node element) { - // Remove selected class from all elements - for (Node node : allDonationElements) { - node.getStyleClass().remove("donation-button-selected"); + private void selectDonation(Node element) { + if (currentlySelected != null) { + currentlySelected.getStyleClass().remove("donation-button-selected"); } + currentlySelected = element; + currentlySelected.getStyleClass().add("donation-button-selected"); - element.getStyleClass().add("donation-button-selected"); - - // Extract and store the amount - extractAndStoreAmount(element); + BigDecimal amount = (BigDecimal) element.getUserData(); + updateDonationAmount(amount); } - private void extractAndStoreAmount(Node element) { - BigDecimal amount = elementAmounts.get(element); - if (amount != null) { - appState.setCurrentDonationAmount(amount); - } else { - System.err.println("Error: No amount found for selected element"); + private void clearSelection() { + if (currentlySelected != null) { + currentlySelected.getStyleClass().remove("donation-button-selected"); + currentlySelected = null; + updateDonationAmount(null); } + + if (customAmountField != null) { + customAmountField.clear(); + } + } + + private void updateDonationAmount(BigDecimal amount) { + appState.setCurrentDonationAmount(amount); } private BigDecimal parseAmount(String amountStr) { diff --git a/src/main/resources/donationpage/donation.css b/src/main/resources/donationpage/donation.css index f690337..edacdd3 100644 --- a/src/main/resources/donationpage/donation.css +++ b/src/main/resources/donationpage/donation.css @@ -55,4 +55,42 @@ .donate-button:hover { -fx-background-color: #c02020; +} + +.clear-button { + -fx-pref-height: 55px; + -fx-background-color: #f0f0f0; + -fx-text-fill: #333; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-padding: 0 30 0 30; + -fx-border-color: #ccc; + -fx-border-width: 1; +} + +.clear-button:hover { + -fx-background-color: #e0e0e0; +} + +.clear-button:pressed { + -fx-background-color: #d0d0d0; +} + +.donation-button-selected .donation-title { + -fx-fill: white; +} + +.donation-button-selected .donation-amount { + -fx-fill: white; +} + +.donation-button-selected .donation-input { + -fx-text-fill: white; +} + +.donation-button-selected .donation-input:focused { + -fx-text-fill: white; + -fx-border-color: transparent transparent white transparent; } \ No newline at end of file From b1a1fb8134fe2d707e40cb66746fa12cc235ad40 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 14 Apr 2026 13:33:22 +0200 Subject: [PATCH 27/33] feat&Update[donationPage]: Add PaymentMethod and backButton from release, fixing up upcoming mege conflict --- .../view/donationpage/DonationPageView.java | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index 49a9a73..bdc8b47 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -16,6 +16,7 @@ import javafx.scene.Node; import java.math.BigDecimal; +import java.util.Objects; public class DonationPageView extends BorderPane { private final AppState appState; @@ -24,25 +25,38 @@ public class DonationPageView extends BorderPane { private Node currentlySelected = null; private TextField customAmountField; + private Node selectedPaymentMethod = null; + private Button donateBtn; public DonationPageView(AppState appState, NavigationController nav, DonationController donationController) { this.appState = appState; this.nav = nav; this.donationController = donationController; - getStylesheets().add(getClass().getResource("/donationpage/donation.css").toExternalForm()); + getStylesheets().add(Objects.requireNonNull(getClass().getResource("/donationpage/donation.css")).toExternalForm()); VBox content = new VBox(); - content.getChildren().addAll(createDonationGrid(), createDonateSection()); + content.getChildren().addAll(createBackButton(), createDonationGrid(), createPaymentMethodSection(), createDonateSection()); content.setOnMouseClicked(e -> { if (e.getTarget() == content) { - clearSelection(); + clearSelection(); } }); setCenter(content); } + + private HBox createBackButton() { + Button backBtn = new Button("←"); + backBtn.getStyleClass().add("back-button"); + backBtn.setOnAction(e -> nav.showOrganizationPage()); + + HBox container = new HBox(backBtn); + container.setPadding(new Insets(10, 0, 0, 10)); + return container; + } + private TilePane createDonationGrid(){ TilePane body = new TilePane(); body.setAlignment(Pos.CENTER); @@ -136,8 +150,9 @@ private VBox createCustomButton() { } private HBox createDonateSection() { - Button donateBtn = new Button("Donate"); + donateBtn = new Button("Donate"); donateBtn.getStyleClass().add("donate-button"); + donateBtn.setDisable(true); donateBtn.setOnAction(e -> donationController.requestDonationConfirmation()); Button clearBtn = new Button("Clear"); @@ -150,6 +165,23 @@ private HBox createDonateSection() { return section; } + public HBox createPaymentMethodSection() { + Button appleBtn = new Button("Apple Pay"); + Button vippsBtn = new Button("Vipps"); + Button visaBtn = new Button("Visa"); + + for (Button btn : new Button[]{appleBtn, vippsBtn, visaBtn}) { + btn.getStyleClass().add("payment-method-button"); + btn.setOnAction(e -> selectPaymentMethod(btn)); + } + + HBox sectionPm = new HBox(appleBtn, vippsBtn, visaBtn); + sectionPm.setAlignment(Pos.CENTER); + sectionPm.setSpacing(20); + sectionPm.setPadding(new Insets(20, 20, 20, 20)); + return sectionPm; + } + private void selectDonation(Node element) { if (currentlySelected != null) { currentlySelected.getStyleClass().remove("donation-button-selected"); @@ -159,6 +191,16 @@ private void selectDonation(Node element) { BigDecimal amount = (BigDecimal) element.getUserData(); updateDonationAmount(amount); + updateDonationButtonState(); + } + + private void selectPaymentMethod(Node element) { + if (selectedPaymentMethod != null) { + selectedPaymentMethod.getStyleClass().remove("payment-method-selected"); + } + selectedPaymentMethod = element; + selectedPaymentMethod.getStyleClass().add("payment-method-selected"); + updateDonationButtonState(); } private void clearSelection() { @@ -168,9 +210,16 @@ private void clearSelection() { updateDonationAmount(null); } + if (selectedPaymentMethod != null) { + selectedPaymentMethod.getStyleClass().remove("payment-method-selected"); + selectedPaymentMethod = null; + } + if (customAmountField != null) { customAmountField.clear(); } + + updateDonationButtonState(); } private void updateDonationAmount(BigDecimal amount) { @@ -185,4 +234,8 @@ private BigDecimal parseAmount(String amountStr) { } } + private void updateDonationButtonState() { + donateBtn.setDisable(currentlySelected == null || selectedPaymentMethod == null); + } + } \ No newline at end of file From 0556c5b534e6bdc03c1df585d569fcf7dcba2453 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 14 Apr 2026 14:07:56 +0200 Subject: [PATCH 28/33] fix&Update[DonationPage]: fix up merge conflict problems and update donationPage to also include payment method --- .../java/edu/group5/app/control/DonationController.java | 4 +++- .../edu/group5/app/view/donationpage/DonationPageView.java | 7 +++++++ .../java/edu/group5/app/view/userpage/UserPageView.java | 5 ----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 8e965ed..a579a9b 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -72,7 +72,8 @@ public void requestDonationConfirmation() { confirmDialog.setHeaderText("Confirm Your Donation"); confirmDialog.setContentText( "Organization: " + currentOrg.name() + "\n" + - "Amount: " + amount + " kr\n\n" + + "Amount: " + amount + " kr\n" + + "Payment Method: " + paymentMethod + "\n\n" + "Are you sure you want to proceed?" ); @@ -88,6 +89,7 @@ private void handleDonate() { User currentUser = appState.getCurrentUser(); Organization currentOrg = appState.getCurrentOrganization(); BigDecimal amount = appState.getCurrentDonationAmount(); + String paymentMethod = appState.getCurrentPaymentMethod(); if (!(currentUser instanceof Customer customer)) { System.err.println("Error: Only customers can donate"); diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index bdc8b47..0c5c6c3 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -170,6 +170,10 @@ public HBox createPaymentMethodSection() { Button vippsBtn = new Button("Vipps"); Button visaBtn = new Button("Visa"); + appleBtn.setUserData("Apple Pay"); + vippsBtn.setUserData("Vipps"); + visaBtn.setUserData("Visa"); + for (Button btn : new Button[]{appleBtn, vippsBtn, visaBtn}) { btn.getStyleClass().add("payment-method-button"); btn.setOnAction(e -> selectPaymentMethod(btn)); @@ -200,6 +204,9 @@ private void selectPaymentMethod(Node element) { } selectedPaymentMethod = element; selectedPaymentMethod.getStyleClass().add("payment-method-selected"); + + String paymentMethod = (String) element.getUserData(); + appState.setCurrentPaymentMethod(paymentMethod); updateDonationButtonState(); } diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index 7dc82a2..6d46f5b 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -105,8 +105,6 @@ private VBox createCausesSection() { } } } - ScrollPane scrollPane = new ScrollPane(causesBox); - scrollPane.setFitToWidth(true); scrollPane.setPrefHeight(150); scrollPane.setContent(causesFlow); @@ -181,9 +179,6 @@ private VBox createDonationsSection() { } } - ScrollPane scrollPane = new ScrollPane(donationsBox); - scrollPane.setFitToWidth(true); - scrollPane.setPrefHeight(200); scrollPane.setContent(donationsBox); return new VBox(10, title, searchBox, scrollPane); From d792af2e54bcea8079e757e76ae04740e9de9be2 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:19:21 +0200 Subject: [PATCH 29/33] update[LoginController]: improve password handling by clearing passward char array, and by not saving a password String as a variable --- .../java/edu/group5/app/control/LoginController.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/group5/app/control/LoginController.java b/src/main/java/edu/group5/app/control/LoginController.java index cdd5b5f..2a860c9 100644 --- a/src/main/java/edu/group5/app/control/LoginController.java +++ b/src/main/java/edu/group5/app/control/LoginController.java @@ -5,6 +5,9 @@ import edu.group5.app.model.user.UserService; import edu.group5.app.view.loginpage.LoginPageView; import edu.group5.app.view.loginpage.SignInPageView; + +import java.util.Arrays; + import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class LoginController { @@ -27,9 +30,13 @@ public void handleSignIn(SignInPageView view, String firstName, String lastName, return; } - String password = new String(passwordChars); BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - String hashedPassword = encoder.encode(password); + + // Clears password char array after creating a hash. + String hashedPassword = encoder.encode(new String(passwordChars)); + for (int i = 0; i < passwordChars.length; i++) { + passwordChars[0] = '0'; + } boolean success = userService.registerUser( "Customer", firstName, lastName, email, hashedPassword); From d6c8d3baa2aa39ab62ab00307ec707ac6dc9ce33 Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:20:19 +0200 Subject: [PATCH 30/33] feat[LoginController]: add dialog box for accepting privacy policy when signing up --- .../group5/app/control/LoginController.java | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/group5/app/control/LoginController.java b/src/main/java/edu/group5/app/control/LoginController.java index 2a860c9..59a1785 100644 --- a/src/main/java/edu/group5/app/control/LoginController.java +++ b/src/main/java/edu/group5/app/control/LoginController.java @@ -5,6 +5,9 @@ import edu.group5.app.model.user.UserService; import edu.group5.app.view.loginpage.LoginPageView; import edu.group5.app.view.loginpage.SignInPageView; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; import java.util.Arrays; @@ -38,17 +41,30 @@ public void handleSignIn(SignInPageView view, String firstName, String lastName, passwordChars[0] = '0'; } - boolean success = userService.registerUser( - "Customer", firstName, lastName, email, hashedPassword); + Alert privacyPolicy = new Alert(Alert.AlertType.CONFIRMATION); + privacyPolicy.setTitle("Accept Privacy Policy"); + privacyPolicy.setHeaderText("Accept Privacy Policy"); + privacyPolicy.setContentText( + "Your user information like:\n" + + "Name and email—as well as donations tied to your account—will be saved locally on your machine.\n" + + "By creating an account, you accept the right of our app to store this information."); - if (success) { - User user = userService.getUserByEmail(email); + if (privacyPolicy.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { + boolean success = userService.registerUser( + "Customer", firstName, lastName, email, hashedPassword); - appState.setCurrentUser(user); - nav.showHomePage(); + if (success) { + User user = userService.getUserByEmail(email); + + appState.setCurrentUser(user); + nav.showHomePage(); + } else { + view.showError("Registration failed. Email may already be in use."); + } } else { - view.showError("Registration failed. Email may already be in use."); + view.showError("Registration failed. Must Accept Privacy Policy to create account."); } + } public void handleLogin(LoginPageView view, String email, char[] passwordChars) { From 85f47f736ae6a40ac6311f734e19fc505e3ce20b Mon Sep 17 00:00:00 2001 From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:28:05 +0200 Subject: [PATCH 31/33] fix[UserService]: perform a check for existing emails when registering a new user --- .../group5/app/model/user/UserService.java | 68 ++++++++++++------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/src/main/java/edu/group5/app/model/user/UserService.java b/src/main/java/edu/group5/app/model/user/UserService.java index 628c785..b237eae 100644 --- a/src/main/java/edu/group5/app/model/user/UserService.java +++ b/src/main/java/edu/group5/app/model/user/UserService.java @@ -1,16 +1,21 @@ package edu.group5.app.model.user; /** - * Service class for managing user-related operations, such as registration and login. - * It interacts with the UserRepository to perform these operations and contains the business logic - * associated with user management, including validation of input data and handling of user authentication. + * Service class for managing user-related operations, such as registration and + * login. + * It interacts with the UserRepository to perform these operations and contains + * the business logic + * associated with user management, including validation of input data and + * handling of user authentication. */ public class UserService { private UserRepository userRepository; /** * Constructs a UserService with the given UserRepository. - * @param userRepository the UserRepository to use for managing user data; must not be null + * + * @param userRepository the UserRepository to use for managing user data; must + * not be null * @throws IllegalArgumentException if userRepository is null */ public UserService(UserRepository userRepository) { @@ -22,7 +27,9 @@ public UserService(UserRepository userRepository) { /** * Getter for the UserRepository used by this service. - * This method allows access to the user repository for managing user data and performing operations such as registration and login. + * This method allows access to the user repository for managing user data and + * performing operations such as registration and login. + * * @return the UserRepository instance used by this service */ public UserRepository getUserRepository() { @@ -30,31 +37,39 @@ public UserRepository getUserRepository() { } /** - * Registers a new user with the given information. Validates the input data and creates a new User object - * based on the specified role. Currently supports registration for customers only. - * @param role the role of the user (e.g., "Customer"); must not be null or empty - * @param firstName the first name of the user; must not be null or empty - * @param lastName the last name of the user; must not be null or empty - * @param email the email address of the user; must not be null or empty - * @param passwordHash the hashed password of the user; must not be null or empty - * @return true if the user was successfully registered, false if any input is invalid or - * if the role is not supported - * @throws IllegalArgumentException if any of the input parameters are null or empty - * or if the role is not supported + * Registers a new user with the given information. Validates the input data and + * creates a new User object + * based on the specified role. Currently supports registration for customers + * only. + * + * @param role the role of the user (e.g., "Customer"); must not be null + * or empty + * @param firstName the first name of the user; must not be null or empty + * @param lastName the last name of the user; must not be null or empty + * @param email the email address of the user; must not be null or empty + * @param passwordHash the hashed password of the user; must not be null or + * empty + * @return true if the user was successfully registered, false if any input is + * invalid or + * if the role is not supported + * @throws IllegalArgumentException if any of the input parameters are null or + * empty + * or if the role is not supported */ public boolean registerUser(String role, String firstName, String lastName, - String email, String passwordHash) { + String email, String passwordHash) { if (role == null || role.trim().isEmpty() || firstName == null || firstName.trim().isEmpty() || lastName == null || lastName.trim().isEmpty() || email == null || email.trim().isEmpty() || - passwordHash == null || passwordHash.trim().isEmpty()) { + passwordHash == null || passwordHash.trim().isEmpty() || + this.getUserByEmail(email) != null) { return false; } User user; if (role.equalsIgnoreCase("Customer")) { user = new Customer(userRepository.getNextUserId(), firstName, lastName, email, passwordHash); - } else { /* TODO when you switch to a real DB, replace getNextUserId with DB auto-increment/identity and ignore manual ID generation in service*/ + } else { return false; } this.userRepository.addContent(user); @@ -63,15 +78,19 @@ public boolean registerUser(String role, String firstName, String lastName, /** * Authenticates a user based on the provided email and password. - * @param email the email address of the user attempting to log in; must not be null or empty - * @param password the plaintext password of the user attempting to log in; must not be null or empty + * + * @param email the email address of the user attempting to log in; must not + * be null or empty + * @param password the plaintext password of the user attempting to log in; must + * not be null or empty * @return the authenticated User object if the login is successful - * (i.e., the user exists and the password is correct), null otherwise - * @throws IllegalArgumentException if email is null or empty, or if password is null or empty + * (i.e., the user exists and the password is correct), null otherwise + * @throws IllegalArgumentException if email is null or empty, or if password is + * null or empty */ public User login(String email, char[] password) { if (email == null || email.trim().isEmpty() || password == null || password.length == 0) { - return null; + return null; } User user = this.userRepository.findUserByEmail(email); if (user != null && user.verifyPassword(password)) { @@ -82,6 +101,7 @@ public User login(String email, char[] password) { /** * Retrieves a user by email address. + * * @param email the email address of the user to find; must not be null or empty * @return the User object if found, null otherwise */ From 26c615c4e348477c9f2aa500782acb28fa4babad Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Tue, 14 Apr 2026 16:21:29 +0200 Subject: [PATCH 32/33] fix: button styling for donationpage --- src/main/resources/donationpage/donation.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/donationpage/donation.css b/src/main/resources/donationpage/donation.css index 18680e3..bc777b6 100644 --- a/src/main/resources/donationpage/donation.css +++ b/src/main/resources/donationpage/donation.css @@ -108,7 +108,7 @@ .donation-button-selected .donation-input:focused { -fx-text-fill: white; -fx-border-color: transparent transparent white transparent; - +} .payment-method-button { -fx-background-color: #111; -fx-text-fill: white; From 1a32ddd488895b076ff293017302f887c7400f06 Mon Sep 17 00:00:00 2001 From: MatheaGjerde Date: Tue, 14 Apr 2026 16:22:02 +0200 Subject: [PATCH 33/33] fix: size of causes section --- src/main/java/edu/group5/app/view/userpage/UserPageView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index 6d46f5b..e3b7c8b 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -105,7 +105,7 @@ private VBox createCausesSection() { } } } - scrollPane.setPrefHeight(150); + scrollPane.setPrefHeight(275); scrollPane.setContent(causesFlow); return new VBox(10, title, scrollPane);