kaze 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kaze/commands/install_command.rb +22 -5
  3. data/lib/kaze/commands/installs_hotwire_stack.rb +62 -0
  4. data/lib/kaze/commands/{install_inertia_stacks.rb → installs_inertia_stacks.rb} +19 -17
  5. data/lib/kaze/version.rb +1 -1
  6. data/stubs/default/app/validators/lowercase_validator.rb +1 -0
  7. data/stubs/default/app/views/layouts/mailer.html.erb +3 -1
  8. data/stubs/default/app/views/layouts/mailer.text.erb +3 -1
  9. data/stubs/default/config/routes.rb +1 -1
  10. data/stubs/hotwire/Procfile.dev +2 -0
  11. data/stubs/hotwire/app/components/application_logo_component.rb +14 -0
  12. data/stubs/hotwire/app/components/auth_session_status_component.rb +17 -0
  13. data/stubs/hotwire/app/components/danger_button_component.rb +13 -0
  14. data/stubs/hotwire/app/components/dropdown_component.html.erb +20 -0
  15. data/stubs/hotwire/app/components/dropdown_component.rb +15 -0
  16. data/stubs/hotwire/app/components/dropdown_link_component.rb +12 -0
  17. data/stubs/hotwire/app/components/input_error_component.rb +18 -0
  18. data/stubs/hotwire/app/components/input_label_component.rb +13 -0
  19. data/stubs/hotwire/app/components/modal_component.html.erb +62 -0
  20. data/stubs/hotwire/app/components/modal_component.rb +14 -0
  21. data/stubs/hotwire/app/components/nav_link_component.rb +15 -0
  22. data/stubs/hotwire/app/components/primary_button_component.rb +13 -0
  23. data/stubs/hotwire/app/components/responsive_nav_link_component.rb +15 -0
  24. data/stubs/hotwire/app/components/secondary_button_component.rb +13 -0
  25. data/stubs/hotwire/app/components/text_input_component.rb +11 -0
  26. data/stubs/hotwire/app/controllers/application_controller.rb +3 -0
  27. data/stubs/hotwire/app/controllers/auth/authenticated_session_controller.rb +29 -0
  28. data/stubs/hotwire/app/controllers/auth/new_password_controller.rb +19 -0
  29. data/stubs/hotwire/app/controllers/auth/password_reset_link_controller.rb +19 -0
  30. data/stubs/hotwire/app/controllers/auth/registered_user_controller.rb +23 -0
  31. data/stubs/hotwire/app/controllers/dashboard_controller.rb +4 -0
  32. data/stubs/hotwire/app/controllers/password_controller.rb +11 -0
  33. data/stubs/hotwire/app/controllers/profile_controller.rb +33 -0
  34. data/stubs/hotwire/app/controllers/welcome_controller.rb +7 -0
  35. data/stubs/hotwire/app/javascript/alpinejs.js +2 -0
  36. data/stubs/hotwire/app/javascript/application.js +8 -0
  37. data/stubs/hotwire/app/views/auth/forgot_password.html.erb +23 -0
  38. data/stubs/hotwire/app/views/auth/login.html.erb +40 -0
  39. data/stubs/hotwire/app/views/auth/register.html.erb +47 -0
  40. data/stubs/hotwire/app/views/auth/reset_password.html.erb +28 -0
  41. data/stubs/hotwire/app/views/dashboard/index.html.erb +15 -0
  42. data/stubs/hotwire/app/views/layouts/_navigation.html.erb +92 -0
  43. data/stubs/hotwire/app/views/layouts/application.html.erb +42 -0
  44. data/stubs/hotwire/app/views/layouts/guest.html.erb +33 -0
  45. data/stubs/hotwire/app/views/profile/edit.html.erb +27 -0
  46. data/stubs/hotwire/app/views/profile/partials/_delete_user_form.html.erb +48 -0
  47. data/stubs/hotwire/app/views/profile/partials/_update_password_form.html.erb +58 -0
  48. data/stubs/hotwire/app/views/profile/partials/_update_profile_information_form.html.erb +49 -0
  49. data/stubs/hotwire/app/views/welcome/index.html.erb +68 -0
  50. data/stubs/hotwire/config/importmap.rb +3 -0
  51. data/stubs/hotwire/config/tailwind.config.js +23 -0
  52. data/stubs/inertia-common/app/controllers/concerns/authenticate.rb +34 -0
  53. data/stubs/{default → inertia-common}/app/controllers/dashboard_controller.rb +1 -1
  54. data/stubs/inertia-common/bin/vite +27 -0
  55. data/stubs/inertia-common/config/vite.json +16 -0
  56. data/stubs/inertia-react-ts/app/views/layouts/application.html.erb +0 -4
  57. data/stubs/inertia-react-ts/config/tailwind.config.js +1 -1
  58. data/stubs/inertia-react-ts/package.json +24 -24
  59. data/stubs/inertia-vue-ts/app/javascript/Pages/Welcome.vue +1 -1
  60. data/stubs/inertia-vue-ts/app/views/layouts/application.html.erb +0 -4
  61. data/stubs/inertia-vue-ts/config/tailwind.config.js +1 -1
  62. metadata +63 -18
  63. data/stubs/default/config/vite.json +0 -16
  64. /data/stubs/{default → hotwire}/app/controllers/concerns/authenticate.rb +0 -0
  65. /data/stubs/{default → hotwire}/bin/vite +0 -0
  66. /data/stubs/{default → inertia-common}/Procfile.dev +0 -0
  67. /data/stubs/{default → inertia-common}/app/controllers/application_controller.rb +0 -0
  68. /data/stubs/{default → inertia-common}/app/controllers/auth/authenticated_session_controller.rb +0 -0
  69. /data/stubs/{default → inertia-common}/app/controllers/auth/new_password_controller.rb +0 -0
  70. /data/stubs/{default → inertia-common}/app/controllers/auth/password_reset_link_controller.rb +0 -0
  71. /data/stubs/{default → inertia-common}/app/controllers/auth/registered_user_controller.rb +0 -0
  72. /data/stubs/{default → inertia-common}/app/controllers/concerns/handle_inertia_requests.rb +0 -0
  73. /data/stubs/{default → inertia-common}/app/controllers/concerns/verify_csrf_token.rb +0 -0
  74. /data/stubs/{default → inertia-common}/app/controllers/password_controller.rb +0 -0
  75. /data/stubs/{default → inertia-common}/app/controllers/profile_controller.rb +0 -0
  76. /data/stubs/{default → inertia-common}/app/controllers/welcome_controller.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa07032d843778439756cd1f5440d066f46e368ca699aadd2c96559721e9456d
4
- data.tar.gz: e9a408babe88ac76f5d524b8ed4e35213eb2407a0135c422dfd1c0ee29ae5438
3
+ metadata.gz: 367b549562d6d96be85bac57b8d0f4528598122dfe773d733fcd992de4e1616d
4
+ data.tar.gz: 9d79187ce1918ff96854c36cb5e2f51e05aaf6a329afeede53a35151eaeb433d
5
5
  SHA512:
6
- metadata.gz: 5e6ca9927646f4aaca149938b413af65d8257f65a0883fe5dff23512f10829e77c865ba08295edca37c8293273443aaf75e60c2cab375fb1fdf5b766c5dcc105
7
- data.tar.gz: 8824870ef8846a2c370cf64c06def95cb657032482dcd56521e63483c895175fa921c9658efb49dbde2f23770cbd8b58be3fa34fc279c924dc0bc2d57e0aad8b
6
+ metadata.gz: f87b1598f94dbdead93d4dea21d766d57819ec6bf354093e45af74c5659ebf7c31927a09f22138e5bd4fd122a1210a3aef92dbd5e0f2f492978bd44000e2d2db
7
+ data.tar.gz: ff833ef663bc961c1ab98c8ab671a26b04ae4da50c57a491a9a96c713b97068570f843905f6ef03591bdf53f3c306f18d881e6b75b56b0039423b5547fd84ea6
@@ -4,10 +4,15 @@ require "open3"
4
4
  require "thor"
5
5
 
6
6
  class Kaze::Commands::InstallCommand < Thor
7
- include Kaze::Commands::InstallInertiaStacks
7
+ include Kaze::Commands::InstallsHotwireStack
8
+ include Kaze::Commands::InstallsInertiaStacks
8
9
 
9
10
  desc "install [STACK]", "Install the Kaze controllers and resources. Supported stacks: react, vue."
10
11
  def install(stack = "hotwire")
12
+ if stack == "hotwire"
13
+ return install_hotwire_stack
14
+ end
15
+
11
16
  if stack == "react"
12
17
  return install_inertia_react_stack
13
18
  end
@@ -21,14 +26,26 @@ class Kaze::Commands::InstallCommand < Thor
21
26
 
22
27
  private
23
28
 
24
- def require_gems(gems = [])
29
+ def install_gems(gems = [], group = nil)
30
+ installed_gems = Bundler::Definition.build("#{Dir.pwd}/Gemfile", nil, {}).dependencies.map(&:name)
31
+
32
+ gem_being_installed = gems.map { |gem| gem unless installed_gems.include?(gem) }.compact
33
+
34
+ return true if gem_being_installed.empty?
35
+
36
+ status = run_command("bundle add #{gem_being_installed.join(" ")}#{group ? " --group \"#{group}\"" : ""}")
37
+
38
+ status.success?
39
+ end
40
+
41
+ def remove_gems(gems = [])
25
42
  installed_gems = Bundler::Definition.build("#{Dir.pwd}/Gemfile", nil, {}).dependencies.map(&:name)
26
43
 
27
- installing_gems = gems.map { |gem| gem unless installed_gems.include?(gem) }.compact
44
+ gems_being_removed = gems.map { |gem| gem if installed_gems.include?(gem) }.compact
28
45
 
29
- return true if installing_gems.empty?
46
+ return true if gems_being_removed.empty?
30
47
 
31
- status = run_command("bundle add #{installing_gems.join(" ")}")
48
+ status = run_command("bundle remove #{gems_being_removed.join(" ")}")
32
49
 
33
50
  status.success?
34
51
  end
@@ -0,0 +1,62 @@
1
+ module Kaze::Commands::InstallsHotwireStack
2
+ private
3
+
4
+ def install_hotwire_stack
5
+ # Gems...
6
+ return unless remove_gems([ "sprockets-rails" ])
7
+ return unless install_gems([ "propshaft", "view_component", "tailwindcss-rails", "turbo-rails", "dotenv", "bcrypt" ])
8
+ return unless install_gems([ "hotwire-livereload" ], "development")
9
+
10
+ # Controllers...
11
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/hotwire/app/controllers", "#{Dir.pwd}/app/controllers")
12
+
13
+ # Models...
14
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/models", "#{Dir.pwd}/app/models")
15
+
16
+ # Forms...
17
+ ensure_directory_exists("#{Dir.pwd}/app/forms")
18
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/forms", "#{Dir.pwd}/app/forms")
19
+
20
+ # Validators...
21
+ ensure_directory_exists("#{Dir.pwd}/app/validators")
22
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/validators", "#{Dir.pwd}/app/validators")
23
+
24
+ # Mailers...
25
+ ensure_directory_exists("#{Dir.pwd}/app/mailers")
26
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/mailers", "#{Dir.pwd}/app/mailers")
27
+
28
+ # Views...
29
+ ensure_directory_exists("#{Dir.pwd}/app/views/layouts")
30
+ ensure_directory_exists("#{Dir.pwd}/app/views/user_mailer")
31
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/hotwire/app/views", "#{Dir.pwd}/app/views")
32
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.html.erb", "#{Dir.pwd}/app/views/layouts/mailer.html.erb")
33
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.text.erb", "#{Dir.pwd}/app/views/layouts/mailer.text.erb")
34
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/user_mailer/reset_password.html.erb", "#{Dir.pwd}/app/views/user_mailer/reset_password.html.erb")
35
+
36
+ # Components + Pages...
37
+ ensure_directory_exists("#{Dir.pwd}/app/components")
38
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/hotwire/app/components", "#{Dir.pwd}/app/components")
39
+ ensure_directory_exists("#{Dir.pwd}/app/javascript")
40
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/hotwire/app/javascript", "#{Dir.pwd}/app/javascript")
41
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/hotwire/config/importmap.rb", "#{Dir.pwd}/config/importmap.rb")
42
+
43
+ # Tests...
44
+
45
+ # Routes...
46
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/config/routes.rb", "#{Dir.pwd}/config/routes.rb")
47
+
48
+ # Migrations...
49
+ install_migrations
50
+
51
+ # Tailwind...
52
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/assets/stylesheets/application.css", "#{Dir.pwd}/app/assets/stylesheets/application.css")
53
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/assets/stylesheets/application.tailwind.css", "#{Dir.pwd}/app/assets/stylesheets/application.tailwind.css")
54
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/hotwire/config/tailwind.config.js", "#{Dir.pwd}/config/tailwind.config.js")
55
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
56
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/hotwire/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
57
+ run_command("rails tailwindcss:build")
58
+
59
+ say ""
60
+ say "Kaze scaffolding installed successfully.", :green
61
+ end
62
+ end
@@ -1,15 +1,16 @@
1
- module Kaze::Commands::InstallInertiaStacks
1
+ module Kaze::Commands::InstallsInertiaStacks
2
2
  private
3
3
 
4
4
  def install_inertia_react_stack
5
- # Install Inertia...
6
- return unless require_gems([ "propshaft", "tailwindcss-rails", "inertia_rails", "vite_rails", "dotenv", "bcrypt", "js-routes" ])
5
+ # Gems...
6
+ return unless remove_gems([ "sprockets-rails" ])
7
+ return unless install_gems([ "propshaft", "tailwindcss-rails", "inertia_rails", "vite_rails", "dotenv", "bcrypt", "js-routes" ])
7
8
 
8
9
  # NPM Packages...
9
10
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/package.json", "#{Dir.pwd}/package.json")
10
11
 
11
12
  # Controllers...
12
- FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/controllers", "#{Dir.pwd}/app/controllers")
13
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/app/controllers", "#{Dir.pwd}/app/controllers")
13
14
 
14
15
  # Models...
15
16
  FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/models", "#{Dir.pwd}/app/models")
@@ -22,6 +23,10 @@ module Kaze::Commands::InstallInertiaStacks
22
23
  ensure_directory_exists("#{Dir.pwd}/app/validators")
23
24
  FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/validators", "#{Dir.pwd}/app/validators")
24
25
 
26
+ # Mailers...
27
+ ensure_directory_exists("#{Dir.pwd}/app/mailers")
28
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/mailers", "#{Dir.pwd}/app/mailers")
29
+
25
30
  # Views...
26
31
  ensure_directory_exists("#{Dir.pwd}/app/views/layouts")
27
32
  ensure_directory_exists("#{Dir.pwd}/app/views/user_mailer")
@@ -30,10 +35,6 @@ module Kaze::Commands::InstallInertiaStacks
30
35
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.text.erb", "#{Dir.pwd}/app/views/layouts/mailer.text.erb")
31
36
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/user_mailer/reset_password.html.erb", "#{Dir.pwd}/app/views/user_mailer/reset_password.html.erb")
32
37
 
33
- # Mailers...
34
- ensure_directory_exists("#{Dir.pwd}/app/mailers")
35
- FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/mailers", "#{Dir.pwd}/app/mailers")
36
-
37
38
  # Components + Pages...
38
39
  ensure_directory_exists("#{Dir.pwd}/app/javascript")
39
40
  FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/app/javascript", "#{Dir.pwd}/app/javascript")
@@ -50,12 +51,12 @@ module Kaze::Commands::InstallInertiaStacks
50
51
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/assets/stylesheets/application.css", "#{Dir.pwd}/app/assets/stylesheets/application.css")
51
52
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/assets/stylesheets/application.tailwind.css", "#{Dir.pwd}/app/assets/stylesheets/application.tailwind.css")
52
53
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/config/tailwind.config.js", "#{Dir.pwd}/config/tailwind.config.js")
53
- FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/config/vite.json", "#{Dir.pwd}/config/vite.json")
54
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/config/vite.json", "#{Dir.pwd}/config/vite.json")
54
55
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/tsconfig.json", "#{Dir.pwd}/tsconfig.json")
55
56
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/vite.config.ts", "#{Dir.pwd}/vite.config.ts")
56
57
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
57
- FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/vite", "#{Dir.pwd}/bin/vite")
58
- FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
58
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/bin/vite", "#{Dir.pwd}/bin/vite")
59
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
59
60
  run_command("rails generate js_routes:middleware")
60
61
 
61
62
  say ""
@@ -76,14 +77,15 @@ module Kaze::Commands::InstallInertiaStacks
76
77
  end
77
78
 
78
79
  def install_inertia_vue_stack
79
- # Install Inertia...
80
- return unless require_gems([ "propshaft", "tailwindcss-rails", "inertia_rails", "vite_rails", "dotenv", "bcrypt", "js-routes" ])
80
+ # Gems...
81
+ return unless remove_gems([ "sprockets-rails" ])
82
+ return unless install_gems([ "propshaft", "tailwindcss-rails", "inertia_rails", "vite_rails", "dotenv", "bcrypt", "js-routes" ])
81
83
 
82
84
  # NPM Packages...
83
85
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-vue-ts/package.json", "#{Dir.pwd}/package.json")
84
86
 
85
87
  # Controllers...
86
- FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/controllers", "#{Dir.pwd}/app/controllers")
88
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/app/controllers", "#{Dir.pwd}/app/controllers")
87
89
 
88
90
  # Models...
89
91
  FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/models", "#{Dir.pwd}/app/models")
@@ -124,12 +126,12 @@ module Kaze::Commands::InstallInertiaStacks
124
126
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/assets/stylesheets/application.css", "#{Dir.pwd}/app/assets/stylesheets/application.css")
125
127
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/assets/stylesheets/application.tailwind.css", "#{Dir.pwd}/app/assets/stylesheets/application.tailwind.css")
126
128
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-vue-ts/config/tailwind.config.js", "#{Dir.pwd}/config/tailwind.config.js")
127
- FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/config/vite.json", "#{Dir.pwd}/config/vite.json")
129
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/config/vite.json", "#{Dir.pwd}/config/vite.json")
128
130
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-vue-ts/tsconfig.json", "#{Dir.pwd}/tsconfig.json")
129
131
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-vue-ts/vite.config.ts", "#{Dir.pwd}/vite.config.ts")
130
132
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
131
- FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/vite", "#{Dir.pwd}/bin/vite")
132
- FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
133
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/bin/vite", "#{Dir.pwd}/bin/vite")
134
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
133
135
  run_command("rails generate js_routes:middleware")
134
136
 
135
137
  say ""
data/lib/kaze/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kaze
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -1,5 +1,6 @@
1
1
  class LowercaseValidator < ActiveModel::EachValidator
2
2
  def validate_each(record, attribute, value)
3
+ value = "" if value.nil?
3
4
  record.errors.add attribute, (options[:message] || "must be lowercase") unless value == value.downcase
4
5
  end
5
6
  end
@@ -340,13 +340,15 @@ img {
340
340
  <%= yield %>
341
341
 
342
342
  <!-- Subcopy -->
343
+ <% if content_for?(:subcopy) %>
343
344
  <table class="subcopy" width="100%" cellpadding="0" cellspacing="0" role="presentation">
344
345
  <tr>
345
346
  <td>
346
- <%= yield(:subcopy) %>
347
+ <%= yield :subcopy %>
347
348
  </td>
348
349
  </tr>
349
350
  </table>
351
+ <% end %>
350
352
 
351
353
  </td>
352
354
  </tr>
@@ -5,7 +5,9 @@
5
5
  <%= yield %>
6
6
 
7
7
  <!-- Subcopy -->
8
- <%= yield(:subcopy) %>
8
+ <% if content_for?(:subcopy) %>
9
+ <%= yield :subcopy %>
10
+ <% end %>
9
11
 
10
12
  <!-- Footer -->
11
13
  © <%= Time.new.year %> <%= ENV.fetch("APP_NAME", "Rails") %>. All rights reserved.
@@ -17,7 +17,7 @@ Rails.application.routes.draw do
17
17
 
18
18
  post "logout", to: "auth/authenticated_session#destroy", as: :logout
19
19
 
20
- get "dashboard", to: "dashboard#show", as: :dashboard
20
+ get "dashboard", to: "dashboard#index", as: :dashboard
21
21
 
22
22
  get "profile", to: "profile#edit", as: :profile_edit
23
23
  patch "profile", to: "profile#update", as: :profile_update
@@ -0,0 +1,2 @@
1
+ web: bin/rails server
2
+ css: bin/rails tailwindcss:watch
@@ -0,0 +1,14 @@
1
+ class ApplicationLogoComponent < ViewComponent::Base
2
+ erb_template <<~ERB
3
+ <svg viewBox="0 -6 32 32" xmlns="http://www.w3.org/2000/svg" <%= sanitize @attributes.join(" ") %>>
4
+ <g fill="none" fill-rule="evenodd">
5
+ <path d="M0-6h32v32H0z"/>
6
+ <path fill="#c00" fill-rule="nonzero" d="M.985 19.636s.422-4.163 3.375-9.087c2.954-4.924 7.99-8.65 12.083-9.017 8.144-.816 15.46 6.485 15.46 6.485s-.24.168-.494.38C23.42 2.49 18.54 5.274 17.005 6.02c-7.033 3.925-4.91 13.616-4.91 13.616H.987zM24.137 2.32c-.45-.182-.9-.35-1.364-.505l.056-.93c.885.254 1.237.423 1.363.493l-.056.943zM22.8 5.304c.45.028.915.084 1.393.183l-.056.872c-.464-.1-.928-.155-1.392-.17l.056-.885zM17.597.913c-.407 0-.815.015-1.223.058l-.268-.83c.465-.056.915-.084 1.35-.084l.282.858h-.14zm.676 5.178c.35-.154.76-.31 1.237-.45l.31.93c-.41.125-.817.294-1.225.49l-.323-.97zm-6.386-3.7c-.366.184-.718.395-1.083.62l-.647-.985c.38-.225.745-.42 1.097-.604l.633.97zm2.883 6.33c.252-.323.548-.646.87-.942l.634.957c-.31.323-.59.647-.83 1L14.77 8.72zm-2.04 4.53c.112-.506.24-1.027.422-1.547l1.012.802c-.14.548-.24 1.097-.295 1.645l-1.14-.9zM6.57 6.57c-.34.35-.662.73-.958 1.11L4.53 6.752c.323-.352.674-.704 1.04-1.055l1 .872zm-4.25 6.286c-.224.52-.52 1.21-.702 1.688L0 13.954c.14-.38.436-1.084.703-1.69l1.618.592zm10.2 3.967l1.518.548c.084.663.21 1.28.337 1.83l-1.688-.605c-.07-.422-.14-1.027-.168-1.772z"/>
7
+ </g>
8
+ </svg>
9
+ ERB
10
+
11
+ def initialize(attributes = {})
12
+ @attributes = attributes.map { |key, attribute| "#{key}=\"#{attribute}\"" }
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ class AuthSessionStatusComponent < ViewComponent::Base
2
+ erb_template <<~ERB
3
+ <div <%= sanitize @attributes.join(" ") %>>
4
+ <%= @status %>
5
+ </div>
6
+ ERB
7
+
8
+ def initialize(attributes = {})
9
+ @status = attributes[:status]
10
+ attributes[:class] = "font-medium text-sm text-green-600 dark:text-green-400#{" #{attributes[:class]}" if attributes[:class]}"
11
+ @attributes = attributes.without(:status).map { |key, attribute| "#{key}=\"#{attribute}\"" }
12
+ end
13
+
14
+ def render?
15
+ @status.present?
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ class DangerButtonComponent < ViewComponent::Base
2
+ erb_template <<~ERB
3
+ <button <%= sanitize @attributes.join(" ") %>>
4
+ <%= content %>
5
+ </button>
6
+ ERB
7
+
8
+ def initialize(attributes = {})
9
+ attributes[:type] = attributes[:type] || "submit"
10
+ attributes[:class] = "inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150#{" #{attributes[:class]}" if attributes[:class]}"
11
+ @attributes = attributes.map { |key, attribute| "#{key}=\"#{attribute}\"" }
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ <div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
2
+ <div @click="open = ! open">
3
+ <%= trigger %>
4
+ </div>
5
+
6
+ <div x-show="open"
7
+ x-transition:enter="transition ease-out duration-200"
8
+ x-transition:enter-start="opacity-0 scale-95"
9
+ x-transition:enter-end="opacity-100 scale-100"
10
+ x-transition:leave="transition ease-in duration-75"
11
+ x-transition:leave-start="opacity-100 scale-100"
12
+ x-transition:leave-end="opacity-0 scale-95"
13
+ class="absolute z-50 mt-2 <%= @width %> rounded-md shadow-lg <%= @alignment_classes %>"
14
+ style="display: none;"
15
+ @click="open = false">
16
+ <div class="rounded-md ring-1 ring-black ring-opacity-5 <%= @content_classes %>">
17
+ <%= content %>
18
+ </div>
19
+ </div>
20
+ </div>
@@ -0,0 +1,15 @@
1
+ class DropdownComponent < ViewComponent::Base
2
+ renders_one :trigger
3
+
4
+ def initialize(align: "right", width: "48", content_classes: "py-1 bg-white dark:bg-gray-700")
5
+ if align == "left"
6
+ @alignment_classes = "ltr:origin-top-left rtl:origin-top-right start-0"
7
+ elsif align == "top"
8
+ @alignment_classes = "origin-top"
9
+ else
10
+ @alignment_classes = "ltr:origin-top-right rtl:origin-top-left end-0"
11
+ end
12
+ @width = width == "48" ? "w-48" : width
13
+ @content_classes = content_classes
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ class DropdownLinkComponent < ViewComponent::Base
2
+ erb_template <<~ERB
3
+ <a <%= sanitize @attributes.join(" ") %>>
4
+ <%= content %>
5
+ </a>
6
+ ERB
7
+
8
+ def initialize(attributes = {})
9
+ attributes[:class] = "block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out#{" #{attributes[:class]}" if attributes[:class]}"
10
+ @attributes = attributes.map { |key, attribute| "#{key}=\"#{attribute}\"" }
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ class InputErrorComponent < ViewComponent::Base
2
+ erb_template <<~ERB
3
+ <div <%= sanitize @attributes.join(" ") %>>
4
+ <p class="text-sm text-red-600 dark:text-red-400">
5
+ <%= @message %>
6
+ </p>
7
+ </div>
8
+ ERB
9
+
10
+ def initialize(attributes = {})
11
+ @message = attributes[:message]
12
+ @attributes = attributes.without(:message).map { |key, attribute| "#{key}=\"#{attribute}\"" }
13
+ end
14
+
15
+ def render?
16
+ @message.present?
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ class InputLabelComponent < ViewComponent::Base
2
+ erb_template <<~ERB
3
+ <a <%= sanitize @attributes.join(" ") %>>
4
+ <%= @value ? @value : content %>
5
+ </a>
6
+ ERB
7
+
8
+ def initialize(attributes = {})
9
+ @value = attributes[:value] || nil
10
+ attributes[:class] = "block font-medium text-sm text-gray-700 dark:text-gray-300#{" #{attributes[:class]}" if attributes[:class]}"
11
+ @attributes = attributes.without(:value).map { |key, attribute| "#{key}=\"#{attribute}\"" }
12
+ end
13
+ end
@@ -0,0 +1,62 @@
1
+ <div
2
+ x-data="{
3
+ show: <%= @show %>,
4
+ focusables() {
5
+ // All focusable element types...
6
+ let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
7
+ return [...$el.querySelectorAll(selector)]
8
+ // All non-disabled elements...
9
+ .filter(el => ! el.hasAttribute('disabled'))
10
+ },
11
+ firstFocusable() { return this.focusables()[0] },
12
+ lastFocusable() { return this.focusables().slice(-1)[0] },
13
+ nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
14
+ prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
15
+ nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
16
+ prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
17
+ }"
18
+ x-init="$watch('show', value => {
19
+ if (value) {
20
+ document.body.classList.add('overflow-y-hidden');
21
+ <%= @attributes.has_key?(:focusable) ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' %>
22
+ } else {
23
+ document.body.classList.remove('overflow-y-hidden');
24
+ }
25
+ })"
26
+ x-on:open-modal.window="$event.detail == '<%= @name %>' ? show = true : null"
27
+ x-on:close-modal.window="$event.detail == '<%= @name %>' ? show = false : null"
28
+ x-on:close.stop="show = false"
29
+ x-on:keydown.escape.window="show = false"
30
+ x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
31
+ x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
32
+ x-show="show"
33
+ class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
34
+ style="display: <%= @show ? 'block' : 'none' %>;"
35
+ >
36
+ <div
37
+ x-show="show"
38
+ class="fixed inset-0 transform transition-all"
39
+ x-on:click="show = false"
40
+ x-transition:enter="ease-out duration-300"
41
+ x-transition:enter-start="opacity-0"
42
+ x-transition:enter-end="opacity-100"
43
+ x-transition:leave="ease-in duration-200"
44
+ x-transition:leave-start="opacity-100"
45
+ x-transition:leave-end="opacity-0"
46
+ >
47
+ <div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div>
48
+ </div>
49
+
50
+ <div
51
+ x-show="show"
52
+ class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full <%= @max_width %> sm:mx-auto"
53
+ x-transition:enter="ease-out duration-300"
54
+ x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
55
+ x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
56
+ x-transition:leave="ease-in duration-200"
57
+ x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
58
+ x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
59
+ >
60
+ <%= content %>
61
+ </div>
62
+ </div>
@@ -0,0 +1,14 @@
1
+ class ModalComponent < ViewComponent::Base
2
+ def initialize(attributes = {})
3
+ @name = attributes[:name]
4
+ @show = attributes[:show] || false
5
+ @max_width = {
6
+ :sm => 'sm:max-w-sm',
7
+ :md => 'sm:max-w-md',
8
+ :lg => 'sm:max-w-lg',
9
+ :xl => 'sm:max-w-xl',
10
+ '2xl' => 'sm:max-w-2xl',
11
+ }[attributes[:max_width] || '2xl']
12
+ @attributes = attributes.without(:name, :show, :max_width)
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ class NavLinkComponent < ViewComponent::Base
2
+ erb_template <<~ERB
3
+ <a <%= sanitize @attributes.join(" ") %>>
4
+ <%= content %>
5
+ </a>
6
+ ERB
7
+
8
+ def initialize(attributes = {})
9
+ classes = attributes[:active] \
10
+ ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' \
11
+ : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out'
12
+ attributes[:class] = "#{classes}#{" #{attributes[:class]}" if attributes[:class]}"
13
+ @attributes = attributes.without(:active).map { |key, attribute| "#{key}=\"#{attribute}\"" }
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ class PrimaryButtonComponent < ViewComponent::Base
2
+ erb_template <<~ERB
3
+ <button <%= sanitize @attributes.join(" ") %>>
4
+ <%= content %>
5
+ </button>
6
+ ERB
7
+
8
+ def initialize(attributes = {})
9
+ attributes[:type] = attributes[:type] || "submit"
10
+ attributes[:class] = "inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150#{" #{attributes[:class]}" if attributes[:class]}"
11
+ @attributes = attributes.map { |key, attribute| "#{key}=\"#{attribute}\"" }
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ class ResponsiveNavLinkComponent < ViewComponent::Base
2
+ erb_template <<~ERB
3
+ <a <%= sanitize @attributes.join(" ") %>>
4
+ <%= content %>
5
+ </a>
6
+ ERB
7
+
8
+ def initialize(attributes = {})
9
+ classes = attributes[:active] \
10
+ ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out' \
11
+ : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out'
12
+ attributes[:class] = "#{classes}#{" #{attributes[:class]}" if attributes[:class]}"
13
+ @attributes = attributes.without(:active).map { |key, attribute| "#{key}=\"#{attribute}\"" }
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ class SecondaryButtonComponent < ViewComponent::Base
2
+ erb_template <<~ERB
3
+ <button <%= sanitize @attributes.join(" ") %>>
4
+ <%= content %>
5
+ </button>
6
+ ERB
7
+
8
+ def initialize(attributes = {})
9
+ attributes[:type] = attributes[:type] || "button"
10
+ attributes[:class] = "inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150#{" #{attributes[:class]}" if attributes[:class]}"
11
+ @attributes = attributes.map { |key, attribute| "#{key}=\"#{attribute}\"" }
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ class TextInputComponent < ViewComponent::Base
2
+ erb_template <<~ERB
3
+ <input <%= @disabled ? "disabled" : "" %> <%= sanitize @attributes.join(" ") %>>
4
+ ERB
5
+
6
+ def initialize(attributes = {})
7
+ @disabled = attributes[:disabled] || false
8
+ attributes[:class] = "border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm#{" #{attributes[:class]}" if attributes[:class]}"
9
+ @attributes = attributes.without(:disabled).map { |key, attribute| "#{key}=\"#{attribute}\"" }
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationController < ActionController::Base
2
+ include Authenticate
3
+ end
@@ -0,0 +1,29 @@
1
+ class Auth::AuthenticatedSessionController < ApplicationController
2
+ skip_authentication only: %i[new create]
3
+
4
+ layout "guest"
5
+
6
+ def new
7
+ @form = Auth::LoginForm.new
8
+
9
+ render "auth/login"
10
+ end
11
+
12
+ def create
13
+ @form = Auth::LoginForm.new params.permit(:email, :password)
14
+
15
+ user = @form.authenticate
16
+
17
+ return render "auth/login", status: :unprocessable_entity if user.nil?
18
+
19
+ login user
20
+
21
+ redirect_to dashboard_path
22
+ end
23
+
24
+ def destroy
25
+ logout
26
+
27
+ redirect_to login_path
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ class Auth::NewPasswordController < ApplicationController
2
+ skip_authentication
3
+
4
+ layout "guest"
5
+
6
+ def new
7
+ @form = Auth::NewPasswordForm.new params.permit(:token)
8
+
9
+ render "auth/reset_password"
10
+ end
11
+
12
+ def create
13
+ @form = Auth::NewPasswordForm.new params.permit(:token, :password, :password_confirmation)
14
+
15
+ return redirect_to login_path, flash: { status: "Your password has been reset." } if @form.reset?
16
+
17
+ render "auth/reset_password", status: :unprocessable_entity
18
+ end
19
+ end