moduler 0.2.0 → 0.4.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/moduler/tailwind.css +2103 -1
  3. data/app/assets/javascripts/moduler/application.js +1 -0
  4. data/app/assets/javascripts/moduler/controllers/flash_controller.js +19 -0
  5. data/app/assets/javascripts/moduler/controllers/wysiwyg_controller.js +126 -0
  6. data/app/assets/javascripts/moduler/vendor/marked.esm.js +77 -0
  7. data/app/assets/stylesheets/moduler/tailwind.css +3 -0
  8. data/app/components/moduler/administration/page_component.html.erb +7 -0
  9. data/app/components/moduler/administration/page_component.rb +6 -0
  10. data/app/components/moduler/badge_component.html.erb +8 -0
  11. data/app/components/moduler/badge_component.rb +10 -0
  12. data/app/components/moduler/button_component.html.erb +7 -0
  13. data/app/components/moduler/button_component.rb +19 -0
  14. data/app/components/moduler/button_link_component.html.erb +5 -0
  15. data/app/components/moduler/button_link_component.rb +15 -0
  16. data/app/components/moduler/filter_component.html.erb +12 -0
  17. data/app/components/moduler/filter_component.rb +15 -0
  18. data/app/components/moduler/form/base_error_component.html.erb +1 -0
  19. data/app/components/moduler/form/base_error_component.rb +9 -0
  20. data/app/components/moduler/form/email_component.html.erb +14 -0
  21. data/app/components/moduler/form/email_component.rb +11 -0
  22. data/app/components/moduler/form/password_component.html.erb +14 -0
  23. data/app/components/moduler/form/password_component.rb +11 -0
  24. data/app/components/moduler/form/section_component.html.erb +5 -0
  25. data/app/components/moduler/form/section_component.rb +14 -0
  26. data/app/components/moduler/form/select_component.html.erb +14 -0
  27. data/app/components/moduler/form/select_component.rb +12 -0
  28. data/app/components/moduler/form/submit_component.html.erb +3 -0
  29. data/app/components/moduler/form/submit_component.rb +10 -0
  30. data/app/components/moduler/form/text_component.html.erb +14 -0
  31. data/app/components/moduler/form/text_component.rb +11 -0
  32. data/app/components/moduler/form/textarea_component.html.erb +14 -0
  33. data/app/components/moduler/form/textarea_component.rb +11 -0
  34. data/app/components/moduler/form/wysiwyg_component.html.erb +30 -0
  35. data/app/components/moduler/form/wysiwyg_component.rb +62 -0
  36. data/app/components/moduler/list_component.html.erb +15 -0
  37. data/app/components/moduler/list_component.rb +18 -0
  38. data/app/components/moduler/list_item_component.html.erb +23 -0
  39. data/app/components/moduler/list_item_component.rb +14 -0
  40. data/app/components/moduler/page_header_component.html.erb +16 -0
  41. data/app/components/moduler/page_header_component.rb +12 -0
  42. data/app/components/moduler/sidebar_action_component.html.erb +5 -0
  43. data/app/components/moduler/sidebar_action_component.rb +15 -0
  44. data/app/controllers/concerns/moduler/authentication.rb +10 -2
  45. data/app/controllers/moduler/administration/application_controller.rb +7 -0
  46. data/app/controllers/moduler/administration/articles_controller.rb +57 -0
  47. data/app/controllers/moduler/administration/home_controller.rb +9 -0
  48. data/app/controllers/moduler/administration/sessions_controller.rb +34 -0
  49. data/app/controllers/moduler/administration/users_controller.rb +56 -0
  50. data/app/controllers/moduler/application_controller.rb +15 -0
  51. data/app/controllers/moduler/articles_controller.rb +15 -0
  52. data/app/controllers/moduler/profile/sessions/revocation_controller.rb +14 -0
  53. data/app/controllers/moduler/profile/sessions_controller.rb +21 -0
  54. data/app/controllers/moduler/profile_controller.rb +27 -0
  55. data/app/controllers/moduler/sessions_controller.rb +8 -1
  56. data/app/controllers/moduler/users_controller.rb +7 -0
  57. data/app/filters/application_filter.rb +41 -0
  58. data/app/filters/query_filter.rb +18 -0
  59. data/app/lib/moduler/form_builder.rb +2 -2
  60. data/app/models/moduler/article.rb +25 -0
  61. data/app/models/moduler/session.rb +12 -0
  62. data/app/models/moduler/user.rb +9 -0
  63. data/app/policies/moduler/administration/article_policy.rb +29 -0
  64. data/app/policies/moduler/administration/base_policy.rb +11 -0
  65. data/app/policies/moduler/administration/home_policy.rb +9 -0
  66. data/app/policies/moduler/administration/session_policy.rb +13 -0
  67. data/app/policies/moduler/administration/user_policy.rb +25 -0
  68. data/app/policies/moduler/application_policy.rb +38 -0
  69. data/app/views/kaminari/_gap.html.erb +1 -0
  70. data/app/views/kaminari/_next_page.html.erb +1 -0
  71. data/app/views/kaminari/_page.html.erb +3 -0
  72. data/app/views/kaminari/_paginator.html.erb +13 -0
  73. data/app/views/kaminari/_prev_page.html.erb +1 -0
  74. data/app/views/layouts/moduler/application.html.erb +5 -6
  75. data/app/views/layouts/moduler/articles.html.erb +18 -0
  76. data/app/views/moduler/administration/articles/_article.html.erb +13 -0
  77. data/app/views/moduler/administration/articles/_form.html.erb +18 -0
  78. data/app/views/moduler/administration/articles/edit.html.erb +1 -0
  79. data/app/views/moduler/administration/articles/index.html.erb +13 -0
  80. data/app/views/moduler/administration/articles/new.html.erb +1 -0
  81. data/app/views/moduler/administration/home/index.html.erb +6 -0
  82. data/app/views/moduler/administration/sessions/_session.html.erb +30 -0
  83. data/app/views/moduler/administration/sessions/index.html.erb +9 -0
  84. data/app/views/moduler/administration/users/_user.html.erb +13 -0
  85. data/app/views/moduler/administration/users/edit.html.erb +14 -0
  86. data/app/views/moduler/administration/users/index.html.erb +13 -0
  87. data/app/views/moduler/administration/users/new.html.erb +18 -0
  88. data/app/views/moduler/articles/_article.html.erb +16 -0
  89. data/app/views/moduler/articles/index.html.erb +15 -0
  90. data/app/views/moduler/articles/show.html.erb +21 -0
  91. data/app/views/moduler/common/_flash.html.erb +16 -0
  92. data/app/views/moduler/common/_header.html.erb +12 -3
  93. data/app/views/moduler/common/_sidebar.html.erb +7 -0
  94. data/app/views/moduler/common/administration/_sidebar.html.erb +12 -0
  95. data/app/views/moduler/common/profile/_sidebar.html.erb +6 -0
  96. data/app/views/moduler/home/index.html.erb +6 -0
  97. data/app/views/moduler/profile/edit.html.erb +28 -0
  98. data/app/views/moduler/profile/sessions/_session.html.erb +52 -0
  99. data/app/views/moduler/profile/sessions/index.html.erb +26 -0
  100. data/app/views/moduler/sessions/new.html.erb +4 -18
  101. data/app/views/moduler/users/new.html.erb +5 -32
  102. data/config/importmap.rb +3 -0
  103. data/config/locales/moduler/administration/articles/en.yml +37 -0
  104. data/config/locales/moduler/administration/sessions/en.yml +24 -0
  105. data/config/locales/moduler/administration/users/en.yml +43 -0
  106. data/config/locales/moduler/articles/en.yml +13 -0
  107. data/config/locales/moduler/common/en.yml +12 -0
  108. data/config/locales/moduler/components/en.yml +27 -0
  109. data/config/locales/moduler/en.yml +4 -0
  110. data/config/locales/moduler/profile/en.yml +26 -0
  111. data/config/locales/moduler/profile/sessions/en.yml +25 -0
  112. data/config/locales/moduler/sessions/en.yml +1 -0
  113. data/config/locales/moduler/users/en.yml +7 -1
  114. data/config/routes.rb +18 -1
  115. data/db/migrate/20261015171504_add_role_to_moduler_users.rb +5 -0
  116. data/db/migrate/20261020191709_enable_pg_trgm.rb +5 -0
  117. data/db/migrate/20261026203827_create_moduler_articles.rb +15 -0
  118. data/lib/moduler/engine.rb +2 -0
  119. data/lib/moduler/version.rb +1 -1
  120. data/lib/moduler.rb +7 -0
  121. data/lib/tasks/moduler_tasks.rake +2 -4
  122. metadata +202 -5
  123. data/app/assets/javascripts/moduler/controllers/hello_controller.js +0 -6
  124. data/app/controllers/moduler/hello_controller.rb +0 -5
  125. data/app/views/moduler/hello/index.html.erb +0 -5
@@ -0,0 +1,25 @@
1
+ module Moduler
2
+ class Article < ApplicationRecord
3
+ include PgSearch::Model
4
+
5
+ PATH_FORMAT = /\A[a-z0-9]+(-[a-z0-9]+)*\z/
6
+ ALLOWED_TAGS = Rails::HTML5::SafeListSanitizer.allowed_tags + %w[ table thead tbody tr th td ]
7
+
8
+ has_many_attached :images
9
+
10
+ validates :path, presence: true, uniqueness: true, format: { with: PATH_FORMAT }
11
+ validates :title, presence: true
12
+ validates :description, presence: true
13
+ validates :keywords, presence: true
14
+ validates :content, presence: true
15
+ validates :images, content_type: %w[ image/png image/jpeg image/gif image/webp ], size: { less_than: 10.megabytes }
16
+
17
+ pg_search_scope :search_by_query, against: %i[ title keywords ], using: { trigram: { word_similarity: true } }
18
+
19
+ def to_html
20
+ html = Redcarpet::Markdown.new(Redcarpet::Render::HTML, tables: true, fenced_code_blocks: true).render(content.to_s)
21
+
22
+ ActionController::Base.helpers.sanitize(html, tags: ALLOWED_TAGS)
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,17 @@
1
1
  module Moduler
2
2
  class Session < ApplicationRecord
3
+ include PgSearch::Model
4
+
5
+ LIFETIME = 30.days
6
+
3
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
4
16
  end
5
17
  end
@@ -1,9 +1,18 @@
1
1
  module Moduler
2
2
  class User < ApplicationRecord
3
+ include PgSearch::Model
4
+
3
5
  has_many :sessions, class_name: "Moduler::Session", foreign_key: "moduler_user_id", dependent: :destroy
4
6
 
5
7
  has_secure_password
6
8
 
9
+ normalizes :email_address, with: ->(email_address) { email_address.strip.downcase }
10
+
7
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 } }
8
17
  end
9
18
  end
@@ -0,0 +1,29 @@
1
+ module Moduler
2
+ module Administration
3
+ class ArticlePolicy < BasePolicy
4
+ def index?
5
+ administrator?
6
+ end
7
+
8
+ def create?
9
+ administrator?
10
+ end
11
+
12
+ def update?
13
+ administrator?
14
+ end
15
+
16
+ def new?
17
+ create?
18
+ end
19
+
20
+ def edit?
21
+ update?
22
+ end
23
+
24
+ def destroy?
25
+ administrator?
26
+ end
27
+ end
28
+ end
29
+ 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" %>
@@ -2,6 +2,7 @@
2
2
  <html>
3
3
  <head>
4
4
  <title>Moduler</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
5
6
  <%= csrf_meta_tags %>
6
7
  <%= csp_meta_tag %>
7
8
 
@@ -9,14 +10,12 @@
9
10
 
10
11
  <%= stylesheet_link_tags_if_exist "moduler/tailwind", "moduler/application", media: "all" %>
11
12
 
12
- <%# Include host application stylesheets %>
13
- <%= stylesheet_link_tags_if_exist "tailwind", "application", media: "all" %>
14
-
15
13
  <%= javascript_importmap_tags "moduler/application" %>
16
14
  </head>
17
- <body>
18
- <div class="min-h-full">
19
- <%= render "moduler/common/header" %>
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" %>
20
19
 
21
20
  <%= yield %>
22
21
  </div>
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= content_for(:title) %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+
9
+ <%= yield :head %>
10
+
11
+ <%= stylesheet_link_tags_if_exist "moduler/tailwind", "moduler/application", media: "all" %>
12
+ </head>
13
+ <body class="flex min-h-screen flex-col">
14
+ <div class="flex flex-1 flex-col">
15
+ <%= yield %>
16
+ </div>
17
+ </body>
18
+ </html>
@@ -0,0 +1,13 @@
1
+ <%= render(Moduler::ListItemComponent.new(article.title, id: dom_id(article))) do |item| %>
2
+ <% item.with_meta do %>
3
+ <%= time_tag article.created_at, t(".created", time: time_ago_in_words(article.created_at)), title: l(article.created_at, format: :long) %>
4
+ <% end %>
5
+
6
+ <% if Moduler::Administration::ArticlePolicy.new(current_user, article).edit? %>
7
+ <% item.with_action do %>
8
+ <%= render(Moduler::ButtonLinkComponent.new(t(".action.edit"), moduler.edit_administration_article_path(article), size: :sm)) %>
9
+ <%= render(Moduler::ButtonComponent.new(t(".action.destroy"), moduler.administration_article_path(article), method: :delete, size: :sm,
10
+ confirm: t(".confirmation.destroy"))) %>
11
+ <% end %>
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: @article, url: @article.persisted? ? moduler.administration_article_path(@article) : moduler.administration_articles_path, data: { turbo: false }, class: "w-full space-y-6" do |form| %>
5
+ <%= render(::Moduler::Form::BaseErrorComponent.new(form)) %>
6
+
7
+ <div class="max-w-lg space-y-6">
8
+ <%= render(::Moduler::Form::TextComponent.new(form, :title, label: t(".label.title"))) %>
9
+ <%= render(::Moduler::Form::TextComponent.new(form, :path, label: t(".label.path"), description: t(".description.path"))) %>
10
+ <%= render(::Moduler::Form::TextareaComponent.new(form, :description, label: t(".label.description"), description: t(".description.description"))) %>
11
+ <%= render(::Moduler::Form::TextComponent.new(form, :keywords, label: t(".label.keywords"), description: t(".description.keywords"))) %>
12
+ </div>
13
+
14
+ <%= render(::Moduler::Form::WysiwygComponent.new(form, :content, label: t(".label.content"))) %>
15
+
16
+ <%= render(::Moduler::Form::SubmitComponent.new(form, text: t(".action.save"))) %>
17
+ <% end %>
18
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= render "form" %>
@@ -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_article_path)) %>
5
+ <% end %>
6
+ <% end %>
7
+
8
+ <%= render(Moduler::FilterComponent.new(@filter, moduler.administration_articles_path)) %>
9
+
10
+ <%= render(Moduler::ListComponent.new(@articles, id: "articles", empty: t(".empty"))) do %>
11
+ <%= render partial: "moduler/administration/articles/article", collection: @articles %>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= render "form" %>
@@ -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
+ <li id="<%= dom_id(article) %>">
2
+ <article class="group relative">
3
+ <%= time_tag article.created_at, l(article.created_at.to_date, format: :long), class: "text-xs text-slate-400" %>
4
+
5
+ <h2 class="mt-2 text-xl font-semibold tracking-tight text-gray-900 group-hover:text-custom">
6
+ <%= link_to moduler.article_path(article.path) do %>
7
+ <span class="absolute inset-0"></span>
8
+ <%= article.title %>
9
+ <% end %>
10
+ </h2>
11
+
12
+ <p class="mt-2 text-sm/6 text-slate-600"><%= article.description %></p>
13
+
14
+ <p class="mt-3 text-sm font-semibold text-gray-900 group-hover:text-custom"><%= t(".action.show") %> <span aria-hidden="true">&rarr;</span></p>
15
+ </article>
16
+ </li>
@@ -0,0 +1,15 @@
1
+ <main class="mx-auto w-full max-w-3xl flex-1 px-4 pb-4 md:mt-8">
2
+ <%= render(Moduler::PageHeaderComponent.new(t(".title"), description: t(".description"))) %>
3
+
4
+ <% if @articles.any? %>
5
+ <ul id="articles" role="list" class="mt-10 space-y-12">
6
+ <%= render partial: "moduler/articles/article", collection: @articles %>
7
+ </ul>
8
+
9
+ <div class="mt-12 flex justify-end">
10
+ <%= paginate @articles %>
11
+ </div>
12
+ <% else %>
13
+ <p class="mt-10 text-sm text-slate-500"><%= t(".empty") %></p>
14
+ <% end %>
15
+ </main>
@@ -0,0 +1,21 @@
1
+ <% content_for :title, @article.title %>
2
+ <% content_for :head do %>
3
+ <meta name="description" content="<%= @article.description %>">
4
+ <meta name="keywords" content="<%= @article.keywords %>">
5
+ <% end %>
6
+
7
+ <main class="mx-auto w-full max-w-3xl flex-1 px-4 pb-4 md:mt-8">
8
+ <%= link_to t(".action.index"), moduler.articles_path, class: "text-sm text-slate-500 hover:text-custom" %>
9
+
10
+ <article id="<%= dom_id(@article) %>" class="mt-6">
11
+ <header class="mb-8 border-b border-slate-200 pb-6">
12
+ <h1 class="text-2xl/9 font-bold tracking-tight text-gray-900"><%= @article.title %></h1>
13
+ <p class="mt-2 text-sm text-slate-500"><%= @article.description %></p>
14
+ <p class="mt-2 text-xs text-slate-400"><%= time_tag @article.created_at, l(@article.created_at.to_date, format: :long) %></p>
15
+ </header>
16
+
17
+ <div class="prose prose-slate prose-100 prose-img:w-full prose-img:max-w-5xl">
18
+ <%= @article.to_html %>
19
+ </div>
20
+ </article>
21
+ </main>
@@ -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 %>
@@ -1,9 +1,18 @@
1
1
  <nav class="border-b border-gray-200 bg-white">
2
- <div class="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
3
- <div class="flex items-center gap-8">
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
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 %>
5
10
  </div>
6
11
 
7
- <%= button_to t(".action.sign_out"), moduler.session_path, method: :delete, class: "cursor-pointer rounded-md px-3 py-1.5 text-sm/6 font-semibold text-gray-900 hover:bg-gray-50" %>
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>
8
17
  </div>
9
18
  </nav>
@@ -0,0 +1,7 @@
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
+ <% Array(Moduler.configuration.dig(:resources)).each do |resource| %>
4
+ <%= render(Moduler::SidebarActionComponent.new(t(".action.#{resource}"), moduler.url_for([resource.to_sym]), "moduler/#{resource}")) %>
5
+ <% end %>
6
+ </nav>
7
+ </aside>
@@ -0,0 +1,12 @@
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
+
6
+ <%= render(Moduler::SidebarActionComponent.new(t(".action.articles"), moduler.administration_articles_path, "moduler/administration/articles")) %>
7
+
8
+ <% Array(Moduler.configuration.dig(:administration, :resources)).each do |resource| %>
9
+ <%= render(Moduler::SidebarActionComponent.new(t(".action.#{resource}"), moduler.url_for([:administration, resource.to_sym]), "moduler/administration/#{resource}")) %>
10
+ <% end %>
11
+ </nav>
12
+ </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.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
+ <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/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,28 @@
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/profile/sidebar" %>
3
+
4
+ <div class="flex min-w-0 flex-1 flex-col pb-4 md:mt-8">
5
+ <div class="mb-4 flex min-h-9 items-center justify-between gap-4">
6
+ <h1 class="text-base font-semibold text-gray-900"><%= t(".title") %></h1>
7
+ </div>
8
+
9
+ <%= form_with model: @user, url: moduler.profile_path, method: :patch, data: { turbo: false }, class: "w-full max-w-lg space-y-6" do |form| %>
10
+ <%= render(::Moduler::Form::BaseErrorComponent.new(form)) %>
11
+
12
+ <%= render(::Moduler::Form::SectionComponent.new(t(".section.data"))) do %>
13
+ <%= render(::Moduler::Form::EmailComponent.new(form, :email_address, label: t(".label.email_address"), disabled: :disabled)) %>
14
+ <% end %>
15
+
16
+ <%= render(::Moduler::Form::SectionComponent.new(t(".section.password"), divider: true)) do %>
17
+ <%= render(::Moduler::Form::PasswordComponent.new(form, :password, label: t(".label.password"))) %>
18
+ <%= render(::Moduler::Form::PasswordComponent.new(form, :password_confirmation, label: t(".label.password_confirmation"), description: t(".description.password_confirmation"))) %>
19
+ <% end %>
20
+
21
+ <%= render(::Moduler::Form::SectionComponent.new(t(".section.current_password"), divider: true)) do %>
22
+ <%= render(::Moduler::Form::PasswordComponent.new(form, :password_challenge, label: t(".label.password_challenge"), description: t(".description.password_challenge"))) %>
23
+ <% end %>
24
+
25
+ <%= render(::Moduler::Form::SubmitComponent.new(form, text: t(".action.save"))) %>
26
+ <% end %>
27
+ </div>
28
+ </main>