Skip to content

Commit

Permalink
Merge branch 'release/v2.0.0' into chore/update/readme-final-touches
Browse files Browse the repository at this point in the history
  • Loading branch information
emilfa authored Apr 24, 2026
2 parents ae6d4bc + 672ec96 commit 271d9a3
Show file tree
Hide file tree
Showing 23 changed files with 490 additions and 98 deletions.
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Help-Me-Help - IDATT1005 Team 5 Portfolio Project Spring 2026 :octocat:

**TEAM 5 STUDENT NAMES**

<br>
Expand Down Expand Up @@ -61,8 +62,8 @@ src/main/java/edu/group5/app/
│ │ ├── DbWrapper.java (H2 database connection & operations)
│ │ └── OrgApiWrapper.java (Innsamlingskontrollen API client)
│ ├── AppState.java (Global application state)
│ ├── Repository.java (Base repository interface)
│ └── DBRepository.java (Database repository interface)
│ ├── Repository.java (Base repository abstract)
│ └── DBRepository.java (Database repository abstract)
|
├── view/ (JavaFX UI components)
│ ├── loginpage/
Expand Down Expand Up @@ -108,7 +109,6 @@ src/main/resources/ (Static assets - CSS, images,
└── donationpage.css (Donation flow styling)
</pre>


### 📦 Package Responsibilities

#### Models: Business logic and data entities
Expand Down Expand Up @@ -178,7 +178,7 @@ The project uses the standard Maven directory structure, which ensures:

## Link to repository📚

https://git.ntnu.no/Group-5/Help-Me-Help
<https://git.ntnu.no/Group-5/Help-Me-Help>

## How to run the project📝

Expand Down Expand Up @@ -207,15 +207,14 @@ https://git.ntnu.no/Group-5/Help-Me-Help
```bash
mvn javafx:run
```
```


**Run From JAR: (Windows)
**Run From JAR: (Windows)**

1. Download Project JAR:
Go to repository and download the JAR from the jar release.

2. Run the JAR in Terminal:

```bash
java --module-path "path\to\javafx\sdk" --add-modules javafx.controls -jar path\to\jar.jar
Expand Down
107 changes: 90 additions & 17 deletions src/main/java/edu/group5/app/control/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
Expand Down Expand Up @@ -74,43 +78,112 @@ public User getCurrentUser() {
* @param passwordChars the user's password
*/
public void handleSignUp(SignUpPageView view, String firstName, String lastName, String email, char[] passwordChars) {
if (firstName == null || firstName.trim().isEmpty() ||
lastName == null || lastName.trim().isEmpty() ||
email == null || email.trim().isEmpty() ||
passwordChars == null || passwordChars.length == 0) {
if (firstName == null || firstName.trim().isEmpty()
|| lastName == null || lastName.trim().isEmpty()
|| email == null || email.trim().isEmpty()
|| passwordChars == null || passwordChars.length == 0) {
view.showError("All fields are required");
return;
}

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

// Clears password char array after creating a hash.
String hashedPassword = encoder.encode(new String(passwordChars));
for (int i = 0; i < passwordChars.length; i++) {
passwordChars[i] = '\u0000';
// Checks if any input is too long.
if (firstName.length() > 32 || lastName.length() > 32
|| email.length() > 32 || passwordChars.length > 72) {

HashMap<String, List<String>> fields = new HashMap<String, List<String>>();
List<String> fields32 = new ArrayList<String>();
List<String> fields72 = new ArrayList<String>();
fields.put("32", fields32);
fields.put("72", fields72);

if (firstName.length() > 32) {
fields32.add("First Name");
}
if (lastName.length() > 32) {
fields32.add("Last Name");
}
if (email.length() > 32) {
fields32.add("Email");
}
if (passwordChars.length > 72) {
fields72.add("Password");
}

int length32 = fields.get("32").size();
int length72 = fields.get("72").size();

String string32 = "";
if (length32 > 0) {
if (length32 > 1) {
for (int i = 0; i < length32; i++) {
if (i == length32 - 1) {
string32 += String.format("and %s", fields.get("32").get(i));
} else {
string32 += String.format("%s, ", fields.get("32").get(i));
}
}
string32 = string32 + " must have lengths of 32 characters.\n";
} else {
string32 = fields.get("32").getFirst() + " must have a length of 32 characters.\n";
}
}

String string72 = "";
if (length72 > 0) {
if (length72 > 1) {
for (int i = 0; i < length72; i++) {
if (i == length72 - 1) {
string72 += String.format("and %s", fields.get("72").get(i));
} else {
string72 += String.format("%s, ", fields.get("72").get(i));
}
}
string72 = string72 + " must have lengths of 72 characters.\n";
} else {
string72 = fields.get("72").getFirst()
+ " must have a length of 72 characters.\n";
}
}

view.showError(string32 + string72 + "Try again.");
return;
}

// Privacy policy pop-up.
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.");
"Your user information like:\n"
+ "Name and email—as well as donations tied to your account—"
+ "will be saved locally on your machine.\n"
+ "This information is only used to create your account,"
+ "and no data will be sold to third parties.\n"
+ "By creating an account,"
+ "you accept the right of our app to store this information on your computer.");

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// Clears password char array after creating a hash.
String hashedPassword = encoder.encode(new String(passwordChars));
for (int i = 0; i < passwordChars.length; i++) {
passwordChars[i] = '\u0000';
}

if (privacyPolicy.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
boolean success = userService.registerUser(
"Customer", firstName, lastName, email, hashedPassword);

if (success) {

User user = userService.getUserByEmail(email);
appState.setCurrentUser(user);
nav.showHomePage();
} else {
view.showError("Registration failed. Email may already be in use.");
nav.showHomePage();
} else {
view.showError("Registration failed. Email may already be in use.");
}
}
}
}


/**
* Handles the login of a {@link User}.
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/edu/group5/app/control/DonationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ private void handleDonate() {
return;
}

// Prevents donations that are too complex from being made
if (amount.stripTrailingZeros().precision() > 32 || amount.stripTrailingZeros().scale() > 16) {
this.showError("The number is too complex, please donate a smaller or less precise number");
return;
}

// Create donation via service
boolean success = service.donate(
customer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public OrganizationController(AppState appState, OrganizationService service) {

/**
* Sets the current selected organization.
* @param organization the organization to set as current
* @param org the organization to set as current
*/
public void setCurrentOrganization(Organization org) {
appState.setCurrentOrganization(org);
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/edu/group5/app/model/AppState.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class AppState {
private User currentUser;
private BigDecimal currentDonationAmount;
private Organization currentOrganization;
private String currentDonation;
private String currentPaymentMethod;

/**
* Gets the current user of the application.
Expand Down Expand Up @@ -73,14 +73,14 @@ public void setCurrentDonationAmount(BigDecimal amount) {
* @return the current payment method
*/
public String getCurrentPaymentMethod() {
return this.currentDonation;
return this.currentPaymentMethod;
}

/**
* Sets the current payment method.
* @param paymentMethod the payment method to set as the current payment method
*/
public void setCurrentPaymentMethod(String paymentMethod){
this.currentDonation = paymentMethod;
this.currentPaymentMethod = paymentMethod;
}
}
8 changes: 0 additions & 8 deletions src/main/java/edu/group5/app/model/Repository.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,4 @@ protected Repository(Map<K, V> content) {
ParameterValidator.objectChecker(content, "content");
this.content = content;
}

/**
* Gets the content of the repository.
* @return the content of the repository
*/
public Map<K, V> getContent() {
return content;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,38 +40,53 @@ public String fetchDescription(String 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) {
section.select("div.extra-info").remove();
section.select("a.read-more").remove();

// Extract all <p> tags and <div> 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())
.filter(text -> !text.isBlank())
.collect(Collectors.joining("\n\n"));

// 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()) {
descriptionCache.put(pageUrl, description);
return description;
}
String description = parseDescription(doc);
if (!description.isBlank()) {
descriptionCache.put(pageUrl, description);
return description;
}
} catch (Exception e) {
System.out.println("Could not get description for: " + pageUrl);
}
return null;
}

/**
* Parses the description from a Document by extracting text content
* from {@code <section class="information">}.
*
* @param doc the Document to parse
* @return the description text, or empty string if not found
*/
protected String parseDescription(Document doc) {
Element section = doc.selectFirst("section.information");
if (section != null) {
section.select("div.extra-info").remove();
section.select("a.read-more").remove();

// Extract all <p> tags and <div> 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())
.filter(text -> !text.isBlank())
.collect(Collectors.joining("\n\n"));

// Fallback: if no paragraphs found, get all text from section
if (description.isBlank()) {
description = section.text().trim();
}
description = description.replace("Les mer", "").trim();

// Only return if we found something meaningful
if (!description.isBlank()) {
return description;
}
}
return "";
}

/**
* 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.
Expand All @@ -88,10 +103,9 @@ public String fetchLogoUrl(String pageUrl) {
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");
String logoUrl = parseLogoUrl(doc);
if (!logoUrl.isBlank()) {
logoCache.put(pageUrl, logoUrl);
return logoUrl;
}
Expand All @@ -100,4 +114,20 @@ public String fetchLogoUrl(String pageUrl) {
}
return null;
}

/**
* Parses the logo URL from a Document by extracting the image src
* from {@code div.logo img}.
*
* @param doc the Document to parse
* @return the absolute logo URL, or empty string if not found
*/
protected String parseLogoUrl(Document doc) {
Element img = doc.selectFirst("div.logo img");
if (img != null) {
String logoUrl = img.absUrl("src");
return logoUrl.isBlank() ? "" : logoUrl;
}
return "";
}
}
Loading

0 comments on commit 271d9a3

Please sign in to comment.