railspress-engine 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +67 -0
- data/Rakefile +10 -0
- data/app/assets/javascripts/railspress/admin.js +210 -0
- data/app/assets/stylesheets/actiontext.css +440 -0
- data/app/assets/stylesheets/railspress/admin.css +1199 -0
- data/app/assets/stylesheets/railspress/application.css +15 -0
- data/app/controllers/railspress/admin/base_controller.rb +38 -0
- data/app/controllers/railspress/admin/categories_controller.rb +53 -0
- data/app/controllers/railspress/admin/dashboard_controller.rb +12 -0
- data/app/controllers/railspress/admin/posts_controller.rb +71 -0
- data/app/controllers/railspress/admin/tags_controller.rb +50 -0
- data/app/controllers/railspress/application_controller.rb +4 -0
- data/app/helpers/railspress/application_helper.rb +4 -0
- data/app/jobs/railspress/application_job.rb +4 -0
- data/app/mailers/railspress/application_mailer.rb +6 -0
- data/app/models/railspress/application_record.rb +6 -0
- data/app/models/railspress/category.rb +18 -0
- data/app/models/railspress/post.rb +74 -0
- data/app/models/railspress/post_tag.rb +8 -0
- data/app/models/railspress/tag.rb +32 -0
- data/app/views/active_storage/blobs/_blob.html.erb +14 -0
- data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
- data/app/views/layouts/railspress/admin.html.erb +41 -0
- data/app/views/layouts/railspress/application.html.erb +17 -0
- data/app/views/railspress/admin/categories/_form.html.erb +32 -0
- data/app/views/railspress/admin/categories/edit.html.erb +6 -0
- data/app/views/railspress/admin/categories/index.html.erb +42 -0
- data/app/views/railspress/admin/categories/new.html.erb +6 -0
- data/app/views/railspress/admin/dashboard/index.html.erb +45 -0
- data/app/views/railspress/admin/posts/_form.html.erb +129 -0
- data/app/views/railspress/admin/posts/edit.html.erb +2 -0
- data/app/views/railspress/admin/posts/index.html.erb +55 -0
- data/app/views/railspress/admin/posts/new.html.erb +2 -0
- data/app/views/railspress/admin/posts/show.html.erb +47 -0
- data/app/views/railspress/admin/shared/_flash.html.erb +5 -0
- data/app/views/railspress/admin/shared/_sidebar.html.erb +38 -0
- data/app/views/railspress/admin/tags/_form.html.erb +27 -0
- data/app/views/railspress/admin/tags/edit.html.erb +6 -0
- data/app/views/railspress/admin/tags/index.html.erb +42 -0
- data/app/views/railspress/admin/tags/new.html.erb +6 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20241218000001_create_railspress_categories.rb +14 -0
- data/db/migrate/20241218000002_create_railspress_tags.rb +13 -0
- data/db/migrate/20241218000003_create_railspress_posts.rb +19 -0
- data/db/migrate/20241218000004_create_railspress_post_tags.rb +12 -0
- data/lib/generators/railspress/authors/authors_generator.rb +73 -0
- data/lib/generators/railspress/authors/templates/add_author_to_railspress_posts.rb.tt +8 -0
- data/lib/generators/railspress/authors/templates/railspress.rb.tt +31 -0
- data/lib/generators/railspress/install/install_generator.rb +144 -0
- data/lib/railspress/engine.rb +14 -0
- data/lib/railspress/version.rb +3 -0
- data/lib/railspress.rb +79 -0
- data/lib/tasks/railspress_tasks.rake +4 -0
- metadata +141 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Railspress
|
|
2
|
+
module Admin
|
|
3
|
+
class BaseController < ActionController::Base
|
|
4
|
+
layout "railspress/admin"
|
|
5
|
+
|
|
6
|
+
helper_method :current_author, :available_authors, :authors_enabled?, :header_images_enabled?
|
|
7
|
+
|
|
8
|
+
# Authentication hook - to be configured later
|
|
9
|
+
# before_action :authenticate_admin!
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def set_flash(type, message)
|
|
14
|
+
flash[type] = message
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def authors_enabled?
|
|
18
|
+
Railspress.authors_enabled?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def header_images_enabled?
|
|
22
|
+
Railspress.header_images_enabled?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def current_author
|
|
26
|
+
return nil unless authors_enabled?
|
|
27
|
+
method_name = Railspress.current_author_method
|
|
28
|
+
return nil unless respond_to?(method_name, true)
|
|
29
|
+
send(method_name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def available_authors
|
|
33
|
+
return [] unless authors_enabled?
|
|
34
|
+
Railspress.available_authors
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Railspress
|
|
2
|
+
module Admin
|
|
3
|
+
class CategoriesController < BaseController
|
|
4
|
+
before_action :set_category, only: [:edit, :update, :destroy]
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@categories = Category.ordered
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def new
|
|
11
|
+
@category = Category.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
@category = Category.new(category_params)
|
|
16
|
+
if @category.save
|
|
17
|
+
redirect_to admin_categories_path, notice: "Category created."
|
|
18
|
+
else
|
|
19
|
+
render :new, status: :unprocessable_entity
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def edit
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
if @category.update(category_params)
|
|
28
|
+
redirect_to admin_categories_path, notice: "Category updated."
|
|
29
|
+
else
|
|
30
|
+
render :edit, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def destroy
|
|
35
|
+
if @category.destroy
|
|
36
|
+
redirect_to admin_categories_path, notice: "Category deleted."
|
|
37
|
+
else
|
|
38
|
+
redirect_to admin_categories_path, alert: "Cannot delete category with posts."
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def set_category
|
|
45
|
+
@category = Category.find(params[:id])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def category_params
|
|
49
|
+
params.require(:category).permit(:name, :slug, :description)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module Railspress
|
|
2
|
+
module Admin
|
|
3
|
+
class PostsController < BaseController
|
|
4
|
+
before_action :set_post, only: [:show, :edit, :update, :destroy]
|
|
5
|
+
before_action :load_categories, only: [:new, :create, :edit, :update]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@posts = Post.includes(:category, :tags).ordered
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def new
|
|
15
|
+
@post = Post.new
|
|
16
|
+
@post.author = current_author if authors_enabled?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create
|
|
20
|
+
@post = Post.new(post_params)
|
|
21
|
+
if @post.save
|
|
22
|
+
redirect_to admin_post_path(@post), notice: "Post created."
|
|
23
|
+
else
|
|
24
|
+
render :new, status: :unprocessable_entity
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def edit
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def update
|
|
32
|
+
if @post.update(post_params)
|
|
33
|
+
redirect_to admin_post_path(@post), notice: "Post updated."
|
|
34
|
+
else
|
|
35
|
+
render :edit, status: :unprocessable_entity
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def destroy
|
|
40
|
+
@post.destroy
|
|
41
|
+
redirect_to admin_posts_path, notice: "Post deleted."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def set_post
|
|
47
|
+
@post = Post.find(params[:id])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def load_categories
|
|
51
|
+
@categories = Category.ordered
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def post_params
|
|
55
|
+
permitted = [
|
|
56
|
+
:title,
|
|
57
|
+
:slug,
|
|
58
|
+
:category_id,
|
|
59
|
+
:content,
|
|
60
|
+
:status,
|
|
61
|
+
:meta_title,
|
|
62
|
+
:meta_description,
|
|
63
|
+
:tag_list
|
|
64
|
+
]
|
|
65
|
+
permitted << :author_id if authors_enabled?
|
|
66
|
+
permitted.push(:header_image, :remove_header_image) if header_images_enabled?
|
|
67
|
+
params.require(:post).permit(permitted)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Railspress
|
|
2
|
+
module Admin
|
|
3
|
+
class TagsController < BaseController
|
|
4
|
+
before_action :set_tag, only: [:edit, :update, :destroy]
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@tags = Tag.ordered
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def new
|
|
11
|
+
@tag = Tag.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
@tag = Tag.new(tag_params)
|
|
16
|
+
if @tag.save
|
|
17
|
+
redirect_to admin_tags_path, notice: "Tag created."
|
|
18
|
+
else
|
|
19
|
+
render :new, status: :unprocessable_entity
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def edit
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
if @tag.update(tag_params)
|
|
28
|
+
redirect_to admin_tags_path, notice: "Tag updated."
|
|
29
|
+
else
|
|
30
|
+
render :edit, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def destroy
|
|
35
|
+
@tag.destroy
|
|
36
|
+
redirect_to admin_tags_path, notice: "Tag deleted."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def set_tag
|
|
42
|
+
@tag = Tag.find(params[:id])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def tag_params
|
|
46
|
+
params.require(:tag).permit(:name, :slug)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Railspress
|
|
2
|
+
class Category < ApplicationRecord
|
|
3
|
+
has_many :posts, dependent: :restrict_with_error
|
|
4
|
+
|
|
5
|
+
validates :name, presence: true, uniqueness: true
|
|
6
|
+
validates :slug, presence: true, uniqueness: true
|
|
7
|
+
|
|
8
|
+
before_validation :generate_slug, if: -> { slug.blank? && name.present? }
|
|
9
|
+
|
|
10
|
+
scope :ordered, -> { order(:name) }
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def generate_slug
|
|
15
|
+
self.slug = name.parameterize
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Railspress
|
|
2
|
+
class Post < ApplicationRecord
|
|
3
|
+
belongs_to :category, optional: true
|
|
4
|
+
# Author association - only functional when Railspress.authors_enabled?
|
|
5
|
+
# The author class is configured via Railspress.configure { |c| c.author_class_name = "User" }
|
|
6
|
+
def author
|
|
7
|
+
return nil unless author_id.present? && Railspress.authors_enabled?
|
|
8
|
+
Railspress.author_class.find_by(id: author_id)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def author=(user)
|
|
12
|
+
self.author_id = user&.id
|
|
13
|
+
end
|
|
14
|
+
has_many :post_tags, dependent: :destroy
|
|
15
|
+
has_many :tags, through: :post_tags
|
|
16
|
+
|
|
17
|
+
has_rich_text :content
|
|
18
|
+
has_one_attached :header_image
|
|
19
|
+
|
|
20
|
+
# Virtual attribute for removing header image via checkbox
|
|
21
|
+
attr_accessor :remove_header_image
|
|
22
|
+
before_save :purge_header_image, if: -> { remove_header_image == "1" }
|
|
23
|
+
|
|
24
|
+
enum :status, { draft: 0, published: 1 }, default: :draft
|
|
25
|
+
|
|
26
|
+
validates :title, presence: true
|
|
27
|
+
validates :slug, presence: true, uniqueness: true
|
|
28
|
+
|
|
29
|
+
before_validation :generate_slug, if: -> { slug.blank? && title.present? }
|
|
30
|
+
before_save :set_published_at
|
|
31
|
+
|
|
32
|
+
scope :ordered, -> { order(created_at: :desc) }
|
|
33
|
+
scope :recent, -> { ordered.limit(10) }
|
|
34
|
+
scope :published, -> { where(status: :published).where.not(published_at: nil) }
|
|
35
|
+
scope :drafts, -> { where(status: :draft) }
|
|
36
|
+
scope :by_author, ->(author) { where(author_id: author.id) }
|
|
37
|
+
|
|
38
|
+
# Accepts CSV string and syncs tags
|
|
39
|
+
def tag_list=(csv_string)
|
|
40
|
+
self.tags = Tag.from_csv(csv_string)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def tag_list
|
|
44
|
+
tags.pluck(:name).join(", ")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def generate_slug
|
|
50
|
+
base_slug = title.parameterize
|
|
51
|
+
slug_candidate = base_slug
|
|
52
|
+
counter = 1
|
|
53
|
+
|
|
54
|
+
while self.class.where(slug: slug_candidate).where.not(id: id).exists?
|
|
55
|
+
slug_candidate = "#{base_slug}-#{counter}"
|
|
56
|
+
counter += 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
self.slug = slug_candidate
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def set_published_at
|
|
63
|
+
if published? && published_at.nil?
|
|
64
|
+
self.published_at = Time.current
|
|
65
|
+
elsif draft?
|
|
66
|
+
self.published_at = nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def purge_header_image
|
|
71
|
+
header_image.purge_later
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Railspress
|
|
2
|
+
class Tag < ApplicationRecord
|
|
3
|
+
has_many :post_tags, dependent: :destroy
|
|
4
|
+
has_many :posts, through: :post_tags
|
|
5
|
+
|
|
6
|
+
validates :name, presence: true, uniqueness: { case_sensitive: false }
|
|
7
|
+
validates :slug, presence: true, uniqueness: true
|
|
8
|
+
|
|
9
|
+
before_validation :normalize_name
|
|
10
|
+
before_validation :generate_slug, if: -> { slug.blank? && name.present? }
|
|
11
|
+
|
|
12
|
+
scope :ordered, -> { order(:name) }
|
|
13
|
+
|
|
14
|
+
# Find or create tags from CSV string
|
|
15
|
+
def self.from_csv(csv_string)
|
|
16
|
+
return [] if csv_string.blank?
|
|
17
|
+
|
|
18
|
+
tag_names = csv_string.split(",").map { |t| t.strip.downcase }.reject(&:blank?).uniq
|
|
19
|
+
tag_names.map { |name| find_or_create_by(name: name) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def normalize_name
|
|
25
|
+
self.name = name.strip.downcase if name.present?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def generate_slug
|
|
29
|
+
self.slug = name.parameterize
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
|
|
2
|
+
<% if blob.representable? %>
|
|
3
|
+
<%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
|
|
4
|
+
<% end %>
|
|
5
|
+
|
|
6
|
+
<figcaption class="attachment__caption">
|
|
7
|
+
<% if caption = blob.try(:caption) %>
|
|
8
|
+
<%= caption %>
|
|
9
|
+
<% else %>
|
|
10
|
+
<span class="attachment__name"><%= blob.filename %></span>
|
|
11
|
+
<span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
|
|
12
|
+
<% end %>
|
|
13
|
+
</figcaption>
|
|
14
|
+
</figure>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title><%= content_for?(:title) ? "#{yield(:title)} - " : "" %>RailsPress Admin</title>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<%= csp_meta_tag %>
|
|
9
|
+
<%= stylesheet_link_tag "lexxy", media: "all" %>
|
|
10
|
+
<%= stylesheet_link_tag "railspress/admin", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div class="rp-admin-layout">
|
|
14
|
+
<!-- Mobile Header -->
|
|
15
|
+
<header class="rp-mobile-header">
|
|
16
|
+
<button class="rp-hamburger" aria-label="Toggle menu" aria-expanded="false">
|
|
17
|
+
<span></span>
|
|
18
|
+
<span></span>
|
|
19
|
+
<span></span>
|
|
20
|
+
</button>
|
|
21
|
+
<span class="rp-mobile-logo">RailsPress</span>
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
<div class="rp-overlay"></div>
|
|
25
|
+
|
|
26
|
+
<!-- Sidebar -->
|
|
27
|
+
<aside class="rp-sidebar">
|
|
28
|
+
<%= render "railspress/admin/shared/sidebar" %>
|
|
29
|
+
</aside>
|
|
30
|
+
|
|
31
|
+
<!-- Main Content -->
|
|
32
|
+
<main class="rp-main">
|
|
33
|
+
<%= render "railspress/admin/shared/flash" %>
|
|
34
|
+
<%= yield %>
|
|
35
|
+
</main>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<%= javascript_importmap_tags %>
|
|
39
|
+
<%= javascript_include_tag "railspress/admin" %>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Railspress</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "railspress/application", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<%= yield %>
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<%= form_with model: [:admin, @category], class: "rp-form rp-form--narrow" do |form| %>
|
|
2
|
+
<% if @category.errors.any? %>
|
|
3
|
+
<div class="rp-form-errors">
|
|
4
|
+
<ul>
|
|
5
|
+
<% @category.errors.full_messages.each do |msg| %>
|
|
6
|
+
<li><%= msg %></li>
|
|
7
|
+
<% end %>
|
|
8
|
+
</ul>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<div class="rp-form-group">
|
|
13
|
+
<%= form.label :name, class: "rp-label rp-label--lg rp-label--required" %>
|
|
14
|
+
<%= form.text_field :name, class: "rp-input rp-input--lg", placeholder: "Category name", data: { slug_source: true }, required: true %>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="rp-form-group">
|
|
18
|
+
<%= form.label :slug, class: "rp-label" %>
|
|
19
|
+
<%= form.text_field :slug, class: "rp-input rp-input--mono", placeholder: "auto-generated-if-blank", data: { slug_target: true } %>
|
|
20
|
+
<p class="rp-hint">Leave blank to auto-generate from name</p>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="rp-form-group">
|
|
24
|
+
<%= form.label :description, class: "rp-label" %>
|
|
25
|
+
<%= form.text_area :description, rows: 3, class: "rp-input", placeholder: "Brief description of this category (optional)" %>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="rp-form-actions">
|
|
29
|
+
<%= form.submit class: "rp-btn rp-btn--primary" %>
|
|
30
|
+
<%= link_to "Cancel", admin_categories_path, class: "rp-btn rp-btn--secondary" %>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<% content_for :title, "Categories" %>
|
|
2
|
+
|
|
3
|
+
<div class="rp-page-header">
|
|
4
|
+
<h1 class="rp-page-title">Categories</h1>
|
|
5
|
+
<div class="rp-page-actions">
|
|
6
|
+
<%= link_to "New Category", new_admin_category_path, class: "rp-btn rp-btn--primary" %>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="rp-card">
|
|
11
|
+
<% if @categories.any? %>
|
|
12
|
+
<div class="rp-table--responsive">
|
|
13
|
+
<table class="rp-table">
|
|
14
|
+
<thead>
|
|
15
|
+
<tr>
|
|
16
|
+
<th>Name</th>
|
|
17
|
+
<th>Slug</th>
|
|
18
|
+
<th>Posts</th>
|
|
19
|
+
<th class="rp-table-actions">Actions</th>
|
|
20
|
+
</tr>
|
|
21
|
+
</thead>
|
|
22
|
+
<tbody>
|
|
23
|
+
<% @categories.each do |category| %>
|
|
24
|
+
<tr>
|
|
25
|
+
<td class="rp-table-primary"><%= category.name %></td>
|
|
26
|
+
<td class="rp-table-secondary"><%= category.slug %></td>
|
|
27
|
+
<td><%= category.posts.count %></td>
|
|
28
|
+
<td class="rp-table-actions">
|
|
29
|
+
<%= link_to "Edit", edit_admin_category_path(category), class: "rp-link" %>
|
|
30
|
+
<%= button_to "Delete", admin_category_path(category), method: :delete,
|
|
31
|
+
data: { turbo_confirm: "Delete this category?" },
|
|
32
|
+
class: "rp-link rp-link--danger" %>
|
|
33
|
+
</td>
|
|
34
|
+
</tr>
|
|
35
|
+
<% end %>
|
|
36
|
+
</tbody>
|
|
37
|
+
</table>
|
|
38
|
+
</div>
|
|
39
|
+
<% else %>
|
|
40
|
+
<p class="rp-empty-state">No categories yet. <%= link_to "Create your first category", new_admin_category_path, class: "rp-link" %>.</p>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<% content_for :title, "Dashboard" %>
|
|
2
|
+
|
|
3
|
+
<div class="rp-page-header">
|
|
4
|
+
<h1 class="rp-page-title">Dashboard</h1>
|
|
5
|
+
<div class="rp-page-actions">
|
|
6
|
+
<%= link_to "New Post", new_admin_post_path, class: "rp-btn rp-btn--primary" %>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<!-- Stats Grid -->
|
|
11
|
+
<div class="rp-stats-grid">
|
|
12
|
+
<div class="rp-stat-card">
|
|
13
|
+
<div class="rp-stat-value"><%= @posts_count %></div>
|
|
14
|
+
<div class="rp-stat-label">Posts</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="rp-stat-card">
|
|
17
|
+
<div class="rp-stat-value"><%= @categories_count %></div>
|
|
18
|
+
<div class="rp-stat-label">Categories</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="rp-stat-card">
|
|
21
|
+
<div class="rp-stat-value"><%= @tags_count %></div>
|
|
22
|
+
<div class="rp-stat-label">Tags</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- Recent Posts -->
|
|
27
|
+
<div class="rp-section">
|
|
28
|
+
<div class="rp-section-header">
|
|
29
|
+
<h2 class="rp-section-title">Recent Posts</h2>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="rp-card">
|
|
32
|
+
<% if @recent_posts.any? %>
|
|
33
|
+
<ul class="rp-list">
|
|
34
|
+
<% @recent_posts.each do |post| %>
|
|
35
|
+
<li class="rp-list-item">
|
|
36
|
+
<%= link_to post.title, admin_post_path(post), class: "rp-link" %>
|
|
37
|
+
<span class="rp-badge rp-badge--<%= post.status %>"><%= post.status.titleize %></span>
|
|
38
|
+
</li>
|
|
39
|
+
<% end %>
|
|
40
|
+
</ul>
|
|
41
|
+
<% else %>
|
|
42
|
+
<p class="rp-empty-state">No posts yet. <%= link_to "Create your first post", new_admin_post_path, class: "rp-link" %>.</p>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|