diff --git a/README.md b/README.md index 3b229575..95cefcc3 100644 --- a/README.md +++ b/README.md @@ -1,248 +1,9 @@ -# Clean Architecture Team Lab Activity: Login and Logout +#csc-207-project-event-discovery +207 Final Project -In this team lab activity, your team will: -- explore an existing use case (login) -- add a new use case (logout). +#TODOs -To earn credit: -- your team must demo your working `logout` use case. +#File Structure -Your demo should be similar to the below example: - -![](images/sample-logout.gif) - ---- - -## Task 0: Fork this repo on GitHub -**To get started, one team member should fork this repo on GitHub and share it with the team. -All team members should then clone it.** - - -**Suggested logistics:** One of you should invite the others to collaborate on their fork of the original repo on GitHub. You can do this in your repo on GitHub under `Settings -> Collaborators`. This will allow you to push branches to a common repo and then use pull requests to contribute your code and review. To prevent others from pushing directly to the main branch, we recommend that you set branch protection rules on GitHub. Below are how the settings might look if you add branch protection rules: - -![image of branch protection rules for main with a requirement of two approvers to merge in pull requests.](images/branch_protection_rules.png) - ---- - -## Task 1: Understanding the Program - -Open the project in IntelliJ. Open `app.Main` and read it as a team. -- What are the currently implemented Views and Use Cases in the program? -- Which Use Cases are triggered from each View? -- Which version of the DAO is `app.Main` using? - -> Observe that the main method makes use of the `app.AppBuilder` class which -is responsible for constructing our CA engine for each use case of the application. To answer the last two questions above, you will need to look inside the details of the `app.AppBuilder` class. - -**Make sure that each member of your team can successfully run `app/Main.java`.** -- Ensure that you are each able to create a new user and log in using the username and password. - -> Note: you may need to set the Project SDK in the `Project Structure...` menu, and possibly -> also manually link the Maven project if the app won't run when you try to run Main. - -### Task 1.1: Exploring the login use case - -Let's take a tour of the login use case code: - -- In IntelliJ, find the `LoginController` class and open it. - -- Set a breakpoint inside its `execute` method. - -- Run the program in debug mode. - -- On the login page, attempt to log in with an existing account. When you click the button, the breakpoint that you set will be triggered. - -- **Step through the code to trace the execution of the login use case.** - Importantly, pay extra close attention to what the Presenter does to ensure that the LoggedInView gets displayed after the user successfully gets logged into the application. - -> Pay attention to the classes involved and the flow of execution. When your team implements the logout use case next, your code will need to have a very similar structure. - -To better understand how the view gets updated, your team may find it useful to review the [Extra Advice about the Presenters, Views, and ViewModels](#extra-advice-about-the-presenters-views-and-viewmodels) section at the end of this README. - -## Task 2: Implementing the Logout Use Case - -Currently, you'll notice that the "Log Out" button in the `LoggedInView` still doesn't actually log you out of the program. Let's fix this. - -We have created all the classes for your team, but some of the code is missing. **As a team, your task is to fill in the missing code so that the logout use case is functional.** - -> The next part of the readme describes how your team will do this. - -Your team will know when you are done when: - -- Clicking the "Log Out" button takes the user back to the Login View when you use the program. -- On the Login View, the username of the logged-out user is filled in. -- The provided `LogoutInteractorTest` test passes. - -### Task 2.1: Dividing up the work - -There are `TODO` comments left in the files. - -> Recall that you can use the TODO tool window to conveniently pull up a complete list. - -Once all TODOs are complete, the "Log Out" button _should_ work! - -**As a team, split up the TODOs (see below) between the members of your team.** - -> Optionally, your team can make GitHub Issues and assign them to each team member. - -Make sure that each member has at least one TODO that they will be responsible for completing. -If your team prefers to work in pairs, that is fine too. - -The TODOs are summarized below (by file) to help your team decide how to split them up: - ---- - -- `Main.java` (tip: look at how other use cases have been added) - -[ ] TODO: add the logout use case to the app - ---- - -- `LoggedInView.java` (tip: refer to the other views for similar code) - -[ ] TODO: save the logout controller in the instance variable. - -[ ] TODO: execute the logout use case through the Controller - ---- - -- `LogoutController.java` (tip: refer to the other controllers for similar code) - -[ ] TODO: Save the interactor in the instance variable. - -[ ] TODO: run the use case interactor for the logout use case - -> Note: there is no input data necessary for this use case. - ---- - -- `LogoutInteractor.java` (tip: refer to `ChangePasswordInteractor.java` for similar code) - -[ ] TODO: save the DAO and Presenter in the instance variables. - -[ ] TODO: implement the logic of the Logout Use Case - -> Note: there is no input data necessary for this use case. - ---- - -- `LogoutPresenter.java` (tip: refer to `SignupPresenter.java` for similar code) - -[ ] TODO: assign to the three instance variables. - -[ ] TODO: have prepareSuccessView update the LoggedInState - -[ ] TODO: have prepareSuccessView update the LoginState - ---- - -### Task 2.2: Complete your TODOs! -With the work divided up, your team should complete the TODOs through a sequence of PRs. - -1. Make a branch for your work. - -> Make sure that you switch to your new branch! - -2. Complete your assigned TODO and make a pull request on GitHub. In your pull request, - briefly describe what your TODO was and how you implemented it. If you aren't sure - about part of it, include this in your pull request so that everyone knows what to look - for when reviewing — or you can of course discuss with your team before making your - pull request. - -3. Review all pull requests to ensure each TODO is correctly implemented. - -4. Once all TODOs are completed, your team should debug as needed to ensure the - correctness of the code. Setting a breakpoint where the logout use case - interactor starts its work will likely be a great place to start when debugging. - -And that's it; your team should now have a working logout use case! - -**Demo your working code to your TA to earn credit.** - ---- - -# Extra Advice about the Presenters, Views, and ViewModels - -One of the trickiest parts of the code will be the flow of information between these pieces of the program. Below briefly explains how these pieces fit together and work in the context of the login use case. - -## ViewModels and States - -In the design of this program, `ViewModel` is written using generics to allow -for different "state" objects to be stored. For a `LoginViewModel`, the state is an instance of class `LoginState`. Each state object will just contain a basic constructor, getters, and setters to store the data of the view model. - -## A View and its ViewModel - -In the constructor of `LoginView`, the following line of code connects this instance of `LoginView` to its associated `LoginViewModel`: - -```java -this.loginViewModel.addPropertyChangeListener(this); -``` - -This should remind you of the code we write when adding an action listener to a button. The code is following the same structure. - -> We'll talk more about this "pattern" of _events_ and _listeners_ in our next module. - -When the presenter updates the view model later, an event will be triggered — resulting in the view's `propertyChange` method getting called, with a `PropertyChangeEvent` object being passed through as the argument to the call. - -For example, the `LoginView.propertyChange` method looks like: - -```java -public void propertyChange(PropertyChangeEvent evt) { - final LoginState state = (LoginState) evt.getNewValue(); - setFields(state); - usernameErrorField.setText(state.getLoginError()); - } -``` - -The `LoginView` gets the `LoginState` object stored in the `LoginViewModel` and updates itself with that information. - -## A Presenter and its ViewModel(s) - -A presenter may have one or more view models associated with it. For example, the login use case's presenter has a reference to a `LoginViewModel` and a `LoggedInViewModel`, since it will need to update both view models. Additionally, our implementation makes use of a `ViewManager` and `ViewManagerModel` to keep track of which view -the user should currently see. - -Let's take a look at the `LoginPresenter.prepareSuccessView` method as an example: - -```java -public void prepareSuccessView(LoginOutputData response) { - // On success, update the loggedInViewModel's state - final LoggedInState loggedInState = loggedInViewModel.getState(); - loggedInState.setUsername(response.getUsername()); - this.loggedInViewModel.firePropertyChanged(); - - // and clear everything from the LoginViewModel's state - this.loginViewModel.setState(new LoginState()); - this.loginViewModel.firePropertyChanged(); - - // switch to the logged in view - this.viewManagerModel.setState(loggedInViewModel.getViewName()); - this.viewManagerModel.firePropertyChanged(); -} -``` - -The first part of the code updates the view model for the logged-in view so that the newly logged-in username will be displayed. Once the state is updated, the `firePropertyChanged` method is called, which is what will trigger the call to the view's `propertyChange` method which will update the view based on the updated view model. - -This can be visualized as a sequence diagram as follows: - -> Note: this diagram has been simplified to focus on the high-level flow of information; the actual stack trace includes some additional intermediate calls which you can see if you step through the code in the debugger or manually click through the code. - -![sequence diagram of the LoginPresenter code](images/login_presenter_sequence_diagram.png) - -We then do the same, but for the login view model whose state we want to clear. - -Lastly, we update the state of the `viewManagerModel`, and alert the viewManager that it should switch to displaying the logged-in view. - -> Setting a breakpoint in the code and stepping through can help you see how the information flows through the system. Pay attention to the contents of the call stack to help you track where you are in the execution of the use case. - -## The ViewManager - -This class may stand out as a bit unclear about how it fits into our architecture, as it isn't in the CA Engine diagram at all. Remember that the CA Engine is representing a single use case in our program. Once our program has _multiple_ use cases, we naturally need some kind of additional code to connect them together. As we have seen, one use case can lead to a change in which view is presented to the user. To facilitate this, our implementation used a `ViewManager` and associated `ViewManagerModel` to take care of this switching for us. The state of a `ViewManagerModel` object is simply a string that indicates the name of the currently visible view (`JPanel` in this implementation). The `ViewManager` uses a `CardLayout` to conveniently display only the currently active view at a given time. - -When the `ViewManager` is alerted of a change to its associated `ViewManagerModel`, its `propertyChange` method is executed: - -```java -public void propertyChange(PropertyChangeEvent evt) { - if (evt.getPropertyName().equals("state")) { - final String viewModelName = (String) evt.getNewValue(); - cardLayout.show(views, viewModelName); - } - } -``` - -This code will update the application to display the view corresponding to the `viewModelName` string. - -> In the `AppBuilder` code, you can see how the views are originally added to the `cardLayout`. - -> Thought Question: Can you think of any alternatives to our `ViewManager` implementation for managing multiple views? - ---- +#Contributors: +1. Christopher Mong \ No newline at end of file diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 3c654cad..f07ada96 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -1,5 +1,14 @@ package app; +import data_access.InMemoryEventDataAccessObject; +import data_access.EventDataAccessInterface; +import interface_adapter.event_description.EventDescriptionViewModel; +import interface_adapter.event_description.EventDescriptionPresenter; +import interface_adapter.event_description.EventDescriptionController; +import use_case.event_description.*; +import view.EventDescriptionView; + + import data_access.FileUserDataAccessObject; import entity.UserFactory; import interface_adapter.ViewManagerModel; @@ -35,21 +44,20 @@ import java.awt.*; public class AppBuilder { + private EventDescriptionViewModel eventDescriptionViewModel; + private EventDescriptionView eventDescriptionView; + + // temporary Event DAO (for now, in-memory) + final EventDataAccessInterface eventDataAccessObject = new InMemoryEventDataAccessObject(); + private final JPanel cardPanel = new JPanel(); private final CardLayout cardLayout = new CardLayout(); final UserFactory userFactory = new UserFactory(); final ViewManagerModel viewManagerModel = new ViewManagerModel(); ViewManager viewManager = new ViewManager(cardPanel, cardLayout, viewManagerModel); - // set which data access implementation to use, can be any - // of the classes from the data_access package - - // DAO version using local file storage final FileUserDataAccessObject userDataAccessObject = new FileUserDataAccessObject("users.csv", userFactory); - // DAO version using a shared external database - // final DBUserDataAccessObject userDataAccessObject = new DBUserDataAccessObject(userFactory); - private SignupView signupView; private SignupViewModel signupViewModel; private LoginViewModel loginViewModel; @@ -95,7 +103,7 @@ public AppBuilder addSignupUseCase() { public AppBuilder addLoginUseCase() { final LoginOutputBoundary loginOutputBoundary = new LoginPresenter(viewManagerModel, - loggedInViewModel, loginViewModel); + loggedInViewModel, loginViewModel, signupViewModel); final LoginInputBoundary loginInteractor = new LoginInteractor( userDataAccessObject, loginOutputBoundary); @@ -132,13 +140,41 @@ public AppBuilder addLogoutUseCase() { return this; } + public AppBuilder addEventDescriptionView() { + eventDescriptionViewModel = new EventDescriptionViewModel(); + eventDescriptionView = new EventDescriptionView(eventDescriptionViewModel); + cardPanel.add(eventDescriptionView, eventDescriptionView.getViewName()); + return this; + } + + public AppBuilder addEventDescriptionUseCase() { + DistanceCalculator distanceCalculator = new HaversineDistanceCalculator(); + + EventDescriptionOutputBoundary presenter = + new EventDescriptionPresenter(eventDescriptionViewModel); + + EventDescriptionInputBoundary interactor = + new EventDescriptionInteractor(eventDataAccessObject, presenter, distanceCalculator); + + EventDescriptionController controller = + new EventDescriptionController(interactor); + +// eventDescriptionView.setController(controller); + return this; + } + + public JFrame build() { final JFrame application = new JFrame("User Login Example"); application.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); application.add(cardPanel); - viewManagerModel.setState(signupView.getViewName()); + // TODO: FOR DEBUGGING PURPOSES ONLY + viewManagerModel.setState(eventDescriptionView.getViewName()); + + // TODO: KEEP CODE BELOW + // viewManagerModel.setState(signupView.getViewName()); viewManagerModel.firePropertyChange(); return application; diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 424404fb..1b93ddc7 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -3,15 +3,25 @@ import javax.swing.*; public class Main { + + /** + * Main gateway into application + * Running this file will begin the application + * @param args input arguments to the main method + + **/ public static void main(String[] args) { AppBuilder appBuilder = new AppBuilder(); JFrame application = appBuilder .addLoginView() .addSignupView() .addLoggedInView() + .addEventDescriptionView() // NEW .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() + .addLogoutUseCase() + .addEventDescriptionUseCase() // NEW .build(); application.pack(); diff --git a/src/main/java/data_access/EventDataAccessInterface.java b/src/main/java/data_access/EventDataAccessInterface.java new file mode 100644 index 00000000..15356c19 --- /dev/null +++ b/src/main/java/data_access/EventDataAccessInterface.java @@ -0,0 +1,18 @@ +package data_access; + +import entity.Event; + +//TODO: DELETE IF NOT USING, OLD CODE +//public interface EventDataAccessInterface { +// /** +// * Return Event with this id, or null if it does not exist +// **/ +// Event getEventById(String id); +//} + +public interface EventDataAccessInterface { + void save(Event event); + Event get(String id); + boolean existsById(String id); +} + diff --git a/src/main/java/data_access/InMemoryEventDataAccessObject.java b/src/main/java/data_access/InMemoryEventDataAccessObject.java new file mode 100644 index 00000000..c6184f82 --- /dev/null +++ b/src/main/java/data_access/InMemoryEventDataAccessObject.java @@ -0,0 +1,56 @@ +package data_access; + +import entity.Event; +import entity.Location; + +import java.util.HashMap; +import java.util.Map; + +public class InMemoryEventDataAccessObject implements EventDataAccessInterface { + + private final Map events = new HashMap<>(); + + public InMemoryEventDataAccessObject() { + // --- FAKE TESTING DATA --- + events.put("1", new Event( + "1", + "Campus Concert", + "Live music at UofT.", + "123 College St", + new Location(43.6629, -79.3957), + "Music", + "2025-11-20 19:00" + )); + + events.put("2", new Event( + "2", + "Art Fair", + "An outdoor art fair with local artists.", + "456 King St", + new Location(43.6532, -79.3832), + "Art", + "2025-12-05 10:00" + )); + } + + // TODO: DELETE IF NOT USING, OLD CODE +// @Override +// public Event getEventById(String id) { +// return events.get(id); +// } + + @Override + public void save(Event event) { + events.put(event.getId(), event); + } + + @Override + public Event get(String id) { + return events.get(id); + } + + @Override + public boolean existsById(String id) { + return events.containsKey(id); + } +} diff --git a/src/main/java/data_access/InMemoryEventDataAccessObjectTest.java b/src/main/java/data_access/InMemoryEventDataAccessObjectTest.java new file mode 100644 index 00000000..1c58de0d --- /dev/null +++ b/src/main/java/data_access/InMemoryEventDataAccessObjectTest.java @@ -0,0 +1 @@ +//TODO: DELETE FILE... \ No newline at end of file diff --git a/src/main/java/entity/Event.java b/src/main/java/entity/Event.java new file mode 100644 index 00000000..9f29c3d7 --- /dev/null +++ b/src/main/java/entity/Event.java @@ -0,0 +1,183 @@ +package entity; + +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.Objects; + +public class Event { + + // Core fields + private final String id; + private final String name; + private final String description; + private final String address; + private final EventCategory category; + private final Location location; + private final LocalDateTime startTime; + private final String imageUrl; + + /** + * Main constructor with strong typing. + */ + public Event(String id, + String name, + String description, + String address, + EventCategory category, + Location location, + LocalDateTime startTime, + String imageUrl) { + + // --- Basic validation --- + + if (id == null || id.trim().isEmpty()) { + throw new IllegalArgumentException("Event ID cannot be null or empty"); + } + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Event name cannot be null or empty"); + } + if (address == null || address.trim().isEmpty()) { + throw new IllegalArgumentException("Event address cannot be null or empty"); + } + if (location == null) { + throw new IllegalArgumentException("Location cannot be null"); + } + if (startTime == null) { + throw new IllegalArgumentException("Start time cannot be null"); + } + + this.id = id.trim(); + this.name = name.trim(); + this.address = address.trim(); + this.description = (description == null) ? "" : description; + + this.category = (category == null) ? EventCategory.MISCELLANEOUS : category; + this.location = location; + this.startTime = startTime; + + if (imageUrl == null || imageUrl.trim().isEmpty()) { + this.imageUrl = ""; + } else { + this.imageUrl = imageUrl.trim(); + } + } + + /** + * Convenience constructor matching your ORIGINAL signature: + * (id, name, description, address, location, categoryString, dateTimeString). + * + * This lets your existing tests and code continue to compile. + */ + public Event(String id, + String name, + String description, + String address, + Location location, + String category, + String dateTime) { + + this( + id, + name, + description, + address, + parseCategory(category), + location, + parseDateTime(dateTime), + "" // no image URL provided in the old constructor + ); + } + + // ---- Helper parsers for the convenience constructor ---- + + private static EventCategory parseCategory(String category) { + if (category == null || category.trim().isEmpty()) { + return EventCategory.MISCELLANEOUS; + } + try { + // assumes enum constants like MUSIC, SPORTS, etc. + return EventCategory.valueOf(category.trim().toUpperCase()); + } catch (IllegalArgumentException ex) { + // if the string doesn't match any enum constant, fall back + return EventCategory.MISCELLANEOUS; + } + } + + private static LocalDateTime parseDateTime(String dateTime) { + if (dateTime == null || dateTime.trim().isEmpty()) { + return LocalDateTime.now(); + } + try { + // works with ISO-8601 strings like "2025-11-20T19:00" + return LocalDateTime.parse(dateTime.trim()); + } catch (DateTimeParseException ex) { + // fallback if parse fails + return LocalDateTime.now(); + } + } + + // ---- Getter methods ---- + + public String getId() { return id; } + public String getName() { return name; } + public String getDescription() { return description; } + public String getAddress() { return address; } + public EventCategory getCategory() { return category; } + public Location getLocation() { return location; } + public LocalDateTime getStartTime() { return startTime; } + + public String getImageUrl() { + if (imageUrl == null || imageUrl.isEmpty()) { + // default placeholder image + return "https://via.placeholder.com/300x200/CCCCCC/969696?text=No+Event+Image"; + } + return imageUrl; + } + + // ---- Core business methods ---- + + // Check if event is within specified radius of a user location + public boolean isWithinRadius(Location userLocation, double radiusKm) { + if (userLocation == null || radiusKm < 0) { + return false; + } + double distance = this.location.calculateDistance(userLocation); + return distance <= radiusKm; + } + + // Distance from event to a user location + public double calculateDistanceTo(Location userLocation) { + if (userLocation == null) { + return Double.MAX_VALUE; + } + return this.location.calculateDistance(userLocation); + } + + // Filter by category + public boolean isInCategory(EventCategory targetCategory) { + return this.category == targetCategory; + } + + // ---- equals / hashCode / toString ---- + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Event)) return false; + Event event = (Event) o; + return id.equals(event.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return String.format( + "Event{id='%s', name='%s', category=%s, image=%s}", + id, name, category, getImageUrl() + ); + } +} diff --git a/src/main/java/entity/EventCategory.java b/src/main/java/entity/EventCategory.java new file mode 100644 index 00000000..f9075929 --- /dev/null +++ b/src/main/java/entity/EventCategory.java @@ -0,0 +1,39 @@ +package entity; + +public enum EventCategory { + MUSIC("Music"), + + SPORTS("Sports"), + + ARTS_THEATRE("Arts & Theatre"), + + FILM("Film"), + + MISCELLANEOUS("Miscellaneous"); + + private final String displayName; + EventCategory(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + public static EventCategory fromString(String eventCategoryName) { + if (eventCategoryName == null) { + return MISCELLANEOUS; + } + for (EventCategory eventCategory : EventCategory.values()) { + if (eventCategory.displayName.equalsIgnoreCase(eventCategoryName)|| + eventCategory.name().equalsIgnoreCase(eventCategoryName)) { + return eventCategory; + } + } + return MISCELLANEOUS; + } + @Override + public String toString() { + return displayName; + } + +} diff --git a/src/main/java/entity/EventRepository.java b/src/main/java/entity/EventRepository.java new file mode 100644 index 00000000..ee961f16 --- /dev/null +++ b/src/main/java/entity/EventRepository.java @@ -0,0 +1,10 @@ +package entity; + +import java.util.List; +import java.util.Optional; + +public interface EventRepository { + List findAllEvents(); + Optional findById(String id); + List findByName(String query); +} diff --git a/src/main/java/entity/Location.java b/src/main/java/entity/Location.java new file mode 100644 index 00000000..ace59d62 --- /dev/null +++ b/src/main/java/entity/Location.java @@ -0,0 +1,100 @@ +package entity; + +import java.util.Objects; + +public class Location { + private final String address; + private final double latitude; + private final double longitude; + + /** + * Convenience constructor when you only care about coordinates. + * Address will be stored as an empty string. + */ + public Location(double latitude, double longitude) { + this("", latitude, longitude); + } + + /** + * Full constructor with address and coordinates. + */ + public Location(String address, double latitude, double longitude) { + this.address = validateAddress(address); + this.latitude = validateCoordinate(latitude, -90, 90, "Latitude"); + this.longitude = validateCoordinate(longitude, -180, 180, "Longitude"); + } + + private String validateAddress(String address) { + // Address can be empty but not null + if (address == null) { + return ""; + } + return address.trim(); + } + + private double validateCoordinate(double value, double min, double max, String fieldName) { + if (value < min || value > max) { + throw new IllegalArgumentException(fieldName + " must be between " + min + " and " + max); + } + return value; + } + + /** + * Distance to another Location using the Haversine formula. + * Returns distance in kilometers. + */ + public double calculateDistance(Location other) { + if (other == null) return Double.MAX_VALUE; + + final double EARTH_RADIUS_KM = 6371.0; + + double lat1 = Math.toRadians(this.latitude); + double lon1 = Math.toRadians(this.longitude); + double lat2 = Math.toRadians(other.latitude); + double lon2 = Math.toRadians(other.longitude); + + double dLat = lat2 - lat1; + double dLon = lon2 - lon1; + + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1) * Math.cos(lat2) + * Math.sin(dLon / 2) * Math.sin(dLon / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return EARTH_RADIUS_KM * c; + } + + public String getAddress() { + return address; + } + + public double getLatitude() { + return latitude; + } + + public double getLongitude() { + return longitude; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Location)) return false; + Location location = (Location) o; + return Double.compare(location.latitude, latitude) == 0 + && Double.compare(location.longitude, longitude) == 0 + && Objects.equals(address, location.address); + } + + @Override + public int hashCode() { + return Objects.hash(address, latitude, longitude); + } + + @Override + public String toString() { + return String.format("Location{address='%s', latitude=%.6f, longitude=%.6f}", + address, latitude, longitude); + } +} diff --git a/src/main/java/interface_adapter/event_description/EventDescriptionController.java b/src/main/java/interface_adapter/event_description/EventDescriptionController.java new file mode 100644 index 00000000..08ef5380 --- /dev/null +++ b/src/main/java/interface_adapter/event_description/EventDescriptionController.java @@ -0,0 +1,19 @@ +package interface_adapter.event_description; + +import use_case.event_description.EventDescriptionInputBoundary; +import use_case.event_description.EventDescriptionInputData; + +public class EventDescriptionController { + + private final EventDescriptionInputBoundary interactor; + + public EventDescriptionController(EventDescriptionInputBoundary interactor) { + this.interactor = interactor; + } + + public void showEvent(String eventId, double userLat, double userLon) { + EventDescriptionInputData inputData = + new EventDescriptionInputData(eventId, userLat, userLon); + interactor.execute(inputData); + } +} diff --git a/src/main/java/interface_adapter/event_description/EventDescriptionPresenter.java b/src/main/java/interface_adapter/event_description/EventDescriptionPresenter.java new file mode 100644 index 00000000..9c0ccfc6 --- /dev/null +++ b/src/main/java/interface_adapter/event_description/EventDescriptionPresenter.java @@ -0,0 +1,32 @@ +package interface_adapter.event_description; + +import use_case.event_description.EventDescriptionOutputBoundary; +import use_case.event_description.EventDescriptionOutputData; + +public class EventDescriptionPresenter implements EventDescriptionOutputBoundary { + + private final EventDescriptionViewModel viewModel; + + public EventDescriptionPresenter(EventDescriptionViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void prepareSuccessView(EventDescriptionOutputData outputData) { + String distanceText = String.format("%.2f km away", outputData.getDistanceKm()); + + viewModel.setEventDetails( + outputData.getName(), + outputData.getDescription(), + outputData.getAddress(), + outputData.getCategory(), + outputData.getDateTime(), + distanceText + ); + } + + @Override + public void prepareFailView(String errorMessage) { + viewModel.setError(errorMessage); + } +} diff --git a/src/main/java/interface_adapter/event_description/EventDescriptionViewModel.java b/src/main/java/interface_adapter/event_description/EventDescriptionViewModel.java new file mode 100644 index 00000000..459f75b2 --- /dev/null +++ b/src/main/java/interface_adapter/event_description/EventDescriptionViewModel.java @@ -0,0 +1,55 @@ +package interface_adapter.event_description; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; + +public class EventDescriptionViewModel { + public static final String VIEW_NAME = "event description"; + + private final PropertyChangeSupport support = new PropertyChangeSupport(this); + + private String name = ""; + private String description = ""; + private String address = ""; + private String category = ""; + private String dateTime = ""; + private String distanceText = ""; + private String errorMessage = ""; + + public String getViewName() { + return VIEW_NAME; + } + + public String getName() { return name; } + public String getDescription() { return description; } + public String getAddress() { return address; } + public String getCategory() { return category; } + public String getDateTime() { return dateTime; } + public String getDistanceText() { return distanceText; } + public String getErrorMessage() { return errorMessage; } + + public void setEventDetails(String name, String description, String address, + String category, String dateTime, String distanceText) { + this.name = name; + this.description = description; + this.address = address; + this.category = category; + this.dateTime = dateTime; + this.distanceText = distanceText; + this.errorMessage = ""; + firePropertyChange(); + } + + public void setError(String errorMessage) { + this.errorMessage = errorMessage; + firePropertyChange(); + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + private void firePropertyChange() { + support.firePropertyChange("eventDescription", null, null); + } +} diff --git a/src/main/java/interface_adapter/login/LoginController.java b/src/main/java/interface_adapter/login/LoginController.java index 57e95066..6f484250 100644 --- a/src/main/java/interface_adapter/login/LoginController.java +++ b/src/main/java/interface_adapter/login/LoginController.java @@ -25,4 +25,12 @@ public void execute(String username, String password) { loginUseCaseInteractor.execute(loginInputData); } + + /** + * Switches to "Sign Up View" using the Log In Use Case + */ + public void switchToSignUpView() { + loginUseCaseInteractor.switchToSignUpView(); + + } } diff --git a/src/main/java/interface_adapter/login/LoginPresenter.java b/src/main/java/interface_adapter/login/LoginPresenter.java index a00bc7c7..e14cb4ec 100644 --- a/src/main/java/interface_adapter/login/LoginPresenter.java +++ b/src/main/java/interface_adapter/login/LoginPresenter.java @@ -3,8 +3,10 @@ import interface_adapter.ViewManagerModel; import interface_adapter.logged_in.LoggedInState; import interface_adapter.logged_in.LoggedInViewModel; +import interface_adapter.signup.SignupViewModel; import use_case.login.LoginOutputBoundary; import use_case.login.LoginOutputData; +import view.SignupView; /** * The Presenter for the Login Use Case. @@ -14,13 +16,15 @@ public class LoginPresenter implements LoginOutputBoundary { private final LoginViewModel loginViewModel; private final LoggedInViewModel loggedInViewModel; private final ViewManagerModel viewManagerModel; + private final SignupViewModel signUpViewModel; public LoginPresenter(ViewManagerModel viewManagerModel, LoggedInViewModel loggedInViewModel, - LoginViewModel loginViewModel) { + LoginViewModel loginViewModel, SignupViewModel signupViewModel) { this.viewManagerModel = viewManagerModel; this.loggedInViewModel = loggedInViewModel; this.loginViewModel = loginViewModel; + this.signUpViewModel = signupViewModel; } @Override @@ -44,4 +48,10 @@ public void prepareFailView(String error) { loginState.setLoginError(error); loginViewModel.firePropertyChange(); } + + @Override + public void switchToSignUpView() { + viewManagerModel.setState(signUpViewModel.getViewName()); + viewManagerModel.firePropertyChange(); + } } diff --git a/src/main/java/interface_adapter/logout/LogoutController.java b/src/main/java/interface_adapter/logout/LogoutController.java index 49eedf89..8f4a7ea9 100644 --- a/src/main/java/interface_adapter/logout/LogoutController.java +++ b/src/main/java/interface_adapter/logout/LogoutController.java @@ -1,6 +1,8 @@ package interface_adapter.logout; +import use_case.login.LoginInputData; import use_case.logout.LogoutInputBoundary; +import use_case.logout.LogoutOutputData; /** * The controller for the Logout Use Case. @@ -10,13 +12,13 @@ public class LogoutController { private LogoutInputBoundary logoutUseCaseInteractor; public LogoutController(LogoutInputBoundary logoutUseCaseInteractor) { - // TODO: Save the interactor in the instance variable. + this.logoutUseCaseInteractor = logoutUseCaseInteractor; } /** * Executes the Logout Use Case. */ public void execute() { - // TODO: run the use case interactor for the logout use case + logoutUseCaseInteractor.execute(); } } diff --git a/src/main/java/interface_adapter/logout/LogoutPresenter.java b/src/main/java/interface_adapter/logout/LogoutPresenter.java index 4df31e73..ee61d54b 100644 --- a/src/main/java/interface_adapter/logout/LogoutPresenter.java +++ b/src/main/java/interface_adapter/logout/LogoutPresenter.java @@ -1,7 +1,9 @@ package interface_adapter.logout; import interface_adapter.ViewManagerModel; +import interface_adapter.logged_in.LoggedInState; import interface_adapter.logged_in.LoggedInViewModel; +import interface_adapter.login.LoginState; import interface_adapter.login.LoginViewModel; import use_case.logout.LogoutOutputBoundary; import use_case.logout.LogoutOutputData; @@ -18,26 +20,23 @@ public class LogoutPresenter implements LogoutOutputBoundary { public LogoutPresenter(ViewManagerModel viewManagerModel, LoggedInViewModel loggedInViewModel, LoginViewModel loginViewModel) { - // TODO: assign to the three instance variables. - } + this.viewManagerModel = viewManagerModel; + this.loggedInViewModel = loggedInViewModel; + this.loginViewModel = loginViewModel; } @Override public void prepareSuccessView(LogoutOutputData response) { - // We need to switch to the login view, which should have - // an empty username and password. + String user = response.getUsername(); + + final LoggedInState loggedInState = loggedInViewModel.getState(); + loggedInState.setUsername(""); - // We also need to set the username in the LoggedInState to - // the empty string. + loggedInViewModel.firePropertyChange(); - // TODO: have prepareSuccessView update the LoggedInState - // 1. get the LoggedInState out of the appropriate View Model, - // 2. set the username in the state to the empty string - // 3. firePropertyChanged so that the View that is listening is updated. + final LoginState loginState = loginViewModel.getState(); + loginState.setUsername(user); + loginViewModel.firePropertyChange(); - // TODO: have prepareSuccessView update the LoginState - // 1. get the LoginState out of the appropriate View Model, - // 2. set the username in the state to be the username of the user that just logged out, - // 3. firePropertyChanged so that the View that is listening is updated. // This code tells the View Manager to switch to the LoginView. this.viewManagerModel.setState(loginViewModel.getViewName()); diff --git a/src/main/java/interface_adapter/signup/SignupViewModel.java b/src/main/java/interface_adapter/signup/SignupViewModel.java index 01f0086b..7fd7c90d 100644 --- a/src/main/java/interface_adapter/signup/SignupViewModel.java +++ b/src/main/java/interface_adapter/signup/SignupViewModel.java @@ -7,10 +7,12 @@ */ public class SignupViewModel extends ViewModel { - public static final String TITLE_LABEL = "Sign Up View"; - public static final String USERNAME_LABEL = "Choose username"; - public static final String PASSWORD_LABEL = "Choose password"; - public static final String REPEAT_PASSWORD_LABEL = "Enter password again"; + public static final String TITLE_LABEL = "Sign Up"; + public static final String USERNAME_LABEL = "Username"; + public static final String PASSWORD_LABEL = "Password"; + public static final String REPEAT_PASSWORD_LABEL = "Re-enter password"; + public static final String TITLE = "Event Discovery"; + public static final String CAPTION = "Join us and never miss an event!"; public static final String SIGNUP_BUTTON_LABEL = "Sign up"; public static final String CANCEL_BUTTON_LABEL = "Cancel"; diff --git a/src/main/java/use_case/event_description/DistanceCalculator.java b/src/main/java/use_case/event_description/DistanceCalculator.java new file mode 100644 index 00000000..8b24bdbd --- /dev/null +++ b/src/main/java/use_case/event_description/DistanceCalculator.java @@ -0,0 +1,5 @@ +package use_case.event_description; + +public interface DistanceCalculator { + double distanceKm(double lat1, double lon1, double lat2, double lon2); +} diff --git a/src/main/java/use_case/event_description/EventDescriptionInputBoundary.java b/src/main/java/use_case/event_description/EventDescriptionInputBoundary.java new file mode 100644 index 00000000..be6653cd --- /dev/null +++ b/src/main/java/use_case/event_description/EventDescriptionInputBoundary.java @@ -0,0 +1,5 @@ +package use_case.event_description; + +public interface EventDescriptionInputBoundary { + void execute(EventDescriptionInputData inputData); +} diff --git a/src/main/java/use_case/event_description/EventDescriptionInputData.java b/src/main/java/use_case/event_description/EventDescriptionInputData.java new file mode 100644 index 00000000..af1077ce --- /dev/null +++ b/src/main/java/use_case/event_description/EventDescriptionInputData.java @@ -0,0 +1,28 @@ +package use_case.event_description; + +public class EventDescriptionInputData { + + private final String eventId; + private final double userLatitude; + private final double userLongitude; + + public EventDescriptionInputData(String eventId, + double userLatitude, + double userLongitude) { + this.eventId = eventId; + this.userLatitude = userLatitude; + this.userLongitude = userLongitude; + } + + public String getEventId() { + return eventId; + } + + public double getUserLatitude() { + return userLatitude; + } + + public double getUserLongitude() { + return userLongitude; + } +} diff --git a/src/main/java/use_case/event_description/EventDescriptionInteractor.java b/src/main/java/use_case/event_description/EventDescriptionInteractor.java new file mode 100644 index 00000000..fe7f0702 --- /dev/null +++ b/src/main/java/use_case/event_description/EventDescriptionInteractor.java @@ -0,0 +1,55 @@ +package use_case.event_description; + +import data_access.EventDataAccessInterface; +import entity.Event; +import entity.Location; + +import java.time.format.DateTimeFormatter; + +public class EventDescriptionInteractor implements EventDescriptionInputBoundary { + + private final EventDataAccessInterface eventDataAccessObject; + private final EventDescriptionOutputBoundary presenter; + private final DistanceCalculator distanceCalculator; + + public EventDescriptionInteractor(EventDataAccessInterface eventDataAccessObject, + EventDescriptionOutputBoundary presenter, + DistanceCalculator distanceCalculator) { + this.eventDataAccessObject = eventDataAccessObject; + this.presenter = presenter; + this.distanceCalculator = distanceCalculator; + } + + @Override + public void execute(EventDescriptionInputData inputData) { + Event event = eventDataAccessObject.get(inputData.getEventId()); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + + if (event == null) { + presenter.prepareFailView("Event not found."); + return; + } + + Location eventLocation = event.getLocation(); + + double distanceKm = distanceCalculator.distanceKm( + inputData.getUserLatitude(), + inputData.getUserLongitude(), + eventLocation.getLatitude(), + eventLocation.getLongitude() + ); + + EventDescriptionOutputData outputData = new EventDescriptionOutputData( + event.getName(), + event.getDescription(), + event.getAddress(), + event.getCategory().toString(), + event.getStartTime().format(formatter), + distanceKm + ); + + presenter.prepareSuccessView(outputData); + } +} + diff --git a/src/main/java/use_case/event_description/EventDescriptionOutputBoundary.java b/src/main/java/use_case/event_description/EventDescriptionOutputBoundary.java new file mode 100644 index 00000000..a3cb2fc4 --- /dev/null +++ b/src/main/java/use_case/event_description/EventDescriptionOutputBoundary.java @@ -0,0 +1,6 @@ +package use_case.event_description; + +public interface EventDescriptionOutputBoundary { + void prepareSuccessView(EventDescriptionOutputData outputData); + void prepareFailView(String errorMessage); +} diff --git a/src/main/java/use_case/event_description/EventDescriptionOutputData.java b/src/main/java/use_case/event_description/EventDescriptionOutputData.java new file mode 100644 index 00000000..f519a2d8 --- /dev/null +++ b/src/main/java/use_case/event_description/EventDescriptionOutputData.java @@ -0,0 +1,49 @@ +package use_case.event_description; + +public class EventDescriptionOutputData { + + private final String name; + private final String description; + private final String address; + private final String category; + private final String dateTime; + private final double distanceKm; + + public EventDescriptionOutputData(String name, + String description, + String address, + String category, + String dateTime, + double distanceKm) { + this.name = name; + this.description = description; + this.address = address; + this.category = category; + this.dateTime = dateTime; + this.distanceKm = distanceKm; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getAddress() { + return address; + } + + public String getCategory() { + return category; + } + + public String getDateTime() { + return dateTime; + } + + public double getDistanceKm() { + return distanceKm; + } +} diff --git a/src/main/java/use_case/event_description/HaversineDistanceCalculator.java b/src/main/java/use_case/event_description/HaversineDistanceCalculator.java new file mode 100644 index 00000000..d3038ee1 --- /dev/null +++ b/src/main/java/use_case/event_description/HaversineDistanceCalculator.java @@ -0,0 +1,20 @@ +package use_case.event_description; + +public class HaversineDistanceCalculator implements DistanceCalculator { + + private static final double EARTH_RADIUS_KM = 6371.0; + + @Override + public double distanceKm(double lat1, double lon1, double lat2, double lon2) { + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) + * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLon / 2) * Math.sin(dLon / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return EARTH_RADIUS_KM * c; + } +} diff --git a/src/main/java/use_case/login/LoginInputBoundary.java b/src/main/java/use_case/login/LoginInputBoundary.java index faf72dc9..3f246cbd 100644 --- a/src/main/java/use_case/login/LoginInputBoundary.java +++ b/src/main/java/use_case/login/LoginInputBoundary.java @@ -10,4 +10,9 @@ public interface LoginInputBoundary { * @param loginInputData the input data */ void execute(LoginInputData loginInputData); + + /** + * Executes the switch to sign up view use case. + */ + void switchToSignUpView(); } diff --git a/src/main/java/use_case/login/LoginInteractor.java b/src/main/java/use_case/login/LoginInteractor.java index 6764b2d7..ca5a7aea 100644 --- a/src/main/java/use_case/login/LoginInteractor.java +++ b/src/main/java/use_case/login/LoginInteractor.java @@ -38,4 +38,9 @@ public void execute(LoginInputData loginInputData) { } } } + + @Override + public void switchToSignUpView() { + loginPresenter.switchToSignUpView(); + } } diff --git a/src/main/java/use_case/login/LoginOutputBoundary.java b/src/main/java/use_case/login/LoginOutputBoundary.java index 08bc4731..81c6934f 100644 --- a/src/main/java/use_case/login/LoginOutputBoundary.java +++ b/src/main/java/use_case/login/LoginOutputBoundary.java @@ -15,4 +15,9 @@ public interface LoginOutputBoundary { * @param errorMessage the explanation of the failure */ void prepareFailView(String errorMessage); + + /** + * Switches to the SignUp View. + */ + void switchToSignUpView(); } diff --git a/src/main/java/use_case/login/LoginOutputData.java b/src/main/java/use_case/login/LoginOutputData.java index c0ff3c3a..fc847e68 100644 --- a/src/main/java/use_case/login/LoginOutputData.java +++ b/src/main/java/use_case/login/LoginOutputData.java @@ -14,5 +14,4 @@ public LoginOutputData(String username) { public String getUsername() { return username; } - } diff --git a/src/main/java/use_case/logout/LogoutInteractor.java b/src/main/java/use_case/logout/LogoutInteractor.java index d252f7c4..63d31c50 100644 --- a/src/main/java/use_case/logout/LogoutInteractor.java +++ b/src/main/java/use_case/logout/LogoutInteractor.java @@ -10,6 +10,8 @@ public class LogoutInteractor implements LogoutInputBoundary { public LogoutInteractor(LogoutUserDataAccessInterface userDataAccessInterface, LogoutOutputBoundary logoutOutputBoundary) { // TODO: save the DAO and Presenter in the instance variables. + this.userDataAccessObject = userDataAccessInterface; + this.logoutPresenter = logoutOutputBoundary; } @Override @@ -18,6 +20,17 @@ public void execute() { // * set the current username to null in the DAO // * instantiate the `LogoutOutputData`, which needs to contain the username. // * tell the presenter to prepare a success view. + + final String username = userDataAccessObject.getCurrentUsername();//get username + userDataAccessObject.setCurrentUsername(null);// set username to null in the DAO + LogoutOutputData outputData = new LogoutOutputData(username);//instantiate the `LogoutOutputData` + logoutPresenter.prepareSuccessView(outputData);//tell the presenter to prepare a success view. + + + + + + } } diff --git a/src/main/java/use_case/logout/LogoutUserDataAccessInterface.java b/src/main/java/use_case/logout/LogoutUserDataAccessInterface.java index 7a7b08e1..3e9cb5fc 100644 --- a/src/main/java/use_case/logout/LogoutUserDataAccessInterface.java +++ b/src/main/java/use_case/logout/LogoutUserDataAccessInterface.java @@ -16,4 +16,5 @@ public interface LogoutUserDataAccessInterface { * @param username the new current username */ void setCurrentUsername(String username); + } diff --git a/src/main/java/view/EventDescriptionView.java b/src/main/java/view/EventDescriptionView.java new file mode 100644 index 00000000..7e6a0397 --- /dev/null +++ b/src/main/java/view/EventDescriptionView.java @@ -0,0 +1,131 @@ +package view; + +import interface_adapter.event_description.EventDescriptionViewModel; + +import javax.swing.*; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public class EventDescriptionView extends JPanel implements PropertyChangeListener { + + private final EventDescriptionViewModel viewModel; + + private final JLabel titleLabel = new JLabel("", SwingConstants.CENTER); + + // value labels + private final JLabel nameValueLabel = new JLabel(); + private final JLabel descriptionValueLabel = new JLabel(); + private final JLabel addressValueLabel = new JLabel(); + private final JLabel categoryValueLabel = new JLabel(); + private final JLabel dateTimeValueLabel = new JLabel(); + private final JLabel distanceValueLabel = new JLabel(); + + public EventDescriptionView(EventDescriptionViewModel viewModel) { + this.viewModel = viewModel; + this.viewModel.addPropertyChangeListener(this); + + setBackground(Color.WHITE); + setLayout(new BorderLayout(0, 24)); + setBorder(new EmptyBorder(24, 32, 24, 32)); + + // ---------- Title (event name) ---------- + titleLabel.setFont(new Font("SansSerif", Font.BOLD, 28)); + titleLabel.setForeground(new Color(20, 20, 20)); + add(titleLabel, BorderLayout.NORTH); + + // ---------- Image area ---------- + JPanel imagePanel = new JPanel(); + imagePanel.setPreferredSize(new Dimension(600, 220)); + imagePanel.setBackground(new Color(240, 244, 252)); // light blue-ish + imagePanel.setBorder(new LineBorder(new Color(220, 228, 240))); + + JLabel imagePlaceholder = new JLabel("Event image", SwingConstants.CENTER); + imagePlaceholder.setFont(new Font("SansSerif", Font.PLAIN, 14)); + imagePlaceholder.setForeground(new Color(130, 140, 160)); + imagePanel.setLayout(new BorderLayout()); + imagePanel.add(imagePlaceholder, BorderLayout.CENTER); + + add(imagePanel, BorderLayout.CENTER); + + // ---------- Details card ---------- + JPanel detailsCard = new JPanel(new GridBagLayout()); + detailsCard.setBackground(Color.WHITE); + detailsCard.setBorder(new CompoundBorder( + new LineBorder(new Color(220, 228, 240)), + new EmptyBorder(16, 24, 16, 24) + )); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(6, 6, 6, 6); + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + + int row = 0; + addRow(detailsCard, gbc, row++, "Name:", nameValueLabel); + addRow(detailsCard, gbc, row++, "Description:", descriptionValueLabel); + addRow(detailsCard, gbc, row++, "Address:", addressValueLabel); + addRow(detailsCard, gbc, row++, "Category:", categoryValueLabel); + addRow(detailsCard, gbc, row++, "Date / Time:", dateTimeValueLabel); + addRow(detailsCard, gbc, row, "Distance:", distanceValueLabel); + + add(detailsCard, BorderLayout.SOUTH); + + // ---------- Fake sample data for now ---------- + viewModel.setEventDetails( + "Live at the Park", + "An outdoor evening concert featuring local bands and food trucks.", + "123 Queen St W, Toronto, ON", + "Music", + "Nov 30, 2025 • 7:30 PM", + "2.4 km away" + ); + } + + /** + * Helper to add one row ("Label:" + value) with blue label styling. + */ + private void addRow(JPanel panel, GridBagConstraints gbc, int row, + String labelText, JLabel valueLabel) { + + Color primaryBlue = new Color(0, 102, 204); // blue highlights + + JLabel label = new JLabel(labelText); + label.setFont(new Font("SansSerif", Font.BOLD, 13)); + label.setForeground(primaryBlue); + + gbc.gridx = 0; + gbc.gridy = row; + gbc.weightx = 0.0; + panel.add(label, gbc); + + valueLabel.setFont(new Font("SansSerif", Font.PLAIN, 13)); + valueLabel.setForeground(new Color(40, 40, 40)); + + gbc.gridx = 1; + gbc.gridy = row; + gbc.weightx = 1.0; + panel.add(valueLabel, gbc); + } + + public String getViewName() { + return EventDescriptionViewModel.VIEW_NAME; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + // Update UI from the ViewModel + titleLabel.setText(viewModel.getName()); + + nameValueLabel.setText(viewModel.getName()); + descriptionValueLabel.setText(viewModel.getDescription()); + addressValueLabel.setText(viewModel.getAddress()); + categoryValueLabel.setText(viewModel.getCategory()); + dateTimeValueLabel.setText(viewModel.getDateTime()); + distanceValueLabel.setText(viewModel.getDistanceText()); + } +} diff --git a/src/main/java/view/LabelTextPanel.java b/src/main/java/view/LabelTextPanel.java index 66bc10c4..db1d81f3 100644 --- a/src/main/java/view/LabelTextPanel.java +++ b/src/main/java/view/LabelTextPanel.java @@ -1,13 +1,28 @@ package view; import javax.swing.*; +import java.awt.*; /** * A panel containing a label and a text field. */ class LabelTextPanel extends JPanel { LabelTextPanel(JLabel label, JTextField textField) { - this.add(label); - this.add(textField); + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + setOpaque(false); + label.setFont(new Font("SegoeUI", Font.BOLD, 14)); + textField.setFont(new Font("SegoeUI", Font.PLAIN, 14)); + + label.setAlignmentX(Component.LEFT_ALIGNMENT); + textField.setAlignmentX(Component.LEFT_ALIGNMENT); + + Dimension fieldSize = new Dimension(250, 40); // width 250px, height 30px + textField.setMaximumSize(fieldSize); + textField.setPreferredSize(fieldSize); + textField.setMinimumSize(fieldSize); + + add(label); + add(Box.createRigidArea(new Dimension(0, 5))); + add(textField); } } diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index f38cc220..f3233f9a 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -23,7 +23,7 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private final LoggedInViewModel loggedInViewModel; private final JLabel passwordErrorField = new JLabel(); private ChangePasswordController changePasswordController = null; - private LogoutController logoutController; + private LogoutController logoutController = null; private final JLabel username; @@ -109,8 +109,13 @@ public void changedUpdate(DocumentEvent e) { */ public void actionPerformed(ActionEvent evt) { System.out.println("Click " + evt.getActionCommand()); + + if (evt.getSource().equals(logOut)) { + logoutController.execute(); + } } + @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals("state")) { @@ -139,6 +144,6 @@ public void setChangePasswordController(ChangePasswordController changePasswordC } public void setLogoutController(LogoutController logoutController) { - // TODO: save the logout controller in the instance variable. + this.logoutController = logoutController; } } diff --git a/src/main/java/view/LoginView.java b/src/main/java/view/LoginView.java index f7809810..b46cf4d2 100644 --- a/src/main/java/view/LoginView.java +++ b/src/main/java/view/LoginView.java @@ -45,9 +45,9 @@ public LoginView(LoginViewModel loginViewModel) { new JLabel("Password"), passwordInputField); final JPanel buttons = new JPanel(); - logIn = new JButton("log in"); + logIn = new JButton("Log In"); buttons.add(logIn); - cancel = new JButton("cancel"); + cancel = new JButton("Back"); buttons.add(cancel); logIn.addActionListener( @@ -65,7 +65,14 @@ public void actionPerformed(ActionEvent evt) { } ); - cancel.addActionListener(this); + cancel.addActionListener( + new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + loginController.switchToSignUpView(); + } + } + ); usernameInputField.getDocument().addDocumentListener(new DocumentListener() { diff --git a/src/main/java/view/SignupView.java b/src/main/java/view/SignupView.java index 1330c097..18a1535e 100644 --- a/src/main/java/view/SignupView.java +++ b/src/main/java/view/SignupView.java @@ -10,6 +10,10 @@ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; @@ -26,15 +30,26 @@ public class SignupView extends JPanel implements ActionListener, PropertyChange private SignupController signupController = null; private final JButton signUp; - private final JButton cancel; private final JButton toLogin; public SignupView(SignupViewModel signupViewModel) { this.signupViewModel = signupViewModel; signupViewModel.addPropertyChangeListener(this); + JPanel leftPanel = brandPanel(); + JPanel rightPanel = new JPanel(); + rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS)); + rightPanel.setBorder(BorderFactory.createEmptyBorder(100, 0, 0, 0)); + + final JLabel title = new JLabel(SignupViewModel.TITLE_LABEL); title.setAlignmentX(Component.CENTER_ALIGNMENT); + title.setFont(new Font("SegoeUI", Font.BOLD, 36)); + + JLabel subtitleLabel = new JLabel("Please enter your details to sign in"); + subtitleLabel.setFont(new Font("SegoeUI", Font.PLAIN, 14)); + subtitleLabel.setForeground(new Color(107, 114, 128)); + subtitleLabel.setAlignmentX(Component.CENTER_ALIGNMENT); final LabelTextPanel usernameInfo = new LabelTextPanel( new JLabel(SignupViewModel.USERNAME_LABEL), usernameInputField); @@ -48,8 +63,9 @@ public SignupView(SignupViewModel signupViewModel) { buttons.add(toLogin); signUp = new JButton(SignupViewModel.SIGNUP_BUTTON_LABEL); buttons.add(signUp); - cancel = new JButton(SignupViewModel.CANCEL_BUTTON_LABEL); - buttons.add(cancel); + + styleButton(signUp, new Color(0, 0, 0)); + styleButton(toLogin, new Color(0, 0, 0)); signUp.addActionListener( // This creates an anonymous subclass of ActionListener and instantiates it. @@ -76,19 +92,108 @@ public void actionPerformed(ActionEvent evt) { } ); - cancel.addActionListener(this); - addUsernameListener(); addPasswordListener(); addRepeatPasswordListener(); - this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + this.setLayout(new GridLayout(1, 2)); + + JPanel titlePanel = new JPanel(); + titlePanel.setLayout(new BoxLayout(titlePanel, BoxLayout.Y_AXIS)); + titlePanel.setOpaque(false); + titlePanel.setAlignmentX(Component.CENTER_ALIGNMENT); + + titlePanel.add(title); + titlePanel.add(Box.createRigidArea(new Dimension(0, 10))); + titlePanel.add(subtitleLabel); + + JPanel formPanel = new JPanel(); + formPanel.setLayout(new BoxLayout(formPanel, BoxLayout.Y_AXIS)); + formPanel.setOpaque(false); + formPanel.setAlignmentX(Component.CENTER_ALIGNMENT); + + formPanel.add(usernameInfo); + formPanel.add(Box.createRigidArea(new Dimension(0, 20))); + + formPanel.add(passwordInfo); + formPanel.add(Box.createRigidArea(new Dimension(0, 20))); + + formPanel.add(repeatPasswordInfo); + + + + rightPanel.add(titlePanel); + rightPanel.add(Box.createRigidArea(new Dimension(0, 20))); + rightPanel.add(formPanel); + rightPanel.add(Box.createRigidArea(new Dimension(0, 20))); + rightPanel.add(buttons); + + leftPanel.setPreferredSize(new Dimension(400, 600)); + rightPanel.setPreferredSize(new Dimension(400, 600)); + + + this.add(leftPanel); + this.add(rightPanel); + } + + /** + * + * @return a custom JPanel for the left side of the sign up view + */ + private JPanel brandPanel() { + JPanel panel = new JPanel() { + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + // Extension of the Graphics class to allow for gradients + Graphics2D g2d = (Graphics2D) g; + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // Create gradient + GradientPaint gradient = new GradientPaint( + 0, 0, new Color(10, 103, 198), + getWidth(), getHeight(), new Color(13, 133, 251) + ); + g2d.setPaint(gradient); + g2d.fillRect(0, 0, getWidth(), getHeight()); + + // Draw decorative circles + g2d.setColor(new Color(255, 255, 255, 30)); + g2d.fillOval(-50, -50, 200, 200); + g2d.fillOval(getWidth() - 150, getHeight() - 150, 200, 200); + } + }; + panel.setLayout(new GridBagLayout()); + + JPanel content = new JPanel(); + content.setOpaque(false); + content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel(SignupViewModel.TITLE); + title.setFont(new Font("Segoe UI", Font.BOLD, 36)); + title.setForeground(Color.WHITE); + title.setAlignmentX(Component.CENTER_ALIGNMENT); + + JLabel subtitle = new JLabel(SignupViewModel.CAPTION); + subtitle.setFont(new Font("Segoe UI", Font.PLAIN, 16)); + subtitle.setForeground(new Color(255, 255, 255, 200)); + subtitle.setAlignmentX(Component.CENTER_ALIGNMENT); + + content.add(title); + content.add(Box.createRigidArea(new Dimension(0, 10))); + content.add(subtitle); + + panel.add(content); + return panel; + } - this.add(title); - this.add(usernameInfo); - this.add(passwordInfo); - this.add(repeatPasswordInfo); - this.add(buttons); + private void styleButton(JButton button, Color color) { + button.setFont(new Font("SegoeUI", Font.BOLD, 14)); + button.setForeground(Color.BLUE); + button.setBackground(color); + button.setFocusPainted(false); + button.setBorderPainted(false); + button.setCursor(new Cursor(Cursor.HAND_CURSOR)); } private void addUsernameListener() { diff --git a/src/main/resources/images/concert.jpg b/src/main/resources/images/concert.jpg new file mode 100644 index 00000000..e13896df Binary files /dev/null and b/src/main/resources/images/concert.jpg differ diff --git a/src/test/java/data_access/data_access.java b/src/test/java/data_access/data_access.java new file mode 100644 index 00000000..0601c78f --- /dev/null +++ b/src/test/java/data_access/data_access.java @@ -0,0 +1,47 @@ +package data_access; + +import entity.Event; +import entity.Location; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class InMemoryEventDataAccessObjectTest { + + @Test + void saveAndGetEvent() { + EventDataAccessInterface eventDAO = new InMemoryEventDataAccessObject(); + + Location loc = new Location(43.0, -79.0); + Event event = new Event( + "1", + "Music Festival", + "Outdoor live music event", + "123 Queen St", + loc, + "Music", + "2025-11-20T19:00" + ); + + eventDAO.save(event); + Event retrieved = eventDAO.get("1"); + + assertTrue(eventDAO.existsById("1")); + assertNotNull(retrieved); + assertEquals("Music Festival", retrieved.getName()); + assertEquals("123 Queen St", retrieved.getAddress()); + assertEquals(43.0, retrieved.getLocation().getLatitude(), 1e-6); + assertEquals(-79.0, retrieved.getLocation().getLongitude(), 1e-6); + } + + @Test + void eventDoesNotExist() { + EventDataAccessInterface eventDAO = new InMemoryEventDataAccessObject(); + + boolean exists = eventDAO.existsById("999"); + Event retrieved = eventDAO.get("999"); + + assertFalse(exists); + assertNull(retrieved); + } +} diff --git a/src/test/java/use_case/event_description/EventDescriptionInteractorTest.java b/src/test/java/use_case/event_description/EventDescriptionInteractorTest.java new file mode 100644 index 00000000..ac1c3657 --- /dev/null +++ b/src/test/java/use_case/event_description/EventDescriptionInteractorTest.java @@ -0,0 +1,80 @@ +package use_case.event_description; + +import data_access.InMemoryEventDataAccessObject; +import entity.Event; +import entity.Location; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class EventDescriptionInteractorTest { + + private static class TestPresenter implements EventDescriptionOutputBoundary { + EventDescriptionOutputData lastOutput; + String lastError; + + @Override + public void prepareSuccessView(EventDescriptionOutputData outputData) { + lastOutput = outputData; + } + + @Override + public void prepareFailView(String errorMessage) { + lastError = errorMessage; + } + } + + @Test + void successPath_eventFoundAndDistanceComputed() { + // Arrange + InMemoryEventDataAccessObject eventDAO = new InMemoryEventDataAccessObject(); + Location loc = new Location(43.0, -79.0); + Event event = new Event( + "1", + "Music Festival", + "Outdoor live music event", + "123 Queen St", + loc, + "Music", + "2025-11-20T19:00" + ); + eventDAO.save(event); + + TestPresenter presenter = new TestPresenter(); + DistanceCalculator distanceCalculator = new HaversineDistanceCalculator(); + + EventDescriptionInputBoundary interactor = + new EventDescriptionInteractor(eventDAO, presenter, distanceCalculator); + + EventDescriptionInputData input = + new EventDescriptionInputData("1", 43.1, -79.0); + + interactor.execute(input); + + assertNull(presenter.lastError); + assertNotNull(presenter.lastOutput); + + assertEquals("Music Festival", presenter.lastOutput.getName()); + assertEquals("123 Queen St", presenter.lastOutput.getAddress()); + + assertTrue(presenter.lastOutput.getDistanceKm() > 0); + } + + @Test + void eventNotFound_callsFailure() { + InMemoryEventDataAccessObject eventDAO = new InMemoryEventDataAccessObject(); + TestPresenter presenter = new TestPresenter(); + DistanceCalculator distanceCalculator = new HaversineDistanceCalculator(); + + EventDescriptionInputBoundary interactor = + new EventDescriptionInteractor(eventDAO, presenter, distanceCalculator); + + EventDescriptionInputData input = + new EventDescriptionInputData("999", 43.0, -79.0); + + interactor.execute(input); + + assertNull(presenter.lastOutput); + assertEquals("Event not found.", presenter.lastError); + } +} diff --git a/src/test/java/use_case/login/LoginInteractorTest.java b/src/test/java/use_case/login/LoginInteractorTest.java index 4662abd9..4a1aa841 100644 --- a/src/test/java/use_case/login/LoginInteractorTest.java +++ b/src/test/java/use_case/login/LoginInteractorTest.java @@ -31,6 +31,11 @@ public void prepareSuccessView(LoginOutputData user) { public void prepareFailView(String error) { fail("Use case failure is unexpected."); } + + @Override + public void switchToSignUpView() { + + } }; LoginInputBoundary interactor = new LoginInteractor(userRepository, successPresenter); @@ -61,6 +66,11 @@ public void prepareSuccessView(LoginOutputData user) { public void prepareFailView(String error) { assertEquals("Incorrect password for \"Paul\".", error); } + + @Override + public void switchToSignUpView() { + + } }; LoginInputBoundary interactor = new LoginInteractor(userRepository, failurePresenter); @@ -86,6 +96,11 @@ public void prepareSuccessView(LoginOutputData user) { public void prepareFailView(String error) { assertEquals("Paul: Account does not exist.", error); } + + @Override + public void switchToSignUpView() { + + } }; LoginInputBoundary interactor = new LoginInteractor(userRepository, failurePresenter);