diff --git a/Gemfile b/Gemfile index 5efe339e6..5c08ca257 100644 --- a/Gemfile +++ b/Gemfile @@ -37,7 +37,7 @@ gem "redis", "~> 4.0" # gem "kredis" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] -# gem "bcrypt", "~> 3.1.7" +gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] @@ -76,6 +76,7 @@ end # Developer Added Gems gem 'dotenv-rails' +gem 'stripe' # Suggested Potentially Useful Gems # gem 'devise' diff --git a/Gemfile.lock b/Gemfile.lock index c3e104938..e16cc931c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,6 +68,7 @@ GEM tzinfo (~> 2.0) addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) + bcrypt (3.1.20) bindex (0.8.1) bootsnap (1.13.0) msgpack (~> 1.2) @@ -132,9 +133,11 @@ GEM digest net-protocol timeout - nio4r (2.5.8) + nio4r (2.7.4) nokogiri (1.13.8-arm64-darwin) racc (~> 1.4) + nokogiri (1.13.8-x86_64-darwin) + racc (~> 1.4) nokogiri (1.13.8-x86_64-linux) racc (~> 1.4) public_suffix (5.0.0) @@ -191,9 +194,12 @@ GEM sprockets (>= 3.0.0) stimulus-rails (1.1.0) railties (>= 6.0.0) + stripe (13.2.0) strscan (3.0.4) tailwindcss-rails (2.0.12-arm64-darwin) railties (>= 6.0.0) + tailwindcss-rails (2.0.12-x86_64-darwin) + railties (>= 6.0.0) tailwindcss-rails (2.0.12-x86_64-linux) railties (>= 6.0.0) thor (1.2.1) @@ -223,9 +229,12 @@ GEM PLATFORMS arm64-darwin-20 + arm64-darwin-23 + x86_64-darwin-23 x86_64-linux DEPENDENCIES + bcrypt (~> 3.1.7) bootsnap capybara debug @@ -239,6 +248,7 @@ DEPENDENCIES selenium-webdriver sprockets-rails stimulus-rails + stripe tailwindcss-rails turbo-rails tzinfo-data diff --git a/README.md b/README.md index 85f6c86ae..9e815feb4 100644 --- a/README.md +++ b/README.md @@ -1,157 +1,105 @@ -# ValetBike - -Smith College CSC223: Software Engineering\ -Starter App for ValetBike project - -## Environment Configuration - -As you configure your environment you should **keep a log** where you **write down all the steps you take** including **each command you type**. You will inevitably run into errors setting up your development environment and maintaining a meticulous log will allow others to help you troubleshoot. Ignore this recommendation at your own peril, but don't say you haven't been warned :] - -Installing Ruby on Rails is not a trivial process. It is the essential first step to developing ValetBike, and it will take you far longer than all the other steps to complete. Be sure to set aside ample time to work through the setup. - -### 0. Remember that versions matter -ValetBike runs on Ruby 3.1.2 and Rails 7.0.3.1. It is essential that you configure your environment to use these precise versions of the language and framework. - -### 1. Install Ruby on Rails with MySQL - -On Mac it is strongly recommended that you use asdf to install Ruby. On Windows you should set up your environment through the Windows Subsystem for Linux (WSL). The guides below explain how to do so in detail: - -- [Mac Ruby on Rails Developer Environment Setup](https://github.com/deadroxy/valetbike/blob/master/notes/mac-setup.md) -- [Windows Ruby on Rails Developer Environment Setup](https://github.com/deadroxy/valetbike/blob/master/notes/windows-setup.md) - -Be sure to complete all the installation procedures in the relevant guide before continuing on to the next step. - -### 2. Fork & clone the ValetBike repo - -- Click fork in the upper right hand corner of the ValetBike GitHub page -- This creates a copy of the repository on your personal GitHub account -- To access this code on your development machine, create a local copy of your fork with:\ - `git clone https://github.com//valetbike.git` -- Note: you should run that command when you are in the folder where you want to store the repo\ - (e.g. `/Users//Development`) - -### 3. Prepare the application - -- Enter the directory you just created: `cd valetbike` -- Add `.tool-versions` to app directory to ensure the right ruby is always used: `asdf local ruby 3.1.2` -- Install required gems with: `bundle install` - -### 4. Configure the database environment variables - -- Add a file called `.env` to the valetbike root directory -- Ensure that it includes the credentials you setup when installing MySQL: - -```shell -MYSQL_USERNAME=root -MYSQL_PASSWORD=YOURPASSWORD -MYSQL_SOCKET=/tmp/mysql.sock # For Mac -MYSQL_SOCKET=/var/run/mysqld/mysqld.sock # For Windows -``` - -### 5. Prepare the database in MySQL - -- Use rails to create both the development and test databases with:\ - `rake db:create` -- Or use mysql to just create the development databse with:\ - `mysql -u root -p`\ - `CREATE DATABASE valetbike_development;`\ - `exit` -- Then run the database migrations with:\ - `rake db:migrate` - -### 6. Confirm that the app runs - -* Launch the web server using `rackup` or `rails s` (short for `rails server`) or `bin/dev` -* If using `rackup` open http://localhost:9292 (or http://127.0.0.1:9292) in a browser -* If using `rails s` or `bin/dev` open http://localhost:3000 (or http://127.0.0.1:3000) in a browser -* You should see ValetBike welcome page - - -## Assignment #1: Hello Stack, Welcome to ValetBike! - -### Brief Background -You and several other junior engineers have just started at ValetBike, a community tech co-op based in Nipmuc Notch, and you are excited to finally be getting paid to contribute to a meaningful app. During your hiring interview, you said you were comfortable doing full stack programming, but stressed you hadn't worked in Ruby on Rails before. The lead developer thought you were right for the position and promised you a guided tour of the codebase on your first day. However, after your onboarding, you learn they've just gone on leave indefinitely. It also turns out the lead developer was the *only* developer at ValetBike, and now the rest of the team is counting on you and the other new programmers to complete the prototype before the scheduled launch day. - -As a fearless software engineer you agree to onboard yourself and attempt to continue the build. You didn't get much information about the architecture or design of the product during your interview. All you remember is that the lead developer had been working with the [ValleyBike](https://valleybike.org) team to iterate on their system which launched in 2018 and that they were using GitHub to collaborate on their codebase. - -### Assignment Goal -Your primary objective is to get your development environment configured so that the existing app will run on your machine. To exceed expectations, you must add at least one feature to the current code. To distinguish yourself, you should add two or more features. See submission guidelines below for complete details. - -### How to Begin -* Create a GitHub account if you don't have one -* Go to [https://github.com/deadroxy/valetbike](https://github.com/deadroxy/valetbike) -* Follow the README instructions to configure your environment - -### Teamwork Guidelines -You may work in teams of up to five people to get your environments set up and to modify the code. But you must each submit your own unique environment screenshots via Moodle. If you choose to fork the repo to add features, you can collaborate on the code, but you must each create and submit a record of a unique pull request. - -### Ruby on Rails Guides -You will probably be unfamiliar with the main components of the ValetBike stack like the language (Ruby), the framework (Rails), and the database (MySQL). Luckily the lead developer left links to their favorite books and tutorials for you below. Consult them regularly as you get your bearings in the new environment. - -* [Getting Started with Rails](https://guides.rubyonrails.org/getting_started.html) -* [I Love Ruby](https://i-love-ruby.gitlab.io/) -* [The Bastards Book of Ruby](http://ruby.bastardsbook.com/) -* [Why's (Poignant) Guide to Ruby](https://poignant.guide/) - -### Exploration Tips -* Review the files the lead developer left in the `notes/` folder -* Pay special attention to the environment setup and [app creation](https://github.com/deadroxy/valetbike/blob/master/notes/app-creation.md) guides -* Use GitHub to dive into previous commits to see what they built so far -* Use `rails console` to experiment with creating & associating records (objects) from the command line - - `s = Station.new(name: "Neu Station", address: "123 Novel Lane", identifier: "45")` - - `s.save` - - `b = Bike.new(identifier: "1234")` - - `b.current_station = s` - - `b.save` - - `s = Station.new(name: "Ye Olde Statione", address: "101 Historic Way", identifier: "99")` - - `s.save` - - `b = Bike.new(identifier: "5678")` - - `b.save` - - `s` - - `Station.last` - - `Station.first` - - `s = Station.first` - - `s` - - `s.id` - - `s.identifier` - - `s.docked_bikes` - - `s.docked_bikes.count` - - `Station.last.docked_bikes.count` - - `Bike.first.current_station` - - `Bike.last.current_station` - - `Bike.find_by(identifier: "1234").update(current_station: Station.last)` - - `Station.last.docked_bikes` - - `Station.first.docked_bikes` - -### Submission Guidelines to Meet Expectations -1. Get ValetBike running on your development machine -2. Modify the welcome message -3. Take a screenshot showing your change works (include browser, console, date/time) -4. Name the screenshot "youremail-a1-ss.png" or *.jpg (for me it would be "jbrewer-a1-ss.png") -5. Create a text file called "youremail-a1-team.txt" (for me it would be "jbrewer-a1-team.txt") -6. List the names of everyone you worked with on this assignment, including your own (for me it would be "Johanna Brewer") -7. Submit your screenshot and team list via Moodle - -### Submission Guidelines to Exceed Expectations or Distinguish yourself -1. Complete all of the Meets Expectations tasks -2. Implement one (Exceeds) or more (Distinguished) of the features below - - Show number of docked bikes at each station - - Create rake task to import station & bike data from csv files - - Allow user to view list of bikes - - Allow user to switch between station and bike list views - - Allow user to reverse sort order of stations or bikes in list view -3. Commit and push your changes to your fork on GitHub -4. Create a pull request from your modified fork to the main ValetBike repo -5. Create a file called "youremail-a1-pr.txt" (for me it would be "jbrewer-a1-pr.txt") -6. Include a complete link to your pull request as the first line of this file (e.g. "https://github.com/deadroxy/valetbike/pull/1234") -7. Submit your pull request file along with your screenshot and team list via Moodle - -## License -Copyright 2021-2022, Johanna Brewer - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses. - +# VectorBike + +VectorBike is now live! Visit the site on [Heroku](https://vectorbike-ecfe326f48ab.herokuapp.com/). + +## Developed By +* Molly Neu +* Molly Daniel +* Skye Weaver Worster '25J +* Emma Ruckle +* Sydney Weisberg + +## Development environment setup +VectorBike requires Ruby 3.1.2 and Rails 7.0.3.1. This guide uses MySQL for database functionality. Set up and start a MySQL database on a socket connection with a username and password. + +Create a `.env` file in the repository directory, add it to your `.gitignore`, and include the following: +``` +MYSQL_USERNAME= +MYSQL_PASSWORD= +MYSQL_SOCKET=.sock +``` +For access to the Stripe API, please reach out to Sydney Weisberg for the secret key. Store the key in your `.env` file as `STRIPE_SECRET_KEY`. + +Execute the following commands from your repository directory: +```rb +bundle install +rails db:reset #reconfigures the database +rails db:migrate #ensures schema is updated +rake initial_import #loads CSV data, generates pseudo-random data +rails s #launches the web server +``` +Navigate to `http://localhost:3000` in a browser, and enjoy! + +On each subsequent deployment, only `rails s` is required to launch the page. If changes to the schema, CSVs, or gems are made, perform all commands from `bundle install` onwards before launching. + +## VectorBike MVP functionality description + +### Account Management +- Users can create an account, sign in, and log out. +- On the `Profile` page, users can view and edit their information or delete their account. +- Users can add funds to their account via Stripe. +- On the `Profile` page, the user’s ride history is displayed. + +### Site Navigation +- Accessible via a top navigation bar. +- Homepage includes an overview of VectorBike and a `Rent a Bike!` button linking to the rental page. `Rent` also links to the rental page. +- `About` page provides detailed information about VectorBike and an about the developers section. +- `Profile` page displays user-specific details. +- Site features a footer with contact information. + +### Rental Process +- Users can access the rental page via `Rent a Bike!` on the homepage or `Rent` in the navigation bar. +- Stations are displayed in list or map view: + - Map View: Shows station locations with clickable icons for details and directions. Searchable dropdown allows station selection. The dropdown only shows stations with at least one available bike. + - List View: Stations sortable by name, address, or available bikes. +- Clicking a station opens a rental form to select a bike (charged > 50%) and rental duration. + +### Rental Form +- Options include `Return to Stations` to cancel or `Rent Now!` to complete the rental. +- Invalid rentals (e.g., no bike selected) do not go through, and the user is shown an informative flash alert. +- Non-logged-in users are redirected to the login page before proceeding. +- If a rental is valid, and the user has enough funds, the rental goes through. + +### Rental Management +- After a successful rental, users see a countdown timer and options to return the bike or view active rentals. +- Users can have up to 5 active rentals. +- The `View Bike Rentals` button lists active rentals, with links to each rental’s countdown page. + +### Returning Bikes +- Users select a return station from a dropdown in a pop-up. +- Clicking `Return` processes the return, while `Close` cancels the action. +- After returning a bike, users see a success screen. + +### Ride History and Metrics +- The `Profile` page lists total biking hours, and the number of rides. Ride history is also displayed. +- The top ten users with the most riding time have a badge displayed next to their name on the profile page. + +### Rental Limits +- Users can have up to five active rentals. +- With at least one active rental, users are routed to the `View Bike Rentals` page when they click `Rent` or `Rent a Bike!`. +- Users with fewer than five rentals can rent additional bikes; users at the limit see a message indicating they have reached the maximum number of rentals. +- Once all rentals are returned, users are routed to the stations list when they click `Rent` or `Rent a Bike!`. + +### Recommended walkthrough +- Navigate to the sign up page, and sign up for an account. +- Add funds to your account via the `Add Funds` button. We recommended putting at least $10.00 into your account. Use the card number `4242 4242 4242 4242` in the Stripe test API, provide a reasonable expiration date for the card, and fill all other fields with random data. +- Navigate to the rent page. +- Use the toggle at the top of the page to view the stations list and the embedded map/search function. +- Return to list view. +- Sort the list of stations by either name, address or number of available bikes by clicking on the corresponding part of the header. +- Select a station with at least one bike by clicking on its row (the number of available bikes is shown at the right end of each station’s row). +- In the rental form, select a bike and a duration. +- Press `Rent Now!`. +- Navigate away from the success page. +- Return to the rent page, observe it is the current rentals page rather than the station list. +- Click `Rent Another Bike` and repeat the rental process. +- Click `View Active Rentals` and observe that you now have two active rentals. +- Click on one of your rentals. +- Click the `Return Your Bike` button to start a bike return. +- Select the station you would like to return the bike to (we suggest Florence Bank station, the first station in the list when it refreshes). +- Click on `Rent` in the nav bar. Repeat the return steps to return your second rental. +- Click on a station you returned a bike to and confirm that it is once again listed in available bikes. +- Navigate to `Profile`. +- Observe that you now have two past rides. +- Click the `Delete Profile` button +- Confirm the deletion of your profile in the pop up. diff --git a/app/assets/images/bike_return.jpg b/app/assets/images/bike_return.jpg new file mode 100644 index 000000000..7a7304c42 Binary files /dev/null and b/app/assets/images/bike_return.jpg differ diff --git a/app/assets/images/emma.png b/app/assets/images/emma.png new file mode 100644 index 000000000..bd582ecbf Binary files /dev/null and b/app/assets/images/emma.png differ diff --git a/app/assets/images/flipped_viktor.png b/app/assets/images/flipped_viktor.png new file mode 100644 index 000000000..8b9dd2de8 Binary files /dev/null and b/app/assets/images/flipped_viktor.png differ diff --git a/app/assets/images/mollyd.png b/app/assets/images/mollyd.png new file mode 100644 index 000000000..36d79a0b8 Binary files /dev/null and b/app/assets/images/mollyd.png differ diff --git a/app/assets/images/mollyn.png b/app/assets/images/mollyn.png new file mode 100644 index 000000000..9199794d5 Binary files /dev/null and b/app/assets/images/mollyn.png differ diff --git a/app/assets/images/rating.png b/app/assets/images/rating.png new file mode 100644 index 000000000..e02fc4c00 Binary files /dev/null and b/app/assets/images/rating.png differ diff --git a/app/assets/images/skye.png b/app/assets/images/skye.png new file mode 100644 index 000000000..f88bc4748 Binary files /dev/null and b/app/assets/images/skye.png differ diff --git a/app/assets/images/syd.png b/app/assets/images/syd.png new file mode 100644 index 000000000..3189c40bd Binary files /dev/null and b/app/assets/images/syd.png differ diff --git a/app/assets/images/top_ten_badge.png b/app/assets/images/top_ten_badge.png new file mode 100644 index 000000000..29304ab01 Binary files /dev/null and b/app/assets/images/top_ten_badge.png differ diff --git a/app/assets/images/valley_bike.png b/app/assets/images/valley_bike.png new file mode 100644 index 000000000..ee18e9a77 Binary files /dev/null and b/app/assets/images/valley_bike.png differ diff --git a/app/assets/images/vector_on_a_ride.jpg b/app/assets/images/vector_on_a_ride.jpg new file mode 100644 index 000000000..2a77fe2bc Binary files /dev/null and b/app/assets/images/vector_on_a_ride.jpg differ diff --git a/app/assets/images/viktor_snail.png b/app/assets/images/viktor_snail.png new file mode 100644 index 000000000..f4688bc29 Binary files /dev/null and b/app/assets/images/viktor_snail.png differ diff --git a/app/assets/images/viktor_snail_profile.png b/app/assets/images/viktor_snail_profile.png new file mode 100644 index 000000000..08781259d Binary files /dev/null and b/app/assets/images/viktor_snail_profile.png differ diff --git a/app/assets/images/viktor_steps.jpg b/app/assets/images/viktor_steps.jpg new file mode 100644 index 000000000..60317b506 Binary files /dev/null and b/app/assets/images/viktor_steps.jpg differ diff --git a/app/assets/stylesheets/alert.css b/app/assets/stylesheets/alert.css new file mode 100644 index 000000000..e1438c4a5 --- /dev/null +++ b/app/assets/stylesheets/alert.css @@ -0,0 +1,69 @@ +#flash { + background-color: maroon; + color: white; + padding: 10px; + border-radius: 10px; + border-width: 2px; + border-color: black; + font-size: large; + width: fit-content; + align-self: center; + text-align: center; +} + +.flash:hover { + background-color: rgb(67, 0, 0); +} + +.notice:hover { + background-color: rgb(0, 79, 0); +} + +.tooltip { + position: relative; + display: inline-block; +} + +/* Tooltip text */ +.tooltip .tooltiptext { + visibility: hidden; + width: 120px; + background-color: #D4AF37; + color: #fff; + text-align: center; + padding: 5px 0; + border-radius: 6px; + top: -5px; + left: 105%; + font-size: 16px; + + /* Position the tooltip text - see examples below! */ + position: absolute; + z-index: 1; +} + +/* Show the tooltip text when you mouse over the tooltip container */ +.tooltip:hover .tooltiptext { + visibility: visible; +} + +.tooltip .tooltiptext::after { + content: " "; + position: absolute; + top: 50%; + right: 100%; + /* To the left of the tooltip */ + margin-top: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent #D4AF37 transparent transparent; +} + +.tooltip .tooltiptext { + opacity: 0; + transition: opacity 1s; +} + +.tooltip:hover .tooltiptext { + opacity: 1; +} \ No newline at end of file diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 8666d2f3c..c8d69a84a 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -2,12 +2,8 @@ @tailwind components; @tailwind utilities; -/* - @layer components { .btn-primary { - @apply py-2 px-4 bg-blue-200; + @apply py-2 px-4 var(--bg-blue-200); } } - -*/ diff --git a/app/assets/stylesheets/button.css b/app/assets/stylesheets/button.css new file mode 100644 index 000000000..51fe25b97 --- /dev/null +++ b/app/assets/stylesheets/button.css @@ -0,0 +1,62 @@ +.button, +.toggle_button, +.profile_button { + background-color: var(--sky-blue-color); + border: var(--black-color); + color: var(--white-color); + padding: 1em 2em; /*10px 20px*/ + width: 25%; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; +} + +.button.active, +.toggle_button.active, +.profile_button.active { + background-color: var(--sky-blue-color); /* Change color for active state */ +} + +.toggle_button.active { + color: var(--white-color); +} + +.toggle_button { + background-color: var(--white-color); + color: var(--black-color); + padding: 0px; + font-size: 20px; + display: block; + width: 100%; + height: 100%; +} + +.button_container { + display: flex; + align-self: center; + width: 30%; + height: 2.5rem; + border: 2.5px solid var(--black-color); + border-radius: 5px; + overflow: hidden; +} + +.button:hover { + background-color: var(--button-hover-color); +} + +.profile_button:hover { + background-color: var(--medium-gray-color); +} + +#rent-a-bike { + width: 10%; + font-size: 55%; + box-shadow: 0 4px 8px 0 var(--drop-shadow-opaque), 0 6px 20px 0 var(--drop-shadow-opaque); +} + +.profile_button { + background-color: var(--white-color); + color: var(--polynesian-blue-color); +} diff --git a/app/assets/stylesheets/dropdown.css b/app/assets/stylesheets/dropdown.css new file mode 100644 index 000000000..1de2a2afb --- /dev/null +++ b/app/assets/stylesheets/dropdown.css @@ -0,0 +1,33 @@ +/* Dropdown Content */ +.dropdown-content { + position: absolute; + background-color: var(--medium-gray-color); + max-height: 75%; + overflow: auto; + border-radius: 5px; + min-width: 15%; + min-height: 6rem; +} + +/* Links inside the dropdown */ +.dropdown-content a { + color: var(--white-color); + padding: 12px 16px; + text-decoration: none; + display: block; +} + +/* Change color of dropdown links on hover */ +.dropdown-content a:hover { + background-color: var(--sky-blue-color); +} + +/* The search field */ +#input { + position: sticky; + top: 0; + font-size: 16px; + width: 100%; + border: 2.5px solid var(--slate-gray-color); + border-radius: 5px; +} diff --git a/app/assets/stylesheets/flexbox-ultralight.css b/app/assets/stylesheets/flexbox-ultralight.css index 3cec74595..9707a78ef 100755 --- a/app/assets/stylesheets/flexbox-ultralight.css +++ b/app/assets/stylesheets/flexbox-ultralight.css @@ -7,11 +7,11 @@ /**************************************************************/ .flexbox { - display: -webkit-box; /* OLD: Safari, iOS, Android browser, older WebKit browsers. */ - display: -moz-box; /* OLD: Firefox (buggy) */ - display: -ms-flexbox; /* MID: IE 10 */ - display: -webkit-flex; /* NEW, Chrome 21+ */ - display: flex; /* NEW: Opera 12.1, Firefox 22+ */ + display: -webkit-box; /* OLD: Safari, iOS, Android browser, older WebKit browsers. */ + display: -moz-box; /* OLD: Firefox (buggy) */ + display: -ms-flexbox; /* MID: IE 10 */ + display: -webkit-flex; /* NEW, Chrome 21+ */ + display: flex; /* NEW: Opera 12.1, Firefox 22+ */ } .flexbox.horizontal { @@ -24,9 +24,9 @@ .flexbox.horizontal-reverse { -webkit-box-orient: horizontal; - -webkit-box-direction: reverse; + -webkit-box-direction: reverse; -moz-box-orient: horizontal; - -moz-box-direction: reverse; + -moz-box-direction: reverse; -ms-flex-direction: row-reverse; -webkit-flex-direction: row-reverse; flex-direction: row-reverse; @@ -42,7 +42,7 @@ .flexbox.vertical-reverse { -webkit-box-orient: vertical; - -webkit-box-direction: reverse; + -webkit-box-direction: reverse; -moz-box-orient: vertical; -moz-box-direction: reverse; -ms-flex-direction: column-reverse; @@ -120,4 +120,4 @@ -webkit-justify-content: space-around; -ms-justify-content: space-around; justify-content: space-around; -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/footer.css b/app/assets/stylesheets/footer.css new file mode 100644 index 000000000..d8280161d --- /dev/null +++ b/app/assets/stylesheets/footer.css @@ -0,0 +1,8 @@ +footer { + font-size: 20px; + margin-top: 50px; + text-align: center; + padding: 3px; + background-color: var(--polynesian-blue-color); + color: white; +} diff --git a/app/assets/stylesheets/form.css b/app/assets/stylesheets/form.css new file mode 100644 index 000000000..ab75b863a --- /dev/null +++ b/app/assets/stylesheets/form.css @@ -0,0 +1,62 @@ +.form-container { + display: flex; + flex-direction: column; + gap: 15px; + font-size: large; +} + +.field { + border-radius: 10px; + border-width: 2px; + width: 20rem; +} + +.button, +.profile_button { + font-size: medium; + border-radius: 10px; + border-width: 2px; + margin-top: 8px; + padding: 7px; + width: fit-content; + align-self: center; + cursor: pointer; +} + +.submit { + background-color: var(--sky-blue-color); +} + +.submit:hover { + background-color: var(--dark-teal-color); +} + +.return { + background-color: var(--medium-gray-color); +} + +.return:hover { + background-color: var(--slate-gray-color); +} + +.subscript { + font-size: small; + color: var(--dark-gray-color); + padding: 2px 7px; +} + +#countdown { + font-size: 20px; +} + +#dropdown { + border-radius: 10px; + border-width: 2px; + width: 100%; + font-size: 20px; +} + +.button-container { + margin-bottom: 50px; + align-self: center; +} diff --git a/app/assets/stylesheets/global.css b/app/assets/stylesheets/global.css index ff48f1abe..de7b6cff9 100644 --- a/app/assets/stylesheets/global.css +++ b/app/assets/stylesheets/global.css @@ -8,21 +8,22 @@ z-index: 1; } -html, body { +html, +body { height: 100%; } body { font-size: 12pt; - -webkit-text-size-adjust:none; - -moz-text-size-adjust:none; - -ms-text-size-adjust:none; - -webkit-text-size-adjust:100%; - -moz-text-size-adjust:100%; - -ms-text-size-adjust:100%; + -webkit-text-size-adjust: none; + -moz-text-size-adjust: none; + -ms-text-size-adjust: none; + -webkit-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; } -#site-container{ +#site-container { position: relative; width: 100%; height: 100%; diff --git a/app/assets/stylesheets/header.css b/app/assets/stylesheets/header.css new file mode 100644 index 000000000..e8a64852a --- /dev/null +++ b/app/assets/stylesheets/header.css @@ -0,0 +1,44 @@ +ul { + list-style-type: none; + margin: 0; + padding: 0; + overflow: hidden; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); + background-color: white; + position: fixed; + top: 0; + width: 100%; + height: 50px; + z-index: 10; + align-items: center; + vertical-align: middle; +} + +.header { + float: right; + display: block; + color: var(--black-color); + text-align: center; + padding: 1rem 1rem; + text-decoration: none; + border-right: 0em solid var(--medium-gray-color); + font-size: 20px; + height: 3rem; + line-height: 1rem; +} + +.header:hover { + color: var(--sky-blue-color); +} + +li:first-child { + width: 4rem; + float: left; + margin-left: 0.5rem; + width: 60px; + height: 50px; +} + +menu_icon { + height: 3rem; +} diff --git a/app/assets/stylesheets/home.css b/app/assets/stylesheets/home.css new file mode 100644 index 000000000..e2f41a6d8 --- /dev/null +++ b/app/assets/stylesheets/home.css @@ -0,0 +1,38 @@ +.column, +.centered_column { + color: var(--black-color); + display: flex; + flex-direction: column; + justify-content: space_evenly; + align-items: flex_start; + float: left; + width: 50%; + padding: 1%; /*10px*/ +} + +.title { + font-size: xx-large; +} + +h1 { + font-size: x-large; +} + +.centered_column { + align-items: center; /* centers content within centered_column */ +} + +/* Clear floats after the columns */ +.row:after { + content: ""; + display: table; + clear: both; +} + +#bike-trail { + background-position: 50% 92%; + color: black; + background-size: cover; /* Ensures the background image covers the entire area */ + background-repeat: no-repeat; + justify-self: center; +} diff --git a/app/assets/stylesheets/image.css b/app/assets/stylesheets/image.css new file mode 100644 index 000000000..6e269c549 --- /dev/null +++ b/app/assets/stylesheets/image.css @@ -0,0 +1,22 @@ +.snail_pics { + width: 25%; + height: 25%; + border-radius: 50%; + background: url(URL) no-repeat; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.8); + border: 5px solid var(--sky-blue-color); + margin: 2.5%; +} + +.snail_container { + display: flex; + justify-content: center; +} + +.rating_image { + width: 25%; +} + +.large_viktor_image { + width: 60%; +} diff --git a/app/assets/stylesheets/pages.css b/app/assets/stylesheets/pages.css index 16bd64b55..ed7d85db5 100644 --- a/app/assets/stylesheets/pages.css +++ b/app/assets/stylesheets/pages.css @@ -13,13 +13,18 @@ margin-left: 1.4em; } -.page-section .section-title { +.page-section .section-title, +.page-section .section-title-centered { font-family: var(--title-font); font-weight: 500; font-size: 2.5em; margin-bottom: 1em; } +.section-title-centered { + align-self: center; +} + .record-row { padding: 0.5em 1.25em 0.5em 1.25em; font-size: 0.85em; @@ -34,19 +39,51 @@ background: var(--dark-white-color); } +.box, +.box1 { + width: 95%; + height: 500px; + border: 4px solid var(--polynesian-blue-color); + border-radius: 5px; + background-color: var(--polynesian-blue-color); + color: var(--white-color); + padding: 10px; + margin: 0px; + overflow: auto; +} -/***********************************/ -/* Responsiveness */ -/***********************************/ - -@media (max-width:48em) { - .page-section { - margin-top: 3em; - } +.box1 { + height: 631px; +} - .page-section .section-title { - font-size: 1.9em; - margin-bottom: .6em; - } +#table_box { + padding: 0; +} +.speech-box { + width: 75%; + height: 50px; + border: 4px solid var(--medium-gray-color); + border-radius: 5px; + padding: 5px; + color: var(--polynesian-blue-color); + text-align: center; + font-size: calc(.7vw + .7vh); + position: relative; + align-self: center; } + +.speech-box:before { + content: ""; + width: 0px; + height: 0px; + position: absolute; + border-left: 10px solid var(--medium-gray-color); + border-right: 10px solid transparent; + border-top: 10px solid var(--medium-gray-color); + border-bottom: 10px solid transparent; + color: var(--polynesian-blue-color); + text-align: center; + left: 20px; + bottom: -20px; +} \ No newline at end of file diff --git a/app/assets/stylesheets/stations.css b/app/assets/stylesheets/stations.css new file mode 100644 index 000000000..5c228fe3b --- /dev/null +++ b/app/assets/stylesheets/stations.css @@ -0,0 +1,52 @@ +/* Bike Selection */ +.container { + margin: auto; + text-align: left; +} + +.return-container { + margin: auto; + text-align: center; +} + +/* Radio Button */ +.custom-radio { + display: none; +} + +.radio { + position: absolute; + /* top: 0; */ + left: 0; + height: 25px; /*25px*/ + width: 25px; /*25px*/ + background-color: var(--light-white-color); + border-radius: 50%; /*50%*/ + margin: 0; + position: absolute; + top: 50%; + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} + +.radio-button-container { + display: block; + position: relative; + padding-left: 35px; /*35px*/ + margin-bottom: 12px; /*12px*/ + cursor: pointer; + font-size: 20px; /*20px*/ + justify-content: center; + align-items: center; +} + +.radio-button-container input:checked ~ .radio { + background-color: var(--sky-blue-color); +} + +.map-container { + gap: 10px; + display: flex; + width: 100%; + justify-content: center; +} diff --git a/app/assets/stylesheets/styled-table.css b/app/assets/stylesheets/styled-table.css new file mode 100644 index 000000000..5db330c4c --- /dev/null +++ b/app/assets/stylesheets/styled-table.css @@ -0,0 +1,120 @@ +/* table for list of stations */ +.styled-table { + border-collapse: separate; + margin: 2em 0; + font-size: 1em; + min-width: 10%; + border-radius: 10px; + overflow: hidden; + border-spacing: 0; +} + +/* header row */ +.styled-table th { + padding: 0; +} + +/* header row */ +.styled-table thead tr { + background-color: var(--polynesian-blue-color); + color: var(--white-color); + text-align: left; +} + +/* table head (if no link) */ +.styled-table th.no-link { + padding: 1em; +} + +/* table head (if link) */ +.styled-table th a { + display: block; + padding: 1em; +} + +/* table head (if link) hover behavior */ +.styled-table thead tr th a:hover { + background-color: var(--sky-blue-color); +} + +/* table field */ +.styled-table td { + padding: 0; +} + +/* table row */ +.styled-table tr { + padding: 0; +} + +/* table field (if no link) */ +.styled-table td.no-link { + padding: 1em; +} + +/* table field (if link) */ +.styled-table td a { + display: block; + padding: 1em; +} + +/* body row */ +.styled-table tbody tr { + border-bottom: 1px solid var(--light-gray-color); +} + +/* body row every-other-row */ +.styled-table tbody tr:nth-of-type(even) { + background-color: var(--dark-white-color); +} + +/* last row (puts border on bottom of table) */ +.styled-table tbody tr:last-of-type { + border-bottom: 1.5px solid var(--delft-blue-color); +} + +/* if there's an active row, bold it and change the text color */ +.styled-table tbody tr.active-row { + font-weight: bold; + color: var(--delft-blue-color); +} + +/* head hover behavior (should look like it doesn't do anything) */ +.styled-table thead tr:hover { + background-color: var(--polynesian-blue-color); +} + +/* row hover behavior */ +/* .styled-table tr.no-link:hover { + background-color: var(--sky-blue-color); +} */ + +/* row hover behavior */ +.styled-table tr.link:hover { + background-color: var(--sky-blue-color); +} + +/* row hover behavior (overrides alternating row color) */ +.styled-table tbody tr.link:nth-of-type(even):hover { + background-color: var(--sky-blue-color); +} + +/* body row every-other-row */ +#history-table tbody tr:nth-of-type(odd) { + background-color: var(--white-color); + color: black; +} + +#history-table tbody tr:nth-of-type(even) { + color: black; +} + +/* rounds upper lefmost corner of first row */ +#history-table tbody tr:nth-of-type(1) td:first-child { + border-top-left-radius: 10px; +} + +/* rounds upper rightmost corner of first row */ +#history-table tbody tr:nth-of-type(1) td:last-child { + border-top-right-radius: 10px; +} diff --git a/app/assets/stylesheets/variables.css b/app/assets/stylesheets/variables.css index 915c14dd3..60d2dc4b7 100644 --- a/app/assets/stylesheets/variables.css +++ b/app/assets/stylesheets/variables.css @@ -1,13 +1,37 @@ :root { - --title-font: 'Rubik', 'Arial'; - --body-font: 'Rubik', 'Arial'; - - --black-color: #160919; - --light-black-color: #2E2230; - --gray-color: #84848C; - --light-gray-color: #D0CED1; - --dark-white-color: #F3F2F3; - --medium-white-color: #FDFCFD; - --white-color: #FFFFFF; - -} \ No newline at end of file + --title-font: "Gill Sans", sans-serif; + --body-font: "Gill Sans", sans-serif; + + /* Monochrome */ + --black-color: #160919; + --light-black-color: #2e2230; + --dark-gray-color: #606060; + --slate-gray-color: #787878; /* SWW: I propose merging this with --gray-color */ + --gray-color: #84848c; + --medium-gray-color: #c4c4c4; + --ccc-color: #cccccc; /* SWW: I propose merging this with --light-gray-color */ + --light-gray-color: #d0ced1; + --dark-white-color: #f3f2f3; + --medium-white-color: #fdfcfd; + --light-white-color: #eeeeee; /* SWW: I propose merging this with --medium-white-color */ + --white-color: #ffffff; + + /* Blues */ + --delft-blue-color: #1d3461; + --polynesian-blue-color: #1f487e; + --dark-teal-color: #418ca0; + --azure-color: #2196f3; + --sky-blue-color: #5ec4e0; + --button-hover-color: #408ca0; + + /* Opaque colors */ + --drop-shadow-opaque: rgba(0, 0, 0, 0.15); + + /* Colors to consider instead of white: */ + --old-lace-color: #fdf5e6; + --cosmic-latte-color: #fff8e7; + --floral-white-color: #fffaf0; + + /* Colors outside theme */ + --bg-blue-200: #bfdbfe; /* from application.tailwind.css */ +} diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb new file mode 100644 index 000000000..a14c52853 --- /dev/null +++ b/app/controllers/about_controller.rb @@ -0,0 +1,4 @@ +class AboutController < ApplicationController + def index + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09705d12a..dee4500d6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,33 @@ -class ApplicationController < ActionController::Base -end +class ApplicationController < ActionController::Base + helper_method :current_user + helper_method :user_signed_in? + helper_method :current_rental + helper_method :active_rental? + + def current_user + @current_user ||= session[:user_id] && User.find_by(id: session[:user_id]) + end + + def user_signed_in? + !current_user.nil? + end + + def authenticate_user + redirect_to login_path, flash: {alert: 'You must be signed in to rent a bike.'} if current_user.nil? + end + + def current_rental + # set current rental to the rental that matches current user id and is active + # this hinges on users only having one active rental, if we allow uses more than one rental we will have to to change this + @current_rental = Rental.find_by(user_id: session[:user_id], active: true) + end + + def active_rental? + # check if current_rental is nil, meaning current user has no active rentals + !current_rental.nil? + end + + # def redirect_if_authenticated + # redirect_to root_path, flash: { info: 'You are already logged in.'} if user_signed_in? + # end +end diff --git a/app/controllers/bikes_controller.rb b/app/controllers/bikes_controller.rb new file mode 100644 index 000000000..01d5c3a52 --- /dev/null +++ b/app/controllers/bikes_controller.rb @@ -0,0 +1,40 @@ +class BikesController < ApplicationController + + def index + @bikes = Bike.all.order(identifier: :asc) + end + + def return + # gets return station id based on the parameter passed to return from the form + station_id = params[:station_id] + # if return station id is valid + if !Station.find_by(id: station_id).nil? + # gets correct bike object based on id + @bike = Bike.find(params[:id]) + # updates bike to reflect being returned + @bike.update(status: "available", current_station_id: station_id) + # gets current rental + @rental = Rental.find(params[:current_rental_id]) + # if bike was returned late, set over_time = true + if @rental.end_time < Time.now + @rental.update(over_time: true) + end + @rental.update(end_time: Time.now) + # change rental to inactive + @rental.update(active: false) + # get current user + @user = User.find_by(id: session[:user_id]) + # calculate elapsed hours, divied by 3600 because ruby time is in seconds + elapsed_hours = (Time.now - @rental.start_time) / 3600.0 + # update user hours to reflect ride + @user.update_attribute(:hours, @user.hours + elapsed_hours) + @rental.update_attribute(:duration, elapsed_hours * 60) + # redirects the users back to the rental page + redirect_to return_path + else + flash.alert = "Please enter a valid station ID." + redirect_to rental_path(current_rental), status: :unprocessable_entity + end + end + +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 000000000..fc0b4743a --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,2 @@ +class HomeController < ApplicationController +end diff --git a/app/controllers/rentals_controller.rb b/app/controllers/rentals_controller.rb new file mode 100644 index 000000000..b9a7a7983 --- /dev/null +++ b/app/controllers/rentals_controller.rb @@ -0,0 +1,129 @@ +class RentalsController < ApplicationController + before_action :set_rental, only: %i[ show edit update destroy ] + before_action :authenticate_user + + # GET /rentals or /rentals.json + def index + @rentals = Rental.all + @user = User.find(session[:user_id]) + end + + # GET /rentals/1 or /rentals/1.json + def show + @rental = Rental.find(params[:id]) + @bike = Bike.find(@rental.id) + @stations = Station.all + end + + # GET /rentals/new + def new + @rental = Rental.new + end + + # GET /rentals/1/edit + def edit + end + + # returns if the user has five rentals, false if less than, true if five + def five_rentals? + return (Rental.where(user_id: session[:user_id], active: true).count == 5) + end + + # POST /rentals or /rentals.json + def create + @rental = Rental.new(rental_params) + amount = rental_cost(@rental.duration) + respond_to do |format| + # check if user already has five active rentals, then display flash message and do not save rental + if five_rentals? + format.html do + flash.alert = "You can only rent five bikes at once." + redirect_to rentals_path, status: :unprocessable_entity + end + format.json { render json: @rental.errors, status: :unprocessable_entity } + elsif (current_user.balance || 0) < amount + format.html do + flash.alert = "You do not have sufficient funds to rent this bike." + redirect_to rentals_path, status: :unprocessable_entity + end + format.json { render json: @rental.errors, status: :unprocessable_entity } + elsif @rental.save + # if the rental is valid... + # update user's balance to reflect purchase + current_user.update_attribute(:balance, current_user.balance - amount) + # update start and end time of rental + @rental.update(start_time: @rental.created_at, end_time: @rental.created_at + @rental.duration.minutes, active: true) + # find bike associated with rental, and update it so it is not at a station and is rented + bike = Bike.find_by(id: @rental.bike_id) + bike.update(status: "rented", current_station_id: nil) + # redirct user to the page for their rental + format.html { redirect_to rental_path(@rental), notice: "Rental was successfully created."} + else + # else, display flash alert and refresh the page for the current station + format.html do + flash.alert = "Please select a bike." + redirect_to stations_path(@station), status: :unprocessable_entity + end + format.json { render json: @rental.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /rentals/1 or /rentals/1.json + def update + respond_to do |format| + if @rental.update(rental_params) + format.html { redirect_to rental_url(@rental), notice: "Rental was successfully updated." } + format.json { render :show, status: :ok, location: @rental } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @rental.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /rentals/1 or /rentals/1.json + def destroy + @rental.destroy + + respond_to do |format| + format.html { redirect_to rentals_url, notice: "Rental was successfully destroyed." } + format.json { head :no_content } + end + end + + # cost of each rental duration + def rental_cost(duration) + case duration + when 15 + return 2.0 + when 30 + return 3.0 + when 45 + return 4.0 + when 60 + return 5.0 + when 75 + return 6.0 + when 90 + return 7.0 + when 105 + return 8.0 + when 120 + return 9.0 + else + return 0.0 + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_rental + @rental = Rental.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def rental_params + params.require(:rental).permit(:bike_id, :end_time, :identifier, :start_time, :over_time, :user_id, :duration) + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 000000000..cd6b680f0 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,24 @@ +class SessionsController < ApplicationController + # skip_before_action :authenticate_user, only: [:new, :create] + # before_action :redirect_if_authenticated, only: [:new, :create] + + def new + @user = User.new + end + + def create + @user = User.find_by(username: params[:user][:username]) + if @user.present? && @user.authenticate(params[:user][:password]) + session[:user_id] = @user.id + redirect_to root_path, flash: { success: 'Logged in successfully' } + else + flash.alert = "Please enter a valid username and password." + redirect_to login_path + end + end + + def destroy + session[:user_id] = nil + redirect_to root_path, flash: { success: 'Logged Out' } + end +end diff --git a/app/controllers/signin_controller.rb b/app/controllers/signin_controller.rb new file mode 100644 index 000000000..5d7742b17 --- /dev/null +++ b/app/controllers/signin_controller.rb @@ -0,0 +1,4 @@ +class SigninController < ApplicationController + def index + end +end diff --git a/app/controllers/stations_controller.rb b/app/controllers/stations_controller.rb index 88090af16..4a66886a3 100644 --- a/app/controllers/stations_controller.rb +++ b/app/controllers/stations_controller.rb @@ -1,7 +1,16 @@ -class StationsController < ApplicationController - - def index - @stations = Station.all.order(identifier: :asc) - end - -end +class StationsController < ApplicationController + include StationsHelper + def index + if params[:sort] == "available_bikes" + @stations = Station.all.sort_by { |station| count_available_bikes(station) }.reverse + else + @stations = Station.all.order(params[:sort]) + end + @view = params[:view] || 'map' + end + + def show + @station = Station.find(params[:id]) + end + +end diff --git a/app/controllers/stripe_controller.rb b/app/controllers/stripe_controller.rb new file mode 100644 index 000000000..ee3728d28 --- /dev/null +++ b/app/controllers/stripe_controller.rb @@ -0,0 +1,53 @@ +class StripeController < ApplicationController + skip_before_action :verify_authenticity_token + + def create_checkout_session + begin + session = Stripe::Checkout::Session.create({ + line_items: [{ + price: 'price_1QOoqiBdC2IsMXiiWSn0leCQ', + quantity: 1, + }], + mode: 'payment', + success_url: Rails.env.production? ? "https://vectorbike-ecfe326f48ab.herokuapp.com/stripe/success?session_id={CHECKOUT_SESSION_ID}" : "http://localhost:3000/stripe/success?session_id={CHECKOUT_SESSION_ID}", + cancel_url: Rails.env.production? ? "https://vectorbike-ecfe326f48ab.herokuapp.com/users/#{current_user.id}" : "http://localhost:3000/users/#{current_user.id}", + client_reference_id: current_user.id + }) + + render json: { id: session.id } + end + end + + def success + session_id = params[:session_id] + + begin + session = Stripe::Checkout::Session.retrieve(session_id) + + if session.payment_status == 'paid' + puts('1 session payment status paid') + Rails.logger.info '1 session payment status paid' + user = User.find(session.client_reference_id) + puts('2 user found') + Rails.logger.info '2 user found' + amount_paid = session.amount_total / 100.0 + puts('3 amount calculated') + Rails.logger.info '3 amount calculated' + user.update_attribute(:balance, user.balance + amount_paid) + puts('4 updated, maybe?') + Rails.logger.info '4 updated, maybe?' + # these do not work :( + flash[:notice] = "Payment successful! Your balance is now $#{user.balance}0" + else + # this doesn't work :( + flash[:alert] = "Payment failed, please try again." + end + rescue Stripe::StripeError => e + flash[:alert] = "Stripe Error: #{e.message}" + rescue => e + flash[:alert] = "Unexpected error: #{e.message}" + end + + redirect_to user_path(current_user) + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 000000000..c05e7a074 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,74 @@ +class UsersController < ApplicationController + before_action :set_user, only: %i[ show edit update ] + before_action :validate_user_profile_page, only: [:show] + + # GET /users or /users.json + def index + @users = User.all + end + + # GET /users/1 or /users/1.json + def show + end + + # GET /users/new + def new + @user = User.new + end + + # GET /users/1/edit + def edit + end + + # POST /users or /users.json + def create + @user = User.new(user_params) + + if @user.save + session[:user_id] = @user.id + redirect_to @user, notice: 'User was successfully created.' + else + flash.now[:alert] = @user.errors.full_messages.join(", ") + render :new, status: :unprocessable_entity + end + end + + # PATCH/PUT /users/1 or /users/1.json + def update + if @user.update(user_params) + redirect_to @user, notice: 'Profile updated successfully.' + else + flash.now[:alert] = @user.errors.full_messages.join(", ") + render :edit, status: :unprocessable_entity + end + end + + # DELETE /users/1 or /users/1.json + def destroy + session[:user_id] = nil + @user = User.find(params[:id]) + @user.destroy + respond_to do |format| + format.html { redirect_to root_path, flash: { success: 'Deleted Profile Successfully' } } + end + end + + def validate_user_profile_page + @user = User.find(params[:id]) + + if current_user != @user + redirect_to root_path + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_user + @user = User.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def user_params + params.require(:user).permit(:identifier, :username, :first, :last, :email, :password, :balance) + end +end diff --git a/app/helpers/about_helper.rb b/app/helpers/about_helper.rb new file mode 100644 index 000000000..68e69aee1 --- /dev/null +++ b/app/helpers/about_helper.rb @@ -0,0 +1,2 @@ +module AboutHelper +end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb new file mode 100644 index 000000000..23de56ac6 --- /dev/null +++ b/app/helpers/home_helper.rb @@ -0,0 +1,2 @@ +module HomeHelper +end diff --git a/app/helpers/rentals_helper.rb b/app/helpers/rentals_helper.rb new file mode 100644 index 000000000..9aef902be --- /dev/null +++ b/app/helpers/rentals_helper.rb @@ -0,0 +1,11 @@ +module RentalsHelper + RENTAL_NUMBER_RANGE = (10_000..99_999) + + def generate_rental_id() + rental_id = loop do + number = rand(RENTAL_NUMBER_RANGE) + break number unless Rental.exists?(identifier: number) + end + return rental_id + end +end diff --git a/app/helpers/signin_helper.rb b/app/helpers/signin_helper.rb new file mode 100644 index 000000000..b94faa665 --- /dev/null +++ b/app/helpers/signin_helper.rb @@ -0,0 +1,2 @@ +module SigninHelper +end diff --git a/app/helpers/signup_helper.rb b/app/helpers/signup_helper.rb new file mode 100644 index 000000000..f954dda59 --- /dev/null +++ b/app/helpers/signup_helper.rb @@ -0,0 +1,2 @@ +module SignupHelper +end diff --git a/app/helpers/stations_helper.rb b/app/helpers/stations_helper.rb index 799631212..eebbdf7b5 100644 --- a/app/helpers/stations_helper.rb +++ b/app/helpers/stations_helper.rb @@ -1,2 +1,9 @@ module StationsHelper + def count_available_bikes(station) + return station.docked_bikes.select{ |bike| bike[:status] == "available" }.count + end + + def bikes_available_sorted(station) + return station.docked_bikes.select{ |bike| bike[:status] == "available"}.sort_by{|bike| -bike.charge} + end end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 000000000..32a916e77 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,10 @@ +module UsersHelper + USER_NUMBER_RANGE=(1_000..9_999) + def generate_user_id() + user_id = loop do + number = rand(USER_NUMBER_RANGE) + break number unless User.exists?(identifier: number) + end + return user_id + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js index 0d7b49404..059eab23a 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,3 +1,3 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "@hotwired/turbo-rails" -import "controllers" +import "controllers" \ No newline at end of file diff --git a/app/javascript/pop_up.js b/app/javascript/pop_up.js new file mode 100644 index 000000000..74522330f --- /dev/null +++ b/app/javascript/pop_up.js @@ -0,0 +1,13 @@ +// Function to show the popup +function popup() { + document.getElementById('popup').style.display = 'block'; + } + + // Function to close the popup + function close_popup() { + document.getElementById('popup').style.display = 'none'; + } + + function close_flash() { + document.getElementById('flash').style.display = 'none'; + } \ No newline at end of file diff --git a/app/models/bike.rb b/app/models/bike.rb index f3648e343..5e7e1b086 100644 --- a/app/models/bike.rb +++ b/app/models/bike.rb @@ -1,7 +1,9 @@ class Bike < ApplicationRecord validates_presence_of :identifier validates_uniqueness_of :identifier - + attribute :status, :string + attribute :charge, :integer + belongs_to :current_station, class_name: :Station, foreign_key: :current_station_id, optional: true end diff --git a/app/models/rental.rb b/app/models/rental.rb new file mode 100644 index 000000000..1543f0731 --- /dev/null +++ b/app/models/rental.rb @@ -0,0 +1,13 @@ +class Rental < ApplicationRecord + validates_presence_of :identifier + validates_uniqueness_of :identifier + validates_presence_of :bike_id + attribute :start_time, :datetime + attribute :end_time, :datetime + attribute :over_time, :boolean + attribute :duration, :integer + attribute :active, :boolean + + belongs_to :user, optional: true + belongs_to :bike +end diff --git a/app/models/station.rb b/app/models/station.rb index 64b9eaada..4043d02a3 100644 --- a/app/models/station.rb +++ b/app/models/station.rb @@ -3,7 +3,10 @@ class Station < ApplicationRecord :name, :address validates_uniqueness_of :identifier - + attribute :capacity, :integer + attribute :lat, :float + attribute :long, :float + has_many :docked_bikes, class_name: :Bike, foreign_key: :current_station_id - + end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 000000000..abfe547ae --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,16 @@ +class User < ApplicationRecord + has_secure_password + + validates :identifier, presence: true, numericality: true, uniqueness: true + validates :username, presence: true, uniqueness: true, length: { maximum: 20, too_long: "%{count} characters is the maximum allowed" } + validates :first, presence: { message: "name can't be blank" } + validates :last, presence: { message: "name can't be blank" } + validates :email, presence: true, uniqueness: true, format: { with: /\A(.+)@(.+)\z/, message: "invalid" } + validates :password, length: { minimum: 8 } + attribute :hours, :float + attribute :balance, :float + + has_many :rentals +end + +# do we actually validate length of password? diff --git a/app/views/about/index.html.erb b/app/views/about/index.html.erb new file mode 100644 index 000000000..8c8765749 --- /dev/null +++ b/app/views/about/index.html.erb @@ -0,0 +1,53 @@ +<%= render(partial: 'layouts/header') %> +
+
+
+ About VectorBike +
+
+
+

Our Story

+
+

VectorBike is a bike rental system in Nipmuc Notch. We offer electric bike rentals for people who want to commute or go for a casual ride. Our low commitment program allows for bikers to rent bikes as often or infrequently as preferred.

+


Our electric bikes make biking accessible for riders of all skill levels. Whether your desire to ride an e-bike comes from a passion for the environment or a love of biking, VectorBike is perfect for you!

+


Customer Testimonials

+

Justine Roberts: I live in Nlorence, and commute to downtown Nipmuc Notch multiple times a week. I love how VectorBike is low commitment, sustainable and I can rent a bike anytime. They also have excellent customer service!

+ <%= image_tag('rating.png', class: "rating_image", alt: '5/5 stars') %> +
+


Steve Adams: I love riding bikes with my wife, so I love that I can rent multiple bikes at once! The website is also incredibly easy for me to use. I never get confused.

+ <%= image_tag('rating.png', class: "rating_image", alt: '5/5 stars') %> +
+
+
+ <%= image_tag('valley_bike.png', alt: 'Valley Bike') %> +
+
+
+
+ About the Developers +
+
+
+ VectorBike was developed by Emma Ruckle, Molly Neu, Molly Daniel, Skye Weaver Worster, and Sydney Weisberg. +
+
+
+ <%= image_tag('emma.png', class: "snail_pics", alt: 'Emma Snail') %> + <%= image_tag('mollyn.png', class: "snail_pics", alt: 'Molly N Snail') %> + <%= image_tag('mollyd.png', class: "snail_pics", alt: 'Molly D Snail') %> +
+
+ <%= image_tag('skye.png', class: "snail_pics", alt: 'Skye Snail') %> + <%= image_tag('syd.png', class: "snail_pics", alt: 'Sydney Snail') %> +
+
+
+

Contact Us

+

1 Chapin Way

+

Northampton, MA 01063

+

1-800-BICYCLE

+

help@vectorbike.com

+
+
+
+
\ No newline at end of file diff --git a/app/views/bikes/_row.html.erb b/app/views/bikes/_row.html.erb new file mode 100644 index 000000000..a8ff93788 --- /dev/null +++ b/app/views/bikes/_row.html.erb @@ -0,0 +1,6 @@ +
flexbox vertical stretch"> + Bike ID: <%= bike.identifier %> +
+ Current Station: <%= bike.current_station %> +
+
\ No newline at end of file diff --git a/app/views/bikes/index.html.erb b/app/views/bikes/index.html.erb new file mode 100644 index 000000000..db435cef8 --- /dev/null +++ b/app/views/bikes/index.html.erb @@ -0,0 +1,26 @@ +<%= render(partial: 'layouts/header') %> +
+
+
+ Thank you for returning your bike! +
+
+
+

+

We hope you enjoyed your ride, and we look forward to seeing you again soon.

+
+
+
+ <% if active_rental? %> + <%# if there is an active rental, link to rental list page %> + <%= link_to rentals_path, class: "button" do %> + View Active Rentals + <% end %> +
+
+ <% end %> +
+ <%= image_tag('bike_return.jpg', class: "large_viktor_image", alt: 'Viktor snail') %> +
+
+
\ No newline at end of file diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb new file mode 100644 index 000000000..885dae9a3 --- /dev/null +++ b/app/views/home/index.html.erb @@ -0,0 +1,101 @@ +<%= render(partial: 'layouts/header') %> +
+
+
+
+
+
+ <% if user_signed_in? %> + Welcome to VectorBike, <%= current_user.first %>! + <% else %> + Welcome to VectorBike, Riders! + <% end %> +

Snail charm. Electric power.

+
+
+ <% if active_rental? %> + <%= link_to rentals_path, class: "button", id:"rent-a-bike" do %> + Rent a Bike! + <% end %> + <% else %> + <%= link_to stations_path, class: "button", id:"rent-a-bike" do %> + Rent a Bike! + <% end %> + <% end %> +
+
+
+
+ +
+

About VectorBike

+
+
+
+

VectorBike is a bike share program located in Nipmuc Notch. Riders can rent electric bikes and return them to any bike station. Our service is great for commuters or casual riders.

+
+
+
+
+

Bike Share Locations

+ <%= render(partial: 'layouts/map') %> +
+
+
+
+

Pricing

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TimePrice
+
+
+
+
+
+

How to Use VectorBike

+
+ <%= image_tag('viktor_steps.jpg', alt: 'Vector Bike Steps', style: 'margin-left: 20px; margin-right: 20px; width: 80%;') %> +
+
+
+ + \ No newline at end of file diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb new file mode 100644 index 000000000..5d5af9612 --- /dev/null +++ b/app/views/layouts/_footer.html.erb @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb new file mode 100644 index 000000000..9636c9572 --- /dev/null +++ b/app/views/layouts/_header.html.erb @@ -0,0 +1,32 @@ +
    +
  • <%= link_to(image_tag('viktor_snail.png', alt: 'Viktor icon'), root_path) %>
  • + <% if user_signed_in? %> +
  • <%= button_to 'Log Out', logout_path, method: :delete, class: "header" %>
  • + <%= link_to user_path(current_user), class: "header" do %> + Profile + <% end %> + <% else %> + <%= link_to signup_path, class: "header" do %> + Sign Up + <% end %> + <%= link_to login_path, class: "header" do %> + Sign In + <% end %> + <% end %> + <% if active_rental? %> + <%# if there is an active rental, link to rental list page %> + <%= link_to rentals_path, class: "header" do %> + Rent + <% end %> + <% else %> + <%= link_to stations_path, class: "header" do %> + Rent + <% end %> + <% end %> + <%= link_to about_path, class: "header" do %> + About + <% end %> + <%= link_to root_path, class: "header" do %> + Home + <% end %> +
\ No newline at end of file diff --git a/app/views/layouts/_map.html.erb b/app/views/layouts/_map.html.erb new file mode 100644 index 000000000..39bb87509 --- /dev/null +++ b/app/views/layouts/_map.html.erb @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 7ace1393e..c51643768 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,6 +1,7 @@ - + + <%= csrf_meta_tags %> @@ -10,13 +11,16 @@ <%= javascript_importmap_tags %> - ValetBike - - + VectorBike + + +
-
- <%= yield %> -
+
+ <%= yield %> +
- - + <%= render "layouts/footer" %> + + + \ No newline at end of file diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index cbd34d2e9..82847fee4 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -1,13 +1,15 @@ - + + - + - + <%= yield %> - - + + + \ No newline at end of file diff --git a/app/views/rentals/_rentals.html.erb b/app/views/rentals/_rentals.html.erb new file mode 100644 index 000000000..8914d21bd --- /dev/null +++ b/app/views/rentals/_rentals.html.erb @@ -0,0 +1,31 @@ + + + + + + + + + + + + <% rentals.each_with_index do |rental, i| %> + + <%= link_to rental_path(rental.id) do %> + + + + + <% end %> + + <% end %> + +
+ <%="Rental Number"%> + + <%="Start Time"%> + + <%="End Time"%> + + <%="Bike ID"%> +
\ No newline at end of file diff --git a/app/views/rentals/error.html.erb b/app/views/rentals/error.html.erb new file mode 100644 index 000000000..e6e60cfc2 --- /dev/null +++ b/app/views/rentals/error.html.erb @@ -0,0 +1,4 @@ +
+

There was an error creating the rental.

+

<%= notice %>

+
\ No newline at end of file diff --git a/app/views/rentals/index.html.erb b/app/views/rentals/index.html.erb new file mode 100644 index 000000000..b2593a7c5 --- /dev/null +++ b/app/views/rentals/index.html.erb @@ -0,0 +1,27 @@ +<%= render(partial: 'layouts/header') %> +
+
+ Active Rentals +
+
+
+

Click on a rental to view your rental countdown or return your bike.

+
+
+
+ <% @rentals = Rental.where(user_id: @user.id, active: true) %> +
+
+ <%= render "rentals", rentals: @rentals %> + <% if Rental.where(user_id: session[:user_id], active: true).count < 5 %> + <%# button %> + <%= link_to stations_path, class: "button" do %> + Rent Another Bike + <% end %> + <% else %> +

You have reached the maximum number of current bike rentals.

+ <% end %> +
+
+
+
\ No newline at end of file diff --git a/app/views/rentals/show.html.erb b/app/views/rentals/show.html.erb new file mode 100644 index 000000000..cd2ffa6be --- /dev/null +++ b/app/views/rentals/show.html.erb @@ -0,0 +1,111 @@ +<%# imports pop up js file %> +<%= javascript_include_tag "pop_up", "data-turbolinks-track" => true %> + +<%= render(partial: 'layouts/header') %> +
+
+

+
+ Success! +
+
+ Enjoy your ride! +
+

Your ride ends in...

+
+   +
+

+
<%= image_tag('viktor_snail.png', alt: 'Bike icon', style: 'width: 100px;') %>
+

+ <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> + <%= link_to rentals_path, class: "button" do %> + View Active Rentals + <% end %> +
+ Return Your Bike +
+

+ + \ No newline at end of file diff --git a/app/views/sessions/_form.html.erb b/app/views/sessions/_form.html.erb new file mode 100644 index 000000000..9f7a348c8 --- /dev/null +++ b/app/views/sessions/_form.html.erb @@ -0,0 +1,11 @@ +<%= form_with(model: @user, url: login_path, local: true) do |form| %> +
+
+ <%= form.text_field :username, placeholder:"Enter Username" , class:"field" %> +
+
+ <%= form.password_field :password, placeholder:"Enter Password", class:"field" %> +
+ <%= form.submit "Sign in!", class:"submit button" %> + <% end %> +
\ No newline at end of file diff --git a/app/views/sessions/index.html.erb b/app/views/sessions/index.html.erb new file mode 100644 index 000000000..e8144af82 --- /dev/null +++ b/app/views/sessions/index.html.erb @@ -0,0 +1,10 @@ +<%# WE DO NOT ROUTE TO THIS PAGE %> +<%= render(partial: 'layouts/header') %> +
+
+
+ Sign In! +
+
+ <%= render(partial: "form") %> +
\ No newline at end of file diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 000000000..e73174ff2 --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,24 @@ +<%= javascript_include_tag "pop_up", "data-turbolinks-track" => true %> +<%= render(partial: 'layouts/header') %> +
+
+ <% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+ <% end %> +
+ Sign In! +
+
+
+<%= render(partial: "form") %> +
+
+

Don't have an account yet?

+
+
+ <%= link_to signup_path, class: "button" do %> + Make one here! + <% end %> +
\ No newline at end of file diff --git a/app/views/stations/_dropdown.html.erb b/app/views/stations/_dropdown.html.erb new file mode 100644 index 000000000..221e74e48 --- /dev/null +++ b/app/views/stations/_dropdown.html.erb @@ -0,0 +1,28 @@ +
+ +
+ + \ No newline at end of file diff --git a/app/views/stations/_form.html.erb b/app/views/stations/_form.html.erb new file mode 100644 index 000000000..aaa1d5f5a --- /dev/null +++ b/app/views/stations/_form.html.erb @@ -0,0 +1,66 @@ +<%= javascript_include_tag "pop_up", "data-turbolinks-track" => true %> +<%# displays flash alert if there is one %> +<% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+<% end %> +

Available Bikes:

+<%# did not use model: @rental because arguments were not being wrapped in rental key, using name: rental[attribute] syntax instead %> +
+ <%= form_with(url: rentals_path, local: true) do |form| %> + <%# if there are bikes at the station, display and allow user to select one%> + <% if count_available_bikes(@station).present? %> + <% bikes_available_sorted(@station).each do |bike|%> + + <% end %> +
+ <%# allow user to select duration of ride %> +

Select Ride Duration:

+ <%= form.select :duration, [ + ["15 minutes", 15], + ["30 minutes", 30], + ["45 minutes", 45], + ["1 Hour", 60], + ["1 Hour 15 Minutes", 75], + ["1 Hour 30 Minutes", 90], + ["1 Hour 45 Minutes", 105], + ["2 Hours", 120]], {}, name: "rental[duration]", + id: "dropdown" + %> + <%# hidden fields %> + <%# placeholder, we will generate identifiers later %> + <%= form.hidden_field :identifier, value: generate_rental_id(), name: "rental[identifier]"%> + <%# init overtime to false %> + <%= form.hidden_field :over_time, value: false, name: "rental[over_time]" %> + <%# hardcode user id %> + <%# hardcode user id %> + + <% if current_user.nil? %> + <%= form.hidden_field :user_id, value: 0, name: "rental[user_id]" %> + <% else %> + <%= form.hidden_field :user_id, value: current_user.id, name: "rental[user_id]" %> + <% end %> + + <%# submit form %> +
+ <%= form.submit "Rent Now!", class: "submit button" %> + <%= link_to stations_path(), class: "return button" do %> + Return to Stations + <% end %> +
+ <% else %> + <%# if there are no bikes available, display the message %> +
No bikes currently available at this station.
+
+ <%= link_to stations_path(), class: "return button" do %> + Return to Stations + <% end %> +
+ <% end %> + <% end %> +
\ No newline at end of file diff --git a/app/views/stations/_row.html.erb b/app/views/stations/_row.html.erb index 68a74d983..8727980f8 100644 --- a/app/views/stations/_row.html.erb +++ b/app/views/stations/_row.html.erb @@ -1,3 +1,7 @@
flexbox vertical stretch"> - <%= station.identifier %>: <%= station.name %> (<%= station.address %>) +
+ <%= link_to station_path(station.id) do %> + <%= station.identifier %>: <%= station.name %> (<%= station.address %>) + <% end %> +
\ No newline at end of file diff --git a/app/views/stations/_stations.html.erb b/app/views/stations/_stations.html.erb new file mode 100644 index 000000000..c447beaeb --- /dev/null +++ b/app/views/stations/_stations.html.erb @@ -0,0 +1,27 @@ + + + + + + + + + + + <% stations.each do |station| %> + + <%= link_to station_path(station.id) do %> + + + + <% end %> + + <% end %> + +
+ <%= link_to "Station Name", :sort => "name", view: @view%> + + <%= link_to "Address", :sort => "address", view: @view%> + + <%= link_to "Available Bikes", :sort => "available_bikes", view: @view%> +
\ No newline at end of file diff --git a/app/views/stations/_toggle.html.erb b/app/views/stations/_toggle.html.erb new file mode 100644 index 000000000..5d7985fb0 --- /dev/null +++ b/app/views/stations/_toggle.html.erb @@ -0,0 +1,8 @@ +
+
+ <%= link_to "Map View", stations_path(view: "map"), class: "toggle_button #{'active' if @view == 'map'}" %> +
+
+ <%= link_to "List View", stations_path(view: "list"), class: "toggle_button #{'active' if @view == 'list'}" %> +
+
\ No newline at end of file diff --git a/app/views/stations/index.html.erb b/app/views/stations/index.html.erb index d211dda42..8b0590bef 100644 --- a/app/views/stations/index.html.erb +++ b/app/views/stations/index.html.erb @@ -1,16 +1,39 @@ -
-
-
- Welcome to ValetBike! -
-
- <% if @stations.present? %> - <%= render(partial: "stations/row", collection: @stations, as: :station) %> - <% else %> -
- No stations found. -
- <% end %> -
-
-
+<%= render(partial: 'layouts/header') %> +
+
+ Rent a Bike! +
+ <%= render(partial: 'stations/toggle') %> +
+
+

To rent a bike, select the station you would like to rent from. This will bring you to a bike rental form. You'll be able to return your bike to any station at the end of your ride.

+
+
+ <% if @view == 'map' %> +
+
+
+
+ <%= render(partial: 'layouts/map') %> +
+
+ <%= render "dropdown", stations: @stations %> +
+
+
+ <% else %> + <% if @stations.present? %> +
+
+ <%= render "stations", stations: @stations %> +
+
+ <% else %> +
+ No stations found. +
+ <% end %> + <% end %> +
+
+
\ No newline at end of file diff --git a/app/views/stations/show.html.erb b/app/views/stations/show.html.erb new file mode 100644 index 000000000..b0d6781a3 --- /dev/null +++ b/app/views/stations/show.html.erb @@ -0,0 +1,17 @@ + +<%= render(partial: 'layouts/header') %> +
+
+
+ <%= @station.name %> (<%= @station.address %>) +
+
+
+<%= render(partial: "form") %> \ No newline at end of file diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb new file mode 100644 index 000000000..6b8b6cd31 --- /dev/null +++ b/app/views/users/_form.html.erb @@ -0,0 +1,31 @@ +<%= form_with model: @user, local: true do |form| %> +
+ <%# <%= @user.errors.full_messages.inspect %> +
+ <%# <%= form.label :identifier %> + <%= form.hidden_field :identifier, value: generate_user_id() , class:"field" %> +
+
+ <%# <%= form.label :email %> + <%= form.email_field :email, placeholder:"Enter Email", class:"field" %> +
+
+ <%# <%= form.label :first %> + <%= form.text_field :first, placeholder:"First Name" , class:"field" %> +
+
+ <%# <%= form.label :last %> + <%= form.text_field :last , placeholder:"Last Name" , class:"field"%> +
+
+ <%# <%= form.label :username %> + <%= form.text_field :username , placeholder:"Create a Username" , class:"field"%> +
+
+ <%# <%= form.label :password %> + <%= form.password_field :password , placeholder:"Create a Password" , class:"field"%> +

Minimum of 8 characters

+
+ <%= form.submit "Start Riding!", class:"submit button" %> + <% end %> +
\ No newline at end of file diff --git a/app/views/users/_history.html.erb b/app/views/users/_history.html.erb new file mode 100644 index 000000000..5a0617df6 --- /dev/null +++ b/app/views/users/_history.html.erb @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + <% rentals.each_with_index do |rental, i| %> + + + + + + + + + <% end %> + +
+ <%="Rental Number"%> + + <%="Start Time"%> + + <%="End Time"%> + + <%="Duration"%> + + <%="Bike ID"%> + + <%="Overtime"%> +
\ No newline at end of file diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb new file mode 100644 index 000000000..ef968bcfe --- /dev/null +++ b/app/views/users/edit.html.erb @@ -0,0 +1,34 @@ +<%= render(partial: 'layouts/header') %> +
+
+
+ Edit Profile! +
+
+
+<% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+<% end %> +<%= form_with(model: @user, local: true) do |form| %> +
+ <%= form.text_field :first , placeholder:"Update First Name" , class:"field"%> +
+
+ <%= form.text_field :last , placeholder:"Update Last Name" , class:"field"%> +
+
+ <%= form.email_field :email, placeholder:"Update Email", class:"field" %> +
+
+ <%= form.text_field :username, placeholder:"Update Username", class:"field" %> +
+
+ <%= form.password_field :password , placeholder:"Update Password" , class:"field"%> +

Minimum of 8 characters

+
+
+ <%= form.submit 'Update Profile', class:"submit button" %> +
+<% end %> \ No newline at end of file diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb new file mode 100644 index 000000000..2677ecae6 --- /dev/null +++ b/app/views/users/index.html.erb @@ -0,0 +1,2 @@ +<%= render(partial: 'layouts/header') %> +

User profile

\ No newline at end of file diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb new file mode 100644 index 000000000..86d47d9eb --- /dev/null +++ b/app/views/users/new.html.erb @@ -0,0 +1,15 @@ +<%= javascript_include_tag "pop_up", "data-turbolinks-track" => true %> +<%= render(partial: 'layouts/header') %> +
+
+
+ Sign Up! +
+
+
+<% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+<% end %> +<%= render(partial: "form") %> \ No newline at end of file diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb new file mode 100644 index 000000000..b3585e609 --- /dev/null +++ b/app/views/users/show.html.erb @@ -0,0 +1,125 @@ +<%# imports pop up js file %> +<%= javascript_include_tag "pop_up", "data-turbolinks-track" => true %> +<%= render(partial: 'layouts/header') %> +
+
+ <% if flash[:notice] %> +
+ <%= flash[:notice] %> +
+ <% end %> +
+
+ +
+
+
+
+
+ Hello, <%= @user.first %>! + <% top_users = User.order(hours: :desc).limit(10) %> + <% if top_users.include?(@user) %> +
+ + <%= image_tag('top_ten_badge.png', alt: 'Top Ten Badge', style: 'width: 60px;') %> + You are in our top ten riders! + +
+ <% end %> +
+
+ <%= link_to edit_user_path(current_user), class: "profile_button" do %> + Edit Profile + <% end %> +
+ Delete Profile +
+
+
+

Account Information

+
+
+
+ First Name: <%= @user.first %> +
+
+
+ Last Name: <%= @user.last %> +
+
+
+ Userame: <%= @user.username %> +
+
+
+ Email: <%= @user.email %> +
+
+
+ Balance: $<%= sprintf("%.2f", @user.balance || 0.0) %> +
+
+
+ + +
+
+
+
+ <%# display number of hours a user has biked in whole hours %> + Congrats, you have biked <%= (@user.hours || 0).truncate() %> hours on <%= Rental.where(user_id: @user.id).count %> rides! +
+ +
<%= image_tag('flipped_viktor.png', alt: 'Flipped_Snail', style: 'width: 100px;') %>
+
+

Your Rental History

+ <% @rentals = Rental.where(user_id: current_user.id, active: false) %> +
+
+ <%= render "history", rentals: @rentals %> +
+
+
+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index e2d9f859d..cae606918 100644 --- a/config/application.rb +++ b/config/application.rb @@ -10,7 +10,8 @@ module Valetbike class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.0 - + config.time_zone = 'Eastern Time (US & Canada)' + config.active_record.default_timezone = :local # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files @@ -18,5 +19,8 @@ class Application < Rails::Application # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") + config.before_configuration do + Stripe.api_key = ENV['STRIPE_SECRET_KEY'] + end end end diff --git a/config/routes.rb b/config/routes.rb index f1eb0a0d8..2b038abbd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,25 @@ -Rails.application.routes.draw do - root to: "stations#index" -end \ No newline at end of file +Rails.application.routes.draw do + root to: "home#index" + get '/bikes', to: 'bikes#index' + get '/stations', to: 'stations#index' + get '/stations/:id', to: 'stations#show', as: 'station' + get '/return', to: 'bikes#index' + get '/users', to: 'users#index' + resources :rentals + resources :users do + get 'history', on: :member + end + get '/about', to: 'about#index' + get '/signup', to: 'users#new', as: 'signup' + resources :users, only: [:create, :update, :edit, :show, :destroy] + get '/login', to: 'sessions#new', as: 'login' + post '/login', to: 'sessions#create' + delete '/logout', to: 'sessions#destroy' + resources :bikes do + member do + patch 'return' # PATCH request to handle the return action for a specific bike + end + end + post 'create-checkout-session', to: 'stripe#create_checkout_session' + get '/stripe/success', to: 'stripe#success', as: 'stripe_success' +end diff --git a/db/migrate/20241031005840_add_status_to_bike.rb b/db/migrate/20241031005840_add_status_to_bike.rb new file mode 100644 index 000000000..d412abfc7 --- /dev/null +++ b/db/migrate/20241031005840_add_status_to_bike.rb @@ -0,0 +1,5 @@ +class AddStatusToBike < ActiveRecord::Migration[7.0] + def change + add_column :bikes, :status, :string + end +end diff --git a/db/migrate/20241031011243_add_charge_to_bike.rb b/db/migrate/20241031011243_add_charge_to_bike.rb new file mode 100644 index 000000000..cfd90b96d --- /dev/null +++ b/db/migrate/20241031011243_add_charge_to_bike.rb @@ -0,0 +1,5 @@ +class AddChargeToBike < ActiveRecord::Migration[7.0] + def change + add_column :bikes, :charge, :integer + end +end diff --git a/db/migrate/20241031011417_add_capacity_to_station.rb b/db/migrate/20241031011417_add_capacity_to_station.rb new file mode 100644 index 000000000..e8a204667 --- /dev/null +++ b/db/migrate/20241031011417_add_capacity_to_station.rb @@ -0,0 +1,5 @@ +class AddCapacityToStation < ActiveRecord::Migration[7.0] + def change + add_column :stations, :capacity, :integer + end +end diff --git a/db/migrate/20241031012732_create_users.rb b/db/migrate/20241031012732_create_users.rb new file mode 100644 index 000000000..a3648a00f --- /dev/null +++ b/db/migrate/20241031012732_create_users.rb @@ -0,0 +1,14 @@ +class CreateUsers < ActiveRecord::Migration[7.0] + def change + create_table :users do |t| + t.integer :identifier + t.string :username + t.string :first + t.string :last + t.string :email + t.string :password + + t.timestamps + end + end +end diff --git a/db/migrate/20241031180838_create_rentals.rb b/db/migrate/20241031180838_create_rentals.rb new file mode 100644 index 000000000..074663f1d --- /dev/null +++ b/db/migrate/20241031180838_create_rentals.rb @@ -0,0 +1,14 @@ +class CreateRentals < ActiveRecord::Migration[7.0] + def change + create_table :rentals do |t| + t.integer :identifier + t.references :user, null: false, foreign_key: true + t.references :bike, null: false, foreign_key: true + t.datetime :start_time + t.datetime :end_time + t.boolean :over_time + + t.timestamps + end + end +end diff --git a/db/migrate/20241108040246_add_duration_to_rentals.rb b/db/migrate/20241108040246_add_duration_to_rentals.rb new file mode 100644 index 000000000..ef2229290 --- /dev/null +++ b/db/migrate/20241108040246_add_duration_to_rentals.rb @@ -0,0 +1,5 @@ +class AddDurationToRentals < ActiveRecord::Migration[7.0] + def change + add_column :rentals, :duration, :integer + end +end diff --git a/db/migrate/20241110204131_rename_password_to_password_digest.rb b/db/migrate/20241110204131_rename_password_to_password_digest.rb new file mode 100644 index 000000000..fee2ebefb --- /dev/null +++ b/db/migrate/20241110204131_rename_password_to_password_digest.rb @@ -0,0 +1,5 @@ +class RenamePasswordToPasswordDigest < ActiveRecord::Migration[7.0] + def change + rename_column :users, :password, :password_digest + end +end diff --git a/db/migrate/20241114235601_add_latlong_to_stations.rb b/db/migrate/20241114235601_add_latlong_to_stations.rb new file mode 100644 index 000000000..72551496b --- /dev/null +++ b/db/migrate/20241114235601_add_latlong_to_stations.rb @@ -0,0 +1,6 @@ +class AddLatlongToStations < ActiveRecord::Migration[7.0] + def change + add_column :stations, :lat, :float + add_column :stations, :long, :float + end +end diff --git a/db/migrate/20241119032243_add_active_to_rentals.rb b/db/migrate/20241119032243_add_active_to_rentals.rb new file mode 100644 index 000000000..4ac36ff23 --- /dev/null +++ b/db/migrate/20241119032243_add_active_to_rentals.rb @@ -0,0 +1,5 @@ +class AddActiveToRentals < ActiveRecord::Migration[7.0] + def change + add_column :rentals, :active, :boolean + end +end diff --git a/db/migrate/20241120001521_add_hours_to_users.rb b/db/migrate/20241120001521_add_hours_to_users.rb new file mode 100644 index 000000000..e15033828 --- /dev/null +++ b/db/migrate/20241120001521_add_hours_to_users.rb @@ -0,0 +1,5 @@ +class AddHoursToUsers < ActiveRecord::Migration[7.0] + def change + add_column :users, :hours, :float + end +end diff --git a/db/migrate/20241121162942_change_user_foreign_key_in_rentals.rb b/db/migrate/20241121162942_change_user_foreign_key_in_rentals.rb new file mode 100644 index 000000000..48313bf69 --- /dev/null +++ b/db/migrate/20241121162942_change_user_foreign_key_in_rentals.rb @@ -0,0 +1,7 @@ +class ChangeUserForeignKeyInRentals < ActiveRecord::Migration[7.0] + def change + change_column_null :rentals, :user_id, true + remove_foreign_key :rentals, :users + add_foreign_key :rentals, :users, on_delete: :nullify + end +end diff --git a/db/migrate/20241124223847_add_balance_to_users.rb b/db/migrate/20241124223847_add_balance_to_users.rb new file mode 100644 index 000000000..5e8f517c1 --- /dev/null +++ b/db/migrate/20241124223847_add_balance_to_users.rb @@ -0,0 +1,6 @@ +class AddBalanceToUsers < ActiveRecord::Migration[7.0] + def change + add_column :users, :balance, :floatail + change_column_default :users, :balance, from:nil, to: 0.0 + end +end diff --git a/db/migrate/20241125163728_add_default_to_users_hours.rb b/db/migrate/20241125163728_add_default_to_users_hours.rb new file mode 100644 index 000000000..73570556a --- /dev/null +++ b/db/migrate/20241125163728_add_default_to_users_hours.rb @@ -0,0 +1,5 @@ +class AddDefaultToUsersHours < ActiveRecord::Migration[7.0] + def change + change_column_default :users, :hours, from: nil, to: 0.0 + end +end diff --git a/db/migrate/20241206032319_fix_balance_column_type.rb b/db/migrate/20241206032319_fix_balance_column_type.rb new file mode 100644 index 000000000..e4bae19c3 --- /dev/null +++ b/db/migrate/20241206032319_fix_balance_column_type.rb @@ -0,0 +1,6 @@ +class FixBalanceColumnType < ActiveRecord::Migration[7.0] + def change + remove_column :users, :balance, :floatail # Remove the invalid column + add_column :users, :balance, :float, default: 0.0 # Add the column with the correct type + end +end diff --git a/db/schema.rb b/db/schema.rb index b98268f0e..81ce4229a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,12 +10,29 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_08_31_090719) do +ActiveRecord::Schema[7.0].define(version: 2024_12_06_032319) do create_table "bikes", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.integer "identifier" t.integer "current_station_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "status" + t.integer "charge" + end + + create_table "rentals", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.integer "identifier" + t.bigint "user_id" + t.bigint "bike_id", null: false + t.datetime "start_time" + t.datetime "end_time" + t.boolean "over_time" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "duration" + t.boolean "active" + t.index ["bike_id"], name: "index_rentals_on_bike_id" + t.index ["user_id"], name: "index_rentals_on_user_id" end create_table "stations", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -24,6 +41,24 @@ t.string "address" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "capacity" + t.float "lat" + t.float "long" + end + + create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.integer "identifier" + t.string "username" + t.string "first" + t.string "last" + t.string "email" + t.string "password_digest" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.float "hours", default: 0.0 + t.float "balance", default: 0.0 end + add_foreign_key "rentals", "bikes" + add_foreign_key "rentals", "users", on_delete: :nullify end diff --git a/db/seeds.rb b/db/seeds.rb index bc25fce30..d05bab8f6 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,7 @@ -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). -# -# Examples: -# -# movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) -# Character.create(name: "Luke", movie: movies.first) +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Examples: +# +# movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) +# Character.create(name: "Luke", movie: movies.first) diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake new file mode 100644 index 000000000..f1ae52424 --- /dev/null +++ b/lib/tasks/import.rake @@ -0,0 +1,68 @@ +require 'csv' + +desc "Import station and bike data" +task initial_import: [:environment] do + # Import initial data from CSV files + CSV.foreach(("./notes/station-data.csv"), headers: true, col_sep: ",") do |row| + s = Station.new(identifier: row[0], name: row[1], address: row[6],lat: row[7],long: row[8]) + s.save! + end + CSV.foreach(("./notes/bike-data.csv"), headers: true, col_sep: ",") do |row| + b = Bike.new(identifier: row[0], current_station: Station.find_by(identifier: row[1])) + b.save! + end + CSV.foreach(("./notes/user-data.csv"), headers: true, col_sep: ",") do |row| + u = User.new(identifier: row[0], username: row[1],first: row[2],last: row[3], email: row[4], password: row[5]) + u.save! + end + + CSV.foreach(("./notes/rental-data.csv"), headers: true, col_sep: ",") do |row| + r = Rental.new(identifier: row[0], user: User.find_by(identifier: row[1]), bike: Bike.find_by(identifier: row[2]), start_time: row[3], end_time: row[4], over_time: row[5], duration: row[6]) + r.save! + end + + # set charges pseudo-randomly (ensure 0 and 100 exist) + Bike.select{|b| b[:identifier]<10000}.each do |b| + b.charge=rand(0...100) + b.save! + end + Bike.select{|b| b[:identifier]<1000}.each do |b| + b.charge=0 + b.save! + end + Bike.select{|b| b[:identifier]>9000}.each do |b| + b.charge=100 + b.save! + end + + # if bike mostly charged + Bike.select{ |bike| bike.charge>=50}.each do |b| + b.update(status: "available") + b.save! + end + # if bike not mostly charged + Bike.select{ |bike| bike.charge<50}.each do |b| + b.update(status: "charging") + b.save! + end + # arbitrary bikes to service + Bike.select{ |bike| bike.identifier%5==0}.each do |b| + b.update(status: "service") + b.save! + end + # if bike being rented + Bike.select{ |bike| bike.current_station_id.nil? }.each do |b| + b.update(status: "rented") + b.save! + end + + # give each station a capacity at least equal to the number of current bikes + 1 + Station.select{ |station| station.capacity.nil? }.each do |s| + current = s.docked_bikes.select{ |bike| bike }.count + s.update(capacity: current + rand(1...5)) + s.save! + end + + + +end \ No newline at end of file diff --git a/notes/rental-data.csv b/notes/rental-data.csv new file mode 100644 index 000000000..d583288db --- /dev/null +++ b/notes/rental-data.csv @@ -0,0 +1,3 @@ +identifier,user_id,bike_id,start_time,end_time,over_time,duration +90210,200,3849,2024-11-07 00:44:22.000000,2024-11-07 00:56:22.373874,false,15 +69420,600,4273,2024-11-05 00:46:22.317859,2024-11-08 00:46:22.373874,true,45 \ No newline at end of file diff --git a/notes/station-data.csv b/notes/station-data.csv index 6f147a8a8..46be07eee 100644 --- a/notes/station-data.csv +++ b/notes/station-data.csv @@ -1,15 +1,15 @@ -identifier,name,has_kiosk,needs_maintenance,dock_count,docked_bike_count,address -21,Florence Bank Station,0,0,7,4,19 Meadow Street Florence MA @ Lilly Library -24,Florence Center,0,0,10,2,31 Main Street Florence MA -30,Jackson Street,0,0,16,8,7A-7D Jackson Street Northampton MA -25,Cooley Dickinson Health Care,0,1,14,6,51 Locust Street Northampton MA -29,State St/Mass Central Rail Trail,0,0,10,4,State Street/Mass Central Rail Trail -33,Northampton High School,0,0,15,8,380 Elm Street Northampton MA -20,Village Hill/State Hospital,0,0,14,6,Village Hill/State Hospital -26,John M Greene Hall/Smith College,0,0,16,5,1 Chapin Way Northampton MA @ John M. Greene Hall -23,Forbes Library,0,0,13,6,20 West Street Northampton MA -27,Pulaski Park/Downtown,1,0,11,3,274 Main Street Northampton MA at Pulaski Park -31,Main Street/Court House,0,1,10,3,99 Main Street Northampton MA -28,Northampton Train Station,1,0,13,5,Northampton Train Station -32,Main Street/Bridge Street,0,0,15,8,1 Bridge Street Northampton MA -22,YMCA/Childs Park,0,0,17,8,YMCA/Childs Park \ No newline at end of file +identifier,name,has_kiosk,needs_maintenance,dock_count,docked_bike_count,address,lat,long +21,Florence Bank Station,0,0,7,4,19 Meadow Street Florence MA @ Lilly Library,42.33568575212715,-72.67385126672643 +24,Florence Center,0,0,10,2,31 Main Street Florence MA,42.334880979616635,-72.66791824003015 +30,Jackson Street,0,0,16,8,7A-7D Jackson Street Northampton MA,42.33719308519181,-72.6426540854265 +25,Cooley Dickinson Health Care,0,1,14,6,51 Locust Street Northampton MA,42.331315396540454,-72.65354724702235 +29,State St/Mass Central Rail Trail,0,0,10,4,State Street/Mass Central Rail Trail,42.32832216259072,-72.63562203846085 +33,Northampton High School,0,0,15,8,380 Elm Street Northampton MA,42.32510450975492,-72.64958159676463 +20,Village Hill/State Hospital,0,0,14,6,Village Hill/State Hospital,42.311924577374455,-72.64742667440147 +26,John M Greene Hall/Smith College,0,0,16,5,1 Chapin Way Northampton MA @ John M. Greene Hall,42.31958302545687,-72.6377670820666 +23,Forbes Library,0,0,13,6,20 West Street Northampton MA,42.31681998401048,-72.63596421993638 +27,Pulaski Park/Downtown,1,0,11,3,274 Main Street Northampton MA at Pulaski Park,42.31754144350353,-72.63347534499954 +31,Main Street/Court House,0,1,10,3,99 Main Street Northampton MA,42.319442953637754,-72.63006535519565 +28,Northampton Train Station,1,0,13,5,Northampton Train Station,42.318138277552556,-72.62736390023808 +32,Main Street/Bridge Street,0,0,15,8,1 Bridge Street Northampton MA,42.32039295130439,-72.62840679984343 +22,YMCA/Childs Park,0,0,17,8,YMCA/Childs Park,42.32858480782373,-72.64393624376771 \ No newline at end of file diff --git a/notes/user-data.csv b/notes/user-data.csv new file mode 100644 index 000000000..ba0e41f69 --- /dev/null +++ b/notes/user-data.csv @@ -0,0 +1,10 @@ +identifier,username,first,last,email,password +100,sworster,Skye Weaver,Worster,sworster@smith.edu,heylookiminadatabase +200,ringbearer,Frodo,Baggins,underhill@bagend.com,elijahwoodistooyoung +300,eloi,HG,Wells,hgwells@moreau.net,1nvisibleMan +400,themapguy,Felix,DePalma,fdepalma@mapoftime.com,oroboros +500,jrrt,JRR,Tolkien,jrrt@beleriand.org,eruilluvatar +600,seaofflames,Anthony,Doerr,doerr@saint-malo.com,cuckoocloudland +700,historybuff,Erik,Larson,erik.larson@thunderstruck.com,splendidOrVile +800,theMartian,Andy,Weir,andy.weir@fourthrock.org,helloMary +900,N7,Commander,Shepherd,spectre@alliance.mil,SSVnormandy \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico index e69de29bb..ec858d278 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/test/controllers/about_controller_test.rb b/test/controllers/about_controller_test.rb new file mode 100644 index 000000000..c0921efd8 --- /dev/null +++ b/test/controllers/about_controller_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class AboutControllerTest < ActionDispatch::IntegrationTest + test "should get index" do + get about_index_url + assert_response :success + end +end diff --git a/test/controllers/home_controller_test.rb b/test/controllers/home_controller_test.rb new file mode 100644 index 000000000..eac0e3360 --- /dev/null +++ b/test/controllers/home_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class HomeControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/rentals_controller_test.rb b/test/controllers/rentals_controller_test.rb new file mode 100644 index 000000000..5d531c3ec --- /dev/null +++ b/test/controllers/rentals_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class RentalsControllerTest < ActionDispatch::IntegrationTest + setup do + @rental = rentals(:one) + end + + test "should get index" do + get rentals_url + assert_response :success + end + + test "should get new" do + get new_rental_url + assert_response :success + end + + test "should create rental" do + assert_difference("Rental.count") do + post rentals_url, params: { rental: { bike_id: @rental.bike_id, end_time: @rental.end_time, identifier: @rental.identifier, over_time: @rental.over_time, start_time: @rental.start_time, user_id: @rental.user_id } } + end + + assert_redirected_to rental_url(Rental.last) + end + + test "should show rental" do + get rental_url(@rental) + assert_response :success + end + + test "should get edit" do + get edit_rental_url(@rental) + assert_response :success + end + + test "should update rental" do + patch rental_url(@rental), params: { rental: { bike_id: @rental.bike_id, end_time: @rental.end_time, identifier: @rental.identifier, over_time: @rental.over_time, start_time: @rental.start_time, user_id: @rental.user_id } } + assert_redirected_to rental_url(@rental) + end + + test "should destroy rental" do + assert_difference("Rental.count", -1) do + delete rental_url(@rental) + end + + assert_redirected_to rentals_url + end +end diff --git a/test/controllers/signin_controller_test.rb b/test/controllers/signin_controller_test.rb new file mode 100644 index 000000000..734d28e8d --- /dev/null +++ b/test/controllers/signin_controller_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class SigninControllerTest < ActionDispatch::IntegrationTest + test "should get index" do + get signin_index_url + assert_response :success + end +end diff --git a/test/controllers/signup_controller_test.rb b/test/controllers/signup_controller_test.rb new file mode 100644 index 000000000..0b4a9a699 --- /dev/null +++ b/test/controllers/signup_controller_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class SignupControllerTest < ActionDispatch::IntegrationTest + test "should get index" do + get signup_index_url + assert_response :success + end +end diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb new file mode 100644 index 000000000..4950a7f13 --- /dev/null +++ b/test/controllers/users_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class UsersControllerTest < ActionDispatch::IntegrationTest + setup do + @user = users(:one) + end + + test "should get index" do + get users_url + assert_response :success + end + + test "should get new" do + get new_user_url + assert_response :success + end + + test "should create user" do + assert_difference("User.count") do + post users_url, params: { user: { email: @user.email, first: @user.first, identifier: @user.identifier, last: @user.last, password: @user.password, username: @user.username } } + end + + assert_redirected_to user_url(User.last) + end + + test "should show user" do + get user_url(@user) + assert_response :success + end + + test "should get edit" do + get edit_user_url(@user) + assert_response :success + end + + test "should update user" do + patch user_url(@user), params: { user: { email: @user.email, first: @user.first, identifier: @user.identifier, last: @user.last, password: @user.password, username: @user.username } } + assert_redirected_to user_url(@user) + end + + test "should destroy user" do + assert_difference("User.count", -1) do + delete user_url(@user) + end + + assert_redirected_to users_url + end +end diff --git a/test/fixtures/rentals.yml b/test/fixtures/rentals.yml new file mode 100644 index 000000000..c28b2fba0 --- /dev/null +++ b/test/fixtures/rentals.yml @@ -0,0 +1,17 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + identifier: 1 + user: one + bike: one + start_time: 2024-10-31 14:08:39 + end_time: 2024-10-31 14:08:39 + over_time: false + +two: + identifier: 1 + user: two + bike: two + start_time: 2024-10-31 14:08:39 + end_time: 2024-10-31 14:08:39 + over_time: false diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 000000000..50e2eeee1 --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,17 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + identifier: 1 + username: MyString + first: MyString + last: MyString + email: MyString + password: MyString + +two: + identifier: 1 + username: MyString + first: MyString + last: MyString + email: MyString + password: MyString diff --git a/test/models/rental_test.rb b/test/models/rental_test.rb new file mode 100644 index 000000000..c3d74bd40 --- /dev/null +++ b/test/models/rental_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class RentalTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 000000000..5c07f4900 --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class UserTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/system/rentals_test.rb b/test/system/rentals_test.rb new file mode 100644 index 000000000..6d7adb96d --- /dev/null +++ b/test/system/rentals_test.rb @@ -0,0 +1,51 @@ +require "application_system_test_case" + +class RentalsTest < ApplicationSystemTestCase + setup do + @rental = rentals(:one) + end + + test "visiting the index" do + visit rentals_url + assert_selector "h1", text: "Rentals" + end + + test "should create rental" do + visit rentals_url + click_on "New rental" + + fill_in "Bike", with: @rental.bike_id + fill_in "End time", with: @rental.end_time + fill_in "Identifier", with: @rental.identifier + check "Over time" if @rental.over_time + fill_in "Start time", with: @rental.start_time + fill_in "User", with: @rental.user_id + click_on "Create Rental" + + assert_text "Rental was successfully created" + click_on "Back" + end + + test "should update Rental" do + visit rental_url(@rental) + click_on "Edit this rental", match: :first + + fill_in "Bike", with: @rental.bike_id + fill_in "End time", with: @rental.end_time + fill_in "Identifier", with: @rental.identifier + check "Over time" if @rental.over_time + fill_in "Start time", with: @rental.start_time + fill_in "User", with: @rental.user_id + click_on "Update Rental" + + assert_text "Rental was successfully updated" + click_on "Back" + end + + test "should destroy Rental" do + visit rental_url(@rental) + click_on "Destroy this rental", match: :first + + assert_text "Rental was successfully destroyed" + end +end diff --git a/test/system/users_test.rb b/test/system/users_test.rb new file mode 100644 index 000000000..fa677ffc0 --- /dev/null +++ b/test/system/users_test.rb @@ -0,0 +1,51 @@ +require "application_system_test_case" + +class UsersTest < ApplicationSystemTestCase + setup do + @user = users(:one) + end + + test "visiting the index" do + visit users_url + assert_selector "h1", text: "Users" + end + + test "should create user" do + visit users_url + click_on "New user" + + fill_in "Email", with: @user.email + fill_in "First", with: @user.first + fill_in "Identifier", with: @user.identifier + fill_in "Last", with: @user.last + fill_in "Password", with: @user.password + fill_in "Username", with: @user.username + click_on "Create User" + + assert_text "User was successfully created" + click_on "Back" + end + + test "should update User" do + visit user_url(@user) + click_on "Edit this user", match: :first + + fill_in "Email", with: @user.email + fill_in "First", with: @user.first + fill_in "Identifier", with: @user.identifier + fill_in "Last", with: @user.last + fill_in "Password", with: @user.password + fill_in "Username", with: @user.username + click_on "Update User" + + assert_text "User was successfully updated" + click_on "Back" + end + + test "should destroy User" do + visit user_url(@user) + click_on "Destroy this user", match: :first + + assert_text "User was successfully destroyed" + end +end