From 316d43c6872bf0db73fac086b5ace98ef58f8f83 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Tue, 24 Mar 2026 13:30:26 +0100 Subject: [PATCH 01/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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 85d6951e2c4c6697864d7516fd3f56ee781684f4 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 9 Apr 2026 12:01:58 +0200 Subject: [PATCH 10/23] 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 9d915ba679db416a032af138cf3e05ff37fb9c2c Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Thu, 9 Apr 2026 14:35:12 +0200 Subject: [PATCH 11/23] 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 12/23] 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 427f4e5068ddd602c14cf946b4e4df3a59e0beb1 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Fri, 10 Apr 2026 09:27:43 +0200 Subject: [PATCH 13/23] 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 14/23] 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 15/23] 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 16/23] 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 17/23] 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 18/23] 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 19/23] 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 20/23] 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 21/23] 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 22/23] 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 23/23] 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);