diff --git a/pom.xml b/pom.xml index 0f3156b..a888bc6 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,12 @@ 3.46.1.0 + + org.jsoup + jsoup + 1.17.2 + + @@ -73,6 +79,19 @@ 3.5.4 + + org.openjfx + javafx-maven-plugin + 0.0.8 + + app.Main + + + + + + + \ No newline at end of file diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index e9947a6..fea73ff 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -1,19 +1,18 @@ package app; import domain.organization.Organization; +import domain.organization.OrganizationDetails; import integration.InnsamlingskontrollenClient; import application.security.PasswordHasher; -import application.user.UserRegister; import domain.donation.Donation; import domain.user.User; import integration.security.Sha256PasswordHasher; import java.math.BigDecimal; import java.sql.SQLException; -import java.util.List; + import persistence.DonationDao; import persistence.OrganizationDao; import persistence.UserDao; -import persistence.UserRepository; import persistence.db.Database; import javafx.application.Application; import javafx.fxml.FXMLLoader; @@ -30,6 +29,11 @@ public static void main(String[] args) { public void start(Stage stage) throws Exception { Database.initializeDatabase(); System.out.println("DB path: " + new java.io.File("givehope.db").getAbsolutePath()); + OrganizationDetails details = InnsamlingskontrollenClient.fetchDetails( + "https://www.innsamlingskontrollen.no/organisasjoner/caritas-norge/" + ); + System.out.println("Description: " + details.getDescription()); + System.out.println("Logo: " + details.getLogoUrl()); OrganizationDao orgDao = new OrganizationDao(); Organization[] orgs = InnsamlingskontrollenClient.fetchOrganizations(); for (Organization org : orgs) { diff --git a/src/main/java/domain/organization/OrganizationDetails.java b/src/main/java/domain/organization/OrganizationDetails.java new file mode 100644 index 0000000..e892e82 --- /dev/null +++ b/src/main/java/domain/organization/OrganizationDetails.java @@ -0,0 +1,19 @@ +package domain.organization; + +public class OrganizationDetails { + private String description; + private String logoUrl; + + public OrganizationDetails(String description, String logoUrl) { + this.description = description; + this.logoUrl = logoUrl; + } + + public String getDescription() { + return description; + } + + public String getLogoUrl() { + return logoUrl; + } +} diff --git a/src/main/java/integration/InnsamlingskontrollenClient.java b/src/main/java/integration/InnsamlingskontrollenClient.java index c0786fa..0699c7d 100644 --- a/src/main/java/integration/InnsamlingskontrollenClient.java +++ b/src/main/java/integration/InnsamlingskontrollenClient.java @@ -4,9 +4,14 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; import domain.organization.Organization; +import domain.organization.OrganizationDetails; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + public class InnsamlingskontrollenClient { @@ -24,4 +29,40 @@ public static Organization[] fetchOrganizations() throws Exception { return mapper.readValue(response.body(), Organization[].class); } + + public static OrganizationDetails fetchDetails(String url) { + try { + org.jsoup.Connection conn = Jsoup.connect(url); + conn.timeout(10000); + Document doc = conn.get(); + doc.charset(StandardCharsets.UTF_8); + System.out.println(doc.select("section.information").text()); + + String description = ""; + System.out.println("Section size: " + doc.select("section.information div > p").size()); + for (org.jsoup.nodes.Element p : doc.select("section.information div > p")) { + System.out.println("P element: " + p.text()); + if (!p.text().isBlank()) { + description = p.text(); + break; + } + } + + if (description.isBlank()) { + String sectionText = doc.select("section.information").text(); + if (!sectionText.isBlank()) { + int lesmerIndex = sectionText.indexOf("Les mer"); + description = lesmerIndex > 0 + ? sectionText.substring(0, lesmerIndex).trim() + : sectionText; + } + } + + String logoUrl = doc.select("img[src*=logo]").attr("src"); + + return new OrganizationDetails(description, logoUrl); + } catch (Exception e) { + return new OrganizationDetails("", ""); + } + } } diff --git a/src/main/java/ui/controller/MainController.java b/src/main/java/ui/controller/MainController.java index b7077fc..aaa182b 100644 --- a/src/main/java/ui/controller/MainController.java +++ b/src/main/java/ui/controller/MainController.java @@ -3,11 +3,14 @@ import application.user.UserRegister; import application.user.UserSignIn; import domain.organization.Organization; +import domain.organization.OrganizationDetails; +import integration.InnsamlingskontrollenClient; import integration.security.Sha256PasswordHasher; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; import persistence.UserDao; import ui.DonationSession; import ui.Page; @@ -24,6 +27,9 @@ public class MainController { @FXML private NavbarController navbarController; + @FXML + private VBox overlay; + private DonationSession donationSession; private DonationFlowController donationFlowController; @@ -79,6 +85,13 @@ public void loadPage(Page page) throws IOException { if (controller instanceof DonationSessionAware c) { c.setDonationSession(donationSession); } + if (controller instanceof OrganizationController c) { + c.setOnDonate(org -> { + try { loadDonationPage(org); } + catch (IOException e) { throw new RuntimeException(e); } + }); + c.setOnShowDetail(org -> showOrganizationDetail(org)); + } mainPane.setCenter(root); navbarController.setActivePage(page); @@ -89,4 +102,45 @@ public void loadPage(Page page) throws IOException { public void loadDonationPage(Organization org) throws IOException { donationFlowController.start(org); } + + public void showOrganizationDetail(Organization org) { + try { + FXMLLoader loader = new FXMLLoader( + getClass().getResource("/view/OrganizationDetail.fxml")); + Parent root = loader.load(); + + OrganizationDetails details = + InnsamlingskontrollenClient.fetchDetails(org.getUrl()); + + OrganizationDetailController controller = loader.getController(); + controller.setOrganization(org, details); + controller.setOnClose(() -> { + overlay.setVisible(false); + overlay.setManaged(false); + overlay.getChildren().clear(); + }); + controller.setOnDonate(o -> { + overlay.setVisible(false); + overlay.setManaged(false); + overlay.getChildren().clear(); + try { + loadDonationPage(o); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + overlay.getChildren().clear(); + overlay.getChildren().add(root); + overlay.setVisible(true); + overlay.setManaged(true); + overlay.setStyle("-fx-background-color: rgba(0,0,0,0.5);"); + overlay.setMaxWidth(Double.MAX_VALUE); + overlay.setMaxHeight(Double.MAX_VALUE); + overlay.setAlignment(javafx.geometry.Pos.CENTER); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/main/java/ui/controller/OrganizationController.java b/src/main/java/ui/controller/OrganizationController.java index e5f4ac2..ede0dd3 100644 --- a/src/main/java/ui/controller/OrganizationController.java +++ b/src/main/java/ui/controller/OrganizationController.java @@ -37,6 +37,8 @@ public class OrganizationController implements NavigationAware{ private Consumer onNavigate; private Consumer onDonate; + private Consumer onShowDetail; + public void setOnNavigate(Consumer onNavigate) { this.onNavigate = onNavigate; @@ -46,10 +48,16 @@ public void setOnDonate(Consumer onDonate) { this.onDonate = onDonate; } + public void setOnShowDetail(Consumer onShowDetail) { + this.onShowDetail = onShowDetail; + } + public void initialize() { try { OrganizationDao dao = new OrganizationDao(); - allOrgs = dao.getAll(); + allOrgs = dao.getAll().stream() + .filter(org -> "approved".equals(org.getStatus())) + .collect(Collectors.toList()); System.out.println("Fetched: " + allOrgs.size() + " organizations"); filtrertOrgs = new ArrayList<>(allOrgs); showPage(); @@ -119,6 +127,7 @@ private VBox createCard(Organization org) { donateBtn.setMaxWidth(Double.MAX_VALUE); donateBtn.setOnAction(e -> onDonate.accept(org)); + card.setOnMouseClicked(e -> onShowDetail.accept(org)); card.getChildren().addAll(name, orgnr, status, donateBtn); return card; } diff --git a/src/main/java/ui/controller/OrganizationDetailController.java b/src/main/java/ui/controller/OrganizationDetailController.java new file mode 100644 index 0000000..96a8b74 --- /dev/null +++ b/src/main/java/ui/controller/OrganizationDetailController.java @@ -0,0 +1,102 @@ +package ui.controller; + +import domain.organization.Organization; +import domain.organization.OrganizationDetails; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import ui.Page; + +import java.awt.Desktop; +import java.net.URI; +import java.util.function.Consumer; + +public class OrganizationDetailController { + + @FXML private Label nameLabel; + @FXML private Label orgNumberLabel; + @FXML private Label verifiedLabel; + @FXML private Label descriptionLabel; + @FXML private ImageView logoImageView; + @FXML private Hyperlink readMoreLink; + + + private Organization organization; + private Consumer onDonate; + private Runnable onClose; + + public void setOrganization(Organization org, OrganizationDetails details) { + this.organization = org; + nameLabel.setText(org.getName()); + orgNumberLabel.setText("Org.nr: " + org.getOrgNumber()); + verifiedLabel.setText(org.isPreApproved() ? "✓ Forhåndsgodkjent" : "✓ Verified"); + + if (details.getDescription() != null && !details.getDescription().isBlank()) { + String description = details.getDescription(); + if (description.length() > 300) { + int lastPeriod = description.lastIndexOf(".", 300); + description = lastPeriod > 0 + ? description.substring(0, lastPeriod + 1) + : description.substring(0, 300) + "..."; + readMoreLink.setVisible(true); + readMoreLink.setManaged(true); + } + descriptionLabel.setText(description); + } else { + descriptionLabel.setText("No description available."); + } + + + + if (details.getLogoUrl() != null && !details.getLogoUrl().isBlank()) { + try { + Image image = new Image(details.getLogoUrl(), true); + image.errorProperty().addListener((obs, old, error) -> { + if (error) { + logoImageView.setVisible(false); + } + }); + logoImageView.setImage(image); + } catch (Exception e) { + logoImageView.setVisible(false); + } + } else { + logoImageView.setVisible(false); + } + } + + public void setOnDonate(Consumer onDonate) { + this.onDonate = onDonate; + } + + public void setOnClose(Runnable onClose) { + this.onClose = onClose; + } + + @FXML + public void handleClose() { + if (onClose != null) onClose.run(); + } + + @FXML + public void handleDonate() { + if (onDonate != null) onDonate.accept(organization); + } + + @FXML + public void handleVisitWebsite() { + try { + Desktop.getDesktop().browse(new URI(organization.getUrl())); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @FXML + public void handleReadMore() { + handleVisitWebsite(); + } +} diff --git a/src/main/resources/css/Donation.css b/src/main/resources/css/Donation.css index 68722d4..ec92d1f 100644 --- a/src/main/resources/css/Donation.css +++ b/src/main/resources/css/Donation.css @@ -96,27 +96,4 @@ .success-icon { -fx-font-size: 48px; -fx-text-fill: #2d6a4f; -} - -.secondary-button { - -fx-background-color: transparent; - -fx-border-color: #1e3a8a; - -fx-border-width: 1.5; - -fx-border-radius: 6; - -fx-background-radius: 6; - -fx-text-fill: #1e3a8a; - -fx-font-weight: bold; - -fx-padding: 10 24 10 24; - -fx-cursor: hand; - -fx-font-size: 14px; -} - -.secondary-button:hover { - -fx-background-color: #f0f4ff; -} - -.secondary-button:pressed { - -fx-background-color: #dce6f7; - -fx-scale-x: 0.97; - -fx-scale-y: 0.97; } \ No newline at end of file diff --git a/src/main/resources/css/Global.css b/src/main/resources/css/Global.css index c7af883..7a2eeac 100644 --- a/src/main/resources/css/Global.css +++ b/src/main/resources/css/Global.css @@ -165,3 +165,25 @@ -fx-pref-height: 40px; } +.secondary-button { + -fx-background-color: transparent; + -fx-border-color: #1e3a8a; + -fx-border-width: 1.5; + -fx-border-radius: 6; + -fx-background-radius: 6; + -fx-text-fill: #1e3a8a; + -fx-font-weight: bold; + -fx-padding: 10 24 10 24; + -fx-cursor: hand; + -fx-font-size: 14px; +} + +.secondary-button:hover { + -fx-background-color: #f0f4ff; +} + +.secondary-button:pressed { + -fx-background-color: #dce6f7; + -fx-scale-x: 0.97; + -fx-scale-y: 0.97; +} diff --git a/src/main/resources/css/OrganizationView.css b/src/main/resources/css/OrganizationView.css index 6904bfd..18c0a90 100644 --- a/src/main/resources/css/OrganizationView.css +++ b/src/main/resources/css/OrganizationView.css @@ -10,6 +10,12 @@ -fx-cursor: hand; } +.org-card:pressed { + -fx-scale-x: 0.98; + -fx-scale-y: 0.98; + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.05), 4, 0, 0, 2); +} + .org-name { -fx-font-size: 20px; } @@ -55,4 +61,57 @@ .search-field:focused { -fx-border-color: #3a86ff; +} + + +.detail-header { + -fx-background-color: #1e3a8a; + -fx-padding: 15 20 15 20; + -fx-alignment: CENTER_LEFT; + -fx-spacing: 10; +} + +.detail-title { + -fx-font-size: 20px; + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-hgrow: always; +} + +.detail-close-button { + -fx-background-color: transparent; + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-cursor: hand; + -fx-border-color: transparent; +} + +.detail-close-button:hover { + -fx-background-color: rgba(255,255,255,0.2); + -fx-background-radius: 50; +} + +.detail-content { + -fx-padding: 20; + -fx-spacing: 15; + -fx-background-color: white; +} + +.detail-verified { + -fx-background-color: #e8f5e9; + -fx-text-fill: #2d6a4f; + -fx-padding: 4 10 4 10; + -fx-background-radius: 4; + -fx-font-size: 12px; +} + +.detail-orgnr { + -fx-font-size: 12px; + -fx-text-fill: #666666; +} + +.detail-description { + -fx-font-size: 13px; + -fx-text-fill: #333333; + -fx-wrap-text: true; } \ No newline at end of file diff --git a/src/main/resources/view/MainView.fxml b/src/main/resources/view/MainView.fxml index 6b655b0..671a97b 100644 --- a/src/main/resources/view/MainView.fxml +++ b/src/main/resources/view/MainView.fxml @@ -1,21 +1,22 @@ - - - - - + + + - - - - -
+ + + + + + + + +
-
+ + +
+
diff --git a/src/main/resources/view/OrganizationDetail.fxml b/src/main/resources/view/OrganizationDetail.fxml new file mode 100644 index 0000000..7f46012 --- /dev/null +++ b/src/main/resources/view/OrganizationDetail.fxml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + +
+ + + + +