maquina 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/images/maquina/maquina.svg +18 -0
  6. data/app/assets/javascripts/maquina/application.js +4 -0
  7. data/app/assets/javascripts/maquina/controllers/alert_controller.js +29 -0
  8. data/app/assets/javascripts/maquina/controllers/application.js +9 -0
  9. data/app/assets/javascripts/maquina/controllers/file_controller.js +60 -0
  10. data/app/assets/javascripts/maquina/controllers/index.js +11 -0
  11. data/app/assets/javascripts/maquina/controllers/mobile_menu_controller.js +31 -0
  12. data/app/assets/javascripts/maquina/controllers/modal_controller.js +39 -0
  13. data/app/assets/javascripts/maquina/controllers/modal_open_controller.js +15 -0
  14. data/app/assets/javascripts/maquina/controllers/popup_menu_controller.js +17 -0
  15. data/app/assets/javascripts/maquina/controllers/submit_form_controller.js +11 -0
  16. data/app/assets/stylesheets/maquina/application.css +15 -0
  17. data/app/assets/stylesheets/maquina/application.tailwind.css +102 -0
  18. data/app/controllers/concerns/maquina/authenticate.rb +41 -0
  19. data/app/controllers/concerns/maquina/create.rb +27 -0
  20. data/app/controllers/concerns/maquina/destroy.rb +28 -0
  21. data/app/controllers/concerns/maquina/edit.rb +29 -0
  22. data/app/controllers/concerns/maquina/index.rb +33 -0
  23. data/app/controllers/concerns/maquina/new.rb +22 -0
  24. data/app/controllers/concerns/maquina/resourceful.rb +180 -0
  25. data/app/controllers/concerns/maquina/show.rb +27 -0
  26. data/app/controllers/concerns/maquina/update.rb +31 -0
  27. data/app/controllers/maquina/accept_invitations_controller.rb +28 -0
  28. data/app/controllers/maquina/application_controller.rb +19 -0
  29. data/app/controllers/maquina/dashboard_controller.rb +16 -0
  30. data/app/controllers/maquina/invitations_controller.rb +51 -0
  31. data/app/controllers/maquina/plans_controller.rb +56 -0
  32. data/app/controllers/maquina/sessions_controller.rb +56 -0
  33. data/app/controllers/maquina/unauthorized_controller.rb +9 -0
  34. data/app/controllers/maquina/users_controller.rb +24 -0
  35. data/app/helpers/maquina/application_helper.rb +19 -0
  36. data/app/helpers/maquina/navbar_menu_helper.rb +29 -0
  37. data/app/helpers/maquina/views_helper.rb +9 -0
  38. data/app/jobs/maquina/application_job.rb +4 -0
  39. data/app/mailers/maquina/application_mailer.rb +8 -0
  40. data/app/mailers/maquina/user_notifications_mailer.rb +16 -0
  41. data/app/models/concerns/maquina/authenticate_by.rb +33 -0
  42. data/app/models/concerns/maquina/blockeable.rb +28 -0
  43. data/app/models/concerns/maquina/multifactor.rb +80 -0
  44. data/app/models/concerns/maquina/retain_passwords.rb +32 -0
  45. data/app/models/concerns/maquina/searchable.rb +24 -0
  46. data/app/models/maquina/active_session.rb +33 -0
  47. data/app/models/maquina/application_record.rb +11 -0
  48. data/app/models/maquina/current.rb +21 -0
  49. data/app/models/maquina/invitation.rb +15 -0
  50. data/app/models/maquina/plan.rb +38 -0
  51. data/app/models/maquina/used_password.rb +17 -0
  52. data/app/models/maquina/user.rb +33 -0
  53. data/app/policies/maquina/application_policy.rb +50 -0
  54. data/app/policies/maquina/invitation_policy.rb +13 -0
  55. data/app/policies/maquina/navigation_policy.rb +13 -0
  56. data/app/policies/maquina/plan_policy.rb +49 -0
  57. data/app/policies/maquina/user_policy.rb +27 -0
  58. data/app/views/layouts/maquina/application.html.erb +26 -0
  59. data/app/views/layouts/maquina/mailer.html.erb +377 -0
  60. data/app/views/layouts/maquina/mailer.text.erb +12 -0
  61. data/app/views/layouts/maquina/sessions.html.erb +24 -0
  62. data/app/views/maquina/accept_invitations/new.html.erb +9 -0
  63. data/app/views/maquina/accept_invitations/new_view.rb +41 -0
  64. data/app/views/maquina/application/_navbar.html.erb +21 -0
  65. data/app/views/maquina/application/alert.rb +104 -0
  66. data/app/views/maquina/application/components/action_text_component.rb +20 -0
  67. data/app/views/maquina/application/components/checkbox_component.rb +21 -0
  68. data/app/views/maquina/application/components/component_base.rb +60 -0
  69. data/app/views/maquina/application/components/file_component.rb +59 -0
  70. data/app/views/maquina/application/components/input_component.rb +20 -0
  71. data/app/views/maquina/application/components/select_component.rb +44 -0
  72. data/app/views/maquina/application/create.turbo_stream.erb +11 -0
  73. data/app/views/maquina/application/edit.html.erb +9 -0
  74. data/app/views/maquina/application/edit.rb +17 -0
  75. data/app/views/maquina/application/form.rb +77 -0
  76. data/app/views/maquina/application/index.html.erb +10 -0
  77. data/app/views/maquina/application/index_header.rb +46 -0
  78. data/app/views/maquina/application/index_modal.rb +43 -0
  79. data/app/views/maquina/application/index_table.rb +121 -0
  80. data/app/views/maquina/application/new.html.erb +9 -0
  81. data/app/views/maquina/application/new.rb +18 -0
  82. data/app/views/maquina/application/sessions_header.rb +31 -0
  83. data/app/views/maquina/application/show.html.erb +1 -0
  84. data/app/views/maquina/application/update.turbo_stream.erb +11 -0
  85. data/app/views/maquina/application_view.rb +46 -0
  86. data/app/views/maquina/dashboard/index.html.erb +0 -0
  87. data/app/views/maquina/invitations/create.turbo_stream.erb +13 -0
  88. data/app/views/maquina/invitations/new.html.erb +3 -0
  89. data/app/views/maquina/navbar/menu.rb +62 -0
  90. data/app/views/maquina/navbar/menu_item_link.rb +34 -0
  91. data/app/views/maquina/navbar/mobile_button.rb +29 -0
  92. data/app/views/maquina/navbar/mobile_menu.rb +47 -0
  93. data/app/views/maquina/navbar/notification.rb +37 -0
  94. data/app/views/maquina/navbar/profile.rb +16 -0
  95. data/app/views/maquina/navbar/profile_button.rb +24 -0
  96. data/app/views/maquina/navbar/profile_menu.rb +108 -0
  97. data/app/views/maquina/navbar/profile_menu_item_link.rb +41 -0
  98. data/app/views/maquina/navbar/search.rb +40 -0
  99. data/app/views/maquina/navbar/title.rb +22 -0
  100. data/app/views/maquina/sessions/create.turbo_stream.erb +11 -0
  101. data/app/views/maquina/sessions/form.rb +56 -0
  102. data/app/views/maquina/sessions/new.html.erb +9 -0
  103. data/app/views/maquina/unauthorized/401.html.erb +1 -0
  104. data/app/views/maquina/user_notifications_mailer/invitation_email.html.erb +40 -0
  105. data/app/views/maquina/user_notifications_mailer/invitation_email.text.erb +12 -0
  106. data/config/definitions.rb +1 -0
  107. data/config/initializers/importmap.rb +17 -0
  108. data/config/initializers/money.rb +116 -0
  109. data/config/initializers/pagy.rb +235 -0
  110. data/config/locales/flash.en.yml +44 -0
  111. data/config/locales/forms.en.yml +58 -0
  112. data/config/locales/mailers.en.yml +35 -0
  113. data/config/locales/models.en.yml +34 -0
  114. data/config/locales/routes.en.yml +7 -0
  115. data/config/locales/views.en.yml +45 -0
  116. data/config/routes.rb +17 -0
  117. data/db/migrate/20221109010726_create_maquina_plans.rb +13 -0
  118. data/db/migrate/20221113000409_create_maquina_users.rb +19 -0
  119. data/db/migrate/20221113020108_create_maquina_used_passwords.rb +10 -0
  120. data/db/migrate/20221115223414_create_maquina_active_sessions.rb +15 -0
  121. data/db/migrate/20230201203922_create_maquina_invitations.rb +12 -0
  122. data/db/schema.rb +1 -0
  123. data/lib/generators/maquina/install_generator.rb +32 -0
  124. data/lib/generators/maquina/install_templates/install_templates_generator.rb +31 -0
  125. data/lib/generators/maquina/tailwind_config/tailwind_config_generator.rb +11 -0
  126. data/lib/generators/maquina/tailwind_config/templates/app/assets/config/maquina/tailwind.config.js.tt +68 -0
  127. data/lib/generators/maquina/templates/config/initializers/maquina.rb +3 -0
  128. data/lib/maquina/engine.rb +17 -0
  129. data/lib/maquina/version.rb +3 -0
  130. data/lib/maquina.rb +48 -0
  131. data/lib/tasks/install.rake +19 -0
  132. data/lib/tasks/maquina_tasks.rake +4 -0
  133. data/lib/tasks/tailwind.rake +25 -0
  134. metadata +456 -0
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ module Components
6
+ class FileComponent < ComponentBase
7
+ include Phlex::Rails::Helpers::NumberToHumanSize
8
+
9
+ def template
10
+ div(**extend_control_options(attribute_name, control_html)) do
11
+ @form.label attribute_name, class: "label #{label_css_class}"
12
+ div(class: "mt-1") do
13
+ div(data_file_target: "container") do
14
+ @form.file_field attribute_name, **extend_input_options(input_html(no_helpers: true))
15
+ img(class: "hidden", data_file_target: "preview")
16
+ svg(viewbox: "0 0 24 24", fill: "currentColor", aria_hidden: "true", data_action: "click->file#select", data_file_target: "placeholder") do |s|
17
+ s.path(fill_rule: "evenodd", d: "M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z", clip_rule: "evenodd")
18
+ end
19
+ help_template
20
+ error_template
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def help_template
29
+ help = t("helpers.help.#{@resource.model_name.i18n_key}.#{attribute_name}", default: nil)
30
+ return if help.blank?
31
+
32
+ p(class: "help", data_action: "click->file#select") { unsafe_raw help }
33
+ end
34
+
35
+ def extend_input_options(input_html)
36
+ {
37
+ multiple: false,
38
+ data: {
39
+ "file-target": "fileInput"
40
+ }
41
+ }.deep_merge(input_html)
42
+ end
43
+
44
+ def extend_control_options(attribute_name, control_html)
45
+ max_size = control_html.dig(:data, :file_max_size_value) || 4_194_304
46
+ human_max_size = number_to_human_size(max_size)
47
+
48
+ {
49
+ data: {
50
+ controller: "file",
51
+ file_max_size_value: max_size,
52
+ file_validation_message_value: t("activerecord.errors.attributes.#{@form.object.model_name.i18n_key}.#{attribute_name}.oversize", size: human_max_size, default: t("activerecord.errors.attributes.file.oversize", size: human_max_size))
53
+ }
54
+ }.deep_merge(control_html)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ module Components
6
+ class InputComponent < ComponentBase
7
+ def template
8
+ div(**control_html) do
9
+ @form.label attribute_name, class: "label #{label_css_class}"
10
+ div(class: "mt-1") do
11
+ @form.text_field attribute_name, **input_html
12
+ help_template
13
+ error_template
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ module Components
6
+ class SelectComponent < ComponentBase
7
+ delegate :controller, :options_for_select, to: :helpers
8
+
9
+ def template
10
+ options = input_html(no_helpers: true).deep_dup
11
+
12
+ div(**control_html) do
13
+ @form.label attribute_name, class: "label #{label_css_class}"
14
+ div(class: "mt-1") do
15
+ @form.select attribute_name, to_values!(options), extract_options!(options), options
16
+ help_template
17
+ error_template
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def extract_options!(options)
25
+ options.extract!(:include_blank)
26
+ end
27
+
28
+ def to_values!(options)
29
+ values = options.delete(:values)
30
+ selected = options.delete(:selected)
31
+
32
+ if values.is_a?(Symbol)
33
+ values = controller.send(values) if controller.respond_to?(values, true)
34
+ else
35
+ # TODO: Add support for other value options
36
+ []
37
+ end
38
+
39
+ options_for_select(values, selected.present? ? selected : nil)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,11 @@
1
+ <%= turbo_stream.replace class_to_form_frame(resource_class) do %>
2
+ <%= turbo_frame_tag class_to_form_frame(resource_class) do %>
3
+ <%= render Maquina::Application::New.new(resource: resource) %>
4
+ <% end %>
5
+ <% end %>
6
+
7
+ <%= turbo_stream.replace :alert do %>
8
+ <%= turbo_frame_tag :alert do %>
9
+ <%= render Maquina::Application::Alert.new(flash) %>
10
+ <% end %>
11
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <div class="bg-white">
2
+ <div class="mx-auto max-w-7xl py-12 px-4 sm:px-6 lg:px-8">
3
+ <div class="mx-auto max-w-3xl">
4
+ <%= turbo_frame_tag class_to_form_frame(resource_class) do %>
5
+ <%= render Maquina::Application::Edit.new(resource: resource) %>
6
+ <% end %>
7
+ </div>
8
+ </div>
9
+ </div>
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class Edit < Phlex::HTML
6
+ include ApplicationView
7
+
8
+ def initialize(resource:)
9
+ @resource = resource
10
+ end
11
+
12
+ def template
13
+ render Maquina::Application::Form.new(resource: @resource)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class Form < Phlex::HTML
6
+ include ApplicationView
7
+ include Phlex::Rails::Helpers::FormWith
8
+
9
+ delegate :resource_path, :collection_path, :submit_path, :form_attributes, to: :helpers
10
+
11
+ def initialize(resource:, modal: false)
12
+ @resource = resource
13
+ @modal = modal
14
+ end
15
+
16
+ def template
17
+ form_with(model: @resource, url: if @resource.persisted?
18
+ resource_path(@resource)
19
+ else
20
+ (submit_path.present? ? submit_path : collection_path)
21
+ end, data: {turbo_frame: :_top}) do |form|
22
+ div(class: "space-y-8 divide-y divide-gray-200") do
23
+ div do
24
+ form_header_template
25
+
26
+ div(class: "mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6") do
27
+ attributes_template(form)
28
+ end
29
+ end
30
+
31
+ div(class: "pt-5") do
32
+ div(class: "flex justify-end") do
33
+ link_to t("helpers.cancel"), collection_path, class: "button", data: @modal ? {action: "modal#toggleModal"} : {turbo_frame: :_top}
34
+ form.submit class: "ml-3 button button-accented"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def attributes_template(form)
44
+ attributes = form_attributes.deep_dup
45
+ attributes.each do |attribute|
46
+ component_resolver(form, attribute)
47
+ end
48
+ end
49
+
50
+ def form_header_template
51
+ div do
52
+ h3(class: "text-lg font-medium leading-6 text-skin-base") { t("new.#{resource_class.model_name.i18n_key}.title", model: model_human_name.downcase, default: t("new.title", default: model_human_name.downcase)) }
53
+ p(class: "mt-1 text-sm text-skin-dimmed") { t("new.#{resource_class.model_name.i18n_key}.description", model: model_human_name.downcase, default: t("new.description", model: model_human_name.downcase)) }
54
+ end
55
+ end
56
+
57
+ def component_resolver(form, attribute)
58
+ attribute_values = attribute.values.first
59
+ input_type = attribute_values.fetch(:type)
60
+ attribute_values[:name] = attribute.keys.first
61
+
62
+ case input_type
63
+ when :checkbox
64
+ render Maquina::Application::Components::CheckboxComponent.new(resource: @resource, form: form, options: attribute_values)
65
+ when :file
66
+ render Maquina::Application::Components::FileComponent.new(resource: @resource, form: form, options: attribute_values)
67
+ when :action_text
68
+ render Maquina::Application::Components::ActionTextComponent.new(resource: @resource, form: form, options: attribute_values)
69
+ when :select
70
+ render Maquina::Application::Components::SelectComponent.new(resource: @resource, form: form, options: attribute_values)
71
+ else
72
+ render Maquina::Application::Components::InputComponent.new(resource: @resource, form: form, options: attribute_values)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,10 @@
1
+ <div class="px-4 sm:px-6 lg:px-8">
2
+ <%= render Maquina::Application::IndexHeader.new(params[:q]) %>
3
+
4
+ <%= render Maquina::Application::IndexTable.new(
5
+ collection: collection,
6
+ pagination: @pagination,
7
+ list_attributes: list_attributes) %>
8
+ </div>
9
+
10
+ <%= render Maquina::Application::IndexModal.new %>
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class IndexHeader < Phlex::HTML
6
+ include ApplicationView
7
+ delegate :new_resource_path, to: :helpers
8
+
9
+ def initialize(filter = nil)
10
+ @filter = filter
11
+ end
12
+
13
+ def template
14
+ div(class: "sm:flex sm:items-center") do
15
+ div(class: "sm:flex-auto") do
16
+ h1(class: "text-xl font-semibold text-skin-base") { model_human_name(plural: true) }
17
+ p(class: "mt-2 text-sm text-skin-muted") { description }
18
+ p(class: "mt-4 text-skin-base") { unsafe_raw t("index.search", search: @filter) } if @filter.present?
19
+ end
20
+ add_new_template
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def add_new_template
27
+ return if policy_class.blank? || !allowed_to?(:new?, with: policy_class)
28
+
29
+ options = {}
30
+ options[:data] = {controller: "modal-open", action: "modal-open#open"} if policy_class.present? && allowed_to?(:new_modal?, with: policy_class)
31
+
32
+ div(class: "mt-4 sm:mt-0 sm:ml-16 sm:flex-none") do
33
+ a(href: new_resource_path, class: "inline-flex items-center justify-center button button-accented", **options) { add_new }
34
+ end
35
+ end
36
+
37
+ def description
38
+ t("index.#{resource_class.model_name.i18n_key}.description", default: "")
39
+ end
40
+
41
+ def add_new
42
+ t("index.#{resource_class.model_name.i18n_key}.add_new", model: model_human_name.downcase, default: t("index.add_new", model: model_human_name.downcase))
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class IndexModal < Phlex::HTML
6
+ include ApplicationView
7
+ register_element :turbo_frame
8
+
9
+ def template
10
+ div(data_controller: "modal",
11
+ data_action: " modal-open:toggle@window->modal#toggleModal close-frame-modal:toggle@window->modal#toggleModal",
12
+ data_frame_src: "") do
13
+ div(class: "fixed inset-0 z-30 hidden overflow-y-auto", aria_labelledby: "modal-title", role: "dialog",
14
+ aria_modal: "true", data_modal_target: "container") do
15
+ div(class: "flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0") do
16
+ div(class: "fixed inset-0 transition-opacity",
17
+ data_transition_enter: "ease-out duration-300",
18
+ data_transition_enter_active: "opacity-0",
19
+ data_transition_enter_to: "opacity-100",
20
+ data_transition_leave: "ease-in duration-200",
21
+ data_transition_leave_active: "opacity-100",
22
+ data_transition_leave_to: "opacity-0",
23
+ aria_hidden: "true") do
24
+ div(class: "fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity", aria_hidden: "true")
25
+ end
26
+ span(class: "hidden sm:inline-block sm:align-middle sm:h-screen", aria_hidden: "true") { "​" }
27
+ div(class: "inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6",
28
+ data_modal_target: "content",
29
+ data_transition_enter: "ease-out duration-300",
30
+ data_transition_enter_active: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
31
+ data_transition_enter_to: "opacity-100 translate-y-0 sm:scale-100",
32
+ data_transition_leave: "ease-in duration-200",
33
+ data_transition_leave_active: "opacity-100 translate-y-0 sm:scale-100",
34
+ data_transition_leave_to: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95") do
35
+ turbo_frame(id: :modal_content, src: "", data: {"modal-target": "frame"}) {}
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class IndexTable < Phlex::HTML
6
+ include ApplicationView
7
+
8
+ delegate :humanized_money_with_symbol, :edit_resource_path, to: :helpers
9
+
10
+ def initialize(collection: nil, pagination: nil, list_attributes: nil)
11
+ @collection = collection
12
+ @list_attributes = list_attributes
13
+ @pagination = pagination
14
+ end
15
+
16
+ def template
17
+ div(class: "mt-8 flex flex-col") do
18
+ div(class: "-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8") do
19
+ div(class: "inline-block min-w-full py-2 align-middle md:px-6 lg:px-8") do
20
+ div(class: "overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg") do
21
+ table(class: "min-w-full divide-y divide-gray-300") do
22
+ thead(class: "bg-gray-50") do
23
+ tr do
24
+ table_headers_template
25
+ th(scope: "col", class: "relative py-3.5 pl-3 pr-4 sm:pr-6") do
26
+ span(class: "sr-only") { "Edit" }
27
+ end
28
+ end
29
+ end
30
+ tbody class: "divide-y divide-gray-200 bg-white" do
31
+ @collection.empty? ? no_items_template : @collection.each { |item| item_template(item) }
32
+ end
33
+ end
34
+ pagination_template
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def table_headers_template
42
+ base_css = "px-3 py-3.5 text-left text-sm font-semibold text-skin-base"
43
+ first_css = "py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-skin-base sm:pl-6"
44
+
45
+ @list_attributes.each_with_index do |attribute, index|
46
+ current_css = if index == 0
47
+ first_css
48
+ else
49
+ base_css
50
+ end
51
+
52
+ th(scope: :col, class: current_css) { attribute_human_name(attribute) }
53
+ end
54
+ end
55
+
56
+ def no_items_template
57
+ col_span = @list_attributes.size + 1
58
+
59
+ tr do
60
+ th(scope: "col", colspan: col_span, class: "whitespace-nowrap px-3 py-4 text-sm text-skin-dimmed") do
61
+ t("index.no_items", model: model_human_name(plural: true))
62
+ end
63
+ end
64
+ end
65
+
66
+ def item_template(item)
67
+ base_css = "whitespace-nowrap px-3 py-4 text-sm text-skin-muted"
68
+ first_css = "whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-skin-base sm:pl-6"
69
+
70
+ tr do
71
+ @list_attributes.each_with_index do |attribute, index|
72
+ td(class: (index == 0) ? first_css : base_css, scope: "col") { attribute_value(item, attribute) }
73
+ end
74
+ td(class: "relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6", scope: "col") do
75
+ a(href: edit_resource_path(item), class: "text-skin-accented hover:text-skin-accented-hover") { t("index.edit") } if policy_class.blank? || allowed_to?(:edit?, item, with: policy_class)
76
+ end
77
+ end
78
+ end
79
+
80
+ def pagination_template
81
+ nav(class: "flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6", aria_label: "Pagination") do
82
+ div(class: "hidden sm:block") do
83
+ p(class: "text-sm text-skin-muted") do
84
+ t("index.pagination.information", from: @pagination.from, to: @pagination.to, count: @pagination.count) if @collection.any? && @pagination.present?
85
+ end
86
+ end
87
+ div(class: "flex flex-1 justify-between sm:justify-end") do
88
+ if @pagination.present? && @pagination.prev.present?
89
+ a(href: "#", class: "relative button") { t("index.pagination.previous") }
90
+ else
91
+ span(class: "relative button", disabled: true) { t("index.pagination.previous") }
92
+ end
93
+
94
+ if @pagination.present? && @pagination.next.present?
95
+ a(href: "#", class: "relative ml-3 button") { t("index.pagination.next") }
96
+ else
97
+ span(class: "relative ml-3 button", disabled: true) { t("index.pagination.next") }
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def attribute_value(item, attribute)
104
+ value = item[attribute] || item.send(attribute)
105
+
106
+ case value
107
+ when TrueClass
108
+ t("yes_value")
109
+ when FalseClass
110
+ t("no_value")
111
+ when ActiveSupport::TimeWithZone, Date
112
+ I18n.l value, format: :short
113
+ when Money
114
+ humanized_money_with_symbol(value)
115
+ else
116
+ value
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,9 @@
1
+ <div class="bg-white">
2
+ <div class="mx-auto max-w-7xl py-12 px-4 sm:px-6 lg:px-8">
3
+ <div class="mx-auto max-w-3xl">
4
+ <%= turbo_frame_tag class_to_form_frame(resource_class) do %>
5
+ <%= render Maquina::Application::New.new(resource: resource) %>
6
+ <% end %>
7
+ </div>
8
+ </div>
9
+ </div>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class New < Phlex::HTML
6
+ include ApplicationView
7
+
8
+ def initialize(resource:, modal: false)
9
+ @resource = resource
10
+ @modal = modal
11
+ end
12
+
13
+ def template
14
+ render Maquina::Application::Form.new(resource: @resource, modal: @modal)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Application
5
+ class SessionsHeader < Phlex::HTML
6
+ include Maquina::ApplicationView
7
+ include Phlex::DeferredRender
8
+
9
+ def initialize(brand_icon:)
10
+ @brand_icon = brand_icon
11
+ end
12
+
13
+ def template
14
+ div(class: "sm:mx-auto sm:w-full sm:max-w-md") do
15
+ image_tag(@brand_icon, class: "mx-auto h-12 w-auto", alt: t("application_name"))
16
+ h2(class: "mt-6 text-center text-3xl font-bold tracking-tight text-skin-base") { t(".title") }
17
+
18
+ if @description.present?
19
+ p(class: "mt-2 text-center text-sm text-skin-dimmed") do
20
+ yield_content(&@description)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ def description(&block)
27
+ @description = block
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1 @@
1
+ <%= resource %>
@@ -0,0 +1,11 @@
1
+ <%= turbo_stream.replace class_to_form_frame(resource_class) do %>
2
+ <%= turbo_frame_tag class_to_form_frame(resource_class) do %>
3
+ <%= render Maquina::Application::Edit.new(resource: resource) %>
4
+ <% end %>
5
+ <% end %>
6
+
7
+ <%= turbo_stream.replace :alert do %>
8
+ <%= turbo_frame_tag :alert do %>
9
+ <%= render Maquina::Application::Alert.new(flash) %>
10
+ <% end %>
11
+ <% end %>
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module ApplicationView
5
+ include Maquina::Engine.routes.url_helpers
6
+ include Phlex::Rails::Helpers::T
7
+
8
+ delegate :resource_class, :l, :default_url_options, :policy_class, :allowed_to?, to: :helpers
9
+
10
+ # TODO: Revisit this later
11
+ def image_tag(source, options = {})
12
+ unsafe_raw helpers.image_tag(source, **options)
13
+ end
14
+
15
+ def link_to(body, url, html_options = {})
16
+ unsafe_raw helpers.link_to(body, url, html_options)
17
+ end
18
+
19
+ def button_to(name, options = nil, html_options = nil)
20
+ unsafe_raw helpers.button_to(name, options, html_options)
21
+ end
22
+
23
+ # type: :fill or :outline
24
+ def svg_icon(type, icon:, view_box: "0 0 24 24", stroke_width: 2, css_class: "block h-6 w-6", data: {})
25
+ svg_attributes = if type == :fill
26
+ {xmlns: "http://www.w3.org/2000/svg", fill: "currentColor", viewBox: view_box, "aria-hidden": "true"}
27
+ else
28
+ {xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: view_box, "stroke-width": stroke_width, stroke: "currentColor", "aria-hidden": "true"}
29
+ end
30
+
31
+ path_attributes = (type == :fill) ? {} : {"stroke-linecap": "round", "stroke-linejoin": "round"}
32
+
33
+ svg(class: css_class, data: data, **svg_attributes) do |markup|
34
+ markup.path(d: icon, **path_attributes)
35
+ end
36
+ end
37
+
38
+ def model_human_name(plural: false)
39
+ resource_class.model_name.human(count: plural ? 2 : 1)
40
+ end
41
+
42
+ def attribute_human_name(attribute)
43
+ resource_class.human_attribute_name(attribute)
44
+ end
45
+ end
46
+ end
File without changes
@@ -0,0 +1,13 @@
1
+ <%= turbo_stream.replace :modal_content do %>
2
+ <%= turbo_frame_tag :modal_content, data: {modal_target: :frame} do %>
3
+ <%= render Maquina::Application::New.new(resource: resource, modal: true) %>
4
+ <% end %>
5
+ <% end %>
6
+
7
+ <% if resource.errors.empty? %>
8
+ <%= turbo_stream.replace :alert do %>
9
+ <%= turbo_frame_tag :alert do %>
10
+ <%= render Maquina::Application::Alert.new(flash) %>
11
+ <% end %>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :modal_content, data: {modal_target: :frame} do %>
2
+ <%= render Maquina::Application::New.new(resource: resource, modal: true) %>
3
+ <% end %>
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maquina
4
+ module Navbar
5
+ # == Navbar main Menu
6
+ #
7
+ # Renders main navbar Menu. It can render menu for desktop o mobile versions.
8
+ #
9
+ # Requires a Rails Helper method to get access to menu options. `menu_options` example:
10
+ #
11
+ # def menu_options
12
+ # {
13
+ # home: root_path,
14
+ # users: users_path
15
+ # accounts: accounts_path
16
+ # }
17
+ # end
18
+ #
19
+ # `Menu` uses I18n to translate menu options. Translation example:
20
+ #
21
+ # menu:
22
+ # main:
23
+ # home: Inicio
24
+ # users: Usuarios
25
+ # accounts: Cuentas
26
+ #
27
+ # `Menu` is initialized by default to render links for desktop. To render mobile links
28
+ # initialize `Menu` as follow:
29
+ #
30
+ # Views::Navbar::Menu.new(desktop: false)
31
+ #
32
+ class Menu < Phlex::HTML
33
+ include Maquina::ApplicationView
34
+ delegate :menu_options, to: :helpers
35
+
36
+ def initialize(desktop: true)
37
+ @desktop = desktop
38
+ end
39
+
40
+ def template
41
+ div class: "#{link_type}-menu" do
42
+ secure_menu_options.each_pair do |option, path|
43
+ render(Maquina::Navbar::MenuItemLink.new(option, path, desktop: @desktop))
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def secure_menu_options
51
+ menu_options
52
+ rescue NoMethodError => ex
53
+ Rails.logger.error "[#{self.class}] Please implement helper method :menu_options :: #{ex.message}"
54
+ {}
55
+ end
56
+
57
+ def link_type
58
+ @desktop ? "desktop" : "mobile"
59
+ end
60
+ end
61
+ end
62
+ end