diff --git a/.gitignore b/.gitignore index f92525c..1303e39 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ # Ignore master key for decrypting credentials and more. /config/master.key + +/app/assets/builds/* +!/app/assets/builds/.keep diff --git a/Gemfile b/Gemfile index c59db3d..ecec5b2 100644 --- a/Gemfile +++ b/Gemfile @@ -61,3 +61,7 @@ group :test do gem "capybara" gem "selenium-webdriver" end + +gem "tailwindcss-rails" + +gem "tailwindcss-ruby", "3.4.17" diff --git a/Gemfile.lock b/Gemfile.lock index 85d0279..3982875 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -317,6 +317,13 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.2) + tailwindcss-rails (3.3.1) + railties (>= 7.0.0) + tailwindcss-ruby (~> 3.0) + tailwindcss-ruby (3.4.17-aarch64-linux) + tailwindcss-ruby (3.4.17-arm-linux) + tailwindcss-ruby (3.4.17-arm64-darwin) + tailwindcss-ruby (3.4.17-x86_64-linux) thor (1.3.2) thruster (0.1.10) thruster (0.1.10-aarch64-linux) @@ -377,6 +384,8 @@ DEPENDENCIES solid_queue sqlite3 (>= 2.1) stimulus-rails + tailwindcss-rails + tailwindcss-ruby (= 3.4.17) thruster turbo-rails tzinfo-data diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000..da151fe --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,2 @@ +web: bin/rails server +css: bin/rails tailwindcss:watch diff --git a/app/assets/builds/.keep b/app/assets/builds/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css new file mode 100644 index 0000000..8666d2f --- /dev/null +++ b/app/assets/stylesheets/application.tailwind.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* + +@layer components { + .btn-primary { + @apply py-2 px-4 bg-blue-200; + } +} + +*/ diff --git a/app/controllers/appointments_controller.rb b/app/controllers/appointments_controller.rb index 5769f51..225c751 100644 --- a/app/controllers/appointments_controller.rb +++ b/app/controllers/appointments_controller.rb @@ -1,12 +1,36 @@ class AppointmentsController < ApplicationController - helper_method :appointments + helper_method :appointments, :appointment - def index + before_action :check_created_by, only: [:edit, :update] + + def update + if appointment.update(appointment_params) + redirect_to appointments_path, notice: "Appointment updated!" + else + render :edit, status: :unprocessable_entity + end end private + def check_created_by + unless appointment.created_by?(Current.user) + redirect_to appointments_path, alert: "Can't modify appointment - you are not the creator." + end + end + + def appointment_params + params.require(:appointment).permit( + :requested_datetime, + :notes + ) + end + + def appointment + @appointment ||= Appointment.find(params[:id]) + end + def appointments - @appointments ||= Appointment.all + @appointments ||= Appointment.includes(:created_by).all end end diff --git a/app/javascript/application.js b/app/javascript/application.js index 0d7b494..d6605c6 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,3 +1,7 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "@hotwired/turbo-rails" import "controllers" + +window.removeFlash = (closeButton) => { + closeButton.parentElement.remove(); +} diff --git a/app/models/appointment.rb b/app/models/appointment.rb index f0d75de..26fb918 100644 --- a/app/models/appointment.rb +++ b/app/models/appointment.rb @@ -2,4 +2,16 @@ class Appointment < ApplicationRecord belongs_to :created_by, foreign_key: :created_by_user_id, class_name: "User" validates :requested_datetime, presence: true + + after_save_commit -> { + broadcast_replace_to( + "appointments_list", + partial: "appointments/load_row", + locals: { appointment: self } + ) + } + + def created_by?(user) + created_by == user + end end diff --git a/app/views/appointments/_appointment.html.erb b/app/views/appointments/_appointment.html.erb index 1bb8f3d..f0eb703 100644 --- a/app/views/appointments/_appointment.html.erb +++ b/app/views/appointments/_appointment.html.erb @@ -1,5 +1,28 @@ -
- <%= appointment.requested_datetime %> - <%= appointment.notes %> - <%= appointment.created_by_user_id %> -
+<%= turbo_frame_tag(appointment) do %> +
+
+

+ Requested Time - <%= appointment.requested_datetime.strftime("%B %-d %Y at %I:%H%P") %> +

+
+

+ <%= truncate(appointment.notes, length: 40) %> +

+
+ <%= appointment.created_by.email_address %> + +
+
+
+
+
+ Actions + <% if appointment.created_by?(Current.user) %> + <%= link_to "Edit", edit_appointment_path(appointment), class: "text-xs text-indigo-500 hover:text-indigo-400", data: { turbo: false } %> + <% end %> +
+
+
+<% end %> diff --git a/app/views/appointments/_load_row.html.erb b/app/views/appointments/_load_row.html.erb new file mode 100644 index 0000000..0a77516 --- /dev/null +++ b/app/views/appointments/_load_row.html.erb @@ -0,0 +1 @@ +<%= turbo_frame_tag(appointment, src: row_appointment_path(appointment)) %> diff --git a/app/views/appointments/edit.html.erb b/app/views/appointments/edit.html.erb new file mode 100644 index 0000000..ec2d4c0 --- /dev/null +++ b/app/views/appointments/edit.html.erb @@ -0,0 +1,19 @@ +
+

Edit Appointment

+ + <%= form_for appointment, html: { class: "mt-12" } do |f| %> +
+
+ <%= f.label :requested_datetime, class: "font-semibold" %> + <%= f.datetime_field :requested_datetime %> +
+ +
+ <%= f.label :notes, class: "font-semibold" %> + <%= f.text_area :notes, cols: 40, rows: 10 %> +
+ + <%= f.submit class: "rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" %> +
+ <% end %> +
diff --git a/app/views/appointments/index.html.erb b/app/views/appointments/index.html.erb index 08e5eb6..fd9cb4f 100644 --- a/app/views/appointments/index.html.erb +++ b/app/views/appointments/index.html.erb @@ -1,3 +1,9 @@ -

Appointments

+<%= turbo_stream_from "appointments_list" %> -<%= render appointments, as: :appointment %> +
+

Appointments

+ +
+ <%= render appointments, as: :appointment %> +
+
diff --git a/app/views/appointments/row.html.erb b/app/views/appointments/row.html.erb new file mode 100644 index 0000000..41db675 --- /dev/null +++ b/app/views/appointments/row.html.erb @@ -0,0 +1 @@ +<%= render "appointment", appointment: %> diff --git a/app/views/layouts/_flash.html.erb b/app/views/layouts/_flash.html.erb new file mode 100644 index 0000000..34f07d6 --- /dev/null +++ b/app/views/layouts/_flash.html.erb @@ -0,0 +1,21 @@ +<% flash.each do |type, msg| %> + <% if type.to_s == "notice" %> +
+ <%= msg %> + +
+ <% else %> +
+ <%= msg %> + +
+ <% end %> +<% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 7c4c2af..9eda789 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -18,11 +18,15 @@ <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> - <%= yield %> +
+ <%= render "layouts/flash" %> + <%= yield %> +
diff --git a/bin/dev b/bin/dev index 5f91c20..ad72c7d 100755 --- a/bin/dev +++ b/bin/dev @@ -1,2 +1,16 @@ -#!/usr/bin/env ruby -exec "./bin/rails", "server", *ARGV +#!/usr/bin/env sh + +if ! gem list foreman -i --silent; then + echo "Installing foreman..." + gem install foreman +fi + +# Default to port 3000 if not specified +export PORT="${PORT:-3000}" + +# Let the debug gem allow remote connections, +# but avoid loading until `debugger` is called +export RUBY_DEBUG_OPEN="true" +export RUBY_DEBUG_LAZY="true" + +exec foreman start -f Procfile.dev "$@" diff --git a/config/routes.rb b/config/routes.rb index a6b64da..673bd13 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,11 @@ Rails.application.routes.draw do resource :session resources :passwords, param: :token - resources :appointments, only: :index + resources :appointments, only: [:index, :edit, :update] do + member do + get :row + end + end # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/config/tailwind.config.js b/config/tailwind.config.js new file mode 100644 index 0000000..d6ad82c --- /dev/null +++ b/config/tailwind.config.js @@ -0,0 +1,22 @@ +const defaultTheme = require('tailwindcss/defaultTheme') + +module.exports = { + content: [ + './public/*.html', + './app/helpers/**/*.rb', + './app/javascript/**/*.js', + './app/views/**/*.{erb,haml,html,slim}' + ], + theme: { + extend: { + fontFamily: { + sans: ['Inter var', ...defaultTheme.fontFamily.sans], + }, + }, + }, + plugins: [ + require('@tailwindcss/forms'), + require('@tailwindcss/typography'), + require('@tailwindcss/container-queries'), + ] +}