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..f29d3ed5 100644 --- a/README.md +++ b/README.md @@ -1,248 +1,118 @@ -# Clean Architecture Team Lab Activity: Login and Logout +#csc-207-project-event-discovery +# 207 Final Project: Ticket Monster +- Have you ever wanted to find local events quick and fast near you? Well, look no further! Ticket Monster is here to help you find places to laugh, cry, and have an overall good time in no time at all! -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](#usage-guide) +- [Contributors](#contributors) +- [PR Style](#style) +- [Setup](#setup) +- [Features](#features) +- [Links](#links) -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.** +# Features +- 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 -**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) +# Contributors +1. Christopher Mong +2. Joyi Xue +3. Min-Hsuan (Angelie) Wu +4. Yifei Yang ---- -## Task 1: Understanding the Program -Open the project in IntelliJ. Open `app.Main` and read it as a team. -- What are the currently implemented Views and Use Cases in the program? -- Which Use Cases are triggered from each View? -- Which version of the DAO is `app.Main` using? +# Style +- [chore]: Used to represent menial tasks that were simple fixes or style cleanup. +- [feat/Feature]: used to represent a large task that is a new feature to the program. -> 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. +# 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. -**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. +Screenshot 2025-11-24 at 6 59 17 PM -> 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. +# Usage Guide +## Features -### Task 1.1: Exploring the login use case +### Searching for Events -Let's take a tour of the login use case code: +#### 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 + +Screenshot 2025-12-01 at 10 22 06 PM -- In IntelliJ, find the `LoginController` class and open it. +Screenshot 2025-12-01 at 10 22 35 PM -- Set a breakpoint inside its `execute` method. +#### 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) + +Screenshot 2025-12-01 at 10 22 58 PM -- Run the program in debug mode. +#### Via Location +1. Sign Up / Log In +2. If you wish to change the location in viewing events, click the search bar at the top of the program +3. Type in anything from a specific address to a general city -- 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. +Screenshot 2025-12-01 at 10 23 13 PM -- **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 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 -> 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 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 -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. +Screenshot 2025-12-01 at 10 23 52 PM -## Task 2: Implementing the Logout Use Case -Currently, you'll notice that the "Log Out" button in the `LoggedInView` still doesn't actually log you out of the program. Let's fix this. +#### ViA Calender +1. Sign Up / Log In +2. Click on the calender button at the left side of the dashboard +3. You will see a calendar view, +4. Clicking on a specific date shows all the events occurring on that day -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.** +Screenshot 2025-12-01 at 10 24 06 PM -> The next part of the readme describes how your team will do this. -Your team will know when you are done when: +**Example Searches:** +- `Raptors` → Find Toronto Raptors basketball games +- `Concert` → Find all concerts in your area -- 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. +#### Changing Search Location +The default search location is Toronto, ON. -### Task 2.1: Dividing up the work +## Links +MVP Planning: https://docs.google.com/document/d/1tQHwKZNdJIcIRBtkS7yiY-cOgxgPninWoLvrRGcAmMo/edit?usp=sharing +Slides: https://docs.google.com/presentation/d/1xiP1yBKb-so6dTxyAJHRgGxrDK1V3najGgcjckkXNWE/edit?slide=id.g3ab8aa55bac_2_0#slide=id.g3ab8aa55bac_2_0 -There are `TODO` comments left in the files. +## Future Plans: +The next key step for this idea to launch, is implementing a way for users to actually purchase the ticket on the program. Although this is not possible through Ticketmaster as it is strictly forbidden to use their events with a third-party payment, there are other possible ways to fetch events, and even more to implement a payment method! We would use an API like Stripe, easy to learn and very well documented. -> Recall that you can use the TODO tool window to conveniently pull up a complete list. +#### Special Thanks to: Professor Calver, Professor Gries, TA Wang for helping us along our journey! -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..12384ecb --- /dev/null +++ b/events_saved/c_events.json @@ -0,0 +1,13 @@ +[{ + "imageUrl": "https://s1.ticketm.net/dam/a/810/587e669b-5946-46c0-a526-b63b6e008810_1339651_CUSTOM.jpg", + "name": "Toronto Raptors vs. Charlotte Hornets", + "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-12-06T00:30", + "location": { + "address": "Scotiabank Arena, 50 Bay Street, Suite 500, Toronto, ON", + "latitude": 43.6439254, + "longitude": -79.3783234 + }, + "id": "1AvZZbsGklFLNdk", + "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/events_saved/joyi_events.json b/events_saved/joyi_events.json new file mode 100644 index 00000000..d0a870ea --- /dev/null +++ b/events_saved/joyi_events.json @@ -0,0 +1,93 @@ +[ + { + "imageUrl": "https://s1.ticketm.net/dam/a/032/92295cd8-69d8-4699-ada5-3f02fc494032_EVENT_DETAIL_PAGE_16_9.jpg", + "name": "Shrek Rave", + "description": "Legal Age 19+ (Valid Photo I.D. Required)MODO-LIVE Presents:Shrek Rave January 30, 2026Lees PalaceToronto, ON", + "startTime": "2026-01-31T03:00", + "location": { + "address": "Lee's Palace, 529 Bloor Street West, Toronto, ON", + "latitude": 43.665249, + "longitude": -79.409447 + }, + "id": "rZ7HnEZ1Af_I8N", + "category": "MUSIC" + }, + { + "imageUrl": "https://s1.ticketm.net/dam/a/516/09e01ba9-6525-4630-bd27-df2bd2e2a516_CUSTOM.jpg", + "name": "Belmont with Avoid, Super Sometimes & Lostvoy", + "description": "Legal Age 19+ (Valid Photo I.D. Required)MODO-LIVE presents..BELMONTw/ Avoid, Super Sometimes & Lostvoy Monday, December 8th, 2025Lees PalaceToronto, ON", + "startTime": "2025-12-09T00:00", + "location": { + "address": "Lee's Palace, 529 Bloor Street West, Toronto, ON", + "latitude": 43.665249, + "longitude": -79.409447 + }, + "id": "rZ7HnEZ1AfGI-K", + "category": "MUSIC" + }, + { + "imageUrl": "https://s1.ticketm.net/dam/c/fbc/b293c0ad-c904-4215-bc59-8d7f2414dfbc_106141_EVENT_DETAIL_PAGE_16_9.jpg", + "name": "The Coldplay Experience & Desire: A U2 Tribute", + "description": "Join us for an unforgettable night of music as two iconic tribute bands take the stage at the legendary El Mocambo!The Coldplay Experience will bring you the soaring anthems and emotive melodies of Coldplay, perfectly capturing the essence of their unforgettable live performances. With stunning visuals and heartfelt renditions, get ready to immerse yourself in classics like Yellow, Fix You, and Viva La Vida that have defined a generation.Desire will follow, delivering a captivating tribute to U2, one of rock historys most celebrated bands. Experience the powerful energy of hits like With or Without You, Sunday Bloody Sunday, and Beautiful Day, brought to life with an electrifying stage presence and soulful performances.What to Expect:Full Multi-Media Show: A complete sensory experience, featuring high-quality visuals, lights, and sound design thatll transport you into the heart of a live concert.Two Bands, One Night: Enjoy the unique talents of both tribute acts, each delivering their own spin on the revered catalogues of Coldplay and U2.Iconic Venue: The historic El Mocambo will provide the perfect backdrop for this extraordinary musical journey, known for hosting legendary acts and unforgettable live performances.Dont miss this chance to relive your favourite hits and create lasting memories with fellow fans! Get ready for a night filled with emotion, nostalgia, and some of the best music of our time.Secure your spot for a night of pure musical bliss!", + "startTime": "2026-01-18T02:00", + "location": { + "address": "The Starlight Room at The El Mocambo, 464 Spadina Avenue, Toronto, ON", + "latitude": 43.6575661, + "longitude": -79.4001349 + }, + "id": "rZ7HnEZ1AfjooS", + "category": "MUSIC" + }, + { + "imageUrl": "https://i.ticketweb.com/i/00/12/70/34/74_Edp.jpg?v=7", + "name": "Permanent Waves", + "description": "19+ Event Only (Must Show Identification at Door)*El Mocambo is a fully accessible venue*19+ Event OnlyDoors Open at 7:30 PM", + "startTime": "2026-02-22T01:00", + "location": { + "address": "Under the Neon Palms at the El Mocambo, 464 Spadina Avenue, Toronto, ON", + "latitude": 43.65758, + "longitude": -79.4002 + }, + "id": "rZ7HnEZ1AKPA1f", + "category": "MUSIC" + }, + { + "imageUrl": "https://s1.ticketm.net/dam/c/df8/81eadad8-4449-412e-a2b1-3d8bbb78edf8_106181_EVENT_DETAIL_PAGE_16_9.jpg", + "name": "Satin Jackets with Special Guests", + "description": "Legal Age 19+ (Valid Photo I.D. Required)Modo Live & Programme Presents...Satin Jacketsw/ Special Guests Saturday December 13, 2025Lees PalaceToronto, ON", + "startTime": "2025-12-14T03:00", + "location": { + "address": "Lee's Palace, 529 Bloor Street West, Toronto, ON", + "latitude": 43.665249, + "longitude": -79.409447 + }, + "id": "rZ7HnEZ1AKqV80", + "category": "MUSIC" + }, + { + "imageUrl": "https://s1.ticketm.net/dam/c/48b/2352e3b5-8496-496b-97a3-e605177e848b_105851_EVENT_DETAIL_PAGE_16_9.jpg", + "name": "BLACKGOLD Motown's Greatest Hits!", + "description": "BLACK GOLD is a talent-packed show featuring six award-winning vocalists and a stalwart band of five musicians performing the golden hits of Motown, Rs Day with the legendary music of such icons as The Four Tops, The Temptations, Donna Summer, The Supremes, Tina Turner, Lionel Ritchie, Whitney Houston, Michael Jackson plus others. From romantic love songs to uplifting solid gold hits that touch heart and soul. Perfect entertainment for the most romantic day of the year!https://www.diamondentertainment.ca/black-gold/The El Mocambo is a 19+ venue.", + "startTime": "2026-02-15T01:00", + "location": { + "address": "Under the Neon Palms at the El Mocambo, 464 Spadina Avenue, Toronto, ON", + "latitude": 43.65758, + "longitude": -79.4002 + }, + "id": "rZ7HnEZ1Af_wuN", + "category": "MUSIC" + }, + { + "imageUrl": "https://i.ticketweb.com/i/00/13/26/06/03_Edp.jpg?v=4", + "name": "The Holly Jolly JCI Christmas Spectacular", + "description": "Celebrating the release of their sensational seasonal 7 single.With special guests George Westerholm and Tanya Philipovich.", + "startTime": "2025-12-14T01:30", + "location": { + "address": "Under the Neon Palms at the El Mocambo, 464 Spadina Avenue, Toronto, ON", + "latitude": 43.65758, + "longitude": -79.4002 + }, + "id": "rZ7HnEZ1AfbUjS", + "category": "MUSIC" + } +] \ No newline at end of file diff --git a/saved_events.csv b/saved_events.csv new file mode 100644 index 00000000..402e9df4 --- /dev/null +++ b/saved_events.csv @@ -0,0 +1,9 @@ +username,eventId,eventName,category,dateTime,address,latitude,longitude,imageUrl,description +c,17kZvxG61h76gZf,"Ash",MUSIC,2025-12-11T19:00:00,"The Academy, 57 Middle Abbey Street, Dublin",53.34804959,-6.26185298,"https://s1.ticketm.net/dam/a/57d/cdac65ba-c49c-4425-8182-a00abcbe557d_RETINA_PORTRAIT_3_2.jpg","Over 18s - ID Required" +c,vvG1YZbgkxmVOg,"3 O'clock Sunday Evening Musical: Featuring KGGR Community Choir",ARTS_THEATRE,2026-01-11T21:00:00,"Majestic Theatre Dallas, 1925 Elm Street, Dallas, TX",32.78232551,-96.79557625,"https://s1.ticketm.net/dam/a/c94/51653144-df22-4fad-82b9-696d923a3c94_EVENT_DETAIL_PAGE_16_9.jpg","In the spirit of the Black Church music tradition, three soul-steering mass choirs participate in a three-part concert series in reminiscence of a Sunday evening custom when congregants gather at various churches to experience some of the best choir musicals performing hymns, old fashion gospel and contemporary music." +joyi,rZ7HnEZ1Af4efS,"GOVERNOR DOUGLAS - Celebrating 10 years",MUSIC,2025-11-30T01:30:00,"Under the Neon Palms at the El Mocambo, 464 Spadina Avenue, Toronto, ON",43.65758,-79.4002,"https://s1.ticketm.net/dam/c/060/c5c08e7a-9912-456c-a060-2758be94e060_105881_RETINA_PORTRAIT_16_9.jpg","A seven-piece cover band bringing unstoppable energy, dance-floor classics, and sing-along anthems youll scream at the top of your lungs every show a party youll never forget." +c,1AvZZbsGklFLNdk,"Toronto Raptors vs. Charlotte Hornets",SPORTS,2025-12-06T00:30:00,"Scotiabank Arena, 50 Bay Street, Suite 500, Toronto, ON",43.6439254,-79.3783234,"https://s1.ticketm.net/dam/a/810/587e669b-5946-46c0-a526-b63b6e008810_1339651_CUSTOM.jpg","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" +c,vvG1YZbgkxmVOg,"3 O'clock Sunday Evening Musical: Featuring KGGR Community Choir",ARTS_THEATRE,2026-01-11T21:00:00,"Majestic Theatre Dallas, 1925 Elm Street, Dallas, TX",32.78232551,-96.79557625,"https://s1.ticketm.net/dam/a/c94/51653144-df22-4fad-82b9-696d923a3c94_EVENT_DETAIL_PAGE_16_9.jpg","In the spirit of the Black Church music tradition, three soul-steering mass choirs participate in a three-part concert series in reminiscence of a Sunday evening custom when congregants gather at various churches to experience some of the best choir musicals performing hymns, old fashion gospel and contemporary music." +c,LvZ18QuA_JCmNqZZnE4il,"Tianyu Lights Festival Season Pass",ARTS_THEATRE,2025-11-08T00:00:00,"Texas Trust Festival Grounds, 1150 Pollock Pl, Grand Prairie, TX",32.76989118,-96.98335838,"https://s1.ticketm.net/dam/a/384/9321d069-ab01-4255-bc10-b9cabc278384_TABLET_LANDSCAPE_3_2.jpg","Unlimited entry for one person age 3 and above.  Perfect for guests who plan to visit the festival multiple times in the winter holidays.  " +joyi,rZ7HnEZ1Af4efS,"GOVERNOR DOUGLAS - Celebrating 10 years",MUSIC,2025-11-30T01:30:00,"Under the Neon Palms at the El Mocambo, 464 Spadina Avenue, Toronto, ON",43.65758,-79.4002,"https://s1.ticketm.net/dam/c/060/c5c08e7a-9912-456c-a060-2758be94e060_105881_RETINA_PORTRAIT_16_9.jpg","A seven-piece cover band bringing unstoppable energy, dance-floor classics, and sing-along anthems youll scream at the top of your lungs every show a party youll never forget." +testtest,rZ7HnEZ1AKPA1f,"Permanent Waves",MUSIC,2026-02-22T01:00:00,"Under the Neon Palms at the El Mocambo, 464 Spadina Avenue, Toronto, ON",43.65758,-79.4002,"https://i.ticketweb.com/i/00/12/70/34/74_Edp.jpg?v=7","19+ Event Only (Must Show Identification at Door)*El Mocambo is a fully accessible venue*19+ Event OnlyDoors Open at 7:30 PM" diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 3c654cad..e7fdf785 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,26 @@ 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 use_case.change_password.ChangePasswordInputBoundary; import use_case.change_password.ChangePasswordInteractor; import use_case.change_password.ChangePasswordOutputBoundary; @@ -23,32 +59,56 @@ 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.LoggedInView; -import view.LoginView; -import view.SignupView; -import view.ViewManager; + +import use_case.display_local_events.DisplayLocalEventsInputBoundary; +import use_case.display_local_events.DisplayLocalEventsInteractor; +import use_case.display_local_events.DisplayLocalEventsOutputBoundary; + + +import data_access.GeocodingService; +import data_access.GeocodingDataAccessInterface; +import interface_adapter.update_location.UpdateLocationController; +import interface_adapter.update_location.UpdateLocationPresenter; +import interface_adapter.update_location.UpdateLocationViewModel; +import use_case.update_location.UpdateLocationInputBoundary; +import use_case.update_location.UpdateLocationInteractor; +import use_case.update_location.UpdateLocationOutputBoundary; + +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. + */ public class AppBuilder { + private EventDescriptionViewModel eventDescriptionViewModel; + private EventDescriptionView eventDescriptionView; + private UpdateLocationViewModel updateLocationViewModel; + + 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,11 +116,45 @@ 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); } + public AppBuilder addUpdateLocationUseCase() { + updateLocationViewModel = new UpdateLocationViewModel(); + + GeocodingDataAccessInterface geocodingService = new GeocodingService(); + UpdateLocationOutputBoundary presenter = new UpdateLocationPresenter(updateLocationViewModel); + UpdateLocationInputBoundary interactor = new UpdateLocationInteractor( + userDataAccessObject, + geocodingService, + presenter + ); + + UpdateLocationController controller = new UpdateLocationController(interactor); + + displayLocalEventsView.setUpdateLocationController(controller); + displayLocalEventsView.setUpdateLocationViewModel(updateLocationViewModel); + displayLocalEventsView.setUserDataAccess(userDataAccessObject); + + return this; + } + public AppBuilder addSignupView() { signupViewModel = new SignupViewModel(); signupView = new SignupView(signupViewModel); @@ -82,6 +176,93 @@ public AppBuilder addLoggedInView() { return this; } + public AppBuilder addCalendarViews() { + calendarFlowViewModel = new CalendarFlowViewModel(); + eventListByDateView = new EventListByDateView(calendarFlowViewModel); + eventListByDateView.setViewManagerModel(viewManagerModel); + calendarView = new CalendarView(); + calendarView.setViewManagerModel(viewManagerModel); + cardPanel.add(calendarView, "calendar view"); + cardPanel.add(eventListByDateView, eventListByDateView.getViewName()); + return this; + } + + public AppBuilder addDisplayLocalEventsView() { + displayLocalEventsViewModel = new DisplayLocalEventsViewModel(); + displayLocalEventsView = new DisplayLocalEventsView(displayLocalEventsViewModel); + displayLocalEventsView.setViewManagerModel(viewManagerModel); + displayLocalEventsView.setCalendarView(calendarView); + 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); + saveEventsView.setViewManagerModel(viewManagerModel); + 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 (eventDescriptionView != null && saveEventInteractor != null) { + eventDescriptionView.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 +276,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 +297,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 +309,143 @@ 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); + eventListByDateView.setController(calendarController); + + // Wire up navigation from event list (calendar) to event description + eventListByDateView.setEventSelectionListener(new EventListByDateView.EventSelectionListener() { + @Override + public void onEventSelected(Event event) { + // Display the event in EventDescriptionView + eventDescriptionView.displayEvent(event); + + // Set previous view so back button returns to event list + eventDescriptionView.setPreviousView("event list by date"); + + // Calculate distance if user location is available + 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 event description view + viewManagerModel.setState(eventDescriptionView.getViewName()); + viewManagerModel.firePropertyChange(); + } + }); + + 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, saveEventViewModel); + + DisplayLocalEventsInputBoundary interactor = + new DisplayLocalEventsInteractor(eventRepository, outputBoundary); + + DisplayLocalEventsController controller = + new DisplayLocalEventsController(interactor); + + displayLocalEventsView.setController(controller); + displayLocalEventsView.setSearchBarController(searchController); + + return this; + } + + /** + * Add Event Description View. + */ + 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. + */ + 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 + displayLocalEventsView.setEventSelectionListener(new DisplayLocalEventsView.EventSelectionListener() { + @Override + public void onEventSelected(Event event) { + // Store the event in the view for display + eventDescriptionView.displayEvent(event); + + // Set previous view so back button returns to dashboard + eventDescriptionView.setPreviousView("display local events"); + + // 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("Ticketmaster Program"); application.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); application.add(cardPanel); @@ -143,6 +455,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..5c41ff03 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -3,15 +3,35 @@ 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() + .addDisplayLocalEventsView() + .addSaveEventView() + .addDisplayLocalEventsView() + .addEventSearchView() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() + .addLogoutUseCase() + .addCalendarFlowUseCase() + .addSaveEventUseCase() + .addEventDescriptionUseCase() + .addDisplayLocalEventsUseCase() + .addSearchEventByNameUseCase() + .addUpdateLocationUseCase() .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..637960f5 --- /dev/null +++ b/src/main/java/data_access/CalendarFlowDataAccessObject.java @@ -0,0 +1,360 @@ +package data_access; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONObject; + +import entity.Event; +import entity.EventCategory; +import entity.Location; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +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 = "oL2pW4zAlAZvhBAhPNi5mNYvS7OsBM9J"; + private final OkHttpClient client = new OkHttpClient(); + + /** + * 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) { + final HttpUrl.Builder urlBuilder = HttpUrl + .parse(BASE_URL + EVENTS_ENDPOINT) + .newBuilder() + .addQueryParameter("apikey", API_KEY); + + final ZonedDateTime startZdt = date.atStartOfDay(ZoneOffset.UTC); + final ZonedDateTime endZdt = date.atTime(23, 59, 59).atZone(ZoneOffset.UTC); + final DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT; + + 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"); + + final String url = urlBuilder.build().toString(); + return fetchEvents(url); + } + + private List fetchEvents(String url) { + final List events = new ArrayList<>(); + + final 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; + } + + final String jsonResponse = response.body().string(); + final JSONObject root = new JSONObject(jsonResponse); + + if (!root.has("_embedded")) { + return events; + } + + final JSONObject embedded = root.getJSONObject("_embedded"); + if (!embedded.has("events")) { + return events; + } + + JSONArray eventsArray = embedded.getJSONArray("events"); + + for (int i = 0; i < eventsArray.length(); i++) { + final JSONObject eventJson = eventsArray.getJSONObject(i); + Event event = parseEvent(eventJson); + if (event != null) { + events.add(event); + } + } + + } + catch (IOException evt) { + System.err.println("Error fetching events: " + evt.getMessage()); + } + + return events; + } + + private Event parseEvent(JSONObject eventJson) { + try { + final String id = eventJson.getString("id"); + final 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"); + } + + final EventCategory category = extractCategory(eventJson); + final Location location = extractLocation(eventJson); + if (location == null) { + return null; + } + + final LocalDateTime startTime = extractStartTime(eventJson); + if (startTime == null) { + return null; + } + + final String imageUrl = extractImageUrl(eventJson); + + return new Event(id, name, description, location.getAddress(), category, location, startTime, imageUrl); + + } + catch (Exception evt) { + System.err.println("Error parsing event: " + evt.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 evt) { + System.err.println("Error extracting image URL: " + evt.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 evt) { + System.err.println("Error extracting category: " + evt.getMessage()); + } + + return EventCategory.MISCELLANEOUS; + } + + private Location extractLocation(JSONObject eventJson) { + try { + if (!eventJson.has("_embedded")) { + return null; + } + + final JSONObject embedded = eventJson.getJSONObject("_embedded"); + if (!embedded.has("venues")) { + return null; + } + + final JSONArray venues = embedded.getJSONArray("venues"); + if (venues.length() == 0) { + return null; + } + + final JSONObject venue = venues.getJSONObject(0); + + final StringBuilder addressBuilder = new StringBuilder(); + + if (venue.has("name")) { + addressBuilder.append(venue.getString("name")); + } + + if (venue.has("address")) { + final JSONObject address = venue.getJSONObject("address"); + if (address.has("line1")) { + if (addressBuilder.length() > 0) { + addressBuilder.append(", "); + } + addressBuilder.append(address.getString("line1")); + } + } + + if (venue.has("city")) { + final JSONObject city = venue.getJSONObject("city"); + if (city.has("name")) { + if (addressBuilder.length() > 0) { + addressBuilder.append(", "); + } + addressBuilder.append(city.getString("name")); + } + } + + if (venue.has("state")) { + final 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; + } + + final double latitude = locationJson.getDouble("latitude"); + final double longitude = locationJson.getDouble("longitude"); + + return new Location(address, latitude, longitude); + + } + catch (Exception evt) { + System.err.println("Error extracting location: " + evt.getMessage()); + return null; + } + } + + private LocalDateTime extractStartTime(JSONObject eventJson) { + try { + if (!eventJson.has("dates")) { + return null; + } + + final JSONObject dates = eventJson.getJSONObject("dates"); + if (!dates.has("start")) { + return null; + } + + final JSONObject start = dates.getJSONObject("start"); + + if (start.has("dateTime")) { + String dateTimeStr = start.getString("dateTime"); + dateTimeStr = dateTimeStr.replace("Z", ""); + final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + return LocalDateTime.parse(dateTimeStr, formatter); + } + else if (start.has("localDate")) { + final 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 evt) { + System.err.println("Error extracting start time: " + evt.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; + } + + final 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/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..7b5f345c --- /dev/null +++ b/src/main/java/data_access/EventDataAccessObject.java @@ -0,0 +1,405 @@ +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 = "oL2pW4zAlAZvhBAhPNi5mNYvS7OsBM9J"; // 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) { + String url = HttpUrl.parse(BASE_URL + EVENTS_ENDPOINT) + .newBuilder() + .addQueryParameter("apikey", API_KEY) + .addQueryParameter("latlong", + String.format("%.6f,%.6f", location.getLatitude(), location.getLongitude())) + .addQueryParameter("radius", String.valueOf((int) radiusKm)) + .addQueryParameter("unit", "km") + .addQueryParameter("size", "50") + .addQueryParameter("sort", "distance,asc") + .build() + .toString(); + + return fetchEvents(url); + } + + /** + * Get events filtered by category + */ + public List getEventsByCategory(Location location, double radiusKm, EventCategory category) { + HttpUrl.Builder urlBuilder = HttpUrl.parse(BASE_URL + EVENTS_ENDPOINT) + .newBuilder() + .addQueryParameter("apikey", API_KEY) + .addQueryParameter("latlong", + String.format("%.6f,%.6f", location.getLatitude(), location.getLongitude())) + .addQueryParameter("radius", String.valueOf((int) radiusKm)) + .addQueryParameter("unit", "km") + .addQueryParameter("size", "50") + .addQueryParameter("sort", "distance,asc"); + + String classificationName = mapCategoryToTicketmaster(category); + if (classificationName != null) { + urlBuilder.addQueryParameter("classificationName", classificationName); + } + + String url = urlBuilder.build().toString(); + return fetchEvents(url); + } + + /** + * Search events by name/keyword + */ + public List searchEventsByName(String keyword, Location location, double radiusKm) { + String url = HttpUrl.parse(BASE_URL + EVENTS_ENDPOINT) + .newBuilder() + .addQueryParameter("apikey", API_KEY) + .addQueryParameter("keyword", keyword) + .addQueryParameter("latlong", + String.format("%.6f,%.6f", location.getLatitude(), location.getLongitude())) + .addQueryParameter("radius", String.valueOf((int) radiusKm)) + .addQueryParameter("unit", "km") + .addQueryParameter("size", "50") + .build() + .toString(); + + return fetchEvents(url); + } + + /** + * 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; + } +} \ No newline at end of file diff --git a/src/main/java/data_access/FileSavedEventsDataAccessObject.java b/src/main/java/data_access/FileSavedEventsDataAccessObject.java new file mode 100644 index 00000000..afdb46f7 --- /dev/null +++ b/src/main/java/data_access/FileSavedEventsDataAccessObject.java @@ -0,0 +1,202 @@ +package data_access; + +import entity.Event; +import entity.Location; +import entity.EventCategory; +import use_case.save_event.SaveEventDataAccessInterface; + +import java.io.*; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * DAO for saved events with FILE PERSISTENCE. + * Events are saved to a CSV file and persist across app restarts. + * + * CSV Format: username,eventId,eventName,category,dateTime,address,latitude,longitude,imageUrl,description + */ +public class FileSavedEventsDataAccessObject implements SaveEventDataAccessInterface { + + private static final String FILENAME = "saved_events.csv"; + private static final String HEADER = "username,eventId,eventName,category,dateTime,address,latitude,longitude,imageUrl,description"; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + + private final File csvFile; + private final Map> savedEventsByUser = new HashMap<>(); + + public FileSavedEventsDataAccessObject() { + this.csvFile = new File(FILENAME); + loadFromFile(); + } + + /** + * Load saved events from file. + */ + private void loadFromFile() { + if (!csvFile.exists() || csvFile.length() == 0) { + return; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(csvFile))) { + String header = reader.readLine(); // Skip header + + String row; + while ((row = reader.readLine()) != null) { + try { + // Parse CSV with proper handling of commas in quoted strings + String[] col = parseCSVLine(row); + + if (col.length < 10) continue; + + String username = col[0].trim(); + String eventId = col[1].trim(); + String eventName = col[2].trim(); + String categoryStr = col[3].trim(); + String dateTimeStr = col[4].trim(); + String address = col[5].trim(); + String latStr = col[6].trim(); + String lonStr = col[7].trim(); + String imageUrl = col[8].trim(); + String description = col[9].trim(); + + // Parse location + double latitude = latStr.isEmpty() ? 0 : Double.parseDouble(latStr); + double longitude = lonStr.isEmpty() ? 0 : Double.parseDouble(lonStr); + Location location = new Location(address, latitude, longitude); + + // Parse category using the fromString method + EventCategory category = EventCategory.fromString(categoryStr); + + // Parse date/time + LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr, DATE_FORMATTER); + + // Create event - matching Event constructor: + // Event(id, name, description, address, category, location, startTime, imageUrl) + Event event = new Event(eventId, eventName, description, address, category, location, dateTime, imageUrl); + + // Add to user's list + savedEventsByUser.computeIfAbsent(username, k -> new ArrayList<>()).add(event); + + } catch (Exception e) { + System.err.println("Error parsing saved event row: " + e.getMessage()); + } + } + + + } catch (IOException ex) { + System.err.println("Error reading saved events file: " + ex.getMessage()); + } + } + + /** + * Parse a CSV line handling quoted strings with commas. + */ + private String[] parseCSVLine(String line) { + List result = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inQuotes = false; + + for (char c : line.toCharArray()) { + if (c == '"') { + inQuotes = !inQuotes; + } else if (c == ',' && !inQuotes) { + result.add(current.toString()); + current = new StringBuilder(); + } else { + current.append(c); + } + } + result.add(current.toString()); + + return result.toArray(new String[0]); + } + + /** + * Save all events to file. + */ + private void saveToFile() { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(csvFile))) { + writer.write(HEADER); + writer.newLine(); + + for (Map.Entry> entry : savedEventsByUser.entrySet()) { + String username = entry.getKey(); + for (Event event : entry.getValue()) { + StringBuilder line = new StringBuilder(); + line.append(username).append(","); + line.append(event.getId()).append(","); + line.append("\"").append(escapeCSV(event.getName())).append("\","); + line.append(event.getCategory().name()).append(","); + line.append(event.getStartTime().format(DATE_FORMATTER)).append(","); + line.append("\"").append(escapeCSV(event.getLocation().getAddress())).append("\","); + line.append(event.getLocation().getLatitude()).append(","); + line.append(event.getLocation().getLongitude()).append(","); + line.append("\"").append(escapeCSV(event.getImageUrl() != null ? event.getImageUrl() : "")).append("\","); + line.append("\"").append(escapeCSV(event.getDescription() != null ? event.getDescription() : "")).append("\""); + + writer.write(line.toString()); + writer.newLine(); + } + } + + } catch (IOException ex) { + System.err.println("Error saving events file: " + ex.getMessage()); + } + } + + /** + * Escape special characters for CSV. + */ + private String escapeCSV(String value) { + if (value == null) return ""; + return value.replace("\"", "\"\"").replace("\n", " ").replace("\r", ""); + } + + @Override + public void saveEvent(String username, Event event) { + if (username == null || event == null) return; + + List userEvents = savedEventsByUser.computeIfAbsent(username, k -> new ArrayList<>()); + + // Don't add duplicates + boolean alreadySaved = userEvents.stream() + .anyMatch(e -> e.getId().equals(event.getId())); + + if (!alreadySaved) { + userEvents.add(event); + saveToFile(); // Persist to file + System.out.println("Saved event: " + event.getName() + " for user: " + username); + } else { + System.out.println("Event already saved: " + event.getName()); + } + } + + @Override + public List getSavedEvents(String username) { + if (username == null) return new ArrayList<>(); + return new ArrayList<>(savedEventsByUser.getOrDefault(username, new ArrayList<>())); + } + + @Override + public void unsaveEvent(String username, Event event) { + if (username == null || event == null) return; + + List userEvents = savedEventsByUser.get(username); + if (userEvents != null) { + userEvents.removeIf(e -> e.getId().equals(event.getId())); + saveToFile(); // Persist to file + System.out.println("Removed event: " + event.getName() + " for user: " + username); + } + } + + @Override + public boolean isEventSaved(String username, String eventId) { + if (username == null || eventId == null) return false; + + List userEvents = savedEventsByUser.get(username); + if (userEvents == null) return false; + + return userEvents.stream().anyMatch(e -> e.getId().equals(eventId)); + } +} \ No newline at end of file diff --git a/src/main/java/data_access/FileUserDataAccessObject.java b/src/main/java/data_access/FileUserDataAccessObject.java index 4cae88c5..40d74f79 100644 --- a/src/main/java/data_access/FileUserDataAccessObject.java +++ b/src/main/java/data_access/FileUserDataAccessObject.java @@ -1,11 +1,13 @@ package data_access; +import entity.Location; import entity.User; import entity.UserFactory; import use_case.change_password.ChangePasswordUserDataAccessInterface; import use_case.login.LoginUserDataAccessInterface; import use_case.logout.LogoutUserDataAccessInterface; import use_case.signup.SignupUserDataAccessInterface; +import use_case.update_location.UpdateLocationUserDataAccessInterface; import java.io.*; import java.util.HashMap; @@ -14,78 +16,104 @@ /** * DAO for user data implemented using a File to persist the data. + * Now includes location storage for each user. + * + * CSV Format: username,password,address,latitude,longitude */ public class FileUserDataAccessObject implements SignupUserDataAccessInterface, - LoginUserDataAccessInterface, - ChangePasswordUserDataAccessInterface, - LogoutUserDataAccessInterface { + LoginUserDataAccessInterface, + ChangePasswordUserDataAccessInterface, + LogoutUserDataAccessInterface, + UpdateLocationUserDataAccessInterface { - private static final String HEADER = "username,password"; + private static final String HEADER = "username,password,address,latitude,longitude"; private final File csvFile; private final Map headers = new LinkedHashMap<>(); private final Map accounts = new HashMap<>(); + private final UserFactory userFactory; private String currentUsername; - /** - * Construct this DAO for saving to and reading from a local file. - * @param csvPath the path of the file to save to - * @param userFactory factory for creating user objects - * @throws RuntimeException if there is an IOException when accessing the file - */ public FileUserDataAccessObject(String csvPath, UserFactory userFactory) { - + this.userFactory = userFactory; csvFile = new File(csvPath); + headers.put("username", 0); headers.put("password", 1); + headers.put("address", 2); + headers.put("latitude", 3); + headers.put("longitude", 4); if (csvFile.length() == 0) { save(); + } else { + loadFromFile(); } - else { - - try (BufferedReader reader = new BufferedReader(new FileReader(csvFile))) { - final String header = reader.readLine(); + } - if (!header.equals(HEADER)) { - throw new RuntimeException(String.format("header should be%n: %s%n but was:%n%s", HEADER, header)); + private void loadFromFile() { + try (BufferedReader reader = new BufferedReader(new FileReader(csvFile))) { + String header = reader.readLine(); + + // Handle old format (username,password) or new format (username,password,address,latitude,longitude) + String row; + while ((row = reader.readLine()) != null) { + String[] col = row.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1); // Handle commas in addresses + + final String username = col[0].trim(); + final String password = col[1].trim(); + + User user; + + // Check if we have location data (new format) + if (col.length >= 5 && !col[2].trim().isEmpty()) { + String address = col[2].trim().replace("\"", ""); // Remove quotes + try { + double latitude = Double.parseDouble(col[3].trim()); + double longitude = Double.parseDouble(col[4].trim()); + Location location = new Location(address, latitude, longitude); + user = userFactory.create(username, password, address, location); + } catch (NumberFormatException e) { + // Invalid coordinates, create user without location + user = userFactory.create(username, password); + } + } else { + // Old format or no location + user = userFactory.create(username, password); } - String row; - while ((row = reader.readLine()) != null) { - final String[] col = row.split(","); - final String username = String.valueOf(col[headers.get("username")]); - final String password = String.valueOf(col[headers.get("password")]); - final User user = userFactory.create(username, password); - accounts.put(username, user); - } - } - catch (IOException ex) { - throw new RuntimeException(ex); + accounts.put(username, user); } + } catch (IOException ex) { + throw new RuntimeException("Error reading users file: " + ex.getMessage(), ex); } } private void save() { - final BufferedWriter writer; - try { - writer = new BufferedWriter(new FileWriter(csvFile)); - writer.write(String.join(",", headers.keySet())); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(csvFile))) { + writer.write(HEADER); writer.newLine(); for (User user : accounts.values()) { - final String line = String.format("%s,%s", - user.getName(), user.getPassword()); - writer.write(line); + StringBuilder line = new StringBuilder(); + line.append(user.getName()).append(","); + line.append(user.getPassword()).append(","); + + if (user.hasLocation()) { + // Wrap address in quotes to handle commas + line.append("\"").append(user.getAddress()).append("\","); + line.append(user.getLocation().getLatitude()).append(","); + line.append(user.getLocation().getLongitude()); + } else { + line.append(",,"); // Empty location fields + } + + writer.write(line.toString()); writer.newLine(); } - - writer.close(); - - } - catch (IOException ex) { - throw new RuntimeException(ex); + } catch (IOException ex) { + throw new RuntimeException("Error saving users file: " + ex.getMessage(), ex); } } @@ -117,8 +145,44 @@ public boolean existsByName(String identifier) { @Override public void changePassword(User user) { - // Replace the User object in the map accounts.put(user.getName(), user); save(); } -} + + /** + * Update the user's location. + */ + @Override + public void updateUserLocation(String username, String address, Location location) { + User existingUser = accounts.get(username); + if (existingUser != null) { + existingUser.setAddress(address); + existingUser.setLocation(location); + save(); + } + } + + /** + * Get the current user's location. + */ + @Override + public Location getCurrentUserLocation() { + if (currentUsername == null) { + return null; + } + User user = accounts.get(currentUsername); + return user != null ? user.getLocation() : null; + } + + /** + * Get the current user's address string. + */ + @Override + public String getCurrentUserAddress() { + if (currentUsername == null) { + return ""; + } + User user = accounts.get(currentUsername); + return user != null ? user.getAddress() : ""; + } +} \ No newline at end of file diff --git a/src/main/java/data_access/GeocodingDataAccessInterface.java b/src/main/java/data_access/GeocodingDataAccessInterface.java new file mode 100644 index 00000000..051aa522 --- /dev/null +++ b/src/main/java/data_access/GeocodingDataAccessInterface.java @@ -0,0 +1,28 @@ +package data_access; + +import entity.Location; +import java.util.Optional; + +/** + * Interface for geocoding services. + * + * CA NOTE: + * interface lives in the data_access package but is used by use cases. + * allows us to swap implementations (e.g., for testing or different APIs). + */ +public interface GeocodingDataAccessInterface { + + /** + * Convert an address string to Location coordinates. + * @param address the address to geocode + * @return Optional containing Location if found, empty if not found + */ + Optional geocodeAddress(String address); + + /** + * Check if an address can be geocoded. + * @param address the address to validate + * @return true if the address is valid and can be geocoded + */ + boolean isValidAddress(String address); +} \ No newline at end of file diff --git a/src/main/java/data_access/GeocodingService.java b/src/main/java/data_access/GeocodingService.java new file mode 100644 index 00000000..3f53339a --- /dev/null +++ b/src/main/java/data_access/GeocodingService.java @@ -0,0 +1,95 @@ +package data_access; + +import entity.Location; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +/** + * Service to convert addresses to coordinates (geocoding). + * Uses the free Nominatim OpenStreetMap API. + * + * CLEAN ARCHITECTURE NOTE: + * This is a Data Access / External Service adapter. + * It implements an interface that the Use Case layer depends on. + */ +public class GeocodingService implements GeocodingDataAccessInterface { + + private static final String NOMINATIM_URL = "https://nominatim.openstreetmap.org/search"; + private final OkHttpClient client; + + public GeocodingService() { + this.client = new OkHttpClient(); + } + + /** + * Convert an address string to Location coordinates. + * + * @param address the address to geocode (e.g., "123 Main St, Toronto, ON") + * @return Optional containing Location if found, empty if not found + */ + @Override + public Optional geocodeAddress(String address) { + if (address == null || address.trim().isEmpty()) { + return Optional.empty(); + } + + try { + String encodedAddress = URLEncoder.encode(address.trim(), StandardCharsets.UTF_8.toString()); + String url = NOMINATIM_URL + "?q=" + encodedAddress + "&format=json&limit=1"; + + Request request = new Request.Builder() + .url(url) + .header("User-Agent", "EventGateApp/1.0") // Required by Nominatim + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + System.err.println("Geocoding failed: " + response.code()); + return Optional.empty(); + } + + String jsonResponse = response.body().string(); + JSONArray results = new JSONArray(jsonResponse); + + if (results.length() == 0) { + System.err.println("No geocoding results for: " + address); + return Optional.empty(); + } + + JSONObject firstResult = results.getJSONObject(0); + double latitude = firstResult.getDouble("lat"); + double longitude = firstResult.getDouble("lon"); + String displayName = firstResult.getString("display_name"); + + // Use the display name from the API for a standardized address + Location location = new Location(displayName, latitude, longitude); + + System.out.println("Geocoded '" + address + "' to: " + latitude + ", " + longitude); + return Optional.of(location); + + } + } catch (IOException e) { + System.err.println("Geocoding error: " + e.getMessage()); + return Optional.empty(); + } catch (Exception e) { + System.err.println("Geocoding parse error: " + e.getMessage()); + return Optional.empty(); + } + } + + /** + * Validate if an address can be geocoded. + */ + @Override + public boolean isValidAddress(String address) { + return geocodeAddress(address).isPresent(); + } +} \ No newline at end of file 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/entity/User.java b/src/main/java/entity/User.java index bf9783d0..eac70a10 100644 --- a/src/main/java/entity/User.java +++ b/src/main/java/entity/User.java @@ -1,12 +1,15 @@ package entity; /** - * A simple entity representing a user. Users have a username and password.. + * a simple entity representing a user. + * users have a username, password, and optionally a saved location. */ public class User { private final String name; private final String password; + private Location location; // User's saved location for distance calculations + private String address; // Human-readable address string /** * Creates a new user with the given non-empty name and non-empty password. @@ -23,6 +26,21 @@ public User(String name, String password) { } this.name = name; this.password = password; + this.location = null; + this.address = ""; + } + + /** + * Creates a new user with location information. + * @param name the username + * @param password the password + * @param address the user's address string + * @param location the user's location coordinates + */ + public User(String name, String password, String address, Location location) { + this(name, password); + this.address = address != null ? address : ""; + this.location = location; } public String getName() { @@ -33,4 +51,26 @@ public String getPassword() { return password; } -} + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address != null ? address : ""; + } + + /** + * Check if user has a saved location. + */ + public boolean hasLocation() { + return location != null; + } +} \ No newline at end of file diff --git a/src/main/java/entity/UserFactory.java b/src/main/java/entity/UserFactory.java index 238578d3..5ef4d655 100644 --- a/src/main/java/entity/UserFactory.java +++ b/src/main/java/entity/UserFactory.java @@ -1,11 +1,21 @@ package entity; /** - * Factory for creating CommonUser objects. + * Factory for creating User objects. */ public class UserFactory { + /** + * Create a user without location. + */ public User create(String name, String password) { return new User(name, password); } -} + + /** + * Create a user with location. + */ + public User create(String name, String password, String address, Location location) { + return new User(name, password, address, location); + } +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/ViewNames.java b/src/main/java/interface_adapter/ViewNames.java new file mode 100644 index 00000000..854a92bd --- /dev/null +++ b/src/main/java/interface_adapter/ViewNames.java @@ -0,0 +1,23 @@ +package interface_adapter; + +/** + * Constants for view names to avoid hardcoded strings. + * Follows Single Responsibility Principle - centralized view name management. + * Prevents violations of Clean Architecture by decoupling layers from specific view names. + */ +public class ViewNames { + private ViewNames() { + throw new AssertionError("Cannot instantiate ViewNames class"); + } + + public static final String SIGNUP = "sign up"; + public static final String LOGIN = "log in"; + public static final String LOGGED_IN = "logged in"; + public static final String DISPLAY_LOCAL_EVENTS = "display local events"; + public static final String CALENDAR = "calendar view"; + public static final String EVENT_LIST_BY_DATE = "event list by date"; + public static final String EVENT_DESCRIPTION = "event description"; + public static final String EVENT_SEARCH = "event search"; + public static final String SAVE_EVENTS = "save events"; + +} 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..d2643a01 --- /dev/null +++ b/src/main/java/interface_adapter/calendarFlow/CalendarFlowController.java @@ -0,0 +1,35 @@ +package interface_adapter.calendarFlow; + +import java.time.LocalDate; + +import entity.Location; +import use_case.calendarFlow.CalendarFlowInputBoundary; +import use_case.calendarFlow.CalendarFlowInputData; + +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); + } + + /** + * Handles the request to switch to the dashboard view. + * This method delegates the view change logic to the calendar flow interactor + */ + 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..133566a5 --- /dev/null +++ b/src/main/java/interface_adapter/calendarFlow/CalendarFlowPresenter.java @@ -0,0 +1,48 @@ +package interface_adapter.calendarFlow; + +import java.util.ArrayList; + +import interface_adapter.ViewManagerModel; +import interface_adapter.ViewNames; +import use_case.calendarFlow.CalendarFlowOutputBoundary; +import use_case.calendarFlow.CalendarFlowOutputData; + +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) { + final 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) { + final CalendarFlowState state = viewModel.getState(); + state.setEventList(new ArrayList<>()); + state.setErrorMessage(errorMessage); + + viewManagerModel.setState(viewModel.getViewName()); + viewManagerModel.firePropertyChange(); + } + + @Override + public void switchToDashboardView() { + viewManagerModel.setState(ViewNames.DISPLAY_LOCAL_EVENTS); + 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..f6a87c97 --- /dev/null +++ b/src/main/java/interface_adapter/calendarFlow/CalendarFlowState.java @@ -0,0 +1,39 @@ +package interface_adapter.calendarFlow; + +import java.time.LocalDate; +import java.util.List; + +import entity.Event; + +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..150b7f1f --- /dev/null +++ b/src/main/java/interface_adapter/calendarFlow/CalendarFlowViewModel.java @@ -0,0 +1,10 @@ +package interface_adapter.calendarFlow; + +import interface_adapter.ViewModel; + +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..97e07767 --- /dev/null +++ b/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsController.java @@ -0,0 +1,48 @@ +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); + } + + public void switchToSaveView() { + interactor.switchToSaveView(); + } +} 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..6f9c262b --- /dev/null +++ b/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsPresenter.java @@ -0,0 +1,85 @@ +package interface_adapter.display_local_events; + +import entity.Event; +import interface_adapter.ViewManagerModel; +import interface_adapter.save_event.SaveEventController; +import interface_adapter.save_event.SaveEventViewModel; +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 SaveEventViewModel saveEventViewModel; + private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + public DisplayLocalEventsPresenter(DisplayLocalEventsViewModel viewModel, + ViewManagerModel viewManagerModel, + SaveEventViewModel saveEventViewModel) { + this.viewModel = viewModel; + this.viewManagerModel = viewManagerModel; + this.saveEventViewModel = saveEventViewModel; + } + + + + @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(); + } + + public void switchToSaveView() { + viewManagerModel.setState(saveEventViewModel.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..a2384fc6 --- /dev/null +++ b/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsState.java @@ -0,0 +1,73 @@ +package interface_adapter.display_local_events; + +import java.util.ArrayList; +import java.util.List; + + +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; + + + 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; + } +} 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..9322f664 --- /dev/null +++ b/src/main/java/interface_adapter/display_local_events/DisplayLocalEventsViewModel.java @@ -0,0 +1,136 @@ +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; + +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; } + } + + + 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(); + } + + + 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(); + } +} 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..8248600b 100644 --- a/src/main/java/interface_adapter/login/LoginPresenter.java +++ b/src/main/java/interface_adapter/login/LoginPresenter.java @@ -1,10 +1,13 @@ package interface_adapter.login; import interface_adapter.ViewManagerModel; +import interface_adapter.ViewNames; 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,28 +17,28 @@ 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 public void prepareSuccessView(LoginOutputData response) { - // On success, update the loggedInViewModel's state final LoggedInState loggedInState = loggedInViewModel.getState(); loggedInState.setUsername(response.getUsername()); this.loggedInViewModel.firePropertyChange(); - // and clear everything from the LoginViewModel's state loginViewModel.setState(new LoginState()); - // switch to the logged in view - this.viewManagerModel.setState(loggedInViewModel.getViewName()); - this.viewManagerModel.firePropertyChange(); + // The view will load the user's location when it becomes visible + viewManagerModel.setState(ViewNames.DISPLAY_LOCAL_EVENTS); + viewManagerModel.firePropertyChange(); } @Override @@ -44,4 +47,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..1f74b0b6 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 = "Ticket Monster"; + 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..2c07c295 --- /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 interface_adapter.ViewNames; +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("event"); + } + + @Override + public void prepareFailureView(String errorMessage) { + SaveEventState currentState = saveEventViewModel.getState(); + currentState.addSavedEvent(null); + saveEventViewModel.setState(currentState); + saveEventViewModel.firePropertyChange("error"); + } + + @Override + public void switchToDashboardView() { + viewManagerModel.setState(ViewNames.DISPLAY_LOCAL_EVENTS); + 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..435b0dc2 --- /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 events"); + 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..1f994211 --- /dev/null +++ b/src/main/java/interface_adapter/search_event_by_name/SearchEventByNamePresenter.java @@ -0,0 +1,45 @@ +package interface_adapter.search_event_by_name; + +import interface_adapter.ViewManagerModel; +import interface_adapter.ViewNames; +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(ViewNames.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..20ee3b9c 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 = "Ticket Monster"; + 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/interface_adapter/update_location/UpdateLocationController.java b/src/main/java/interface_adapter/update_location/UpdateLocationController.java new file mode 100644 index 00000000..087c7cf0 --- /dev/null +++ b/src/main/java/interface_adapter/update_location/UpdateLocationController.java @@ -0,0 +1,29 @@ +package interface_adapter.update_location; + +import use_case.update_location.UpdateLocationInputBoundary; +import use_case.update_location.UpdateLocationInputData; + +/** + * Controller for the Update Location Use Case. + * + * CLEAN ARCHITECTURE NOTE: + * The Controller receives input from the View and converts it to + * Use Case input data, then calls the Use Case. + */ +public class UpdateLocationController { + + private final UpdateLocationInputBoundary updateLocationInteractor; + + public UpdateLocationController(UpdateLocationInputBoundary updateLocationInteractor) { + this.updateLocationInteractor = updateLocationInteractor; + } + + /** + * Execute the update location use case. + * @param address the address entered by the user + */ + public void execute(String address) { + UpdateLocationInputData inputData = new UpdateLocationInputData(address); + updateLocationInteractor.execute(inputData); + } +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/update_location/UpdateLocationPresenter.java b/src/main/java/interface_adapter/update_location/UpdateLocationPresenter.java new file mode 100644 index 00000000..2664335d --- /dev/null +++ b/src/main/java/interface_adapter/update_location/UpdateLocationPresenter.java @@ -0,0 +1,42 @@ +package interface_adapter.update_location; + +import use_case.update_location.UpdateLocationOutputBoundary; +import use_case.update_location.UpdateLocationOutputData; + +/** + * Presenter for the Update Location Use Case. + * + * CLEAN ARCHITECTURE NOTE: + * The Presenter receives output from the Use Case and formats it + * for the View by updating the ViewModel. + */ +public class UpdateLocationPresenter implements UpdateLocationOutputBoundary { + + private final UpdateLocationViewModel viewModel; + + public UpdateLocationPresenter(UpdateLocationViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void prepareSuccessView(UpdateLocationOutputData outputData) { + UpdateLocationState state = viewModel.getState(); + state.setAddress(outputData.getAddress()); + state.setLocation(outputData.getLocation()); + state.setError(null); + state.setSuccess(true); + + viewModel.setState(state); + viewModel.firePropertyChange(); + } + + @Override + public void prepareFailView(String errorMessage) { + UpdateLocationState state = viewModel.getState(); + state.setError(errorMessage); + state.setSuccess(false); + + viewModel.setState(state); + viewModel.firePropertyChange(); + } +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/update_location/UpdateLocationState.java b/src/main/java/interface_adapter/update_location/UpdateLocationState.java new file mode 100644 index 00000000..5c9dcb7d --- /dev/null +++ b/src/main/java/interface_adapter/update_location/UpdateLocationState.java @@ -0,0 +1,50 @@ +package interface_adapter.update_location; + +import entity.Location; + +/** + * State for the Update Location ViewModel. + */ +public class UpdateLocationState { + + private String address = ""; + private Location location = null; + private String error = null; + private boolean success = false; + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public boolean hasLocation() { + return location != null; + } +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/update_location/UpdateLocationViewModel.java b/src/main/java/interface_adapter/update_location/UpdateLocationViewModel.java new file mode 100644 index 00000000..d0f4f2ed --- /dev/null +++ b/src/main/java/interface_adapter/update_location/UpdateLocationViewModel.java @@ -0,0 +1,14 @@ +package interface_adapter.update_location; + +import interface_adapter.ViewModel; + +/** + * ViewModel for the Update Location feature. + */ +public class UpdateLocationViewModel extends ViewModel { + + public UpdateLocationViewModel() { + super("update location"); + setState(new UpdateLocationState()); + } +} \ No newline at end of file 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..2d078a4a --- /dev/null +++ b/src/main/java/use_case/calendarFlow/CalendarFlowDataAccessInterface.java @@ -0,0 +1,24 @@ +package use_case.calendarFlow; + +import java.time.LocalDate; +import java.util.List; + +import entity.Event; +import entity.Location; + +/** + * 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..7b329a2d --- /dev/null +++ b/src/main/java/use_case/calendarFlow/CalendarFlowInputBoundary.java @@ -0,0 +1,20 @@ +package use_case.calendarFlow; + +/** + * InputBoundary for the Calendar Flow Use Case. + * Defines the interface for executing the calendar flow use case + */ +public interface CalendarFlowInputBoundary { + + /** + * Executes the Calendar Flow use case using the provided input data. + * This includes validating the date, user location, and radius, and retrieving the corresponding list of events. + * @param inputData the input data containing the selected date, user location, and search radius + */ + void execute(CalendarFlowInputData inputData); + + /** + * 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..4a5f149b --- /dev/null +++ b/src/main/java/use_case/calendarFlow/CalendarFlowInputData.java @@ -0,0 +1,33 @@ +package use_case.calendarFlow; + +import java.time.LocalDate; + +import entity.Location; + +/** + * 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..4f370e02 --- /dev/null +++ b/src/main/java/use_case/calendarFlow/CalendarFlowInteractor.java @@ -0,0 +1,55 @@ +package use_case.calendarFlow; + +import java.time.LocalDate; +import java.util.List; + +import entity.Event; + +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 { + final List events = calendarFlowDataAccess.getEventsByDate( + inputData.getSelectedDate(), + inputData.getUserLocation(), + inputData.getRadiusKm() + ); + + final LocalDate date = inputData.getSelectedDate(); + if (events == null || events.isEmpty()) { + presenter.prepareFailView("No events found for " + date); + } + else { + final CalendarFlowOutputData outputData = new CalendarFlowOutputData(date, events); + presenter.prepareSuccessView(outputData); + } + } + catch (Exception ex) { + presenter.prepareFailView("Error fetching events: " + ex.getMessage()); + } + } + + @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..6bd739b6 --- /dev/null +++ b/src/main/java/use_case/calendarFlow/CalendarFlowOutputBoundary.java @@ -0,0 +1,24 @@ +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); + + /** + * 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..f244319a --- /dev/null +++ b/src/main/java/use_case/calendarFlow/CalendarFlowOutputData.java @@ -0,0 +1,35 @@ +package use_case.calendarFlow; + +import java.time.LocalDate; +import java.util.List; + +import entity.Event; + +/** + * 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..c1d3abd8 --- /dev/null +++ b/src/main/java/use_case/display_local_events/DisplayLocalEventsInputBoundary.java @@ -0,0 +1,10 @@ +package use_case.display_local_events; + +public interface DisplayLocalEventsInputBoundary { + void execute(DisplayLocalEventsInputData inputData); + + /** + * Executes switch to save event view use case + */ + void switchToSaveView(); +} 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..e4110153 --- /dev/null +++ b/src/main/java/use_case/display_local_events/DisplayLocalEventsInteractor.java @@ -0,0 +1,117 @@ +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()); + } + } + + @Override + public void switchToSaveView() { + outputBoundary.switchToSaveView(); + } + + /** + * 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..eff8ec8c --- /dev/null +++ b/src/main/java/use_case/display_local_events/DisplayLocalEventsOutputBoundary.java @@ -0,0 +1,12 @@ +package use_case.display_local_events; + +public interface DisplayLocalEventsOutputBoundary { + void presentSuccess(DisplayLocalEventsOutputData outputData); + + void presentError(String errorMessage); + + /** + * Executes switch to save view use case + */ + void switchToSaveView(); +} 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/SaveEventDataAccessInterface.java b/src/main/java/use_case/save_event/SaveEventDataAccessInterface.java new file mode 100644 index 00000000..39b9ad89 --- /dev/null +++ b/src/main/java/use_case/save_event/SaveEventDataAccessInterface.java @@ -0,0 +1,32 @@ +package use_case.save_event; + +import entity.Event; +import java.util.List; + +/** + * Interface for saved events data access. + * Following Clean Architecture - the use case defines what it needs, + * and the data access layer implements it. + */ +public interface SaveEventDataAccessInterface { + + /** + * Save an event for a user. + */ + void saveEvent(String username, Event event); + + /** + * Get all saved events for a user. + */ + List getSavedEvents(String username); + + /** + * Remove a saved event for a user. + */ + void unsaveEvent(String username, Event event); + + /** + * Check if an event is already saved by a user. + */ + boolean isEventSaved(String username, String eventId); +} \ No newline at end of file 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..da2e1324 --- /dev/null +++ b/src/main/java/use_case/save_event/SaveEventInteractor.java @@ -0,0 +1,74 @@ +package use_case.save_event; + +import data_access.FileSavedEventsDataAccessObject; +import entity.Event; +import use_case.login.LoginUserDataAccessInterface; + +import java.util.List; + +public class SaveEventInteractor implements SaveEventInputBoundary { + + private SaveEventOutputBoundary eventPresenter; + private SaveEventDataAccessInterface savedEventsDAO; + private LoginUserDataAccessInterface userDataAccess; + + public SaveEventInteractor(SaveEventOutputBoundary outputBoundary, + SaveEventDataAccessInterface 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(); + + // FIXED: Changed from isSavedEvent to isEventSaved + if (savedEventsDAO.isEventSaved(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(); + } + + // FIXED: Changed from removeEvent to unsaveEvent + public void removeEvent(Event event) { + String currentUsername = userDataAccess.getCurrentUsername(); + if (currentUsername != null) { + savedEventsDAO.unsaveEvent(currentUsername, event); + } + } + + public List getSavedEvents() { + String username = userDataAccess.getCurrentUsername(); + if (username == null) return List.of(); + return savedEventsDAO.getSavedEvents(username); + } + + public void unsaveEvent(Event event) { + String username = userDataAccess.getCurrentUsername(); + if (username != null) { + savedEventsDAO.unsaveEvent(username, event); + } + } + + public boolean isEventSaved(String eventId) { + String username = userDataAccess.getCurrentUsername(); + if (username == null || eventId == null) return false; + return savedEventsDAO.isEventSaved(username, eventId); + } +} \ No newline at end of file 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/use_case/update_location/UpdateLocationInputBoundary.java b/src/main/java/use_case/update_location/UpdateLocationInputBoundary.java new file mode 100644 index 00000000..99661c81 --- /dev/null +++ b/src/main/java/use_case/update_location/UpdateLocationInputBoundary.java @@ -0,0 +1,17 @@ +package use_case.update_location; + +/** + * Input Boundary for the Update Location Use Case. + * + * CLEAN ARCHITECTURE NOTE: + * This interface defines what the Controller can ask the Use Case to do. + * The Interactor implements this interface. + */ +public interface UpdateLocationInputBoundary { + + /** + * Execute the update location use case. + * @param inputData the input data containing the address + */ + void execute(UpdateLocationInputData inputData); +} \ No newline at end of file diff --git a/src/main/java/use_case/update_location/UpdateLocationInputData.java b/src/main/java/use_case/update_location/UpdateLocationInputData.java new file mode 100644 index 00000000..8cf5e3bf --- /dev/null +++ b/src/main/java/use_case/update_location/UpdateLocationInputData.java @@ -0,0 +1,17 @@ +package use_case.update_location; + +/** + * Input Data for the Update Location Use Case. + */ +public class UpdateLocationInputData { + + private final String address; + + public UpdateLocationInputData(String address) { + this.address = address; + } + + public String getAddress() { + return address; + } +} \ No newline at end of file diff --git a/src/main/java/use_case/update_location/UpdateLocationInteractor.java b/src/main/java/use_case/update_location/UpdateLocationInteractor.java new file mode 100644 index 00000000..935e8000 --- /dev/null +++ b/src/main/java/use_case/update_location/UpdateLocationInteractor.java @@ -0,0 +1,71 @@ +package use_case.update_location; + +import data_access.GeocodingDataAccessInterface; +import entity.Location; + +import java.util.Optional; + +/** + * Interactor for the Update Location Use Case. + * + * CLEAN ARCHITECTURE NOTE: + * This is the Use Case / Business Logic layer. + * - It receives input via UpdateLocationInputBoundary + * - It uses Data Access interfaces (not concrete implementations) + * - It outputs via UpdateLocationOutputBoundary + * - It contains the business rules for updating location + */ +public class UpdateLocationInteractor implements UpdateLocationInputBoundary { + + private final UpdateLocationUserDataAccessInterface userDataAccess; + private final GeocodingDataAccessInterface geocodingService; + private final UpdateLocationOutputBoundary presenter; + + public UpdateLocationInteractor(UpdateLocationUserDataAccessInterface userDataAccess, + GeocodingDataAccessInterface geocodingService, + UpdateLocationOutputBoundary presenter) { + this.userDataAccess = userDataAccess; + this.geocodingService = geocodingService; + this.presenter = presenter; + } + + @Override + public void execute(UpdateLocationInputData inputData) { + String address = inputData.getAddress(); + + // Validate input + if (address == null || address.trim().isEmpty()) { + presenter.prepareFailView("Please enter an address."); + return; + } + + // Check if user is logged in + String username = userDataAccess.getCurrentUsername(); + if (username == null) { + presenter.prepareFailView("You must be logged in to update your location."); + return; + } + + // Geocode the address + Optional locationOptional = geocodingService.geocodeAddress(address.trim()); + + if (locationOptional.isEmpty()) { + presenter.prepareFailView("Could not find that address. Please try a more specific address."); + return; + } + + Location location = locationOptional.get(); + + // Save to user account + userDataAccess.updateUserLocation(username, address.trim(), location); + + // Prepare success response + UpdateLocationOutputData outputData = new UpdateLocationOutputData( + address.trim(), + location, + true + ); + + presenter.prepareSuccessView(outputData); + } +} \ No newline at end of file diff --git a/src/main/java/use_case/update_location/UpdateLocationOutputBoundary.java b/src/main/java/use_case/update_location/UpdateLocationOutputBoundary.java new file mode 100644 index 00000000..d1cc650f --- /dev/null +++ b/src/main/java/use_case/update_location/UpdateLocationOutputBoundary.java @@ -0,0 +1,23 @@ +package use_case.update_location; + +/** + * Output Boundary for the Update Location Use Case. + * + * CLEAN ARCHITECTURE NOTE: + * This interface defines how the Use Case communicates results back. + * The Presenter implements this interface. + */ +public interface UpdateLocationOutputBoundary { + + /** + * Prepare the success view when location is updated. + * @param outputData the output data with location info + */ + void prepareSuccessView(UpdateLocationOutputData outputData); + + /** + * Prepare the failure view when location update fails. + * @param errorMessage the error message + */ + void prepareFailView(String errorMessage); +} \ No newline at end of file diff --git a/src/main/java/use_case/update_location/UpdateLocationOutputData.java b/src/main/java/use_case/update_location/UpdateLocationOutputData.java new file mode 100644 index 00000000..81dc09cd --- /dev/null +++ b/src/main/java/use_case/update_location/UpdateLocationOutputData.java @@ -0,0 +1,31 @@ +package use_case.update_location; + +import entity.Location; + +/** + * Output Data for the Update Location Use Case. + */ +public class UpdateLocationOutputData { + + private final String address; + private final Location location; + private final boolean success; + + public UpdateLocationOutputData(String address, Location location, boolean success) { + this.address = address; + this.location = location; + this.success = success; + } + + public String getAddress() { + return address; + } + + public Location getLocation() { + return location; + } + + public boolean isSuccess() { + return success; + } +} \ No newline at end of file diff --git a/src/main/java/use_case/update_location/UpdateLocationUserDataAccessInterface.java b/src/main/java/use_case/update_location/UpdateLocationUserDataAccessInterface.java new file mode 100644 index 00000000..24cc9285 --- /dev/null +++ b/src/main/java/use_case/update_location/UpdateLocationUserDataAccessInterface.java @@ -0,0 +1,34 @@ +package use_case.update_location; + +import entity.Location; + +/** + * Data Access Interface for the Update Location Use Case. + */ +public interface UpdateLocationUserDataAccessInterface { + + /** + * Get the current logged-in username. + */ + String getCurrentUsername(); + + /** + * Update the user's location. + * @param username the username + * @param address the address string + * @param location the location coordinates + */ + void updateUserLocation(String username, String address, Location location); + + /** + * Get the current user's location. + * @return the location or null if not set + */ + Location getCurrentUserLocation(); + + /** + * Get the current user's address string. + * @return the address or empty string if not set + */ + String getCurrentUserAddress(); +} \ 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..c51dd950 --- /dev/null +++ b/src/main/java/view/CalendarView.java @@ -0,0 +1,187 @@ +package view; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.time.LocalDate; +import java.time.YearMonth; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; + +import entity.Location; +import interface_adapter.ViewManagerModel; +import interface_adapter.ViewNames; +import interface_adapter.calendarFlow.CalendarFlowController; + +/** + * View for displaying a calendar with month navigation. + * Note: This View class has coupling of 8 types, which is expected for GUI components + * that coordinate multiple UI elements. This follows Clean Architecture principles + */ +public class CalendarView extends JPanel implements ActionListener { + private final String viewName = ViewNames.CALENDAR; + 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[Constants.NUMBER_OF_DAY_BUTTONS]; + private YearMonth currentYearMonth; + private final String textFormat = "SegoeUI"; + + private ViewManagerModel viewManagerModel; + private CalendarFlowController calendarFlowController; + private Location userLocation; + private double searchRadiusKm = Constants.SEARCH_RADIUS_KM_DEFAULT; + + public CalendarView() { + this.setLayout(new BorderLayout()); + + monthYearLabel.setFont(new Font(textFormat, Font.BOLD, Constants.MONTH_YEAR_LABEL_TEXT_FONT_SIZE)); + previousMonthButton.addActionListener( + new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + changeMonth(-1); + } + } + ); + + nextMonthButton.addActionListener( + new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + changeMonth(1); + } + } + ); + + final JButton backButton = new JButton("← Back"); + backButton.addActionListener(evt -> { + if (calendarFlowController != null) { + calendarFlowController.switchToDashboardView(); + } + }); + + final JPanel topPanel = new JPanel(new BorderLayout()); + topPanel.add(backButton, BorderLayout.WEST); + + final JPanel monthNavPanel = new JPanel(new FlowLayout()); + monthNavPanel.add(previousMonthButton); + monthNavPanel.add(monthYearLabel); + monthNavPanel.add(nextMonthButton); + + topPanel.add(monthNavPanel, BorderLayout.CENTER); + this.add(topPanel, BorderLayout.NORTH); + + final JPanel dayAndDayNamePanel = new JPanel(new BorderLayout()); + final String[] dayNames = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + for (String dayName : dayNames) { + final JLabel dayLabel = new JLabel(dayName, SwingConstants.CENTER); + dayLabel.setFont(new Font(textFormat, Font.BOLD, Constants.DAY_LABEL_TEXT_FONT_SIZE)); + dayNamesPanel.add(dayLabel); + } + + for (int i = 0; i < Constants.NUMBER_OF_DAY_BUTTONS; i++) { + dayButtons[i] = new JButton(); + dayButtons[i].setFont(new Font(textFormat, Font.PLAIN, Constants.DAY_BUTTONS_FONT_SIZE)); + 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()); + + final LocalDate firstOfMonth = yearMonth.atDay(1); + final int firstDayOfWeek = firstOfMonth.getDayOfWeek().getValue() % 7; + final int daysInMonth = yearMonth.lengthOfMonth(); + + for (JButton button : dayButtons) { + button.setText(""); + button.setEnabled(false); + + for (var listener : button.getActionListeners()) { + button.removeActionListener(listener); + } + } + + for (int day = 1; day <= daysInMonth; day++) { + final 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 setEventController(CalendarFlowController controller) { + this.calendarFlowController = controller; + } + + public void setViewManagerModel(ViewManagerModel viewManagerModel) { + this.viewManagerModel = viewManagerModel; + } + + public void setUserLocation(Location location) { + this.userLocation = location; + } + + public void setSearchRadiusKm(double kiloMeter) { + this.searchRadiusKm = kiloMeter; + } + + /** + * Handles the selection of a specific date on the calendar. + * Triggers a search for events occurring on the selected date within + * the specified search radius from the user's location. + * @param date the date selected by the user + */ + public void getSelectedDay(LocalDate date) { + if (calendarFlowController != null) { + calendarFlowController.execute(date, userLocation, searchRadiusKm); + } + + } + + public String getViewName() { + return viewName; + } + + /** + * Handles action events triggered by button clicks. + * Currently, logs the action command to the console for debugging purposes. + * @param evt the action event triggered by a button click + */ + public void actionPerformed(ActionEvent evt) { + System.out.println("Click " + evt.getActionCommand()); + } +} + diff --git a/src/main/java/view/Constants.java b/src/main/java/view/Constants.java new file mode 100644 index 00000000..68aae7ec --- /dev/null +++ b/src/main/java/view/Constants.java @@ -0,0 +1,98 @@ +package view; + +/** + * Constants used in this program. + */ +public final class Constants { + // Constants for CalendarView + public static final int NUMBER_OF_DAY_BUTTONS = 42; + public static final double SEARCH_RADIUS_KM_DEFAULT = 50.0; + public static final int MONTH_YEAR_LABEL_TEXT_FONT_SIZE = 18; + public static final int DAY_LABEL_TEXT_FONT_SIZE = 12; + public static final int DAY_BUTTONS_FONT_SIZE = 12; + + // Constants for EventListByDateView.java + // EventListByDateView method + public static final int EMPTY_BORDER_SIZE = 5; + public static final int CONTAINER_EMPTY_BORDER_SIZE = 10; + public static final int BACKGROUND_COLOR_R = 245; + public static final int BACKGROUND_COLOR_G = 247; + public static final int BACKGROUND_COLOR_B = 250; + public static final int UNIT_INCREMENT_SCALE = 16; + + // buildTopPanel method + // Top Panel + public static final int TOP_PANEL_EMPTY_BORDER_SIZE = 10; + public static final int TOP_PANEL_BACKGROUND_COLOR_R = 25; + public static final int TOP_PANEL_BACKGROUND_COLOR_G = 118; + public static final int TOP_PANEL_BACKGROUND_COLOR_B = 210; + // Back Button + public static final int BACK_BUTTON_LINE_BOARDER_COLOR_R = 220; + public static final int BACK_BUTTON_LINE_BOARDER_COLOR_G = 220; + public static final int BACK_BUTTON_LINE_BOARDER_COLOR_B = 220; + public static final int BACK_BUTTON_EMPTY_BOARDER_TOP = 8; + public static final int BACK_BUTTON_EMPTY_BOARDER_BOTTOM = 8; + public static final int BACK_BUTTON_EMPTY_BOARDER_LEFT = 15; + public static final int BACK_BUTTON_EMPTY_BOARDER_RIGHT = 15; + public static final int BACK_BUTTON_FONT_SIZE = 12; + // Center Panel + public static final int CENTER_PANEL_TITLE_LABEL_FONT_SIZE = 24; + public static final int CENTER_PANEL_DATE_LABEL_FONT_SIZE = 16; + public static final int CENTER_PANEL_BOX_VERTICAL_STRUC_HIGH = 5; + + // renderError method + // ErrorLabel + public static final float ERROR_LABEL_TEXT_FONT_SIZE = 16f; + public static final int ERROR_LABEL_BACKGROUND_COLOR_R = 120; + public static final int ERROR_LABEL_BACKGROUND_COLOR_G = 120; + public static final int ERROR_LABEL_BACKGROUND_COLOR_B = 120; + + // Event Lists UI + // Events Container + public static final int EVENT_CONTAINER_BOX_VERTICAL_STRUC_HEIGHT = 10; + + // buildEventCard method + // time label + public static final int TIME_LABEL_TEXT_FONT_SIZE = 12; + public static final int TIME_LABEL_BACKGROUND_COLOR_R = 33; + public static final int TIME_LABEL_BACKGROUND_COLOR_G = 150; + public static final int TIME_LABEL_BACKGROUND_COLOR_B = 243; + // info panel + public static final int INFO_PANEL_BOX_VERTICAL_STRUC_HEIGHT = 4; + // explore button for mouseEntered + public static final int EXPLORE_BUTTON_MOUSE_ENTER_COLOR_R = 21; + public static final int EXPLORE_BUTTON_MOUSE_ENTER_COLOR_G = 101; + public static final int EXPLORE_BUTTON_MOUSE_ENTER_COLOR_B = 192; + // explore button for mouseExited + public static final int EXPLORE_BUTTON_MOUSE_EXITED_COLOR_R = 25; + public static final int EXPLORE_BUTTON_MOUSE_EXITED_COLOR_G = 118; + public static final int EXPLORE_BUTTON_MOUSE_EXITED_COLOR_B = 210; + // createExploreButton method + public static final int EXPLORE_BUTTON_FONT_SIZE = 20; + public static final int EXPLORE_BUTTON_BACKGROUND_COLOR_R = 25; + public static final int EXPLORE_BUTTON_BACKGROUND_COLOR_G = 118; + public static final int EXPLORE_BUTTON_BACKGROUND_COLOR_B = 210; + public static final int EXPLORE_BUTTON_EMPTY_BOARDER_TOP = 15; + public static final int EXPLORE_BUTTON_EMPTY_BOARDER_BOTTOM = 15; + public static final int EXPLORE_BUTTON_EMPTY_BOARDER_LEFT = 25; + public static final int EXPLORE_BUTTON_EMPTY_BOARDER_RIGHT = 25; + public static final int EXPLORE_BUTTON_PREFERRED_WIDTH = 80; + public static final int EXPLORE_BUTTON_PREFERRED_HEIGHT = 80; + // createLocationLabel method + public static final int LOCATION_LABEL_TEXT_FONT_SIZE = 12; + public static final int LOCATION_LABEL_BACKGROUND_COLOR_R = 100; + public static final int LOCATION_LABEL_BACKGROUND_COLOR_G = 100; + public static final int LOCATION_LABEL_BACKGROUND_COLOR_B = 100; + // createNameLabel method + public static final float NAMEL_LABEL_TEXT_FONT_SIZE = 15f; + // createCardBasePanel method + public static final int CARD_BASE_PANEL_MAXIMUM_SIZE_HEIGHT = 100; + public static final int CARD_BASE_PANEL_LINE_BOARDER_COLOR_R = 230; + public static final int CARD_BASE_PANEL_LINE_BOARDER_COLOR_G = 230; + public static final int CARD_BASE_PANEL_LINE_BOARDER_COLOR_B = 230; + public static final int CARD_BASE_PANEL_EMPTY_BOARDER_TOP = 12; + public static final int CARD_BASE_PANEL_EMPTY_BOARDER_BOTTOM = 12; + public static final int CARD_BASE_PANEL_EMPTY_BOARDER_LEFT = 15; + public static final int CARD_BASE_PANEL_EMPTY_BOARDER_RIGHT = 15; + +} diff --git a/src/main/java/view/DisplayLocalEventsView.java b/src/main/java/view/DisplayLocalEventsView.java new file mode 100644 index 00000000..f4938714 --- /dev/null +++ b/src/main/java/view/DisplayLocalEventsView.java @@ -0,0 +1,864 @@ +package view; + +import entity.Event; +import entity.Location; +import interface_adapter.ViewManagerModel; +import interface_adapter.ViewNames; +import interface_adapter.search.SearchController; +import interface_adapter.display_local_events.DisplayLocalEventsController; +import interface_adapter.display_local_events.DisplayLocalEventsViewModel; +import interface_adapter.update_location.UpdateLocationController; +import interface_adapter.update_location.UpdateLocationViewModel; +import interface_adapter.update_location.UpdateLocationState; +import use_case.update_location.UpdateLocationUserDataAccessInterface; + +import javax.imageio.ImageIO; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.net.URL; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * View for displaying local events with user location support and date filtering. + * Supports loading saved location from previous sessions. + */ +public class DisplayLocalEventsView extends JPanel implements PropertyChangeListener { + + private DisplayLocalEventsController controller; + private final DisplayLocalEventsViewModel viewModel; + + private CalendarView calendarView; + + private final String viewName = ViewNames.DISPLAY_LOCAL_EVENTS; + + // Location components + private UpdateLocationController updateLocationController; + private UpdateLocationViewModel updateLocationViewModel; + private UpdateLocationUserDataAccessInterface userDataAccess; + private JTextField addressField; + private JButton setLocationButton; + private JLabel currentLocationLabel; + private JButton searchEventsButton; + + // Date filter components + private LocalDate selectedDate = null; // null means show all dates + private JLabel dateFilterLabel; + private JButton clearDateFilterButton; + + private final JLabel appNameLabel = new JLabel("Event Gate"); + 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("Set your location and search to see local events.", SwingConstants.CENTER); + private static final double DEFAULT_RADIUS_KM = 50.0; + private ViewManagerModel viewManagerModel; + + // User's saved location + private Location userLocation = null; + private boolean locationLoaded = false; // Track if we've tried to load saved location + + // Event selection listener for navigation + private EventSelectionListener eventSelectionListener; + + public interface EventSelectionListener { + void onEventSelected(Event event); + } + + public DisplayLocalEventsView(DisplayLocalEventsViewModel viewModel) { + this.viewModel = viewModel; + this.viewModel.addPropertyChangeListener(this); + + setLayout(new BorderLayout()); + setBorder(new EmptyBorder(0, 0, 0, 0)); + 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(); + + calendarButton.addActionListener(e -> navigateToCalendar()); + logoutButton.addActionListener(e -> handleLogout()); + savedEventsButton.addActionListener(e -> navigateToSavedEvents()); + + // Load saved location when view becomes visible + addComponentListener(new java.awt.event.ComponentAdapter() { + @Override + public void componentShown(java.awt.event.ComponentEvent e) { + loadSavedLocationIfNeeded(); + } + }); + } + + public void setViewManagerModel(ViewManagerModel viewManagerModel) { + this.viewManagerModel = viewManagerModel; + } + + public String getViewName() { + return viewName; + } + + public void setController(DisplayLocalEventsController controller) { + this.controller = controller; + } + + public void setUpdateLocationController(UpdateLocationController controller) { + this.updateLocationController = controller; + } + + public void setUpdateLocationViewModel(UpdateLocationViewModel viewModel) { + this.updateLocationViewModel = viewModel; + this.updateLocationViewModel.addPropertyChangeListener(this); + } + + /** + * Set the user data access for loading saved location. + */ + public void setUserDataAccess(UpdateLocationUserDataAccessInterface userDataAccess) { + this.userDataAccess = userDataAccess; + } + + public void setEventSelectionListener(EventSelectionListener listener) { + this.eventSelectionListener = listener; + } + + /** + * Load the user's saved location from the database if not already set. + * Called when the view becomes visible after login. + */ + private void loadSavedLocationIfNeeded() { + // Only try to load once per session and only if we don't have a location + if (!locationLoaded && userLocation == null && userDataAccess != null) { + locationLoaded = true; + + Location savedLocation = userDataAccess.getCurrentUserLocation(); + String savedAddress = userDataAccess.getCurrentUserAddress(); + + if (savedLocation != null && savedAddress != null && !savedAddress.isEmpty()) { + this.userLocation = savedLocation; + String displayAddress = shortenAddress(savedAddress); + currentLocationLabel.setText(displayAddress); + currentLocationLabel.setForeground(new Color(34, 197, 94)); // Green + System.out.println("Loaded saved location: " + savedAddress); + } + } + } + + /** + * Set the user's saved location (called on login or externally). + */ + public void setUserLocation(Location location, String address) { + this.userLocation = location; + if (location != null && currentLocationLabel != null) { + String displayAddress = shortenAddress(address); + currentLocationLabel.setText(displayAddress); + currentLocationLabel.setForeground(new Color(34, 197, 94)); + } + } + + private String shortenAddress(String address) { + if (address == null) return "Location set"; + if (address.length() <= 35) return address; + String[] parts = address.split(","); + if (parts.length >= 2) { + return parts[0].trim() + ", " + parts[1].trim(); + } + return address.substring(0, 32) + "..."; + } + + /** + * Get the current user location for distance calculations. + */ + public Location getCurrentLocationForOthers() { + return userLocation; + } + + /** + * Search events for a specific date. + * Called from CalendarView when user clicks on a date. + */ + public void searchEventsForDate(LocalDate date) { + this.selectedDate = date; + updateDateFilterDisplay(); + + if (userLocation == null) { + userLocation = new Location("Toronto, ON", 43.6532, -79.3832); + currentLocationLabel.setText("Toronto, ON (default)"); + currentLocationLabel.setForeground(new Color(255, 193, 7)); + } + + onSearch(); + } + + /** + * Clear the date filter and show all events. + */ + public void clearDateFilter() { + this.selectedDate = null; + updateDateFilterDisplay(); + renderEvents(); + } + + private void updateDateFilterDisplay() { + if (dateFilterLabel != null && clearDateFilterButton != null) { + if (selectedDate != null) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy"); + dateFilterLabel.setText("Showing events for: " + selectedDate.format(formatter)); + dateFilterLabel.setForeground(new Color(25, 118, 210)); + dateFilterLabel.setVisible(true); + clearDateFilterButton.setVisible(true); + } else { + dateFilterLabel.setText(""); + dateFilterLabel.setVisible(false); + clearDateFilterButton.setVisible(false); + } + } + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if ("state".equals(evt.getPropertyName())) { + if (updateLocationViewModel != null && evt.getSource() == updateLocationViewModel) { + handleLocationUpdate(); + } else { + renderEvents(); + } + } + } + + private void handleLocationUpdate() { + UpdateLocationState state = updateLocationViewModel.getState(); + if (state.isSuccess()) { + userLocation = state.getLocation(); + searchBarView.setLocation(userLocation); + String displayAddress = shortenAddress(state.getAddress()); + currentLocationLabel.setText(displayAddress); + currentLocationLabel.setForeground(new Color(34, 197, 94)); + addressField.setText(""); + + JOptionPane.showMessageDialog(this, + "Location saved!\nYou can now search for events near you.", + "Location Set", JOptionPane.INFORMATION_MESSAGE); + } else if (state.getError() != null) { + JOptionPane.showMessageDialog(this, + state.getError(), + "Location Error", JOptionPane.ERROR_MESSAGE); + } + } + + + public void setCalendarView(CalendarView calendarView) { + this.calendarView = calendarView; + } + + /** + * Build a two-row top bar for better layout. + */ + private JPanel buildTopBar() { + JPanel topBarContainer = new JPanel(); + topBarContainer.setLayout(new BoxLayout(topBarContainer, BoxLayout.Y_AXIS)); + + // === ROW 1: App name + Location input === + JPanel row1 = new JPanel(new BorderLayout()); + row1.setBorder(new EmptyBorder(10, 15, 5, 15)); + row1.setBackground(new Color(25, 118, 210)); + + appNameLabel.setFont(new Font("Segoe UI", Font.BOLD, 22)); + appNameLabel.setForeground(Color.WHITE); + row1.add(appNameLabel, BorderLayout.WEST); + + JPanel locationPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 0)); + locationPanel.setOpaque(false); + + JLabel locationLabel = new JLabel("Your Location:"); + locationLabel.setForeground(Color.WHITE); + locationLabel.setFont(new Font("Segoe UI", Font.PLAIN, 13)); + locationPanel.add(locationLabel); + + addressField = new JTextField(22); + addressField.setPreferredSize(new Dimension(220, 30)); + addressField.setToolTipText("Enter address, intersection, or landmark (e.g., 'Bloor and Spadina', 'CN Tower')"); + addressField.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(200, 200, 200)), + BorderFactory.createEmptyBorder(5, 10, 5, 10) + )); + addressField.setText("Enter address or landmark..."); + addressField.setForeground(Color.GRAY); + addressField.addFocusListener(new java.awt.event.FocusAdapter() { + @Override + public void focusGained(java.awt.event.FocusEvent e) { + if (addressField.getText().equals("Enter address or landmark...")) { + addressField.setText(""); + addressField.setForeground(Color.BLACK); + } + } + + @Override + public void focusLost(java.awt.event.FocusEvent e) { + if (addressField.getText().isEmpty()) { + addressField.setText("Enter address or landmark..."); + addressField.setForeground(Color.GRAY); + } + } + }); + locationPanel.add(addressField); + + setLocationButton = new JButton("Set Location"); + setLocationButton.addActionListener(e -> onSetLocation()); + styleButton(setLocationButton, new Color(76, 175, 80), Color.WHITE); + locationPanel.add(setLocationButton); + + row1.add(locationPanel, BorderLayout.CENTER); + + currentLocationLabel = new JLabel("No location set"); + currentLocationLabel.setForeground(new Color(255, 200, 200)); + currentLocationLabel.setFont(new Font("Segoe UI", Font.ITALIC, 12)); + row1.add(currentLocationLabel, BorderLayout.EAST); + + // === ROW 2: Search + Filters === + JPanel row2 = new JPanel(new BorderLayout()); + row2.setBorder(new EmptyBorder(5, 15, 10, 15)); + row2.setBackground(new Color(30, 136, 229)); + + JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); + searchPanel.setOpaque(false); + + searchEventsButton = new JButton("Search Events"); + searchEventsButton.setFont(new Font("Segoe UI", Font.BOLD, 14)); + searchEventsButton.addActionListener(e -> onSearch()); + styleButton(searchEventsButton, Color.WHITE, new Color(25, 118, 210)); + searchEventsButton.setPreferredSize(new Dimension(140, 35)); + searchPanel.add(searchEventsButton); + + row2.add(searchPanel, BorderLayout.WEST); + + JPanel filterPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 15, 0)); + filterPanel.setOpaque(false); + + JLabel categoryLabel = new JLabel("Category:"); + categoryLabel.setForeground(Color.WHITE); + filterPanel.add(categoryLabel); + categoryBox.setPreferredSize(new Dimension(140, 28)); + filterPanel.add(categoryBox); + + JLabel sortLabel = new JLabel("Sort by:"); + sortLabel.setForeground(Color.WHITE); + filterPanel.add(sortLabel); + sortBox.setPreferredSize(new Dimension(100, 28)); + sortBox.addActionListener(e -> renderEvents()); + filterPanel.add(sortBox); + + row2.add(filterPanel, BorderLayout.CENTER); + + JPanel nameSearchPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0)); + nameSearchPanel.setOpaque(false); + + JLabel searchLabel = new JLabel("Search by name:"); + searchLabel.setForeground(Color.WHITE); + nameSearchPanel.add(searchLabel); + + searchBarView = new SearchBarView("Event name...", new Location("Toronto, ON", 43.6426, 79.3871)); + searchBarView.setPreferredSize(new Dimension(180, 32)); + nameSearchPanel.add(searchBarView); + + row2.add(nameSearchPanel, BorderLayout.EAST); + + // === ROW 3: Date Filter Display (hidden by default) === + JPanel row3 = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 5)); + row3.setBackground(new Color(255, 248, 225)); + row3.setBorder(new EmptyBorder(5, 10, 5, 10)); + + dateFilterLabel = new JLabel(""); + dateFilterLabel.setFont(new Font("Segoe UI", Font.BOLD, 13)); + dateFilterLabel.setForeground(new Color(25, 118, 210)); + dateFilterLabel.setVisible(false); + row3.add(dateFilterLabel); + + clearDateFilterButton = new JButton("Clear Filter"); + clearDateFilterButton.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + clearDateFilterButton.addActionListener(e -> clearDateFilter()); + clearDateFilterButton.setVisible(false); + clearDateFilterButton.setFocusPainted(false); + clearDateFilterButton.setBackground(new Color(244, 67, 54)); + clearDateFilterButton.setForeground(Color.WHITE); + clearDateFilterButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + row3.add(clearDateFilterButton); + + topBarContainer.add(row1); + topBarContainer.add(row2); + topBarContainer.add(row3); + + return topBarContainer; + } + + private void styleButton(JButton button, Color bgColor, Color fgColor) { + button.setBackground(bgColor); + button.setForeground(fgColor); + button.setFocusPainted(false); + button.setBorderPainted(false); + button.setOpaque(true); + button.setCursor(new Cursor(Cursor.HAND_CURSOR)); + button.setFont(new Font("Segoe UI", Font.BOLD, 12)); + } + + private JPanel buildSideBar() { + JPanel sideBar = new JPanel(); + sideBar.setLayout(new BoxLayout(sideBar, BoxLayout.Y_AXIS)); + sideBar.setBorder(new EmptyBorder(15, 10, 15, 10)); + sideBar.setBackground(new Color(250, 250, 250)); + sideBar.setPreferredSize(new Dimension(130, 0)); + + styleSideButton(calendarButton, "Calendar"); + styleSideButton(savedEventsButton, "Saved"); + styleSideButton(logoutButton, "Logout"); + + savedEventsButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + controller.switchToSaveView(); + } + }); + + sideBar.add(calendarButton); + sideBar.add(Box.createVerticalStrut(8)); + sideBar.add(savedEventsButton); + sideBar.add(Box.createVerticalStrut(8)); + sideBar.add(logoutButton); + sideBar.add(Box.createVerticalGlue()); + + JLabel helpLabel = new JLabel("
Tip: Click a date in Calendar to filter!
"); + helpLabel.setForeground(new Color(150, 150, 150)); + helpLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + sideBar.add(helpLabel); + + return sideBar; + } + + private void styleSideButton(JButton button, String text) { + button.setText(text); + button.setAlignmentX(Component.CENTER_ALIGNMENT); + button.setMaximumSize(new Dimension(120, 40)); + button.setPreferredSize(new Dimension(120, 40)); + button.setFocusPainted(false); + button.setBackground(Color.WHITE); + button.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + button.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(220, 220, 220)), + new EmptyBorder(8, 10, 8, 10) + )); + button.setCursor(new Cursor(Cursor.HAND_CURSOR)); + + button.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseEntered(java.awt.event.MouseEvent e) { + button.setBackground(new Color(240, 240, 240)); + } + + @Override + public void mouseExited(java.awt.event.MouseEvent e) { + button.setBackground(Color.WHITE); + } + }); + } + + private void onSetLocation() { + String address = addressField.getText().trim(); + + if (address.isEmpty() || address.equals("Enter address or landmark...")) { + JOptionPane.showMessageDialog(this, + "Please enter your location.\n\nYou can enter:\n- A full address (123 Main St, Toronto)\n- An intersection (Bloor and Spadina)\n- A landmark (CN Tower)\n- Just a city (Toronto, ON)", + "Address Required", JOptionPane.INFORMATION_MESSAGE); + return; + } + + if (updateLocationController != null) { + setLocationButton.setEnabled(false); + setLocationButton.setText("Finding..."); + + SwingWorker worker = new SwingWorker<>() { + @Override + protected Void doInBackground() { + updateLocationController.execute(address); + return null; + } + + @Override + protected void done() { + setLocationButton.setEnabled(true); + setLocationButton.setText("Set Location"); + } + }; + worker.execute(); + } else { + JOptionPane.showMessageDialog(this, + "Location service not available. Please try again.", + "Error", JOptionPane.ERROR_MESSAGE); + } + } + + private void onSearch() { + if (controller == null) { + JOptionPane.showMessageDialog(this, "Search not available.", + "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + if (userLocation == null) { + int choice = JOptionPane.showConfirmDialog(this, + "You haven't set your location yet.\n\nWould you like to use Toronto as your default location?", + "No Location Set", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); + + if (choice == JOptionPane.YES_OPTION) { + userLocation = new Location("Toronto, ON", 43.6532, -79.3832); + currentLocationLabel.setText("Toronto, ON (default)"); + currentLocationLabel.setForeground(new Color(255, 193, 7)); + } else { + return; + } + } + + String category = (String) categoryBox.getSelectedItem(); + + searchEventsButton.setText("Searching..."); + searchEventsButton.setEnabled(false); + + SwingWorker worker = new SwingWorker<>() { + @Override + protected Void doInBackground() { + controller.display(userLocation, DEFAULT_RADIUS_KM, category); + return null; + } + + @Override + protected void done() { + searchEventsButton.setText("Search Events"); + searchEventsButton.setEnabled(true); + } + }; + worker.execute(); + } + + private void renderEmptyState() { + cardsContainer.removeAll(); + cardsContainer.setLayout(new GridBagLayout()); + cardsContainer.setBackground(new Color(245, 247, 250)); + + JPanel emptyPanel = new JPanel(); + emptyPanel.setLayout(new BoxLayout(emptyPanel, BoxLayout.Y_AXIS)); + emptyPanel.setBackground(new Color(245, 247, 250)); + emptyPanel.setBorder(new EmptyBorder(30, 50, 30, 50)); + + // Icon + JLabel iconLabel = new JLabel("*", SwingConstants.CENTER); + iconLabel.setFont(new Font("Segoe UI", Font.BOLD, 48)); + iconLabel.setForeground(new Color(100, 100, 100)); + iconLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + + // Title + JLabel titleLabel = new JLabel("Find Events Near You"); + titleLabel.setFont(new Font("Segoe UI", Font.BOLD, 24)); + titleLabel.setForeground(new Color(50, 50, 50)); + titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + + // Description + String message = selectedDate != null + ? "
No events found for the selected date.
Try a different date or clear the filter.
" + : "
Enter your location above and click
'Search Events' to discover local events!
"; + JLabel descLabel = new JLabel(message); + descLabel.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + descLabel.setForeground(new Color(120, 120, 120)); + descLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + descLabel.setHorizontalAlignment(SwingConstants.CENTER); + + emptyPanel.add(iconLabel); + emptyPanel.add(Box.createVerticalStrut(20)); + emptyPanel.add(titleLabel); + emptyPanel.add(Box.createVerticalStrut(12)); + emptyPanel.add(descLabel); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.anchor = GridBagConstraints.CENTER; + + cardsContainer.add(emptyPanel, gbc); + 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()); + + // Apply date filter if set + if (selectedDate != null) { + cards = cards.stream() + .filter(card -> { + String dateTimeStr = card.getDateTime(); + if (dateTimeStr != null && dateTimeStr.length() >= 10) { + try { + LocalDate cardDate = LocalDate.parse(dateTimeStr.substring(0, 10)); + return cardDate.equals(selectedDate); + } catch (Exception e) { + return true; + } + } + return true; + }) + .collect(Collectors.toList()); + } + + if (cards.isEmpty()) { + renderEmptyState(); + return; + } + + 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, 12, 12)); + for (DisplayLocalEventsViewModel.EventCard cardData : cards) { + JPanel card = buildEventCard(cardData); + cardsContainer.add(card); + } + } + + cardsContainer.revalidate(); + cardsContainer.repaint(); + } + + 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; + } + } + + 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)); + + 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(248, 250, 252)); + 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, 120)); + pictureLabel.setBackground(new Color(240, 240, 240)); + pictureLabel.setOpaque(true); + ImageIcon icon = loadImageIcon(cardData.getImageUrl(), 200, 120); + if (icon != null) { + pictureLabel.setIcon(icon); + } else { + pictureLabel.setText("[No Image]"); + pictureLabel.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + pictureLabel.setForeground(new Color(180, 180, 180)); + } + card.add(pictureLabel, BorderLayout.NORTH); + + JPanel textPanel = new JPanel(); + textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS)); + textPanel.setBorder(new EmptyBorder(10, 12, 10, 12)); + textPanel.setOpaque(false); + + String truncatedName = truncateText(cardData.getName(), 40); + JLabel nameLabel = new JLabel("" + truncatedName + ""); + nameLabel.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + + String truncatedAddress = truncateText(cardData.getAddress(), 50); + JLabel addressLabel = new JLabel(truncatedAddress); + addressLabel.setFont(new Font("Segoe UI", Font.PLAIN, 11)); + addressLabel.setForeground(new Color(100, 100, 100)); + + JLabel dateLabel = new JLabel(cardData.getDateTime()); + dateLabel.setFont(new Font("Segoe UI", Font.PLAIN, 11)); + dateLabel.setForeground(new Color(120, 120, 120)); + + JLabel distanceLabel = new JLabel(cardData.getDistanceText() + " away"); + distanceLabel.setFont(new Font("Segoe UI", Font.BOLD, 12)); + distanceLabel.setForeground(new Color(25, 118, 210)); + + textPanel.add(nameLabel); + textPanel.add(Box.createVerticalStrut(4)); + textPanel.add(addressLabel); + textPanel.add(Box.createVerticalStrut(4)); + textPanel.add(dateLabel); + textPanel.add(Box.createVerticalStrut(6)); + textPanel.add(distanceLabel); + + card.add(textPanel, BorderLayout.CENTER); + return card; + } + + /** + * Truncate text to a maximum length, adding "..." if truncated. + */ + private String truncateText(String text, int maxLength) { + if (text == null) return ""; + if (text.length() <= maxLength) return text; + return text.substring(0, maxLength - 3) + "..."; + } + + private void onEventCardClicked(String eventId) { + List events = viewModel.getEvents(); + + Event selectedEvent = null; + if (events != null) { + for (Event event : events) { + if (event.getId().equals(eventId)) { + selectedEvent = event; + break; + } + } + } + + if (selectedEvent != null && eventSelectionListener != null) { + eventSelectionListener.onEventSelected(selectedEvent); + } + } + + 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) { + return null; + } + } + + public void setSearchBarController(SearchController controller) { + if (searchBarView != null) { + this.searchBarView.setSearchController(controller); + } + } + + private void navigateToCalendar() { + // Use the user's saved/entered location, or default to Toronto + Location userLoc = userLocation; + if (userLoc == null) { + userLoc = new Location("Toronto, ON", 43.6532, -79.3832); + } + + if (calendarView != null) { + calendarView.setUserLocation(userLoc); + calendarView.setSearchRadiusKm(DEFAULT_RADIUS_KM); + } + + if (viewManagerModel != null) { + viewManagerModel.setState(ViewNames.CALENDAR); + viewManagerModel.firePropertyChange(); + } + } + + private void navigateToSavedEvents() { + if (viewManagerModel != null) { + viewManagerModel.setState(ViewNames.SAVE_EVENTS); + viewManagerModel.firePropertyChange(); + } + } + + private void handleLogout() { + userLocation = null; + locationLoaded = false; + selectedDate = null; + updateDateFilterDisplay(); + if (currentLocationLabel != null) { + currentLocationLabel.setText("No location set"); + currentLocationLabel.setForeground(new Color(255, 200, 200)); + } + if (viewManagerModel != null) { + viewManagerModel.setState(ViewNames.LOGIN); + viewManagerModel.firePropertyChange(); + } + } + + public DisplayLocalEventsViewModel getViewModel() { + return this.viewModel; + } +} \ 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..d6c24da9 --- /dev/null +++ b/src/main/java/view/EventDescriptionView.java @@ -0,0 +1,441 @@ +package view; + +import entity.Event; +import entity.Location; +import interface_adapter.ViewManagerModel; +import interface_adapter.ViewNames; +import interface_adapter.event_description.EventDescriptionViewModel; +import interface_adapter.save_event.SaveEventController; +import use_case.save_event.SaveEventInteractor; + +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.time.format.DateTimeFormatter; + +/** + * View for displaying event details. + * + * Features: + * - Shows event details (name, category, date, location, description) + * - Displays event image + * - Shows distance from user's location + * - Save Event button that shows "Already Saved" if event is already saved + * - Back navigation to previous view (events list or calendar event list) + */ +public class EventDescriptionView extends JPanel implements PropertyChangeListener { + + private final EventDescriptionViewModel viewModel; + private ViewManagerModel viewManagerModel; + private SaveEventController saveEventController; + private SaveEventInteractor saveEventInteractor; + private Event currentEvent; + + // Track which view we came from for back navigation + private String previousViewName = ViewNames.DISPLAY_LOCAL_EVENTS; + + // 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"); + 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) { + if (saveButton.isEnabled()) { + saveButton.setBackground(new Color(37, 99, 235)); + } + } + + @Override + public void mouseExited(java.awt.event.MouseEvent e) { + if (saveButton.isEnabled()) { + saveButton.setBackground(new Color(59, 130, 246)); + } + } + }); + } + + /** + * Set the previous view name for back navigation. + * Called before navigating to this view. + */ + public void setPreviousView(String viewName) { + this.previousViewName = viewName; + } + + /** + * 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."); + resetSaveButton(); + 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..."); + + // Check if event is already saved and update button accordingly + updateSaveButtonState(); + } + + /** + * Check if the current event is already saved and update button state. + */ + private void updateSaveButtonState() { + if (currentEvent != null && saveEventInteractor != null) { + boolean isAlreadySaved = saveEventInteractor.isEventSaved(currentEvent.getId()); + if (isAlreadySaved) { + showAlreadySavedState(); + } else { + resetSaveButton(); + } + } else { + resetSaveButton(); + } + } + + /** + * Show the button in "Already Saved" state. + */ + private void showAlreadySavedState() { + saveButton.setText("Saved"); + saveButton.setBackground(new Color(34, 197, 94)); + saveButton.setEnabled(false); + saveButton.setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); + } + + /** + * Reset save button to default state. + */ + private void resetSaveButton() { + saveButton.setText("Save Event"); + saveButton.setBackground(new Color(59, 130, 246)); + saveButton.setEnabled(true); + saveButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + } + + /** + * 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(); + } + + /** + * Navigate back to the previous view. + * This respects which view the user came from. + */ + private void navigateBack() { + if (viewManagerModel != null) { + viewManagerModel.setState(previousViewName); + viewManagerModel.firePropertyChange(); + } + } + + private void saveEvent() { + if (currentEvent != null && saveEventController != null) { + saveEventController.saveEvent(currentEvent); + showAlreadySavedState(); + + JOptionPane.showMessageDialog(this, + "Event saved! You can view it in your Saved Events.", + "Event Saved", JOptionPane.INFORMATION_MESSAGE); + } 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; + } + + public void setSaveEventInteractor(SaveEventInteractor saveEventInteractor) { + this.saveEventInteractor = saveEventInteractor; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + 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..42d166f1 --- /dev/null +++ b/src/main/java/view/EventListByDateView.java @@ -0,0 +1,339 @@ +package view; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Font; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Locale; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingConstants; +import javax.swing.border.EmptyBorder; + +import org.jetbrains.annotations.NotNull; + +import entity.Event; +import interface_adapter.ViewManagerModel; +import interface_adapter.ViewNames; +import interface_adapter.calendarFlow.CalendarFlowController; +import interface_adapter.calendarFlow.CalendarFlowState; +import interface_adapter.calendarFlow.CalendarFlowViewModel; + +/** + * View for displaying a list of events occurring on a specific date. + * This view shows events retrieved from the calendar in a scrollable list format, + * with each event displayed as a card showing its name, location, and time. + * Users can click on individual events to view more details. + */ +public class EventListByDateView extends JPanel implements PropertyChangeListener { + private final String viewName = ViewNames.EVENT_LIST_BY_DATE; + private CalendarFlowViewModel calendarFlowViewModel; + private CalendarFlowController calendarFlowController; + private ViewManagerModel viewManagerModel; + private EventSelectionListener eventSelectionListener; + + 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 = "Segoe UI"; + + public EventListByDateView(CalendarFlowViewModel calendarFlowViewModel) { + this.calendarFlowViewModel = calendarFlowViewModel; + this.calendarFlowViewModel.addPropertyChangeListener(this); + + setLayout(new BorderLayout()); + setBorder(new EmptyBorder( + Constants.EMPTY_BORDER_SIZE, + Constants.EMPTY_BORDER_SIZE, + Constants.EMPTY_BORDER_SIZE, + Constants.EMPTY_BORDER_SIZE)); + setBackground(new Color(Constants.BACKGROUND_COLOR_R, + Constants.BACKGROUND_COLOR_G, + Constants.BACKGROUND_COLOR_B)); + + add(buildTopPanel(), BorderLayout.NORTH); + + eventsContainer.setLayout(new BoxLayout(eventsContainer, BoxLayout.Y_AXIS)); + eventsContainer.setBorder(new EmptyBorder(Constants.CONTAINER_EMPTY_BORDER_SIZE, + Constants.CONTAINER_EMPTY_BORDER_SIZE, + Constants.CONTAINER_EMPTY_BORDER_SIZE, + Constants.CONTAINER_EMPTY_BORDER_SIZE)); + eventsContainer.setBackground(new Color(Constants.BACKGROUND_COLOR_R, + Constants.BACKGROUND_COLOR_G, + Constants.BACKGROUND_COLOR_B)); + + scrollPane = new JScrollPane(eventsContainer); + scrollPane.getVerticalScrollBar().setUnitIncrement(Constants.UNIT_INCREMENT_SCALE); + scrollPane.setBorder(null); + add(scrollPane, BorderLayout.CENTER); + } + + public void setController(CalendarFlowController controller) { + this.calendarFlowController = controller; + } + + public void setViewManagerModel(ViewManagerModel viewManagerModel) { + this.viewManagerModel = viewManagerModel; + } + + public void setEventSelectionListener(EventSelectionListener listener) { + this.eventSelectionListener = listener; + } + + private JPanel buildTopPanel() { + final JPanel topPanel = new JPanel(new BorderLayout()); + topPanel.setBorder(new EmptyBorder(Constants.TOP_PANEL_EMPTY_BORDER_SIZE, + Constants.TOP_PANEL_EMPTY_BORDER_SIZE, + Constants.TOP_PANEL_EMPTY_BORDER_SIZE, + Constants.TOP_PANEL_EMPTY_BORDER_SIZE)); + topPanel.setBackground(new Color(Constants.TOP_PANEL_BACKGROUND_COLOR_R, + Constants.TOP_PANEL_BACKGROUND_COLOR_G, + Constants.TOP_PANEL_BACKGROUND_COLOR_B)); + + backButton.setFocusPainted(false); + backButton.setBackground(Color.WHITE); + backButton.setForeground(Color.BLACK); + backButton.setContentAreaFilled(true); + backButton.setBorderPainted(true); + backButton.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(Constants.BACK_BUTTON_LINE_BOARDER_COLOR_R, + Constants.BACK_BUTTON_LINE_BOARDER_COLOR_G, + Constants.BACK_BUTTON_LINE_BOARDER_COLOR_B), 1), + new EmptyBorder(Constants.BACK_BUTTON_EMPTY_BOARDER_TOP, + Constants.BACK_BUTTON_EMPTY_BOARDER_LEFT, + Constants.BACK_BUTTON_EMPTY_BOARDER_BOTTOM, + Constants.BACK_BUTTON_EMPTY_BOARDER_RIGHT) + ) + ); + backButton.setFont(new Font(textFont, Font.PLAIN, Constants.BACK_BUTTON_FONT_SIZE)); + backButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + backButton.setUI(new javax.swing.plaf.basic.BasicButtonUI()); + + backButton.addActionListener(event -> { + if (calendarFlowController != null) { + calendarFlowController.switchToDashboardView(); + } + }); + + topPanel.add(backButton, BorderLayout.WEST); + + final JPanel centerPanel = new JPanel(); + centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS)); + centerPanel.setOpaque(false); + + titleLabel.setFont(new Font(textFont, Font.BOLD, Constants.CENTER_PANEL_TITLE_LABEL_FONT_SIZE)); + titleLabel.setForeground(Color.WHITE); + titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + + dateLabel.setFont(new Font(textFont, Font.PLAIN, Constants.CENTER_PANEL_DATE_LABEL_FONT_SIZE)); + dateLabel.setForeground(Color.WHITE); + dateLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + + centerPanel.add(titleLabel); + centerPanel.add(Box.createVerticalStrut(Constants.CENTER_PANEL_BOX_VERTICAL_STRUC_HIGH)); + centerPanel.add(dateLabel); + topPanel.add(centerPanel, BorderLayout.CENTER); + + return topPanel; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + final CalendarFlowState state = (CalendarFlowState) evt.getNewValue(); + + if (state.getDate() != null) { + final 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()); + } + } + + private void renderError(String errorMessage) { + eventsContainer.removeAll(); + eventsContainer.setLayout(new BorderLayout()); + + final JLabel errorLabel = new JLabel(errorMessage, SwingConstants.CENTER); + errorLabel.setFont(errorLabel.getFont().deriveFont(Font.PLAIN, Constants.ERROR_LABEL_TEXT_FONT_SIZE)); + errorLabel.setForeground(new Color(Constants.ERROR_LABEL_BACKGROUND_COLOR_R, + Constants.ERROR_LABEL_BACKGROUND_COLOR_G, + Constants.ERROR_LABEL_BACKGROUND_COLOR_B)); + + 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) { + final JPanel eventCard = buildEventCard(event); + eventsContainer.add(eventCard); + eventsContainer.add(Box.createVerticalStrut(Constants.EVENT_CONTAINER_BOX_VERTICAL_STRUC_HEIGHT)); + } + + eventsContainer.revalidate(); + eventsContainer.repaint(); + } + + private JPanel buildEventCard(Event event) { + final JPanel card = createCardBasePanel(); + final JPanel infoPanel = createInfoPanel(); + final JLabel nameLabel = createNameLabel(event); + final JLabel locationLabel = createLocationLabel(event); + + final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("h:mm a", Locale.ENGLISH); + final String timeText = event.getStartTime().format(timeFormatter); + final JLabel timeLabel = new JLabel(timeText); + timeLabel.setFont(new Font(textFont, Font.PLAIN, Constants.TIME_LABEL_TEXT_FONT_SIZE)); + timeLabel.setForeground(new Color(Constants.TIME_LABEL_BACKGROUND_COLOR_R, + Constants.TIME_LABEL_BACKGROUND_COLOR_G, + Constants.TIME_LABEL_BACKGROUND_COLOR_B)); + timeLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + infoPanel.add(nameLabel); + infoPanel.add(Box.createVerticalStrut(Constants.INFO_PANEL_BOX_VERTICAL_STRUC_HEIGHT)); + infoPanel.add(locationLabel); + infoPanel.add(Box.createVerticalStrut(Constants.INFO_PANEL_BOX_VERTICAL_STRUC_HEIGHT)); + infoPanel.add(timeLabel); + + card.add(infoPanel, BorderLayout.CENTER); + + // Button that navigates to event description view + final JButton exploreButton = createExploreButton(); + exploreButton.setUI(new javax.swing.plaf.basic.BasicButtonUI()); + + // Add hover effect + exploreButton.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseEntered(java.awt.event.MouseEvent e) { + exploreButton.setBackground(new Color(Constants.EXPLORE_BUTTON_MOUSE_ENTER_COLOR_R, + Constants.EXPLORE_BUTTON_MOUSE_ENTER_COLOR_G, + Constants.EXPLORE_BUTTON_MOUSE_ENTER_COLOR_B)); + } + + @Override + public void mouseExited(java.awt.event.MouseEvent e) { + exploreButton.setBackground(new Color(Constants.EXPLORE_BUTTON_MOUSE_EXITED_COLOR_R, + Constants.EXPLORE_BUTTON_MOUSE_EXITED_COLOR_G, + Constants.EXPLORE_BUTTON_MOUSE_EXITED_COLOR_B)); + } + }); + + // Navigate to event description view when clicked + exploreButton.addActionListener(eve -> { + System.out.println("Explore event: " + event.getName()); + if (eventSelectionListener != null) { + eventSelectionListener.onEventSelected(event); + } + }); + + card.add(exploreButton, BorderLayout.EAST); + + return card; + } + + @NotNull + private JButton createExploreButton() { + final JButton exploreButton = new JButton("->"); + exploreButton.setFont(new Font(textFont, Font.BOLD, Constants.EXPLORE_BUTTON_FONT_SIZE)); + exploreButton.setFocusPainted(false); + exploreButton.setOpaque(true); + exploreButton.setContentAreaFilled(true); + exploreButton.setBorderPainted(false); + exploreButton.setBackground(new Color(Constants.EXPLORE_BUTTON_BACKGROUND_COLOR_R, + Constants.EXPLORE_BUTTON_BACKGROUND_COLOR_G, + Constants.EXPLORE_BUTTON_BACKGROUND_COLOR_B)); + exploreButton.setForeground(Color.WHITE); + exploreButton.setBorder(BorderFactory.createEmptyBorder( + Constants.EXPLORE_BUTTON_EMPTY_BOARDER_TOP, + Constants.EXPLORE_BUTTON_EMPTY_BOARDER_LEFT, + Constants.EXPLORE_BUTTON_EMPTY_BOARDER_BOTTOM, + Constants.EXPLORE_BUTTON_EMPTY_BOARDER_RIGHT)); + exploreButton.setPreferredSize(new Dimension(Constants.EXPLORE_BUTTON_PREFERRED_WIDTH, + Constants.EXPLORE_BUTTON_PREFERRED_HEIGHT)); + exploreButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + return exploreButton; + } + + @NotNull + private JLabel createLocationLabel(Event event) { + final JLabel locationLabel = new JLabel(event.getLocation().getAddress()); + locationLabel.setFont(new Font(textFont, Font.PLAIN, Constants.LOCATION_LABEL_TEXT_FONT_SIZE)); + locationLabel.setForeground(new Color(Constants.LOCATION_LABEL_BACKGROUND_COLOR_R, + Constants.LOCATION_LABEL_BACKGROUND_COLOR_G, + Constants.LOCATION_LABEL_BACKGROUND_COLOR_B)); + locationLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + return locationLabel; + } + + @NotNull + private static JLabel createNameLabel(Event event) { + final JLabel nameLabel = new JLabel(event.getName()); + nameLabel.setFont(nameLabel.getFont().deriveFont(Font.BOLD, Constants.NAMEL_LABEL_TEXT_FONT_SIZE)); + nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + return nameLabel; + } + + @NotNull + private static JPanel createCardBasePanel() { + final JPanel card = new JPanel(new BorderLayout()); + card.setMaximumSize(new Dimension(Integer.MAX_VALUE, Constants.CARD_BASE_PANEL_MAXIMUM_SIZE_HEIGHT)); + card.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(Constants.CARD_BASE_PANEL_LINE_BOARDER_COLOR_R, + Constants.CARD_BASE_PANEL_LINE_BOARDER_COLOR_G, + Constants.CARD_BASE_PANEL_LINE_BOARDER_COLOR_B), 1), + new EmptyBorder(Constants.CARD_BASE_PANEL_EMPTY_BOARDER_TOP, + Constants.CARD_BASE_PANEL_EMPTY_BOARDER_LEFT, + Constants.CARD_BASE_PANEL_EMPTY_BOARDER_BOTTOM, + Constants.CARD_BASE_PANEL_EMPTY_BOARDER_RIGHT) + )); + card.setBackground(Color.WHITE); + return card; + } + + @NotNull + private static JPanel createInfoPanel() { + final JPanel infoPanel = new JPanel(); + infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.Y_AXIS)); + infoPanel.setOpaque(false); + return infoPanel; + } + + public String getViewName() { + return viewName; + } + + /** + * Listener interface for handling event selection actions. + */ + public interface EventSelectionListener { + /** + * Called when an event is selected by the user. + * @param event event the event that was selected + */ + void onEventSelected(Event event); + } +} 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..194e52cb 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -1,5 +1,6 @@ package view; +import interface_adapter.ViewNames; import interface_adapter.logged_in.ChangePasswordController; import interface_adapter.logged_in.LoggedInState; import interface_adapter.logged_in.LoggedInViewModel; @@ -19,11 +20,11 @@ */ public class LoggedInView extends JPanel implements ActionListener, PropertyChangeListener { - private final String viewName = "logged in"; + private final String viewName = ViewNames.LOGGED_IN; 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; @@ -108,9 +109,13 @@ public void changedUpdate(DocumentEvent e) { * @param evt the ActionEvent to react to */ 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..5ab9e8b6 100644 --- a/src/main/java/view/LoginView.java +++ b/src/main/java/view/LoginView.java @@ -1,8 +1,10 @@ package view; +import interface_adapter.ViewNames; 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; @@ -18,7 +20,7 @@ */ public class LoginView extends JPanel implements ActionListener, PropertyChangeListener { - private final String viewName = "log in"; + private final String viewName = ViewNames.LOGIN; private final LoginViewModel loginViewModel; private final JTextField usernameInputField = new JTextField(15); @@ -33,21 +35,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 +76,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 +85,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 +118,6 @@ public void changedUpdate(DocumentEvent e) { } }); - this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - passwordInputField.getDocument().addDocumentListener(new DocumentListener() { private void documentListenerHelper() { @@ -117,13 +142,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..b12683c1 --- /dev/null +++ b/src/main/java/view/SaveButtonView.java @@ -0,0 +1,194 @@ +package view; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.Timer; + +import entity.Event; +import interface_adapter.save_event.SaveEventController; + +/** + * View for the save event button. + */ +public class SaveButtonView extends JPanel { + + private static final int BUTTON_WIDTH = 160; + private static final int BUTTON_HEIGHT = 45; + private static final int BORDER_WIDTH_NORMAL = 3; + private static final int BORDER_WIDTH_HOVER = 2; + private static final int FONT_SIZE = 14; + private static final int RESET_DELAY = 10000; + + private static final Color BASE_COLOR = new Color(59, 130, 246); + private static final Color PRESSED_BACKGROUND_COLOR = new Color(37, 99, 235); + private static final Color WHITE_COLOR = Color.WHITE; + + private static final String FONT_NAME = "Segoe UI"; + private static final String SAVE_BUTTON_TEXT = "Save Event"; + private static final String SAVED_BUTTON_TEXT = "Saved"; + private static final String YIPPEE_MESSAGE = "YIPPEE"; + + private final String viewName = "save"; + private JButton saveButton; + private Event event; + private SaveEventController saveButtonController; + private Timer resetTimer; + + /** + * Constructor for SaveButtonView. + */ + public SaveButtonView() { + initializeButton(); + addActionListeners(); + add(saveButton); + } + + private void initializeButton() { + saveButton = new JButton(SAVE_BUTTON_TEXT); + saveButton.setFont(new Font(FONT_NAME, Font.BOLD, FONT_SIZE)); + saveButton.setForeground(BASE_COLOR); + saveButton.setBackground(WHITE_COLOR); + saveButton.setBorder(BorderFactory.createLineBorder(BASE_COLOR, BORDER_WIDTH_NORMAL)); + saveButton.setPreferredSize(new Dimension(BUTTON_WIDTH, BUTTON_HEIGHT)); + saveButton.setContentAreaFilled(false); + saveButton.setOpaque(true); + } + + private void addActionListeners() { + addMouseListeners(); + addActionListener(); + } + + private void addMouseListeners() { + saveButton.addMouseListener(new MouseAdapter() { + @Override + public void mouseEntered(final MouseEvent mouseEvent) { + handleMouseEntered(); + } + + @Override + public void mouseExited(final MouseEvent mouseEvent) { + handleMouseExited(); + } + + @Override + public void mousePressed(final MouseEvent mouseEvent) { + handleMousePressed(); + } + + @Override + public void mouseReleased(final MouseEvent mouseEvent) { + handleMouseReleased(mouseEvent); + } + }); + } + + private void handleMouseEntered() { + saveButton.setBackground(BASE_COLOR); + saveButton.setForeground(WHITE_COLOR); + saveButton.setBorder(BorderFactory.createLineBorder(WHITE_COLOR, BORDER_WIDTH_HOVER)); + } + + private void handleMouseExited() { + saveButton.setBackground(WHITE_COLOR); + saveButton.setForeground(BASE_COLOR); + saveButton.setBorder(BorderFactory.createLineBorder(BASE_COLOR, BORDER_WIDTH_NORMAL)); + } + + private void handleMousePressed() { + saveButton.setBackground(PRESSED_BACKGROUND_COLOR); + saveButton.setBorder(BorderFactory.createLineBorder(PRESSED_BACKGROUND_COLOR, + BORDER_WIDTH_HOVER)); + } + + private void handleMouseReleased(final MouseEvent mouseEvent) { + if (saveButton.contains(mouseEvent.getPoint())) { + saveButton.setBackground(BASE_COLOR); + saveButton.setBorder(BorderFactory.createLineBorder(BASE_COLOR, BORDER_WIDTH_HOVER)); + } + else { + saveButton.setBackground(WHITE_COLOR); + saveButton.setForeground(BASE_COLOR); + saveButton.setBorder(BorderFactory.createLineBorder(BASE_COLOR, BORDER_WIDTH_HOVER)); + } + } + + private void addActionListener() { + saveButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent actionEvent) { + handleSaveAction(); + } + }); + } + + private void handleSaveAction() { + if (saveButtonController != null && event != null) { + saveButtonController.saveEvent(event); + saveButton.setText(SAVED_BUTTON_TEXT); + saveButton.setEnabled(false); + startResetTimer(); + } + } + + private void startResetTimer() { + if (resetTimer != null && resetTimer.isRunning()) { + resetTimer.stop(); + } + + resetTimer = new Timer(RESET_DELAY, new ActionListener() { + @Override + public void actionPerformed(final ActionEvent actionEvent) { + resetButtonState(); + } + }); + resetTimer.setRepeats(false); + resetTimer.start(); + } + + private void resetButtonState() { + if (saveButton != null) { + saveButton.setText(SAVE_BUTTON_TEXT); + saveButton.setEnabled(true); + saveButton.setForeground(BASE_COLOR); + saveButton.setBackground(WHITE_COLOR); + saveButton.setBorder(BorderFactory.createLineBorder(BASE_COLOR, BORDER_WIDTH_NORMAL)); + } + } + + /** + * Sets the event to be saved. + * + * @param newEvent the event to set + */ + public void setEvent(final Event newEvent) { + this.event = newEvent; + } + + /** + * Sets the save event controller. + * + * @param controller the controller to set + */ + public void setSaveEventController(final SaveEventController controller) { + this.saveButtonController = controller; + } + + /** + * Gets the view name. + * + * @return the view name + */ + public String getViewName() { + return viewName; + } +} diff --git a/src/main/java/view/SaveEventsView.java b/src/main/java/view/SaveEventsView.java new file mode 100644 index 00000000..c3957ba2 --- /dev/null +++ b/src/main/java/view/SaveEventsView.java @@ -0,0 +1,438 @@ +package view; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingConstants; +import javax.swing.border.EmptyBorder; + +import entity.Event; +import interface_adapter.ViewManagerModel; +import interface_adapter.ViewNames; +import interface_adapter.save_event.SaveEventController; +import interface_adapter.save_event.SaveEventViewModel; +import use_case.save_event.SaveEventInteractor; + +/** + * View for displaying saved events. + * Shows all events the user has saved and allows them to remove events. + * Automatically refreshes when the view becomes visible. + */ +public class SaveEventsView extends JPanel implements PropertyChangeListener { + + private static final int HEADER_PADDING = 15; + private static final int HEADER_BUTTON_PADDING = 8; + private static final int CONTAINER_PADDING = 20; + private static final int SCROLL_UNIT = 16; + private static final int HEADER_FONT_SIZE = 20; + private static final int BUTTON_FONT_SIZE = 14; + private static final int SMALL_BUTTON_FONT_SIZE = 12; + private static final int EMPTY_ICON_SIZE = 48; + private static final int EMPTY_TITLE_SIZE = 24; + private static final int COUNT_FONT_SIZE = 14; + private static final int CARD_FONT_SIZE = 16; + private static final int BADGE_FONT_SIZE = 10; + private static final int CARD_SPACING = 12; + private static final int CARD_PADDING = 15; + private static final int MAX_CARD_HEIGHT = 120; + private static final int MAX_NAME_LENGTH = 50; + private static final int MAX_ADDRESS_LENGTH = 60; + + private static final Color BACKGROUND_COLOR = new Color(245, 247, 250); + private static final Color HEADER_COLOR = new Color(25, 118, 210); + private static final Color HEADER_HOVER = new Color(21, 101, 192); + private static final Color PRIMARY_BLUE = new Color(59, 130, 246); + private static final Color BADGE_BLUE = new Color(219, 234, 254); + private static final Color CARD_BORDER = new Color(230, 230, 230); + private static final Color CARD_HOVER = new Color(250, 250, 250); + private static final Color ERROR_RED = new Color(220, 53, 69); + private static final Color ERROR_BG = new Color(255, 240, 240); + private static final Color TEXT_DARK = new Color(30, 30, 30); + private static final Color TEXT_GRAY = new Color(100, 100, 100); + private static final Color TEXT_LIGHT_GRAY = new Color(120, 120, 120); + private static final Color TEXT_MEDIUM_GRAY = new Color(150, 150, 150); + private static final Color TEXT_DARK_GRAY = new Color(50, 50, 50); + + private final String viewName = ViewNames.SAVE_EVENTS; + private final SaveEventViewModel viewModel; + private ViewManagerModel viewManagerModel; + private SaveEventInteractor saveEventInteractor; + private SaveEventController saveEventController; + + private final JPanel eventsContainer = new JPanel(); + private final JButton backButton = new JButton("<- Back to Events"); + private final DateTimeFormatter dateFormatter = + DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy 'at' h:mm a"); + + public SaveEventsView(SaveEventViewModel viewModel) { + this.viewModel = viewModel; + this.viewModel.addPropertyChangeListener(this); + + setLayout(new BorderLayout()); + setBackground(BACKGROUND_COLOR); + + add(createHeader(), BorderLayout.NORTH); + + eventsContainer.setLayout(new BoxLayout(eventsContainer, BoxLayout.Y_AXIS)); + eventsContainer.setBackground(BACKGROUND_COLOR); + eventsContainer.setBorder(new EmptyBorder(CONTAINER_PADDING, CONTAINER_PADDING, + CONTAINER_PADDING, CONTAINER_PADDING)); + + final JScrollPane scrollPane = new JScrollPane(eventsContainer); + scrollPane.setBorder(null); + scrollPane.getVerticalScrollBar().setUnitIncrement(SCROLL_UNIT); + scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); + add(scrollPane, BorderLayout.CENTER); + + addComponentListener(new java.awt.event.ComponentAdapter() { + @Override + public void componentShown(java.awt.event.ComponentEvent evt) { + renderSavedEvents(); + } + }); + + renderSavedEvents(); + } + + private JPanel createHeader() { + final JPanel header = new JPanel(new BorderLayout()); + header.setBackground(HEADER_COLOR); + header.setBorder(new EmptyBorder(HEADER_PADDING, CONTAINER_PADDING, + HEADER_PADDING, CONTAINER_PADDING)); + + backButton.setFont(new Font("Segoe UI", Font.PLAIN, BUTTON_FONT_SIZE)); + backButton.setForeground(Color.WHITE); + backButton.setBackground(HEADER_COLOR); + backButton.setBorder(BorderFactory.createEmptyBorder(HEADER_BUTTON_PADDING, + HEADER_PADDING, HEADER_BUTTON_PADDING, HEADER_PADDING)); + backButton.setFocusPainted(false); + backButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + backButton.setOpaque(true); + backButton.setContentAreaFilled(false); + + backButton.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseEntered(java.awt.event.MouseEvent evt) { + backButton.setContentAreaFilled(true); + backButton.setBackground(HEADER_HOVER); + } + + @Override + public void mouseExited(java.awt.event.MouseEvent evt) { + backButton.setContentAreaFilled(false); + } + }); + + backButton.addActionListener(evt -> navigateBack()); + header.add(backButton, BorderLayout.WEST); + + final JLabel title = new JLabel("Your Saved Events"); + title.setFont(new Font("Segoe UI", Font.BOLD, HEADER_FONT_SIZE)); + title.setForeground(Color.WHITE); + title.setHorizontalAlignment(SwingConstants.CENTER); + header.add(title, BorderLayout.CENTER); + + final JButton refreshButton = new JButton("Refresh"); + refreshButton.setFont(new Font("Segoe UI", Font.PLAIN, SMALL_BUTTON_FONT_SIZE)); + refreshButton.setForeground(Color.WHITE); + refreshButton.setBackground(HEADER_COLOR); + refreshButton.setBorder(BorderFactory.createEmptyBorder(HEADER_BUTTON_PADDING, + HEADER_PADDING, HEADER_BUTTON_PADDING, HEADER_PADDING)); + refreshButton.setFocusPainted(false); + refreshButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + refreshButton.setContentAreaFilled(false); + refreshButton.addActionListener(evt -> renderSavedEvents()); + header.add(refreshButton, BorderLayout.EAST); + + return header; + } + + private void navigateBack() { + if (viewManagerModel != null) { + viewManagerModel.setState("display local events"); + viewManagerModel.firePropertyChange(); + } + } + + /** + * Render the saved events list. + * Called on initial load, when view becomes visible, and when events change. + */ + public void renderSavedEvents() { + eventsContainer.removeAll(); + + List savedEvents = null; + if (saveEventInteractor != null) { + savedEvents = saveEventInteractor.getSavedEvents(); + System.out.println("Loaded " + (savedEvents != null ? savedEvents.size() : 0) + + " saved events"); + } + + if (savedEvents == null || savedEvents.isEmpty()) { + renderEmptyState(); + } + else { + eventsContainer.setLayout(new BoxLayout(eventsContainer, BoxLayout.Y_AXIS)); + + final JLabel countLabel = new JLabel("You have " + savedEvents.size() + " saved event" + + (savedEvents.size() == 1 ? "" : "s")); + countLabel.setFont(new Font("Segoe UI", Font.PLAIN, COUNT_FONT_SIZE)); + countLabel.setForeground(TEXT_GRAY); + countLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + countLabel.setBorder(new EmptyBorder(0, 0, HEADER_PADDING, 0)); + eventsContainer.add(countLabel); + + for (Event event : savedEvents) { + final JPanel card = createEventCard(event); + card.setAlignmentX(Component.LEFT_ALIGNMENT); + eventsContainer.add(card); + eventsContainer.add(Box.createVerticalStrut(CARD_SPACING)); + } + + eventsContainer.add(Box.createVerticalGlue()); + } + + eventsContainer.revalidate(); + eventsContainer.repaint(); + } + + private void renderEmptyState() { + eventsContainer.setLayout(new GridBagLayout()); + + final JPanel emptyPanel = new JPanel(); + emptyPanel.setLayout(new BoxLayout(emptyPanel, BoxLayout.Y_AXIS)); + emptyPanel.setBackground(BACKGROUND_COLOR); + emptyPanel.setBorder(new EmptyBorder(MAX_NAME_LENGTH, MAX_NAME_LENGTH, + MAX_NAME_LENGTH, MAX_NAME_LENGTH)); + + final JLabel iconLabel = new JLabel("📅", SwingConstants.CENTER); + iconLabel.setFont(new Font("Segoe UI", Font.BOLD, EMPTY_ICON_SIZE)); + iconLabel.setForeground(TEXT_MEDIUM_GRAY); + iconLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + + final JLabel titleLabel = new JLabel("No Saved Events Yet"); + titleLabel.setFont(new Font("Segoe UI", Font.BOLD, EMPTY_TITLE_SIZE)); + titleLabel.setForeground(TEXT_DARK_GRAY); + titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + + final JLabel descLabel = new JLabel("
" + + "Events you save will appear here.
" + + "Click 'Save Event' on any event to add it!
"); + descLabel.setFont(new Font("Segoe UI", Font.PLAIN, COUNT_FONT_SIZE)); + descLabel.setForeground(TEXT_LIGHT_GRAY); + descLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + descLabel.setHorizontalAlignment(SwingConstants.CENTER); + + final JButton browseButton = new JButton("Browse Events"); + browseButton.setFont(new Font("Segoe UI", Font.BOLD, BUTTON_FONT_SIZE)); + browseButton.setForeground(Color.WHITE); + browseButton.setBackground(PRIMARY_BLUE); + browseButton.setBorder(new EmptyBorder(CARD_SPACING, EMPTY_TITLE_SIZE, + CARD_SPACING, EMPTY_TITLE_SIZE)); + browseButton.setFocusPainted(false); + browseButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + browseButton.setAlignmentX(Component.CENTER_ALIGNMENT); + browseButton.addActionListener(evt -> navigateBack()); + + emptyPanel.add(iconLabel); + emptyPanel.add(Box.createVerticalStrut(CONTAINER_PADDING)); + emptyPanel.add(titleLabel); + emptyPanel.add(Box.createVerticalStrut(CARD_SPACING)); + emptyPanel.add(descLabel); + emptyPanel.add(Box.createVerticalStrut(EMPTY_TITLE_SIZE + 1)); + emptyPanel.add(browseButton); + + final GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.anchor = GridBagConstraints.CENTER; + + eventsContainer.add(emptyPanel, gbc); + } + + private JPanel createEventCard(Event event) { + final JPanel card = new JPanel(new BorderLayout(HEADER_PADDING, 0)); + card.setBackground(Color.WHITE); + card.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(CARD_BORDER), + new EmptyBorder(CARD_PADDING, CARD_PADDING, CARD_PADDING, CARD_PADDING) + )); + card.setMaximumSize(new Dimension(Integer.MAX_VALUE, MAX_CARD_HEIGHT)); + + final JPanel infoPanel = new JPanel(); + infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.Y_AXIS)); + infoPanel.setOpaque(false); + + final String eventName = truncateText(event.getName(), MAX_NAME_LENGTH); + final JLabel nameLabel = new JLabel(eventName); + nameLabel.setFont(new Font("Segoe UI", Font.BOLD, CARD_FONT_SIZE)); + nameLabel.setForeground(TEXT_DARK); + nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + final JLabel categoryLabel = new JLabel(event.getCategory().getDisplayName()); + categoryLabel.setFont(new Font("Segoe UI", Font.BOLD, BADGE_FONT_SIZE)); + categoryLabel.setForeground(PRIMARY_BLUE); + categoryLabel.setOpaque(true); + categoryLabel.setBackground(BADGE_BLUE); + categoryLabel.setBorder(new EmptyBorder(2, HEADER_BUTTON_PADDING, + 2, HEADER_BUTTON_PADDING)); + categoryLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + final String address = event.getLocation() != null + ? event.getLocation().getAddress() : "Location not available"; + final String truncatedAddress = truncateText(address, MAX_ADDRESS_LENGTH); + final JLabel locationLabel = new JLabel(truncatedAddress); + locationLabel.setFont(new Font("Segoe UI", Font.PLAIN, SMALL_BUTTON_FONT_SIZE)); + locationLabel.setForeground(TEXT_GRAY); + locationLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + final String dateTimeStr = event.getStartTime() != null + ? event.getStartTime().format(dateFormatter) : "Date not available"; + final JLabel dateLabel = new JLabel(dateTimeStr); + dateLabel.setFont(new Font("Segoe UI", Font.PLAIN, SMALL_BUTTON_FONT_SIZE)); + dateLabel.setForeground(TEXT_GRAY); + dateLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + + final int spacing = 5; + infoPanel.add(nameLabel); + infoPanel.add(Box.createVerticalStrut(spacing)); + infoPanel.add(categoryLabel); + infoPanel.add(Box.createVerticalStrut(HEADER_BUTTON_PADDING)); + infoPanel.add(locationLabel); + infoPanel.add(Box.createVerticalStrut(spacing / 2)); + infoPanel.add(dateLabel); + + card.add(infoPanel, BorderLayout.CENTER); + + final JButton removeButton = new JButton("Remove"); + removeButton.setFont(new Font("Segoe UI", Font.PLAIN, SMALL_BUTTON_FONT_SIZE)); + removeButton.setForeground(ERROR_RED); + removeButton.setBackground(Color.WHITE); + removeButton.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(ERROR_RED), + new EmptyBorder(HEADER_BUTTON_PADDING, CARD_SPACING, + HEADER_BUTTON_PADDING, CARD_SPACING) + )); + removeButton.setFocusPainted(false); + removeButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + + removeButton.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseEntered(java.awt.event.MouseEvent evt) { + removeButton.setBackground(ERROR_BG); + } + + @Override + public void mouseExited(java.awt.event.MouseEvent evt) { + removeButton.setBackground(Color.WHITE); + } + }); + + removeButton.addActionListener(evt -> handleRemoveEvent(event)); + + final JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + buttonPanel.setOpaque(false); + buttonPanel.add(removeButton); + card.add(buttonPanel, BorderLayout.EAST); + + card.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseEntered(java.awt.event.MouseEvent evt) { + card.setBackground(CARD_HOVER); + card.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(PRIMARY_BLUE), + new EmptyBorder(CARD_PADDING, CARD_PADDING, + CARD_PADDING, CARD_PADDING) + )); + } + + @Override + public void mouseExited(java.awt.event.MouseEvent evt) { + card.setBackground(Color.WHITE); + card.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(CARD_BORDER), + new EmptyBorder(CARD_PADDING, CARD_PADDING, + CARD_PADDING, CARD_PADDING) + )); + } + }); + + return card; + } + + private void handleRemoveEvent(Event event) { + final int confirm = JOptionPane.showConfirmDialog(this, + "Remove \"" + event.getName() + "\" from saved events?", + "Confirm Remove", + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE); + + if (confirm == JOptionPane.YES_OPTION && saveEventInteractor != null) { + saveEventInteractor.unsaveEvent(event); + renderSavedEvents(); + JOptionPane.showMessageDialog(this, + "Event removed from saved events.", + "Removed", JOptionPane.INFORMATION_MESSAGE); + } + } + + /** + * Truncate text to a maximum length, adding "..." if truncated. + * @param text the text to truncate + * @param maxLength the maximum length + * @return the truncated text + */ + private String truncateText(String text, int maxLength) { + if (text == null) { + return ""; + } + if (text.length() <= maxLength) { + return text; + } + final int truncateLength = 3; + return text.substring(0, maxLength - truncateLength) + "..."; + } + + public void setViewManagerModel(ViewManagerModel viewManagerModel) { + this.viewManagerModel = viewManagerModel; + } + + public void setSaveEventInteractor(SaveEventInteractor interactor) { + this.saveEventInteractor = interactor; + } + + public void setSaveEventController(SaveEventController controller) { + this.saveEventController = controller; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + renderSavedEvents(); + } + + public String getViewName() { + return viewName; + } +} \ No newline at end of file diff --git a/src/main/java/view/SearchBarView.java b/src/main/java/view/SearchBarView.java new file mode 100644 index 00000000..b682cce3 --- /dev/null +++ b/src/main/java/view/SearchBarView.java @@ -0,0 +1,119 @@ +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, 40)); + 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(5, 20, 5, 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(1, 8, 1, 8)); + 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; + } + + public void setLocation(Location location) { + this.location = location; + } + +} diff --git a/src/main/java/view/SearchEventByNameView.java b/src/main/java/view/SearchEventByNameView.java new file mode 100644 index 00000000..70299566 --- /dev/null +++ b/src/main/java/view/SearchEventByNameView.java @@ -0,0 +1,233 @@ +package view; + +import javax.swing.*; + +import interface_adapter.ViewNames; +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 = ViewNames.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..06274cf4 100644 --- a/src/main/java/view/SignupView.java +++ b/src/main/java/view/SignupView.java @@ -1,5 +1,6 @@ package view; +import interface_adapter.ViewNames; import interface_adapter.signup.SignupController; import interface_adapter.signup.SignupState; import interface_adapter.signup.SignupViewModel; @@ -10,6 +11,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; @@ -17,7 +22,7 @@ * The View for the Signup Use Case. */ public class SignupView extends JPanel implements ActionListener, PropertyChangeListener { - private final String viewName = "sign up"; + private final String viewName = ViewNames.SIGNUP; private final SignupViewModel signupViewModel; private final JTextField usernameInputField = new JTextField(15); @@ -26,15 +31,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 +64,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 +93,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..9202292e --- /dev/null +++ b/src/test/java/demo/NoDataAccessEventViewDemo.java @@ -0,0 +1,91 @@ +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); + + } +} \ 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..ce000a80 --- /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..d6deeff1 --- /dev/null +++ b/src/test/java/use_case/calendarFlow/CalendarFlowInteractorTest.java @@ -0,0 +1,320 @@ +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 testNullEventList() { + mockDataAccess.setReturnNull(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); + + interactor.execute(inputData); + + assertTrue(mockPresenter.isFailViewCalled()); + assertTrue(mockPresenter.getErrorMessage().contains("No events found")); + } + + @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; + private boolean returnNull = false; + + public void setReturnEmptyList(boolean returnEmptyList) { + this.returnEmptyList = returnEmptyList; + } + + public void setReturnNull(boolean returnNull) { + this.returnNull = returnNull; + } + + 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 (returnNull) { + return null; + } + 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", + "Address", + 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", + "Toronto", + 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", + "Toronto2", + 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; + } + } + + @Test + public void testSwitchToDashboardViewDelegatesToPresenter() { + assertFalse(mockPresenter.isSwitchToDashboardViewCalled()); + interactor.switchToDashboardView(); + assertTrue(mockPresenter.isSwitchToDashboardViewCalled()); + } + + /** + * 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; + private boolean switchToDashboardViewCalled = false; // NEW + + + @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; + } + + @Override + public void switchToDashboardView() { // NEW + this.switchToDashboardViewCalled = true; // NEW + } + + public boolean isSwitchToDashboardViewCalled() { // NEW + return switchToDashboardViewCalled; + } + + public CalendarFlowOutputData getOutputData() { + return outputData; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void reset() { + successViewCalled = false; + failViewCalled = false; + switchToDashboardViewCalled = false; + outputData = null; + errorMessage = null; + } + } +} diff --git a/src/test/java/use_case/calendarFlow/viewDemoAndTest/CalendarFlowDataAccessObjectTest.java b/src/test/java/use_case/calendarFlow/viewDemoAndTest/CalendarFlowDataAccessObjectTest.java new file mode 100644 index 00000000..b69ecac0 --- /dev/null +++ b/src/test/java/use_case/calendarFlow/viewDemoAndTest/CalendarFlowDataAccessObjectTest.java @@ -0,0 +1,66 @@ +package use_case.calendarFlow.viewDemoAndTest; + +import data_access.CalendarFlowDataAccessObject; +import entity.Event; +import entity.Location; +import interface_adapter.calendarFlow.CalendarFlowState; +import interface_adapter.calendarFlow.CalendarFlowViewModel; +import view.EventListByDateView; + +import javax.swing.*; + import java.time.LocalDate; +import java.util.List; + +/** + * Simple manual test/demo: + * - makes up a date, location, radius + * - calls CalendarFlowDataAccessObject.getEventsByDate(...) + * - shows the result in EventListByDateView + */ +public class CalendarFlowDataAccessObjectTest { + + public static void main(String[] args) { + // 1. Make up test inputs (change these to whatever you want) + LocalDate selectedDate = LocalDate.of(2025, 11, 30); + Location userLocation = new Location( + "Downtown Toronto", + 43.6532, // latitude + -79.3832 // longitude + ); + double radiusKm = 10.0; + + // 2. Call the real data access object + CalendarFlowDataAccessObject dao = new CalendarFlowDataAccessObject(); + List events = dao.getEventsByDate(selectedDate, userLocation, radiusKm); + + // Just to see something in console as well + System.out.println("Selected date: " + selectedDate); + System.out.println("Location: " + userLocation); + System.out.println("Radius (km): " + radiusKm); + System.out.println("Number of events returned: " + events.size()); + + // 3. Show the result in EventListByDateView + SwingUtilities.invokeLater(() -> { + // ViewModel and View + CalendarFlowViewModel viewModel = new CalendarFlowViewModel(); + EventListByDateView view = new EventListByDateView(viewModel); + + // Build state from the DAO result + CalendarFlowState state = new CalendarFlowState(); + state.setDate(selectedDate); + state.setEventList(events); + + // Push state into the ViewModel (this will trigger propertyChange on the view) + viewModel.setState(state); + viewModel.firePropertyChange(); + + // 4. Wrap the view in a frame and show it + JFrame frame = new JFrame("Events for " + selectedDate); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setContentPane(view); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + }); + } +} diff --git a/src/test/java/use_case/calendarFlow/viewDemoAndTest/CalendarFlowViewDemo.java b/src/test/java/use_case/calendarFlow/viewDemoAndTest/CalendarFlowViewDemo.java new file mode 100644 index 00000000..2c01131e --- /dev/null +++ b/src/test/java/use_case/calendarFlow/viewDemoAndTest/CalendarFlowViewDemo.java @@ -0,0 +1,18 @@ +package use_case.calendarFlow.viewDemoAndTest; + +import view.CalendarView; +import javax.swing.*; + +public class CalendarFlowViewDemo { + 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/viewDemoAndTest/CodeCombinationTest.java b/src/test/java/use_case/calendarFlow/viewDemoAndTest/CodeCombinationTest.java new file mode 100644 index 00000000..5a28ae6e --- /dev/null +++ b/src/test/java/use_case/calendarFlow/viewDemoAndTest/CodeCombinationTest.java @@ -0,0 +1,157 @@ +//Assisted by AI +package use_case.calendarFlow.viewDemoAndTest; +// ============================================ +// SimpleCalendarAPITest.java +// Simple test: Click date → See REAL events from API +// With YOUR mock location and radius +// ============================================ + +import use_case.calendarFlow.CalendarFlowDataAccessInterface; +import use_case.calendarFlow.CalendarFlowInputBoundary; +import use_case.calendarFlow.CalendarFlowInteractor; +import use_case.calendarFlow.CalendarFlowOutputBoundary; +import view.CalendarView; +import view.EventListByDateView; +import interface_adapter.calendarFlow.*; +import interface_adapter.ViewManagerModel; +import data_access.CalendarFlowDataAccessObject; +import entity.Location; + +import javax.swing.*; +import java.awt.*; + +/** + * SIMPLE TEST to see real API results + * + * What you'll see: + * 1. Calendar view opens + * 2. Click any date + * 3. Real API call with YOUR location and radius + * 4. Event list shows REAL events from Ticketmaster + * + * Mock Data: + * - Location: Toronto, ON (43.6532, -79.3832) + * - Radius: 100 km + * + * You can change these below! + */ +public class CodeCombinationTest { + + // ============================================ + // 🎯 CHANGE THESE TO TEST DIFFERENT LOCATIONS + // ============================================ + private static final String TEST_LOCATION_NAME = "Toronto, ON"; + private static final double TEST_LATITUDE = 43.6532; + private static final double TEST_LONGITUDE = -79.3832; + private static final double TEST_RADIUS_KM = 100.0; + + // Other test locations you can try: + // Montreal: 45.5019, -73.5674 + // New York: 40.7128, -74.0060 + // Los Angeles: 34.0522, -118.2437 + // Chicago: 41.8781, -87.6298 + + public static void main(String[] args) { + System.out.println("╔════════════════════════════════════════════════════════╗"); + System.out.println("║ SIMPLE CALENDAR API TEST ║"); + System.out.println("║ Real Ticketmaster Events ║"); + System.out.println("╚════════════════════════════════════════════════════════╝\n"); + + System.out.println("📍 Test Configuration:"); + System.out.println(" Location: " + TEST_LOCATION_NAME); + System.out.println(" Coordinates: (" + TEST_LATITUDE + ", " + TEST_LONGITUDE + ")"); + System.out.println(" Search Radius: " + TEST_RADIUS_KM + " km"); + System.out.println(); + System.out.println("⚠️ Make sure API key is set in CalendarFlowDataAccessObject.java"); + System.out.println(" Line: private static final String API_KEY = \"YOUR_KEY_HERE\";\n"); + + SwingUtilities.invokeLater(() -> { + // Create test location + Location testLocation = new Location(TEST_LOCATION_NAME, TEST_LATITUDE, TEST_LONGITUDE); + + // Setup view switching + CardLayout cardLayout = new CardLayout(); + JPanel cardPanel = new JPanel(cardLayout); + + ViewManagerModel viewManagerModel = new ViewManagerModel(); + viewManagerModel.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals("state")) { + String viewName = (String) evt.getNewValue(); + cardLayout.show(cardPanel, viewName); + System.out.println("→ Switched to: " + viewName); + } + }); + + // Create ViewModel + CalendarFlowViewModel viewModel = new CalendarFlowViewModel(); + + // Create Views + CalendarView calendarView = new CalendarView(); + EventListByDateView eventListView = new EventListByDateView(viewModel); + + // Create Use Case with REAL API + CalendarFlowDataAccessInterface dataAccess = new CalendarFlowDataAccessObject(); + CalendarFlowOutputBoundary presenter = new CalendarFlowPresenter(viewModel, viewManagerModel); + CalendarFlowInputBoundary interactor = new CalendarFlowInteractor(dataAccess, presenter); + CalendarFlowController controller = new CalendarFlowController(interactor); + + // Wire up views with TEST location and radius + calendarView.setUserLocation(testLocation); + calendarView.setSearchRadiusKm(TEST_RADIUS_KM); + calendarView.setEventController(controller); + + eventListView.setController(controller); + + // Add views to panel + cardPanel.add(calendarView, "calendar view"); + cardPanel.add(eventListView, "event list by date"); + + // Create window + JFrame frame = new JFrame("Calendar API Test - " + TEST_LOCATION_NAME); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.add(cardPanel); + frame.setSize(900, 700); + frame.setLocationRelativeTo(null); + + // Start with calendar view + cardLayout.show(cardPanel, "calendar view"); + frame.setVisible(true); + + // Instructions + System.out.println("✅ Application started!\n"); + System.out.println("╔════════════════════════════════════════════════════════╗"); + System.out.println("║ HOW TO USE ║"); + System.out.println("╚════════════════════════════════════════════════════════╝"); + System.out.println("1. You see the calendar"); + System.out.println("2. Click ANY date"); + System.out.println("3. Watch console for API call details"); + System.out.println("4. Wait 1-2 seconds for API response"); + System.out.println("5. See REAL events in the list!"); + System.out.println(); + System.out.println("╔════════════════════════════════════════════════════════╗"); + System.out.println("║ WHAT YOU'LL SEE IN THE EVENT LIST ║"); + System.out.println("╚════════════════════════════════════════════════════════╝"); + System.out.println("• Event name (e.g., 'Toronto Raptors vs Lakers')"); + System.out.println("• Venue address (e.g., 'Scotiabank Arena, 40 Bay St')"); + System.out.println("• Event time (e.g., '7:30 PM')"); + System.out.println("• Category indicator (Sports/Music/Arts/Film)"); + System.out.println(); + System.out.println("╔════════════════════════════════════════════════════════╗"); + System.out.println("║ WATCH CONSOLE FOR ║"); + System.out.println("╚════════════════════════════════════════════════════════╝"); + System.out.println("• Selected date"); + System.out.println("• API request details"); + System.out.println("• Number of events found"); + System.out.println("• Event details"); + System.out.println(); + }); + } +} + +// ============================================ +// DetailedCalendarAPITest.java +// Same as above but with DETAILED console output +// Shows exactly what API returns +// ============================================ + + diff --git a/src/test/java/use_case/calendarFlow/viewDemoAndTest/EventListByDateViewTest.java b/src/test/java/use_case/calendarFlow/viewDemoAndTest/EventListByDateViewTest.java new file mode 100644 index 00000000..ce2bf476 --- /dev/null +++ b/src/test/java/use_case/calendarFlow/viewDemoAndTest/EventListByDateViewTest.java @@ -0,0 +1,184 @@ +package use_case.calendarFlow.viewDemoAndTest; + +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", + "Demo Address 1", + 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", + "Demo Address 2", + 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", + "Demo Address 3", + 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 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()); + + 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(); + frame.setLocationRelativeTo(null); + 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..b5841cf2 --- /dev/null +++ b/src/test/java/use_case/display_local_events/DisplayLocalEventsInteractorTest.java @@ -0,0 +1,924 @@ +package use_case.display_local_events; + +import data_access.TicketmasterEventRepositoryAdapter; +import entity.Event; +import entity.EventCategory; +import entity.EventRepository; +import entity.Location; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Nested; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +//by using Chatgpt: I use the ai to help me to do the interactor test. +class DisplayLocalEventsInteractorTest { + + private DisplayLocalEventsInteractor interactor; + private TestEventRepository testEventRepository; + private TestOutputBoundary testOutputBoundary; + + @BeforeEach + void setUp() { + testEventRepository = new TestEventRepository(); + testOutputBoundary = new TestOutputBoundary(); + interactor = new DisplayLocalEventsInteractor(testEventRepository, testOutputBoundary); + } + + @Test + void testExecuteWithValidInput() { + Location userLocation = new Location(43.6532, -79.3832); + double radius = 10.0; + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, radius, null + ); + + Event event1 = createTestEvent("1", "Concert", 43.6532, -79.3832, EventCategory.MUSIC); + Event event2 = createTestEvent("2", "Game", 43.6600, -79.3900, EventCategory.SPORTS); + testEventRepository.addEvent(event1); + testEventRepository.addEvent(event2); + + interactor.execute(inputData); + + assertTrue(testOutputBoundary.isSuccessCalled); + assertFalse(testOutputBoundary.isErrorCalled); + assertNotNull(testOutputBoundary.outputData); + assertEquals(2, testOutputBoundary.outputData.getEvents().size()); + assertTrue(testOutputBoundary.outputData.getMessage().contains("Found 2 events")); + } + + @Test + void testExecuteWithNullLocation() { + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + null, 10.0, null + ); + + interactor.execute(inputData); + + assertTrue(testOutputBoundary.isErrorCalled); + assertFalse(testOutputBoundary.isSuccessCalled); + assertEquals("Invalid input: location and radius are required", + testOutputBoundary.errorMessage); + } + + @Test + void testExecuteWithNegativeRadius() { + Location userLocation = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, -5.0, null + ); + + interactor.execute(inputData); + + assertTrue(testOutputBoundary.isErrorCalled); + assertFalse(testOutputBoundary.isSuccessCalled); + } + + + + @Test + void testExecuteWithZeroRadius() { + Location userLocation = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, 0.0, null + ); + + interactor.execute(inputData); + + assertTrue(testOutputBoundary.isErrorCalled); + } + + @Test + void testExecuteWithCategoryFilter() { + Location userLocation = new Location(43.6532, -79.3832); + double radius = 10.0; + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, radius, EventCategory.MUSIC + ); + + Event musicEvent = createTestEvent("1", "Concert", 43.6532, -79.3832, EventCategory.MUSIC); + Event sportsEvent = createTestEvent("2", "Game", 43.6600, -79.3900, EventCategory.SPORTS); + testEventRepository.addEvent(musicEvent); + testEventRepository.addEvent(sportsEvent); + + interactor.execute(inputData); + + assertTrue(testOutputBoundary.isSuccessCalled); + assertEquals(1, testOutputBoundary.outputData.getEvents().size()); + assertEquals(EventCategory.MUSIC, + testOutputBoundary.outputData.getEvents().get(0).getCategory()); + assertTrue(testOutputBoundary.outputData.getMessage().contains("Music")); + } + + @Test + void testExecuteWithNoEventsFound() { + Location userLocation = new Location(43.6532, -79.3832); + double radius = 10.0; + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, radius, null + ); + + interactor.execute(inputData); + + assertTrue(testOutputBoundary.isSuccessCalled); + assertEquals(0, testOutputBoundary.outputData.getEvents().size()); + assertTrue(testOutputBoundary.outputData.getMessage().contains("Found 0 events")); + } + + @Test + void testExecuteEventsSortedByDistance() { + Location userLocation = new Location(43.6532, -79.3832); + double radius = 50.0; + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, radius, null + ); + + Event nearEvent = createTestEvent("1", "Near Event", 43.6532, -79.3832, EventCategory.MUSIC); + Event farEvent = createTestEvent("2", "Far Event", 43.7000, -79.4000, EventCategory.MUSIC); + + testEventRepository.addEvent(farEvent); + testEventRepository.addEvent(nearEvent); + + interactor.execute(inputData); + + assertTrue(testOutputBoundary.isSuccessCalled); + List events = testOutputBoundary.outputData.getEvents(); + assertEquals(2, events.size()); + + double distance1 = events.get(0).calculateDistanceTo(userLocation); + double distance2 = events.get(1).calculateDistanceTo(userLocation); + assertTrue(distance1 <= distance2, + "Events should be sorted by distance (closest first)"); + } + + @Test + void testEqualsWithSameEventsDifferentOrder() { + List events1 = new ArrayList<>(); + events1.add(createTestEvent("1", "Event 1", 43.6532, -79.3832, EventCategory.MUSIC)); + events1.add(createTestEvent("2", "Event 2", 43.6600, -79.3900, EventCategory.SPORTS)); + + List events2 = new ArrayList<>(); + events2.add(createTestEvent("2", "Event 2", 43.6600, -79.3900, EventCategory.SPORTS)); + events2.add(createTestEvent("1", "Event 1", 43.6532, -79.3832, EventCategory.MUSIC)); + + DisplayLocalEventsOutputData outputData1 = new DisplayLocalEventsOutputData( + events1, "message" + ); + DisplayLocalEventsOutputData outputData2 = new DisplayLocalEventsOutputData( + events2, "message" + ); + + assertNotEquals(outputData1, outputData2); + } + + @Test + void testExecuteWithException() { + TestEventRepository errorRepository = new TestEventRepository(); + errorRepository.setShouldThrowException(true); + DisplayLocalEventsInteractor errorInteractor = new DisplayLocalEventsInteractor( + errorRepository, testOutputBoundary + ); + + Location userLocation = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, 10.0, null + ); + + errorInteractor.execute(inputData); + + assertTrue(testOutputBoundary.isErrorCalled); + assertTrue(testOutputBoundary.errorMessage.contains("Error displaying local events")); + } + + @Test + void testSwitchToSaveView() { + interactor.switchToSaveView(); + assertTrue(testOutputBoundary.isSwitchToSaveViewCalled); + } + + @Test + void testEventDistancesCalculated() { + Location userLocation = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, 10.0, null + ); + + Event event = createTestEvent("1", "Event", 43.6532, -79.3832, EventCategory.MUSIC); + testEventRepository.addEvent(event); + + interactor.execute(inputData); + + Map distances = testOutputBoundary.outputData.getEventDistances(); + assertNotNull(distances); + assertTrue(distances.containsKey("1")); + assertTrue(distances.get("1") >= 0); + } + + @Test + void testEventsOutsideRadiusFiltered() { + Location userLocation = new Location(43.6532, -79.3832); + double radius = 5.0; + + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, radius, null + ); + + Event nearEvent = createTestEvent("1", "Near", 43.6532, -79.3832, EventCategory.MUSIC); + Event farEvent = createTestEvent("2", "Far", 44.0000, -80.0000, EventCategory.MUSIC); + + testEventRepository.addEvent(nearEvent); + testEventRepository.addEvent(farEvent); + + interactor.execute(inputData); + + assertTrue(testOutputBoundary.isSuccessCalled); + assertEquals(1, testOutputBoundary.outputData.getEvents().size()); + assertEquals("1", testOutputBoundary.outputData.getEvents().get(0).getId()); + } + + @Test + void testMultipleCategoriesWithFilter() { + Location userLocation = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, 10.0, EventCategory.SPORTS + ); + + Event musicEvent = createTestEvent("1", "Concert", 43.6532, -79.3832, EventCategory.MUSIC); + Event sportsEvent1 = createTestEvent("2", "Game 1", 43.6600, -79.3900, EventCategory.SPORTS); + Event sportsEvent2 = createTestEvent("3", "Game 2", 43.6550, -79.3850, EventCategory.SPORTS); + Event artsEvent = createTestEvent("4", "Play", 43.6580, -79.3880, EventCategory.ARTS_THEATRE); + + testEventRepository.addEvent(musicEvent); + testEventRepository.addEvent(sportsEvent1); + testEventRepository.addEvent(sportsEvent2); + testEventRepository.addEvent(artsEvent); + + interactor.execute(inputData); + + assertTrue(testOutputBoundary.isSuccessCalled); + assertEquals(2, testOutputBoundary.outputData.getEvents().size()); + for (Event event : testOutputBoundary.outputData.getEvents()) { + assertEquals(EventCategory.SPORTS, event.getCategory()); + } + } + + @Test + void testEqualsReturnsTrueForEqualEvents() { + List events = createTestEventList(); + + DisplayLocalEventsOutputData outputData1 = new DisplayLocalEventsOutputData( + events, "same message" + ); + DisplayLocalEventsOutputData outputData2 = new DisplayLocalEventsOutputData( + events, "same message" + ); + + assertTrue(outputData1.equals(outputData2)); + } + + @Test + void testExecuteWithTicketmasterAdapter() { + TestTicketmasterAdapter ticketmasterAdapter = new TestTicketmasterAdapter(); + DisplayLocalEventsInteractor ticketmasterInteractor = new DisplayLocalEventsInteractor( + ticketmasterAdapter, testOutputBoundary + ); + + Location userLocation = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, 10.0, null + ); + + Event event = createTestEvent("1", "Event", 43.6532, -79.3832, EventCategory.MUSIC); + ticketmasterAdapter.addEvent(event); + + ticketmasterInteractor.execute(inputData); + + assertTrue(testOutputBoundary.isSuccessCalled); + assertTrue(ticketmasterAdapter.findEventsCalled); + assertEquals(1, testOutputBoundary.outputData.getEvents().size()); + } + + @Test + void testExecuteMessageWithCategory() { + Location userLocation = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, 10.0, EventCategory.MUSIC + ); + + Event event = createTestEvent("1", "Concert", 43.6532, -79.3832, EventCategory.MUSIC); + testEventRepository.addEvent(event); + + interactor.execute(inputData); + + assertTrue(testOutputBoundary.isSuccessCalled); + String message = testOutputBoundary.outputData.getMessage(); + assertTrue(message.contains("Music")); + assertTrue(message.contains("1")); + } + + @Test + void testExecuteMessageWithoutCategory() { + Location userLocation = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, 10.0, null + ); + + Event event = createTestEvent("1", "Event", 43.6532, -79.3832, EventCategory.MUSIC); + testEventRepository.addEvent(event); + + interactor.execute(inputData); + + assertTrue(testOutputBoundary.isSuccessCalled); + String message = testOutputBoundary.outputData.getMessage(); + assertFalse(message.contains("Music")); + assertTrue(message.contains("1")); + } + + @Test + void testCalculateEventDistancesMultipleEvents() { + Location userLocation = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + userLocation, 10.0, null + ); + + Event event1 = createTestEvent("1", "Event1", 43.6532, -79.3832, EventCategory.MUSIC); + Event event2 = createTestEvent("2", "Event2", 43.6600, -79.3900, EventCategory.MUSIC); + testEventRepository.addEvent(event1); + testEventRepository.addEvent(event2); + + interactor.execute(inputData); + + Map distances = testOutputBoundary.outputData.getEventDistances(); + assertEquals(2, distances.size()); + assertTrue(distances.containsKey("1")); + assertTrue(distances.containsKey("2")); + assertTrue(distances.get("1") >= 0); + assertTrue(distances.get("2") >= 0); + } + + @Nested + class InputDataTests { + @Test + void testConstructorAndGetters() { + Location location = new Location(43.6532, -79.3832); + double radius = 10.0; + EventCategory category = EventCategory.MUSIC; + + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + location, radius, category + ); + + assertEquals(location, inputData.getUserLocation()); + assertEquals(radius, inputData.getRadiusKm()); + assertEquals(category, inputData.getFilterCategory()); + } + + @Test + void testIsValidWithValidData() { + Location location = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + location, 10.0, null + ); + + assertTrue(inputData.isValid()); + } + + @Test + void testIsValidWithNullLocation() { + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + null, 10.0, null + ); + + assertFalse(inputData.isValid()); + } + + @Test + void testIsValidWithNegativeRadius() { + Location location = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + location, -5.0, null + ); + + assertFalse(inputData.isValid()); + } + + @Test + void testIsValidWithZeroRadius() { + Location location = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + location, 0.0, null + ); + + assertFalse(inputData.isValid()); + } + + @Test + void testHasCategoryFilterWithCategory() { + Location location = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + location, 10.0, EventCategory.MUSIC + ); + + assertTrue(inputData.hasCategoryFilter()); + } + + @Test + void testHasCategoryFilterWithoutCategory() { + Location location = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + location, 10.0, null + ); + + assertFalse(inputData.hasCategoryFilter()); + } + + @Test + void testToString() { + Location location = new Location(43.6532, -79.3832); + DisplayLocalEventsInputData inputData = new DisplayLocalEventsInputData( + location, 10.0, EventCategory.MUSIC + ); + + String result = inputData.toString(); + assertNotNull(result); + assertTrue(result.contains("DisplayLocalEventsInputData")); + } + + @Test + void testEqualsAndHashCode() { + Location location1 = new Location(43.6532, -79.3832); + Location location2 = new Location(43.6532, -79.3832); + + DisplayLocalEventsInputData inputData1 = new DisplayLocalEventsInputData( + location1, 10.0, EventCategory.MUSIC + ); + DisplayLocalEventsInputData inputData2 = new DisplayLocalEventsInputData( + location2, 10.0, EventCategory.MUSIC + ); + DisplayLocalEventsInputData inputData3 = new DisplayLocalEventsInputData( + location1, 15.0, EventCategory.MUSIC + ); + + assertEquals(inputData1, inputData2); + assertNotEquals(inputData1, inputData3); + assertEquals(inputData1, inputData1); + assertNotEquals(inputData1, null); + assertNotEquals(inputData1, "string"); + + assertEquals(inputData1.hashCode(), inputData2.hashCode()); + } + + @Test + void testEqualsWithDifferentCategory() { + Location location = new Location(43.6532, -79.3832); + + DisplayLocalEventsInputData inputData1 = new DisplayLocalEventsInputData( + location, 10.0, EventCategory.MUSIC + ); + DisplayLocalEventsInputData inputData2 = new DisplayLocalEventsInputData( + location, 10.0, EventCategory.SPORTS + ); + DisplayLocalEventsInputData inputData3 = new DisplayLocalEventsInputData( + location, 10.0, null + ); + + assertNotEquals(inputData1, inputData2); + assertNotEquals(inputData1, inputData3); + } + + @Test + void testEqualsWithBothNullCategories() { + Location location = new Location(43.6532, -79.3832); + + DisplayLocalEventsInputData inputData1 = new DisplayLocalEventsInputData( + location, 10.0, null + ); + DisplayLocalEventsInputData inputData2 = new DisplayLocalEventsInputData( + location, 10.0, null + ); + + assertEquals(inputData1, inputData2); + assertEquals(inputData1.hashCode(), inputData2.hashCode()); + } + + @Test + void testEqualsWithOneNullCategory() { + Location location = new Location(43.6532, -79.3832); + + DisplayLocalEventsInputData inputData1 = new DisplayLocalEventsInputData( + location, 10.0, null + ); + DisplayLocalEventsInputData inputData2 = new DisplayLocalEventsInputData( + location, 10.0, EventCategory.MUSIC + ); + + assertNotEquals(inputData1, inputData2); + } + + @Test + void testEqualsWithDifferentLocations() { + Location location1 = new Location(43.6532, -79.3832); + Location location2 = new Location(43.7000, -79.4000); + + DisplayLocalEventsInputData inputData1 = new DisplayLocalEventsInputData( + location1, 10.0, null + ); + DisplayLocalEventsInputData inputData2 = new DisplayLocalEventsInputData( + location2, 10.0, null + ); + + assertNotEquals(inputData1, inputData2); + } + } + + @Nested + class OutputDataTests { + @Test + void testConstructorWithTwoParameters() { + List events = createTestEventList(); + String message = "Found 2 events"; + + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + events, message + ); + + assertEquals(events, outputData.getEvents()); + assertEquals(message, outputData.getMessage()); + assertEquals(2, outputData.getTotalEvents()); + assertNotNull(outputData.getEventDistances()); + } + + @Test + void testConstructorWithThreeParameters() { + List events = createTestEventList(); + String message = "Found 2 events"; + Map distances = new HashMap<>(); + distances.put("1", 5.0); + distances.put("2", 10.0); + + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + events, message, distances + ); + + assertEquals(events, outputData.getEvents()); + assertEquals(message, outputData.getMessage()); + assertEquals(2, outputData.getTotalEvents()); + assertEquals(distances, outputData.getEventDistances()); + } + + @Test + void testConstructorWithNullEvents() { + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + null, "message" + ); + + assertNotNull(outputData.getEvents()); + assertEquals(0, outputData.getEvents().size()); + assertEquals(0, outputData.getTotalEvents()); + } + + @Test + void testConstructorWithNullMessage() { + List events = createTestEventList(); + + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + events, null + ); + + assertEquals("", outputData.getMessage()); + } + + @Test + void testConstructorWithNullDistances() { + List events = createTestEventList(); + + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + events, "message", null + ); + + assertNotNull(outputData.getEventDistances()); + assertEquals(0, outputData.getEventDistances().size()); + } + + @Test + void testConstructorWithAllNullParameters() { + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + null, null, null + ); + + assertNotNull(outputData.getEvents()); + assertEquals(0, outputData.getEvents().size()); + assertEquals("", outputData.getMessage()); + assertNotNull(outputData.getEventDistances()); + assertEquals(0, outputData.getEventDistances().size()); + } + + @Test + void testGetDistanceForEvent() { + List events = createTestEventList(); + Map distances = new HashMap<>(); + distances.put("1", 5.0); + distances.put("2", 10.0); + + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + events, "message", distances + ); + + assertEquals(5.0, outputData.getDistanceForEvent("1")); + assertEquals(10.0, outputData.getDistanceForEvent("2")); + assertEquals(-1.0, outputData.getDistanceForEvent("999")); + } + + @Test + void testGetTotalEvents() { + List events = createTestEventList(); + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + events, "message" + ); + + assertEquals(2, outputData.getTotalEvents()); + } + + @Test + void testHasEvents() { + List events = createTestEventList(); + DisplayLocalEventsOutputData outputData1 = new DisplayLocalEventsOutputData( + events, "message" + ); + DisplayLocalEventsOutputData outputData2 = new DisplayLocalEventsOutputData( + new ArrayList<>(), "message" + ); + + assertTrue(outputData1.hasEvents()); + assertFalse(outputData2.hasEvents()); + } + + @Test + void testHasDistanceData() { + Map distances = new HashMap<>(); + distances.put("1", 5.0); + + DisplayLocalEventsOutputData outputData1 = new DisplayLocalEventsOutputData( + createTestEventList(), "message", distances + ); + DisplayLocalEventsOutputData outputData2 = new DisplayLocalEventsOutputData( + createTestEventList(), "message", new HashMap<>() + ); + + assertTrue(outputData1.hasDistanceData()); + assertFalse(outputData2.hasDistanceData()); + } + + @Test + void testToString() { + List events = createTestEventList(); + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + events, "Test message" + ); + + String result = outputData.toString(); + assertNotNull(result); + assertTrue(result.contains("DisplayLocalEventsOutputData")); + assertTrue(result.contains("2")); + assertTrue(result.contains("Test message")); + } + + @Test + void testToStringWithDistances() { + Map distances = new HashMap<>(); + distances.put("1", 5.0); + + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + createTestEventList(), "Test message", distances + ); + + String result = outputData.toString(); + assertTrue(result.contains("true")); + } + + @Test + void testToStringWithoutDistances() { + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + createTestEventList(), "Test message", new HashMap<>() + ); + + String result = outputData.toString(); + assertTrue(result.contains("false")); + } + + @Test + void testEqualsAndHashCode() { + List events1 = createTestEventList(); + List events2 = createTestEventList(); + List events3 = new ArrayList<>(); + + DisplayLocalEventsOutputData outputData1 = new DisplayLocalEventsOutputData( + events1, "message" + ); + DisplayLocalEventsOutputData outputData2 = new DisplayLocalEventsOutputData( + events2, "message" + ); + DisplayLocalEventsOutputData outputData3 = new DisplayLocalEventsOutputData( + events3, "message" + ); + DisplayLocalEventsOutputData outputData4 = new DisplayLocalEventsOutputData( + events1, "different message" + ); + + assertEquals(outputData1, outputData2); + assertNotEquals(outputData1, outputData3); + assertNotEquals(outputData1, outputData4); + assertEquals(outputData1, outputData1); + assertNotEquals(outputData1, null); + assertNotEquals(outputData1, "string"); + + assertEquals(outputData1.hashCode(), outputData2.hashCode()); + } + + @Test + void testEqualsWithDifferentTotalEvents() { + List events1 = createTestEventList(); + List events2 = new ArrayList<>(); + events2.add(createTestEvent("1", "Event", 43.6532, -79.3832, EventCategory.MUSIC)); + + DisplayLocalEventsOutputData outputData1 = new DisplayLocalEventsOutputData( + events1, "message" + ); + DisplayLocalEventsOutputData outputData2 = new DisplayLocalEventsOutputData( + events2, "message" + ); + + assertNotEquals(outputData1, outputData2); + } + + @Test + void testEqualsWithDifferentEvents() { + List events1 = createTestEventList(); + List events2 = new ArrayList<>(); + events2.add(createTestEvent("3", "Different Event", 43.6532, -79.3832, EventCategory.FILM)); + + DisplayLocalEventsOutputData outputData1 = new DisplayLocalEventsOutputData( + events1, "message" + ); + DisplayLocalEventsOutputData outputData2 = new DisplayLocalEventsOutputData( + events2, "message" + ); + + assertNotEquals(outputData1, outputData2); + } + + @Test + void testHashCodeConsistency() { + List events = createTestEventList(); + DisplayLocalEventsOutputData outputData = new DisplayLocalEventsOutputData( + events, "message" + ); + + int hash1 = outputData.hashCode(); + int hash2 = outputData.hashCode(); + + assertEquals(hash1, hash2); + } + } + + private Event createTestEvent(String id, String name, double lat, double lon, + EventCategory category) { + return new Event( + id, + name, + "Description for " + name, + "123 Test St", + category, + new Location(lat, lon), + LocalDateTime.now().plusDays(1), + "https://example.com/image.jpg" + ); + } + + private List createTestEventList() { + List events = new ArrayList<>(); + events.add(new Event( + "1", + "Event 1", + "Description", + "Address 1", + EventCategory.MUSIC, + new Location(43.6532, -79.3832), + LocalDateTime.now().plusDays(1), + "image1.jpg" + )); + events.add(new Event( + "2", + "Event 2", + "Description", + "Address 2", + EventCategory.SPORTS, + new Location(43.6600, -79.3900), + LocalDateTime.now().plusDays(2), + "image2.jpg" + )); + return events; + } + + private static class TestEventRepository implements EventRepository { + private List events = new ArrayList<>(); + private boolean shouldThrowException = false; + + public void addEvent(Event event) { + events.add(event); + } + + public void setShouldThrowException(boolean value) { + this.shouldThrowException = value; + } + + @Override + public List findAllEvents() { + if (shouldThrowException) { + throw new RuntimeException("Test exception"); + } + return new ArrayList<>(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()); + } + } + + private static class TestTicketmasterAdapter extends TicketmasterEventRepositoryAdapter { + private List events = new ArrayList<>(); + boolean findEventsCalled = false; + + public TestTicketmasterAdapter() { + super(null, new Location(43.6532, -79.3832), 10.0); + } + + public void addEvent(Event event) { + events.add(event); + } + + @Override + public List findEvents(Location center, double radiusKm) { + findEventsCalled = true; + return new ArrayList<>(events); + } + + @Override + public List findAllEvents() { + return new ArrayList<>(events); + } + + @Override + public Optional findById(String id) { + return events.stream() + .filter(e -> e.getId().equals(id)) + .findFirst(); + } + + @Override + public List findByName(String query) { + return new ArrayList<>(); + } + } + + private static class TestOutputBoundary implements DisplayLocalEventsOutputBoundary { + boolean isSuccessCalled = false; + boolean isErrorCalled = false; + boolean isSwitchToSaveViewCalled = false; + DisplayLocalEventsOutputData outputData; + String errorMessage; + + @Override + public void presentSuccess(DisplayLocalEventsOutputData outputData) { + this.isSuccessCalled = true; + this.outputData = outputData; + } + + @Override + public void presentError(String errorMessage) { + this.isErrorCalled = true; + this.errorMessage = errorMessage; + } + + @Override + public void switchToSaveView() { + this.isSwitchToSaveViewCalled = true; + } + } +} 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..114f5e59 --- /dev/null +++ b/src/test/java/use_case/event_description/EventDescriptionInteractorTest.java @@ -0,0 +1,186 @@ +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); + } + + // ============ Additional tests for 100% coverage ============ + + @Test + void testEventDescriptionInputData_allGetters() { + // Test all getters in EventDescriptionInputData + EventDescriptionInputData inputData = + new EventDescriptionInputData("event-123", 45.5, -75.5); + + assertEquals("event-123", inputData.getEventId()); + assertEquals(45.5, inputData.getUserLatitude(), 0.001); + assertEquals(-75.5, inputData.getUserLongitude(), 0.001); + } + + @Test + void testEventDescriptionOutputData_allGetters() { + // Test all getters in EventDescriptionOutputData + EventDescriptionOutputData outputData = new EventDescriptionOutputData( + "Concert Name", + "A great concert", + "456 Main St", + "MUSIC", + "2025-12-25 20:00", + 15.5 + ); + + assertEquals("Concert Name", outputData.getName()); + assertEquals("A great concert", outputData.getDescription()); + assertEquals("456 Main St", outputData.getAddress()); + assertEquals("MUSIC", outputData.getCategory()); + assertEquals("2025-12-25 20:00", outputData.getDateTime()); + assertEquals(15.5, outputData.getDistanceKm(), 0.001); + } + + @Test + void testHaversineDistanceCalculator_sameLocation() { + // Test distance calculator with same coordinates (should be 0) + HaversineDistanceCalculator calculator = new HaversineDistanceCalculator(); + + double distance = calculator.distanceKm(43.0, -79.0, 43.0, -79.0); + + assertEquals(0.0, distance, 0.001); + } + + @Test + void testHaversineDistanceCalculator_knownDistance() { + // Test distance calculator with known coordinates + // Toronto to Montreal is approximately 500 km + HaversineDistanceCalculator calculator = new HaversineDistanceCalculator(); + + // Toronto: 43.6532, -79.3832 + // Montreal: 45.5017, -73.5673 + double distance = calculator.distanceKm(43.6532, -79.3832, 45.5017, -73.5673); + + // Should be approximately 500-550 km + assertTrue(distance > 400 && distance < 600, + "Distance between Toronto and Montreal should be ~500km, was: " + distance); + } + + @Test + void testHaversineDistanceCalculator_antipodal() { + // Test with very far apart locations + HaversineDistanceCalculator calculator = new HaversineDistanceCalculator(); + + // North pole to South pole (approximately 20,000 km) + double distance = calculator.distanceKm(90.0, 0.0, -90.0, 0.0); + + assertTrue(distance > 19000 && distance < 21000, + "Pole to pole distance should be ~20,000km, was: " + distance); + } + + @Test + void successPath_verifyAllOutputFields() { + // Test that all output fields are correctly populated + InMemoryEventDataAccessObject eventDAO = new InMemoryEventDataAccessObject(); + Location loc = new Location("789 Test Ave, Toronto", 43.65, -79.38); + Event event = new Event( + "test-id", + "Test Event Name", + "Test Description Here", + "789 Test Ave, Toronto", + loc, + "Sports", + "2025-06-15T14:30" + ); + eventDAO.save(event); + + TestPresenter presenter = new TestPresenter(); + DistanceCalculator distanceCalculator = new HaversineDistanceCalculator(); + + EventDescriptionInteractor interactor = + new EventDescriptionInteractor(eventDAO, presenter, distanceCalculator); + + EventDescriptionInputData input = + new EventDescriptionInputData("test-id", 43.65, -79.38); + + interactor.execute(input); + + assertNotNull(presenter.lastOutput); + assertEquals("Test Event Name", presenter.lastOutput.getName()); + assertEquals("Test Description Here", presenter.lastOutput.getDescription()); + assertEquals("789 Test Ave, Toronto", presenter.lastOutput.getAddress()); + assertEquals("Sports", presenter.lastOutput.getCategory()); + assertEquals("2025-06-15 14:30", presenter.lastOutput.getDateTime()); + // Distance should be 0 or very small since user is at event location + assertTrue(presenter.lastOutput.getDistanceKm() < 1.0); + } +} \ No newline at end of file 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..7cc3105d --- /dev/null +++ b/src/test/java/use_case/save_event/SaveEventInteractorTest.java @@ -0,0 +1,344 @@ +//Assisted by AI +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.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class for SaveEventInteractor. + * Tests the save event use case functionality including: + * - Successful event saving + * - Duplicate event detection + * - Null event handling + * - Event removal + * - Checking if event is saved + */ +class SaveEventInteractorTest { + + private SaveEventInteractor interactor; + private TestSaveEventPresenter testPresenter; + private TestSavedEventsDAO testDAO; + private TestUserDataAccess testUserDataAccess; + private Event testEvent; + + @BeforeEach + void setUp() { + // Create test event + Location location = new Location("Test Venue, Toronto", 43.6532, -79.3832); + testEvent = new Event( + "test-123", + "Test Concert", + "A great test concert", + "Test Venue, Toronto", + EventCategory.MUSIC, + location, + LocalDateTime.of(2025, 12, 15, 19, 0), + "http://example.com/image.jpg" + ); + + testPresenter = new TestSaveEventPresenter(); + testDAO = new TestSavedEventsDAO(); + testUserDataAccess = new TestUserDataAccess(); + + interactor = new SaveEventInteractor(testPresenter, testDAO, testUserDataAccess); + } + + @Test + void testExecute_successfulSave() { + testUserDataAccess.setCurrentUsername("testuser"); + SaveEventInputData inputData = new SaveEventInputData(testEvent); + + interactor.execute(inputData); + + assertTrue(testPresenter.isSuccessCalled(), "Success view should be called"); + assertFalse(testPresenter.isFailureCalled(), "Failure view should not be called"); + assertEquals(testEvent, testPresenter.getOutputData().getEvent(), "Output should contain the saved event"); + assertEquals(1, testDAO.getSavedEventCount("testuser"), "Event should be saved once"); + assertTrue(testDAO.isEventSaved("testuser", testEvent.getId()), "Event should be marked as saved"); + } + + @Test + void testExecute_nullEvent() { + // Arrange + testUserDataAccess.setCurrentUsername("testuser"); + SaveEventInputData inputData = new SaveEventInputData(null); + + // Act + interactor.execute(inputData); + + // Assert + assertTrue(testPresenter.isFailureCalled(), "Failure view should be called"); + assertFalse(testPresenter.isSuccessCalled(), "Success view should not be called"); + assertEquals("No Event Found", testPresenter.getErrorMessage(), "Error message should indicate no event"); + assertEquals(0, testDAO.getSavedEventCount("testuser"), "No events should be saved"); + } + + @Test + void testExecute_duplicateEvent() { + testUserDataAccess.setCurrentUsername("testuser"); + SaveEventInputData inputData = new SaveEventInputData(testEvent); + + interactor.execute(inputData); + testPresenter.reset(); + + interactor.execute(inputData); + + assertTrue(testPresenter.isFailureCalled(), "Failure view should be called for duplicate"); + assertFalse(testPresenter.isSuccessCalled(), "Success view should not be called for duplicate"); + assertEquals("Event already saved", testPresenter.getErrorMessage(), + "Error message should indicate duplicate"); + assertEquals(1, testDAO.getSavedEventCount("testuser"), + "Event should still only be saved once"); + } + + @Test + void testGetSavedEvents() { + testUserDataAccess.setCurrentUsername("testuser"); + SaveEventInputData inputData = new SaveEventInputData(testEvent); + interactor.execute(inputData); + + List savedEvents = interactor.getSavedEvents(); + + assertNotNull(savedEvents, "Saved events list should not be null"); + assertEquals(1, savedEvents.size(), "Should have one saved event"); + assertEquals(testEvent.getId(), savedEvents.get(0).getId(), "Saved event should match test event"); + } + + @Test + void testGetSavedEvents_noUser() { + testUserDataAccess.setCurrentUsername(null); + + List savedEvents = interactor.getSavedEvents(); + + assertNotNull(savedEvents, "Saved events list should not be null"); + assertEquals(0, savedEvents.size(), "Should have no saved events when no user"); + } + + @Test + void testUnsaveEvent() { + testUserDataAccess.setCurrentUsername("testuser"); + SaveEventInputData inputData = new SaveEventInputData(testEvent); + interactor.execute(inputData); + + interactor.unsaveEvent(testEvent); + + assertEquals(0, testDAO.getSavedEventCount("testuser"), "Event should be removed"); + assertFalse(testDAO.isEventSaved("testuser", testEvent.getId()), "Event should not be marked as saved"); + } + + @Test + void testUnsaveEvent_noUser() { + testUserDataAccess.setCurrentUsername("testuser"); + SaveEventInputData inputData = new SaveEventInputData(testEvent); + interactor.execute(inputData); + + testUserDataAccess.setCurrentUsername(null); + + interactor.unsaveEvent(testEvent); + + assertEquals(1, testDAO.getSavedEventCount("testuser"), "Event should not be removed without user"); + } + + @Test + void testIsEventSaved() { + testUserDataAccess.setCurrentUsername("testuser"); + SaveEventInputData inputData = new SaveEventInputData(testEvent); + interactor.execute(inputData); + + assertTrue(interactor.isEventSaved(testEvent.getId()), "Event should be marked as saved"); + assertFalse(interactor.isEventSaved("nonexistent-id"), "Non-existent event should not be marked as saved"); + } + + @Test + void testIsEventSaved_nullEventId() { + testUserDataAccess.setCurrentUsername("testuser"); + + assertFalse(interactor.isEventSaved(null), "Null event ID should return false"); + } + + @Test + void testIsEventSaved_noUser() { + testUserDataAccess.setCurrentUsername(null); + + assertFalse(interactor.isEventSaved(testEvent.getId()), "Should return false when no user"); + } + + @Test + void testSwitchToDashboardView() { + interactor.switchToDashboardView(); + + assertTrue(testPresenter.isSwitchToDashboardCalled(), "Switch to dashboard should be called"); + } + + @Test + void testRemoveEvent() { + testUserDataAccess.setCurrentUsername("testuser"); + SaveEventInputData inputData = new SaveEventInputData(testEvent); + interactor.execute(inputData); + + interactor.removeEvent(testEvent); + + assertEquals(0, testDAO.getSavedEventCount("testuser"), "Event should be removed"); + assertFalse(testDAO.isEventSaved("testuser", testEvent.getId()), "Event should not be marked as saved"); + } + + @Test + void testRemoveEvent_noUser() { + testUserDataAccess.setCurrentUsername("testuser"); + SaveEventInputData inputData = new SaveEventInputData(testEvent); + interactor.execute(inputData); + + testUserDataAccess.setCurrentUsername(null); + + interactor.removeEvent(testEvent); + + assertEquals(1, testDAO.getSavedEventCount("testuser"), "Event should not be removed without user"); + } + + @Test + void testIsEventSaved_nullUsername() { + testUserDataAccess.setCurrentUsername(null); + + assertFalse(interactor.isEventSaved("some-event-id"), "Should return false when username is null"); + } + + + /** + * Test implementation of SaveEventOutputBoundary. + */ + private static class TestSaveEventPresenter implements SaveEventOutputBoundary { + private boolean successCalled = false; + private boolean failureCalled = false; + private boolean switchToDashboardCalled = false; + private SaveEventOutputData outputData; + private String errorMessage; + + @Override + public void prepareSuccessView(SaveEventOutputData outputData) { + this.successCalled = true; + this.outputData = outputData; + } + + @Override + public void prepareFailureView(String errorMessage) { + this.failureCalled = true; + this.errorMessage = errorMessage; + } + + @Override + public void switchToDashboardView() { + this.switchToDashboardCalled = true; + } + + public boolean isSuccessCalled() { + return successCalled; + } + + public boolean isFailureCalled() { + return failureCalled; + } + + public boolean isSwitchToDashboardCalled() { + return switchToDashboardCalled; + } + + public SaveEventOutputData getOutputData() { + return outputData; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void reset() { + successCalled = false; + failureCalled = false; + switchToDashboardCalled = false; + outputData = null; + errorMessage = null; + } + } + + /** + * Test implementation of FileSavedEventsDataAccessObject. + */ + private static class TestSavedEventsDAO extends FileSavedEventsDataAccessObject { + private final List savedEvents = new ArrayList<>(); + private String currentUser = null; + + @Override + public void saveEvent(String username, Event event) { + currentUser = username; + if (!isEventSaved(username, event.getId())) { + savedEvents.add(event); + } + } + + @Override + public List getSavedEvents(String username) { + if (username == null) { + return new ArrayList<>(); + } + return new ArrayList<>(savedEvents); + } + + @Override + public void unsaveEvent(String username, Event event) { + savedEvents.removeIf(e -> e.getId().equals(event.getId())); + } + + @Override + public boolean isEventSaved(String username, String eventId) { + if (username == null || eventId == null) { + return false; + } + return savedEvents.stream().anyMatch(e -> e.getId().equals(eventId)); + } + + public int getSavedEventCount(String username) { + return getSavedEvents(username).size(); + } + } + + /** + * Test implementation of LoginUserDataAccessInterface. + */ + private static class TestUserDataAccess implements LoginUserDataAccessInterface { + private String currentUsername; + + @Override + public boolean existsByName(String username) { + return true; + } + + @Override + public void save(entity.User user) { + } + + @Override + public entity.User get(String username) { + return null; + } + + @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/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 { +}