diff --git a/.env b/.env new file mode 100644 index 00000000..b207e4a2 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +TICKETMASTER_API_KEY=oL2pW4zAlAZvhBAhPNi5mNYvS7OsBM9J \ No newline at end of file diff --git a/207-project-event-discovery b/207-project-event-discovery new file mode 160000 index 00000000..d83bd1e2 --- /dev/null +++ b/207-project-event-discovery @@ -0,0 +1 @@ +Subproject commit d83bd1e206776d42a29d78586fe3218c13fbb0b7 diff --git a/README.md b/README.md index 3b229575..1a9da7b4 100644 --- a/README.md +++ b/README.md @@ -1,248 +1,93 @@ -# 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). -To earn credit: -- your team must demo your working `logout` use case. +# Table of Contents +- [Overview](#overview) +- [Usage Guide](#guide) +- [Contributors](#contributors) +- [Demo/Screenshots](#demo) +- [PR Style](#style) +- [Setup](#setup) -Your demo should be similar to the below example: -![](images/sample-logout.gif) +

---- +# Overview: +Event Gate: This project acts like an alternative to Ticketmaster, allowing users to search for local events, view them through a calendar, and saving these events into memory for viewing at later times. -## 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.** +# TODOs -**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) +# File Structure +- Users can create accounts with a personal username and password to be stored into local memory +- Users can search for events near them based on location +- Users can categorize the events they search for based on specific categories +- Users can view events in a calendar format, viewing events based off of date +- Users can save events to be viewed later +- Users can view the description of the events ---- -## Task 1: Understanding the Program +#Contributors: +1. Christopher Mong +2. Joyi Xue -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. +# PR Style: +- [chore]: Used to represent menial tasks that were simple fixes or style cleanup. -> 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. +# Initial Setup: +In order to properly use the Ticketmaster API, you must sign up to Ticketmaster's open source developer site; then you will receive an API Key from the Discovery API. This should be substituted into the EventDataAccessObject's API_KEY constant. Only from here will you have access to real time events from Ticketmaster. -### Task 1.1: Exploring the login use case +Screenshot 2025-11-24 at 6 59 17 PM -Let's take a tour of the login use case code: +# Usage Guide +## Features -- In IntelliJ, find the `LoginController` class and open it. +### Searching for Events -- Set a breakpoint inside its `execute` method. +#### Login/Signup +1. Launch the application +2. You will see a sign up page +3. Sign up, or navigate to the login page to login to a pre-existing user +4. Click the log in/sign up button -- 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. +#### Via Search Bar +1. Sign Up/ Log In +2. You'll see a search bar at the top of the dashboard +3. Type an event name (e.g., "Raptors", "Drake", "Hamilton") +4. Press **Enter** or click the search button +5. The app will display matching events near your default location (Toronto) -- **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. +#### Via Location +1. Sign Up / Log In +2. If you wish to change the location in viewing events, click the location drop down +3. Change to one of the pre-saved locations +4. Click search -> 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. +#### ViA Category +1. Sign Up / Log In +2. If you wish to diplay events by different categories, click the category drop down +3. Change it to the category you want -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. +#### ViA Distance/Datetime/Name +1. Sign Up / Log In +2. If you wish to diplay events by Distance/Datetime/Name, click the Distance drop down which is Distance default +3. Change to the events which you want these to be sorted by -## Task 2: Implementing the Logout Use Case +#### ViA Calender +1. Sign Up / Log In +2. Click on the calender button at the left side of the dashboard +3. You will see all the events displayed in a calendar view -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. +**Example Searches:** +- `Raptors` → Find Toronto Raptors basketball games +- `Concert` → Find all concerts in your area -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.** +#### Changing Search Location +The default search location is Toronto, ON. To change it: -> 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? - ---- diff --git a/events_saved/c_events.json b/events_saved/c_events.json new file mode 100644 index 00000000..aa30ba76 --- /dev/null +++ b/events_saved/c_events.json @@ -0,0 +1,28 @@ +[ + { + "imageUrl": "https://s1.ticketm.net/dam/a/810/587e669b-5946-46c0-a526-b63b6e008810_1339651_CUSTOM.jpg", + "name": "Toronto Raptors vs. Indiana Pacers", + "description": "Fans in attendance agree to abide by the health and safety measures in effect at the time of the event. Prior to attending please review and ensure compliance with all policies and protocols at Scotiabank Arena Know Before You GoCapacity: 21,823Please take note of the venue's Bag Policy", + "startTime": "2025-11-27T00:30", + "location": { + "address": "Scotiabank Arena, 50 Bay Street, Suite 500, Toronto, ON", + "latitude": 43.6439254, + "longitude": -79.3783234 + }, + "id": "1AvZZbsGklFqdeB", + "category": "SPORTS" + }, + { + "imageUrl": "https://s1.ticketm.net/dam/a/2d9/2f921db0-3766-4ceb-b1a8-597b8cc672d9_1277181_ARTIST_PAGE_3_2.jpg", + "name": "Toronto Maple Leafs v Montreal Canadiens", + "description": "Fans in attendance agree to abide by the health and safety measures in effect at the time of the event. Prior to attending please review and ensure compliance with all policies and protocols at Scotiabank Arena Know Before You GoCapacity: 20,543Please take note of the venue's Bag Policy", + "startTime": "2025-12-07T00:00", + "location": { + "address": "Scotiabank Arena, 50 Bay Street, Suite 500, Toronto, ON", + "latitude": 43.6439254, + "longitude": -79.3783234 + }, + "id": "177ZvxG62riyKGH", + "category": "SPORTS" + } +] \ No newline at end of file diff --git a/events_saved/demo_user_events.json b/events_saved/demo_user_events.json new file mode 100644 index 00000000..870ea49c --- /dev/null +++ b/events_saved/demo_user_events.json @@ -0,0 +1,13 @@ +[{ + "imageUrl": "https://via.placeholder.com/500x700", + "name": "Toronto Raptors vs Los Angeles Lakers", + "description": "An epic showdown between two NBA powerhouses. Watch the Raptors take on LeBron James and the Lakers in what promises to be an unforgettable night of basketball action!", + "startTime": "2025-12-15T19:30", + "location": { + "address": "Scotiabank Arena, 40 Bay St, Toronto, ON", + "latitude": 43.6435, + "longitude": -79.3791 + }, + "id": "event-1", + "category": "SPORTS" +}] \ No newline at end of file diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 3c654cad..c9a5f4b9 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -1,8 +1,27 @@ 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.*; + + import data_access.FileUserDataAccessObject; +import entity.Event; +import entity.Location; +import data_access.EventDataAccessObject; +import data_access.TicketmasterEventRepositoryAdapter; +import data_access.CalendarFlowDataAccessObject; +import data_access.SearchEventDataAccessObject; +import data_access.FileSavedEventsDataAccessObject; import entity.UserFactory; import interface_adapter.ViewManagerModel; +import interface_adapter.calendarFlow.CalendarFlowViewModel; +import interface_adapter.calendarFlow.CalendarFlowPresenter; +import interface_adapter.calendarFlow.CalendarFlowController; import interface_adapter.logged_in.ChangePasswordController; import interface_adapter.logged_in.ChangePasswordPresenter; import interface_adapter.logged_in.LoggedInViewModel; @@ -11,9 +30,27 @@ import interface_adapter.login.LoginViewModel; import interface_adapter.logout.LogoutController; import interface_adapter.logout.LogoutPresenter; +import interface_adapter.search.SearchController; +import interface_adapter.search.SearchPresenter; +import interface_adapter.search_event_by_name.SearchEventByNameController; +import interface_adapter.search_event_by_name.SearchEventByNamePresenter; +import interface_adapter.search_event_by_name.SearchEventByNameViewModel; import interface_adapter.signup.SignupController; import interface_adapter.signup.SignupPresenter; import interface_adapter.signup.SignupViewModel; +import use_case.calendarFlow.CalendarFlowInputBoundary; +import use_case.calendarFlow.CalendarFlowInteractor; +import use_case.calendarFlow.CalendarFlowOutputBoundary; +import use_case.calendarFlow.CalendarFlowDataAccessInterface; +import interface_adapter.save_event.SaveEventController; +import interface_adapter.save_event.SaveEventPresenter; +import interface_adapter.save_event.SaveEventViewModel; + +import interface_adapter.display_local_events.DisplayLocalEventsController; +import interface_adapter.display_local_events.DisplayLocalEventsPresenter; +import interface_adapter.display_local_events.DisplayLocalEventsViewModel; +import interface_adapter.ViewManagerModel; + import use_case.change_password.ChangePasswordInputBoundary; import use_case.change_password.ChangePasswordInteractor; import use_case.change_password.ChangePasswordOutputBoundary; @@ -23,32 +60,69 @@ import use_case.logout.LogoutInputBoundary; import use_case.logout.LogoutInteractor; import use_case.logout.LogoutOutputBoundary; +import use_case.search.SearchInputBoundary; +import use_case.search.SearchInteractor; +import use_case.search.SearchOutputBoundary; +import use_case.search_event_by_name.SearchEventByNameInputBoundary; +import use_case.search_event_by_name.SearchEventByNameInteractor; +import use_case.search_event_by_name.SearchEventByNameOutputBoundary; import use_case.signup.SignupInputBoundary; import use_case.signup.SignupInteractor; import use_case.signup.SignupOutputBoundary; +import view.*; + +import use_case.display_local_events.DisplayLocalEventsInputBoundary; +import use_case.display_local_events.DisplayLocalEventsInteractor; +import use_case.display_local_events.DisplayLocalEventsOutputBoundary; + import view.LoggedInView; import view.LoginView; import view.SignupView; import view.ViewManager; +import view.DisplayLocalEventsView; +import use_case.save_event.SaveEventInputBoundary; +import use_case.save_event.SaveEventInteractor; +import use_case.save_event.SaveEventOutputBoundary; import javax.swing.*; import java.awt.*; +/** + * AppBuilder - Constructs and wires together all components of the application. + * + * CLEAN ARCHITECTURE NOTE: + * This class is responsible for Dependency Injection and wiring. + * It creates all the layers and connects them: + * + * 1. ENTITIES (innermost): Event, User, Location, etc. + * 2. USE CASES: Interactors (business logic) + * 3. INTERFACE ADAPTERS: Controllers, Presenters, ViewModels + * 4. FRAMEWORKS & DRIVERS (outermost): Views, Data Access Objects + * + * The Dependency Rule: Dependencies point INWARD. + * - Views depend on ViewModels and Controllers + * - Controllers depend on Use Case Input Boundaries (interfaces) + * - Presenters implement Use Case Output Boundaries (interfaces) + * - Use Cases depend on Entity interfaces, not concrete implementations + * + * This builder ensures proper dependency injection so each layer + * only knows about the layers inside it, never outside. + */ public class AppBuilder { + private EventDescriptionViewModel eventDescriptionViewModel; + private EventDescriptionView eventDescriptionView; + + final EventDataAccessInterface eventDataAccessObject = new InMemoryEventDataAccessObject(); + final FileSavedEventsDataAccessObject savedEventsDAO = new FileSavedEventsDataAccessObject(); + 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); + final FileUserDataAccessObject userDataAccessObject = + new FileUserDataAccessObject("users.csv", userFactory); private SignupView signupView; private SignupViewModel signupViewModel; @@ -56,6 +130,20 @@ public class AppBuilder { private LoggedInViewModel loggedInViewModel; private LoggedInView loggedInView; private LoginView loginView; + private SearchEventByNameView searchEventView; + private SearchEventByNameViewModel searchEventViewModel; + private SaveEventViewModel saveEventViewModel; + private SaveEventsView saveEventsView; + private SaveButtonView saveButtonView; + private SaveEventInteractor saveEventInteractor; + private SaveEventController saveEventController; + + private CalendarFlowViewModel calendarFlowViewModel; + private CalendarView calendarView; + private EventListByDateView eventListByDateView; + + private DisplayLocalEventsViewModel displayLocalEventsViewModel; + private DisplayLocalEventsView displayLocalEventsView; public AppBuilder() { cardPanel.setLayout(cardLayout); @@ -82,6 +170,85 @@ public AppBuilder addLoggedInView() { return this; } + public AppBuilder addCalendarViews() { + calendarFlowViewModel = new CalendarFlowViewModel(); + eventListByDateView = new EventListByDateView(calendarFlowViewModel); + calendarView = new CalendarView(); + cardPanel.add(calendarView, "calendar view"); + cardPanel.add(eventListByDateView, calendarFlowViewModel.getViewName()); + return this; + } + + public AppBuilder addDisplayLocalEventsView() { + displayLocalEventsViewModel = new DisplayLocalEventsViewModel(); + displayLocalEventsView = new DisplayLocalEventsView(displayLocalEventsViewModel); + displayLocalEventsView.setViewManagerModel(viewManagerModel); + cardPanel.add(displayLocalEventsView, displayLocalEventsView.getViewName()); + return this; + } + + public AppBuilder addEventSearchView() { + searchEventViewModel = new SearchEventByNameViewModel(); + searchEventView = new SearchEventByNameView(searchEventViewModel); + cardPanel.add(searchEventView, searchEventView.getViewName()); + return this; + } + + public AppBuilder addSaveEventView() { + saveEventViewModel = new SaveEventViewModel(); + saveEventsView = new SaveEventsView(saveEventViewModel); + cardPanel.add(saveEventsView, saveEventsView.getViewName()); + return this; + } + + public AppBuilder addSaveEventUseCase() { + final FileSavedEventsDataAccessObject savedEventsDAO = new FileSavedEventsDataAccessObject(); + + final SaveEventOutputBoundary saveEventPresenter = new SaveEventPresenter( + saveEventViewModel, + viewManagerModel + ); + + this.saveEventInteractor = new SaveEventInteractor( + saveEventPresenter, + savedEventsDAO, + userDataAccessObject + ); + + this.saveEventController = new SaveEventController(saveEventInteractor); + + if (saveEventsView != null) { + saveEventsView.setSaveEventController(saveEventController); + saveEventsView.setSaveEventInteractor(saveEventInteractor); + } + + if (saveButtonView != null) { + saveButtonView.setSaveEventController(saveEventController); + } + + return this; + } + + public AppBuilder addSearchEventByNameUseCase() { + final SearchEventDataAccessObject searchDataAccess = new SearchEventDataAccessObject(); + + final SearchEventByNameOutputBoundary presenter = new SearchEventByNamePresenter( + searchEventViewModel, + viewManagerModel + ); + + final SearchEventByNameInputBoundary interactor = new SearchEventByNameInteractor( + searchDataAccess, + presenter + ); + + final SearchEventByNameController controller = new SearchEventByNameController(interactor); + searchEventView.setEventController(controller); + searchEventView.setSaveButtonController(saveEventController); + + return this; + } + public AppBuilder addSignupUseCase() { final SignupOutputBoundary signupOutputBoundary = new SignupPresenter(viewManagerModel, signupViewModel, loginViewModel); @@ -95,7 +262,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); @@ -116,10 +283,6 @@ public AppBuilder addChangePasswordUseCase() { return this; } - /** - * Adds the Logout Use Case to the application. - * @return this builder - */ public AppBuilder addLogoutUseCase() { final LogoutOutputBoundary logoutOutputBoundary = new LogoutPresenter(viewManagerModel, loggedInViewModel, loginViewModel); @@ -132,8 +295,135 @@ public AppBuilder addLogoutUseCase() { return this; } + public AppBuilder addCalendarFlowUseCase() { + CalendarFlowDataAccessInterface calendarGateway = new CalendarFlowDataAccessObject(); + CalendarFlowOutputBoundary calendarOutputBoundary = + new CalendarFlowPresenter(calendarFlowViewModel, viewManagerModel); + CalendarFlowInputBoundary calendarInteractor = + new CalendarFlowInteractor(calendarGateway, calendarOutputBoundary); + CalendarFlowController calendarController = new CalendarFlowController(calendarInteractor); + calendarView.setEventController(calendarController); + + return this; + } + + public AppBuilder addDisplayLocalEventsUseCase() { + EventDataAccessObject dao = new EventDataAccessObject(); + + final SearchEventDataAccessObject searchDataAccess = new SearchEventDataAccessObject(); + + final SearchOutputBoundary searchPresenter = new SearchPresenter( + searchEventViewModel, + viewManagerModel + ); + + final SearchInputBoundary searchInteractor = new SearchInteractor( + searchDataAccess, + searchPresenter + ); + + final SearchController searchController = new SearchController(searchInteractor); + + Location defaultCenter = new Location("Toronto, ON", 43.6532, -79.3832); + double defaultRadiusKm = 100.0; + + TicketmasterEventRepositoryAdapter eventRepository = + new TicketmasterEventRepositoryAdapter(dao, defaultCenter, defaultRadiusKm); + + DisplayLocalEventsOutputBoundary outputBoundary = + new DisplayLocalEventsPresenter(displayLocalEventsViewModel, viewManagerModel); + + DisplayLocalEventsInputBoundary interactor = + new DisplayLocalEventsInteractor(eventRepository, outputBoundary); + + DisplayLocalEventsController controller = + new DisplayLocalEventsController(interactor); + + displayLocalEventsView.setController(controller); + displayLocalEventsView.setSearchBarController(searchController); + + return this; + } + + /** + * Add Event Description View. + * + * CLEAN ARCHITECTURE NOTE: + * The View is created with its ViewModel. + * The View only knows about the ViewModel, not about Use Cases or Data Access. + */ + public AppBuilder addEventDescriptionView() { + eventDescriptionViewModel = new EventDescriptionViewModel(); + eventDescriptionView = new EventDescriptionView(eventDescriptionViewModel); + eventDescriptionView.setViewManagerModel(viewManagerModel); + cardPanel.add(eventDescriptionView, eventDescriptionView.getViewName()); + return this; + } + + /** + * Add Event Description Use Case and wire up event navigation. + * + * CLEAN ARCHITECTURE NOTE: + * This method demonstrates the complete wiring of Clean Architecture: + * + * 1. Create the Use Case (Interactor) with its dependencies: + * - Data Access (injected via interface) + * - Output Boundary (Presenter implements this interface) + * + * 2. Create the Controller with the Use Case (via Input Boundary interface) + * + * 3. Wire the View to use the Controller + * + * 4. Set up navigation using an EventSelectionListener + * - This follows the Dependency Inversion Principle + * - The View depends on an abstraction (listener interface) + * - The concrete implementation is injected here + */ + public AppBuilder addEventDescriptionUseCase() { + DistanceCalculator distanceCalculator = new HaversineDistanceCalculator(); + + EventDescriptionOutputBoundary presenter = + new EventDescriptionPresenter(eventDescriptionViewModel); + + EventDescriptionInputBoundary interactor = + new EventDescriptionInteractor(eventDataAccessObject, presenter, distanceCalculator); + + EventDescriptionController controller = + new EventDescriptionController(interactor); + + // Wire the save event controller to the event description view + if (saveEventController != null) { + eventDescriptionView.setSaveEventController(saveEventController); + } + + // Set up the event selection listener for navigation from DisplayLocalEventsView + // This is where we connect the "click on event card" action to the navigation + displayLocalEventsView.setEventSelectionListener(new DisplayLocalEventsView.EventSelectionListener() { + @Override + public void onEventSelected(Event event) { + // Store the event in the view for display + eventDescriptionView.displayEvent(event); + + // Calculate and set distance + Location userLocation = displayLocalEventsView.getCurrentLocationForOthers(); + if (userLocation != null && event.getLocation() != null) { + double distance = event.calculateDistanceTo(userLocation); + eventDescriptionView.setDistanceText(String.format("%.1f km from your location", distance)); + } else { + eventDescriptionView.setDistanceText("Distance unavailable"); + } + + // Navigate to the event description view + viewManagerModel.setState(eventDescriptionView.getViewName()); + viewManagerModel.firePropertyChange(); + } + }); + + return this; + } + public JFrame build() { - final JFrame application = new JFrame("User Login Example"); + final JFrame application = new JFrame("Event Gate"); application.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); application.add(cardPanel); @@ -143,6 +433,4 @@ public JFrame build() { return application; } - - -} +} \ No newline at end of file diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 424404fb..87c66e3a 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -3,15 +3,33 @@ 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() + .addCalendarViews() + .addEventDescriptionView() // NEW + .addDisplayLocalEventsView() + .addSaveEventView() + .addEventSearchView() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() + .addLogoutUseCase() + .addCalendarFlowUseCase() + .addEventDescriptionUseCase() // NEW + .addSaveEventUseCase() + .addDisplayLocalEventsUseCase() + .addSearchEventByNameUseCase() .build(); application.pack(); diff --git a/src/main/java/data_access/CalendarFlowDataAccessObject.java b/src/main/java/data_access/CalendarFlowDataAccessObject.java new file mode 100644 index 00000000..52538593 --- /dev/null +++ b/src/main/java/data_access/CalendarFlowDataAccessObject.java @@ -0,0 +1,348 @@ +package data_access; + +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import java.time.LocalDate; + +import entity.EventCategory; +import okhttp3.*; +import java.time.*; +import java.time.format.DateTimeFormatter; + +import entity.Event; +import entity.Location; +import org.json.JSONArray; +import org.json.JSONObject; +import use_case.calendarFlow.CalendarFlowDataAccessInterface; +public class CalendarFlowDataAccessObject implements CalendarFlowDataAccessInterface{ + private static final String BASE_URL = "https://app.ticketmaster.com/discovery/v2"; + private static final String EVENTS_ENDPOINT = "/events.json"; + private static final String API_KEY = "API_KEY"; + private final OkHttpClient client = new OkHttpClient(); + +// public CalendarFlowDataAccessObject() { +// } + /** + * Search events by date + * @param date the selected specific date + * @param location the user's location + * @param radiusKm the search radius in kilometers + */ + @Override + public List getEventsByDate(LocalDate date, Location location, double radiusKm) { + List events = new ArrayList<>(); + + HttpUrl.Builder urlBuilder = HttpUrl + .parse(BASE_URL + EVENTS_ENDPOINT) + .newBuilder() + .addQueryParameter("apikey", API_KEY); + + ZonedDateTime startZdt = date.atStartOfDay(ZoneOffset.UTC);//LocalDate with time 00:00 + ZonedDateTime endZdt = date.atTime(23, 59, 59).atZone(ZoneOffset.UTC);//LocalDate with time 23:59 + DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT;//a converter that convert time in timezone to string + + urlBuilder.addQueryParameter("startDateTime", formatter.format(startZdt)); + urlBuilder.addQueryParameter("endDateTime", formatter.format(endZdt)); + urlBuilder.addQueryParameter("latlong", + String.format("%.6f,%.6f", location.getLatitude(), location.getLongitude())); + urlBuilder.addQueryParameter("radius", String.valueOf((int) radiusKm)); + urlBuilder.addQueryParameter("unit", "km"); + urlBuilder.addQueryParameter("size", "50"); + + String url = urlBuilder.build().toString(); + return fetchEvents(url); + } + + private List fetchEvents(String url) { + List events = new ArrayList<>(); + + Request request = new Request.Builder() + .url(url) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + System.err.println("Failed to fetch events: " + response.code()); + return events; + } + + String jsonResponse = response.body().string(); + JSONObject root = new JSONObject(jsonResponse); + + if (!root.has("_embedded")) { + return events; + } + + JSONObject embedded = root.getJSONObject("_embedded"); + if (!embedded.has("events")) { + return events; + } + + JSONArray eventsArray = embedded.getJSONArray("events"); + + for (int i = 0; i < eventsArray.length(); i++) { + JSONObject eventJson = eventsArray.getJSONObject(i); + Event event = parseEvent(eventJson); + if (event != null) { + events.add(event); + } + } + + } catch (IOException e) { + System.err.println("Error fetching events: " + e.getMessage()); + } + + return events; + } + + private Event parseEvent(JSONObject eventJson) { + try { + String id = eventJson.getString("id"); + String name = eventJson.getString("name"); + + String description = ""; + if (eventJson.has("info")) { + description = eventJson.getString("info"); + } else if (eventJson.has("description")) { + description = eventJson.getString("description"); + } else if (eventJson.has("pleaseNote")) { + description = eventJson.getString("pleaseNote"); + } + + EventCategory category = extractCategory(eventJson); + Location location = extractLocation(eventJson); + if (location == null) { + return null; + } + + LocalDateTime startTime = extractStartTime(eventJson); + if (startTime == null) { + return null; + } + + String imageUrl = extractImageUrl(eventJson); + + return new Event(id, name, description,location.getAddress() ,category, location, startTime, imageUrl); + + } catch (Exception e) { + System.err.println("Error parsing event: " + e.getMessage()); + return null; + } + } + + private String extractImageUrl(JSONObject eventJson) { + try { + if (eventJson.has("images")) { + JSONArray images = eventJson.getJSONArray("images"); + if (images.length() > 0) { + JSONObject firstImage = images.getJSONObject(3); + if (firstImage.has("url")) { + return firstImage.getString("url"); + } + } + } + } catch (Exception e) { + System.err.println("Error extracting image URL: " + e.getMessage()); + } + return ""; + } + + private EventCategory extractCategory(JSONObject eventJson) { + try { + if (eventJson.has("classifications")) { + JSONArray classifications = eventJson.getJSONArray("classifications"); + if (classifications.length() > 0) { + JSONObject classification = classifications.getJSONObject(0); + + if (classification.has("segment")) { + JSONObject segment = classification.getJSONObject("segment"); + if (segment.has("name")) { + String segmentName = segment.getString("name"); + EventCategory category = mapTicketmasterToCategory(segmentName); + if (category != EventCategory.MISCELLANEOUS) { + return category; + } + } + } + + if (classification.has("genre")) { + JSONObject genre = classification.getJSONObject("genre"); + if (genre.has("name")) { + String genreName = genre.getString("name"); + EventCategory category = mapTicketmasterToCategory(genreName); + if (category != EventCategory.MISCELLANEOUS) { + return category; + } + } + } + } + } + } catch (Exception e) { + System.err.println("Error extracting category: " + e.getMessage()); + } + + return EventCategory.MISCELLANEOUS; + } + private Location extractLocation(JSONObject eventJson) { + try { + if (!eventJson.has("_embedded")) { + return null; + } + + JSONObject embedded = eventJson.getJSONObject("_embedded"); + if (!embedded.has("venues")) { + return null; + } + + JSONArray venues = embedded.getJSONArray("venues"); + if (venues.length() == 0) { + return null; + } + + JSONObject venue = venues.getJSONObject(0); + + StringBuilder addressBuilder = new StringBuilder(); + + if (venue.has("name")) { + addressBuilder.append(venue.getString("name")); + } + + if (venue.has("address")) { + JSONObject address = venue.getJSONObject("address"); + if (address.has("line1")) { + if (addressBuilder.length() > 0) { + addressBuilder.append(", "); + } + addressBuilder.append(address.getString("line1")); + } + } + + if (venue.has("city")) { + JSONObject city = venue.getJSONObject("city"); + if (city.has("name")) { + if (addressBuilder.length() > 0) { + addressBuilder.append(", "); + } + addressBuilder.append(city.getString("name")); + } + } + + if (venue.has("state")) { + JSONObject state = venue.getJSONObject("state"); + if (state.has("stateCode")) { + if (addressBuilder.length() > 0) { + addressBuilder.append(", "); + } + addressBuilder.append(state.getString("stateCode")); + } + } + + String address = addressBuilder.toString(); + if (address.trim().isEmpty()) { + address = "Address not available"; + } + + if (!venue.has("location")) { + return null; + } + + JSONObject locationJson = venue.getJSONObject("location"); + if (!locationJson.has("latitude") || !locationJson.has("longitude")) { + return null; + } + + double latitude = locationJson.getDouble("latitude"); + double longitude = locationJson.getDouble("longitude"); + + return new Location(address, latitude, longitude); + + } catch (Exception e) { + System.err.println("Error extracting location: " + e.getMessage()); + return null; + } + } + + private LocalDateTime extractStartTime(JSONObject eventJson) { + try { + if (!eventJson.has("dates")) { + return null; + } + + JSONObject dates = eventJson.getJSONObject("dates"); + if (!dates.has("start")) { + return null; + } + + JSONObject start = dates.getJSONObject("start"); + + if (start.has("dateTime")) { + String dateTimeStr = start.getString("dateTime"); + dateTimeStr = dateTimeStr.replace("Z", ""); + DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + return LocalDateTime.parse(dateTimeStr, formatter); + } else if (start.has("localDate")) { + String localDate = start.getString("localDate"); + String localTime = "19:00:00"; + if (start.has("localTime")) { + localTime = start.getString("localTime"); + } + return LocalDateTime.parse(localDate + "T" + localTime); + } + + return null; + + } catch (Exception e) { + System.err.println("Error extracting start time: " + e.getMessage()); + return null; + } + } + + private String mapCategoryToTicketmaster(EventCategory category) { + switch (category) { + case SPORTS: + return "Sports"; + case MUSIC: + return "Music"; + case ARTS_THEATRE: + return "Arts & Theatre"; + case FILM: + return "Film"; + case MISCELLANEOUS: + return null; + default: + return null; + } + } + + private EventCategory mapTicketmasterToCategory(String ticketmasterName) { + if (ticketmasterName == null) { + return EventCategory.MISCELLANEOUS; + } + + String normalized = ticketmasterName.toLowerCase().trim(); + + if (normalized.contains("music") || normalized.contains("concert")) { + return EventCategory.MUSIC; + } + + if (normalized.contains("sports") || normalized.contains("basketball") || + normalized.contains("football") || normalized.contains("hockey") || + normalized.contains("soccer") || normalized.contains("baseball")) { + return EventCategory.SPORTS; + } + + if (normalized.contains("arts") || normalized.contains("theatre") || + normalized.contains("theater") || normalized.contains("dance") || + normalized.contains("opera") || normalized.contains("ballet")) { + return EventCategory.ARTS_THEATRE; + } + + if (normalized.contains("film") || normalized.contains("movie") || + normalized.contains("cinema")) { + return EventCategory.FILM; + } + + return EventCategory.MISCELLANEOUS; + } +} \ No newline at end of file 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/EventDataAccessObject.java b/src/main/java/data_access/EventDataAccessObject.java new file mode 100644 index 00000000..80e92ca3 --- /dev/null +++ b/src/main/java/data_access/EventDataAccessObject.java @@ -0,0 +1,397 @@ +package data_access; + +import entity.Event; +import entity.EventCategory; +import entity.Location; +import okhttp3.*; +import org.json.*; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +public class EventDataAccessObject { + + private static final String API_KEY = "API_KEY"; // Replace with actual API key + private static final String BASE_URL = "https://app.ticketmaster.com/discovery/v2"; + private static final String EVENTS_ENDPOINT = "/events.json"; + + private final OkHttpClient client; + + public EventDataAccessObject() { + this.client = new OkHttpClient(); + } + + /** + * Get events near a location within a radius + */ + public List getEventsByLocation(Location location, double radiusKm) { + HttpUrl.Builder urlBuilder = HttpUrl.parse(BASE_URL + EVENTS_ENDPOINT).newBuilder(); + urlBuilder.addQueryParameter("apikey", API_KEY); + urlBuilder.addQueryParameter("latlong", + String.format("%.6f,%.6f", location.getLatitude(), location.getLongitude())); + urlBuilder.addQueryParameter("radius", String.valueOf((int) radiusKm)); + urlBuilder.addQueryParameter("unit", "km"); + urlBuilder.addQueryParameter("size", "50"); + urlBuilder.addQueryParameter("sort", "distance,asc"); + + return fetchEvents(urlBuilder.build().toString()); + } + + /** + * Get events filtered by category + */ + public List getEventsByCategory(Location location, double radiusKm, EventCategory category) { + HttpUrl.Builder urlBuilder = HttpUrl.parse(BASE_URL + EVENTS_ENDPOINT).newBuilder(); + urlBuilder.addQueryParameter("apikey", API_KEY); + urlBuilder.addQueryParameter("latlong", + String.format("%.6f,%.6f", location.getLatitude(), location.getLongitude())); + urlBuilder.addQueryParameter("radius", String.valueOf((int) radiusKm)); + urlBuilder.addQueryParameter("unit", "km"); + urlBuilder.addQueryParameter("size", "50"); + urlBuilder.addQueryParameter("sort", "distance,asc"); + + String classificationName = mapCategoryToTicketmaster(category); + if (classificationName != null) { + urlBuilder.addQueryParameter("classificationName", classificationName); + } + + return fetchEvents(urlBuilder.build().toString()); + } + + /** + * Search events by name/keyword + */ + public List searchEventsByName(String keyword, Location location, double radiusKm) { + HttpUrl.Builder urlBuilder = HttpUrl.parse(BASE_URL + EVENTS_ENDPOINT).newBuilder(); + urlBuilder.addQueryParameter("apikey", API_KEY); + urlBuilder.addQueryParameter("keyword", keyword); + urlBuilder.addQueryParameter("latlong", + String.format("%.6f,%.6f", location.getLatitude(), location.getLongitude())); + urlBuilder.addQueryParameter("radius", String.valueOf((int) radiusKm)); + urlBuilder.addQueryParameter("unit", "km"); + urlBuilder.addQueryParameter("size", "50"); + + return fetchEvents(urlBuilder.build().toString()); + } + + /** + * Get a specific event by ID + */ + public Event getEventById(String eventId) { + String url = BASE_URL + "/events/" + eventId + ".json?apikey=" + API_KEY; + + Request request = new Request.Builder() + .url(url) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + return null; + } + + String jsonResponse = response.body().string(); + JSONObject eventJson = new JSONObject(jsonResponse); + + return parseEvent(eventJson); + + } catch (IOException e) { + return null; + } + } + + private List fetchEvents(String url) { + List events = new ArrayList<>(); + + Request request = new Request.Builder() + .url(url) + .build(); + + try (Response response = client.newCall(request).execute()){ + if (!response.isSuccessful()) { + System.err.println("Failed to fetch events: " + response.code()); + return events; + } + + String jsonResponse = response.body().string(); + JSONObject root = new JSONObject(jsonResponse); + + if (!root.has("_embedded")) { + return events; + } + + JSONObject embedded = root.getJSONObject("_embedded"); + if (!embedded.has("events")) { + return events; + } + + JSONArray eventsArray = embedded.getJSONArray("events"); + + for (int i = 0; i < eventsArray.length(); i++) { + JSONObject eventJson = eventsArray.getJSONObject(i); + Event event = parseEvent(eventJson); + if (event != null) { + events.add(event); + } + } + + } catch (IOException e) { + System.err.println("Error fetching events: " + e.getMessage()); + } + + return events; + } + + private Event parseEvent(JSONObject eventJson) { + try { + String id = eventJson.getString("id"); + String name = eventJson.getString("name"); + + String description = ""; + if (eventJson.has("info")) { + description = eventJson.getString("info"); + } else if (eventJson.has("description")) { + description = eventJson.getString("description"); + } else if (eventJson.has("pleaseNote")) { + description = eventJson.getString("pleaseNote"); + } + + EventCategory category = extractCategory(eventJson); + Location location = extractLocation(eventJson); + if (location == null) { + return null; + } + + LocalDateTime startTime = extractStartTime(eventJson); + if (startTime == null) { + return null; + } + + String imageUrl = extractImageUrl(eventJson); + + return new Event(id, name, description, location.getAddress(), category, location, startTime, imageUrl); + + } catch (Exception e) { + System.err.println("Error parsing event: " + e.getMessage()); + return null; + } + } + + private String extractImageUrl(JSONObject eventJson) { + try { + if (eventJson.has("images")) { + JSONArray images = eventJson.getJSONArray("images"); + if (images.length() > 0) { + JSONObject firstImage = images.getJSONObject(3); + if (firstImage.has("url")) { + return firstImage.getString("url"); + } + } + } + } catch (Exception e) { + System.err.println("Error extracting image URL: " + e.getMessage()); + } + return ""; + } + + private EventCategory extractCategory(JSONObject eventJson) { + try { + if (eventJson.has("classifications")) { + JSONArray classifications = eventJson.getJSONArray("classifications"); + if (classifications.length() > 0) { + JSONObject classification = classifications.getJSONObject(0); + + if (classification.has("segment")) { + JSONObject segment = classification.getJSONObject("segment"); + if (segment.has("name")) { + String segmentName = segment.getString("name"); + EventCategory category = mapTicketmasterToCategory(segmentName); + if (category != EventCategory.MISCELLANEOUS) { + return category; + } + } + } + + if (classification.has("genre")) { + JSONObject genre = classification.getJSONObject("genre"); + if (genre.has("name")) { + String genreName = genre.getString("name"); + EventCategory category = mapTicketmasterToCategory(genreName); + if (category != EventCategory.MISCELLANEOUS) { + return category; + } + } + } + } + } + } catch (Exception e) { + System.err.println("Error extracting category: " + e.getMessage()); + } + + return EventCategory.MISCELLANEOUS; + } + + private Location extractLocation(JSONObject eventJson) { + try { + if (!eventJson.has("_embedded")) { + return null; + } + + JSONObject embedded = eventJson.getJSONObject("_embedded"); + if (!embedded.has("venues")) { + return null; + } + + JSONArray venues = embedded.getJSONArray("venues"); + if (venues.length() == 0) { + return null; + } + + JSONObject venue = venues.getJSONObject(0); + + StringBuilder addressBuilder = new StringBuilder(); + + if (venue.has("name")) { + addressBuilder.append(venue.getString("name")); + } + + if (venue.has("address")) { + JSONObject address = venue.getJSONObject("address"); + if (address.has("line1")) { + if (addressBuilder.length() > 0) { + addressBuilder.append(", "); + } + addressBuilder.append(address.getString("line1")); + } + } + + if (venue.has("city")) { + JSONObject city = venue.getJSONObject("city"); + if (city.has("name")) { + if (addressBuilder.length() > 0) { + addressBuilder.append(", "); + } + addressBuilder.append(city.getString("name")); + } + } + + if (venue.has("state")) { + JSONObject state = venue.getJSONObject("state"); + if (state.has("stateCode")) { + if (addressBuilder.length() > 0) { + addressBuilder.append(", "); + } + addressBuilder.append(state.getString("stateCode")); + } + } + + String address = addressBuilder.toString(); + if (address.trim().isEmpty()) { + address = "Address not available"; + } + + if (!venue.has("location")) { + return null; + } + + JSONObject locationJson = venue.getJSONObject("location"); + if (!locationJson.has("latitude") || !locationJson.has("longitude")) { + return null; + } + + double latitude = locationJson.getDouble("latitude"); + double longitude = locationJson.getDouble("longitude"); + + return new Location(address, latitude, longitude); + + } catch (Exception e) { + System.err.println("Error extracting location: " + e.getMessage()); + return null; + } + } + + private LocalDateTime extractStartTime(JSONObject eventJson) { + try { + if (!eventJson.has("dates")) { + return null; + } + + JSONObject dates = eventJson.getJSONObject("dates"); + if (!dates.has("start")) { + return null; + } + + JSONObject start = dates.getJSONObject("start"); + + if (start.has("dateTime")) { + String dateTimeStr = start.getString("dateTime"); + dateTimeStr = dateTimeStr.replace("Z", ""); + DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + return LocalDateTime.parse(dateTimeStr, formatter); + } else if (start.has("localDate")) { + String localDate = start.getString("localDate"); + String localTime = "19:00:00"; + if (start.has("localTime")) { + localTime = start.getString("localTime"); + } + return LocalDateTime.parse(localDate + "T" + localTime); + } + + return null; + + } catch (Exception e) { + System.err.println("Error extracting start time: " + e.getMessage()); + return null; + } + } + + private String mapCategoryToTicketmaster(EventCategory category) { + switch (category) { + case SPORTS: + return "Sports"; + case MUSIC: + return "Music"; + case ARTS_THEATRE: + return "Arts & Theatre"; + case FILM: + return "Film"; + case MISCELLANEOUS: + return null; + default: + return null; + } + } + + private EventCategory mapTicketmasterToCategory(String ticketmasterName) { + if (ticketmasterName == null) { + return EventCategory.MISCELLANEOUS; + } + + String normalized = ticketmasterName.toLowerCase().trim(); + + if (normalized.contains("music") || normalized.contains("concert")) { + return EventCategory.MUSIC; + } + + if (normalized.contains("sports") || normalized.contains("basketball") || + normalized.contains("football") || normalized.contains("hockey") || + normalized.contains("soccer") || normalized.contains("baseball")) { + return EventCategory.SPORTS; + } + + if (normalized.contains("arts") || normalized.contains("theatre") || + normalized.contains("theater") || normalized.contains("dance") || + normalized.contains("opera") || normalized.contains("ballet")) { + return EventCategory.ARTS_THEATRE; + } + + if (normalized.contains("film") || normalized.contains("movie") || + normalized.contains("cinema")) { + return EventCategory.FILM; + } + + return EventCategory.MISCELLANEOUS; + } +} diff --git a/src/main/java/data_access/FileSavedEventsDataAccessObject.java b/src/main/java/data_access/FileSavedEventsDataAccessObject.java new file mode 100644 index 00000000..e44db0d9 --- /dev/null +++ b/src/main/java/data_access/FileSavedEventsDataAccessObject.java @@ -0,0 +1,156 @@ +package data_access; + +import entity.Event; +import entity.EventCategory; +import entity.Location; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +public class FileSavedEventsDataAccessObject { + private static final String SAVED_EVENTS_DIR = "events_saved"; + + public FileSavedEventsDataAccessObject() { + // This is to create the file directory with the name "events_saved", this wasn't done with a csv but a json + File directory = new File(SAVED_EVENTS_DIR); + // Make sure the directory doesn't exist before the directory is created + if (directory.exists()) { + return; + } else { + directory.mkdir(); + } + } + + public void saveEvent(String username, Event event) { + List savedEvents = getSavedEvents(username); + for (int i = 0; i < savedEvents.size(); i++) { + if (savedEvents.get(i).getId().equals(event.getId())) { + return; + } + } + savedEvents.add(event); + writeEventsToFile(username, savedEvents); + } + + public void removeEvent(String username, Event event) { + List savedEvents = getSavedEvents(username); + for (int i = 0; i < savedEvents.size(); i++) { + if (savedEvents.get(i).getId().equals(event.getId())) { + savedEvents.remove(i); + } + } + writeEventsToFile(username, savedEvents); + } + + public List getSavedEvents(String username) { + File file = new File(SAVED_EVENTS_DIR, username + "_events.json"); + if (!file.exists()) { + return new ArrayList<>(); + } + try { + String content = new String(Files.readAllBytes(Paths.get(file.getPath()))); + JSONArray jsonArray = new JSONArray(content); + + List events = new ArrayList<>(); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject eventJson = jsonArray.getJSONObject(i); + Event event = parseEventFromJson(eventJson); + if (event != null) { + events.add(event); + } + } + return events; + + } catch (IOException e) { + System.err.println("Error reading saved events for user " + username + ": " + e.getMessage()); + return new ArrayList<>(); + } + } + + public boolean isSavedEvent(String username, String id) { + List savedEvents = getSavedEvents(username); + for (int i = 0; i < savedEvents.size(); i++) { + if (savedEvents.get(i).getId().equals(id)) { + return true; + } + } + return false; + } + + /** + * Write events to file for a specific user. + */ + private void writeEventsToFile(String username, List events) { + File file = new File(SAVED_EVENTS_DIR, username + "_events.json"); + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + JSONArray jsonArray = new JSONArray(); + + for (Event event : events) { + JSONObject eventJson = convertEventToJson(event); + jsonArray.put(eventJson); + } + + writer.write(jsonArray.toString(4)); + + } catch (IOException e) { + System.err.println("Error writing saved events for user " + username + ": " + e.getMessage()); + } + } + /** + * Convert an Event object to JSON. + */ + private JSONObject convertEventToJson(Event event) { + JSONObject json = new JSONObject(); + json.put("id", event.getId()); + json.put("name", event.getName()); + json.put("description", event.getDescription()); + json.put("category", event.getCategory().name()); + json.put("imageUrl", event.getImageUrl()); + json.put("startTime", event.getStartTime()); + + // Location + JSONObject locationJson = new JSONObject(); + locationJson.put("address", event.getLocation().getAddress()); + locationJson.put("latitude", event.getLocation().getLatitude()); + locationJson.put("longitude", event.getLocation().getLongitude()); + json.put("location", locationJson); + + return json; + } + + /** + * Parse an Event object from JSON. + */ + private Event parseEventFromJson(JSONObject json) { + try { + String id = json.getString("id"); + String name = json.getString("name"); + String description = json.getString("description"); + EventCategory category = EventCategory.valueOf(json.getString("category")); + String imageUrl = json.getString("imageUrl"); + LocalDateTime startTime = LocalDateTime.parse(json.getString("startTime")); + + JSONObject locationJson = json.getJSONObject("location"); + Location location = new Location( + locationJson.getString("address"), + locationJson.getDouble("latitude"), + locationJson.getDouble("longitude") + ); + + return new Event(id, name, description, location.getAddress(), category, location, startTime, imageUrl); + + } catch (Exception e) { + System.err.println("Error parsing event from JSON: " + e.getMessage()); + return null; + } + } + +} 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/data_access/SearchEventDataAccessObject.java b/src/main/java/data_access/SearchEventDataAccessObject.java new file mode 100644 index 00000000..74b6db25 --- /dev/null +++ b/src/main/java/data_access/SearchEventDataAccessObject.java @@ -0,0 +1,20 @@ +package data_access; + +import entity.Event; +import entity.Location; +import use_case.search_event_by_name.SearchEventByNameDataAccessInterface; +import java.util.List; + +public class SearchEventDataAccessObject implements SearchEventByNameDataAccessInterface { + + private final EventDataAccessObject eventDAO; + + public SearchEventDataAccessObject() { + this.eventDAO = new EventDataAccessObject(); + } + + @Override + public List searchEventsByName(String keyword, Location location, double radiusKm) { + return eventDAO.searchEventsByName(keyword, location, radiusKm); + } +} \ No newline at end of file diff --git a/src/main/java/data_access/TicketmasterEventRepositoryAdapter.java b/src/main/java/data_access/TicketmasterEventRepositoryAdapter.java new file mode 100644 index 00000000..2e15133f --- /dev/null +++ b/src/main/java/data_access/TicketmasterEventRepositoryAdapter.java @@ -0,0 +1,41 @@ +package data_access; + +import entity.Event; +import entity.EventRepository; +import entity.Location; + +import java.util.List; +import java.util.Optional; + +public class TicketmasterEventRepositoryAdapter implements EventRepository { + private final EventDataAccessObject dao; + private final Location defaultCenter; + private final double defaultRadiusKm; + + public TicketmasterEventRepositoryAdapter(EventDataAccessObject dao, + Location defaultCenter, + double defaultRadiusKm) { + this.dao = dao; + this.defaultCenter = defaultCenter; + this.defaultRadiusKm = defaultRadiusKm; + } + + public List findEvents(Location center, double radiusKm) { + return dao.getEventsByLocation(center, radiusKm); + } + + @Override + public List findAllEvents() { + return dao.getEventsByLocation(defaultCenter, defaultRadiusKm); + } + + @Override + public Optional findById(String id) { + return Optional.ofNullable(dao.getEventById(id)); + } + + @Override + public List findByName(String query) { + return dao.searchEventsByName(query, defaultCenter, defaultRadiusKm); + } +} \ 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..88c868e1 --- /dev/null +++ b/src/main/java/entity/Event.java @@ -0,0 +1,178 @@ +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) { + + + 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; + } + + // 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; + } + + @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/calendarFlow/CalendarFlowController.java b/src/main/java/interface_adapter/calendarFlow/CalendarFlowController.java new file mode 100644 index 00000000..a9121fb4 --- /dev/null +++ b/src/main/java/interface_adapter/calendarFlow/CalendarFlowController.java @@ -0,0 +1,31 @@ +package interface_adapter.calendarFlow; + +import entity.Location; +import use_case.calendarFlow.CalendarFlowInputBoundary; +import use_case.calendarFlow.CalendarFlowInputData; +import java.time.LocalDate; + +public class CalendarFlowController { + private final CalendarFlowInputBoundary CalendarFlowInteractor; + + public CalendarFlowController(CalendarFlowInputBoundary interactor) { + this.CalendarFlowInteractor = interactor; + } + + /** + * Execute the search event by name use case. + * @param selectedDate the event date to search for + * @param userLocation the user's location + * @param radiusKm the search radius in kilometers + */ + public void execute(LocalDate selectedDate, Location userLocation, double radiusKm){ + final CalendarFlowInputData inputData = new CalendarFlowInputData(selectedDate, userLocation, radiusKm); + + CalendarFlowInteractor.execute(inputData); + } + +// //to implement later +// public void switchToDashboardView() { +// CalendarFlowInteractor.switchToDashboardView(); +// } +} diff --git a/src/main/java/interface_adapter/calendarFlow/CalendarFlowPresenter.java b/src/main/java/interface_adapter/calendarFlow/CalendarFlowPresenter.java new file mode 100644 index 00000000..0bdfc643 --- /dev/null +++ b/src/main/java/interface_adapter/calendarFlow/CalendarFlowPresenter.java @@ -0,0 +1,42 @@ +package interface_adapter.calendarFlow; + +import interface_adapter.ViewManagerModel; +import use_case.calendarFlow.CalendarFlowOutputBoundary; +import use_case.calendarFlow.CalendarFlowOutputData; + +import java.util.ArrayList; + +public class CalendarFlowPresenter implements CalendarFlowOutputBoundary { + private final CalendarFlowViewModel viewModel; + private final ViewManagerModel viewManagerModel; + + public CalendarFlowPresenter(CalendarFlowViewModel viewModel, ViewManagerModel viewManagerModel) { + this.viewModel = viewModel; + this.viewManagerModel = viewManagerModel; + } + + @Override + public void prepareSuccessView(CalendarFlowOutputData outputData){ + CalendarFlowState state = new CalendarFlowState(); + state.setEventList(outputData.getEvents()); + state.setDate(outputData.getSelectedDate()); + state.setErrorMessage(null); + + viewModel.setState(state); + viewModel.firePropertyChange(); + + viewManagerModel.setState(viewModel.getViewName()); + viewManagerModel.firePropertyChange(); + } + + @Override + public void prepareFailView(String errorMessage){ + CalendarFlowState state = viewModel.getState(); + state.setEventList(new ArrayList<>()); + state.setErrorMessage(errorMessage); + + viewManagerModel.setState(viewModel.getViewName()); + viewManagerModel.firePropertyChange(); + } + +} diff --git a/src/main/java/interface_adapter/calendarFlow/CalendarFlowState.java b/src/main/java/interface_adapter/calendarFlow/CalendarFlowState.java new file mode 100644 index 00000000..c103bfce --- /dev/null +++ b/src/main/java/interface_adapter/calendarFlow/CalendarFlowState.java @@ -0,0 +1,35 @@ +package interface_adapter.calendarFlow; + +import entity.Event; +import java.util.List; +import java.time.LocalDate; + +public class CalendarFlowState { + private List eventList = null; + private LocalDate date = null; + private String errorMessage = null; + + public CalendarFlowState() { + } + + public void setDate(LocalDate date) { + this.date = date; + } + public void setEventList(List eventList) { + this.eventList = eventList; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public List getEventList() { + return eventList; + } + public LocalDate getDate() { + return date; + } + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/src/main/java/interface_adapter/calendarFlow/CalendarFlowViewModel.java b/src/main/java/interface_adapter/calendarFlow/CalendarFlowViewModel.java new file mode 100644 index 00000000..743dc0ad --- /dev/null +++ b/src/main/java/interface_adapter/calendarFlow/CalendarFlowViewModel.java @@ -0,0 +1,13 @@ +package interface_adapter.calendarFlow; + +import entity.Event; +import interface_adapter.ViewModel; +import java.time.LocalDate; +import java.util.List; + +public class CalendarFlowViewModel extends ViewModel { + public CalendarFlowViewModel() { + super("event list by date"); + setState(new CalendarFlowState()); + } +} diff --git a/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsController.java b/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsController.java new file mode 100644 index 00000000..8baf28bd --- /dev/null +++ b/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsController.java @@ -0,0 +1,44 @@ +package interface_adapter.display_local_events; + +import entity.EventCategory; +import entity.Location; +import use_case.display_local_events.DisplayLocalEventsInputBoundary; +import use_case.display_local_events.DisplayLocalEventsInputData; + +/** + * Controller for the Display Local Events use case. + * This class receives raw input from the UI layer (such as the user's location, + * search radius, and selected category), constructs an InputData object, and + * delegates execution to the use case interactor via the InputBoundary. + */ +public class DisplayLocalEventsController { + + private final DisplayLocalEventsInputBoundary interactor; + + public DisplayLocalEventsController(DisplayLocalEventsInputBoundary interactor) { + this.interactor = interactor; + } + + /** + * Entry point for the UI to trigger the Display Local Events use case. + * + * @param userLocation the user's current location (already constructed). + * @param radiusKm the search radius in kilometers. + * @param categoryOrNull the selected category as a String. + * - If null, empty, or "ALL" (case-insensitive), no category filter is applied. + * - Otherwise, it will be converted to an EventCategory via EventCategory.fromString(). + */ + public void display(Location userLocation, double radiusKm, String categoryOrNull) { + EventCategory category = null; + + if (categoryOrNull != null && !categoryOrNull.isBlank() + && !categoryOrNull.equalsIgnoreCase("ALL")) { + category = EventCategory.fromString(categoryOrNull); + } + + DisplayLocalEventsInputData inputData = + new DisplayLocalEventsInputData(userLocation, radiusKm, category); + + interactor.execute(inputData); + } +} diff --git a/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsPresenter.java b/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsPresenter.java new file mode 100644 index 00000000..c849ff03 --- /dev/null +++ b/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsPresenter.java @@ -0,0 +1,74 @@ +package interface_adapter.display_local_events; + +import entity.Event; +import interface_adapter.ViewManagerModel; +import use_case.display_local_events.DisplayLocalEventsOutputBoundary; +import use_case.display_local_events.DisplayLocalEventsOutputData; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + + +public class DisplayLocalEventsPresenter implements DisplayLocalEventsOutputBoundary { + private final DisplayLocalEventsViewModel viewModel; + private final ViewManagerModel viewManagerModel; + private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + public DisplayLocalEventsPresenter(DisplayLocalEventsViewModel viewModel, + ViewManagerModel viewManagerModel) { + this.viewModel = viewModel; + this.viewManagerModel = viewManagerModel; + } + + + + @Override + public void presentSuccess(DisplayLocalEventsOutputData outputData) { + List cards = new ArrayList<>(); + Map distances = outputData.getEventDistances(); + + // Transform Event entities into EventCard view objects + for (Event event : outputData.getEvents()) { + String distanceText = ""; + if (distances != null && distances.containsKey(event.getId())) { + double distance = distances.get(event.getId()); + distanceText = String.format("%.1f km", distance); + } + + DisplayLocalEventsViewModel.EventCard card = new DisplayLocalEventsViewModel.EventCard( + event.getId(), + event.getName(), + event.getStartTime().format(dateTimeFormatter), + event.getLocation().getAddress(), + event.getCategory().getDisplayName(), + distanceText, + event.getImageUrl() + ); + cards.add(card); + } + + // Update the ViewModel state + viewModel.setEventCards(cards); + viewModel.setMessage(outputData.getMessage()); + viewModel.setError(""); + + // Store the actual Event objects for click handling + viewModel.setEvents(outputData.getEvents()); + + // Trigger view update via ViewManagerModel + viewManagerModel.setState(viewModel.getViewName()); + viewManagerModel.firePropertyChange(); + } + + @Override + public void presentError(String errorMessage) { + viewModel.setEventCards(List.of()); + viewModel.setMessage(""); + viewModel.setError(errorMessage != null ? errorMessage : "Unknown error"); + viewModel.setEvents(List.of()); + + viewManagerModel.setState(viewModel.getViewName()); + viewManagerModel.firePropertyChange(); + } +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsState.java b/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsState.java new file mode 100644 index 00000000..5f4ddb7b --- /dev/null +++ b/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsState.java @@ -0,0 +1,75 @@ +package interface_adapter.display_local_events; + +import java.util.ArrayList; +import java.util.List; + +/** + * 简化的 State 类 - 只存储必要的数据 + */ +public class DisplayLocalEventsState { + private List eventCards = new ArrayList<>(); + private String message = ""; + private String error = ""; + + // 搜索参数 + private String lastSearchLocation = ""; + private String lastSearchCategory = ""; + private double lastSearchRadius = 0.0; + + // Getters and Setters + public List getEventCards() { + return eventCards; + } + + public void setEventCards(List cards) { + this.eventCards = (cards != null ? cards : new ArrayList<>()); + } + + public String getMessage() { + return message; + } + + public void setMessage(String msg) { + this.message = (msg != null ? msg : ""); + } + + public String getError() { + return error; + } + + public void setError(String err) { + this.error = (err != null ? err : ""); + } + + public boolean hasEvents() { + return !eventCards.isEmpty(); + } + + public boolean hasError() { + return !error.isEmpty(); + } + + public String getLastSearchLocation() { + return lastSearchLocation; + } + + public void setLastSearchLocation(String location) { + this.lastSearchLocation = (location != null ? location : ""); + } + + public String getLastSearchCategory() { + return lastSearchCategory; + } + + public void setLastSearchCategory(String category) { + this.lastSearchCategory = (category != null ? category : ""); + } + + public double getLastSearchRadius() { + return lastSearchRadius; + } + + public void setLastSearchRadius(double radius) { + this.lastSearchRadius = radius; + } +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsViewModel.java b/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsViewModel.java new file mode 100644 index 00000000..2270acca --- /dev/null +++ b/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsViewModel.java @@ -0,0 +1,140 @@ +package interface_adapter.display_local_events; + +import entity.Event; +import interface_adapter.ViewModel; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +/** + * 简化的 ViewModel - 基本的 PropertyChange 支持 + */ + +public class DisplayLocalEventsViewModel extends ViewModel { + + public static final String VIEW_NAME = "display local events"; + private LocalDate selectedDateFromCalendar; + + // Store actual Event objects for navigation purposes + private List events = new ArrayList<>(); + + public DisplayLocalEventsViewModel() { + super(VIEW_NAME); + this.setState(new DisplayLocalEventsState()); + } + + /** + * EventCard - a view-specific data structure. + * + * CLEAN ARCHITECTURE NOTE: + * This is a Data Transfer Object (DTO) for the View layer. + * It contains only the data needed for display, formatted appropriately. + * This separates the View's data needs from the domain Entity structure. + */ + public static class EventCard { + private final String id; + private final String name; + private final String dateTime; + private final String address; + private final String category; + private final String distanceText; + private final String imageUrl; + + public EventCard(String id, String name, String dateTime, String address, + String category, String distanceText, String imageUrl) { + this.id = id; + this.name = name; + this.dateTime = dateTime; + this.address = address; + this.category = category; + this.distanceText = distanceText; + this.imageUrl = imageUrl; + } + + public String getId() { return id; } + public String getName() { return name; } + public String getDateTime() { return dateTime; } + public String getAddress() { return address; } + public String getCategory() { return category; } + public String getDistanceText() { return distanceText; } + public String getImageUrl() { return imageUrl; } + } + + // Getter/setter for Event objects (for navigation) + public List getEvents() { + return events; + } + + public void setEvents(List events) { + this.events = events != null ? new ArrayList<>(events) : new ArrayList<>(); + } + + /** + * Find an Event by its ID. + * Used when user clicks on an EventCard to navigate to details. + */ + public Event getEventById(String id) { + for (Event event : events) { + if (event.getId().equals(id)) { + return event; + } + } + return null; + } + + // Basic getter/setter methods + public List getEventCards() { + return this.getState().getEventCards(); + } + + public void setEventCards(List eventCards) { + this.getState().setEventCards(eventCards); + this.firePropertyChange(); + } + + public String getMessage() { + return this.getState().getMessage(); + } + + public void setMessage(String message) { + this.getState().setMessage(message); + this.firePropertyChange(); + } + + public String getError() { + return this.getState().getError(); + } + + public void setError(String error) { + this.getState().setError(error); + this.firePropertyChange(); + } + + public boolean hasEvents() { + return this.getState().hasEvents(); + } + + public boolean hasError() { + return this.getState().hasError(); + } + + // Search parameter methods + public void updateSearchParams(String location, String category, double radius) { + this.getState().setLastSearchLocation(location); + this.getState().setLastSearchCategory(category); + this.getState().setLastSearchRadius(radius); + this.firePropertyChange(); + } + + public String getLastSearchLocation() { + return this.getState().getLastSearchLocation(); + } + + public String getLastSearchCategory() { + return this.getState().getLastSearchCategory(); + } + + public double getLastSearchRadius() { + return this.getState().getLastSearchRadius(); + } +} \ No newline at end of file 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..0d6fd7a0 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 @@ -34,8 +38,8 @@ public void prepareSuccessView(LoginOutputData response) { loginViewModel.setState(new LoginState()); // switch to the logged in view - this.viewManagerModel.setState(loggedInViewModel.getViewName()); - this.viewManagerModel.firePropertyChange(); + viewManagerModel.setState("display local events"); + viewManagerModel.firePropertyChange(); } @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/login/LoginViewModel.java b/src/main/java/interface_adapter/login/LoginViewModel.java index 41bea7a2..a68f7771 100644 --- a/src/main/java/interface_adapter/login/LoginViewModel.java +++ b/src/main/java/interface_adapter/login/LoginViewModel.java @@ -6,6 +6,11 @@ * The View Model for the Login View. */ public class LoginViewModel extends ViewModel { + public static final String TITLE_LABEL = "Log In"; + public static final String TITLE = "Event Gate"; + public static final String CAPTION = "Log in with your credentials below"; + + public LoginViewModel() { super("log in"); 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/save_event/SaveEventController.java b/src/main/java/interface_adapter/save_event/SaveEventController.java new file mode 100644 index 00000000..21dfb896 --- /dev/null +++ b/src/main/java/interface_adapter/save_event/SaveEventController.java @@ -0,0 +1,22 @@ +package interface_adapter.save_event; + +import entity.Event; +import use_case.save_event.SaveEventInputBoundary; +import use_case.save_event.SaveEventInputData; + +public class SaveEventController { + private final SaveEventInputBoundary saveEventInteractor; + + public SaveEventController(SaveEventInputBoundary saveEventUseCase) { + this.saveEventInteractor = saveEventUseCase; + } + + public void saveEvent(Event event) { + SaveEventInputData inputData = new SaveEventInputData(event); + saveEventInteractor.execute(inputData); + } + + public void switchToDashboardView() { + saveEventInteractor.switchToDashboardView(); + } +} diff --git a/src/main/java/interface_adapter/save_event/SaveEventPresenter.java b/src/main/java/interface_adapter/save_event/SaveEventPresenter.java new file mode 100644 index 00000000..3dd405eb --- /dev/null +++ b/src/main/java/interface_adapter/save_event/SaveEventPresenter.java @@ -0,0 +1,40 @@ +package interface_adapter.save_event; + +import interface_adapter.ViewManagerModel; +import use_case.save_event.SaveEventOutputBoundary; +import use_case.save_event.SaveEventOutputData; + +public class SaveEventPresenter implements SaveEventOutputBoundary { + + private final SaveEventViewModel saveEventViewModel; + private final ViewManagerModel viewManagerModel; + + public SaveEventPresenter(SaveEventViewModel saveEventViewModel, ViewManagerModel viewManagerModel) { + this.saveEventViewModel = saveEventViewModel; + this.viewManagerModel = viewManagerModel; + } + + @Override + public void prepareSuccessView(SaveEventOutputData outputData) { + SaveEventState currentState = saveEventViewModel.getState(); + currentState.addSavedEvent(outputData.getEvent()); + saveEventViewModel.setState(currentState); + saveEventViewModel.firePropertyChange(); + } + + @Override + public void prepareFailureView(String errorMessage) { + SaveEventState currentState = saveEventViewModel.getState(); + currentState.addSavedEvent(null); + saveEventViewModel.setState(currentState); + saveEventViewModel.firePropertyChange(); + } + + @Override + public void switchToDashboardView() { + viewManagerModel.setState("display local events"); // or whatever the dashboard view name is + viewManagerModel.firePropertyChange(); + } + + +} diff --git a/src/main/java/interface_adapter/save_event/SaveEventState.java b/src/main/java/interface_adapter/save_event/SaveEventState.java new file mode 100644 index 00000000..649739cc --- /dev/null +++ b/src/main/java/interface_adapter/save_event/SaveEventState.java @@ -0,0 +1,57 @@ +package interface_adapter.save_event; + +import entity.Event; +import java.util.ArrayList; +import java.util.List; + +public class SaveEventState { + private List savedEvents = new ArrayList<>(); + private String successMessage = ""; + private String errorMessage = ""; + private Event lastSavedEvent = null; + + public SaveEventState() { + } + + public List getSavedEvents() { + return new ArrayList<>(savedEvents); + } + + public void setSavedEvents(List savedEvents) { + this.savedEvents = new ArrayList<>(savedEvents); + } + + public void addSavedEvent(Event event) { + if (!savedEvents.contains(event)) { + savedEvents.add(event); + } + } + + public void removeSavedEvent(Event event) { + savedEvents.remove(event); + } + + public String getSuccessMessage() { + return successMessage; + } + + public void setSuccessMessage(String successMessage) { + this.successMessage = successMessage; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public Event getLastSavedEvent() { + return lastSavedEvent; + } + + public void setLastSavedEvent(Event lastSavedEvent) { + this.lastSavedEvent = lastSavedEvent; + } +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/save_event/SaveEventViewModel.java b/src/main/java/interface_adapter/save_event/SaveEventViewModel.java new file mode 100644 index 00000000..bcdcd28b --- /dev/null +++ b/src/main/java/interface_adapter/save_event/SaveEventViewModel.java @@ -0,0 +1,11 @@ +package interface_adapter.save_event; + +import interface_adapter.ViewModel; +import interface_adapter.signup.SignupState; + +public class SaveEventViewModel extends ViewModel { + public SaveEventViewModel() { + super("save event"); + setState(new SaveEventState()); + } +} diff --git a/src/main/java/interface_adapter/search/SearchController.java b/src/main/java/interface_adapter/search/SearchController.java new file mode 100644 index 00000000..64f0121d --- /dev/null +++ b/src/main/java/interface_adapter/search/SearchController.java @@ -0,0 +1,27 @@ +package interface_adapter.search; + +import entity.Location; +import use_case.search.SearchInputBoundary; +import use_case.search.SearchInputData; + +public class SearchController { + + private final SearchInputBoundary searchUseCaseInteractor; + + public SearchController(SearchInputBoundary searchUseCaseInteractor) { + this.searchUseCaseInteractor = searchUseCaseInteractor; + } + + public void execute(String query, String searchType) { + Location defaultLocation = new Location("Toronto, ON", 43.6532, -79.3832); + execute(query, searchType, defaultLocation); + } + + public void execute(String query, String searchType, Location location) { + SearchInputData inputData = new SearchInputData(query, searchType, location); + searchUseCaseInteractor.execute(inputData); + } + + + +} diff --git a/src/main/java/interface_adapter/search/SearchPresenter.java b/src/main/java/interface_adapter/search/SearchPresenter.java new file mode 100644 index 00000000..bba8bd70 --- /dev/null +++ b/src/main/java/interface_adapter/search/SearchPresenter.java @@ -0,0 +1,37 @@ +package interface_adapter.search; + +import interface_adapter.ViewManagerModel; +import interface_adapter.search_event_by_name.SearchEventByNameState; +import interface_adapter.search_event_by_name.SearchEventByNameViewModel; +import use_case.search.SearchOutputBoundary; +import use_case.search.SearchOutputData; + +public class SearchPresenter implements SearchOutputBoundary { + private final SearchEventByNameViewModel searchEventByNameViewModel; + private final ViewManagerModel viewManagerModel; + + public SearchPresenter(SearchEventByNameViewModel searchEventByNameViewModel, ViewManagerModel viewManagerModel) { + this.searchEventByNameViewModel = searchEventByNameViewModel; + this.viewManagerModel = viewManagerModel; + } + + @Override + public void prepareSuccessView(SearchOutputData outputData) { + SearchEventByNameState state = searchEventByNameViewModel.getState(); + state.setEvent(outputData.getEvent()); // ← Assumes SearchOutputData has getEvent() + searchEventByNameViewModel.setState(state); + searchEventByNameViewModel.firePropertyChange(); + + viewManagerModel.setState(searchEventByNameViewModel.getViewName()); + viewManagerModel.firePropertyChange(); + } + + @Override + public void prepareFailureView(String failMessage) { + + } + + @Override + public void switchToEventSearchView(SearchOutputData outputData) { + } +} diff --git a/src/main/java/interface_adapter/search/SearchState.java b/src/main/java/interface_adapter/search/SearchState.java new file mode 100644 index 00000000..70a54441 --- /dev/null +++ b/src/main/java/interface_adapter/search/SearchState.java @@ -0,0 +1,4 @@ +package interface_adapter.search; + +public class SearchState { +} diff --git a/src/main/java/interface_adapter/search/SearchViewModel.java b/src/main/java/interface_adapter/search/SearchViewModel.java new file mode 100644 index 00000000..1a716b6c --- /dev/null +++ b/src/main/java/interface_adapter/search/SearchViewModel.java @@ -0,0 +1,4 @@ +package interface_adapter.search; + +public class SearchViewModel { +} diff --git a/src/main/java/interface_adapter/search_event_by_name/SearchEventByNameController.java b/src/main/java/interface_adapter/search_event_by_name/SearchEventByNameController.java new file mode 100644 index 00000000..62a4ed08 --- /dev/null +++ b/src/main/java/interface_adapter/search_event_by_name/SearchEventByNameController.java @@ -0,0 +1,34 @@ +package interface_adapter.search_event_by_name; + +import use_case.search_event_by_name.SearchEventByNameInputBoundary; +import use_case.signup.SignupInputBoundary; +import entity.Location; +import use_case.search_event_by_name.SearchEventByNameInputBoundary; +import use_case.search_event_by_name.SearchEventByNameInputData; + +public class SearchEventByNameController { + + private final SearchEventByNameInputBoundary searchEventByNameInteractor; + + public SearchEventByNameController(SearchEventByNameInputBoundary searchEventByNameInteractor) { + this.searchEventByNameInteractor = searchEventByNameInteractor; + } + + /** + * Execute the search event by name use case. + * @param keyword the event name to search for + * @param userLocation the user's location + * @param radiusKm the search radius in kilometers + */ + public void execute(String keyword, Location userLocation, double radiusKm) { + SearchEventByNameInputData inputData = new SearchEventByNameInputData(keyword, userLocation, radiusKm); + searchEventByNameInteractor.execute(inputData); + } + + /** + * Executes the "switch to DashboardView" Use Case. + */ + public void switchToDashboardView() { + searchEventByNameInteractor.switchToDashboardView(); + } +} diff --git a/src/main/java/interface_adapter/search_event_by_name/SearchEventByNamePresenter.java b/src/main/java/interface_adapter/search_event_by_name/SearchEventByNamePresenter.java new file mode 100644 index 00000000..57ec4823 --- /dev/null +++ b/src/main/java/interface_adapter/search_event_by_name/SearchEventByNamePresenter.java @@ -0,0 +1,44 @@ +package interface_adapter.search_event_by_name; + +import interface_adapter.ViewManagerModel; +import use_case.search_event_by_name.SearchEventByNameOutputBoundary; +import use_case.search_event_by_name.SearchEventByNameOutputData; + +public class SearchEventByNamePresenter implements SearchEventByNameOutputBoundary { + + private final SearchEventByNameViewModel searchEventByNameViewModel; + private final ViewManagerModel viewManagerModel; + + public SearchEventByNamePresenter(SearchEventByNameViewModel searchEventByNameViewModel, + ViewManagerModel viewManagerModel) { + this.searchEventByNameViewModel = searchEventByNameViewModel; + this.viewManagerModel = viewManagerModel; + } + + @Override + public void prepareSuccessView(SearchEventByNameOutputData outputData) { + SearchEventByNameState state = searchEventByNameViewModel.getState(); + state.setEvent(outputData.getEvent()); + state.setErrorMessage(null); + searchEventByNameViewModel.setState(state); + searchEventByNameViewModel.firePropertyChange(); + + viewManagerModel.setState(searchEventByNameViewModel.getViewName()); + viewManagerModel.firePropertyChange(); + } + + @Override + public void prepareFailView(String errorMessage) { + SearchEventByNameState state = searchEventByNameViewModel.getState(); + state.setEvent(null); + state.setErrorMessage(errorMessage); + searchEventByNameViewModel.setState(state); + searchEventByNameViewModel.firePropertyChange(); + } + + @Override + public void switchToDashboardView() { + viewManagerModel.setState("display local events"); + viewManagerModel.firePropertyChange(); + } +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/search_event_by_name/SearchEventByNameState.java b/src/main/java/interface_adapter/search_event_by_name/SearchEventByNameState.java new file mode 100644 index 00000000..efab7745 --- /dev/null +++ b/src/main/java/interface_adapter/search_event_by_name/SearchEventByNameState.java @@ -0,0 +1,25 @@ +package interface_adapter.search_event_by_name; + +import entity.Event; + +public class SearchEventByNameState { + private Event event = null; + private String errorMessage; + + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/src/main/java/interface_adapter/search_event_by_name/SearchEventByNameViewModel.java b/src/main/java/interface_adapter/search_event_by_name/SearchEventByNameViewModel.java new file mode 100644 index 00000000..e29106db --- /dev/null +++ b/src/main/java/interface_adapter/search_event_by_name/SearchEventByNameViewModel.java @@ -0,0 +1,17 @@ +package interface_adapter.search_event_by_name; + +import entity.Event; +import interface_adapter.ViewModel; + +public class SearchEventByNameViewModel extends ViewModel { + + public SearchEventByNameViewModel() { + super("event search"); + setState(new SearchEventByNameState()); + } + + public Event getEvent() { + return getState().getEvent(); + } + +} diff --git a/src/main/java/interface_adapter/signup/SignupViewModel.java b/src/main/java/interface_adapter/signup/SignupViewModel.java index 01f0086b..9916f39c 100644 --- a/src/main/java/interface_adapter/signup/SignupViewModel.java +++ b/src/main/java/interface_adapter/signup/SignupViewModel.java @@ -7,10 +7,13 @@ */ 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 Gate"; + public static final String CAPTION = "Join us and never miss an event!"; + public static final String TITLE_CAPTION = "Please enter your details to sign in"; 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/calendarFlow/CalendarFlowDataAccessInterface.java b/src/main/java/use_case/calendarFlow/CalendarFlowDataAccessInterface.java new file mode 100644 index 00000000..7f44e1c0 --- /dev/null +++ b/src/main/java/use_case/calendarFlow/CalendarFlowDataAccessInterface.java @@ -0,0 +1,24 @@ +package use_case.calendarFlow; + +import entity.Event; +import entity.Location; + +import java.util.List; +import java.time.LocalDate; + +/** + * Data Access Interface for the Calendar Flow Use Case. + * Defines methods for retrieving event data. + */ + +public interface CalendarFlowDataAccessInterface { + + /** + * Search for events by specific date + * @param date the selected specific date + * @param location the user's location + * @param radiusKm the search radius in kilometers + * @return list of events matching the search + */ + List getEventsByDate(LocalDate date, Location location, double radiusKm); +} \ No newline at end of file diff --git a/src/main/java/use_case/calendarFlow/CalendarFlowInputBoundary.java b/src/main/java/use_case/calendarFlow/CalendarFlowInputBoundary.java new file mode 100644 index 00000000..75da1135 --- /dev/null +++ b/src/main/java/use_case/calendarFlow/CalendarFlowInputBoundary.java @@ -0,0 +1,16 @@ +package use_case.calendarFlow; + +/** + * InputBoundary for the Calendar Flow Use Case + * Defines the interface for executing the calendar flow use case + */ +public interface CalendarFlowInputBoundary { + + void execute(CalendarFlowInputData inputData); + +// //to implement later +// /** +// * Switch to the Dashboard View. +// */ +// void switchToDashboardView(); +} diff --git a/src/main/java/use_case/calendarFlow/CalendarFlowInputData.java b/src/main/java/use_case/calendarFlow/CalendarFlowInputData.java new file mode 100644 index 00000000..eb4e1107 --- /dev/null +++ b/src/main/java/use_case/calendarFlow/CalendarFlowInputData.java @@ -0,0 +1,32 @@ +package use_case.calendarFlow; + +import entity.Location; + +import java.time.LocalDate; +/** + * Input Data for the Calendar Flow Use Case. + */ +public class CalendarFlowInputData { + private final LocalDate selectedDate; + private final Location userLocation; + private final double radiusKm; + + public CalendarFlowInputData(LocalDate selectedDate, Location userLocation, double radiusKm) { + this.selectedDate = selectedDate; + this.userLocation = userLocation; + this.radiusKm = radiusKm; + } + + public LocalDate getSelectedDate() { + return selectedDate; + } + + public Location getUserLocation() { + return userLocation; + } + + public double getRadiusKm() { + return radiusKm; + } + +} diff --git a/src/main/java/use_case/calendarFlow/CalendarFlowInteractor.java b/src/main/java/use_case/calendarFlow/CalendarFlowInteractor.java new file mode 100644 index 00000000..ebc69f6f --- /dev/null +++ b/src/main/java/use_case/calendarFlow/CalendarFlowInteractor.java @@ -0,0 +1,54 @@ +package use_case.calendarFlow; + +import entity.Event; + +import java.time.LocalDate; +import java.util.List; + +public class CalendarFlowInteractor implements CalendarFlowInputBoundary { + private final CalendarFlowDataAccessInterface calendarFlowDataAccess; + private final CalendarFlowOutputBoundary presenter; + + public CalendarFlowInteractor(CalendarFlowDataAccessInterface calendarFlowDataAccess, + CalendarFlowOutputBoundary calendarFlowOutputBoundary) { + this.calendarFlowDataAccess = calendarFlowDataAccess; + this.presenter = calendarFlowOutputBoundary; + } + + @Override + public void execute(CalendarFlowInputData inputData) { + if (inputData.getSelectedDate() == null) { + presenter.prepareFailView("Date cannot be null"); + return; + } + + if (inputData.getUserLocation() == null) { + presenter.prepareFailView("User location is required"); + return; + } + + try { + List events = calendarFlowDataAccess.getEventsByDate( + inputData.getSelectedDate(), + inputData.getUserLocation(), + inputData.getRadiusKm() + ); + + LocalDate date = inputData.getSelectedDate(); + if (events == null || events.isEmpty()) { + presenter.prepareFailView("No events found for " + date); + } else { + CalendarFlowOutputData outputData = new CalendarFlowOutputData(date, events); + presenter.prepareSuccessView(outputData); + } + } catch (Exception e) { + presenter.prepareFailView("Error fetching events: " + e.getMessage()); + } + } + +// //to implement later +// @Override +// public void switchToDashboardView() { +// presenter.switchToDashboardView(); +// } +} diff --git a/src/main/java/use_case/calendarFlow/CalendarFlowOutputBoundary.java b/src/main/java/use_case/calendarFlow/CalendarFlowOutputBoundary.java new file mode 100644 index 00000000..6a4cd741 --- /dev/null +++ b/src/main/java/use_case/calendarFlow/CalendarFlowOutputBoundary.java @@ -0,0 +1,25 @@ +package use_case.calendarFlow; + +/** + * OutputBoundary for the Calendar Flow Use Case + * Defines the interface for presenting the results + */ +public interface CalendarFlowOutputBoundary { + /** + * Prepares the success view when events are found for the selected date. + * @param outputData the output data containing the date and events + */ + void prepareSuccessView(CalendarFlowOutputData outputData); + + /** + * Prepares the view when no events are found for the selected date. + * @param errorMessage the error message to display + */ + void prepareFailView(String errorMessage); + +// //to implement later +// /** +// * Switch to the Dashboard View. +// */ +// void switchToDashboardView(); +} diff --git a/src/main/java/use_case/calendarFlow/CalendarFlowOutputData.java b/src/main/java/use_case/calendarFlow/CalendarFlowOutputData.java new file mode 100644 index 00000000..27b91fda --- /dev/null +++ b/src/main/java/use_case/calendarFlow/CalendarFlowOutputData.java @@ -0,0 +1,34 @@ +package use_case.calendarFlow; + +import entity.Event; +import java.util.List; +import java.time.LocalDate; + +/** + * Output Data for the Calendar Flow Use Case. + * Contains the selected date and the events found for that date. + */ +public class CalendarFlowOutputData { + private final LocalDate selectedDate; + private final List events; + private final boolean hasEvents; + + public CalendarFlowOutputData(LocalDate selectedDate, List events) { + this.selectedDate = selectedDate; + this.events = events; + this.hasEvents = events != null && !events.isEmpty(); + } + + public LocalDate getSelectedDate() { + return selectedDate; + } + + public List getEvents() { + return events; + } + + public boolean getHasEvents() { + return hasEvents; + } + +} diff --git a/src/main/java/use_case/display_local_events/DisplayLocalEventsInputBoundary.java b/src/main/java/use_case/display_local_events/DisplayLocalEventsInputBoundary.java new file mode 100644 index 00000000..8b650387 --- /dev/null +++ b/src/main/java/use_case/display_local_events/DisplayLocalEventsInputBoundary.java @@ -0,0 +1,5 @@ +package use_case.display_local_events; + +public interface DisplayLocalEventsInputBoundary { + void execute(DisplayLocalEventsInputData inputData); +} diff --git a/src/main/java/use_case/display_local_events/DisplayLocalEventsInputData.java b/src/main/java/use_case/display_local_events/DisplayLocalEventsInputData.java new file mode 100644 index 00000000..48a53f39 --- /dev/null +++ b/src/main/java/use_case/display_local_events/DisplayLocalEventsInputData.java @@ -0,0 +1,75 @@ +package use_case.display_local_events; +import entity.Location; +import entity.EventCategory; + +public class DisplayLocalEventsInputData { + private final Location userLocation; + private final double radiusKm; + private final EventCategory filterCategory; + + public DisplayLocalEventsInputData(Location userLocation, double radiusKm, EventCategory filterCategory) { + this.userLocation = userLocation; + this.radiusKm = radiusKm; + this.filterCategory = filterCategory; + } + + public Location getUserLocation() { + return userLocation; + } + + public double getRadiusKm() { + return radiusKm; + } + + public EventCategory getFilterCategory() { + return filterCategory; + } + + /** + * Validates the input data + * @return true if the input data is valid, false otherwise + */ + public boolean isValid() { + return userLocation != null && radiusKm > 0; + } + + /** + * Checks if a category filter is specified + * @return true if a category filter is set, false otherwise + */ + public boolean hasCategoryFilter() { + return filterCategory != null; + } + + @Override + public String toString() { + return String.format( + "DisplayLocalEventsInputData{userLocation=%s, radiusKm=%.1f, filterCategory=%s}", + userLocation, radiusKm, filterCategory + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DisplayLocalEventsInputData)) return false; + + DisplayLocalEventsInputData that = (DisplayLocalEventsInputData) o; + + if (Double.compare(that.radiusKm, radiusKm) != 0) return false; + if (!userLocation.equals(that.userLocation)) return false; + return filterCategory != null ? filterCategory.equals(that.filterCategory) : that.filterCategory == null; + } + + @Override + public int hashCode() { + int result; + long temp; + result = userLocation.hashCode(); + temp = Double.doubleToLongBits(radiusKm); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + (filterCategory != null ? filterCategory.hashCode() : 0); + return result; + } +} + diff --git a/src/main/java/use_case/display_local_events/DisplayLocalEventsInteractor.java b/src/main/java/use_case/display_local_events/DisplayLocalEventsInteractor.java new file mode 100644 index 00000000..7f229d84 --- /dev/null +++ b/src/main/java/use_case/display_local_events/DisplayLocalEventsInteractor.java @@ -0,0 +1,112 @@ +package use_case.display_local_events; + +import data_access.TicketmasterEventRepositoryAdapter; +import entity.Event; +import entity.EventRepository; +import entity.Location; + +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Comparator; +import java.util.stream.Collectors; + +/** + * Interactor - implements the core business logic for displaying local events + * This is the heart of the Display Local Events functionality + */ +public class DisplayLocalEventsInteractor implements DisplayLocalEventsInputBoundary { + private final EventRepository eventRepository; + private final DisplayLocalEventsOutputBoundary outputBoundary; + + /** + * Constructor for DisplayLocalEventsInteractor + * @param eventRepository the repository for accessing event data + * @param outputBoundary the output boundary for presenting results + */ + public DisplayLocalEventsInteractor(EventRepository eventRepository, + DisplayLocalEventsOutputBoundary outputBoundary) { + this.eventRepository = eventRepository; + this.outputBoundary = outputBoundary; + } + + @Override + public void execute(DisplayLocalEventsInputData inputData) { + try { + if (!inputData.isValid()) { + outputBoundary.presentError("Invalid input: location and radius are required"); + return; + } + + List allEvents; + if (eventRepository instanceof TicketmasterEventRepositoryAdapter) { + TicketmasterEventRepositoryAdapter adapter = + (TicketmasterEventRepositoryAdapter) eventRepository; + allEvents = adapter.findEvents( + inputData.getUserLocation(), + inputData.getRadiusKm() + ); + } else { + allEvents = eventRepository.findAllEvents(); + } + + List localEvents = allEvents.stream() + .filter(event -> event.isWithinRadius(inputData.getUserLocation(), inputData.getRadiusKm())) + .collect(java.util.stream.Collectors.toList()); + + List filteredEvents = localEvents; + if (inputData.hasCategoryFilter()) { + filteredEvents = localEvents.stream() + .filter(event -> event.isInCategory(inputData.getFilterCategory())) + .collect(Collectors.toList()); + } + + List sortedEvents = filteredEvents.stream() + .sorted(Comparator.comparing(event -> + event.calculateDistanceTo(inputData.getUserLocation()))) + .collect(Collectors.toList()); + + Map eventDistances = calculateEventDistances(sortedEvents, inputData.getUserLocation()); + + String message = buildSuccessMessage(sortedEvents.size(), inputData); + + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + sortedEvents, + message, + eventDistances + ); + + outputBoundary.presentSuccess(outputData); + + } catch (Exception e) { + outputBoundary.presentError("Error displaying local events: " + e.getMessage()); + } + } + + /** + * Calculates distances for each event from the user's location + */ + private Map calculateEventDistances(List events, Location userLocation) { + Map distances = new HashMap<>(); + for (Event event : events) { + double distance = event.calculateDistanceTo(userLocation); + distances.put(event.getId(), distance); + } + return distances; + } + + /** + * Builds a user-friendly success message based on the results + */ + private String buildSuccessMessage(int eventCount, DisplayLocalEventsInputData inputData) { + if (inputData.hasCategoryFilter()) { + return String.format("Found %d %s events within %.1f km, sorted by distance", + eventCount, + inputData.getFilterCategory().getDisplayName(), + inputData.getRadiusKm()); + } else { + return String.format("Found %d events within %.1f km, sorted by distance", + eventCount, inputData.getRadiusKm()); + } + } +} \ No newline at end of file diff --git a/src/main/java/use_case/display_local_events/DisplayLocalEventsOutputBoundary.java b/src/main/java/use_case/display_local_events/DisplayLocalEventsOutputBoundary.java new file mode 100644 index 00000000..d4159df7 --- /dev/null +++ b/src/main/java/use_case/display_local_events/DisplayLocalEventsOutputBoundary.java @@ -0,0 +1,7 @@ +package use_case.display_local_events; + +public interface DisplayLocalEventsOutputBoundary { + void presentSuccess(DisplayLocalEventsOutputData outputData); + + void presentError(String errorMessage); +} diff --git a/src/main/java/use_case/display_local_events/DisplayLocalEventsOutputData.java b/src/main/java/use_case/display_local_events/DisplayLocalEventsOutputData.java new file mode 100644 index 00000000..09be4570 --- /dev/null +++ b/src/main/java/use_case/display_local_events/DisplayLocalEventsOutputData.java @@ -0,0 +1,108 @@ +package use_case.display_local_events; + +import entity.Event; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** + * Output data for the Display Local Events use case + * Contains all the results from executing the use case + */ +public class DisplayLocalEventsOutputData { + private final List events; + private final String message; + private final int totalEvents; + private final Map eventDistances; + + public DisplayLocalEventsOutputData(List events, String message) { + this.events = events != null ? events : List.of(); + this.message = message != null ? message : ""; + this.totalEvents = this.events.size(); + this.eventDistances = new HashMap<>(); // 可以在Interactor中填充具体距离 + } + + /** + * Constructor with distance information + * @param events the list of events to display + * @param message the result message for the user + * @param eventDistances map of event IDs to their distances from user + */ + public DisplayLocalEventsOutputData(List events, String message, Map eventDistances) { + this.events = events != null ? events : List.of(); + this.message = message != null ? message : ""; + this.totalEvents = this.events.size(); + this.eventDistances = eventDistances != null ? eventDistances : new HashMap<>(); + } + + // Getter methods + public List getEvents() { + return events; + } + + public String getMessage() { + return message; + } + + public int getTotalEvents() { + return totalEvents; + } + + public Map getEventDistances() { + return eventDistances; + } + + /** + * Gets the distance for a specific event + * @param eventId the ID of the event + * @return the distance in kilometers, or -1 if not found + */ + public double getDistanceForEvent(String eventId) { + return eventDistances.getOrDefault(eventId, -1.0); + } + + /** + * Checks if there are any events to display + * @return true if there are events, false otherwise + */ + public boolean hasEvents() { + return !events.isEmpty(); + } + + /** + * Checks if distance information is available + * @return true if distance data exists, false otherwise + */ + public boolean hasDistanceData() { + return !eventDistances.isEmpty(); + } + + @Override + public String toString() { + return String.format( + "DisplayLocalEventsOutputData{totalEvents=%d, message='%s', hasDistances=%s}", + totalEvents, message, hasDistanceData() + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DisplayLocalEventsOutputData)) return false; + + DisplayLocalEventsOutputData that = (DisplayLocalEventsOutputData) o; + + if (totalEvents != that.totalEvents) return false; + if (!events.equals(that.events)) return false; + return message.equals(that.message); + } + + @Override + public int hashCode() { + int result = events.hashCode(); + result = 31 * result + message.hashCode(); + result = 31 * result + totalEvents; + return result; + } +} + 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/use_case/save_button/SaveButtonEventDataAccessInterface.java b/src/main/java/use_case/save_button/SaveButtonEventDataAccessInterface.java new file mode 100644 index 00000000..eba17479 --- /dev/null +++ b/src/main/java/use_case/save_button/SaveButtonEventDataAccessInterface.java @@ -0,0 +1,4 @@ +package use_case.save_button; + +public interface SaveButtonEventDataAccessInterface { +} diff --git a/src/main/java/use_case/save_button/SaveButtonInputBoundary.java b/src/main/java/use_case/save_button/SaveButtonInputBoundary.java new file mode 100644 index 00000000..6de3875c --- /dev/null +++ b/src/main/java/use_case/save_button/SaveButtonInputBoundary.java @@ -0,0 +1,12 @@ +package use_case.save_button; + +import use_case.save_button.SaveButtonInputData; + +public interface SaveButtonInputBoundary { + + /** + * Executes the save button use case + * @param inputData the input data + */ + void execute(SaveButtonInputData inputData); +} diff --git a/src/main/java/use_case/save_button/SaveButtonInputData.java b/src/main/java/use_case/save_button/SaveButtonInputData.java new file mode 100644 index 00000000..c1051587 --- /dev/null +++ b/src/main/java/use_case/save_button/SaveButtonInputData.java @@ -0,0 +1,15 @@ +package use_case.save_button; + +import entity.Event; + +public class SaveButtonInputData { + private final Event event; + + public SaveButtonInputData(Event event) { + this.event = event; + } + + public Event getEvent() { + return this.event; + } +} diff --git a/src/main/java/use_case/save_button/SaveButtonInteractor.java b/src/main/java/use_case/save_button/SaveButtonInteractor.java new file mode 100644 index 00000000..ec8fad56 --- /dev/null +++ b/src/main/java/use_case/save_button/SaveButtonInteractor.java @@ -0,0 +1,16 @@ +package use_case.save_button; + + +public class SaveButtonInteractor implements SaveButtonInputBoundary{ + + private SaveButtonOutputBoundary eventPresenter; + + public SaveButtonInteractor(SaveButtonOutputBoundary outputBoundary) { + this.eventPresenter = outputBoundary; + } + + @Override + public void execute(SaveButtonInputData inputData) { + + } +} diff --git a/src/main/java/use_case/save_button/SaveButtonOutputBoundary.java b/src/main/java/use_case/save_button/SaveButtonOutputBoundary.java new file mode 100644 index 00000000..00893b13 --- /dev/null +++ b/src/main/java/use_case/save_button/SaveButtonOutputBoundary.java @@ -0,0 +1,16 @@ +package use_case.save_button; + +public interface SaveButtonOutputBoundary { + + /** + * Prepare success view for save button use case + * @param outputData + */ + void prepareSuccessView(SaveButtonOutputData outputData); + + /** + * Prepare failure view for save button use case + * @param errorMessage + */ + void prepareFailureView(String errorMessage); +} diff --git a/src/main/java/use_case/save_button/SaveButtonOutputData.java b/src/main/java/use_case/save_button/SaveButtonOutputData.java new file mode 100644 index 00000000..9858f5a5 --- /dev/null +++ b/src/main/java/use_case/save_button/SaveButtonOutputData.java @@ -0,0 +1,15 @@ +package use_case.save_button; + +import entity.Event; + +public class SaveButtonOutputData { + private final Event event; + + public SaveButtonOutputData(Event event) { + this.event = event; + } + + public Event getEvent() { + return event; + } +} diff --git a/src/main/java/use_case/save_event/SaveEventEventDataAccessInterface.java b/src/main/java/use_case/save_event/SaveEventEventDataAccessInterface.java new file mode 100644 index 00000000..f995b240 --- /dev/null +++ b/src/main/java/use_case/save_event/SaveEventEventDataAccessInterface.java @@ -0,0 +1,4 @@ +package use_case.save_event; + +public interface SaveEventEventDataAccessInterface { +} diff --git a/src/main/java/use_case/save_event/SaveEventInputBoundary.java b/src/main/java/use_case/save_event/SaveEventInputBoundary.java new file mode 100644 index 00000000..d2006c56 --- /dev/null +++ b/src/main/java/use_case/save_event/SaveEventInputBoundary.java @@ -0,0 +1,16 @@ +package use_case.save_event; + +import use_case.login.LoginInputData; + +public interface SaveEventInputBoundary { + /** + * Executes the save event use case + * @param inputData the input data + */ + void execute(SaveEventInputData inputData); + + /** + * Executes switch to dashboard view use case + */ + void switchToDashboardView(); +} diff --git a/src/main/java/use_case/save_event/SaveEventInputData.java b/src/main/java/use_case/save_event/SaveEventInputData.java new file mode 100644 index 00000000..3ab3a9d9 --- /dev/null +++ b/src/main/java/use_case/save_event/SaveEventInputData.java @@ -0,0 +1,16 @@ +package use_case.save_event; + +import entity.Event; + +public class SaveEventInputData { + private final Event event; + + public SaveEventInputData(Event event) { + this.event = event; + } + + public Event getEvent() { + return this.event; + } + +} diff --git a/src/main/java/use_case/save_event/SaveEventInteractor.java b/src/main/java/use_case/save_event/SaveEventInteractor.java new file mode 100644 index 00000000..822d7e8d --- /dev/null +++ b/src/main/java/use_case/save_event/SaveEventInteractor.java @@ -0,0 +1,63 @@ +package use_case.save_event; + +import data_access.FileSavedEventsDataAccessObject; +import data_access.FileUserDataAccessObject; +import entity.Event; +import use_case.login.LoginUserDataAccessInterface; + +import java.util.ArrayList; +import java.util.List; + +public class SaveEventInteractor implements SaveEventInputBoundary { + + private SaveEventOutputBoundary eventPresenter; + private FileSavedEventsDataAccessObject savedEventsDAO; + private LoginUserDataAccessInterface userDataAccess; + + public SaveEventInteractor(SaveEventOutputBoundary outputBoundary, + FileSavedEventsDataAccessObject savedEventsDAO, + LoginUserDataAccessInterface userDataAccess) { + this.eventPresenter = outputBoundary; + this.savedEventsDAO = savedEventsDAO; + this.userDataAccess = userDataAccess; + } + + @Override + public void execute(SaveEventInputData inputData) { + if (inputData.getEvent() == null) { + eventPresenter.prepareFailureView("No Event Found"); + } else { + Event event = inputData.getEvent(); + String currentUsername = userDataAccess.getCurrentUsername(); + + if (savedEventsDAO.isSavedEvent(currentUsername, event.getId())) { + eventPresenter.prepareFailureView("Event already saved"); + return; + } + + savedEventsDAO.saveEvent(currentUsername, event); + SaveEventOutputData outputData = new SaveEventOutputData(event); + eventPresenter.prepareSuccessView(outputData); + } + } + + @Override + public void switchToDashboardView() { + eventPresenter.switchToDashboardView(); + } + + public List getSavedEvents() { + String currentUsername = userDataAccess.getCurrentUsername(); + if (currentUsername == null) { + return List.of(); + } + return savedEventsDAO.getSavedEvents(currentUsername); + } + + public void removeEvent(Event event) { + String currentUsername = userDataAccess.getCurrentUsername(); + if (currentUsername != null) { + savedEventsDAO.removeEvent(currentUsername, event); + } + } +} diff --git a/src/main/java/use_case/save_event/SaveEventOutputBoundary.java b/src/main/java/use_case/save_event/SaveEventOutputBoundary.java new file mode 100644 index 00000000..23b9bc2c --- /dev/null +++ b/src/main/java/use_case/save_event/SaveEventOutputBoundary.java @@ -0,0 +1,23 @@ +package use_case.save_event; + +public interface SaveEventOutputBoundary { + + + /** + * Prepares the success view for the Save event use case + * @param outputData + */ + void prepareSuccessView(SaveEventOutputData outputData); + + /** + * Prepares the failure view for the Save event use case + * @param errorMessage + */ + void prepareFailureView(String errorMessage); + + /** + * Switches to dashboard view for the use case + */ + void switchToDashboardView(); + +} diff --git a/src/main/java/use_case/save_event/SaveEventOutputData.java b/src/main/java/use_case/save_event/SaveEventOutputData.java new file mode 100644 index 00000000..24b1f109 --- /dev/null +++ b/src/main/java/use_case/save_event/SaveEventOutputData.java @@ -0,0 +1,15 @@ +package use_case.save_event; + +import entity.Event; + +public class SaveEventOutputData { + private final Event event; + + public SaveEventOutputData(Event event) { + this.event = event; + } + + public Event getEvent() { + return event; + } +} diff --git a/src/main/java/use_case/search/SearchEventDataAccessInterface.java b/src/main/java/use_case/search/SearchEventDataAccessInterface.java new file mode 100644 index 00000000..eeb349a1 --- /dev/null +++ b/src/main/java/use_case/search/SearchEventDataAccessInterface.java @@ -0,0 +1,4 @@ +package use_case.search; + +public class SearchEventDataAccessInterface { +} diff --git a/src/main/java/use_case/search/SearchInputBoundary.java b/src/main/java/use_case/search/SearchInputBoundary.java new file mode 100644 index 00000000..bf389014 --- /dev/null +++ b/src/main/java/use_case/search/SearchInputBoundary.java @@ -0,0 +1,12 @@ +package use_case.search; + +import use_case.login.LoginInputData; + +public interface SearchInputBoundary { + + /** + * Executes the login use case. + * @param inputData the input data + */ + void execute(SearchInputData inputData); +} diff --git a/src/main/java/use_case/search/SearchInputData.java b/src/main/java/use_case/search/SearchInputData.java new file mode 100644 index 00000000..7ae0a748 --- /dev/null +++ b/src/main/java/use_case/search/SearchInputData.java @@ -0,0 +1,22 @@ +package use_case.search; + +import entity.Location; + +public class SearchInputData { + private final String query; + private final String searchType; + private final Location location; + + public SearchInputData(String query, String searchType, Location location) { + this.query = query; + this.searchType = searchType; // This is for implementation into search by location vs event + this.location = location; + } + + public String getQuery() { return query; } + public String getSearchType() { return searchType; } + + public Location getLocation() { + return location; + } +} diff --git a/src/main/java/use_case/search/SearchInteractor.java b/src/main/java/use_case/search/SearchInteractor.java new file mode 100644 index 00000000..e22e7d71 --- /dev/null +++ b/src/main/java/use_case/search/SearchInteractor.java @@ -0,0 +1,50 @@ +package use_case.search; + +import entity.Event; +import entity.Location; +import use_case.search_event_by_name.SearchEventByNameDataAccessInterface; +import java.util.List; + +public class SearchInteractor implements SearchInputBoundary { + + private final SearchEventByNameDataAccessInterface eventDataAccess; + private final SearchOutputBoundary searchPresenter; + private final double RADIUS_KM = 50.0; + + + public SearchInteractor(SearchEventByNameDataAccessInterface eventDataAccess, + SearchOutputBoundary searchPresenter) { + this.eventDataAccess = eventDataAccess; + this.searchPresenter = searchPresenter; + } + + @Override + public void execute(SearchInputData inputData) { + if (inputData.getQuery() == null || inputData.getQuery().trim().isEmpty()) { + searchPresenter.prepareFailureView("Search query cannot be empty"); + return; + } + + Location userLocation = inputData.getLocation(); + + // Search for events + List events = eventDataAccess.searchEventsByName( + inputData.getQuery(), + userLocation, + RADIUS_KM + ); + + if (events.isEmpty()) { + searchPresenter.prepareFailureView("No events found for: " + inputData.getQuery()); + } else { + Event firstEvent = events.get(0); + SearchOutputData outputData = new SearchOutputData( + events, + inputData.getQuery(), + true, + "Found " + events.size() + " events" + ); + searchPresenter.prepareSuccessView(outputData); + } + } +} \ No newline at end of file diff --git a/src/main/java/use_case/search/SearchOutputBoundary.java b/src/main/java/use_case/search/SearchOutputBoundary.java new file mode 100644 index 00000000..b8676347 --- /dev/null +++ b/src/main/java/use_case/search/SearchOutputBoundary.java @@ -0,0 +1,22 @@ +package use_case.search; + +public interface SearchOutputBoundary { + + /** + * Prepare the success view for the Search Use Case + * @param outputData + */ + void prepareSuccessView(SearchOutputData outputData); + + /** + * Prepares the failure view for the Search Use Case. + * @param failMessage the explanation of the failure + */ + void prepareFailureView(String failMessage); + + /** + * Switch to the search by event view + * @param outputData + */ + void switchToEventSearchView(SearchOutputData outputData); +} diff --git a/src/main/java/use_case/search/SearchOutputData.java b/src/main/java/use_case/search/SearchOutputData.java new file mode 100644 index 00000000..44f8fbf2 --- /dev/null +++ b/src/main/java/use_case/search/SearchOutputData.java @@ -0,0 +1,26 @@ +package use_case.search; + +import entity.Event; +import java.util.List; + +public class SearchOutputData { + private final List events; + private final String query; + private final boolean success; + private final String message; + + public SearchOutputData(List events, String query, boolean success, String message) { + this.events = events; + this.query = query; + this.success = success; + this.message = message; + } + + public List getEvents() { return events; } + public String getQuery() { return query; } + public boolean isSuccess() { return success; } + public String getMessage() { return message; } + public Event getEvent() { + return events != null && !events.isEmpty() ? events.get(0) : null; + } +} diff --git a/src/main/java/use_case/search_event_by_name/SearchEventByNameDataAccessInterface.java b/src/main/java/use_case/search_event_by_name/SearchEventByNameDataAccessInterface.java new file mode 100644 index 00000000..12ff131e --- /dev/null +++ b/src/main/java/use_case/search_event_by_name/SearchEventByNameDataAccessInterface.java @@ -0,0 +1,16 @@ +package use_case.search_event_by_name; + +import entity.Event; +import entity.Location; +import java.util.List; + +public interface SearchEventByNameDataAccessInterface { + /** + * Search for events by keyword near a location. + * @param keyword the search keyword + * @param location the user's location + * @param radiusKm the search radius in kilometers + * @return list of events matching the search + */ + List searchEventsByName(String keyword, Location location, double radiusKm); +} \ No newline at end of file diff --git a/src/main/java/use_case/search_event_by_name/SearchEventByNameInputBoundary.java b/src/main/java/use_case/search_event_by_name/SearchEventByNameInputBoundary.java new file mode 100644 index 00000000..cfb7a5e6 --- /dev/null +++ b/src/main/java/use_case/search_event_by_name/SearchEventByNameInputBoundary.java @@ -0,0 +1,11 @@ +package use_case.search_event_by_name; + +public interface SearchEventByNameInputBoundary { + + void execute(SearchEventByNameInputData inputData); + + /** + * Executes the switch to login view use case. + */ + void switchToDashboardView(); +} diff --git a/src/main/java/use_case/search_event_by_name/SearchEventByNameInputData.java b/src/main/java/use_case/search_event_by_name/SearchEventByNameInputData.java new file mode 100644 index 00000000..86d3a3ef --- /dev/null +++ b/src/main/java/use_case/search_event_by_name/SearchEventByNameInputData.java @@ -0,0 +1,27 @@ +package use_case.search_event_by_name; + +import entity.Location; + +public class SearchEventByNameInputData { + private final String keyword; + private final Location userLocation; + private final double radiusKm; + + public SearchEventByNameInputData(String keyword, Location userLocation, double radiusKm) { + this.keyword = keyword; + this.userLocation = userLocation; + this.radiusKm = radiusKm; + } + + public String getKeyword() { + return keyword; + } + + public Location getUserLocation() { + return userLocation; + } + + public double getRadiusKm() { + return radiusKm; + } +} diff --git a/src/main/java/use_case/search_event_by_name/SearchEventByNameInteractor.java b/src/main/java/use_case/search_event_by_name/SearchEventByNameInteractor.java new file mode 100644 index 00000000..03326468 --- /dev/null +++ b/src/main/java/use_case/search_event_by_name/SearchEventByNameInteractor.java @@ -0,0 +1,55 @@ +package use_case.search_event_by_name; + +import entity.User; +import entity.UserFactory; +import use_case.signup.SignupOutputBoundary; +import use_case.signup.SignupUserDataAccessInterface; +import entity.Event; +import java.util.List; + +public class SearchEventByNameInteractor implements SearchEventByNameInputBoundary{ + + private final SearchEventByNameDataAccessInterface eventDataAccess; + private final SearchEventByNameOutputBoundary userPresenter; + + public SearchEventByNameInteractor( + SearchEventByNameDataAccessInterface eventDataAccess, + SearchEventByNameOutputBoundary searchEventByNameOutputBoundary) { + this.eventDataAccess = eventDataAccess; + this.userPresenter = searchEventByNameOutputBoundary; + } + + @Override + public void execute(SearchEventByNameInputData inputData) { + if (inputData.getKeyword() == null || inputData.getKeyword().trim().isEmpty()) { + userPresenter.prepareFailView("Search keyword cannot be empty"); + return; + } + + if (inputData.getUserLocation() == null) { + userPresenter.prepareFailView("User location is required"); + return; + } + + // Search for events + List events = eventDataAccess.searchEventsByName( + inputData.getKeyword(), + inputData.getUserLocation(), + inputData.getRadiusKm() + ); + + if (events.isEmpty()) { + userPresenter.prepareFailView("No events found for: " + inputData.getKeyword()); + } else { + // Return the first matching event + Event foundEvent = events.get(0); + SearchEventByNameOutputData outputData = new SearchEventByNameOutputData(foundEvent, true); + userPresenter.prepareSuccessView(outputData); + } + } + + @Override + public void switchToDashboardView() { + userPresenter.switchToDashboardView(); + } +} diff --git a/src/main/java/use_case/search_event_by_name/SearchEventByNameOutputBoundary.java b/src/main/java/use_case/search_event_by_name/SearchEventByNameOutputBoundary.java new file mode 100644 index 00000000..d814d40b --- /dev/null +++ b/src/main/java/use_case/search_event_by_name/SearchEventByNameOutputBoundary.java @@ -0,0 +1,20 @@ +package use_case.search_event_by_name; + +public interface SearchEventByNameOutputBoundary { + /** + * Prepares the success view with the found event. + * @param outputData the output data containing the event + */ + void prepareSuccessView(SearchEventByNameOutputData outputData); + + /** + * Prepares the failure view when no event is found. + * @param errorMessage the error message to display + */ + void prepareFailView(String errorMessage); + + /** + * Switches to the Dashboard View. + */ + void switchToDashboardView(); +} \ No newline at end of file diff --git a/src/main/java/use_case/search_event_by_name/SearchEventByNameOutputData.java b/src/main/java/use_case/search_event_by_name/SearchEventByNameOutputData.java new file mode 100644 index 00000000..71446ed1 --- /dev/null +++ b/src/main/java/use_case/search_event_by_name/SearchEventByNameOutputData.java @@ -0,0 +1,21 @@ +package use_case.search_event_by_name; + +import entity.Event; + +public class SearchEventByNameOutputData { + private final Event event; + private final boolean success; + + public SearchEventByNameOutputData(Event event, boolean success) { + this.event = event; + this.success = success; + } + + public Event getEvent() { + return event; + } + + public boolean isSuccess() { + return success; + } +} \ No newline at end of file diff --git a/src/main/java/view/CalendarView.java b/src/main/java/view/CalendarView.java new file mode 100644 index 00000000..d4aa7f29 --- /dev/null +++ b/src/main/java/view/CalendarView.java @@ -0,0 +1,138 @@ +package view; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionListener; +import java.awt.event.ActionEvent; +import java.time.LocalDate; +import java.time.YearMonth; + +import interface_adapter.calendarFlow.CalendarFlowController; +import entity.Location; + + +public class CalendarView extends JPanel implements ActionListener { + private final String viewName = "calendar view"; + private final JLabel monthYearLabel = new JLabel("", SwingConstants.CENTER); + private final JButton previousMonthButton = new JButton("◀"); + private final JButton nextMonthButton = new JButton("▶"); + private final JPanel dayNamesPanel = new JPanel(new GridLayout(1, 7)); + private final JPanel dayPanel = new JPanel(new GridLayout(6, 7, 5, 5)); + private final JButton[] dayButtons = new JButton[42]; + private YearMonth currentYearMonth; // Track which month in which year we're displaying + private String textFormat = "SegoeUI"; + + private CalendarFlowController calendarFlowController; + private Location userLocation; + private double searchRadiusKm = 50.0; //default + + public void setEventController(CalendarFlowController controller) { + this.calendarFlowController = controller; + } + + public void setUserLocation(Location location) { + this.userLocation = location; + } + + public void setSearchRadiusKm(double km) { + this.searchRadiusKm = km; + } + + public CalendarView(){ + this.setLayout(new BorderLayout()); + + monthYearLabel.setFont(new Font(textFormat, Font.BOLD, 18)); + previousMonthButton.addActionListener( + new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) {changeMonth(-1);} + }); + + nextMonthButton.addActionListener( + new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) {changeMonth(1);} + } + ); + + JPanel topPanel = new JPanel(new BorderLayout()); + topPanel.add(previousMonthButton, BorderLayout.WEST); + topPanel.add(monthYearLabel, BorderLayout.CENTER); + topPanel.add(nextMonthButton, BorderLayout.EAST); + this.add(topPanel, BorderLayout.NORTH); + + JPanel dayAndDayNamePanel = new JPanel(new BorderLayout()); + String[] dayNames = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + for (String dayName : dayNames){ + JLabel dayLabel = new JLabel(dayName, SwingConstants.CENTER); + dayLabel.setFont(new Font(textFormat, Font.BOLD, 12)); + dayNamesPanel.add(dayLabel); + } + + for(int i = 0; i < 42; i++){ + dayButtons[i] = new JButton(); + dayButtons[i].setFont(new Font(textFormat, Font.PLAIN, 12)); + dayPanel.add(dayButtons[i]); + } + + dayAndDayNamePanel.add(dayNamesPanel, BorderLayout.NORTH); + dayAndDayNamePanel.add(dayPanel, BorderLayout.CENTER); + + this.add(dayAndDayNamePanel, BorderLayout.CENTER); + currentYearMonth = YearMonth.now(); + displayMonth(currentYearMonth); + } + + private void changeMonth(int monthOffset) { + currentYearMonth = currentYearMonth.plusMonths(monthOffset); + displayMonth(currentYearMonth); + } + + private void displayMonth(YearMonth yearMonth){ + currentYearMonth = yearMonth; + monthYearLabel.setText(yearMonth.getMonth() + " " + yearMonth.getYear()); + + LocalDate firstOfMonth = yearMonth.atDay(1); + int firstDayOfWeek = firstOfMonth.getDayOfWeek().getValue() % 7; + int daysInMonth = yearMonth.lengthOfMonth(); + + //Defaulting all the button first + for (JButton button : dayButtons) { + button.setText(""); + button.setEnabled(false); + + for (var listener : button.getActionListeners()) { + button.removeActionListener(listener); + } + } + + for(int day = 1; day <= daysInMonth; day++){ + int dayToIndex = firstDayOfWeek + day - 1; + dayButtons[dayToIndex].setText(String.valueOf(day)); + dayButtons[dayToIndex].setEnabled(true); + + final LocalDate date = yearMonth.atDay(day); + dayButtons[dayToIndex].addActionListener( + new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + getSelectedDay(date); + } + }); + + } + } + + public void getSelectedDay(LocalDate date){ + System.out.println("Selected day: " + date.toString()); + if (calendarFlowController != null){ + calendarFlowController.execute(date, userLocation, searchRadiusKm); + } + + } + + public void actionPerformed(ActionEvent evt) { + System.out.println("Click " + evt.getActionCommand()); + } + +} diff --git a/src/main/java/view/DisplayLocalEventsView.java b/src/main/java/view/DisplayLocalEventsView.java new file mode 100644 index 00000000..e2d0a974 --- /dev/null +++ b/src/main/java/view/DisplayLocalEventsView.java @@ -0,0 +1,498 @@ +package view; + +import entity.Event; +import entity.Location; +import interface_adapter.ViewManagerModel; +import interface_adapter.search.SearchController; +import interface_adapter.display_local_events.DisplayLocalEventsController; +import interface_adapter.display_local_events.DisplayLocalEventsViewModel; + +import javax.imageio.ImageIO; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * View for displaying local events. + * + * CLEAN ARCHITECTURE NOTE: + * This is the View layer - it only knows about ViewModels and Controllers. + * It does NOT know about Use Cases, Entities directly, or Data Access. + * Communication flows: View -> Controller -> Use Case -> Presenter -> ViewModel -> View + */ +public class DisplayLocalEventsView extends JPanel implements PropertyChangeListener { + + private DisplayLocalEventsController controller; + private final DisplayLocalEventsViewModel viewModel; + private final String viewName = "display local events"; + + private final JLabel appNameLabel = new JLabel("Dashboard"); + private final JComboBox cityBox = new JComboBox<>(new String[]{"Toronto", "Montreal", "New York"}); + private final JButton searchButton = new JButton("Search"); + private final JComboBox categoryBox = new JComboBox<>(new String[]{"ALL", "Music", "Sports", "Arts & Theatre", "Film"}); + private final JComboBox sortBox = new JComboBox<>(new String[]{"Distance", "Date", "Name"}); + private SearchBarView searchBarView; + private final JButton calendarButton = new JButton("Calendar"); + private final JButton logoutButton = new JButton("Logout"); + private final JButton savedEventsButton = new JButton("Saved Events"); + private final JPanel cardsContainer = new JPanel(); + private final JScrollPane cardsScrollPane; + private final JLabel emptyStateLabel = new JLabel("Choose a location and click search to see local events.", SwingConstants.CENTER); + private static final double DEFAULT_RADIUS_KM = 50.0; + private ViewManagerModel viewManagerModel; + + // Store events for click handling - maps card index to Event + private List currentEvents = new ArrayList<>(); + + // Callback interface for event selection (Clean Architecture - Dependency Inversion) + private EventSelectionListener eventSelectionListener; + + /** + * Interface for handling event selection. + * This follows the Dependency Inversion Principle - the View depends on an abstraction, + * not a concrete implementation. + */ + public interface EventSelectionListener { + void onEventSelected(Event event); + } + + public DisplayLocalEventsView(DisplayLocalEventsViewModel viewModel) { + this.viewModel = viewModel; + this.viewModel.addPropertyChangeListener(this); + + setLayout(new BorderLayout()); + setBorder(new EmptyBorder(5, 5, 5, 5)); + setBackground(new Color(245, 247, 250)); + + add(buildTopBar(), BorderLayout.NORTH); + add(buildSideBar(), BorderLayout.WEST); + + cardsContainer.setBorder(new EmptyBorder(10, 10, 10, 10)); + cardsContainer.setBackground(new Color(245, 247, 250)); + + cardsScrollPane = new JScrollPane(cardsContainer); + cardsScrollPane.getVerticalScrollBar().setUnitIncrement(16); + add(cardsScrollPane, BorderLayout.CENTER); + + renderEmptyState(); + + searchButton.addActionListener(e -> onSearch()); + sortBox.addActionListener(e -> renderEvents()); + calendarButton.addActionListener(e -> navigateToCalendar()); + logoutButton.addActionListener(e -> + JOptionPane.showMessageDialog(this, "Logout / Login UI not implemented yet.")); + savedEventsButton.addActionListener(e -> navigateToSavedEvents()); + } + + public void setViewManagerModel(ViewManagerModel viewManagerModel) { + this.viewManagerModel = viewManagerModel; + } + + public String getViewName() { + return viewName; + } + + public void setController(DisplayLocalEventsController controller) { + this.controller = controller; + } + + /** + * Set the listener for event selection. + * This is how we handle navigation to event details while maintaining Clean Architecture. + */ + public void setEventSelectionListener(EventSelectionListener listener) { + this.eventSelectionListener = listener; + } + + /** + * Store the current events list for click handling. + * Called when events are loaded. + */ + public void setCurrentEvents(List events) { + this.currentEvents = events != null ? new ArrayList<>(events) : new ArrayList<>(); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if ("state".equals(evt.getPropertyName())) { + renderEvents(); + } + } + + public void addViewListener(PropertyChangeListener listener) { + this.viewModel.addPropertyChangeListener(listener); + } + + public void updateFromLocation(Location location, double radiusKm) { + if (controller != null && location != null) { + String category = (String) categoryBox.getSelectedItem(); + controller.display(location, radiusKm, category); + } + } + + public void updateFromCategory(String category) { + if (category != null) { + categoryBox.setSelectedItem(category); + onSearch(); + } + } + + public Location getCurrentLocationForOthers() { + return getCurrentLocation(); + } + + public DisplayLocalEventsViewModel getViewModel() { + return this.viewModel; + } + + private JPanel buildTopBar() { + JPanel topBar = new JPanel(new BorderLayout()); + topBar.setBorder(new EmptyBorder(8, 12, 8, 12)); + topBar.setBackground(new Color(25, 118, 210)); + + appNameLabel.setFont(appNameLabel.getFont().deriveFont(Font.BOLD, 18f)); + appNameLabel.setForeground(Color.WHITE); + topBar.add(appNameLabel, BorderLayout.WEST); + + JPanel centerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + centerPanel.setOpaque(false); + JLabel locationLabel = new JLabel("Location:"); + locationLabel.setForeground(Color.WHITE); + centerPanel.add(locationLabel); + cityBox.setPreferredSize(new Dimension(130, 24)); + centerPanel.add(cityBox); + styleTopBarButton(searchButton); + centerPanel.add(searchButton); + topBar.add(centerPanel, BorderLayout.CENTER); + + JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0)); + rightPanel.setOpaque(false); + JLabel categoryLabel = new JLabel("Category:"); + categoryLabel.setForeground(Color.WHITE); + rightPanel.add(categoryLabel); + categoryBox.setPreferredSize(new Dimension(140, 24)); + rightPanel.add(categoryBox); + JLabel sortLabel = new JLabel("Sorting by:"); + sortLabel.setForeground(Color.WHITE); + rightPanel.add(sortLabel); + sortBox.setPreferredSize(new Dimension(120, 24)); + rightPanel.add(sortBox); + JLabel nameSearchLabel = new JLabel("Search by name:"); + nameSearchLabel.setForeground(Color.WHITE); + rightPanel.add(nameSearchLabel); + Location defaultLocation = getCurrentLocation(); + searchBarView = new SearchBarView("Search events...", defaultLocation); + searchBarView.setPreferredSize(new Dimension(200, 40)); + rightPanel.add(searchBarView); + topBar.add(rightPanel, BorderLayout.EAST); + + return topBar; + } + + private void styleTopBarButton(JButton button) { + button.setFocusPainted(false); + button.setCursor(new Cursor(Cursor.HAND_CURSOR)); + } + + private JPanel buildSideBar() { + JPanel sideBar = new JPanel(); + sideBar.setLayout(new BoxLayout(sideBar, BoxLayout.Y_AXIS)); + sideBar.setBorder(new EmptyBorder(10, 8, 10, 8)); + sideBar.setBackground(new Color(245, 247, 250)); + + styleSideButton(calendarButton); + styleSideButton(logoutButton); + styleSideButton(savedEventsButton); + + sideBar.add(calendarButton); + sideBar.add(Box.createVerticalStrut(10)); + sideBar.add(logoutButton); + sideBar.add(Box.createVerticalStrut(10)); + sideBar.add(savedEventsButton); + sideBar.add(Box.createVerticalGlue()); + + return sideBar; + } + + private void styleSideButton(JButton button) { + button.setAlignmentX(Component.CENTER_ALIGNMENT); + button.setMaximumSize(new Dimension(140, 36)); + button.setFocusPainted(false); + button.setBackground(Color.WHITE); + button.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(220, 220, 220)), + new EmptyBorder(5, 10, 5, 10) + )); + button.setCursor(new Cursor(Cursor.HAND_CURSOR)); + } + + private void onSearch() { + if (controller == null) { + JOptionPane.showMessageDialog(this, "Controller not initialized.", + "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + String selectedCity = (String) cityBox.getSelectedItem(); + if (selectedCity == null) { + JOptionPane.showMessageDialog(this, "Please select a location.", + "Input required", JOptionPane.WARNING_MESSAGE); + return; + } + + Location userLoc; + switch (selectedCity) { + case "Montreal": + userLoc = new Location("Montreal, QC", 45.5019, -73.5674); + break; + case "New York": + userLoc = new Location("New York, NY", 40.7128, -74.0060); + break; + case "Toronto": + default: + userLoc = new Location("Toronto, ON", 43.6435, -79.3791); + break; + } + + String category = (String) categoryBox.getSelectedItem(); + controller.display(userLoc, DEFAULT_RADIUS_KM, category); + } + + private void renderEmptyState() { + cardsContainer.removeAll(); + cardsContainer.setLayout(new BorderLayout()); + emptyStateLabel.setFont(emptyStateLabel.getFont().deriveFont(Font.PLAIN, 14f)); + emptyStateLabel.setForeground(new Color(120, 120, 120)); + cardsContainer.add(emptyStateLabel, BorderLayout.CENTER); + cardsContainer.revalidate(); + cardsContainer.repaint(); + } + + public void renderEvents() { + cardsContainer.removeAll(); + + if (viewModel.hasError()) { + cardsContainer.setLayout(new BorderLayout()); + JLabel errorLabel = new JLabel("Error: " + viewModel.getError(), SwingConstants.CENTER); + errorLabel.setForeground(new Color(211, 47, 47)); + errorLabel.setFont(errorLabel.getFont().deriveFont(Font.BOLD, 13f)); + cardsContainer.add(errorLabel, BorderLayout.CENTER); + } else if (!viewModel.hasEvents()) { + renderEmptyState(); + return; + } else { + List cards = new ArrayList<>(viewModel.getEventCards()); + String sortBy = (String) sortBox.getSelectedItem(); + + if ("Name".equalsIgnoreCase(sortBy)) { + cards.sort(Comparator.comparing( + DisplayLocalEventsViewModel.EventCard::getName, + String.CASE_INSENSITIVE_ORDER)); + } else if ("Date".equalsIgnoreCase(sortBy)) { + cards.sort(Comparator.comparing( + DisplayLocalEventsViewModel.EventCard::getDateTime)); + } else { + cards.sort(Comparator.comparingDouble(this::parseDistance)); + } + + cardsContainer.setLayout(new GridLayout(0, 2, 10, 10)); + for (DisplayLocalEventsViewModel.EventCard cardData : cards) { + JPanel card = buildEventCard(cardData); + cardsContainer.add(card); + } + } + + cardsContainer.revalidate(); + cardsContainer.repaint(); + } + + private Location getCurrentLocation() { + String selectedCity = (String) cityBox.getSelectedItem(); + if (selectedCity == null) { + return new Location("Toronto, ON", 43.6532, -79.3832); + } + switch (selectedCity) { + case "Montreal": + return new Location("Montreal, QC", 45.5019, -73.5674); + case "New York": + return new Location("New York, NY", 40.7128, -74.0060); + case "Toronto": + default: + return new Location("Toronto, ON", 43.6532, -79.3832); + } + } + + private double parseDistance(DisplayLocalEventsViewModel.EventCard card) { + String text = card.getDistanceText(); + if (text == null || text.isBlank()) { + return Double.MAX_VALUE; + } + try { + String firstToken = text.split("\\s+")[0]; + return Double.parseDouble(firstToken); + } catch (Exception e) { + return Double.MAX_VALUE; + } + } + + /** + * Build an event card that is clickable. + * When clicked, it triggers the event selection listener. + */ + private JPanel buildEventCard(DisplayLocalEventsViewModel.EventCard cardData) { + JPanel card = new JPanel(new BorderLayout()); + card.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(4, 4, 4, 4), + BorderFactory.createLineBorder(new Color(230, 230, 230), 1) + )); + card.setBackground(Color.WHITE); + card.setCursor(new Cursor(Cursor.HAND_CURSOR)); + + // Make the entire card clickable + card.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent e) { + onEventCardClicked(cardData.getId()); + } + + @Override + public void mouseEntered(java.awt.event.MouseEvent e) { + card.setBackground(new Color(245, 245, 245)); + card.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(4, 4, 4, 4), + BorderFactory.createLineBorder(new Color(25, 118, 210), 2) + )); + } + + @Override + public void mouseExited(java.awt.event.MouseEvent e) { + card.setBackground(Color.WHITE); + card.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(4, 4, 4, 4), + BorderFactory.createLineBorder(new Color(230, 230, 230), 1) + )); + } + }); + + JLabel pictureLabel = new JLabel("", SwingConstants.CENTER); + pictureLabel.setPreferredSize(new Dimension(200, 110)); + ImageIcon icon = loadImageIcon(cardData.getImageUrl(), 200, 110); + if (icon != null) { + pictureLabel.setIcon(icon); + } else { + pictureLabel.setText("No Image"); + pictureLabel.setForeground(new Color(150, 150, 150)); + } + card.add(pictureLabel, BorderLayout.NORTH); + + JPanel textPanel = new JPanel(); + textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS)); + textPanel.setBorder(new EmptyBorder(8, 8, 8, 8)); + textPanel.setOpaque(false); + + JLabel nameLabel = new JLabel(cardData.getName()); + nameLabel.setFont(nameLabel.getFont().deriveFont(Font.BOLD, 13f)); + JLabel addressLabel = new JLabel(cardData.getAddress()); + addressLabel.setForeground(new Color(100, 100, 100)); + JLabel dateLabel = new JLabel(cardData.getDateTime()); + dateLabel.setForeground(new Color(120, 120, 120)); + JLabel distanceLabel = new JLabel(cardData.getDistanceText()); + distanceLabel.setForeground(new Color(33, 150, 243)); + + // Add "Click to view details" hint + JLabel clickHint = new JLabel("Click to view details →"); + clickHint.setFont(clickHint.getFont().deriveFont(Font.ITALIC, 11f)); + clickHint.setForeground(new Color(25, 118, 210)); + + textPanel.add(nameLabel); + textPanel.add(Box.createVerticalStrut(2)); + textPanel.add(addressLabel); + textPanel.add(Box.createVerticalStrut(2)); + textPanel.add(dateLabel); + textPanel.add(Box.createVerticalStrut(4)); + textPanel.add(distanceLabel); + textPanel.add(Box.createVerticalStrut(4)); + textPanel.add(clickHint); + + card.add(textPanel, BorderLayout.CENTER); + return card; + } + + /** + * Handle event card click - find the event and notify the listener. + * This method finds the event by ID and delegates to the listener. + */ + private void onEventCardClicked(String eventId) { + System.out.println("Event card clicked: " + eventId); + + // GET EVENTS FROM VIEWMODEL - THIS IS THE FIX! + List events = viewModel.getEvents(); + + System.out.println("Number of events in viewModel: " + (events != null ? events.size() : 0)); + + Event selectedEvent = null; + if (events != null) { + for (Event event : events) { + if (event.getId().equals(eventId)) { + selectedEvent = event; + System.out.println("Found event: " + event.getName()); + break; + } + } + } + + if (selectedEvent != null && eventSelectionListener != null) { + eventSelectionListener.onEventSelected(selectedEvent); + } else { + if (selectedEvent == null) { + System.err.println("Event not found with ID: " + eventId); + } + if (eventSelectionListener == null) { + System.err.println("No event selection listener set!"); + } + } + } + + private ImageIcon loadImageIcon(String imageUrl, int width, int height) { + if (imageUrl == null || imageUrl.isBlank()) { + return null; + } + try { + BufferedImage img = ImageIO.read(new URL(imageUrl)); + if (img == null) { + return null; + } + Image scaled = img.getScaledInstance(width, height, Image.SCALE_SMOOTH); + return new ImageIcon(scaled); + } catch (IOException e) { + System.err.println("Failed to load image: " + e.getMessage()); + return null; + } + } + + public void setSearchBarController(SearchController controller) { + this.searchBarView.setSearchController(controller); + } + + private void navigateToCalendar() { + if (viewManagerModel != null) { + viewManagerModel.setState("calendar view"); + viewManagerModel.firePropertyChange(); + } + } + + private void navigateToSavedEvents() { + if (viewManagerModel != null) { + viewManagerModel.setState("save event"); + viewManagerModel.firePropertyChange(); + } + } +} \ No newline at end of file diff --git a/src/main/java/view/EventDescriptionView.java b/src/main/java/view/EventDescriptionView.java new file mode 100644 index 00000000..b6d760ec --- /dev/null +++ b/src/main/java/view/EventDescriptionView.java @@ -0,0 +1,375 @@ +package view; + +import entity.Event; +import interface_adapter.ViewManagerModel; +import interface_adapter.event_description.EventDescriptionViewModel; +import interface_adapter.save_event.SaveEventController; + +import javax.imageio.ImageIO; +import javax.swing.*; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.net.URL; +import java.time.format.DateTimeFormatter; + + +public class EventDescriptionView extends JPanel implements PropertyChangeListener { + + private final EventDescriptionViewModel viewModel; + private ViewManagerModel viewManagerModel; + private SaveEventController saveEventController; + private Event currentEvent; + + // UI Components + private final JLabel titleLabel = new JLabel("", SwingConstants.LEFT); + private final JLabel categoryLabel = new JLabel(); + private final JLabel dateTimeLabel = new JLabel(); + private final JLabel addressLabel = new JLabel(); + private final JTextArea descriptionArea = new JTextArea(); + private final JLabel distanceLabel = new JLabel(); + private final JLabel imageLabel = new JLabel(); + private final JButton backButton = new JButton("← Back to Events"); + private final JButton saveButton = new JButton("Save Event"); + + private final DateTimeFormatter displayFormatter = DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy 'at' h:mm a"); + + public EventDescriptionView(EventDescriptionViewModel viewModel) { + this.viewModel = viewModel; + this.viewModel.addPropertyChangeListener(this); + + initializeUI(); + } + + private void initializeUI() { + setLayout(new BorderLayout()); + setBackground(Color.WHITE); + + // Header with back button + JPanel headerPanel = createHeaderPanel(); + add(headerPanel, BorderLayout.NORTH); + + // Main content - split into image and details + JSplitPane mainContent = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + mainContent.setDividerLocation(500); + mainContent.setDividerSize(0); + mainContent.setBorder(null); + + // Left side - Image + JPanel imagePanel = createImagePanel(); + mainContent.setLeftComponent(imagePanel); + + // Right side - Details + JPanel detailsPanel = createDetailsPanel(); + JScrollPane detailsScroll = new JScrollPane(detailsPanel); + detailsScroll.setBorder(null); + detailsScroll.getVerticalScrollBar().setUnitIncrement(16); + mainContent.setRightComponent(detailsScroll); + + add(mainContent, BorderLayout.CENTER); + } + + private JPanel createHeaderPanel() { + JPanel header = new JPanel(new BorderLayout()); + header.setBackground(new Color(25, 118, 210)); + header.setBorder(new EmptyBorder(15, 20, 15, 20)); + + // Style back button + backButton.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + backButton.setForeground(Color.WHITE); + backButton.setBackground(new Color(25, 118, 210)); + backButton.setBorder(BorderFactory.createEmptyBorder(8, 15, 8, 15)); + backButton.setFocusPainted(false); + backButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + backButton.setOpaque(true); + + backButton.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseEntered(java.awt.event.MouseEvent e) { + backButton.setBackground(new Color(21, 101, 192)); + } + + @Override + public void mouseExited(java.awt.event.MouseEvent e) { + backButton.setBackground(new Color(25, 118, 210)); + } + }); + + backButton.addActionListener(e -> navigateBack()); + + header.add(backButton, BorderLayout.WEST); + + JLabel headerTitle = new JLabel("Event Details"); + headerTitle.setFont(new Font("Segoe UI", Font.BOLD, 20)); + headerTitle.setForeground(Color.WHITE); + headerTitle.setHorizontalAlignment(SwingConstants.CENTER); + header.add(headerTitle, BorderLayout.CENTER); + + return header; + } + + private JPanel createImagePanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBackground(new Color(30, 30, 30)); + panel.setPreferredSize(new Dimension(500, 600)); + + imageLabel.setHorizontalAlignment(SwingConstants.CENTER); + imageLabel.setVerticalAlignment(SwingConstants.CENTER); + imageLabel.setForeground(Color.WHITE); + imageLabel.setText("Loading image..."); + + panel.add(imageLabel, BorderLayout.CENTER); + return panel; + } + + private JPanel createDetailsPanel() { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBackground(Color.WHITE); + panel.setBorder(new EmptyBorder(30, 30, 30, 30)); + + // Category badge + categoryLabel.setFont(new Font("Segoe UI", Font.BOLD, 12)); + categoryLabel.setForeground(new Color(59, 130, 246)); + categoryLabel.setOpaque(true); + categoryLabel.setBackground(new Color(219, 234, 254)); + categoryLabel.setBorder(new EmptyBorder(5, 12, 5, 12)); + categoryLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + panel.add(categoryLabel); + panel.add(Box.createVerticalStrut(15)); + + // Title + titleLabel.setFont(new Font("Segoe UI", Font.BOLD, 28)); + titleLabel.setForeground(new Color(17, 24, 39)); + titleLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + panel.add(titleLabel); + panel.add(Box.createVerticalStrut(20)); + + // Date/Time section + panel.add(createInfoRow("📅 Date & Time", dateTimeLabel)); + panel.add(Box.createVerticalStrut(15)); + + // Location section + panel.add(createInfoRow("📍 Location", addressLabel)); + panel.add(Box.createVerticalStrut(15)); + + // Distance section + panel.add(createInfoRow("📏 Distance", distanceLabel)); + panel.add(Box.createVerticalStrut(25)); + + // Description section + JLabel descTitle = new JLabel("About This Event"); + descTitle.setFont(new Font("Segoe UI", Font.BOLD, 18)); + descTitle.setForeground(new Color(17, 24, 39)); + descTitle.setAlignmentX(Component.LEFT_ALIGNMENT); + panel.add(descTitle); + panel.add(Box.createVerticalStrut(10)); + + descriptionArea.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + descriptionArea.setForeground(new Color(75, 85, 99)); + descriptionArea.setBackground(Color.WHITE); + descriptionArea.setLineWrap(true); + descriptionArea.setWrapStyleWord(true); + descriptionArea.setEditable(false); + descriptionArea.setBorder(null); + descriptionArea.setAlignmentX(Component.LEFT_ALIGNMENT); + descriptionArea.setMaximumSize(new Dimension(400, 200)); + panel.add(descriptionArea); + panel.add(Box.createVerticalStrut(30)); + + // Save button + styleSaveButton(); + saveButton.setAlignmentX(Component.LEFT_ALIGNMENT); + saveButton.addActionListener(e -> saveEvent()); + panel.add(saveButton); + + return panel; + } + + private JPanel createInfoRow(String label, JLabel valueLabel) { + JPanel row = new JPanel(); + row.setLayout(new BoxLayout(row, BoxLayout.Y_AXIS)); + row.setOpaque(false); + row.setAlignmentX(Component.LEFT_ALIGNMENT); + + JLabel titleLbl = new JLabel(label); + titleLbl.setFont(new Font("Segoe UI", Font.BOLD, 14)); + titleLbl.setForeground(new Color(107, 114, 128)); + titleLbl.setAlignmentX(Component.LEFT_ALIGNMENT); + + valueLabel.setFont(new Font("Segoe UI", Font.PLAIN, 16)); + valueLabel.setForeground(new Color(17, 24, 39)); + valueLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + row.add(titleLbl); + row.add(Box.createVerticalStrut(5)); + row.add(valueLabel); + + return row; + } + + private void styleSaveButton() { + saveButton.setFont(new Font("Segoe UI", Font.BOLD, 14)); + saveButton.setForeground(Color.WHITE); + saveButton.setBackground(new Color(59, 130, 246)); + saveButton.setBorder(new EmptyBorder(12, 30, 12, 30)); + saveButton.setFocusPainted(false); + saveButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + saveButton.setOpaque(true); + + saveButton.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseEntered(java.awt.event.MouseEvent e) { + saveButton.setBackground(new Color(37, 99, 235)); + } + + @Override + public void mouseExited(java.awt.event.MouseEvent e) { + saveButton.setBackground(new Color(59, 130, 246)); + } + }); + } + + /** + * Display an event's details. + * This method is called when navigating to this view with an event. + */ + public void displayEvent(Event event) { + this.currentEvent = event; + + if (event == null) { + titleLabel.setText("Event Not Found"); + categoryLabel.setText("N/A"); + dateTimeLabel.setText("N/A"); + addressLabel.setText("N/A"); + distanceLabel.setText("N/A"); + descriptionArea.setText("The requested event could not be found."); + return; + } + + // Update all fields + titleLabel.setText("" + event.getName() + ""); + categoryLabel.setText(event.getCategory().getDisplayName()); + dateTimeLabel.setText(event.getStartTime().format(displayFormatter)); + addressLabel.setText("" + event.getLocation().getAddress() + ""); + + // Description + String desc = event.getDescription(); + if (desc == null || desc.trim().isEmpty()) { + descriptionArea.setText("No description available for this event."); + } else { + descriptionArea.setText(desc); + } + + // Load image + loadEventImage(event.getImageUrl()); + + // Distance will be set separately via setDistanceText + distanceLabel.setText("Calculating..."); + } + + /** + * Set the distance text (called after distance calculation). + */ + public void setDistanceText(String distanceText) { + distanceLabel.setText(distanceText); + } + + private void loadEventImage(String imageUrl) { + if (imageUrl == null || imageUrl.trim().isEmpty()) { + imageLabel.setIcon(null); + imageLabel.setText("No image available"); + return; + } + + // Load image in background thread to avoid UI freeze + new SwingWorker() { + @Override + protected ImageIcon doInBackground() { + try { + BufferedImage img = ImageIO.read(new URL(imageUrl)); + if (img != null) { + Image scaled = img.getScaledInstance(480, 400, Image.SCALE_SMOOTH); + return new ImageIcon(scaled); + } + } catch (IOException e) { + System.err.println("Failed to load event image: " + e.getMessage()); + } + return null; + } + + @Override + protected void done() { + try { + ImageIcon icon = get(); + if (icon != null) { + imageLabel.setIcon(icon); + imageLabel.setText(""); + } else { + imageLabel.setIcon(null); + imageLabel.setText("Failed to load image"); + } + } catch (Exception e) { + imageLabel.setText("Error loading image"); + } + } + }.execute(); + } + + private void navigateBack() { + if (viewManagerModel != null) { + viewManagerModel.setState("display local events"); + viewManagerModel.firePropertyChange(); + } + } + + private void saveEvent() { + if (currentEvent != null && saveEventController != null) { + saveEventController.saveEvent(currentEvent); + saveButton.setText("Saved ✓"); + saveButton.setBackground(new Color(34, 197, 94)); + saveButton.setEnabled(false); + + // Reset button after 3 seconds + Timer timer = new Timer(3000, e -> { + saveButton.setText("Save Event"); + saveButton.setBackground(new Color(59, 130, 246)); + saveButton.setEnabled(true); + }); + timer.setRepeats(false); + timer.start(); + } else { + JOptionPane.showMessageDialog(this, + "Unable to save event. Please try again.", + "Error", JOptionPane.ERROR_MESSAGE); + } + } + + // Setters for dependency injection + public void setViewManagerModel(ViewManagerModel viewManagerModel) { + this.viewManagerModel = viewManagerModel; + } + + public void setSaveEventController(SaveEventController saveEventController) { + this.saveEventController = saveEventController; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + // Update from ViewModel if needed + if (viewModel.getErrorMessage() != null && !viewModel.getErrorMessage().isEmpty()) { + JOptionPane.showMessageDialog(this, + viewModel.getErrorMessage(), + "Error", JOptionPane.ERROR_MESSAGE); + } + } + + public String getViewName() { + return EventDescriptionViewModel.VIEW_NAME; + } +} \ No newline at end of file diff --git a/src/main/java/view/EventListByDateView.java b/src/main/java/view/EventListByDateView.java new file mode 100644 index 00000000..0f7e2fbb --- /dev/null +++ b/src/main/java/view/EventListByDateView.java @@ -0,0 +1,234 @@ +package view; + +import javax.swing.*; +import java.time.LocalDate; +import java.util.List; +import java.time.format.DateTimeFormatter; +import java.awt.*; +import java.util.Locale; +import javax.swing.border.EmptyBorder; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import interface_adapter.calendarFlow.CalendarFlowViewModel; + +import entity.Event; +import interface_adapter.calendarFlow.CalendarFlowState; + + +public class EventListByDateView extends JPanel implements PropertyChangeListener { + private final String viewName = "event list by date"; + private CalendarFlowViewModel calendarFlowViewModel; + + private final JLabel titleLabel = new JLabel("Events", SwingConstants.CENTER); + private final JLabel dateLabel = new JLabel("", SwingConstants.CENTER); + private final JPanel eventsContainer = new JPanel(); + private final JScrollPane scrollPane; + + private final JLabel emptyStateLabel = new JLabel("No events found for this date.", SwingConstants.CENTER); + private final JButton backButton = new JButton("← Back to DashBoard"); + private final String textFont = "SegoeUI"; + + public EventListByDateView(CalendarFlowViewModel calendarFlowViewModel) { + this.calendarFlowViewModel = calendarFlowViewModel; + this.calendarFlowViewModel.addPropertyChangeListener(this); + + setLayout(new BorderLayout()); + setBorder(new EmptyBorder(5, 5, 5, 5)); + setBackground(new Color(245, 247, 250)); + + add(buildTopPanel(), BorderLayout.NORTH); + + eventsContainer.setLayout(new BoxLayout(eventsContainer, BoxLayout.Y_AXIS)); + eventsContainer.setBorder(new EmptyBorder(10, 10, 10, 10)); + eventsContainer.setBackground(new Color(245, 247, 250)); + + scrollPane = new JScrollPane(eventsContainer); + scrollPane.getVerticalScrollBar().setUnitIncrement(16); + scrollPane.setBorder(null); + add(scrollPane, BorderLayout.CENTER); + + } + + private JPanel buildTopPanel() { + JPanel topPanel = new JPanel(new BorderLayout()); + topPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + topPanel.setBackground(new Color(25, 118, 210)); + + backButton.setFocusPainted(false); + backButton.setContentAreaFilled(true); + backButton.setOpaque(true); + backButton.setBackground(Color.WHITE); + backButton.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(220, 220, 220)), + new EmptyBorder(6, 12, 6, 12) + ) + ); + backButton.setFont(new Font(textFont, Font.PLAIN, 12)); + topPanel.add(backButton, BorderLayout.WEST); + + JPanel centerPanel = new JPanel(); + centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS)); + centerPanel.setOpaque(false); + + titleLabel.setFont(new Font(textFont, Font.BOLD, 24)); + titleLabel.setForeground(Color.WHITE); + titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + + dateLabel.setFont(new Font(textFont, Font.PLAIN, 16)); + dateLabel.setForeground(Color.WHITE); + dateLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + + centerPanel.add(titleLabel); + centerPanel.add(Box.createVerticalStrut(5));//space between two subpanel + centerPanel.add(dateLabel); + topPanel.add(centerPanel, BorderLayout.CENTER); + + return topPanel; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + CalendarFlowState state = (CalendarFlowState) evt.getNewValue(); + + if (state.getDate() != null) { + // Update date label + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy", Locale.ENGLISH); + dateLabel.setText(state.getDate().format(formatter)); + } + + if (state.getEventList() != null && !state.getEventList().isEmpty()) { + eventListsUI(state.getEventList()); + } else if (state.getErrorMessage() != null) { + renderError(state.getErrorMessage()); + } +// else { +// noEventUI(); +// } + } + +// private void noEventUI() { +// eventsContainer.removeAll(); +// eventsContainer.setLayout(new BorderLayout()); +// +// emptyStateLabel.setFont(new Font(textFont, Font.PLAIN, 16)); +// emptyStateLabel.setForeground(new Color(120, 120, 120)); +// +// eventsContainer.add(emptyStateLabel, BorderLayout.CENTER); +// eventsContainer.revalidate(); // re-run layout +// eventsContainer.repaint(); // redraw the graphics +// +// } + + private void renderError(String errorMessage) { + eventsContainer.removeAll(); + eventsContainer.setLayout(new BorderLayout()); + + JLabel errorLabel = new JLabel(errorMessage, SwingConstants.CENTER); + errorLabel.setFont(errorLabel.getFont().deriveFont(Font.PLAIN, 16f)); +// errorLabel.setForeground(new Color(211, 47, 47)); + emptyStateLabel.setForeground(new Color(120, 120, 120)); + + eventsContainer.add(errorLabel, BorderLayout.CENTER); + eventsContainer.revalidate(); + eventsContainer.repaint(); + } + + private void eventListsUI(List events) { + eventsContainer.removeAll(); + eventsContainer.setLayout(new BoxLayout(eventsContainer, BoxLayout.Y_AXIS)); + + for (Event event : events) { + JPanel eventCard = buildEventCard(event); + eventsContainer.add(eventCard); + eventsContainer.add(Box.createVerticalStrut(10)); // Spacing between cards + } + + eventsContainer.revalidate(); + eventsContainer.repaint(); + } + + private JPanel buildEventCard(Event event) { + JPanel card = new JPanel(new BorderLayout()); + card.setMaximumSize(new Dimension(Integer.MAX_VALUE, 80)); + card.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(230, 230, 230), 1), + new EmptyBorder(12, 15, 12, 15) + )); + card.setBackground(Color.WHITE); + + JPanel infoPanel = new JPanel(); + infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.Y_AXIS)); + infoPanel.setOpaque(false); + + JLabel nameLabel = new JLabel(event.getName()); + nameLabel.setFont(nameLabel.getFont().deriveFont(Font.BOLD, 15f)); + nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + JLabel locationLabel = new JLabel(event.getLocation().getAddress()); + locationLabel.setFont(new Font(textFont, Font.PLAIN, 12)); + locationLabel.setForeground(new Color(100, 100, 100)); + locationLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("h:mm a", Locale.ENGLISH); + String timeText = event.getStartTime().format(timeFormatter); + JLabel timeLabel = new JLabel(timeText); + timeLabel.setFont(new Font(textFont, Font.PLAIN, 12)); + timeLabel.setForeground(new Color(33, 150, 243)); + timeLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + infoPanel.add(nameLabel); + infoPanel.add(Box.createVerticalStrut(4)); + infoPanel.add(locationLabel); + infoPanel.add(Box.createVerticalStrut(4)); + infoPanel.add(timeLabel); + + card.add(infoPanel, BorderLayout.CENTER); + + //button that links to Joy's usecase + JButton exploreButton = new JButton("→"); + exploreButton.setFont(new Font(textFont, Font.BOLD, 20)); + exploreButton.setFocusPainted(false); + + exploreButton.setOpaque(true); + exploreButton.setContentAreaFilled(true); + exploreButton.setBorderPainted(false); + + exploreButton.setBackground(new Color(25, 118, 210)); + exploreButton.setForeground(Color.WHITE); + exploreButton.setBorder(new EmptyBorder(5, 20, 5, 20)); + exploreButton.setPreferredSize(new Dimension(60, 60)); + + //Place to links + exploreButton.addActionListener(e -> { + // For now, just print to test + System.out.println("Explore event: " + event.getName()); + }); + + JPanel arrowWrapper = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0)); + arrowWrapper.setOpaque(false); + arrowWrapper.add(exploreButton); + + card.add(arrowWrapper, BorderLayout.EAST); +// card.add(exploreButton, BorderLayout.EAST); + + return card; + } + + public String getViewName() { + return viewName; + } + + /** + * Sets the action for the back button + * @param listener the action listener + * implement by i.e. eventListView.setBackButtonAction(e -> viewManager.show("view name")); + */ + public void setBackButtonAction(java.awt.event.ActionListener listener) { + // Remove existing listeners + for (var l : backButton.getActionListeners()) { + backButton.removeActionListener(l); + } + backButton.addActionListener(listener); + } + +} 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..3cca39f9 100644 --- a/src/main/java/view/LoginView.java +++ b/src/main/java/view/LoginView.java @@ -3,6 +3,7 @@ import interface_adapter.login.LoginController; import interface_adapter.login.LoginState; import interface_adapter.login.LoginViewModel; +import interface_adapter.signup.SignupViewModel; import javax.swing.*; import javax.swing.event.DocumentEvent; @@ -33,21 +34,40 @@ public class LoginView extends JPanel implements ActionListener, PropertyChangeL public LoginView(LoginViewModel loginViewModel) { + this.setLayout(new GridLayout(1, 2)); this.loginViewModel = loginViewModel; this.loginViewModel.addPropertyChangeListener(this); - final JLabel title = new JLabel("Login Screen"); + JPanel leftPanel = createBrandPanel(); + 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(LoginViewModel.TITLE_LABEL); title.setAlignmentX(Component.CENTER_ALIGNMENT); + title.setFont(new Font("SegoeUI", Font.BOLD, 36)); + + JLabel subtitleLabel = new JLabel(LoginViewModel.CAPTION); + 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("Username"), usernameInputField); + usernameInfo.setAlignmentX(Component.CENTER_ALIGNMENT); final LabelTextPanel passwordInfo = new LabelTextPanel( new JLabel("Password"), passwordInputField); + passwordInfo.setAlignmentX(Component.CENTER_ALIGNMENT); final JPanel buttons = new JPanel(); - logIn = new JButton("log in"); + buttons.setAlignmentX(Component.CENTER_ALIGNMENT); + logIn = new JButton("Log In"); buttons.add(logIn); - cancel = new JButton("cancel"); + cancel = new JButton("Back"); + + styleButton(logIn, new Color(0,0,0,0)); + styleButton(cancel, new Color(0,0,0,0)); buttons.add(cancel); logIn.addActionListener( @@ -55,7 +75,6 @@ public LoginView(LoginViewModel loginViewModel) { public void actionPerformed(ActionEvent evt) { if (evt.getSource().equals(logIn)) { final LoginState currentState = loginViewModel.getState(); - loginController.execute( currentState.getUsername(), currentState.getPassword() @@ -65,7 +84,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() { @@ -91,8 +117,6 @@ public void changedUpdate(DocumentEvent e) { } }); - this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - passwordInputField.getDocument().addDocumentListener(new DocumentListener() { private void documentListenerHelper() { @@ -117,13 +141,96 @@ public void changedUpdate(DocumentEvent e) { } }); - this.add(title); - this.add(usernameInfo); - this.add(usernameErrorField); - this.add(passwordInfo); - this.add(buttons); + 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(buttons); + + rightPanel.add(titlePanel); + rightPanel.add(Box.createRigidArea(new Dimension(0, 20))); + rightPanel.add(formPanel); + + leftPanel.setPreferredSize(new Dimension(400, 600)); + rightPanel.setPreferredSize(new Dimension(400, 600)); + + this.add(leftPanel); + this.add(rightPanel); } + private JPanel createBrandPanel() { + 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; + } + + 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)); + } + + /** * React to a button click that results in evt. * @param evt the ActionEvent to react to diff --git a/src/main/java/view/SaveButtonView.java b/src/main/java/view/SaveButtonView.java new file mode 100644 index 00000000..0f8ef57a --- /dev/null +++ b/src/main/java/view/SaveButtonView.java @@ -0,0 +1,129 @@ +package view; + +import entity.Event; +import interface_adapter.save_event.SaveEventController; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +public class SaveButtonView extends JPanel { + + private final String viewName = "save"; + private JButton saveButton = null; + private Event event; + private SaveEventController saveButtonController = null; + private Timer resetTimer; + + + public SaveButtonView() { + Color base = new Color(59, 130, 246); + + saveButton = new JButton("Save Event"); + saveButton.setFont(new Font("SegoeUI", Font.BOLD, 14)); + saveButton.setForeground(new Color(59, 130, 246)); + saveButton.setBackground(Color.WHITE); + saveButton.setBorder(BorderFactory.createLineBorder(base, 3)); + saveButton.setPreferredSize(new Dimension(160, 45)); + + saveButton.setContentAreaFilled(false); + saveButton.setOpaque(true); + + addActionListeners(); + + add(saveButton); + + + } + + private void addActionListeners() { + Color base = new Color(59, 130, 246); + Color pressedBackground = new Color(37, 99, 235); + + + saveButton.addMouseListener(new MouseAdapter() { + @Override + public void mouseEntered(MouseEvent e) { + saveButton.setBackground(base); + saveButton.setForeground(Color.WHITE); + saveButton.setBorder(BorderFactory.createLineBorder(Color.WHITE, 3)); + } + + @Override + public void mouseExited(MouseEvent e) { + saveButton.setBackground(Color.WHITE); + saveButton.setForeground(base); + } + + @Override + public void mousePressed(MouseEvent e) { + // Visual feedback when pressed + saveButton.setBackground(pressedBackground); + saveButton.setBorder(BorderFactory.createLineBorder(pressedBackground, 2)); + } + + @Override + public void mouseReleased(MouseEvent e) { + // Return to hover state if still hovering + if (saveButton.contains(e.getPoint())) { + saveButton.setBackground(base); + saveButton.setBorder(BorderFactory.createLineBorder(base, 2)); + } else { + saveButton.setBackground(Color.WHITE); + saveButton.setForeground(base); + saveButton.setBorder(BorderFactory.createLineBorder(base, 2)); + } + } + }); + + saveButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (saveButtonController != null && event != null) { + System.out.println("YIPPEE"); + saveButtonController.saveEvent(event); + saveButton.setText("Saved"); + saveButton.setEnabled(false); + + if (resetTimer != null && resetTimer.isRunning()) { + resetTimer.stop(); + } + + resetTimer = new Timer(10000, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + resetButtonState(); + } + }); + resetTimer.setRepeats(false); // Only run once + resetTimer.start(); + + } + } + }); + } + + private void resetButtonState() { + if (saveButton != null) { + saveButton.setText("Save Event"); + saveButton.setEnabled(true); + + Color base = new Color(59, 130, 246); + saveButton.setForeground(base); + saveButton.setBackground(Color.WHITE); + saveButton.setBorder(BorderFactory.createLineBorder(base, 3)); + } + } + + public void setEvent(Event event) { + this.event = event; + } + + public void setSaveEventController(SaveEventController controller) { + this.saveButtonController = controller; + } + +} \ No newline at end of file diff --git a/src/main/java/view/SaveEventsView.java b/src/main/java/view/SaveEventsView.java new file mode 100644 index 00000000..62c86f3e --- /dev/null +++ b/src/main/java/view/SaveEventsView.java @@ -0,0 +1,283 @@ +package view; + +import entity.Event; +import entity.EventCategory; +import interface_adapter.save_event.SaveEventController; +import interface_adapter.save_event.SaveEventViewModel; +import use_case.save_event.SaveEventInteractor; + +import javax.swing.*; +import javax.swing.border.*; +import java.awt.*; +import java.awt.event.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.List; + +public class SaveEventsView extends JPanel implements PropertyChangeListener { + private String viewName = "save events"; + private SaveEventViewModel saveEventsViewModel = null; + private List savedEvents = new ArrayList<>(); + private JPanel eventsContainer; + private SaveEventController saveEventController = null; + private SaveEventInteractor saveEventInteractor = null; + + public SaveEventsView(SaveEventViewModel saveEventsViewModel) { + this.saveEventsViewModel = saveEventsViewModel; + saveEventsViewModel.addPropertyChangeListener(this); + + initializeUI(); + } + + private void initializeUI() { + setLayout(new BorderLayout()); + setBackground(new Color(249, 250, 251)); + + JPanel header = createHeader(); + add(header, BorderLayout.NORTH); + + eventsContainer = new JPanel(); + eventsContainer.setLayout(new BoxLayout(eventsContainer, BoxLayout.Y_AXIS)); + eventsContainer.setBackground(new Color(249, 250, 251)); + eventsContainer.setBorder(new EmptyBorder(20, 40, 20, 40)); + + JScrollPane scrollPane = new JScrollPane(eventsContainer); + scrollPane.setBorder(null); + scrollPane.getVerticalScrollBar().setUnitIncrement(16); + add(scrollPane); + + } + + private JPanel createHeader() { + JPanel header = new JPanel(new BorderLayout()); + header.setBackground(new Color(13, 133, 251)); + header.setBorder(new CompoundBorder( + new MatteBorder(0, 0, 1, 0, new Color(10, 103, 198)), + new EmptyBorder(20, 40, 20, 40) + )); + + JLabel title = new JLabel("My Saved Events"); + + title.setFont(new Font("SegoeUI", Font.BOLD, 24)); + title.setForeground(Color.WHITE); + title.setOpaque(false); + + JButton backButton = new JButton("← Back to Events"); + backButton.setFont(new Font("SegoeUI", Font.PLAIN, 13)); + backButton.setAlignmentX(Component.LEFT_ALIGNMENT); + + backButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + saveEventController.switchToDashboardView(); + } + }); + + header.add(title, BorderLayout.WEST); + header.add(backButton, BorderLayout.EAST); + return header; + + } + + public void refreshEventsList() { + eventsContainer.removeAll(); + + List savedEvents = saveEventInteractor.getSavedEvents(); + + + if (savedEvents.isEmpty()) { + JPanel emptyPanel = new JPanel(new FlowLayout()); + emptyPanel.setBackground(new Color(249, 250, 251)); + + JLabel emptyLabel = new JLabel("No saved events yet. Start exploring and save events you're interested in!"); + emptyLabel.setFont(new Font("Segoe UI", Font.PLAIN, 16)); + emptyLabel.setForeground(new Color(107, 114, 128)); + + emptyPanel.add(emptyLabel); + eventsContainer.add(emptyPanel); + } else { + for (Event event : savedEvents) { + JPanel eventCard = createEventCard(event); + eventsContainer.add(eventCard); + eventsContainer.add(Box.createVerticalStrut(15)); + } + } + + eventsContainer.revalidate(); + eventsContainer.repaint(); + } + + private JPanel createEventCard(Event event) { + JPanel card = new JPanel(new BorderLayout(20, 0)); + card.setBackground(Color.WHITE); + card.setBorder(new CompoundBorder( + new LineBorder(new Color(229, 231, 235), 1, true), + new EmptyBorder(20, 20, 20, 20) + )); + card.setMaximumSize(new Dimension(Integer.MAX_VALUE, 140)); + // Written with the help of generative AI + JPanel imagePanel = new JPanel() { + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + Color categoryColor = getCategoryColor(event.getCategory()); + GradientPaint gradient = new GradientPaint( + 0, 0, categoryColor.darker(), + getWidth(), getHeight(), categoryColor + ); + g2.setPaint(gradient); + g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12); + + g2.setColor(Color.WHITE); + g2.setFont(new Font("Segoe UI", Font.BOLD, 36)); + FontMetrics fm = g2.getFontMetrics(); + String icon = "Event"; + int x = (getWidth() - fm.stringWidth(icon)) / 2; + int y = (getHeight() + fm.getAscent()) / 2 - 4; + g2.drawString(icon, x, y); + } + }; + imagePanel.setPreferredSize(new Dimension(100, 100)); + imagePanel.setOpaque(false); + + JPanel detailsPanel = new JPanel(); + detailsPanel.setLayout(new BoxLayout(detailsPanel, BoxLayout.Y_AXIS)); + detailsPanel.setOpaque(false); + + Color categoryColor = getCategoryColor(event.getCategory()); + JLabel categoryLabel = new JLabel(event.getCategory().toString()); + categoryLabel.setFont(new Font("Segoe UI", Font.BOLD, 11)); + categoryLabel.setForeground(categoryColor); + categoryLabel.setOpaque(true); + categoryLabel.setBackground(new Color(categoryColor.getRed(), + categoryColor.getGreen(), + categoryColor.getBlue(), 40)); + categoryLabel.setBorder(new EmptyBorder(4, 10, 4, 10)); + categoryLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + JLabel nameLabel = new JLabel(event.getName()); + nameLabel.setFont(new Font("Segoe UI", Font.BOLD, 18)); + nameLabel.setForeground(new Color(17, 24, 39)); + nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + JLabel dateLabel = new JLabel("Date: " + event.getStartTime().toString()); + dateLabel.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + dateLabel.setForeground(new Color(107, 114, 128)); + dateLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + // Location + JLabel locationLabel = new JLabel("Venue: " + event.getLocation().getAddress()); + locationLabel.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + locationLabel.setForeground(new Color(107, 114, 128)); + locationLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + detailsPanel.add(categoryLabel); + detailsPanel.add(Box.createVerticalStrut(8)); + detailsPanel.add(nameLabel); + detailsPanel.add(Box.createVerticalStrut(6)); + detailsPanel.add(dateLabel); + detailsPanel.add(Box.createVerticalStrut(4)); + detailsPanel.add(locationLabel); + + JPanel actionsPanel = new JPanel(); + actionsPanel.setLayout(new BoxLayout(actionsPanel, BoxLayout.Y_AXIS)); + actionsPanel.setOpaque(false); + + + JButton removeButton = createActionButton("Remove", new Color(239, 68, 68)); + removeButton.setBorder(new CompoundBorder( + new LineBorder(new Color(229, 231, 235), 1, true), + new EmptyBorder(5, 20, 5, 20) + )); + + removeButton.addActionListener(e -> { + int result = JOptionPane.showConfirmDialog(this, + "Remove \"" + event.getName() + "\" from saved events?", + "Confirm Removal", + JOptionPane.YES_NO_OPTION); + + if (result == JOptionPane.YES_OPTION) { + saveEventInteractor.removeEvent(event); + refreshEventsList(); + } + }); + + actionsPanel.add(Box.createVerticalStrut(35)); + actionsPanel.add(removeButton); + + card.add(imagePanel, BorderLayout.WEST); + card.add(detailsPanel, BorderLayout.CENTER); + card.add(actionsPanel, BorderLayout.EAST); + + return card; + } + + private JButton createActionButton(String text, Color color) { + JButton button = new JButton(text); + button.setFont(new Font("Segoe UI", Font.BOLD, 12)); + button.setForeground(color); + button.setBackground(Color.WHITE); + button.setBorder(BorderFactory.createEmptyBorder(8, 16, 8, 16)); + button.setFocusPainted(false); + button.setCursor(new Cursor(Cursor.HAND_CURSOR)); + + button.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseEntered(java.awt.event.MouseEvent evt) { + button.setBackground(color.darker()); + } + + public void mouseExited(java.awt.event.MouseEvent evt) { + button.setBackground(color); + } + }); + + return button; + } + + private Color getCategoryColor(EventCategory category) { + switch (category) { + case MUSIC: + return new Color(147, 51, 234); // Purple + case SPORTS: + return new Color(59, 130, 246); // Blue + case ARTS_THEATRE: + return new Color(236, 72, 153); // Pink + case MISCELLANEOUS: + default: + return new Color(107, 114, 128); // Gray + } + } + + public void addSavedEvent(Event event) { + if (!savedEvents.contains(event)) { + savedEvents.add(event); + refreshEventsList(); + } + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if ("event".equals(evt.getPropertyName())) { + refreshEventsList(); + } else if ("error".equals(evt.getPropertyName())) { + System.out.println("Event was null, floundering..."); + } + } + + public void setSaveEventInteractor(SaveEventInteractor interactor) { + this.saveEventInteractor = interactor; + } + + public void setSaveEventController(SaveEventController saveEventController) { + this.saveEventController = saveEventController; + } + + public String getViewName() { + return viewName; + } + +} diff --git a/src/main/java/view/SearchBarView.java b/src/main/java/view/SearchBarView.java new file mode 100644 index 00000000..a6d21542 --- /dev/null +++ b/src/main/java/view/SearchBarView.java @@ -0,0 +1,117 @@ +package view; + +import entity.Location; +import interface_adapter.search.SearchController; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; + +public class SearchBarView extends JPanel { + private JTextField searchField; + private Color borderColor = new Color(220, 220, 220); + private Color focusBorderColor = new Color(180, 180, 180); + private Color backgroundColor = new Color(250, 250, 250); + private boolean isFocused = false; + private String searchDescription = null; + private SearchController searchController = null; + private Location location = null; + + + public SearchBarView(String text, Location userLocation) { + setLayout(new BorderLayout()); + setPreferredSize(new Dimension(600, 30)); + setOpaque(false); + this.searchDescription = text; + this.location = userLocation; + add(initComponents(searchDescription)); + } + + private JPanel initComponents(String text) { + JPanel container = new JPanel(new BorderLayout()) { + // Written with the help of generative AI + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g2.setColor(new Color(0, 0, 0, 15)); + g2.fillRoundRect(1, 3, getWidth() - 2, getHeight() - 3, 30, 30); + + g2.setColor(backgroundColor); + g2.fillRoundRect(0, 0, getWidth() - 2, getHeight() - 4, 30, 30); + + g2.setColor(isFocused ? focusBorderColor : borderColor); + g2.setStroke(new BasicStroke(1.5f)); + g2.drawRoundRect(0, 0, getWidth() - 2, getHeight() - 4, 30, 30); + + g2.dispose(); + } + }; + container.setOpaque(false); + container.setBorder(new EmptyBorder(8, 20, 8, 20)); + + searchField = new JTextField() { + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + if (getText().isEmpty() && !isFocused) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setColor(new Color(170, 170, 170)); + g2.setFont(getFont()); + g2.drawString(text, 5, getHeight() / 2 + 6); + g2.dispose(); + } + } + }; + searchField.setFont(new Font("Segoe UI", Font.PLAIN, 18)); + searchField.setBorder(new EmptyBorder(8, 5, 8, 5)); + searchField.setOpaque(false); + searchField.setForeground(new Color(100, 100, 100)); + searchField.setBackground(new Color(0, 0, 0, 0)); + + searchField.addFocusListener(new FocusAdapter() { + public void focusGained(FocusEvent e) { + isFocused = false; + container.repaint(); + } + + public void focusLost(FocusEvent e) { + isFocused = false; + container.repaint(); + } + }); + + searchField.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (searchController != null) { + String query = searchField.getText().trim(); + if (!query.isEmpty()) { + // Execute search with "name" as search type: currently has a default Location + searchController.execute(query, "name", location); + } + } + } + }); + + container.add(searchField, BorderLayout.CENTER); + return container; + } + + public String getSearchText() { + return searchField.getText(); + } + + public void setSearchController(SearchController controller) { + this.searchController = controller; + } + + + +} diff --git a/src/main/java/view/SearchEventByNameView.java b/src/main/java/view/SearchEventByNameView.java new file mode 100644 index 00000000..af829598 --- /dev/null +++ b/src/main/java/view/SearchEventByNameView.java @@ -0,0 +1,232 @@ +package view; + +import javax.swing.*; + +import interface_adapter.save_event.SaveEventController; +import interface_adapter.search_event_by_name.SearchEventByNameController; +import interface_adapter.search_event_by_name.SearchEventByNameViewModel; +import entity.Event; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.net.URL; +import java.time.format.DateTimeFormatter; + +public class SearchEventByNameView extends JPanel implements PropertyChangeListener { + + private final String viewName = "event search"; + private final SearchEventByNameViewModel searchEventByNameViewModel; + private SearchEventByNameController eventController = null; + + // Components that need to be updated + private JLabel categoryLabel; + private JLabel nameLabel; + private JLabel dateLabel; + private JLabel venueLabel; + private JTextArea descArea; + private JPanel imagePanel; + private SaveButtonView saveButtonView; + private SaveEventController saveEventController; + + public SearchEventByNameView(SearchEventByNameViewModel searchEventByNameViewModel) { + this.searchEventByNameViewModel = searchEventByNameViewModel; + this.searchEventByNameViewModel.addPropertyChangeListener(this); + this.setLayout(new BorderLayout()); + + JPanel detailsPanel = createDetailsPanel(); + add(detailsPanel, BorderLayout.CENTER); + + imagePanel = createImagePanel(); + imagePanel = createImagePanel(); + add(imagePanel, BorderLayout.EAST); + + // Initial update with current state + updateView(); + } + + private JPanel createDetailsPanel() { + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); + mainPanel.setForeground(Color.WHITE); + mainPanel.setBorder(BorderFactory.createEmptyBorder(30, 30, 30, 30)); + mainPanel.setPreferredSize(new Dimension(400, 700)); + + JButton backButton = new JButton("← Back to Events"); + backButton.setFont(new Font("SegoeUI", Font.PLAIN, 13)); + backButton.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(backButton); + mainPanel.add(Box.createVerticalStrut(24)); + + backButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (eventController != null) { + eventController.switchToDashboardView(); + } + } + }); + + categoryLabel = new JLabel("Loading"); + categoryLabel.setFont(new Font("SegoeUI", Font.BOLD, 11)); + categoryLabel.setForeground(new Color(59, 130, 246)); + categoryLabel.setOpaque(true); + categoryLabel.setBackground(new Color(219, 234, 254)); + categoryLabel.setBorder(BorderFactory.createEmptyBorder(5, 12, 5, 12)); + categoryLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(categoryLabel); + mainPanel.add(Box.createVerticalStrut(12)); + + nameLabel = new JLabel("Loading Event..."); + nameLabel.setFont(new Font("SegoeUI", Font.BOLD, 32)); + nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(nameLabel); + mainPanel.add(Box.createVerticalStrut(12)); + + JPanel dateRow = createRow("Date:", ""); + dateLabel = (JLabel) dateRow.getComponent(1); + mainPanel.add(dateRow); + mainPanel.add(Box.createVerticalStrut(12)); + + JPanel venueRow = createRow("Venue:", ""); + venueLabel = (JLabel) venueRow.getComponent(1); + mainPanel.add(venueRow); + mainPanel.add(Box.createVerticalStrut(12)); + + JLabel descTitle = new JLabel("About this event"); + descTitle.setFont(new Font("SegoeUI", Font.BOLD, 16)); + descTitle.setForeground(new Color(20, 20, 20)); + descTitle.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(descTitle); + mainPanel.add(Box.createVerticalStrut(12)); + + descArea = new JTextArea("Loading description..."); + descArea.setFont(new Font("SegoeUI", Font.PLAIN, 14)); + descArea.setForeground(new Color(80, 80, 80)); + descArea.setBackground(Color.WHITE); + descArea.setLineWrap(true); + descArea.setWrapStyleWord(true); + descArea.setEditable(false); + descArea.setBorder(null); + descArea.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(descArea); + mainPanel.add(Box.createVerticalStrut(30)); + + saveButtonView = new SaveButtonView(); + saveButtonView.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(saveButtonView); + + JScrollPane scrollPane = new JScrollPane(mainPanel); + scrollPane.setBorder(null); + scrollPane.getVerticalScrollBar().setUnitIncrement(16); + + JPanel wrapper = new JPanel(new BorderLayout()); + wrapper.add(scrollPane); + + return wrapper; + } + + private JPanel createImagePanel() { + JPanel mainPanel = new JPanel(new BorderLayout()); + mainPanel.setPreferredSize(new Dimension(500, 700)); + mainPanel.setBackground(new Color(30, 30, 30)); + + JLabel placeholderLabel = new JLabel("Loading", SwingConstants.CENTER); + placeholderLabel.setForeground(Color.WHITE); + mainPanel.add(placeholderLabel, BorderLayout.CENTER); + + return mainPanel; + } + + private void updateImagePanel(String imageUrl) { + imagePanel.removeAll(); + + try { + URL url = new URL(imageUrl); + ImageIcon icon = new ImageIcon(url); + Image image = icon.getImage().getScaledInstance(500, 700, Image.SCALE_SMOOTH); + JLabel imageLabel = new JLabel(new ImageIcon(image)); + imagePanel.add(imageLabel, BorderLayout.CENTER); + } catch (Exception e) { + JLabel placeholderLabel = new JLabel("No Image Available", SwingConstants.CENTER); + placeholderLabel.setForeground(Color.WHITE); + imagePanel.add(placeholderLabel, BorderLayout.CENTER); + } + + imagePanel.revalidate(); + imagePanel.repaint(); + } + + private JPanel createRow(String category, String text) { + JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + row.setOpaque(false); + row.setAlignmentX(Component.LEFT_ALIGNMENT); + row.setMaximumSize(new Dimension(400, 30)); + + JLabel title = new JLabel(category); + title.setFont(new Font("SegoeUI", Font.BOLD, 14)); + title.setForeground(new Color(50, 50, 50)); + + JLabel description = new JLabel(text); + description.setFont(new Font("SegoeUI", Font.PLAIN, 14)); + description.setForeground(new Color(50, 50, 50)); + + row.add(title); + row.add(description); + + return row; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + updateView(); + } + + private void updateView() { + Event event = searchEventByNameViewModel.getEvent(); + + if (event != null) { + categoryLabel.setText(event.getCategory().getDisplayName()); + + nameLabel.setText("" + event.getName().replace(" ", "
") + ""); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM dd, yyyy"); + dateLabel.setText(event.getStartTime().format(formatter)); + + + String venueText = event.getLocation().getAddress(); + if (venueText.length() > 50) { + venueText = venueText.substring(0, 47) + "..."; + } + venueLabel.setText(venueText); + + String desc = event.getDescription(); + descArea.setText(desc.isEmpty() ? "No description available" : desc); + + updateImagePanel(event.getImageUrl()); + + saveButtonView.setEvent(event); + saveButtonView.setSaveEventController(saveEventController); + + } + } + + public String getViewName() { + return viewName; + } + + + + public void setEventController(SearchEventByNameController controller) { + this.eventController = controller; + } + + public void setSaveButtonController(SaveEventController eventController) { + this.saveButtonView.setSaveEventController(eventController); + this.saveEventController = eventController; + } + +} + diff --git a/src/main/java/view/SignupView.java b/src/main/java/view/SignupView.java index 1330c097..7da3efcf 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(SignupViewModel.TITLE_CAPTION); + 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,107 @@ 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 + * Written with the help of Generative AI + */ + 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/main/resources/images/placeholder.jpg b/src/main/resources/images/placeholder.jpg new file mode 100644 index 00000000..c44ebc89 Binary files /dev/null and b/src/main/resources/images/placeholder.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/demo/NoDataAccessEventViewDemo.java b/src/test/java/demo/NoDataAccessEventViewDemo.java new file mode 100644 index 00000000..21658221 --- /dev/null +++ b/src/test/java/demo/NoDataAccessEventViewDemo.java @@ -0,0 +1,92 @@ +package demo; + +import interface_adapter.login.LoginViewModel; +import use_case.search_event_by_name.SearchEventByNameDataAccessInterface; +import entity.Event; +import entity.Location; +import interface_adapter.ViewManagerModel; +import interface_adapter.search_event_by_name.SearchEventByNameController; +import interface_adapter.search_event_by_name.SearchEventByNamePresenter; +import interface_adapter.search_event_by_name.SearchEventByNameViewModel; +import use_case.search_event_by_name.SearchEventByNameInputBoundary; +import use_case.search_event_by_name.SearchEventByNameInteractor; +import use_case.search_event_by_name.SearchEventByNameOutputBoundary; +import view.SearchEventByNameView; + +import javax.swing.*; +import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.List; + +public class NoDataAccessEventViewDemo { + public static void main(String[] args) { + JFrame frame = new JFrame("Event View Test"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setLayout(new CardLayout()); + + final ViewManagerModel viewManagerModel = new ViewManagerModel(); + final LoginViewModel loginViewModel = new LoginViewModel(); + + // Create a simple login view for testing + JPanel loginView = new JPanel(); + loginView.setBackground(Color.BLUE); + loginView.add(new JLabel("Login View - Back button worked!")); + + // Create mock data access + SearchEventByNameDataAccessInterface mockDataAccess = new SearchEventByNameDataAccessInterface() { + @Override + public List searchEventsByName(String keyword, Location location, double radiusKm) { + return new ArrayList<>(); + } + + + public Event getEventByName(String eventName) { + return null; + } + + + public List searchEvents(String query) { + return new ArrayList<>(); + } + }; + + // Create the search event view + final SearchEventByNameViewModel viewModel = new SearchEventByNameViewModel(); + final SearchEventByNameOutputBoundary outputBoundary = new SearchEventByNamePresenter(viewModel, viewManagerModel); + final SearchEventByNameInputBoundary inputBoundary = new SearchEventByNameInteractor(mockDataAccess, outputBoundary); + final SearchEventByNameController eventController = new SearchEventByNameController(inputBoundary); + + SearchEventByNameView eventView = new SearchEventByNameView(viewModel); + eventView.setEventController(eventController); + + // Add both views to the frame with CardLayout + frame.add(eventView, "eventView"); + frame.add(loginView, "loginView"); + + // Set up ViewManagerModel listener to switch views + viewManagerModel.addPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + if ("state".equals(evt.getPropertyName())) { + CardLayout cardLayout = (CardLayout) frame.getContentPane().getLayout(); + String newView = (String) evt.getNewValue(); + cardLayout.show(frame.getContentPane(), newView); + frame.revalidate(); + frame.repaint(); + } + } + }); + + // Set initial view + viewManagerModel.setState(""); + viewManagerModel.firePropertyChange(); + + frame.setSize(900, 700); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + + System.out.println("Testing switchToDashboardView..."); + } +} \ No newline at end of file diff --git a/src/test/java/demo/SaveButtonViewDemo.java b/src/test/java/demo/SaveButtonViewDemo.java new file mode 100644 index 00000000..d69186f5 --- /dev/null +++ b/src/test/java/demo/SaveButtonViewDemo.java @@ -0,0 +1,18 @@ +package demo; + +import view.SaveButtonView; + +import javax.swing.*; + +public class SaveButtonViewDemo { + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + JFrame frame = new JFrame("SaveButtonView Demo"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.add(new SaveButtonView()); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + }); + } +} diff --git a/src/test/java/demo/SaveEventViewDemo.java b/src/test/java/demo/SaveEventViewDemo.java new file mode 100644 index 00000000..7fe8fd0f --- /dev/null +++ b/src/test/java/demo/SaveEventViewDemo.java @@ -0,0 +1,327 @@ +package demo; + +import data_access.FileSavedEventsDataAccessObject; +import entity.Event; +import entity.EventCategory; +import entity.Location; +import interface_adapter.ViewManagerModel; +import interface_adapter.save_event.SaveEventController; +import interface_adapter.save_event.SaveEventPresenter; +import interface_adapter.save_event.SaveEventViewModel; +import use_case.login.LoginUserDataAccessInterface; +import use_case.save_event.SaveEventInteractor; +import view.SaveButtonView; +import view.SaveEventsView; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.time.LocalDateTime; + +/** + * Visual demo to test the SaveButtonView and SaveEventsView integration with file persistence + * Run this to see the complete save event workflow with data persistence + * WRITTEN BY GENAI for DEMO purposes only + */ +public class SaveEventViewDemo { + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + // Create the main frame + JFrame frame = new JFrame("Save Event Feature Demo (With Persistence!)"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setSize(1200, 800); + frame.setLocationRelativeTo(null); + + // Create a mock user data access with a demo user + MockUserDataAccess userDataAccess = new MockUserDataAccess(); + userDataAccess.setCurrentUsername("demo_user"); // Fallback user for demo + + // Create the file-based saved events DAO + FileSavedEventsDataAccessObject savedEventsDAO = new FileSavedEventsDataAccessObject(); + + // Set up the architecture components + SaveEventViewModel saveEventViewModel = new SaveEventViewModel(); + ViewManagerModel viewManagerModel = new ViewManagerModel(); + SaveEventPresenter presenter = new SaveEventPresenter(saveEventViewModel, viewManagerModel); + SaveEventInteractor interactor = new SaveEventInteractor(presenter, savedEventsDAO, userDataAccess); + SaveEventController controller = new SaveEventController(interactor); + + // Create views + SaveEventsView saveEventsView = new SaveEventsView(saveEventViewModel); + saveEventsView.setSaveEventInteractor(interactor); + saveEventsView.setSaveEventController(controller); + + // Create main panel with CardLayout for switching views + CardLayout cardLayout = new CardLayout(); + JPanel mainPanel = new JPanel(cardLayout); + + // Create the event browsing panel + JPanel browsePanel = createBrowsePanel(controller, cardLayout, mainPanel, saveEventsView); + + // Add panels to card layout + mainPanel.add(browsePanel, "browse"); + mainPanel.add(saveEventsView, "saved"); + + viewManagerModel.addPropertyChangeListener(evt -> { + if ("state".equals(evt.getPropertyName())) { + cardLayout.show(mainPanel, "browse"); + } + }); + + frame.add(mainPanel); + frame.setVisible(true); + }); + } + + private static JPanel createBrowsePanel(SaveEventController controller, + CardLayout cardLayout, + JPanel mainPanel, + SaveEventsView saveEventsView) { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBackground(new Color(249, 250, 251)); + + // Header + JPanel header = new JPanel(new BorderLayout()); + header.setBackground(Color.WHITE); + header.setBorder(new EmptyBorder(20, 40, 20, 40)); + + JLabel title = new JLabel("Browse Events (demo_user)"); + title.setFont(new Font("Segoe UI", Font.BOLD, 28)); + title.setForeground(new Color(17, 24, 39)); + + JButton viewSavedButton = new JButton("View My Saved Events →"); + viewSavedButton.setFont(new Font("Segoe UI", Font.BOLD, 14)); + viewSavedButton.setForeground(new Color(59, 130, 246)); + viewSavedButton.setBackground(Color.WHITE); + viewSavedButton.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(59, 130, 246), 2), + BorderFactory.createEmptyBorder(10, 20, 10, 20) + )); + viewSavedButton.setFocusPainted(false); + viewSavedButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + + viewSavedButton.addActionListener(e -> { + saveEventsView.refreshEventsList(); + cardLayout.show(mainPanel, "saved"); + }); + + header.add(title, BorderLayout.WEST); + header.add(viewSavedButton, BorderLayout.EAST); + + // Events container + JPanel eventsContainer = new JPanel(); + eventsContainer.setLayout(new BoxLayout(eventsContainer, BoxLayout.Y_AXIS)); + eventsContainer.setBackground(new Color(249, 250, 251)); + eventsContainer.setBorder(new EmptyBorder(20, 40, 20, 40)); + + // Add sample events + eventsContainer.add(createEventCard(createSampleEvent1(), controller)); + eventsContainer.add(Box.createVerticalStrut(20)); +// eventsContainer.add(createEventCard(createSampleEvent2(), controller)); +// eventsContainer.add(Box.createVerticalStrut(20)); +// eventsContainer.add(createEventCard(createSampleEvent3(), controller)); +// eventsContainer.add(Box.createVerticalStrut(20)); +// eventsContainer.add(createEventCard(createSampleEvent4(), controller)); +// eventsContainer.add(Box.createVerticalStrut(20)); +// eventsContainer.add(createEventCard(createSampleEvent5(), controller)); + + JScrollPane scrollPane = new JScrollPane(eventsContainer); + scrollPane.setBorder(null); + scrollPane.getVerticalScrollBar().setUnitIncrement(16); + + panel.add(header, BorderLayout.NORTH); + panel.add(scrollPane, BorderLayout.CENTER); + + return panel; + } + + private static JPanel createEventCard(Event event, SaveEventController controller) { + JPanel card = new JPanel(new BorderLayout(20, 0)); + card.setBackground(Color.WHITE); + card.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(229, 231, 235), 1), + BorderFactory.createEmptyBorder(20, 20, 20, 20) + )); + card.setMaximumSize(new Dimension(Integer.MAX_VALUE, 180)); + + // Image placeholder + JPanel imagePanel = createImagePanel(event.getCategory()); + imagePanel.setPreferredSize(new Dimension(140, 140)); + + // Details panel + JPanel detailsPanel = new JPanel(); + detailsPanel.setLayout(new BoxLayout(detailsPanel, BoxLayout.Y_AXIS)); + detailsPanel.setOpaque(false); + + // Category badge + JLabel categoryLabel = new JLabel(event.getCategory().toString()); + categoryLabel.setFont(new Font("Segoe UI", Font.BOLD, 11)); + Color categoryColor = getCategoryColor(event.getCategory()); + categoryLabel.setForeground(categoryColor); + categoryLabel.setOpaque(true); + categoryLabel.setBackground(new Color(categoryColor.getRed(), + categoryColor.getGreen(), categoryColor.getBlue(), 40)); + categoryLabel.setBorder(BorderFactory.createEmptyBorder(4, 10, 4, 10)); + categoryLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + // Event name + JLabel nameLabel = new JLabel(event.getName()); + nameLabel.setFont(new Font("Segoe UI", Font.BOLD, 20)); + nameLabel.setForeground(new Color(17, 24, 39)); + nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + // Description + JLabel descLabel = new JLabel("" + truncateText(event.getDescription(), 100) + ""); + descLabel.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + descLabel.setForeground(new Color(107, 114, 128)); + descLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + // Date + JLabel dateLabel = new JLabel("📅 " + formatDate(event.getStartTime())); + dateLabel.setFont(new Font("Segoe UI", Font.PLAIN, 13)); + dateLabel.setForeground(new Color(107, 114, 128)); + dateLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + // Location + JLabel locationLabel = new JLabel("📍 " + event.getLocation().getAddress()); + locationLabel.setFont(new Font("Segoe UI", Font.PLAIN, 13)); + locationLabel.setForeground(new Color(107, 114, 128)); + locationLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + detailsPanel.add(categoryLabel); + detailsPanel.add(Box.createVerticalStrut(10)); + detailsPanel.add(nameLabel); + detailsPanel.add(Box.createVerticalStrut(8)); + detailsPanel.add(descLabel); + detailsPanel.add(Box.createVerticalStrut(10)); + detailsPanel.add(dateLabel); + detailsPanel.add(Box.createVerticalStrut(5)); + detailsPanel.add(locationLabel); + + // Save button + SaveButtonView saveButtonView = new SaveButtonView(); + saveButtonView.setSaveEventController(controller); + saveButtonView.setEvent(event); + saveButtonView.setOpaque(false); + + card.add(imagePanel, BorderLayout.WEST); + card.add(detailsPanel, BorderLayout.CENTER); + card.add(saveButtonView, BorderLayout.EAST); + + return card; + } + + private static JPanel createImagePanel(EventCategory category) { + return new JPanel() { + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + Color categoryColor = getCategoryColor(category); + GradientPaint gradient = new GradientPaint( + 0, 0, categoryColor.darker(), + getWidth(), getHeight(), categoryColor + ); + g2.setPaint(gradient); + g2.fillRoundRect(0, 0, getWidth(), getHeight(), 15, 15); + + // Draw icon + g2.setColor(Color.WHITE); + g2.setFont(new Font("Segoe UI", Font.BOLD, 48)); + String icon = getIconForCategory(category); + FontMetrics fm = g2.getFontMetrics(); + int x = (getWidth() - fm.stringWidth(icon)) / 2; + int y = (getHeight() + fm.getAscent()) / 2 - 5; + g2.drawString(icon, x, y); + } + }; + } + + private static String getIconForCategory(EventCategory category) { + switch (category) { + case MUSIC: return "♪"; + case SPORTS: return "⚽"; + case ARTS_THEATRE: return "🎭"; + case FILM: return "🎬"; + default: return "★"; + } + } + + private static Color getCategoryColor(EventCategory category) { + switch (category) { + case MUSIC: + return new Color(147, 51, 234); // Purple + case SPORTS: + return new Color(59, 130, 246); // Blue + case ARTS_THEATRE: + return new Color(236, 72, 153); // Pink + case FILM: + return new Color(245, 158, 11); // Orange + default: + return new Color(107, 114, 128); // Gray + } + } + + private static String truncateText(String text, int maxLength) { + if (text == null) return ""; + if (text.length() <= maxLength) return text; + return text.substring(0, maxLength) + "..."; + } + + private static String formatDate(LocalDateTime dateTime) { + String[] months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + return months[dateTime.getMonthValue() - 1] + " " + + dateTime.getDayOfMonth() + ", " + + dateTime.getYear() + " at " + + String.format("%02d:%02d", dateTime.getHour(), dateTime.getMinute()); + } + + // Sample Events + private static Event createSampleEvent1() { + Location location = new Location("Scotiabank Arena, 40 Bay St, Toronto, ON", 43.6435, -79.3791); + return new Event( + "event-1", + "Toronto Raptors vs Los Angeles Lakers", + "An epic showdown between two NBA powerhouses. Watch the Raptors take on LeBron James and the Lakers in what promises to be an unforgettable night of basketball action!", + location.getAddress(), + EventCategory.SPORTS, + location, + LocalDateTime.of(2025, 12, 15, 19, 30), + "https://via.placeholder.com/500x700" + ); + } + + // Mock User Data Access for demo purposes + private static class MockUserDataAccess implements LoginUserDataAccessInterface { + private String currentUsername; + + @Override + public boolean existsByName(String identifier) { + return "demo_user".equals(identifier); + } + + @Override + public void save(entity.User user) { + // Not needed for demo + } + + @Override + public entity.User get(String username) { + return null; // Not needed for demo + } + + @Override + public void setCurrentUsername(String name) { + this.currentUsername = name; + } + + @Override + public String getCurrentUsername() { + return currentUsername; + } + } +} \ No newline at end of file diff --git a/src/test/java/demo/SearchToEventViewDemo.java b/src/test/java/demo/SearchToEventViewDemo.java new file mode 100644 index 00000000..52f6136c --- /dev/null +++ b/src/test/java/demo/SearchToEventViewDemo.java @@ -0,0 +1,153 @@ +package demo; + +import data_access.EventDataAccessObject; +import entity.Event; +import entity.Location; +import interface_adapter.ViewManagerModel; +import interface_adapter.search.SearchController; +import interface_adapter.search.SearchPresenter; +import interface_adapter.search_event_by_name.SearchEventByNameController; +import interface_adapter.search_event_by_name.SearchEventByNamePresenter; +import interface_adapter.search_event_by_name.SearchEventByNameViewModel; +import use_case.search.SearchInputBoundary; +import use_case.search.SearchInteractor; +import use_case.search.SearchOutputBoundary; +import use_case.search_event_by_name.SearchEventByNameDataAccessInterface; +import use_case.search_event_by_name.SearchEventByNameInputBoundary; +import use_case.search_event_by_name.SearchEventByNameInteractor; +import use_case.search_event_by_name.SearchEventByNameOutputBoundary; +import view.SearchBarView; +import view.SearchEventByNameView; +import view.ViewManager; + +import javax.swing.*; +import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.List; + +/** + * Test application that demonstrates search bar -> event view flow + */ +public class SearchToEventViewDemo { + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + JFrame frame = new JFrame("Event Search Test"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setSize(1100, 750); + + // Creates the cardLayout + CardLayout cardLayout = new CardLayout(); + JPanel cardPanel = new JPanel(cardLayout); + ViewManagerModel viewManagerModel = new ViewManagerModel(); + ViewManager viewManager = new ViewManager(cardPanel, cardLayout, viewManagerModel); + SearchEventByNameDataAccessInterface dataAccess = new SearchEventByNameDataAccessObject(); + SearchEventByNameViewModel searchEventViewModel = new SearchEventByNameViewModel(); + SearchEventByNameView eventView = new SearchEventByNameView(searchEventViewModel); + + SearchOutputBoundary searchPresenter = new SearchPresenter( + searchEventViewModel, + viewManagerModel + ); + + SearchInputBoundary searchInteractor = new SearchInteractor( + dataAccess, + searchPresenter + ); + + SearchController searchController = new SearchController(searchInteractor); + + SearchEventByNameOutputBoundary eventPresenter = new SearchEventByNamePresenter( + searchEventViewModel, + viewManagerModel + ); + + SearchEventByNameInputBoundary eventInteractor = new SearchEventByNameInteractor( + dataAccess, + eventPresenter + ); + + SearchEventByNameController eventController = new SearchEventByNameController(eventInteractor); + eventView.setEventController(eventController); + + JPanel searchPanel = createSearchView(searchController); + + cardPanel.add(searchPanel, "search"); + cardPanel.add(eventView, eventView.getViewName()); + + viewManagerModel.addPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals("state")) { + String viewName = (String) evt.getNewValue(); + cardLayout.show(cardPanel, viewName); + } + } + }); + + viewManagerModel.setState("search"); + viewManagerModel.firePropertyChange(); + + frame.add(cardPanel); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + + System.out.println("Application started! YIPPEE"); + }); + } + + /** + * Creates a simple search view with a search bar + */ + private static JPanel createSearchView(SearchController searchController) { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBackground(Color.WHITE); + + // Title + JLabel titleLabel = new JLabel("Event Search", SwingConstants.CENTER); + titleLabel.setFont(new Font("SansSerif", Font.BOLD, 36)); + titleLabel.setBorder(BorderFactory.createEmptyBorder(50, 0, 30, 0)); + + // Search bar with FIXED dimensions + SearchBarView searchBar = new SearchBarView("Search for events...", new Location("Milwaukee, WI", 43.0389, -87.9065)); + searchBar.setSearchController(searchController); + searchBar.setMinimumSize(new Dimension(600, 60)); + searchBar.setMaximumSize(new Dimension(600, 60)); + searchBar.setPreferredSize(new Dimension(600, 60)); + + // Center panel - use FlowLayout to respect fixed sizes + JPanel centerPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); + centerPanel.setBackground(Color.WHITE); + + JPanel verticalWrapper = new JPanel(); + verticalWrapper.setLayout(new BoxLayout(verticalWrapper, BoxLayout.Y_AXIS)); + verticalWrapper.setBackground(Color.WHITE); + verticalWrapper.add(searchBar); + verticalWrapper.add(Box.createVerticalStrut(10)); + + centerPanel.add(verticalWrapper); + + // Add components to main panel + panel.add(titleLabel, BorderLayout.NORTH); + panel.add(centerPanel, BorderLayout.CENTER); + + return panel; + } + + /** + * Implementation of the data access interface that uses EventDataAccessObject + */ + private static class SearchEventByNameDataAccessObject implements SearchEventByNameDataAccessInterface { + private final EventDataAccessObject eventDAO; + + public SearchEventByNameDataAccessObject() { + this.eventDAO = new EventDataAccessObject(); + } + + @Override + public List searchEventsByName(String keyword, Location location, double radiusKm) { + return eventDAO.searchEventsByName(keyword, location, radiusKm); + } + } +} \ No newline at end of file diff --git a/src/test/java/demo/SimpleSearchBarDemo.java b/src/test/java/demo/SimpleSearchBarDemo.java new file mode 100644 index 00000000..53a0cd38 --- /dev/null +++ b/src/test/java/demo/SimpleSearchBarDemo.java @@ -0,0 +1,26 @@ +package demo; + + +import entity.Location; +import view.SearchBarView; +import javax.swing.*; + +public class SimpleSearchBarDemo { + public static void main(String[] args) { + JFrame frame = new JFrame("Simple Search Bar Demo"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setSize(700, 200); + frame.setLocationRelativeTo(null); + + // Create main panel + JPanel mainPanel = new JPanel(); + mainPanel.setBorder(BorderFactory.createEmptyBorder(50, 50, 50, 50)); + + // Create and add your search bar + SearchBarView searchBar = new SearchBarView("Hello!", new Location("Milwaukee, WI", 43.0389, -87.9065)); + mainPanel.add(searchBar); + + frame.add(mainPanel); + frame.setVisible(true); + } +} \ No newline at end of file diff --git a/src/test/java/entity/EventTest.java b/src/test/java/entity/EventTest.java new file mode 100644 index 00000000..2d41f75e --- /dev/null +++ b/src/test/java/entity/EventTest.java @@ -0,0 +1,820 @@ +package entity; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.BeforeEach; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +class EventTest { + + private Location defaultLocation; + private LocalDateTime defaultDateTime; + + @BeforeEach + void setUp() { + defaultLocation = new Location(43.6629, -79.3957); + defaultDateTime = LocalDateTime.of(2025, 11, 20, 19, 0); + } + + @Nested + @DisplayName("Main Constructor Tests") + class MainConstructorTests { + + @Test + @DisplayName("Should create event with all valid parameters") + void testConstructorWithValidParameters() { + Event event = new Event( + "1", + "Test Event", + "Test Description", + "123 Main St", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + + assertEquals("1", event.getId()); + assertEquals("Test Event", event.getName()); + assertEquals("Test Description", event.getDescription()); + assertEquals("123 Main St", event.getAddress()); + assertEquals(EventCategory.MUSIC, event.getCategory()); + assertEquals(defaultLocation, event.getLocation()); + assertEquals(defaultDateTime, event.getStartTime()); + assertEquals("https://example.com/image.jpg", event.getImageUrl()); + } + + @Test + @DisplayName("Should set empty description for null description") + void testConstructorWithNullDescription() { + Event event = new Event( + "1", + "Test Event", + null, + "123 Main St", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + + assertEquals("", event.getDescription()); + } + + @Test + @DisplayName("Should set MISCELLANEOUS category for null category") + void testConstructorWithNullCategory() { + Event event = new Event( + "1", + "Test Event", + "Description", + "123 Main St", + null, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + + assertEquals(EventCategory.MISCELLANEOUS, event.getCategory()); + } + + @Test + @DisplayName("Should set empty image URL for null image URL") + void testConstructorWithNullImageUrl() { + Event event = new Event( + "1", + "Test Event", + "Description", + "123 Main St", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + null + ); + + // Should return placeholder + assertTrue(event.getImageUrl().contains("placeholder")); + } + + @Test + @DisplayName("Should set empty image URL for empty string image URL") + void testConstructorWithEmptyImageUrl() { + Event event = new Event( + "1", + "Test Event", + "Description", + "123 Main St", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "" + ); + + // Should return placeholder + assertTrue(event.getImageUrl().contains("placeholder")); + } + + @Test + @DisplayName("Should throw exception for null ID") + void testConstructorWithNullId() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Event( + null, + "Test Event", + "Description", + "123 Main St", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + }); + + assertTrue(exception.getMessage().contains("Event ID")); + } + + @Test + @DisplayName("Should throw exception for empty ID") + void testConstructorWithEmptyId() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Event( + "", + "Test Event", + "Description", + "123 Main St", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + }); + + assertTrue(exception.getMessage().contains("Event ID")); + } + + @Test + @DisplayName("Should throw exception for whitespace-only ID") + void testConstructorWithWhitespaceId() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Event( + " ", + "Test Event", + "Description", + "123 Main St", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + }); + + assertTrue(exception.getMessage().contains("Event ID")); + } + + @Test + @DisplayName("Should throw exception for null name") + void testConstructorWithNullName() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Event( + "1", + null, + "Description", + "123 Main St", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + }); + + assertTrue(exception.getMessage().contains("Event name")); + } + + @Test + @DisplayName("Should throw exception for empty name") + void testConstructorWithEmptyName() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Event( + "1", + "", + "Description", + "123 Main St", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + }); + + assertTrue(exception.getMessage().contains("Event name")); + } + + @Test + @DisplayName("Should throw exception for null address") + void testConstructorWithNullAddress() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Event( + "1", + "Test Event", + "Description", + null, + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + }); + + assertTrue(exception.getMessage().contains("Event address")); + } + + @Test + @DisplayName("Should throw exception for empty address") + void testConstructorWithEmptyAddress() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Event( + "1", + "Test Event", + "Description", + "", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + }); + + assertTrue(exception.getMessage().contains("Event address")); + } + + @Test + @DisplayName("Should throw exception for null location") + void testConstructorWithNullLocation() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Event( + "1", + "Test Event", + "Description", + "123 Main St", + EventCategory.MUSIC, + null, + defaultDateTime, + "https://example.com/image.jpg" + ); + }); + + assertTrue(exception.getMessage().contains("Location")); + } + + @Test + @DisplayName("Should throw exception for null start time") + void testConstructorWithNullStartTime() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Event( + "1", + "Test Event", + "Description", + "123 Main St", + EventCategory.MUSIC, + defaultLocation, + null, + "https://example.com/image.jpg" + ); + }); + + assertTrue(exception.getMessage().contains("Start time")); + } + } + + @Nested + @DisplayName("Convenience Constructor Tests") + class ConvenienceConstructorTests { + + @Test + @DisplayName("Should create event with string category and datetime") + void testConvenienceConstructor() { + Event event = new Event( + "1", + "Test Event", + "Description", + "123 Main St", + defaultLocation, + "Music", + "2025-11-20T19:00" + ); + + assertEquals("1", event.getId()); + assertEquals("Test Event", event.getName()); + assertEquals(EventCategory.MUSIC, event.getCategory()); + assertEquals(2025, event.getStartTime().getYear()); + assertEquals(11, event.getStartTime().getMonthValue()); + assertEquals(20, event.getStartTime().getDayOfMonth()); + assertEquals(19, event.getStartTime().getHour()); + assertEquals(0, event.getStartTime().getMinute()); + } + + @Test + @DisplayName("Should parse category case-insensitively") + void testConvenienceConstructorCaseInsensitiveCategory() { + Event event1 = new Event("1", "Test", "Desc", "Addr", defaultLocation, "music", "2025-11-20T19:00"); + Event event2 = new Event("2", "Test", "Desc", "Addr", defaultLocation, "MUSIC", "2025-11-20T19:00"); + Event event3 = new Event("3", "Test", "Desc", "Addr", defaultLocation, "MuSiC", "2025-11-20T19:00"); + + assertEquals(EventCategory.MUSIC, event1.getCategory()); + assertEquals(EventCategory.MUSIC, event2.getCategory()); + assertEquals(EventCategory.MUSIC, event3.getCategory()); + } + + @Test + @DisplayName("Should default to MISCELLANEOUS for invalid category") + void testConvenienceConstructorInvalidCategory() { + Event event = new Event( + "1", + "Test Event", + "Description", + "123 Main St", + defaultLocation, + "InvalidCategory", + "2025-11-20T19:00" + ); + + assertEquals(EventCategory.MISCELLANEOUS, event.getCategory()); + } + + @Test + @DisplayName("Should default to MISCELLANEOUS for null category") + void testConvenienceConstructorNullCategory() { + Event event = new Event( + "1", + "Test Event", + "Description", + "123 Main St", + defaultLocation, + null, + "2025-11-20T19:00" + ); + + assertEquals(EventCategory.MISCELLANEOUS, event.getCategory()); + } + + @Test + @DisplayName("Should default to MISCELLANEOUS for empty category") + void testConvenienceConstructorEmptyCategory() { + Event event = new Event( + "1", + "Test Event", + "Description", + "123 Main St", + defaultLocation, + "", + "2025-11-20T19:00" + ); + + assertEquals(EventCategory.MISCELLANEOUS, event.getCategory()); + } + + @Test + @DisplayName("Should default to current time for null datetime") + void testConvenienceConstructorNullDateTime() { + LocalDateTime before = LocalDateTime.now(); + + Event event = new Event( + "1", + "Test Event", + "Description", + "123 Main St", + defaultLocation, + "Music", + null + ); + + LocalDateTime after = LocalDateTime.now(); + + assertFalse(event.getStartTime().isBefore(before)); + assertFalse(event.getStartTime().isAfter(after)); + } + + @Test + @DisplayName("Should default to current time for empty datetime") + void testConvenienceConstructorEmptyDateTime() { + LocalDateTime before = LocalDateTime.now(); + + Event event = new Event( + "1", + "Test Event", + "Description", + "123 Main St", + defaultLocation, + "Music", + "" + ); + + LocalDateTime after = LocalDateTime.now(); + + assertFalse(event.getStartTime().isBefore(before)); + assertFalse(event.getStartTime().isAfter(after)); + } + + @Test + @DisplayName("Should default to current time for invalid datetime format") + void testConvenienceConstructorInvalidDateTime() { + LocalDateTime before = LocalDateTime.now(); + + Event event = new Event( + "1", + "Test Event", + "Description", + "123 Main St", + defaultLocation, + "Music", + "invalid-date-format" + ); + + LocalDateTime after = LocalDateTime.now(); + + assertFalse(event.getStartTime().isBefore(before)); + assertFalse(event.getStartTime().isAfter(after)); + } + + @Test + @DisplayName("Should have empty image URL with convenience constructor") + void testConvenienceConstructorImageUrl() { + Event event = new Event( + "1", + "Test Event", + "Description", + "123 Main St", + defaultLocation, + "Music", + "2025-11-20T19:00" + ); + + // Should return placeholder since no image URL was provided + assertTrue(event.getImageUrl().contains("placeholder")); + } + } + + @Nested + @DisplayName("Getter Tests") + class GetterTests { + + private Event testEvent; + + @BeforeEach + void setUp() { + testEvent = new Event( + "test-id", + "Test Event Name", + "Test Description", + "456 Test Street", + EventCategory.SPORTS, + defaultLocation, + defaultDateTime, + "https://example.com/test.jpg" + ); + } + + @Test + @DisplayName("Should return correct ID") + void testGetId() { + assertEquals("test-id", testEvent.getId()); + } + + @Test + @DisplayName("Should return correct name") + void testGetName() { + assertEquals("Test Event Name", testEvent.getName()); + } + + @Test + @DisplayName("Should return correct description") + void testGetDescription() { + assertEquals("Test Description", testEvent.getDescription()); + } + + @Test + @DisplayName("Should return correct address") + void testGetAddress() { + assertEquals("456 Test Street", testEvent.getAddress()); + } + + @Test + @DisplayName("Should return correct category") + void testGetCategory() { + assertEquals(EventCategory.SPORTS, testEvent.getCategory()); + } + + @Test + @DisplayName("Should return correct location") + void testGetLocation() { + assertEquals(defaultLocation, testEvent.getLocation()); + } + + @Test + @DisplayName("Should return correct start time") + void testGetStartTime() { + assertEquals(defaultDateTime, testEvent.getStartTime()); + } + + @Test + @DisplayName("Should return correct image URL when provided") + void testGetImageUrlWhenProvided() { + assertEquals("https://example.com/test.jpg", testEvent.getImageUrl()); + } + + @Test + @DisplayName("Should return placeholder when image URL is empty") + void testGetImageUrlPlaceholder() { + Event eventWithoutImage = new Event( + "1", + "Test", + "Desc", + "Addr", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "" + ); + + String imageUrl = eventWithoutImage.getImageUrl(); + + assertTrue(imageUrl.contains("placeholder")); + assertTrue(imageUrl.contains("No+Event+Image")); + } + } + + @Nested + @DisplayName("Business Method Tests") + class BusinessMethodTests { + + private Event testEvent; + private Location eventLocation; + + @BeforeEach + void setUp() { + eventLocation = new Location(43.6532, -79.3832); // Toronto + testEvent = new Event( + "1", + "Test Event", + "Description", + "123 Main St", + EventCategory.MUSIC, + eventLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + } + + @Test + @DisplayName("Should return true when location is within radius") + void testIsWithinRadiusTrue() { + Location userLocation = new Location(43.6629, -79.3957); // ~1.5 km away + + assertTrue(testEvent.isWithinRadius(userLocation, 2.0)); + } + + @Test + @DisplayName("Should return false when location is outside radius") + void testIsWithinRadiusFalse() { + Location userLocation = new Location(43.6629, -79.3957); // ~1.5 km away + + assertFalse(testEvent.isWithinRadius(userLocation, 1.0)); + } + + @Test + @DisplayName("Should return false for null user location") + void testIsWithinRadiusNullLocation() { + assertFalse(testEvent.isWithinRadius(null, 5.0)); + } + + @Test + @DisplayName("Should return false for negative radius") + void testIsWithinRadiusNegativeRadius() { + Location userLocation = new Location(43.6629, -79.3957); + + assertFalse(testEvent.isWithinRadius(userLocation, -1.0)); + } + + @Test + @DisplayName("Should return true when exactly at radius boundary") + void testIsWithinRadiusExactBoundary() { + Location userLocation = new Location(43.6629, -79.3957); + double exactDistance = testEvent.calculateDistanceTo(userLocation); + + assertTrue(testEvent.isWithinRadius(userLocation, exactDistance)); + } + + @Test + @DisplayName("Should calculate correct distance to user location") + void testCalculateDistanceTo() { + Location userLocation = new Location(43.6629, -79.3957); + + double distance = testEvent.calculateDistanceTo(userLocation); + + assertTrue(distance > 0); + assertTrue(distance < 3.0); // Should be less than 3 km + } + + @Test + @DisplayName("Should return MAX_VALUE for null user location in distance calculation") + void testCalculateDistanceToNull() { + double distance = testEvent.calculateDistanceTo(null); + + assertEquals(Double.MAX_VALUE, distance); + } + + @Test + @DisplayName("Should return true for matching category") + void testIsInCategoryTrue() { + assertTrue(testEvent.isInCategory(EventCategory.MUSIC)); + } + + @Test + @DisplayName("Should return false for non-matching category") + void testIsInCategoryFalse() { + assertFalse(testEvent.isInCategory(EventCategory.SPORTS)); + } + + @Test + @DisplayName("Should handle null category comparison") + void testIsInCategoryNull() { + assertFalse(testEvent.isInCategory(null)); + } + } + + @Nested + @DisplayName("Equals and HashCode Tests") + class EqualsAndHashCodeTests { + + private Event event1; + private Event event2; + + @BeforeEach + void setUp() { + event1 = new Event( + "same-id", + "Event Name", + "Description", + "Address", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + + event2 = new Event( + "same-id", + "Different Name", + "Different Description", + "Different Address", + EventCategory.SPORTS, + new Location(0, 0), + LocalDateTime.now(), + "https://different.com/image.jpg" + ); + } + + @Test + @DisplayName("Should be equal to itself") + void testEqualsItself() { + assertEquals(event1, event1); + } + + @Test + @DisplayName("Should be equal to event with same ID") + void testEqualsSameId() { + assertEquals(event1, event2); + } + + @Test + @DisplayName("Should not be equal to event with different ID") + void testNotEqualsDifferentId() { + Event differentEvent = new Event( + "different-id", + "Event Name", + "Description", + "Address", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + + assertNotEquals(event1, differentEvent); + } + + @Test + @DisplayName("Should not be equal to null") + void testNotEqualsNull() { + assertNotEquals(null, event1); + } + + @Test + @DisplayName("Should not be equal to different class object") + void testNotEqualsDifferentClass() { + assertNotEquals(event1, "Not an Event"); + } + + @Test + @DisplayName("Should have same hashCode for events with same ID") + void testHashCodeSameForSameId() { + assertEquals(event1.hashCode(), event2.hashCode()); + } + + @Test + @DisplayName("Should have different hashCode for events with different ID") + void testHashCodeDifferentForDifferentId() { + Event differentEvent = new Event( + "different-id", + "Event Name", + "Description", + "Address", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + + assertNotEquals(event1.hashCode(), differentEvent.hashCode()); + } + + @Test + @DisplayName("Should have consistent hashCode") + void testHashCodeConsistent() { + int hash1 = event1.hashCode(); + int hash2 = event1.hashCode(); + + assertEquals(hash1, hash2); + } + } + + @Nested + @DisplayName("ToString Tests") + class ToStringTests { + + private Event testEvent; + + @BeforeEach + void setUp() { + testEvent = new Event( + "test-id", + "Test Event", + "Description", + "Address", + EventCategory.MUSIC, + defaultLocation, + defaultDateTime, + "https://example.com/image.jpg" + ); + } + + @Test + @DisplayName("Should contain ID in toString") + void testToStringContainsId() { + String result = testEvent.toString(); + + assertTrue(result.contains("test-id")); + } + + @Test + @DisplayName("Should contain name in toString") + void testToStringContainsName() { + String result = testEvent.toString(); + + assertTrue(result.contains("Test Event")); + } + + @Test + @DisplayName("Should contain category in toString") + void testToStringContainsCategory() { + String result = testEvent.toString(); + + assertTrue(result.contains("MUSIC") || result.contains("Music")); + } + + @Test + @DisplayName("Should contain image URL in toString") + void testToStringContainsImageUrl() { + String result = testEvent.toString(); + + assertTrue(result.contains("example.com")); + } + + @Test + @DisplayName("Should have proper format") + void testToStringFormat() { + String result = testEvent.toString(); + + assertTrue(result.startsWith("Event{")); + assertTrue(result.contains("id=")); + assertTrue(result.contains("name=")); + assertTrue(result.contains("category=")); + assertTrue(result.contains("image=")); + assertTrue(result.endsWith("}")); + } + } +} \ No newline at end of file diff --git a/src/test/java/entity/LocationTest.java b/src/test/java/entity/LocationTest.java new file mode 100644 index 00000000..2cebb96d --- /dev/null +++ b/src/test/java/entity/LocationTest.java @@ -0,0 +1,372 @@ +package entity; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; + +import static org.junit.jupiter.api.Assertions.*; + +class LocationTest { + + @Nested + @DisplayName("Constructor Tests") + class ConstructorTests { + + @Test + @DisplayName("Should create location with latitude and longitude only") + void testConstructorWithCoordinatesOnly() { + Location location = new Location(43.6629, -79.3957); + + assertEquals(43.6629, location.getLatitude(), 0.0001); + assertEquals(-79.3957, location.getLongitude(), 0.0001); + assertEquals("", location.getAddress()); + } + + @Test + @DisplayName("Should create location with address and coordinates") + void testConstructorWithAddressAndCoordinates() { + Location location = new Location("123 Main St", 43.6629, -79.3957); + + assertEquals("123 Main St", location.getAddress()); + assertEquals(43.6629, location.getLatitude(), 0.0001); + assertEquals(-79.3957, location.getLongitude(), 0.0001); + } + + @Test + @DisplayName("Should trim whitespace from address") + void testConstructorTrimsAddress() { + Location location = new Location(" 123 Main St ", 43.6629, -79.3957); + + assertEquals("123 Main St", location.getAddress()); + } + + @Test + @DisplayName("Should handle null address by setting empty string") + void testConstructorWithNullAddress() { + Location location = new Location(null, 43.6629, -79.3957); + + assertEquals("", location.getAddress()); + assertNotNull(location.getAddress()); + } + + @Test + @DisplayName("Should throw exception for latitude above 90") + void testConstructorWithLatitudeAbove90() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Location(91.0, -79.3957); + }); + + assertTrue(exception.getMessage().contains("Latitude")); + assertTrue(exception.getMessage().contains("90")); + } + + @Test + @DisplayName("Should throw exception for latitude below -90") + void testConstructorWithLatitudeBelow90() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Location(-91.0, -79.3957); + }); + + assertTrue(exception.getMessage().contains("Latitude")); + assertTrue(exception.getMessage().contains("-90")); + } + + @Test + @DisplayName("Should throw exception for longitude above 180") + void testConstructorWithLongitudeAbove180() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Location(43.6629, 181.0); + }); + + assertTrue(exception.getMessage().contains("Longitude")); + assertTrue(exception.getMessage().contains("180")); + } + + @Test + @DisplayName("Should throw exception for longitude below -180") + void testConstructorWithLongitudeBelow180() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + new Location(43.6629, -181.0); + }); + + assertTrue(exception.getMessage().contains("Longitude")); + assertTrue(exception.getMessage().contains("-180")); + } + + @Test + @DisplayName("Should accept boundary latitude values") + void testConstructorWithBoundaryLatitudes() { + Location location1 = new Location(90.0, 0.0); + Location location2 = new Location(-90.0, 0.0); + + assertEquals(90.0, location1.getLatitude(), 0.0001); + assertEquals(-90.0, location2.getLatitude(), 0.0001); + } + + @Test + @DisplayName("Should accept boundary longitude values") + void testConstructorWithBoundaryLongitudes() { + Location location1 = new Location(0.0, 180.0); + Location location2 = new Location(0.0, -180.0); + + assertEquals(180.0, location1.getLongitude(), 0.0001); + assertEquals(-180.0, location2.getLongitude(), 0.0001); + } + } + + @Nested + @DisplayName("Distance Calculation Tests") + class DistanceCalculationTests { + + @Test + @DisplayName("Should calculate distance between two locations") + void testCalculateDistanceBetweenTwoLocations() { + // Toronto to Mississauga (approximate) + Location toronto = new Location(43.6532, -79.3832); + Location mississauga = new Location(43.5890, -79.6441); + + double distance = toronto.calculateDistance(mississauga); + + // Distance should be approximately 23-24 km + assertTrue(distance > 22.0 && distance < 25.0, + "Distance should be approximately 23-24 km, was: " + distance); + } + + @Test + @DisplayName("Should calculate zero distance for same location") + void testCalculateDistanceSameLocation() { + Location location = new Location(43.6532, -79.3832); + + double distance = location.calculateDistance(location); + + assertEquals(0.0, distance, 0.001); + } + + @Test + @DisplayName("Should calculate distance with very close coordinates") + void testCalculateDistanceVeryClose() { + Location loc1 = new Location(43.6532, -79.3832); + Location loc2 = new Location(43.6533, -79.3833); + + double distance = loc1.calculateDistance(loc2); + + // Should be very small (less than 0.2 km) + assertTrue(distance < 0.2, + "Distance should be very small, was: " + distance); + } + + @Test + @DisplayName("Should return MAX_VALUE when calculating distance to null location") + void testCalculateDistanceToNull() { + Location location = new Location(43.6532, -79.3832); + + double distance = location.calculateDistance(null); + + assertEquals(Double.MAX_VALUE, distance); + } + + @Test + @DisplayName("Should calculate long distance correctly (Toronto to Vancouver)") + void testCalculateLongDistance() { + Location toronto = new Location(43.6532, -79.3832); + Location vancouver = new Location(49.2827, -123.1207); + + double distance = toronto.calculateDistance(vancouver); + + // Distance should be approximately 3350-3400 km + assertTrue(distance > 3300 && distance < 3450, + "Distance Toronto-Vancouver should be ~3350-3400 km, was: " + distance); + } + + @Test + @DisplayName("Should calculate distance across the equator") + void testCalculateDistanceAcrossEquator() { + Location northernHemisphere = new Location(10.0, 0.0); + Location southernHemisphere = new Location(-10.0, 0.0); + + double distance = northernHemisphere.calculateDistance(southernHemisphere); + + // Distance should be approximately 2220 km (20 degrees of latitude) + assertTrue(distance > 2200 && distance < 2250, + "Distance should be approximately 2220 km, was: " + distance); + } + + @Test + @DisplayName("Should calculate distance at the poles") + void testCalculateDistanceAtPoles() { + Location northPole = new Location(90.0, 0.0); + Location southPole = new Location(-90.0, 0.0); + + double distance = northPole.calculateDistance(southPole); + + // Distance should be approximately half Earth's circumference (~20,000 km) + assertTrue(distance > 19900 && distance < 20100, + "Distance pole to pole should be ~20,000 km, was: " + distance); + } + } + + @Nested + @DisplayName("Equals and HashCode Tests") + class EqualsAndHashCodeTests { + + @Test + @DisplayName("Should be equal to itself") + void testEqualsItself() { + Location location = new Location("123 Main St", 43.6629, -79.3957); + + assertEquals(location, location); + } + + @Test + @DisplayName("Should be equal to location with same values") + void testEqualsSameValues() { + Location location1 = new Location("123 Main St", 43.6629, -79.3957); + Location location2 = new Location("123 Main St", 43.6629, -79.3957); + + assertEquals(location1, location2); + assertEquals(location2, location1); + } + + @Test + @DisplayName("Should not be equal to location with different address") + void testNotEqualsDifferentAddress() { + Location location1 = new Location("123 Main St", 43.6629, -79.3957); + Location location2 = new Location("456 Oak Ave", 43.6629, -79.3957); + + assertNotEquals(location1, location2); + } + + @Test + @DisplayName("Should not be equal to location with different latitude") + void testNotEqualsDifferentLatitude() { + Location location1 = new Location("123 Main St", 43.6629, -79.3957); + Location location2 = new Location("123 Main St", 43.7629, -79.3957); + + assertNotEquals(location1, location2); + } + + @Test + @DisplayName("Should not be equal to location with different longitude") + void testNotEqualsDifferentLongitude() { + Location location1 = new Location("123 Main St", 43.6629, -79.3957); + Location location2 = new Location("123 Main St", 43.6629, -79.4957); + + assertNotEquals(location1, location2); + } + + @Test + @DisplayName("Should not be equal to null") + void testNotEqualsNull() { + Location location = new Location("123 Main St", 43.6629, -79.3957); + + assertNotEquals(null, location); + } + + @Test + @DisplayName("Should not be equal to different class object") + void testNotEqualsDifferentClass() { + Location location = new Location("123 Main St", 43.6629, -79.3957); + String notALocation = "Not a location"; + + assertNotEquals(location, notALocation); + } + + @Test + @DisplayName("Should have same hashCode for equal locations") + void testHashCodeSameForEqualLocations() { + Location location1 = new Location("123 Main St", 43.6629, -79.3957); + Location location2 = new Location("123 Main St", 43.6629, -79.3957); + + assertEquals(location1.hashCode(), location2.hashCode()); + } + + @Test + @DisplayName("Should have consistent hashCode") + void testHashCodeConsistent() { + Location location = new Location("123 Main St", 43.6629, -79.3957); + + int hashCode1 = location.hashCode(); + int hashCode2 = location.hashCode(); + + assertEquals(hashCode1, hashCode2); + } + } + + @Nested + @DisplayName("ToString Tests") + class ToStringTests { + + @Test + @DisplayName("Should contain address in toString") + void testToStringContainsAddress() { + Location location = new Location("123 Main St", 43.6629, -79.3957); + + String result = location.toString(); + + assertTrue(result.contains("123 Main St")); + } + + @Test + @DisplayName("Should contain latitude in toString") + void testToStringContainsLatitude() { + Location location = new Location("123 Main St", 43.6629, -79.3957); + + String result = location.toString(); + + assertTrue(result.contains("43.6")); + } + + @Test + @DisplayName("Should contain longitude in toString") + void testToStringContainsLongitude() { + Location location = new Location("123 Main St", 43.6629, -79.3957); + + String result = location.toString(); + + assertTrue(result.contains("-79.3")); + } + + @Test + @DisplayName("Should have proper format") + void testToStringFormat() { + Location location = new Location("Test Address", 43.6629, -79.3957); + + String result = location.toString(); + + assertTrue(result.startsWith("Location{")); + assertTrue(result.contains("address=")); + assertTrue(result.contains("latitude=")); + assertTrue(result.contains("longitude=")); + assertTrue(result.endsWith("}")); + } + } + + @Nested + @DisplayName("Getter Tests") + class GetterTests { + + @Test + @DisplayName("Should return correct address") + void testGetAddress() { + Location location = new Location("123 Main St", 43.6629, -79.3957); + + assertEquals("123 Main St", location.getAddress()); + } + + @Test + @DisplayName("Should return correct latitude") + void testGetLatitude() { + Location location = new Location("123 Main St", 43.6629, -79.3957); + + assertEquals(43.6629, location.getLatitude(), 0.0001); + } + + @Test + @DisplayName("Should return correct longitude") + void testGetLongitude() { + Location location = new Location("123 Main St", 43.6629, -79.3957); + + assertEquals(-79.3957, location.getLongitude(), 0.0001); + } + } +} \ No newline at end of file diff --git a/src/test/java/use_case/calendarFlow/CalendarFlowInteractorTest.java b/src/test/java/use_case/calendarFlow/CalendarFlowInteractorTest.java new file mode 100644 index 00000000..7748864c --- /dev/null +++ b/src/test/java/use_case/calendarFlow/CalendarFlowInteractorTest.java @@ -0,0 +1,276 @@ +//package use_case.calendarFlow; +// +//import entity.Event; +//import entity.EventCategory; +//import entity.Location; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +// +//import java.time.LocalDate; +//import java.time.LocalDateTime; +//import java.util.ArrayList; +//import java.util.List; +// +//import static org.junit.jupiter.api.Assertions.*; +// +///** +// * Test class for CalendarFlowInteractor. +// * Tests the business logic for fetching events by date. +// */ +//public class CalendarFlowInteractorTest { +// +// private CalendarFlowInteractor interactor; +// private MockCalendarFlowDataAccess mockDataAccess; +// private MockCalendarFlowPresenter mockPresenter; +// +// @BeforeEach +// public void setUp() { +// mockDataAccess = new MockCalendarFlowDataAccess(); +// mockPresenter = new MockCalendarFlowPresenter(); +// interactor = new CalendarFlowInteractor(mockDataAccess, mockPresenter); +// } +// +// @Test +// public void testSuccessfulEventRetrieval() { +// // Setup test data +// LocalDate date = LocalDate.of(2024, 11, 22); +// Location location = new Location("Toronto, ON", 43.6532, -79.3832); +// double radius = 50.0; +// +// // Create input data +// CalendarFlowInputData inputData = new CalendarFlowInputData(date, location, radius); +// +// // Execute +// interactor.execute(inputData); +// +// // Verify success view was called +// assertTrue(mockPresenter.isSuccessViewCalled()); +// assertFalse(mockPresenter.isFailViewCalled()); +// +// // Verify output data +// assertNotNull(mockPresenter.getOutputData()); +// assertEquals(date, mockPresenter.getOutputData().getSelectedDate()); +// assertEquals(3, mockPresenter.getOutputData().getEvents().size()); +// } +// +// @Test +// public void testNoEventsFound() { +// // Setup - data access returns empty list +// mockDataAccess.setReturnEmptyList(true); +// +// LocalDate date = LocalDate.of(2024, 12, 25); +// Location location = new Location("Toronto, ON", 43.6532, -79.3832); +// double radius = 50.0; +// +// CalendarFlowInputData inputData = new CalendarFlowInputData(date, location, radius); +// +// // Execute +// interactor.execute(inputData); +// +// // Verify fail view was called +// assertFalse(mockPresenter.isSuccessViewCalled()); +// assertTrue(mockPresenter.isFailViewCalled()); +// assertTrue(mockPresenter.getErrorMessage().contains("No events found")); +// } +// +// @Test +// public void testNullDate() { +// // Create input with null date +// Location location = new Location("Toronto, ON", 43.6532, -79.3832); +// CalendarFlowInputData inputData = new CalendarFlowInputData(null, location, 50.0); +// +// // Execute +// interactor.execute(inputData); +// +// // Verify fail view was called with appropriate error +// assertTrue(mockPresenter.isFailViewCalled()); +// assertFalse(mockPresenter.isSuccessViewCalled()); +// assertEquals("Date cannot be null", mockPresenter.getErrorMessage()); +// } +// +// @Test +// public void testNullLocation() { +// // Create input with null location +// LocalDate date = LocalDate.of(2024, 11, 22); +// CalendarFlowInputData inputData = new CalendarFlowInputData(date, null, 50.0); +// +// // Execute +// interactor.execute(inputData); +// +// // Verify fail view was called with appropriate error +// assertTrue(mockPresenter.isFailViewCalled()); +// assertFalse(mockPresenter.isSuccessViewCalled()); +// assertEquals("User location is required", mockPresenter.getErrorMessage()); +// } +// +// @Test +// public void testDataAccessException() { +// // Setup - data access throws exception +// mockDataAccess.setShouldThrowException(true); +// +// LocalDate date = LocalDate.of(2024, 11, 22); +// Location location = new Location("Toronto, ON", 43.6532, -79.3832); +// CalendarFlowInputData inputData = new CalendarFlowInputData(date, location, 50.0); +// +// // Execute +// interactor.execute(inputData); +// +// // Verify fail view was called +// assertTrue(mockPresenter.isFailViewCalled()); +// assertFalse(mockPresenter.isSuccessViewCalled()); +// assertTrue(mockPresenter.getErrorMessage().contains("Error fetching events")); +// } +// +// @Test +// public void testDifferentRadii() { +// // Test with small radius +// LocalDate date = LocalDate.of(2024, 11, 22); +// Location location = new Location("Toronto, ON", 43.6532, -79.3832); +// +// CalendarFlowInputData inputData1 = new CalendarFlowInputData(date, location, 10.0); +// interactor.execute(inputData1); +// assertTrue(mockPresenter.isSuccessViewCalled()); +// +// // Reset +// mockPresenter.reset(); +// +// // Test with large radius +// CalendarFlowInputData inputData2 = new CalendarFlowInputData(date, location, 100.0); +// interactor.execute(inputData2); +// assertTrue(mockPresenter.isSuccessViewCalled()); +// } +// +// @Test +// public void testMultipleExecutions() { +// LocalDate date1 = LocalDate.of(2024, 11, 22); +// LocalDate date2 = LocalDate.of(2024, 11, 23); +// Location location = new Location("Toronto, ON", 43.6532, -79.3832); +// +// // First execution +// CalendarFlowInputData inputData1 = new CalendarFlowInputData(date1, location, 50.0); +// interactor.execute(inputData1); +// assertTrue(mockPresenter.isSuccessViewCalled()); +// +// // Reset presenter +// mockPresenter.reset(); +// +// // Second execution +// CalendarFlowInputData inputData2 = new CalendarFlowInputData(date2, location, 50.0); +// interactor.execute(inputData2); +// assertTrue(mockPresenter.isSuccessViewCalled()); +// } +// +// /** +// * Mock implementation of CalendarFlowDataAccessInterface for testing. +// */ +// private static class MockCalendarFlowDataAccess implements CalendarFlowDataAccessInterface { +// private boolean returnEmptyList = false; +// private boolean shouldThrowException = false; +// +// public void setReturnEmptyList(boolean returnEmptyList) { +// this.returnEmptyList = returnEmptyList; +// } +// +// public void setShouldThrowException(boolean shouldThrowException) { +// this.shouldThrowException = shouldThrowException; +// } +// +// @Override +// public List getEventsByDate(LocalDate date, Location location, double radiusKm) { +// if (shouldThrowException) { +// throw new RuntimeException("API connection failed"); +// } +// +// if (returnEmptyList) { +// return new ArrayList<>(); +// } +// +// // Return mock events +// List events = new ArrayList<>(); +// +// Location loc1 = new Location("Scotiabank Arena, Toronto, ON", 43.6435, -79.3791); +// Event event1 = new Event( +// "1", +// "Toronto Maple Leafs vs Boston Bruins", +// "Hockey game", +// EventCategory.SPORTS, +// loc1, +// LocalDateTime.of(2024, 11, 22, 19, 0), +// "https://example.com/image1.jpg" +// ); +// +// Location loc2 = new Location("Rogers Centre, Toronto, ON", 43.6414, -79.3894); +// Event event2 = new Event( +// "2", +// "Drake Concert", +// "Music concert", +// EventCategory.MUSIC, +// loc2, +// LocalDateTime.of(2024, 11, 22, 20, 30), +// "https://example.com/image2.jpg" +// ); +// +// Location loc3 = new Location("Second City, Toronto, ON", 43.6426, -79.3871); +// Event event3 = new Event( +// "3", +// "Comedy Night", +// "Comedy show", +// EventCategory.ARTS_THEATRE, +// loc3, +// LocalDateTime.of(2024, 11, 22, 21, 0), +// "https://example.com/image3.jpg" +// ); +// +// events.add(event1); +// events.add(event2); +// events.add(event3); +// +// return events; +// } +// } +// +// /** +// * Mock implementation of CalendarFlowOutputBoundary for testing. +// */ +// private static class MockCalendarFlowPresenter implements CalendarFlowOutputBoundary { +// private boolean successViewCalled = false; +// private boolean failViewCalled = false; +// private CalendarFlowOutputData outputData; +// private String errorMessage; +// +// @Override +// public void prepareSuccessView(CalendarFlowOutputData outputData) { +// this.successViewCalled = true; +// this.outputData = outputData; +// } +// +// @Override +// public void prepareFailView(String errorMessage) { +// this.failViewCalled = true; +// this.errorMessage = errorMessage; +// } +// +// public boolean isSuccessViewCalled() { +// return successViewCalled; +// } +// +// public boolean isFailViewCalled() { +// return failViewCalled; +// } +// +// public CalendarFlowOutputData getOutputData() { +// return outputData; +// } +// +// public String getErrorMessage() { +// return errorMessage; +// } +// +// public void reset() { +// successViewCalled = false; +// failViewCalled = false; +// outputData = null; +// errorMessage = null; +// } +// } +//} diff --git a/src/test/java/use_case/calendarFlow/CalendarFlowViewTest.java b/src/test/java/use_case/calendarFlow/CalendarFlowViewTest.java new file mode 100644 index 00000000..49a6092a --- /dev/null +++ b/src/test/java/use_case/calendarFlow/CalendarFlowViewTest.java @@ -0,0 +1,18 @@ +package use_case.calendarFlow; + +import view.CalendarView; +import javax.swing.*; + +public class CalendarFlowViewTest { + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + JFrame frame = new JFrame("Calendar Demo"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + CalendarView calendarView = new CalendarView(); + frame.add(calendarView); + frame.pack(); + frame.setLocationRelativeTo(null); // Center on screen + frame.setVisible(true); + }); + } +} diff --git a/src/test/java/use_case/calendarFlow/EventListByDateViewTest.java b/src/test/java/use_case/calendarFlow/EventListByDateViewTest.java new file mode 100644 index 00000000..40158da8 --- /dev/null +++ b/src/test/java/use_case/calendarFlow/EventListByDateViewTest.java @@ -0,0 +1,197 @@ +//package use_case.calendarFlow; +// +//import entity.Event; +//import entity.EventCategory; +//import entity.Location; +//import interface_adapter.calendarFlow.CalendarFlowState; +//import interface_adapter.calendarFlow.CalendarFlowViewModel; +//import view.EventListByDateView; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +// +//import javax.swing.*; +//import java.time.LocalDate; +//import java.time.LocalDateTime; +//import java.util.ArrayList; +//import java.util.List; +// +//import static org.junit.jupiter.api.Assertions.*; +// +///** +// * Test class for EventListByDateView. +// * Tests the UI rendering and state changes. +// */ +//public class EventListByDateViewTest { +// private EventListByDateView view; +// private CalendarFlowViewModel viewModel; +// +// /** +// * Helper method to create test events +// */ +// private static List createTestEvents() { +// List events = new ArrayList<>(); +// +// Location location1 = new Location("Scotiabank Arena, Toronto, ON", 43.6435, -79.3791); +// Event event1 = new Event( +// "1", +// "Toronto Maple Leafs vs Boston Bruins", +// "Exciting hockey game", +// EventCategory.SPORTS, +// location1, +// LocalDateTime.of(2024, 11, 22, 19, 0), +// "https://example.com/image1.jpg" +// ); +// +// Location location2 = new Location("Rogers Centre, Toronto, ON", 43.6414, -79.3894); +// Event event2 = new Event( +// "2", +// "Drake Concert", +// "Live music performance", +// EventCategory.MUSIC, +// location2, +// LocalDateTime.of(2024, 11, 22, 20, 30), +// "https://example.com/image2.jpg" +// ); +// +// Location location3 = new Location("Second City, Toronto, ON", 43.6426, -79.3871); +// Event event3 = new Event( +// "3", +// "Comedy Night", +// "Stand-up comedy show", +// EventCategory.ARTS_THEATRE, +// location3, +// LocalDateTime.of(2024, 11, 22, 21, 0), +// "https://example.com/image3.jpg" +// ); +// +// events.add(event1); +// events.add(event2); +// events.add(event3); +// +// return events; +// } +// +// +// @BeforeEach +// public void setUp() { +// // Create ViewModel +// viewModel = new CalendarFlowViewModel(); +// // Create View +// view = new EventListByDateView(viewModel); +// } +// +// @Test +// public void testViewCreation() { +// // Test that view is created successfully +// assertNotNull(view); +// assertEquals("event list by date", view.getViewName()); +// } +// +// @Test +// public void testInitialState() { +// // Test that view starts with empty state +// assertNotNull(view); +// +// // View should be visible and properly initialized +// assertTrue(view.isVisible() || !view.isVisible()); // Just checking it exists +// } +// +// @Test +// public void testViewWithEvents() { +// // Create test events +// List events = createTestEvents(); +// +// // Create state with events +// CalendarFlowState state = new CalendarFlowState(); +// state.setDate(LocalDate.of(2024, 11, 22)); +// state.setEventList(events); +// +// // Update ViewModel (this should trigger view update) +// viewModel.setState(state); +// viewModel.firePropertyChange(); +// +// // Verify state was set +// assertEquals(3, viewModel.getState().getEventList().size()); +// assertEquals(LocalDate.of(2024, 11, 22), viewModel.getState().getDate()); +// } +// +// @Test +// public void testDateFormatting() { +// // Create state with date +// CalendarFlowState state = new CalendarFlowState(); +// state.setDate(LocalDate.of(2024, 11, 22)); +// state.setEventList(new ArrayList<>()); +// +// // Update ViewModel +// viewModel.setState(state); +// viewModel.firePropertyChange(); +// +// // Verify date is set +// assertEquals(LocalDate.of(2024, 11, 22), viewModel.getState().getDate()); +// } +// +// @Test +// public void testBackButtonAction() { +// // Test that back button action can be set +// boolean[] actionPerformed = {false}; +// +// view.setBackButtonAction(e -> { +// actionPerformed[0] = true; +// }); +// +// // Action should not be performed yet +// assertFalse(actionPerformed[0]); +// +// // Note: Actually clicking the button would require UI testing framework +// } +// +// @Test +// public void testMultipleStateUpdates() { +// // First update - with events +// CalendarFlowState state1 = new CalendarFlowState(); +// state1.setDate(LocalDate.of(2024, 11, 22)); +// state1.setEventList(createTestEvents()); +// +// viewModel.setState(state1); +// viewModel.firePropertyChange(); +// +// assertEquals(3, viewModel.getState().getEventList().size()); +// +// // Second update - no events +// CalendarFlowState state2 = new CalendarFlowState(); +// state2.setDate(LocalDate.of(2024, 11, 23)); +// state2.setEventList(new ArrayList<>()); +// state2.setErrorMessage("No events found"); +// +// viewModel.setState(state2); +// viewModel.firePropertyChange(); +// +// assertTrue(viewModel.getState().getEventList().isEmpty()); +// assertEquals(LocalDate.of(2024, 11, 23), viewModel.getState().getDate()); +// } +// +// public static void main(String[] args) { +// SwingUtilities.invokeLater(() -> { +// // Create ViewModel and View +// CalendarFlowViewModel viewModel = new CalendarFlowViewModel(); +// EventListByDateView view = new EventListByDateView(viewModel); +// +// // Set up a sample state +// CalendarFlowState state = new CalendarFlowState(); +// state.setDate(LocalDate.of(2024, 11, 22)); +// state.setEventList(createTestEvents()); // uses the static helper +// +// viewModel.setState(state); +// viewModel.firePropertyChange(); +// +// // Show in a JFrame +// JFrame frame = new JFrame("EventListByDateView – Preview"); +// frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); +// frame.setContentPane(view); +// frame.pack(); // sizes frame to preferred sizes +// frame.setLocationRelativeTo(null); // center on screen +// frame.setVisible(true); +// }); +// } +// +//} diff --git a/src/test/java/use_case/display_local_events/DisplayLocalEventsInteractorTest.java b/src/test/java/use_case/display_local_events/DisplayLocalEventsInteractorTest.java new file mode 100644 index 00000000..97a5ab3d --- /dev/null +++ b/src/test/java/use_case/display_local_events/DisplayLocalEventsInteractorTest.java @@ -0,0 +1,141 @@ +//package use_case.display_local_events; +// +//import entity.Event; +//import entity.EventCategory; +//import entity.EventRepository; +//import entity.Location; +// +//import java.time.LocalDateTime; +//import java.util.List; +//import java.util.Optional; +// +///** +// * A self-contained test class that does NOT require JUnit. +// */ +//public class DisplayLocalEventsInteractorTest { +// +// /** In-memory fake repository */ +// static class InMemoryEventRepository implements EventRepository { +// private final List events; +// +// InMemoryEventRepository(List events) { +// this.events = events; +// } +// +// @Override +// public List findAllEvents() { +// return events; +// } +// +// @Override +// public Optional findById(String id) { +// return events.stream().filter(e -> e.getId().equals(id)).findFirst(); +// } +// +// @Override +// public List findByName(String query) { +// return events.stream() +// .filter(e -> e.getName().toLowerCase().contains(query.toLowerCase())) +// .collect(java.util.stream.Collectors.toList()); +// } +// } +// +// /** Fake presenter to capture output */ +// static class TestOutputBoundary implements DisplayLocalEventsOutputBoundary { +// DisplayLocalEventsOutputData successData; +// String errorMessage; +// boolean successCalled = false; +// boolean errorCalled = false; +// +// @Override +// public void presentSuccess(DisplayLocalEventsOutputData outputData) { +// this.successCalled = true; +// this.successData = outputData; +// } +// +// @Override +// public void presentError(String errorMessage) { +// this.errorCalled = true; +// this.errorMessage = errorMessage; +// } +// } +// +// public static void main(String[] args) { +// System.out.println("=== Running DisplayLocalEventsInteractorSelfTest ==="); +// +// testInvalidInput(); +// testFilteringAndSorting(); +// +// System.out.println("=== All tests completed ==="); +// } +// +// private static void testInvalidInput() { +// System.out.println("Test 1: invalid input"); +// +// DisplayLocalEventsInputData input = +// new DisplayLocalEventsInputData(null, -10, null); +// +// InMemoryEventRepository repo = new InMemoryEventRepository(List.of()); +// TestOutputBoundary presenter = new TestOutputBoundary(); +// DisplayLocalEventsInteractor interactor = +// new DisplayLocalEventsInteractor(repo, presenter); +// +// interactor.execute(input); +// +// if (presenter.errorCalled && !presenter.successCalled) { +// System.out.println("PASS: Error reported successfully."); +// } else { +// System.out.println("FAIL: Expected error but got success."); +// } +// } +// +// private static void testFilteringAndSorting() { +// System.out.println("Test 2: filtering by distance + category"); +// +// Location userLoc = new Location("User", 43.6532, -79.3832); +// +// Location near = new Location("Near", 43.6533, -79.3833); +// Location far = new Location("Far", 40.0, -74.0); +// +// Event nearMusic = new Event( +// "1", "Near Music", "desc", +// EventCategory.MUSIC, +// near, +// LocalDateTime.now(), +// "" +// ); +// +// Event farSports = new Event( +// "2", "Far Sports", "desc", +// EventCategory.SPORTS, +// far, +// LocalDateTime.now(), +// "" +// ); +// +// InMemoryEventRepository repo = +// new InMemoryEventRepository(List.of(nearMusic, farSports)); +// +// TestOutputBoundary presenter = new TestOutputBoundary(); +// DisplayLocalEventsInteractor interactor = +// new DisplayLocalEventsInteractor(repo, presenter); +// +// DisplayLocalEventsInputData input = +// new DisplayLocalEventsInputData(userLoc, 5.0, EventCategory.MUSIC); +// +// interactor.execute(input); +// +// if (!presenter.successCalled) { +// System.out.println("FAIL: Expected success."); +// return; +// } +// +// DisplayLocalEventsOutputData out = presenter.successData; +// +// if (out.getTotalEvents() == 1 && out.getEvents().get(0).getId().equals("1")) { +// System.out.println("PASS: Correct event filtered."); +// } else { +// System.out.println("FAIL: Incorrect filtering result."); +// } +// } +//} \ No newline at end of file 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..3e4723d3 --- /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); diff --git a/src/test/java/use_case/save_event/SaveEventInteractorTest.java b/src/test/java/use_case/save_event/SaveEventInteractorTest.java new file mode 100644 index 00000000..56bfe22a --- /dev/null +++ b/src/test/java/use_case/save_event/SaveEventInteractorTest.java @@ -0,0 +1,339 @@ +package use_case.save_event; + +import data_access.FileSavedEventsDataAccessObject; +import entity.Event; +import entity.EventCategory; +import entity.Location; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import use_case.login.LoginUserDataAccessInterface; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class for SaveEventInteractor + * Tests the business logic for saving events + */ +class SaveEventInteractorTest { + + private SaveEventInteractor interactor; + private MockSaveEventPresenter mockPresenter; + private MockFileSavedEventsDAO mockSavedEventsDAO; + private MockLoginUserDataAccess mockUserDataAccess; + private Event testEvent; + + @BeforeEach + void setUp() { + mockPresenter = new MockSaveEventPresenter(); + mockSavedEventsDAO = new MockFileSavedEventsDAO(); + mockUserDataAccess = new MockLoginUserDataAccess(); + + interactor = new SaveEventInteractor( + mockPresenter, + mockSavedEventsDAO, + mockUserDataAccess + ); + + Location location = new Location("Scotiabank Arena, Toronto, ON", 43.6435, -79.3791); + testEvent = new Event( + "evt123", + "Toronto Raptors vs Lakers", + "Exciting NBA game", + "Scotiabank Arena", + EventCategory.SPORTS, + location, + LocalDateTime.of(2025, 11, 28, 19, 30), + "https://example.com/image.jpg" + ); + } + + @Test + void testExecute_SuccessfulSave() { + mockUserDataAccess.setCurrentUsername("testUser"); + SaveEventInputData inputData = new SaveEventInputData(testEvent); + + interactor.execute(inputData); + + assertTrue(mockPresenter.isSuccessViewPrepared(), "Success view should be prepared"); + assertFalse(mockPresenter.isFailureViewPrepared(), "Failure view should not be prepared"); + assertEquals(testEvent, mockPresenter.getOutputData().getEvent(), "Output event should match input"); + assertTrue(mockSavedEventsDAO.wasSaveCalled(), "Save should be called on DAO"); + assertEquals("testUser", mockSavedEventsDAO.getLastSavedUsername(), "Username should match"); + assertEquals(testEvent, mockSavedEventsDAO.getLastSavedEvent(), "Saved event should match"); + } + + @Test + void testExecute_NullEvent() { + SaveEventInputData inputData = new SaveEventInputData(null); + + interactor.execute(inputData); + + assertTrue(mockPresenter.isFailureViewPrepared(), "Failure view should be prepared"); + assertFalse(mockPresenter.isSuccessViewPrepared(), "Success view should not be prepared"); + assertEquals("No Event Found", mockPresenter.getErrorMessage()); + assertFalse(mockSavedEventsDAO.wasSaveCalled(), "Save should not be called for null event"); + } + + @Test + void testExecute_EventAlreadySaved() { + mockUserDataAccess.setCurrentUsername("testUser"); + mockSavedEventsDAO.addSavedEvent("testUser", testEvent); // Pre-save the event + SaveEventInputData inputData = new SaveEventInputData(testEvent); + + interactor.execute(inputData); + + assertTrue(mockPresenter.isFailureViewPrepared(), "Failure view should be prepared"); + assertFalse(mockPresenter.isSuccessViewPrepared(), "Success view should not be prepared"); + assertEquals("Event already saved", mockPresenter.getErrorMessage()); + } + + @Test + void testGetSavedEvents_WithValidUser() { + mockUserDataAccess.setCurrentUsername("testUser"); + Event event1 = createTestEvent("evt1", "Event 1"); + Event event2 = createTestEvent("evt2", "Event 2"); + mockSavedEventsDAO.addSavedEvent("testUser", event1); + mockSavedEventsDAO.addSavedEvent("testUser", event2); + + List savedEvents = interactor.getSavedEvents(); + + assertNotNull(savedEvents); + assertEquals(2, savedEvents.size()); + assertTrue(savedEvents.contains(event1)); + assertTrue(savedEvents.contains(event2)); + } + + @Test + void testGetSavedEvents_NoCurrentUser() { + // Arrange + mockUserDataAccess.setCurrentUsername(null); + + // Act + List savedEvents = interactor.getSavedEvents(); + + // Assert + assertNotNull(savedEvents); + assertTrue(savedEvents.isEmpty(), "Should return empty list when no current user"); + } + + @Test + void testGetSavedEvents_UserHasNoSavedEvents() { + mockUserDataAccess.setCurrentUsername("userWithNoEvents"); + + List savedEvents = interactor.getSavedEvents(); + + assertNotNull(savedEvents); + assertTrue(savedEvents.isEmpty(), "Should return empty list for user with no saved events"); + } + + @Test + void testRemoveEvent_Success() { + mockUserDataAccess.setCurrentUsername("testUser"); + mockSavedEventsDAO.addSavedEvent("testUser", testEvent); + + interactor.removeEvent(testEvent); + + assertTrue(mockSavedEventsDAO.wasRemoveCalled(), "Remove should be called on DAO"); + assertEquals("testUser", mockSavedEventsDAO.getLastRemovedUsername()); + assertEquals(testEvent, mockSavedEventsDAO.getLastRemovedEvent()); + } + + @Test + void testRemoveEvent_NoCurrentUser() { + mockUserDataAccess.setCurrentUsername(null); + + interactor.removeEvent(testEvent); + + assertFalse(mockSavedEventsDAO.wasRemoveCalled(), "Remove should not be called when no user"); + } + + @Test + void testSwitchToDashboardView() { + interactor.switchToDashboardView(); + + assertTrue(mockPresenter.wasSwitchToDashboardCalled(), "Switch to dashboard should be called on presenter"); + } + + @Test + void testExecute_MultipleEventsSameName() { + mockUserDataAccess.setCurrentUsername("testUser"); + Event event1 = createTestEvent("evt1", "Same Name"); + Event event2 = createTestEvent("evt2", "Same Name"); + + interactor.execute(new SaveEventInputData(event1)); + interactor.execute(new SaveEventInputData(event2)); + + List savedEvents = interactor.getSavedEvents(); + assertEquals(2, savedEvents.size(), "Should save both events even with same name"); + } + + private Event createTestEvent(String id, String name) { + Location location = new Location("Test Venue", 43.0, -79.0); + return new Event( + id, + name, + "Test description", + "Test Address", + EventCategory.MUSIC, + location, + LocalDateTime.now().plusDays(1), + "" + ); + } + + /** + * Mock presenter for testing + */ + private static class MockSaveEventPresenter implements SaveEventOutputBoundary { + private boolean successViewPrepared = false; + private boolean failureViewPrepared = false; + private boolean switchToDashboardCalled = false; + private SaveEventOutputData outputData; + private String errorMessage; + + @Override + public void prepareSuccessView(SaveEventOutputData outputData) { + this.successViewPrepared = true; + this.outputData = outputData; + } + + @Override + public void prepareFailureView(String errorMessage) { + this.failureViewPrepared = true; + this.errorMessage = errorMessage; + } + + @Override + public void switchToDashboardView() { + this.switchToDashboardCalled = true; + } + + public boolean isSuccessViewPrepared() { + return successViewPrepared; + } + + public boolean isFailureViewPrepared() { + return failureViewPrepared; + } + + public boolean wasSwitchToDashboardCalled() { + return switchToDashboardCalled; + } + + public SaveEventOutputData getOutputData() { + return outputData; + } + + public String getErrorMessage() { + return errorMessage; + } + } + + /** + * Mock FileSavedEventsDataAccessObject for testing + */ + private static class MockFileSavedEventsDAO extends FileSavedEventsDataAccessObject { + private boolean saveCalled = false; + private boolean removeCalled = false; + private String lastSavedUsername; + private Event lastSavedEvent; + private String lastRemovedUsername; + private Event lastRemovedEvent; + private java.util.Map> storage = new java.util.HashMap<>(); + + @Override + public void saveEvent(String username, Event event) { + this.saveCalled = true; + this.lastSavedUsername = username; + this.lastSavedEvent = event; + storage.computeIfAbsent(username, k -> new java.util.ArrayList<>()).add(event); + } + + @Override + public void removeEvent(String username, Event event) { + this.removeCalled = true; + this.lastRemovedUsername = username; + this.lastRemovedEvent = event; + List events = storage.get(username); + if (events != null) { + events.removeIf(e -> e.getId().equals(event.getId())); + } + } + + @Override + public List getSavedEvents(String username) { + return storage.getOrDefault(username, new java.util.ArrayList<>()); + } + + @Override + public boolean isSavedEvent(String username, String id) { + List events = storage.get(username); + if (events == null) return false; + return events.stream().anyMatch(e -> e.getId().equals(id)); + } + + public void addSavedEvent(String username, Event event) { + storage.computeIfAbsent(username, k -> new java.util.ArrayList<>()).add(event); + } + + public boolean wasSaveCalled() { + return saveCalled; + } + + public boolean wasRemoveCalled() { + return removeCalled; + } + + public String getLastSavedUsername() { + return lastSavedUsername; + } + + public Event getLastSavedEvent() { + return lastSavedEvent; + } + + public String getLastRemovedUsername() { + return lastRemovedUsername; + } + + public Event getLastRemovedEvent() { + return lastRemovedEvent; + } + } + + /** + * Mock LoginUserDataAccessInterface for testing + */ + private static class MockLoginUserDataAccess implements LoginUserDataAccessInterface { + private String currentUsername; + + public void setCurrentUsername(String username) { + this.currentUsername = username; + } + + @Override + public String getCurrentUsername() { + return currentUsername; + } + + // Implement other required methods from interface + @Override + public boolean existsByName(String identifier) { + return false; + } + + @Override + public void save(entity.User user) { + // Not needed for this test + } + + @Override + public entity.User get(String username) { + return null; + } + + } +} \ No newline at end of file diff --git a/src/test/java/use_case/search_event/SearchEventByNameInteractorTest.java b/src/test/java/use_case/search_event/SearchEventByNameInteractorTest.java new file mode 100644 index 00000000..55020b61 --- /dev/null +++ b/src/test/java/use_case/search_event/SearchEventByNameInteractorTest.java @@ -0,0 +1,4 @@ +package use_case.search_event; + +public class SearchEventByNameInteractorTest { +}