macron-cms 1.0.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 +265 -0
- data/Rakefile +4 -0
- data/app/assets/stylesheets/macron/cms/application.css +2 -0
- data/app/assets/tailwind/macron/cms/application.css +4 -0
- data/app/components/macron/cms/widget_component.rb +29 -0
- data/app/controllers/macron/cms/application_controller.rb +36 -0
- data/app/controllers/macron/cms/article_contents_controller.rb +103 -0
- data/app/controllers/macron/cms/articles_controller.rb +56 -0
- data/app/controllers/macron/cms/content_types_controller.rb +20 -0
- data/app/controllers/macron/cms/dashboard_controller.rb +10 -0
- data/app/helpers/macron/cms/application_helper.rb +40 -0
- data/app/helpers/macron/cms/content_helper.rb +31 -0
- data/app/helpers/macron/cms/fields_helper.rb +30 -0
- data/app/helpers/macron/cms/seo_helper.rb +37 -0
- data/app/jobs/macron/cms/application_job.rb +6 -0
- data/app/mailers/macron/cms/application_mailer.rb +8 -0
- data/app/models/macron/cms/application_record.rb +7 -0
- data/app/models/macron/cms/article.rb +42 -0
- data/app/models/macron/cms/article_content.rb +31 -0
- data/app/models/macron/cms/content.rb +34 -0
- data/app/views/layouts/macron/cms/application.html.erb +129 -0
- data/app/views/macron/cms/articles/_form.html.erb +65 -0
- data/app/views/macron/cms/articles/_table.html.erb +54 -0
- data/app/views/macron/cms/articles/edit.html.erb +15 -0
- data/app/views/macron/cms/articles/index.html.erb +28 -0
- data/app/views/macron/cms/articles/new.html.erb +14 -0
- data/app/views/macron/cms/articles/show.html.erb +366 -0
- data/app/views/macron/cms/content_types/index.html.erb +87 -0
- data/app/views/macron/cms/content_types/show.html.erb +148 -0
- data/app/views/macron/cms/dashboard/index.html.erb +81 -0
- data/app/views/macron/cms/fields/_string.html.erb +14 -0
- data/app/views/macron/cms/fields/_text.html.erb +13 -0
- data/app/views/macron/cms/shared/_breadcrumbs.html.erb +14 -0
- data/config/routes.rb +14 -0
- data/db/migrate/20261028000001_create_macron_cms_articles.rb +9 -0
- data/db/migrate/20261028000002_add_name_and_slug_to_macron_cms_articles.rb +8 -0
- data/db/migrate/20261028000003_add_seo_metadata_to_macron_cms_articles.rb +6 -0
- data/db/migrate/20261028000004_create_macron_cms_contents.rb +13 -0
- data/db/migrate/20261028000005_create_macron_cms_article_contents.rb +13 -0
- data/db/migrate/20261028000006_move_payload_to_macron_cms_article_contents.rb +6 -0
- data/db/seeds.rb +83 -0
- data/lib/generators/macron/cms/content_type/content_type_generator.rb +88 -0
- data/lib/macron/cms/configuration.rb +25 -0
- data/lib/macron/cms/engine.rb +20 -0
- data/lib/macron/cms/version.rb +5 -0
- data/lib/macron/cms.rb +26 -0
- data/lib/tasks/macron/cms_tasks.rake +75 -0
- metadata +164 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<% content_for :title, "Dashboard - Macron CMS" %>
|
|
2
|
+
<% cms_breadcrumb "Dashboard" %>
|
|
3
|
+
|
|
4
|
+
<div class="space-y-8">
|
|
5
|
+
<!-- Page Header -->
|
|
6
|
+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 border-b border-zinc-200 pb-6">
|
|
7
|
+
<div>
|
|
8
|
+
<h1 class="text-2xl font-bold tracking-tight text-zinc-900 sm:text-3xl">CMS Dashboard</h1>
|
|
9
|
+
<p class="mt-1 text-sm text-zinc-500">Overview of your published content, system health, and quick actions.</p>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="flex items-center gap-3">
|
|
12
|
+
<%= link_to new_article_path, class: "inline-flex items-center gap-2 rounded-lg bg-zinc-900 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-zinc-800 transition-colors" do %>
|
|
13
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
14
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
15
|
+
</svg>
|
|
16
|
+
New Article
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Metric Stat Cards -->
|
|
22
|
+
<div class="grid grid-cols-1 gap-5 sm:grid-cols-3">
|
|
23
|
+
<!-- Stat 1: Total Articles -->
|
|
24
|
+
<div class="overflow-hidden rounded-xl bg-white p-6 border border-zinc-200 shadow-xs">
|
|
25
|
+
<div class="flex items-center justify-between">
|
|
26
|
+
<dt class="truncate text-sm font-medium text-zinc-500">Total Articles</dt>
|
|
27
|
+
<span class="rounded-md bg-zinc-100 p-2 text-zinc-700">
|
|
28
|
+
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
29
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
|
30
|
+
</svg>
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
<dd class="mt-3 text-3xl font-bold tracking-tight text-zinc-900"><%= @articles_count %></dd>
|
|
34
|
+
<p class="mt-1 text-xs text-zinc-500">Managed via Macron CMS</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Stat 2: Engine Version -->
|
|
38
|
+
<div class="overflow-hidden rounded-xl bg-white p-6 border border-zinc-200 shadow-xs">
|
|
39
|
+
<div class="flex items-center justify-between">
|
|
40
|
+
<dt class="truncate text-sm font-medium text-zinc-500">Engine Version</dt>
|
|
41
|
+
<span class="rounded-md bg-zinc-100 p-2 text-zinc-700">
|
|
42
|
+
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
43
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 0 1 0 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 0 1 0-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281Z" />
|
|
44
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
|
45
|
+
</svg>
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
<dd class="mt-3 text-3xl font-bold tracking-tight text-zinc-900 font-mono">v<%= Macron::Cms::VERSION %></dd>
|
|
49
|
+
<p class="mt-1 text-xs text-zinc-500">Standalone Tailwind Enabled</p>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Stat 3: Access Guard -->
|
|
53
|
+
<div class="overflow-hidden rounded-xl bg-white p-6 border border-zinc-200 shadow-xs">
|
|
54
|
+
<div class="flex items-center justify-between">
|
|
55
|
+
<dt class="truncate text-sm font-medium text-zinc-500">Access Guard</dt>
|
|
56
|
+
<span class="rounded-md bg-emerald-50 p-2 text-emerald-700">
|
|
57
|
+
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
58
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
|
|
59
|
+
</svg>
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
<dd class="mt-3 text-3xl font-bold tracking-tight text-zinc-900">Protected</dd>
|
|
63
|
+
<p class="mt-1 text-xs text-zinc-500">Devise <%= Macron::Cms.configuration.user_class %> (<%= Macron::Cms.configuration.admin_flag %>: true)</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Recent Articles Section -->
|
|
68
|
+
<div class="space-y-4">
|
|
69
|
+
<div class="flex items-center justify-between">
|
|
70
|
+
<div>
|
|
71
|
+
<h2 class="text-lg font-semibold text-zinc-900">Recent Articles</h2>
|
|
72
|
+
<p class="text-xs text-zinc-500">Latest content items updated or created in the engine.</p>
|
|
73
|
+
</div>
|
|
74
|
+
<% if @articles_count > 0 %>
|
|
75
|
+
<%= link_to "View all articles →".html_safe, articles_path, class: "text-sm font-medium text-zinc-700 hover:text-zinc-900 transition-colors" %>
|
|
76
|
+
<% end %>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<%= render "macron/cms/articles/table", articles: @recent_articles %>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<div class="space-y-1.5">
|
|
2
|
+
<label for="<%= input_id %>" class="block text-xs font-mono font-semibold text-zinc-700">
|
|
3
|
+
<%= field_name %>
|
|
4
|
+
<span class="ml-1 text-[10px] font-normal text-zinc-400">(string)</span>
|
|
5
|
+
</label>
|
|
6
|
+
<input
|
|
7
|
+
type="text"
|
|
8
|
+
name="<%= input_name %>"
|
|
9
|
+
id="<%= input_id %>"
|
|
10
|
+
value="<%= value %>"
|
|
11
|
+
class="block w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 shadow-2xs placeholder:text-zinc-400 focus:border-zinc-900 focus:outline-none focus:ring-1 focus:ring-zinc-900"
|
|
12
|
+
placeholder="Enter <%= field_name.to_s.humanize.downcase %>..."
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="space-y-1.5">
|
|
2
|
+
<label for="<%= input_id %>" class="block text-xs font-mono font-semibold text-zinc-700">
|
|
3
|
+
<%= field_name %>
|
|
4
|
+
<span class="ml-1 text-[10px] font-normal text-zinc-400">(text)</span>
|
|
5
|
+
</label>
|
|
6
|
+
<textarea
|
|
7
|
+
name="<%= input_name %>"
|
|
8
|
+
id="<%= input_id %>"
|
|
9
|
+
rows="4"
|
|
10
|
+
class="block w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 shadow-2xs placeholder:text-zinc-400 focus:border-zinc-900 focus:outline-none focus:ring-1 focus:ring-zinc-900"
|
|
11
|
+
placeholder="Enter <%= field_name.to_s.humanize.downcase %>..."
|
|
12
|
+
><%= value %></textarea>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<% if cms_breadcrumbs.any? %>
|
|
2
|
+
<% cms_breadcrumbs.each_with_index do |crumb, index| %>
|
|
3
|
+
<% if index > 0 %>
|
|
4
|
+
<span class="text-zinc-400 select-none">/</span>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% if index == cms_breadcrumbs.size - 1 || crumb[:path].blank? %>
|
|
7
|
+
<span class="font-medium text-zinc-900 truncate max-w-xs sm:max-w-md"><%= crumb[:label] %></span>
|
|
8
|
+
<% else %>
|
|
9
|
+
<%= link_to crumb[:label], crumb[:path], class: "hover:text-zinc-900 transition-colors" %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% else %>
|
|
13
|
+
<span class="font-medium text-zinc-900"><%= content_for(:header_title) || "Overview" %></span>
|
|
14
|
+
<% end %>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Macron::Cms::Engine.routes.draw do
|
|
2
|
+
root to: "dashboard#index"
|
|
3
|
+
resources :articles do
|
|
4
|
+
resources :article_contents, only: %i[create update destroy] do
|
|
5
|
+
member do
|
|
6
|
+
patch :move
|
|
7
|
+
end
|
|
8
|
+
collection do
|
|
9
|
+
patch :reorder
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
resources :content_types, only: %i[index show]
|
|
14
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class AddNameAndSlugToMacronCmsArticles < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :macron_cms_articles, :name, :string, null: false
|
|
4
|
+
add_column :macron_cms_articles, :slug, :string, null: false
|
|
5
|
+
add_index :macron_cms_articles, :name, unique: true
|
|
6
|
+
add_index :macron_cms_articles, :slug, unique: true
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateMacronCmsContents < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :macron_cms_contents do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.jsonb :fields, default: {}, null: false
|
|
6
|
+
t.jsonb :payload, default: {}, null: false
|
|
7
|
+
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index :macron_cms_contents, :name, unique: true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateMacronCmsArticleContents < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :macron_cms_article_contents do |t|
|
|
4
|
+
t.references :article, null: false, foreign_key: { to_table: :macron_cms_articles }
|
|
5
|
+
t.references :content, null: false, foreign_key: { to_table: :macron_cms_contents }
|
|
6
|
+
t.integer :sort_order, default: 0, null: false
|
|
7
|
+
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index :macron_cms_article_contents, [ :article_id, :sort_order ]
|
|
12
|
+
end
|
|
13
|
+
end
|
data/db/seeds.rb
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
articles = [
|
|
2
|
+
{
|
|
3
|
+
title: "Getting Started with MacronCMS",
|
|
4
|
+
meta_title: "Getting Started with MacronCMS | Rails 8 Content Engine",
|
|
5
|
+
meta_description: "Discover how to install, configure, and build interactive content architectures with MacronCMS on Rails 8."
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
title: "Designing Content Structures with Rails 8",
|
|
9
|
+
meta_title: "Designing Content Structures with Rails 8 | MacronCMS Guide",
|
|
10
|
+
meta_description: "A comprehensive guide to modeling content hierarchies, flexible attributes, and schema migrations in Rails 8."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
title: "The wp_loop Architecture: Clean Content Iteration",
|
|
14
|
+
meta_title: "Understanding The wp_loop Architecture in Rails | MacronCMS",
|
|
15
|
+
meta_description: "Explore the wp_loop-inspired data pattern for declarative, clean, and decoupled content iteration in Ruby on Rails."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
title: "Hydrating React and Three.js in Modern Server-Rendered Views",
|
|
19
|
+
meta_title: "Hydrating React and Three.js in Rails Views | MacronCMS",
|
|
20
|
+
meta_description: "Seamlessly integrate interactive React components and Three.js 3D scenes into server-rendered Rails templates."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: "Tailwind CSS Layouts for Dynamic Content",
|
|
24
|
+
meta_title: "Tailwind CSS Layouts for Dynamic Content Publishing | MacronCMS",
|
|
25
|
+
meta_description: "Master modern utility-first typography, fluid layouts, and responsive components for publishing with Tailwind CSS."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
title: "High-Performance Publishing with Hotwire and Turbo",
|
|
29
|
+
meta_title: "High-Performance Publishing with Hotwire & Turbo | MacronCMS",
|
|
30
|
+
meta_description: "Boost site speed and user experience by leveraging Turbo Drive, Turbo Frames, and Turbo Streams in your CMS."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: "Migrating Legacy Content into MacronCMS with AI Agents",
|
|
34
|
+
meta_title: "Migrating Legacy Content with AI Agents | MacronCMS",
|
|
35
|
+
meta_description: "How to use autonomous AI agents to transform, clean, and import unstructured legacy datasets into MacronCMS."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
title: "Building Accessible Digital Experiences with Rails",
|
|
39
|
+
meta_title: "Building Accessible Web Experiences with Rails | MacronCMS",
|
|
40
|
+
meta_description: "Best practices and strategies for building WCAG-compliant, accessible content layouts and UI controls in Rails."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: "Optimizing Content Queries and Asset Pipelines",
|
|
44
|
+
meta_title: "Optimizing Content Queries and Asset Pipelines | MacronCMS",
|
|
45
|
+
meta_description: "Techniques for eliminating N+1 queries, optimizing cache keys, and streamlining modern asset delivery pipelines."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: "The Future of Developer-First Content Management Systems",
|
|
49
|
+
meta_title: "The Future of Developer-First CMS Platforms | MacronCMS",
|
|
50
|
+
meta_description: "An editorial on modularity, headless workflows, AI integration, and the evolution of developer-first CMS platforms."
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
articles.each do |data|
|
|
55
|
+
article = Macron::Cms::Article.find_or_initialize_by(name: data[:title].parameterize)
|
|
56
|
+
article.title = data[:title]
|
|
57
|
+
article.meta_title = data[:meta_title]
|
|
58
|
+
article.meta_description = data[:meta_description]
|
|
59
|
+
article.save!
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
puts "Seeded #{Macron::Cms::Article.count} Macron::Cms articles."
|
|
63
|
+
|
|
64
|
+
user_class_name = Macron::Cms.configuration.user_class
|
|
65
|
+
user_class = user_class_name.safe_constantize
|
|
66
|
+
|
|
67
|
+
if user_class
|
|
68
|
+
admin_flag = Macron::Cms.configuration.admin_flag
|
|
69
|
+
|
|
70
|
+
admin_user = user_class.find_or_initialize_by(email: "admin@example.com")
|
|
71
|
+
admin_user.password = "password" if admin_user.respond_to?(:password=)
|
|
72
|
+
admin_user.password_confirmation = "password" if admin_user.respond_to?(:password_confirmation=)
|
|
73
|
+
admin_user.public_send(:"#{admin_flag}=", true) if admin_user.respond_to?(:"#{admin_flag}=")
|
|
74
|
+
admin_user.save!
|
|
75
|
+
|
|
76
|
+
regular_user = user_class.find_or_initialize_by(email: "user@example.com")
|
|
77
|
+
regular_user.password = "password" if regular_user.respond_to?(:password=)
|
|
78
|
+
regular_user.password_confirmation = "password" if regular_user.respond_to?(:password_confirmation=)
|
|
79
|
+
regular_user.public_send(:"#{admin_flag}=", false) if regular_user.respond_to?(:"#{admin_flag}=")
|
|
80
|
+
regular_user.save!
|
|
81
|
+
|
|
82
|
+
puts "Seeded test users (admin: admin@example.com, regular: user@example.com)."
|
|
83
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module Macron
|
|
6
|
+
module Cms
|
|
7
|
+
module Generators
|
|
8
|
+
class ContentTypeGenerator < ::Rails::Generators::NamedBase
|
|
9
|
+
desc "Generates a Macron CMS content type YAML definition file and a matching ViewComponent widget in app/components/widgets/."
|
|
10
|
+
|
|
11
|
+
argument :fields, type: :array, default: [], banner: "field:kind field:kind"
|
|
12
|
+
|
|
13
|
+
def create_content_type_file
|
|
14
|
+
parsed = parse_and_validate_fields
|
|
15
|
+
target_path = File.join(base_directory.to_s, "#{file_name}.yml")
|
|
16
|
+
create_file target_path, build_yaml(parsed)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_widget_component_files
|
|
20
|
+
parsed = parse_and_validate_fields
|
|
21
|
+
component_path = File.join("app/components/widgets", "#{file_name}_component.rb")
|
|
22
|
+
template_path = File.join("app/components/widgets", "#{file_name}_component.html.erb")
|
|
23
|
+
|
|
24
|
+
create_file component_path, build_widget_ruby
|
|
25
|
+
create_file template_path, build_widget_template(parsed)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def base_directory
|
|
31
|
+
Macron::Cms.configuration.content_types_path || "config/content_types"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def parse_and_validate_fields
|
|
35
|
+
@parsed_fields ||= fields.map do |field|
|
|
36
|
+
parts = field.split(":", 2)
|
|
37
|
+
if parts.length != 2 || parts[0].blank? || parts[1].blank?
|
|
38
|
+
raise Thor::Error, "Invalid field '#{field}'. Fields must be in the format 'name:kind' (e.g. title:string)."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
{ "name" => parts[0], "kind" => parts[1] }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_yaml(parsed_fields)
|
|
46
|
+
if parsed_fields.empty?
|
|
47
|
+
"fields: []\n"
|
|
48
|
+
else
|
|
49
|
+
lines = [ "fields:" ]
|
|
50
|
+
parsed_fields.each do |f|
|
|
51
|
+
lines << " - name: '#{f['name']}'"
|
|
52
|
+
lines << " kind: '#{f['kind']}'"
|
|
53
|
+
end
|
|
54
|
+
"#{lines.join("\n")}\n"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_widget_ruby
|
|
59
|
+
<<~RUBY
|
|
60
|
+
module Widgets
|
|
61
|
+
class #{file_name.camelize}Component < Macron::Cms::WidgetComponent
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
RUBY
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_widget_template(parsed_fields)
|
|
68
|
+
if parsed_fields.empty?
|
|
69
|
+
<<~ERB
|
|
70
|
+
<div class="my-4">
|
|
71
|
+
<!-- #{file_name.camelize} Widget -->
|
|
72
|
+
</div>
|
|
73
|
+
ERB
|
|
74
|
+
else
|
|
75
|
+
field_tags = parsed_fields.map do |f|
|
|
76
|
+
" <p><%= #{f['name']} %></p>"
|
|
77
|
+
end
|
|
78
|
+
<<~ERB
|
|
79
|
+
<div class="my-4">
|
|
80
|
+
#{field_tags.join("\n")}
|
|
81
|
+
</div>
|
|
82
|
+
ERB
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Macron
|
|
2
|
+
module Cms
|
|
3
|
+
class Configuration
|
|
4
|
+
attr_accessor :user_class, :admin_flag, :content_types_path
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@user_class = "User"
|
|
8
|
+
@admin_flag = :admin
|
|
9
|
+
@content_types_path = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def devise_scope
|
|
13
|
+
user_class.to_s.underscore.to_sym
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def authenticate_method
|
|
17
|
+
:"authenticate_#{devise_scope}!"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def current_user_method
|
|
21
|
+
:"current_#{devise_scope}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Macron
|
|
2
|
+
module Cms
|
|
3
|
+
class Engine < ::Rails::Engine
|
|
4
|
+
isolate_namespace Macron::Cms
|
|
5
|
+
|
|
6
|
+
initializer :append_migrations do |app|
|
|
7
|
+
config.paths["db/migrate"].expanded.each do |expanded_path|
|
|
8
|
+
app.config.paths["db/migrate"] << expanded_path
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
initializer "macron_cms.helpers" do
|
|
13
|
+
ActiveSupport.on_load(:action_view) do
|
|
14
|
+
include Macron::Cms::SeoHelper
|
|
15
|
+
include Macron::Cms::ContentHelper
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/macron/cms.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "friendly_id"
|
|
2
|
+
require "devise"
|
|
3
|
+
require "view_component"
|
|
4
|
+
require "macron/cms/version"
|
|
5
|
+
require "macron/cms/configuration"
|
|
6
|
+
require "macron/cms/engine"
|
|
7
|
+
|
|
8
|
+
module Macron
|
|
9
|
+
module Cms
|
|
10
|
+
class MissingWidgetError < StandardError; end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def configuration
|
|
14
|
+
@configuration ||= Configuration.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def configure
|
|
18
|
+
yield(configuration)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def reset_configuration!
|
|
22
|
+
@configuration = Configuration.new
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
namespace :macron do
|
|
2
|
+
namespace :cms do
|
|
3
|
+
desc "Import content types from YAML files into Macron::Cms::Content"
|
|
4
|
+
task import_content_types: :environment do
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
base_path = Macron::Cms.configuration.content_types_path ||
|
|
8
|
+
(defined?(Rails.root) && Rails.root ? Rails.root.join("config", "content_types") : File.expand_path("config/content_types"))
|
|
9
|
+
directory = Pathname.new(base_path)
|
|
10
|
+
|
|
11
|
+
unless directory.exist?
|
|
12
|
+
puts "Content types directory '#{directory}' does not exist. Skipping import."
|
|
13
|
+
next
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
files = Dir.glob(directory.join("*.{yml,yaml}")).sort
|
|
17
|
+
|
|
18
|
+
if files.empty?
|
|
19
|
+
puts "No YAML content types found in #{directory}."
|
|
20
|
+
next
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
files.each do |file|
|
|
24
|
+
content_type_name = File.basename(file, ".*")
|
|
25
|
+
data = YAML.safe_load_file(file, permitted_classes: [ Symbol ], aliases: true) || {}
|
|
26
|
+
fields = data["fields"] || data[:fields] || {}
|
|
27
|
+
|
|
28
|
+
content = Macron::Cms::Content.find_or_initialize_by(name: content_type_name)
|
|
29
|
+
content.fields = fields
|
|
30
|
+
content.save!
|
|
31
|
+
|
|
32
|
+
puts "Imported content type: #{content_type_name}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
namespace :content do
|
|
37
|
+
desc "Alias for macron:cms:import_content_types"
|
|
38
|
+
task import: :"macron:cms:import_content_types"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
namespace :tailwind do
|
|
42
|
+
def tailwind_executable
|
|
43
|
+
require "tailwindcss/ruby"
|
|
44
|
+
Tailwindcss::Ruby.executable
|
|
45
|
+
rescue LoadError
|
|
46
|
+
"tailwindcss"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc "Build Macron CMS Tailwind CSS"
|
|
50
|
+
task :build do
|
|
51
|
+
engine_root = File.expand_path("../../..", __dir__)
|
|
52
|
+
input = File.join(engine_root, "app/assets/tailwind/macron/cms/application.css")
|
|
53
|
+
output = File.join(engine_root, "app/assets/stylesheets/macron/cms/application.css")
|
|
54
|
+
|
|
55
|
+
exe = tailwind_executable
|
|
56
|
+
cmd = [ exe, "-i", input, "-o", output, "--minify" ]
|
|
57
|
+
puts "Building Macron CMS Tailwind CSS..."
|
|
58
|
+
success = system(*cmd, chdir: engine_root)
|
|
59
|
+
abort("Tailwind build failed") unless success
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
desc "Watch Macron CMS Tailwind CSS for changes"
|
|
63
|
+
task :watch do
|
|
64
|
+
engine_root = File.expand_path("../../..", __dir__)
|
|
65
|
+
input = File.join(engine_root, "app/assets/tailwind/macron/cms/application.css")
|
|
66
|
+
output = File.join(engine_root, "app/assets/stylesheets/macron/cms/application.css")
|
|
67
|
+
|
|
68
|
+
exe = tailwind_executable
|
|
69
|
+
cmd = [ exe, "-i", input, "-o", output, "-w" ]
|
|
70
|
+
puts "Watching Macron CMS Tailwind CSS..."
|
|
71
|
+
system(*cmd, chdir: engine_root)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|