kaze 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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