diff --git a/.env.test.example b/.env.test.example index ba0adffbc..574de2eff 100644 --- a/.env.test.example +++ b/.env.test.example @@ -2,3 +2,7 @@ MARKETPLACE_VENDOR_STRIPE_ACCOUNT=acct_GET_THIS_FROM_STRIPE MARKETPLACE_VENDOR_SQUARE_ACCESS_TOKEN=GET_THIS_FROM_SQUARE MARKETPLACE_VENDOR_SQUARE_LOCATION_ID=GET_THIS_FROM_SQUARE + +AWS_S3_ACCESS_KEY_ID="GET_THIS_FROM_AWS" +AWS_S3_SECRET_ACCESS_KEY="GET_THIS_FROM_AWS" +AWS_S3_BUCKET="GET_THIS_FROM_AWS" diff --git a/.github/workflows/test-convene-web.yml b/.github/workflows/test-convene-web.yml index 1b5149b5b..c2a9d46e0 100644 --- a/.github/workflows/test-convene-web.yml +++ b/.github/workflows/test-convene-web.yml @@ -142,88 +142,6 @@ jobs: name: rspec-failed-screenshot path: tmp/capybara/*.png - test-features: - name: Run Cucumber tests - runs-on: ubuntu-latest - needs: [setup] - - services: - postgres: - image: postgres:latest - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - ports: - - 5432:5432 - # needed because the postgres container does not provide a healthcheck - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - redis: - image: redis - ports: - # Maps port 6379 on service container to the host - - 6379:6379 - # Set health checks to wait until redis has started - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Ruby and install gems - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - name: Setup Node with cache - uses: actions/setup-node@v3 - with: - node-version: 18 - cache: 'yarn' - - - name: Install Firefox - uses: browser-actions/setup-firefox@latest - - - name: Allow Ruby process to access port 80 - run: sudo setcap 'cap_net_bind_service=+ep' `which ruby` - - - name: Setup CI database.yml - run: cp config/database.yml.github-actions config/database.yml - - - name: Use Development mode env - run: cp .env.example .env - - - name: Install Overmind - run: | - wget https://github.com/DarthSim/overmind/releases/download/v2.3.0/overmind-v2.3.0-linux-386.gz - gunzip -d overmind-v2.3.0-linux-386.gz - mv overmind-v2.3.0-linux-386 overmind - chmod +x overmind - - - name: Install Maildev - run: yarn global add maildev - - - name: Start Rails server - run: | - export PATH=$PATH:~/bin:. - bin/setup-rails - bin/run & - - - name: Run tests - run: | - # To wait for asset built - # TODO: Start server in production mode - curl --connect-timeout 5 --retry 5 --retry-delay 5 --retry-max-time 60 --retry-connrefused localhost:3000 1> /dev/null - yarn run test - - name: Upload Test Results - uses: actions/upload-artifact@v2 - if: failure() - with: - name: feature-test-failed-screenshot - path: features/test_reports/*.png - lint: name: Run style checks runs-on: ubuntu-latest diff --git a/Gemfile b/Gemfile index 6c22ac410..83f484dc7 100644 --- a/Gemfile +++ b/Gemfile @@ -47,7 +47,7 @@ gem "view_component", "~> 3.10" gem "rqrcode", "~> 2.2" # Pagination! -gem "pagy", "~> 6.3" +gem "pagy", "~> 6.4" # Database Layer # @@ -78,7 +78,7 @@ gem "pg", "~> 1.5" gem "image_processing" # Use S3 for file storage -gem "aws-sdk-s3", "~> 1.142", require: false +gem "aws-sdk-s3", "~> 1.143", require: false # Date/Time and Internationalization # # Windows does not include zoneinfo files, so bundle the tzinfo-data gem @@ -117,9 +117,9 @@ group :development, :test do # Our preferred testing library for Ruby and Rails projects gem "rails-controller-testing" - gem "rspec-rails", "~> 6.1.0" + gem "rspec-rails", "~> 6.1.1" gem "rswag-specs" - gem "shoulda-matchers", "~> 6.0" + gem "shoulda-matchers", "~> 6.1" gem "capybara" gem "selenium-webdriver" diff --git a/Gemfile.lock b/Gemfile.lock index 892c32377..94fc0aebe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,7 +90,7 @@ GEM tzinfo (~> 2.0) addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) - apimatic_core (0.3.1) + apimatic_core (0.3.4) apimatic_core_interfaces (~> 0.2.0) certifi (~> 2018.1, >= 2018.01.18) faraday-multipart (~> 1.0) @@ -109,17 +109,17 @@ GEM faraday-retry (~> 2.0) ast (2.4.2) aws-eventstream (1.3.0) - aws-partitions (1.873.0) - aws-sdk-core (3.190.1) + aws-partitions (1.883.0) + aws-sdk-core (3.191.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.75.0) - aws-sdk-core (~> 3, >= 3.188.0) + aws-sdk-kms (1.77.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.142.0) - aws-sdk-core (~> 3, >= 3.189.0) + aws-sdk-s3 (1.143.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) aws-sigv4 (1.8.0) @@ -130,7 +130,7 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.1.5) + bigdecimal (3.1.6) bindex (0.8.1) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) @@ -151,7 +151,7 @@ GEM choice (0.2.0) chunky_png (1.4.0) coderay (1.1.3) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) connection_pool (2.4.1) crack (0.4.5) rexml @@ -180,22 +180,21 @@ GEM factory_bot_rails (6.4.3) factory_bot (~> 6.4) railties (>= 5.0.0) - faker (3.2.2) + faker (3.2.3) i18n (>= 1.8.11, < 2) - faraday (2.7.12) - base64 - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) faraday-follow_redirects (0.3.0) faraday (>= 1, < 3) faraday-gzip (1.0.0) faraday (>= 1.0) zlib (~> 2.1) - faraday-http-cache (2.5.0) + faraday-http-cache (2.5.1) faraday (>= 0.8) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (3.0.2) + faraday-net_http (3.1.0) + net-http faraday-net_http_persistent (2.1.0) faraday (~> 2.5) net-http-persistent (~> 4.0) @@ -219,7 +218,7 @@ GEM image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - io-console (0.7.1) + io-console (0.7.2) irb (1.11.1) rdoc reline (>= 0.4.2) @@ -227,7 +226,7 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) - jsbundling-rails (1.2.2) + jsbundling-rails (1.3.0) railties (>= 6.0.0) json (2.7.1) json-pointer (0.0.1) @@ -265,7 +264,7 @@ GEM mini_magick (4.12.0) mini_mime (1.1.5) mini_portile2 (2.8.5) - minitest (5.20.0) + minitest (5.21.2) monetize (1.12.0) money (~> 6.12) money (6.16.0) @@ -278,6 +277,8 @@ GEM msgpack (1.7.2) multipart-post (2.3.0) mutex_m (0.2.0) + net-http (0.4.1) + uri net-http-persistent (4.0.2) connection_pool (~> 2.2) net-imap (0.4.1) @@ -293,7 +294,7 @@ GEM nokogiri (1.16.0) mini_portile2 (~> 2.8.2) racc (~> 1.4) - pagy (6.3.0) + pagy (6.4.3) parallel (1.24.0) parser (3.3.0.0) ast (~> 2.4.1) @@ -369,7 +370,7 @@ GEM rdoc (6.6.2) psych (>= 4.0.0) redcarpet (3.6.0) - redis-client (0.18.0) + redis-client (0.19.1) connection_pool regexp_parser (2.8.3) reline (0.4.2) @@ -389,7 +390,7 @@ GEM rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.1.0) + rspec-rails (6.1.1) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -445,7 +446,8 @@ GEM ffi (~> 1.12) ruby2_keywords (0.0.5) rubyzip (2.3.2) - selenium-webdriver (4.16.0) + selenium-webdriver (4.17.0) + base64 (~> 0.2) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -454,13 +456,13 @@ GEM sentry-ruby (~> 5.16.1) sentry-ruby (5.16.1) concurrent-ruby (~> 1.0, >= 1.0.2) - shoulda-matchers (6.0.0) + shoulda-matchers (6.1.0) activesupport (>= 5.2.0) - sidekiq (7.2.0) + sidekiq (7.2.1) concurrent-ruby (< 2) connection_pool (>= 2.3.0) rack (>= 2.2.4) - redis-client (>= 0.14.0) + redis-client (>= 0.19.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -475,7 +477,7 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - square.rb (34.1.0.20231213) + square.rb (35.0.1.20240118) apimatic_core (~> 0.3.0) apimatic_core_interfaces (~> 0.2.0) apimatic_faraday_client_adapter (~> 0.1.0) @@ -508,6 +510,7 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) + uri (0.13.0) view_component (3.10.0) activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) @@ -538,7 +541,7 @@ PLATFORMS DEPENDENCIES active_record_extended (~> 3.2) activerecord-postgres_enum (~> 2.0) - aws-sdk-s3 (~> 1.142) + aws-sdk-s3 (~> 1.143) bcrypt (~> 3.1.20) better_errors binding_of_caller @@ -560,7 +563,7 @@ DEPENDENCIES lockbox (= 1.3.2) lookbook (>= 2.0.0.beta.4) money-rails - pagy (~> 6.3) + pagy (~> 6.4) pg (~> 1.5) pry-byebug puma (~> 6.4) @@ -572,7 +575,7 @@ DEPENDENCIES redcarpet (~> 3.6) rotp (~> 6.3) rqrcode (~> 2.2) - rspec-rails (~> 6.1.0) + rspec-rails (~> 6.1.1) rswag-api rswag-specs rswag-ui @@ -581,7 +584,7 @@ DEPENDENCIES selenium-webdriver sentry-rails sentry-ruby - shoulda-matchers (~> 6.0) + shoulda-matchers (~> 6.1) sidekiq simplecov spring diff --git a/app/components/card_component.html.erb b/app/components/card_component.html.erb index 9098fecfa..dd83e6f4f 100644 --- a/app/components/card_component.html.erb +++ b/app/components/card_component.html.erb @@ -1,15 +1,17 @@ -
+
> + <% if header? %> + <%= header%> + <%- end %> <%# NOTE: content? is not always working as described, and is returning a proc in some cases rather than a boolean %> <% if content.present? %> -
> +
<%= content %>
<% end %> + <% if footer? %> - + <%= footer %> <% end %>
diff --git a/app/components/card_component.rb b/app/components/card_component.rb index 9e55bf761..c305f9f8a 100644 --- a/app/components/card_component.rb +++ b/app/components/card_component.rb @@ -1,32 +1,17 @@ class CardComponent < ApplicationComponent - renders_one :footer + HEADER_VARIANTS = {default: "p-2 sm:p-4", no_padding: ""} + renders_one :header, ->(variant: :default, &block) { + content_tag(:header, class: HEADER_VARIANTS.fetch(variant), &block) + } - private - - def card_classes_content - [ - "p-4", - "sm:p-6" - ].compact.join(" ") - end - - def card_classes_wrapper - [ - "shadow", - "rounded-lg", - "h-full", - "bg-white", - "group-hover:bg-slate-50" - ].compact.join(" ") - end - - def card_classes_footer - [ - "bg-orange-50", - "p-4", - "sm:p-6", - # content? is not always working as described, and is returning a proc in some cases rather than a boolean - ("rounded-t-none" if content.blank?) - ].compact.join(" ") - end + DEFAULT_FOOTER = "bg-slate-50 p-2 sm:p-4" + FOOTER_VARIANTS = { + default: DEFAULT_FOOTER, + action_bar: [DEFAULT_FOOTER, "flex flex-row justify-between"].join(" ") + } + renders_one :footer, ->(variant: :default, &block) { + classes = FOOTER_VARIANTS.fetch(variant) + classes += " rounded-t-none" unless content? || header? + content_tag(:footer, class: classes, &block) + } end diff --git a/app/components/marketplace/stripe_overview_component/stripe_overview_component.html.erb b/app/components/marketplace/stripe_overview_component/stripe_overview_component.html.erb index d66281c94..f0dac4f63 100644 --- a/app/components/marketplace/stripe_overview_component/stripe_overview_component.html.erb +++ b/app/components/marketplace/stripe_overview_component/stripe_overview_component.html.erb @@ -1,22 +1,23 @@ -<%= render CardComponent.new(dom_id: "stripe_overview", classes: "flex flex-col h-full -") do %> +<%= render CardComponent.new(dom_id: "stripe_overview") do |card| %> <%- if marketplace.stripe_api_key? %> -
- <%= marketplace_stripe_utility.name %> -
-
+ <%- card.with_header do %> +

<%= marketplace_stripe_utility.name %>

+ <%- end %> + + <%- card.with_footer(variant: :action_bar) do %> <%= render ButtonComponent.new( - label: "View #{t('marketplace.stripe_accounts.show.link_to')}", - title: "View #{t('marketplace.stripe_accounts.show.link_to')}", - href: marketplace.location(:index, child: :stripe_account), - method: :get, - scheme: :secondary) %> -
+ label: "View #{t('marketplace.stripe_accounts.show.link_to')}", + title: "View #{t('marketplace.stripe_accounts.show.link_to')}", + href: marketplace.location(:index, child: :stripe_account), + method: :get, + scheme: :secondary) %> + <%- end %> <%- else %> -
To start accepting payments, add your Stripe Account.
-
+ <%- card.with_header do %> +

To start accepting payments, add your Stripe Account.

+ <%- end %> + + <%- card.with_footer(variant: :action_bar) do %> <%= render ButtonComponent.new( label: "Add #{t('marketplace.stripe_accounts.show.link_to')}", title: "Add #{t('marketplace.stripe_accounts.show.link_to')}", @@ -25,6 +26,6 @@ scheme: :secondary ) %> -
+ <%- end %> <%- end %> <%- end %> diff --git a/app/controllers/rooms_controller.rb b/app/controllers/rooms_controller.rb index adf5a983e..9ea5ac400 100644 --- a/app/controllers/rooms_controller.rb +++ b/app/controllers/rooms_controller.rb @@ -25,7 +25,17 @@ def create def update respond_to do |format| - if room.update(room_params) + # TODO: Move logic for Media resource management to a dedicated controller (see: https://github.com/zinc-collective/convene/pull/2101/files#r1464115624) + new_media = Media.create + new_media.upload.attach(room_params[:hero_image_upload]) + room_params_for_update = {}.merge( + room_params.except(:hero_image_upload), + { + hero_image: new_media + } + ) + + if room.update(room_params_for_update) format.html do redirect_to [:edit, room.space], notice: t(".success", room_name: room.name) end diff --git a/app/furniture/marketplace/cart_product.rb b/app/furniture/marketplace/cart_product.rb index 61b585f29..dce1b0e2b 100644 --- a/app/furniture/marketplace/cart_product.rb +++ b/app/furniture/marketplace/cart_product.rb @@ -25,6 +25,10 @@ def price_total product.price * quantity end + def quantity_picker + QuantityPicker.new(cart_product: self) + end + private def editable_cart diff --git a/app/furniture/marketplace/cart_product/quantity_picker.rb b/app/furniture/marketplace/cart_product/quantity_picker.rb new file mode 100644 index 000000000..6a454472b --- /dev/null +++ b/app/furniture/marketplace/cart_product/quantity_picker.rb @@ -0,0 +1,6 @@ +class Marketplace + class CartProduct::QuantityPicker < Model + attr_accessor :cart_product + delegate :location, :quantity, to: :cart_product + end +end diff --git a/app/furniture/marketplace/cart_product/quantity_pickers/_quantity_picker.html.erb b/app/furniture/marketplace/cart_product/quantity_pickers/_quantity_picker.html.erb new file mode 100644 index 000000000..1993d6eaf --- /dev/null +++ b/app/furniture/marketplace/cart_product/quantity_pickers/_quantity_picker.html.erb @@ -0,0 +1,13 @@ +
+ <%- if quantity_picker.quantity < 1%> + + <%- elsif quantity_picker.quantity == 1 %> + <%= button_to("🗑️", quantity_picker.location, method: :delete) %> + <%- else %> + <%= button_to("➖", quantity_picker.location, method: :put, params: { cart_product: { quantity: quantity_picker.quantity - 1 } }) %> + <%- end %> + + <%= quantity_picker.quantity %> + + <%= button_to("➕", quantity_picker.location, method: :put, params: { cart_product: { quantity: quantity_picker.quantity + 1 } }) %> +
diff --git a/app/furniture/marketplace/cart_product_component.html.erb b/app/furniture/marketplace/cart_product_component.html.erb index 8af527a27..a0cae15e0 100644 --- a/app/furniture/marketplace/cart_product_component.html.erb +++ b/app/furniture/marketplace/cart_product_component.html.erb @@ -1,16 +1,7 @@ - <%- if product.photo.present? %> -
- <%= image_tag product.photo.variant(resize_to_limit: [150, 150]).processed.url, class: "mx-auto h-40 overflow-hidden object-center rounded-lg" - %> -
- <%=product.name%> -
-
- <%- else %> + <%= product.name %> - <%- end %>
<%= product.class.human_attribute_name(:price) %>
@@ -27,15 +18,10 @@ <%= humanized_money_with_symbol(product.price) %> -
- <%= render "buttons/minus", title: t('marketplace.cart_product_component.remove'), method: remove_method, - disabled: cart_product.quantity.zero?, - href: remove_href %> - - <%= quantity %> - - <%= render "buttons/plus", method: add_method, title: t('marketplace.cart_product_component.add'), - href: add_href %> -
+ <%- if cart_product.quantity.zero? %> + <%= button_to("Add to Cart", cart.location(child: :cart_products), method: :post, params: { cart_product: { product_id: product.id, quantity: 1 } }) %> + <%- else %> + <%= render cart_product.quantity_picker %> + <%- end %> diff --git a/app/furniture/marketplace/cart_product_component.rb b/app/furniture/marketplace/cart_product_component.rb index d89c9ed64..06df98d22 100644 --- a/app/furniture/marketplace/cart_product_component.rb +++ b/app/furniture/marketplace/cart_product_component.rb @@ -1,7 +1,7 @@ class Marketplace class CartProductComponent < ApplicationComponent attr_accessor :cart_product - delegate :name, :description, :location, to: :cart_product + delegate :name, :description, :quantity, :location, to: :cart_product delegate :cart, :product, to: :cart_product def initialize(cart_product:, **kwargs) @@ -10,36 +10,8 @@ def initialize(cart_product:, **kwargs) self.cart_product = cart_product end - def quantity - cart_product.destroyed? ? 0 : cart_product.quantity - end - - def add_quantity - quantity + 1 - end - - def add_method - (add_quantity == 1) ? :post : :put - end - - def add_href - cart_product.location(query_params: {cart_product: {quantity: add_quantity, product_id: product.id}}) - end - - def remove_quantity - [quantity - 1, 0].max - end - - def remove_method - remove_quantity.zero? ? :delete : :put - end - - def remove_href - cart_product.location(query_params: {cart_product: {quantity: remove_quantity, product_id: product.id}}) - end - def dom_id - super(product).gsub("product", "cart_product") + super(cart_product) end end end diff --git a/app/furniture/marketplace/cart_products_controller.rb b/app/furniture/marketplace/cart_products_controller.rb index 363cf57f2..65567b73e 100644 --- a/app/furniture/marketplace/cart_products_controller.rb +++ b/app/furniture/marketplace/cart_products_controller.rb @@ -6,84 +6,47 @@ class CartProductsController < Controller def create authorize(cart_product).save - respond_to do |format| - format.html do - if cart_product.errors.empty? - flash[:notice] = t(".success", - product: cart_product.product.name.pluralize(cart_product.quantity), - quantity: cart_product.quantity) - else - flash[:alert] = t(".failure", - product: cart_product.product.name.pluralize(cart_product.quantity), - quantity: cart_product.quantity) - end - - redirect_to [marketplace.space, marketplace.room] - end - - format.turbo_stream do - render turbo_stream: [ - turbo_stream.replace(cart_product_component.dom_id, cart_product_component), - turbo_stream.replace("cart-footer-#{cart.id}", - partial: "marketplace/carts/footer", locals: {cart: cart}), - turbo_stream.replace("cart-total-#{cart.id}", partial: "marketplace/carts/total", locals: {cart: cart}) - ] - end + if cart_product.errors.empty? + flash[:notice] = t(".success", + product: cart_product.product.name.pluralize(cart_product.quantity), + quantity: cart_product.quantity) + else + flash[:alert] = t(".failure", + product: cart_product.product.name.pluralize(cart_product.quantity), + quantity: cart_product.quantity) end + + redirect_to marketplace.location end def update authorize(cart_product).update(cart_product_params) - respond_to do |format| - format.html do - if cart_product.errors.empty? - flash[:notice] = - t(".success", product: cart_product.product.name.pluralize(cart_product.quantity), - quantity: cart_product.quantity) - else - flash[:alert] = - t(".failure", product: cart_product.product.name.pluralize(cart_product.quantity), - quantity: cart_product.quantity) - end - - redirect_to [marketplace.space, marketplace.room] - end - format.turbo_stream do - render turbo_stream: [ - turbo_stream.replace(cart_product_component.dom_id, cart_product_component), - turbo_stream.replace("cart-footer-#{cart.id}", - partial: "marketplace/carts/footer", locals: {cart: cart}), - turbo_stream.replace("cart-total-#{cart.id}", partial: "marketplace/carts/total", locals: {cart: cart}) - ] - end + if cart_product.errors.empty? + flash[:notice] = + t(".success", product: cart_product.product.name.pluralize(cart_product.quantity), + quantity: cart_product.quantity) + else + flash[:alert] = + t(".failure", product: cart_product.product.name.pluralize(cart_product.quantity), + quantity: cart_product.quantity) end + + redirect_to marketplace.location end def destroy authorize(cart_product).destroy - respond_to do |format| - format.html do - if cart_product.destroyed? - flash[:notice] = - t(".success", product: cart_product.product.name.pluralize(cart_product.quantity), - quantity: cart_product.quantity) - else - flash[:alert] = - t(".failure", product: cart_product.product.name.pluralize(cart_product.quantity), - quantity: cart_product.quantity) - end - redirect_to [marketplace.space, marketplace.room] - end - format.turbo_stream do - render turbo_stream: [ - turbo_stream.replace(cart_product_component.dom_id, cart_product_component), - turbo_stream.replace("cart-footer-#{cart.id}", - partial: "marketplace/carts/footer", locals: {cart: cart}), - turbo_stream.replace("cart-total-#{cart.id}", partial: "marketplace/carts/total", locals: {cart: cart}) - ] - end + if cart_product.destroyed? + flash[:notice] = + t(".success", product: cart_product.product.name.pluralize(cart_product.quantity), + quantity: cart_product.quantity) + else + flash[:alert] = + t(".failure", product: cart_product.product.name.pluralize(cart_product.quantity), + quantity: cart_product.quantity) end + redirect_to marketplace.location end def cart_product_component diff --git a/app/furniture/marketplace/carts/_cart.html.erb b/app/furniture/marketplace/carts/_cart.html.erb index 3142a710c..002c8fe9f 100644 --- a/app/furniture/marketplace/carts/_cart.html.erb +++ b/app/furniture/marketplace/carts/_cart.html.erb @@ -16,8 +16,8 @@ - <%- cart.marketplace.products.unarchived.each do |product| %> - <%= render Marketplace::CartProductComponent.new(cart_product: cart.cart_products.find_or_initialize_by(product: product)) %> + <%- cart.cart_products.order(created_at: :desc).each do |cart_product| %> + <%= render Marketplace::CartProductComponent.new(cart_product:) %> <%- end %> <%= render "marketplace/carts/footer", cart: cart %> diff --git a/app/furniture/marketplace/delivery_area_component.html.erb b/app/furniture/marketplace/delivery_area_component.html.erb index fb1e5e7f7..10b2363d7 100644 --- a/app/furniture/marketplace/delivery_area_component.html.erb +++ b/app/furniture/marketplace/delivery_area_component.html.erb @@ -1,11 +1,14 @@ -<%= render CardComponent.new(dom_id: dom_id(delivery_area), classes: "flex flex-col justify-between gap-y-2 w-full") do %> -
- <%= label %> +<%= render CardComponent.new(dom_id: dom_id(delivery_area)) do |card| %> + <%- card.with_header do %> +

+ <%= label %> +

+ <%- if delivery_area.archived? %> - (archived) +

(archived)

<%- end %> -
+ <%- end %>
@@ -17,9 +20,9 @@
-
+ <%- card.with_footer(variant: :action_bar) do %> <%= render edit_button if edit_button? %> <%= render archive_button if archive_button? %> <%= render destroy_button if destroy_button? %> -
+ <%- end %> <%- end %> diff --git a/app/furniture/marketplace/locales/en.yml b/app/furniture/marketplace/locales/en.yml index c98027191..7c0fe553d 100644 --- a/app/furniture/marketplace/locales/en.yml +++ b/app/furniture/marketplace/locales/en.yml @@ -124,9 +124,6 @@ en: update: success: "Changed %{quantity} %{product} on Cart" failure: "Could not change %{quantity} %{product} on Cart" - cart_product_component: - remove: Remove from Cart - add: Add to Cart payment_settings: index: link_to: "Payment Settings" diff --git a/app/furniture/marketplace/marketplace_component.html.erb b/app/furniture/marketplace/marketplace_component.html.erb index 836b9e305..ecdc2adaf 100644 --- a/app/furniture/marketplace/marketplace_component.html.erb +++ b/app/furniture/marketplace/marketplace_component.html.erb @@ -1,5 +1,9 @@
+ + <%= render delivery_area_component %> + <%= render Marketplace::MenuComponent.new(marketplace:, cart:) %> + <%= render cart %>
diff --git a/app/furniture/marketplace/menu/product_component.html.erb b/app/furniture/marketplace/menu/product_component.html.erb new file mode 100644 index 000000000..73f2793b1 --- /dev/null +++ b/app/furniture/marketplace/menu/product_component.html.erb @@ -0,0 +1,32 @@ +<%= render CardComponent.new(dom_id: dom_id(product)) do |card| %> + <%- card.with_header(variant: :no_padding) do %> + <% if product.photo.present? %> +
+ <%= image_tag hero_image, class: "rounded-t-lg w-full" %> +
+

<%= name %>

+
+
+ <%- else %> +

<%= name %>

+ <% end %> + <%- end %> + +
+ <%= description %> +
+ +
+

<%= price %>

+
+ + <%- card.with_footer do %> + <%- cart_product = cart.cart_products.find_by(product:) %> + + <%- if !cart_product %> + <%= button_to("Add to Cart", cart.location(child: :cart_products), method: :post, params: { cart_product: { product_id: product.id, quantity: 1 } }, class: "w-full --secondary") %> + <%- else %> + <%= render cart_product.quantity_picker %> + <%- end %> + <%- end %> +<%- end %> diff --git a/app/furniture/marketplace/menu/product_component.rb b/app/furniture/marketplace/menu/product_component.rb new file mode 100644 index 000000000..982f19e34 --- /dev/null +++ b/app/furniture/marketplace/menu/product_component.rb @@ -0,0 +1,9 @@ +class Marketplace + class Menu::ProductComponent < ProductComponent + attr_accessor :cart + def initialize(product:, cart:, **kwargs) + super(product:, **kwargs) + self.cart = cart + end + end +end diff --git a/app/furniture/marketplace/menu_component.html.erb b/app/furniture/marketplace/menu_component.html.erb new file mode 100644 index 000000000..0973effae --- /dev/null +++ b/app/furniture/marketplace/menu_component.html.erb @@ -0,0 +1,5 @@ +
+ <%- marketplace.products.unarchived.each do |product| %> + <%= render Marketplace::Menu::ProductComponent.new(product:, cart:)%> + <%- end %> +
diff --git a/app/furniture/marketplace/menu_component.rb b/app/furniture/marketplace/menu_component.rb new file mode 100644 index 000000000..7b913ad89 --- /dev/null +++ b/app/furniture/marketplace/menu_component.rb @@ -0,0 +1,11 @@ +class Marketplace + class MenuComponent < ApplicationComponent + attr_accessor :marketplace, :cart + + def initialize(marketplace:, cart:, **kwargs) + super(**kwargs) + self.marketplace = marketplace + self.cart = cart + end + end +end diff --git a/app/furniture/marketplace/notification_method_component.html.erb b/app/furniture/marketplace/notification_method_component.html.erb index 0be40c98a..3efd379aa 100644 --- a/app/furniture/marketplace/notification_method_component.html.erb +++ b/app/furniture/marketplace/notification_method_component.html.erb @@ -1,9 +1,10 @@ -<%= render CardComponent.new(dom_id: dom_id(notification_method), classes: "flex flex-col justify-between gap-y-2 w-full") do %> -
- <%= contact_location %> -
-
+<%= render CardComponent.new(dom_id: dom_id(notification_method)) do |card| %> + <%- card.with_header do %> +

<%= contact_location %>

+ <%- end %> + + <%- card.with_footer(variant: :action_bar) do %> <%= render edit_button if edit_button? %> <%= render destroy_button if destroy_button? %> -
+ <%- end %> <%- end %> diff --git a/app/furniture/marketplace/payment_settings/index.html.erb b/app/furniture/marketplace/payment_settings/index.html.erb index 458aed87e..bcb30cf17 100644 --- a/app/furniture/marketplace/payment_settings/index.html.erb +++ b/app/furniture/marketplace/payment_settings/index.html.erb @@ -3,20 +3,21 @@
<%= render Marketplace::StripeOverviewComponent.new(marketplace: marketplace) %> - <%= render CardComponent.new(classes: "flex flex-col h-full") do %> -
- Did we miss something? -
-
- Let us know about other payment service options that will help your marketplace to thrive. -
-
+ <%= render CardComponent.new do |card| %> + <%- card.with_header do %> +

Did we miss something?

+ <%- end %> + +

Let us know about other payment service options that will help your + marketplace to thrive.

+ + <%- card.with_footer(variant: :action_bar) do %> <%= render ButtonComponent.new( label: "Contact Us", title: "Contact Us", href: "mailto:#{ApplicationMailer::DEFAULT_FROM}", scheme: :secondary) %> -
+ <%-end %> <% end %>
diff --git a/app/furniture/marketplace/product.rb b/app/furniture/marketplace/product.rb index b91b7401b..03b4c81a8 100644 --- a/app/furniture/marketplace/product.rb +++ b/app/furniture/marketplace/product.rb @@ -4,6 +4,7 @@ class Marketplace class Product < Record include Archivable + # TODO: Refactor to use Media model has_one_attached :photo, dependent: :destroy self.table_name = "marketplace_products" diff --git a/app/furniture/marketplace/product_component.html.erb b/app/furniture/marketplace/product_component.html.erb index 3e58768db..08a966de5 100644 --- a/app/furniture/marketplace/product_component.html.erb +++ b/app/furniture/marketplace/product_component.html.erb @@ -1,32 +1,34 @@ -<%= render CardComponent.new(dom_id: dom_id(product), classes: "flex flex-col justify-between max-w-prose") do %> -
-

<%= name %>

- <%- if product.archived? %> - (archived) - <%- end %> -
- -
+<%= render CardComponent.new(dom_id: dom_id(product)) do |card| %> + <%- card.with_header(variant: :no_padding) do %> <% if product.photo.present? %> -
- <%= image_tag product.photo.variant(resize_to_limit: [150, 150]) %> -
+
+ <%= image_tag hero_image, class: "rounded-t-lg w-full" %> +
+

<%= name %>

+ <%- if product.archived? %> + (archived) + <%- end %> +
+
+ <%- else %> +

<%= name %>

<% end %> -
- <%= description %> -
+ <%- end %> + +
+ <%= description %> +
-
-
- <%= tax_rates %> -
-

<%= price %>

+
+
+ <%= tax_rates %>
+

<%= price %>

-
+ <%- card.with_footer(variant: :action_bar) do %> <%= render edit_button if edit_button? %> <%= render archive_button if archive_button? %> <%= render destroy_button if destroy_button? %> -
+ <%- end %> <%- end %> diff --git a/app/furniture/marketplace/product_component.rb b/app/furniture/marketplace/product_component.rb index f9e84b0ac..0463c7aa0 100644 --- a/app/furniture/marketplace/product_component.rb +++ b/app/furniture/marketplace/product_component.rb @@ -13,6 +13,14 @@ def edit_button super(title: t("marketplace.products.edit.link_to", name: name), href: location(:edit)) end + # 16:9 of 1290 is 1290:725.625 + # We rounded up. + # @see https://www.ios-resolution.com/ + FULL_WIDTH_16_BY_9 = [1290, 726] + def hero_image + product.photo.variant(resize_to_fill: FULL_WIDTH_16_BY_9) + end + def tax_rates product.tax_rates.map do |tax_rate| "#{tax_rate.label} #{helpers.number_to_percentage(tax_rate.tax_rate, precision: 2)}" diff --git a/app/furniture/marketplace/tax_rate_component.html.erb b/app/furniture/marketplace/tax_rate_component.html.erb index 4cec26d3e..516f974d0 100644 --- a/app/furniture/marketplace/tax_rate_component.html.erb +++ b/app/furniture/marketplace/tax_rate_component.html.erb @@ -1,12 +1,14 @@ -<%= render CardComponent.new(dom_id: dom_id(tax_rate), classes: "flex flex-col justify-between gap-y-2 w-full") do %> -
- <%= label %> -
+<%= render CardComponent.new(dom_id: dom_id(tax_rate)) do |card| %> + <%- card.with_header do |card| %> +

<%= label %>

+ <%- end %> +
<%= rate %>
-
+ + <%- card.with_footer(variant: :action_bar) do %> <%= render edit_button if edit_button? %> <%= render destroy_button if destroy_button? %> -
+ <%- end %> <%- end %> diff --git a/app/models/media.rb b/app/models/media.rb new file mode 100644 index 000000000..3d5ccce42 --- /dev/null +++ b/app/models/media.rb @@ -0,0 +1,7 @@ +# The Media resource manages file uploads to the platform +class Media < ApplicationRecord + # NOTE: Dependent destroy is defaulted, but when it becomes important to + # separate the destroy request and purge operations, let's add the + # `dependent: :purge_later` option. + has_one_attached :upload +end diff --git a/app/models/room.rb b/app/models/room.rb index 0b92eced1..4794a282a 100644 --- a/app/models/room.rb +++ b/app/models/room.rb @@ -1,5 +1,7 @@ # A Room in Convene acts as a gathering place. class Room < ApplicationRecord + belongs_to :hero_image, class_name: "Media", optional: true + # The space whose settings govern the default publicity and access controls for the Room. belongs_to :space, inverse_of: :rooms location(parent: :space) diff --git a/app/policies/room_policy.rb b/app/policies/room_policy.rb index f212df4be..67c721eb0 100644 --- a/app/policies/room_policy.rb +++ b/app/policies/room_policy.rb @@ -19,7 +19,7 @@ def create? alias_method :new?, :create? def permitted_attributes(params) - [:access_level, :name, :description, :slug, gizmos_attributes: + [:access_level, :name, :description, :slug, :hero_image_upload, gizmos_attributes: policy(Furniture).permitted_attributes(params)] end diff --git a/app/views/application/_file_field.html.erb b/app/views/application/_file_field.html.erb index f9f623854..63f51e28e 100644 --- a/app/views/application/_file_field.html.erb +++ b/app/views/application/_file_field.html.erb @@ -1,6 +1,11 @@ <% required ||= required | false %>
- <%= form.label attribute %> + <%= form.label local_assigns[:label] || attribute %> + <% if local_assigns[:label_hint] %> +
+ <%= label_hint %> +
+ <% end %> <%= form.file_field attribute, required: required %> <%= render partial: "error", locals: { model: form.object, attribute: attribute } %>
diff --git a/app/views/rooms/_form.html.erb b/app/views/rooms/_form.html.erb index fb3c404fc..478af73ff 100644 --- a/app/views/rooms/_form.html.erb +++ b/app/views/rooms/_form.html.erb @@ -3,6 +3,18 @@ <%= render "text_field", attribute: :name, form: room_form %> <%= render "text_field", attribute: :slug, form: room_form %> <%= render "text_area", attribute: :description, form: room_form, label_hint: 'Add brief summary of your section for search engines to display as snippets in results' %> + <%- if room.hero_image&.upload&.attached? %> +
+ <%= image_tag room.hero_image&.upload %> +
+ <% end %> + <%= render "file_field", + attribute: :hero_image_upload, + form: room_form, + label: "Upload a header image", + label_hint: "Images are great for marketing!", + required: false + %>

Privacy and Security

<%= render "radio_group", attribute: :access_level, options: Room::access_levels.values, form: room_form %>