moduler 0.1.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/moduler/tailwind.css +1359 -0
  3. data/app/assets/config/moduler/manifest.js +2 -0
  4. data/app/assets/javascripts/moduler/application.js +9 -0
  5. data/app/assets/javascripts/moduler/controllers/flash_controller.js +19 -0
  6. data/app/assets/stylesheets/moduler/tailwind.css +21 -0
  7. data/app/components/moduler/administration/page_component.html.erb +7 -0
  8. data/app/components/moduler/administration/page_component.rb +6 -0
  9. data/app/components/moduler/badge_component.html.erb +8 -0
  10. data/app/components/moduler/badge_component.rb +10 -0
  11. data/app/components/moduler/button_component.html.erb +7 -0
  12. data/app/components/moduler/button_component.rb +19 -0
  13. data/app/components/moduler/button_link_component.html.erb +5 -0
  14. data/app/components/moduler/button_link_component.rb +15 -0
  15. data/app/components/moduler/filter_component.html.erb +12 -0
  16. data/app/components/moduler/filter_component.rb +15 -0
  17. data/app/components/moduler/form/base_error_component.html.erb +1 -0
  18. data/app/components/moduler/form/base_error_component.rb +9 -0
  19. data/app/components/moduler/form/email_component.html.erb +14 -0
  20. data/app/components/moduler/form/email_component.rb +11 -0
  21. data/app/components/moduler/form/password_component.html.erb +14 -0
  22. data/app/components/moduler/form/password_component.rb +11 -0
  23. data/app/components/moduler/form/section_component.html.erb +5 -0
  24. data/app/components/moduler/form/section_component.rb +14 -0
  25. data/app/components/moduler/form/select_component.html.erb +14 -0
  26. data/app/components/moduler/form/select_component.rb +12 -0
  27. data/app/components/moduler/form/submit_component.html.erb +3 -0
  28. data/app/components/moduler/form/submit_component.rb +10 -0
  29. data/app/components/moduler/form/text_component.html.erb +14 -0
  30. data/app/components/moduler/form/text_component.rb +11 -0
  31. data/app/components/moduler/list_component.html.erb +15 -0
  32. data/app/components/moduler/list_component.rb +18 -0
  33. data/app/components/moduler/list_item_component.html.erb +23 -0
  34. data/app/components/moduler/list_item_component.rb +14 -0
  35. data/app/components/moduler/page_header_component.html.erb +16 -0
  36. data/app/components/moduler/page_header_component.rb +12 -0
  37. data/app/components/moduler/sidebar_action_component.html.erb +5 -0
  38. data/app/components/moduler/sidebar_action_component.rb +15 -0
  39. data/app/controllers/concerns/moduler/authentication.rb +62 -0
  40. data/app/controllers/moduler/administration/application_controller.rb +7 -0
  41. data/app/controllers/moduler/administration/home_controller.rb +9 -0
  42. data/app/controllers/moduler/administration/sessions_controller.rb +34 -0
  43. data/app/controllers/moduler/administration/users_controller.rb +56 -0
  44. data/app/controllers/moduler/application_controller.rb +18 -0
  45. data/app/controllers/moduler/home_controller.rb +5 -0
  46. data/app/controllers/moduler/profile/sessions/revocation_controller.rb +14 -0
  47. data/app/controllers/moduler/profile/sessions_controller.rb +21 -0
  48. data/app/controllers/moduler/profile_controller.rb +27 -0
  49. data/app/controllers/moduler/sessions_controller.rb +39 -0
  50. data/app/controllers/moduler/users_controller.rb +36 -0
  51. data/app/filters/application_filter.rb +41 -0
  52. data/app/filters/query_filter.rb +18 -0
  53. data/app/helpers/moduler/application_helper.rb +4 -0
  54. data/app/lib/moduler/form_builder.rb +23 -0
  55. data/app/models/moduler/current.rb +6 -0
  56. data/app/models/moduler/session.rb +17 -0
  57. data/app/models/moduler/user.rb +18 -0
  58. data/app/policies/moduler/administration/base_policy.rb +11 -0
  59. data/app/policies/moduler/administration/home_policy.rb +9 -0
  60. data/app/policies/moduler/administration/session_policy.rb +13 -0
  61. data/app/policies/moduler/administration/user_policy.rb +25 -0
  62. data/app/policies/moduler/application_policy.rb +38 -0
  63. data/app/views/kaminari/_gap.html.erb +1 -0
  64. data/app/views/kaminari/_next_page.html.erb +1 -0
  65. data/app/views/kaminari/_page.html.erb +3 -0
  66. data/app/views/kaminari/_paginator.html.erb +13 -0
  67. data/app/views/kaminari/_prev_page.html.erb +1 -0
  68. data/app/views/layouts/moduler/application.html.erb +16 -10
  69. data/app/views/moduler/administration/home/index.html.erb +6 -0
  70. data/app/views/moduler/administration/sessions/_session.html.erb +30 -0
  71. data/app/views/moduler/administration/sessions/index.html.erb +9 -0
  72. data/app/views/moduler/administration/users/_user.html.erb +13 -0
  73. data/app/views/moduler/administration/users/edit.html.erb +14 -0
  74. data/app/views/moduler/administration/users/index.html.erb +13 -0
  75. data/app/views/moduler/administration/users/new.html.erb +18 -0
  76. data/app/views/moduler/common/_flash.html.erb +16 -0
  77. data/app/views/moduler/common/_header.html.erb +18 -0
  78. data/app/views/moduler/common/_sidebar.html.erb +6 -0
  79. data/app/views/moduler/common/administration/_sidebar.html.erb +6 -0
  80. data/app/views/moduler/home/index.html.erb +0 -0
  81. data/app/views/moduler/profile/edit.html.erb +28 -0
  82. data/app/views/moduler/profile/sessions/_session.html.erb +52 -0
  83. data/app/views/moduler/profile/sessions/index.html.erb +26 -0
  84. data/app/views/moduler/sessions/new.html.erb +31 -0
  85. data/app/views/moduler/users/new.html.erb +26 -0
  86. data/config/importmap.rb +5 -0
  87. data/config/locales/moduler/administration/sessions/en.yml +24 -0
  88. data/config/locales/moduler/administration/users/en.yml +43 -0
  89. data/config/locales/moduler/common/en.yml +13 -0
  90. data/config/locales/moduler/components/en.yml +11 -0
  91. data/config/locales/moduler/en.yml +4 -0
  92. data/config/locales/moduler/profile/en.yml +26 -0
  93. data/config/locales/moduler/profile/sessions/en.yml +25 -0
  94. data/config/locales/moduler/sessions/en.yml +18 -0
  95. data/config/locales/moduler/users/en.yml +21 -0
  96. data/config/routes.rb +19 -0
  97. data/db/migrate/20260322152259_create_moduler_users.rb +12 -0
  98. data/db/migrate/20260322153744_create_moduler_sessions.rb +13 -0
  99. data/db/migrate/20261015171504_add_role_to_moduler_users.rb +5 -0
  100. data/db/migrate/20261020191709_enable_pg_trgm.rb +5 -0
  101. data/lib/moduler/configuration.rb +12 -0
  102. data/lib/moduler/engine.rb +20 -0
  103. data/lib/moduler/router.rb +10 -0
  104. data/lib/moduler/routing.rb +11 -0
  105. data/lib/moduler/version.rb +1 -1
  106. data/lib/moduler.rb +12 -1
  107. data/lib/tasks/moduler_tasks.rake +42 -4
  108. metadata +250 -2
@@ -1,4 +1,22 @@
1
1
  module Moduler
2
2
  class ApplicationController < ActionController::Base
3
+ include Authentication
4
+ include Pundit::Authorization
5
+
6
+ default_form_builder FormBuilder
7
+
8
+ rescue_from Pundit::NotAuthorizedError, with: :not_authorized
9
+
10
+ helper_method :current_user
11
+
12
+ private
13
+
14
+ def current_user
15
+ Current.user
16
+ end
17
+
18
+ def not_authorized
19
+ redirect_to moduler.root_path, alert: t("moduler.flash.not_authorized")
20
+ end
3
21
  end
4
22
  end
@@ -0,0 +1,5 @@
1
+ module Moduler
2
+ class HomeController < ApplicationController
3
+ def index; end
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ module Moduler
2
+ module Profile
3
+ module Sessions
4
+ class RevocationController < ApplicationController
5
+ def destroy
6
+ Current.user.sessions.active.update_all(signed_out_at: Time.current)
7
+ cookies.delete(:session_id)
8
+
9
+ redirect_to moduler.new_session_path, status: :see_other
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ module Moduler
2
+ module Profile
3
+ class SessionsController < ApplicationController
4
+ def index
5
+ @sessions = Current.user.sessions.order(created_at: :desc).page(params[:page]).per(10)
6
+ end
7
+
8
+ def destroy
9
+ session = Current.user.sessions.active.find(params[:id])
10
+ session.touch(:signed_out_at)
11
+
12
+ if session == Current.session
13
+ cookies.delete(:session_id)
14
+ redirect_to moduler.new_session_path, status: :see_other
15
+ else
16
+ redirect_to moduler.profile_sessions_path, status: :see_other
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ module Moduler
2
+ class ProfileController < ApplicationController
3
+ def edit
4
+ @user = Current.user
5
+ end
6
+
7
+ def update
8
+ @user = Current.user
9
+
10
+ @user.assign_attributes(permitted_parameters)
11
+
12
+ if @user.save(context: :self_service_update)
13
+ @user.sessions.active.where.not(id: Current.session.id).update_all(signed_out_at: Time.current) if @user.saved_change_to_password_digest?
14
+
15
+ redirect_to moduler.edit_profile_path, notice: t(".notice")
16
+ else
17
+ render :edit, status: :unprocessable_entity
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def permitted_parameters
24
+ params.expect(user: %i[ password_challenge password password_confirmation ])
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ module Moduler
2
+ class SessionsController < ApplicationController
3
+ allow_unauthenticated_access only: %i[ new create ]
4
+
5
+ rate_limit to: 10, within: 3.minutes, only: :create,
6
+ with: -> { redirect_to moduler.new_session_path, alert: t("moduler.sessions.create.rate_limited") }
7
+
8
+ rate_limit to: 5, within: 3.minutes, only: :create,
9
+ by: -> { params.dig(:user, :email_address) },
10
+ with: -> { redirect_to moduler.new_session_path, alert: t("moduler.sessions.create.rate_limited") }
11
+
12
+ def new
13
+ if authenticated?
14
+ redirect_to after_authentication_url
15
+ end
16
+
17
+ @user = User.new
18
+ end
19
+
20
+ def create
21
+ if user = User.authenticate_by(params.require(:user).permit(:email_address, :password))
22
+ start_new_session_for user
23
+
24
+ redirect_to after_authentication_url
25
+ else
26
+ @user = User.new.tap do |user|
27
+ user.errors.add(:base, t("moduler.sessions.create.invalid_credentials"))
28
+ end
29
+
30
+ render :new
31
+ end
32
+ end
33
+
34
+ def destroy
35
+ terminate_session
36
+ redirect_to moduler.new_session_path, status: :see_other
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ module Moduler
2
+ class UsersController < ApplicationController
3
+ allow_unauthenticated_access only: %i[ new create ]
4
+
5
+ rate_limit to: 10, within: 3.minutes, only: :create,
6
+ with: -> { redirect_to moduler.new_user_path, alert: t("moduler.users.create.rate_limited") }
7
+
8
+ rate_limit to: 5, within: 3.minutes, only: :create,
9
+ by: -> { params.dig(:user, :email_address) },
10
+ with: -> { redirect_to moduler.new_user_path, alert: t("moduler.users.create.rate_limited") }
11
+
12
+ def new
13
+ if authenticated?
14
+ redirect_to after_authentication_url
15
+ end
16
+
17
+ @user = User.new
18
+ end
19
+
20
+ def create
21
+ @user = User.new(permitted_parameters)
22
+
23
+ if @user.save
24
+ redirect_to moduler.new_session_path, notice: t("moduler.users.create.success")
25
+ else
26
+ render :new
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def permitted_parameters
33
+ params.require(:user).permit(:email_address, :password, :password_confirmation)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ class ApplicationFilter
2
+ include ActiveModel::Model
3
+ include ActiveModel::Dirty
4
+ include ActiveModel::Attributes
5
+
6
+ class << self
7
+ def modifiers
8
+ @modifiers ||= []
9
+ end
10
+ end
11
+
12
+ def initialize(scope, parameters, options = {})
13
+ super(parameters)
14
+
15
+ @scope = scope
16
+ @options = options
17
+ end
18
+
19
+ def call
20
+ [ filter, results ]
21
+ end
22
+
23
+ private
24
+
25
+ def filter
26
+ return self unless changed?
27
+
28
+ tap(&:valid?)
29
+ end
30
+
31
+ def results
32
+ return @scope.none.page(1) if !changed? && @options.fetch(:allow_empty, false)
33
+ return @scope.none.page(1) if invalid?
34
+
35
+ self.class.modifiers.each do |modifier|
36
+ @scope = modifier.call(@scope, self) || @scope
37
+ end
38
+
39
+ @scope
40
+ end
41
+ end
@@ -0,0 +1,18 @@
1
+ class QueryFilter < ApplicationFilter
2
+ attribute :query, :string
3
+ attribute :page, :integer
4
+
5
+ validates :query, length: { maximum: 100 }
6
+
7
+ modifiers << proc do |scope, filter|
8
+ scope.page(filter.page).per(10)
9
+ end
10
+
11
+ modifiers << proc do |scope, _filter|
12
+ scope.order(created_at: :desc)
13
+ end
14
+
15
+ modifiers << proc do |scope, filter|
16
+ scope.search_by_query(filter.query) if filter.query.present?
17
+ end
18
+ end
@@ -1,4 +1,8 @@
1
1
  module Moduler
2
2
  module ApplicationHelper
3
+ def stylesheet_link_tags_if_exist(*sources, **options)
4
+ existing = sources.select { |source| Rails.application.assets.load_path.find("#{source}.css") }
5
+ stylesheet_link_tag(*existing, **options) if existing.any?
6
+ end
3
7
  end
4
8
  end
@@ -0,0 +1,23 @@
1
+ module Moduler
2
+ class FormBuilder < ActionView::Helpers::FormBuilder
3
+ def error(field, options = {})
4
+ return unless error?(field)
5
+
6
+ @template.content_tag :div, **options do
7
+ @object.errors[field].first
8
+ end
9
+ end
10
+
11
+ def full_error(field, options = {})
12
+ return unless error?(field)
13
+
14
+ @template.content_tag :div, **options do
15
+ @object.errors.full_messages_for(field).first
16
+ end
17
+ end
18
+
19
+ def error?(field)
20
+ @object && @object.errors[field].try(:any?)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,6 @@
1
+ module Moduler
2
+ class Current < ActiveSupport::CurrentAttributes
3
+ attribute :session
4
+ delegate :user, to: :session, allow_nil: true
5
+ end
6
+ end
@@ -0,0 +1,17 @@
1
+ module Moduler
2
+ class Session < ApplicationRecord
3
+ include PgSearch::Model
4
+
5
+ LIFETIME = 30.days
6
+
7
+ belongs_to :user, class_name: "Moduler::User", foreign_key: "moduler_user_id"
8
+
9
+ scope :active, -> { where(signed_out_at: nil) }
10
+
11
+ pg_search_scope :search_by_query, against: %i[ ip_address user_agent ], associated_against: { user: %i[ email_address ] }, using: { trigram: { word_similarity: true } }
12
+
13
+ def active?
14
+ signed_out_at.nil?
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module Moduler
2
+ class User < ApplicationRecord
3
+ include PgSearch::Model
4
+
5
+ has_many :sessions, class_name: "Moduler::Session", foreign_key: "moduler_user_id", dependent: :destroy
6
+
7
+ has_secure_password
8
+
9
+ normalizes :email_address, with: ->(email_address) { email_address.strip.downcase }
10
+
11
+ validates :email_address, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
12
+ validates :password, length: { minimum: 8 }, allow_nil: true
13
+
14
+ enum :role, { member: 0, administrator: 1 }
15
+
16
+ pg_search_scope :search_by_query, against: %i[ email_address ], using: { trigram: { word_similarity: true } }
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module Moduler
2
+ module Administration
3
+ class BasePolicy < ApplicationPolicy
4
+ private
5
+
6
+ def administrator?
7
+ user&.administrator?
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module Moduler
2
+ module Administration
3
+ class HomePolicy < BasePolicy
4
+ def index?
5
+ administrator?
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module Moduler
2
+ module Administration
3
+ class SessionPolicy < BasePolicy
4
+ def index?
5
+ administrator?
6
+ end
7
+
8
+ def destroy?
9
+ administrator?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ module Moduler
2
+ module Administration
3
+ class UserPolicy < BasePolicy
4
+ def index?
5
+ administrator?
6
+ end
7
+
8
+ def create?
9
+ administrator?
10
+ end
11
+
12
+ def update?
13
+ administrator? && record != user
14
+ end
15
+
16
+ def new?
17
+ create?
18
+ end
19
+
20
+ def edit?
21
+ update?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,38 @@
1
+ module Moduler
2
+ class ApplicationPolicy
3
+ attr_reader :user, :record
4
+
5
+ def initialize(user, record)
6
+ @user = user
7
+ @record = record
8
+ end
9
+
10
+ def index?
11
+ false
12
+ end
13
+
14
+ def show?
15
+ false
16
+ end
17
+
18
+ def create?
19
+ false
20
+ end
21
+
22
+ def new?
23
+ create?
24
+ end
25
+
26
+ def update?
27
+ false
28
+ end
29
+
30
+ def edit?
31
+ update?
32
+ end
33
+
34
+ def destroy?
35
+ false
36
+ end
37
+ end
38
+ end
@@ -0,0 +1 @@
1
+ <span class="px-2 text-slate-400"><%= raw(t("views.pagination.truncate")) %></span>
@@ -0,0 +1 @@
1
+ <%= link_to raw(t("views.pagination.next")), url, rel: "next", remote: remote, class: "rounded px-3 py-1 text-slate-600 hover:bg-slate-100" %>
@@ -0,0 +1,3 @@
1
+ <%= link_to page, url, remote: remote, rel: page.rel,
2
+ class: "rounded px-3 py-1 #{page.current? ? 'bg-slate-900 text-white' : 'text-slate-600 hover:bg-slate-100'}",
3
+ "aria-current": (page.current? ? "page" : nil) %>
@@ -0,0 +1,13 @@
1
+ <%= paginator.render do %>
2
+ <nav class="flex flex-wrap items-center justify-end gap-1 text-sm" aria-label="Pagination">
3
+ <%= prev_page_tag unless current_page.first? %>
4
+ <% each_page do |page| %>
5
+ <% if page.left_outer? || page.right_outer? || page.inside_window? %>
6
+ <%= page_tag page %>
7
+ <% elsif !page.was_truncated? %>
8
+ <%= gap_tag %>
9
+ <% end %>
10
+ <% end %>
11
+ <%= next_page_tag unless current_page.last? %>
12
+ </nav>
13
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= link_to raw(t("views.pagination.previous")), url, rel: "prev", remote: remote, class: "rounded px-3 py-1 text-slate-600 hover:bg-slate-100" %>
@@ -1,17 +1,23 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
- <head>
4
- <title>Moduler</title>
5
- <%= csrf_meta_tags %>
6
- <%= csp_meta_tag %>
3
+ <head>
4
+ <title>Moduler</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
7
8
 
8
- <%= yield :head %>
9
+ <%= yield :head %>
9
10
 
10
- <%= stylesheet_link_tag "moduler/application", media: "all" %>
11
- </head>
12
- <body>
11
+ <%= stylesheet_link_tags_if_exist "moduler/tailwind", "moduler/application", media: "all" %>
13
12
 
14
- <%= yield %>
13
+ <%= javascript_importmap_tags "moduler/application" %>
14
+ </head>
15
+ <body class="flex min-h-screen flex-col">
16
+ <div class="flex flex-1 flex-col">
17
+ <%= render "moduler/common/header" if authenticated? %>
18
+ <%= render "moduler/common/flash" %>
15
19
 
16
- </body>
20
+ <%= yield %>
21
+ </div>
22
+ </body>
17
23
  </html>
@@ -0,0 +1,6 @@
1
+ <main class="mx-auto flex w-full flex-1 max-w-[120rem] flex-col gap-4 px-4 md:flex-row md:gap-8 md:pl-0 md:pr-6">
2
+ <%= render "moduler/common/administration/sidebar" %>
3
+
4
+ <div class="flex min-w-0 flex-1 flex-col pb-4 md:mt-8">
5
+ </div>
6
+ </main>
@@ -0,0 +1,30 @@
1
+ <% current = session == Moduler::Current.session %>
2
+
3
+ <%= render(Moduler::ListItemComponent.new(session.user.email_address, id: dom_id(session))) do |item| %>
4
+ <% if current %>
5
+ <% item.with_badge(t(".status.current"), variant: :success) %>
6
+ <% elsif session.active? %>
7
+ <% item.with_badge(t(".status.active")) %>
8
+ <% else %>
9
+ <% item.with_badge(t(".status.signed_out"), variant: :muted) %>
10
+ <% end %>
11
+
12
+ <% item.with_meta do %>
13
+ <%= session.ip_address.presence || t(".unknown_ip") %>
14
+ <span aria-hidden="true">&middot;</span>
15
+ <span title="<%= session.user_agent %>"><%= truncate(session.user_agent.presence || t(".unknown_device"), length: 60) %></span>
16
+ <span aria-hidden="true">&middot;</span>
17
+ <%= time_tag session.signed_in_at, t(".signed_in", time: time_ago_in_words(session.signed_in_at)), title: l(session.signed_in_at, format: :long) %>
18
+ <% if session.signed_out_at %>
19
+ <span aria-hidden="true">&middot;</span>
20
+ <%= time_tag session.signed_out_at, t(".signed_out", time: time_ago_in_words(session.signed_out_at)), title: l(session.signed_out_at, format: :long) %>
21
+ <% end %>
22
+ <% end %>
23
+
24
+ <% if session.active? %>
25
+ <% item.with_action do %>
26
+ <%= render(Moduler::ButtonComponent.new(t(".action.destroy"), moduler.administration_session_path(session), method: :delete, size: :sm,
27
+ confirm: t(current ? ".confirmation.destroy_current" : ".confirmation.destroy"))) %>
28
+ <% end %>
29
+ <% end %>
30
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <%= render(Moduler::Administration::PageComponent.new) do %>
2
+ <%= render(Moduler::PageHeaderComponent.new(t(".title"), description: t(".description"))) %>
3
+
4
+ <%= render(Moduler::FilterComponent.new(@filter, moduler.administration_sessions_path)) %>
5
+
6
+ <%= render(Moduler::ListComponent.new(@sessions, id: "sessions", empty: t(".empty"))) do %>
7
+ <%= render partial: "moduler/administration/sessions/session", collection: @sessions %>
8
+ <% end %>
9
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <%= render(Moduler::ListItemComponent.new(user.email_address, id: dom_id(user))) do |item| %>
2
+ <% item.with_badge(t("moduler.administration.users.roles.#{user.role}"), variant: user.administrator? ? :success : :neutral) %>
3
+
4
+ <% item.with_meta do %>
5
+ <%= time_tag user.created_at, t(".created", time: time_ago_in_words(user.created_at)), title: l(user.created_at, format: :long) %>
6
+ <% end %>
7
+
8
+ <% if Moduler::Administration::UserPolicy.new(current_user, user).edit? %>
9
+ <% item.with_action do %>
10
+ <%= render(Moduler::ButtonLinkComponent.new(t(".action.edit"), moduler.edit_administration_user_path(user), size: :sm)) %>
11
+ <% end %>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <%= render(Moduler::Administration::PageComponent.new) do %>
2
+ <%= render(Moduler::PageHeaderComponent.new(t(".title"))) %>
3
+
4
+ <%= form_with model: @user, url: moduler.administration_user_path(@user), method: :patch, data: { turbo: false }, class: "w-full max-w-lg space-y-6" do |form| %>
5
+ <%= render(::Moduler::Form::BaseErrorComponent.new(form)) %>
6
+
7
+ <%= render(::Moduler::Form::SectionComponent.new(t(".section.data"))) do %>
8
+ <%= render(::Moduler::Form::EmailComponent.new(form, :email_address, label: t(".label.email_address"))) %>
9
+ <%= render(::Moduler::Form::SelectComponent.new(form, :role, Moduler::User.roles.keys.map { |role| [ t("moduler.administration.users.roles.#{role}"), role ] }, label: t(".label.role"))) %>
10
+ <% end %>
11
+
12
+ <%= render(::Moduler::Form::SubmitComponent.new(form, text: t(".action.save"))) %>
13
+ <% end %>
14
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <%= render(Moduler::Administration::PageComponent.new) do %>
2
+ <%= render(Moduler::PageHeaderComponent.new(t(".title"), description: t(".description"))) do |header| %>
3
+ <% header.with_action do %>
4
+ <%= render(Moduler::ButtonLinkComponent.new(t(".action.new"), moduler.new_administration_user_path)) %>
5
+ <% end %>
6
+ <% end %>
7
+
8
+ <%= render(Moduler::FilterComponent.new(@filter, moduler.administration_users_path)) %>
9
+
10
+ <%= render(Moduler::ListComponent.new(@users, id: "users", empty: t(".empty"))) do %>
11
+ <%= render partial: "moduler/administration/users/user", collection: @users %>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,18 @@
1
+ <%= render(Moduler::Administration::PageComponent.new) do %>
2
+ <%= render(Moduler::PageHeaderComponent.new(t(".title"))) %>
3
+
4
+ <%= form_with model: @user, url: moduler.administration_users_path, data: { turbo: false }, class: "w-full max-w-lg space-y-6" do |form| %>
5
+ <%= render(::Moduler::Form::BaseErrorComponent.new(form)) %>
6
+
7
+ <%= render(::Moduler::Form::SectionComponent.new(t(".section.data"))) do %>
8
+ <%= render(::Moduler::Form::EmailComponent.new(form, :email_address, label: t(".label.email_address"))) %>
9
+ <%= render(::Moduler::Form::SelectComponent.new(form, :role, Moduler::User.roles.keys.map { |role| [ t("moduler.administration.users.roles.#{role}"), role ] }, label: t(".label.role"))) %>
10
+ <% end %>
11
+
12
+ <%= render(::Moduler::Form::SectionComponent.new(t(".section.password"), divider: true)) do %>
13
+ <%= render(::Moduler::Form::PasswordComponent.new(form, :password, label: t(".label.password"), description: t(".description.password"))) %>
14
+ <% end %>
15
+
16
+ <%= render(::Moduler::Form::SubmitComponent.new(form, text: t(".action.create"))) %>
17
+ <% end %>
18
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <% if flash.any? { |_, message| message.present? } %>
2
+ <div class="fixed top-4 right-4 z-50 flex w-full max-w-sm flex-col gap-2 px-4 sm:px-0">
3
+ <% flash.each do |key, message| %>
4
+ <% next if message.blank? %>
5
+ <div data-controller="flash"
6
+ class="flex items-start gap-3 rounded-md border px-4 py-3 text-sm shadow-lg transition duration-200 <%= key.to_s.in?(%w[alert error]) ? "border-red-200 bg-red-50 text-red-700" : key.to_s == "warning" ? "border-amber-200 bg-amber-50 text-amber-700" : "border-emerald-200 bg-emerald-50 text-emerald-700" %>">
7
+ <p class="flex-1"><%= message %></p>
8
+ <button type="button" data-action="flash#dismiss" class="shrink-0 cursor-pointer text-slate-400 transition hover:text-slate-600">
9
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="h-4 w-4">
10
+ <path d="M18 6 6 18M6 6l12 12" />
11
+ </svg>
12
+ </button>
13
+ </div>
14
+ <% end %>
15
+ </div>
16
+ <% end %>
@@ -0,0 +1,18 @@
1
+ <nav class="border-b border-gray-200 bg-white">
2
+ <div class="flex h-16 w-full items-center justify-between gap-4 px-4 sm:px-6 lg:px-6">
3
+ <div class="flex shrink-0 items-center gap-4 sm:gap-8">
4
+ <%= link_to t(".nav.home"), moduler.root_path, class: "text-sm/6 font-semibold text-gray-900 hover:text-custom" %>
5
+
6
+ <% if policy([:moduler, :administration, :home]).index? %>
7
+ |
8
+ <%= link_to t(".nav.administration"), moduler.administration_root_path, class: "text-sm/6 font-semibold text-gray-900 hover:text-custom" %>
9
+ <% end %>
10
+ </div>
11
+
12
+ <div class="flex min-w-0 items-center gap-3 text-sm text-gray-900 sm:gap-6">
13
+ <%= link_to current_user.email_address, moduler.edit_profile_path, class: "truncate" %>
14
+ |
15
+ <%= button_to t(".action.sign_out"), moduler.session_path, method: :delete, class: "cursor-pointer whitespace-nowrap font-semibold" %>
16
+ </div>
17
+ </div>
18
+ </nav>
@@ -0,0 +1,6 @@
1
+ <aside class="-mx-4 border-b border-gray-200 px-2 md:mx-0 md:w-1/8 md:shrink-0 md:border-b-0 md:border-r md:px-0">
2
+ <nav class="flex gap-1 overflow-x-auto md:flex-col text-sm p-2">
3
+ <%= render(Moduler::SidebarActionComponent.new(t(".action.profile"), moduler.edit_profile_path, "moduler/profile")) %>
4
+ <%= render(Moduler::SidebarActionComponent.new(t(".action.sessions"), moduler.profile_sessions_path, "moduler/profile/sessions")) %>
5
+ </nav>
6
+ </aside>
@@ -0,0 +1,6 @@
1
+ <aside class="-mx-4 border-b border-gray-200 px-2 md:mx-0 md:w-1/8 md:shrink-0 md:border-b-0 md:border-r md:px-0">
2
+ <nav class="flex gap-1 overflow-x-auto md:flex-col text-sm p-2">
3
+ <%= render(Moduler::SidebarActionComponent.new(t(".action.users"), moduler.administration_users_path, "moduler/administration/users")) %>
4
+ <%= render(Moduler::SidebarActionComponent.new(t(".action.sessions"), moduler.administration_sessions_path, "moduler/administration/sessions")) %>
5
+ </nav>
6
+ </aside>
File without changes