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.
- checksums.yaml +4 -4
- data/app/assets/builds/moduler/tailwind.css +2103 -1
- data/app/assets/javascripts/moduler/application.js +1 -0
- data/app/assets/javascripts/moduler/controllers/flash_controller.js +19 -0
- data/app/assets/javascripts/moduler/controllers/wysiwyg_controller.js +126 -0
- data/app/assets/javascripts/moduler/vendor/marked.esm.js +77 -0
- data/app/assets/stylesheets/moduler/tailwind.css +3 -0
- data/app/components/moduler/administration/page_component.html.erb +7 -0
- data/app/components/moduler/administration/page_component.rb +6 -0
- data/app/components/moduler/badge_component.html.erb +8 -0
- data/app/components/moduler/badge_component.rb +10 -0
- data/app/components/moduler/button_component.html.erb +7 -0
- data/app/components/moduler/button_component.rb +19 -0
- data/app/components/moduler/button_link_component.html.erb +5 -0
- data/app/components/moduler/button_link_component.rb +15 -0
- data/app/components/moduler/filter_component.html.erb +12 -0
- data/app/components/moduler/filter_component.rb +15 -0
- data/app/components/moduler/form/base_error_component.html.erb +1 -0
- data/app/components/moduler/form/base_error_component.rb +9 -0
- data/app/components/moduler/form/email_component.html.erb +14 -0
- data/app/components/moduler/form/email_component.rb +11 -0
- data/app/components/moduler/form/password_component.html.erb +14 -0
- data/app/components/moduler/form/password_component.rb +11 -0
- data/app/components/moduler/form/section_component.html.erb +5 -0
- data/app/components/moduler/form/section_component.rb +14 -0
- data/app/components/moduler/form/select_component.html.erb +14 -0
- data/app/components/moduler/form/select_component.rb +12 -0
- data/app/components/moduler/form/submit_component.html.erb +3 -0
- data/app/components/moduler/form/submit_component.rb +10 -0
- data/app/components/moduler/form/text_component.html.erb +14 -0
- data/app/components/moduler/form/text_component.rb +11 -0
- data/app/components/moduler/form/textarea_component.html.erb +14 -0
- data/app/components/moduler/form/textarea_component.rb +11 -0
- data/app/components/moduler/form/wysiwyg_component.html.erb +30 -0
- data/app/components/moduler/form/wysiwyg_component.rb +62 -0
- data/app/components/moduler/list_component.html.erb +15 -0
- data/app/components/moduler/list_component.rb +18 -0
- data/app/components/moduler/list_item_component.html.erb +23 -0
- data/app/components/moduler/list_item_component.rb +14 -0
- data/app/components/moduler/page_header_component.html.erb +16 -0
- data/app/components/moduler/page_header_component.rb +12 -0
- data/app/components/moduler/sidebar_action_component.html.erb +5 -0
- data/app/components/moduler/sidebar_action_component.rb +15 -0
- data/app/controllers/concerns/moduler/authentication.rb +10 -2
- data/app/controllers/moduler/administration/application_controller.rb +7 -0
- data/app/controllers/moduler/administration/articles_controller.rb +57 -0
- data/app/controllers/moduler/administration/home_controller.rb +9 -0
- data/app/controllers/moduler/administration/sessions_controller.rb +34 -0
- data/app/controllers/moduler/administration/users_controller.rb +56 -0
- data/app/controllers/moduler/application_controller.rb +15 -0
- data/app/controllers/moduler/articles_controller.rb +15 -0
- data/app/controllers/moduler/profile/sessions/revocation_controller.rb +14 -0
- data/app/controllers/moduler/profile/sessions_controller.rb +21 -0
- data/app/controllers/moduler/profile_controller.rb +27 -0
- data/app/controllers/moduler/sessions_controller.rb +8 -1
- data/app/controllers/moduler/users_controller.rb +7 -0
- data/app/filters/application_filter.rb +41 -0
- data/app/filters/query_filter.rb +18 -0
- data/app/lib/moduler/form_builder.rb +2 -2
- data/app/models/moduler/article.rb +25 -0
- data/app/models/moduler/session.rb +12 -0
- data/app/models/moduler/user.rb +9 -0
- data/app/policies/moduler/administration/article_policy.rb +29 -0
- data/app/policies/moduler/administration/base_policy.rb +11 -0
- data/app/policies/moduler/administration/home_policy.rb +9 -0
- data/app/policies/moduler/administration/session_policy.rb +13 -0
- data/app/policies/moduler/administration/user_policy.rb +25 -0
- data/app/policies/moduler/application_policy.rb +38 -0
- data/app/views/kaminari/_gap.html.erb +1 -0
- data/app/views/kaminari/_next_page.html.erb +1 -0
- data/app/views/kaminari/_page.html.erb +3 -0
- data/app/views/kaminari/_paginator.html.erb +13 -0
- data/app/views/kaminari/_prev_page.html.erb +1 -0
- data/app/views/layouts/moduler/application.html.erb +5 -6
- data/app/views/layouts/moduler/articles.html.erb +18 -0
- data/app/views/moduler/administration/articles/_article.html.erb +13 -0
- data/app/views/moduler/administration/articles/_form.html.erb +18 -0
- data/app/views/moduler/administration/articles/edit.html.erb +1 -0
- data/app/views/moduler/administration/articles/index.html.erb +13 -0
- data/app/views/moduler/administration/articles/new.html.erb +1 -0
- data/app/views/moduler/administration/home/index.html.erb +6 -0
- data/app/views/moduler/administration/sessions/_session.html.erb +30 -0
- data/app/views/moduler/administration/sessions/index.html.erb +9 -0
- data/app/views/moduler/administration/users/_user.html.erb +13 -0
- data/app/views/moduler/administration/users/edit.html.erb +14 -0
- data/app/views/moduler/administration/users/index.html.erb +13 -0
- data/app/views/moduler/administration/users/new.html.erb +18 -0
- data/app/views/moduler/articles/_article.html.erb +16 -0
- data/app/views/moduler/articles/index.html.erb +15 -0
- data/app/views/moduler/articles/show.html.erb +21 -0
- data/app/views/moduler/common/_flash.html.erb +16 -0
- data/app/views/moduler/common/_header.html.erb +12 -3
- data/app/views/moduler/common/_sidebar.html.erb +7 -0
- data/app/views/moduler/common/administration/_sidebar.html.erb +12 -0
- data/app/views/moduler/common/profile/_sidebar.html.erb +6 -0
- data/app/views/moduler/home/index.html.erb +6 -0
- data/app/views/moduler/profile/edit.html.erb +28 -0
- data/app/views/moduler/profile/sessions/_session.html.erb +52 -0
- data/app/views/moduler/profile/sessions/index.html.erb +26 -0
- data/app/views/moduler/sessions/new.html.erb +4 -18
- data/app/views/moduler/users/new.html.erb +5 -32
- data/config/importmap.rb +3 -0
- data/config/locales/moduler/administration/articles/en.yml +37 -0
- data/config/locales/moduler/administration/sessions/en.yml +24 -0
- data/config/locales/moduler/administration/users/en.yml +43 -0
- data/config/locales/moduler/articles/en.yml +13 -0
- data/config/locales/moduler/common/en.yml +12 -0
- data/config/locales/moduler/components/en.yml +27 -0
- data/config/locales/moduler/en.yml +4 -0
- data/config/locales/moduler/profile/en.yml +26 -0
- data/config/locales/moduler/profile/sessions/en.yml +25 -0
- data/config/locales/moduler/sessions/en.yml +1 -0
- data/config/locales/moduler/users/en.yml +7 -1
- data/config/routes.rb +18 -1
- data/db/migrate/20261015171504_add_role_to_moduler_users.rb +5 -0
- data/db/migrate/20261020191709_enable_pg_trgm.rb +5 -0
- data/db/migrate/20261026203827_create_moduler_articles.rb +15 -0
- data/lib/moduler/engine.rb +2 -0
- data/lib/moduler/version.rb +1 -1
- data/lib/moduler.rb +7 -0
- data/lib/tasks/moduler_tasks.rake +2 -4
- metadata +202 -5
- data/app/assets/javascripts/moduler/controllers/hello_controller.js +0 -6
- data/app/controllers/moduler/hello_controller.rb +0 -5
- 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
|
data/app/models/moduler/user.rb
CHANGED
|
@@ -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,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,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="
|
|
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,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">·</span>
|
|
15
|
+
<span title="<%= session.user_agent %>"><%= truncate(session.user_agent.presence || t(".unknown_device"), length: 60) %></span>
|
|
16
|
+
<span aria-hidden="true">·</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">·</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">→</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="
|
|
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
|
-
|
|
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,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>
|