Skip to content

Commit

Permalink
Add a Seat Selection to Shopping Cart
Browse files Browse the repository at this point in the history
For comparison [see this commit][commit]

Mostly similar to the above. A few things to call out is the introduction of a
`ButtonTo` component and the usage of `form_props`.

1. [form_props] is a forked of `form_with`, but made to output html props for
react components instead of HTML. It allows us to use Rails forms in React.

2. Rail has the equivalent `button_to` helper which builds a form that looks
like a link when the action is POST, PUT, DELETE. We can easily replicate this
with `form_props` and a custom component `ButtonTo`.

[commit]: seanpdoyle@9ea344f
[form_props]: https://github.com/thoughtbot/form_props
  • Loading branch information
jho406 committed Oct 8, 2023
1 parent 2067cd6 commit 918fd12
Show file tree
Hide file tree
Showing 21 changed files with 240 additions and 38 deletions.
14 changes: 14 additions & 0 deletions app/components/ButtonTo.js
@@ -0,0 +1,14 @@
import React from 'react'

export default ({props, extras, inputs, children, ...rest}) => {
const btnSubmit = (inputs && inputs.submit) ?
<button {...inputs.submit} {...rest}>{inputs.submit.text}</button> :
<button type="submit" {...rest}>{ children }</button>;

return (
<form {...props}>
{Object.values(extras).map((hiddenProps) => (<input {...hiddenProps} key={hiddenProps.name}/>))}
{btnSubmit}
</form>
)
}
54 changes: 54 additions & 0 deletions app/components/Cart.js
@@ -0,0 +1,54 @@
import React from 'react'
import SVG from 'react-inlinesvg';
import closeSvg from '../assets/images/icons/x-circle.svg'

export default class extends React.Component {
render () {
const cartItems = this.props.cart.map(({rowNumber, price, removeSvg, id}) => (
<tr key={id}>
<td> {rowNumber} </td>
<td className="syos-table__cell--numerals"> {price} </td>
<td className="syos-u-text-align-right">
<button className="syos-button syos-button--transparent">
<SVG src={ closeSvg } className="syos-icon" title="Remove"/>
</button>
</td>
</tr>
))

return (
<div id="cart-summary">
<h2 className="syos-u-margin-bottom-2">
Your seat selections
</h2>

<p className="syos-u-font-size-small syos-u-margin-bottom-2">
Seats are not reserved until added to the cart.
</p>

<table className="syos-table">
<thead>
<tr>
<th>
Seat
</th>

<th className="syos-table__cell--numerals">
Price
</th>

<th className="visually-hidden">
Remove
</th>
</tr>
</thead>

<tbody>
{ cartItems }
</tbody>
</table>
</div>
)
}
}

10 changes: 4 additions & 6 deletions app/components/SeatDialog.js
@@ -1,5 +1,6 @@
import React from 'react'
import Dialog from './Dialog'
import ButtonTo from './ButtonTo'
import SVG from 'react-inlinesvg';
import closeSvg from '../assets/images/icons/x-circle.svg'

Expand All @@ -10,6 +11,7 @@ export default class extends React.Component {
rowNumber,
price,
show,
seatSelectionForm,
} = this.props

if (!show) {
Expand Down Expand Up @@ -66,12 +68,8 @@ export default class extends React.Component {
</p>
</div>

<div className="syos-inline-stack__item">
<button
className="syos-button"
>
Select
</button>
<div className="syos-inline-stack__item" >
<ButtonTo {...seatSelectionForm} />
</div>
</div>
</footer>
Expand Down
7 changes: 7 additions & 0 deletions app/controllers/application_controller.rb
@@ -1,2 +1,9 @@
class ApplicationController < ActionController::Base
before_action do
cart_token = cookies[:cart_token]

Current.cart ||= Cart.create_or_find_by(token: cart_token)

cookies[:cart_token] ||= Current.cart.token
end
end
9 changes: 9 additions & 0 deletions app/controllers/selections_controller.rb
@@ -0,0 +1,9 @@
class SelectionsController < ApplicationController
def create
seat = Seat.find(params[:seat_id])

Current.cart.seat_selections.create_or_find_by(seat: seat)

redirect_to venue_floor_seats_url(seat.venue, seat.floor)
end
end
6 changes: 6 additions & 0 deletions app/models/cart.rb
@@ -0,0 +1,6 @@
class Cart < ApplicationRecord
has_secure_token

has_many :seat_selections
has_many :seats, through: :seat_selections
end
3 changes: 3 additions & 0 deletions app/models/current.rb
@@ -0,0 +1,3 @@
class Current < ActiveSupport::CurrentAttributes
attribute :cart
end
2 changes: 2 additions & 0 deletions app/models/seat.rb
@@ -1,5 +1,7 @@
class Seat < ApplicationRecord
belongs_to :section
has_one :floor, through: :section
has_one :venue, through: :floor

def self.find_by_row_number!(row_number)
row, number = row_number.split("-")
Expand Down
4 changes: 4 additions & 0 deletions app/models/seat_selection.rb
@@ -0,0 +1,4 @@
class SeatSelection < ApplicationRecord
belongs_to :seat
belongs_to :cart
end
5 changes: 5 additions & 0 deletions app/views/seats/_cart.json.props
@@ -0,0 +1,5 @@
json.array! Current.cart.seats do |seat|
json.id seat.id
json.row_number seat.row_number
json.price number_to_currency(seat.section.price / 100.0)
end
35 changes: 4 additions & 31 deletions app/views/seats/index.js
@@ -1,6 +1,7 @@
import React from 'react'
import Layout from '../../components/Layout'
import SeatDialog from '../../components/SeatDialog'
import Cart from '../../components/Cart'

const buildSectionElements = (sections) => {
return sections.map((section, index) => {
Expand All @@ -25,7 +26,8 @@ export default (props) => {
const {
venueName,
sections,
seat
cart,
seat,
} = props

const sectionElements = buildSectionElements(sections)
Expand Down Expand Up @@ -58,36 +60,7 @@ export default (props) => {
</div>

<div className="syos-frame__sidebar">
<div id="cart-summary">
<h2 className="syos-u-margin-bottom-2">
Your seat selections
</h2>

<p className="syos-u-font-size-small syos-u-margin-bottom-2">
Seats are not reserved until added to the cart.
</p>

<table className="syos-table">
<thead>
<tr>
<th>
Seat
</th>

<th className="syos-table__cell--numerals">
Price
</th>

<th className="visually-hidden">
Remove
</th>
</tr>
</thead>

<tbody>
</tbody>
</table>
</div>
<Cart cart={cart} />
</div>
</section>
</main>
Expand Down
3 changes: 3 additions & 0 deletions app/views/seats/index.json.props
Expand Up @@ -3,6 +3,9 @@ json.venue_name venue.name
json.sections(partial: ['sections', locals: local_assigns]) do
end

json.cart(partial: ['cart', locals: local_assigns]) do
end

json.seat do
json.show false
end
8 changes: 8 additions & 0 deletions app/views/seats/show.json.props
Expand Up @@ -3,9 +3,17 @@ json.venue_name venue.name
json.sections(partial: ['sections', locals: local_assigns]) do
end

json.cart(partial: ['cart', locals: local_assigns]) do
end

json.seat do
json.show true
json.section_name seat.section.name
json.row_number seat.row_number
json.price number_to_currency(seat.section.price / 100.0)
json.seat_selection_form do
form_props(url: seat_selections_path(seat)) do |f|
f.submit
end
end
end
4 changes: 4 additions & 0 deletions config/routes.rb
Expand Up @@ -6,5 +6,9 @@
end
end

resources :seats, only: [] do
resources :selections, only: [:create]
end

root to: redirect("/venues/benedum_center/floors/orchestra/seats")
end
10 changes: 10 additions & 0 deletions db/migrate/20190525190929_create_carts.rb
@@ -0,0 +1,10 @@
class CreateCarts < ActiveRecord::Migration[6.0]
def change
create_table :carts do |t|
t.string :token, null: false

t.timestamps
end
add_index :carts, :token, unique: true
end
end
12 changes: 12 additions & 0 deletions db/migrate/20190525190937_create_seat_selections.rb
@@ -0,0 +1,12 @@
class CreateSeatSelections < ActiveRecord::Migration[6.0]
def change
create_table :seat_selections do |t|
t.references :seat, null: false, foreign_key: true
t.references :cart, null: false, foreign_key: true

t.timestamps
end

add_index :seat_selections, [:seat_id, :cart_id], unique: true
end
end
21 changes: 20 additions & 1 deletion db/schema.rb
Expand Up @@ -10,10 +10,17 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2019_05_25_141044) do
ActiveRecord::Schema[7.0].define(version: 2019_05_25_190937) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

create_table "carts", force: :cascade do |t|
t.string "token", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["token"], name: "index_carts_on_token", unique: true
end

create_table "floors", force: :cascade do |t|
t.string "name", null: false
t.string "slug", null: false
Expand All @@ -24,6 +31,16 @@
t.index ["venue_id"], name: "index_floors_on_venue_id"
end

create_table "seat_selections", force: :cascade do |t|
t.bigint "seat_id", null: false
t.bigint "cart_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["cart_id"], name: "index_seat_selections_on_cart_id"
t.index ["seat_id", "cart_id"], name: "index_seat_selections_on_seat_id_and_cart_id", unique: true
t.index ["seat_id"], name: "index_seat_selections_on_seat_id"
end

create_table "seats", force: :cascade do |t|
t.bigint "section_id", null: false
t.string "row", null: false
Expand Down Expand Up @@ -57,6 +74,8 @@
end

add_foreign_key "floors", "venues"
add_foreign_key "seat_selections", "carts"
add_foreign_key "seat_selections", "seats"
add_foreign_key "seats", "sections"
add_foreign_key "sections", "floors"
end
24 changes: 24 additions & 0 deletions test/controllers/seats_controller_test.rb
@@ -0,0 +1,24 @@
require "test_helper"

class SeatsControllerTest < ActionDispatch::IntegrationTest
test "#index when a Cart exists, does not create a new one" do
venue = create(:benedum_center)
floor = create(:orchestra, venue: venue)
cart = create(:cart)

cookies[:cart_token] = cart.token
get venue_floor_seats_path(venue, floor)

assert_equal Cart.count, 1
assert_equal cookies[:cart_token], cart.token
end

test "#index when a Cart does not exist, creates a new one" do
venue = create(:benedum_center)
floor = create(:orchestra, venue: venue)

get venue_floor_seats_path(venue, floor)

assert_equal Cart.last.token, cookies[:cart_token]
end
end
22 changes: 22 additions & 0 deletions test/controllers/selections_controller_test.rb
@@ -0,0 +1,22 @@
require "test_helper"

class SelectionsControllerTest < ActionDispatch::IntegrationTest
test "#create when a Seat is not selected" do
seat = create(:seat)

post seat_selections_path(seat)

assert_equal SeatSelection.pluck(:seat_id), [seat.id]
end

test "#create when a Seat is already selected" do
seat_selection = create(:seat_selection)
cart = seat_selection.cart
seat = seat_selection.seat
cookies[:cart_token] = cart.token

post seat_selections_path(seat)

assert_equal cart.seats.ids, [seat.id]
end
end
8 changes: 8 additions & 0 deletions test/factories.rb
Expand Up @@ -19,6 +19,9 @@
price { 10_00 }
end

factory :cart do
end

factory :seat do
association :section

Expand All @@ -27,4 +30,9 @@
x { 0 }
y { 0 }
end

factory :seat_selection do
association(:cart)
association(:seat)
end
end
17 changes: 17 additions & 0 deletions test/system/visitor_selects_seat_test.rb
@@ -0,0 +1,17 @@
require "application_system_test_case"

class VisitorSelectsSeatTest < ApplicationSystemTestCase
test "visiting the seat page" do
venue = create(:benedum_center)
floor = create(:orchestra, venue: venue)
section = create(:section, floor: floor, price: 10_00)
seat = create(:seat, row: "AA", number: "101", section: section)

visit("/venues/benedum_center/floors/orchestra/seats/AA-101")
click_on("Select")

within("#cart-summary") do
assert_text "$10.00"
end
end
end

0 comments on commit 918fd12

Please sign in to comment.