plum-cms 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/CHANGELOG.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +252 -0
- data/app/assets/builds/tailwind.css +2 -0
- data/app/assets/images/plum-mark.svg +7 -0
- data/app/assets/images/table-needs-logo.svg +83 -0
- data/app/assets/javascripts/plum/application.js +15 -0
- data/app/assets/stylesheets/application.css +10 -0
- data/app/assets/stylesheets/plum/control_panel.css +2 -0
- data/app/assets/tailwind/application.css +47 -0
- data/app/controllers/plum/application_controller.rb +34 -0
- data/app/controllers/plum/cp/assets_controller.rb +84 -0
- data/app/controllers/plum/cp/base_controller.rb +45 -0
- data/app/controllers/plum/cp/content_types_controller.rb +59 -0
- data/app/controllers/plum/cp/dashboard_controller.rb +10 -0
- data/app/controllers/plum/cp/entries_controller.rb +240 -0
- data/app/controllers/plum/cp/form_definitions_controller.rb +105 -0
- data/app/controllers/plum/cp/form_submissions_controller.rb +26 -0
- data/app/controllers/plum/cp/globals_controller.rb +84 -0
- data/app/controllers/plum/cp/nav_items_controller.rb +68 -0
- data/app/controllers/plum/cp/nav_menus_controller.rb +56 -0
- data/app/controllers/plum/cp/site_settings_controller.rb +87 -0
- data/app/controllers/plum/cp/taxonomies_controller.rb +56 -0
- data/app/controllers/plum/cp/terms_controller.rb +55 -0
- data/app/controllers/plum/cp/theme_previews_controller.rb +27 -0
- data/app/controllers/plum/cp/themes_controller.rb +98 -0
- data/app/controllers/plum/form_submissions_controller.rb +44 -0
- data/app/controllers/plum/pages_controller.rb +161 -0
- data/app/controllers/plum/sessions_controller.rb +25 -0
- data/app/controllers/plum/theme_assets_controller.rb +14 -0
- data/app/helpers/application_helper.rb +2 -0
- data/app/helpers/markdown_helper.rb +21 -0
- data/app/helpers/plum/cp/assets_helper.rb +21 -0
- data/app/helpers/plum/cp/theme_settings_helper.rb +26 -0
- data/app/javascript/application.js +18 -0
- data/app/javascript/controllers/application.js +9 -0
- data/app/javascript/controllers/index.js +4 -0
- data/app/javascript/controllers/plum/blocks_editor_controller.js +275 -0
- data/app/javascript/controllers/plum/blueprint_controller.js +120 -0
- data/app/javascript/controllers/plum/form_fields_controller.js +84 -0
- data/app/javascript/controllers/plum/image_picker_controller.js +143 -0
- data/app/javascript/controllers/plum/theme_settings_controller.js +17 -0
- data/app/models/plum/application_record.rb +5 -0
- data/app/models/plum/asset.rb +69 -0
- data/app/models/plum/content_type.rb +35 -0
- data/app/models/plum/entry.rb +67 -0
- data/app/models/plum/entry_term.rb +8 -0
- data/app/models/plum/form_definition.rb +68 -0
- data/app/models/plum/form_submission.rb +76 -0
- data/app/models/plum/global.rb +24 -0
- data/app/models/plum/nav_item.rb +58 -0
- data/app/models/plum/nav_menu.rb +24 -0
- data/app/models/plum/site.rb +112 -0
- data/app/models/plum/site_scoped.rb +17 -0
- data/app/models/plum/site_setting.rb +35 -0
- data/app/models/plum/taxonomy.rb +24 -0
- data/app/models/plum/term.rb +23 -0
- data/app/models/plum/user.rb +16 -0
- data/app/services/plum/base_blocks.rb +161 -0
- data/app/services/plum/block_editor_config.rb +65 -0
- data/app/services/plum/block_library.rb +31 -0
- data/app/services/plum/builder_renderer.rb +63 -0
- data/app/services/plum/field_expander.rb +78 -0
- data/app/services/plum/form_renderer.rb +110 -0
- data/app/services/plum/liquid_context.rb +335 -0
- data/app/services/plum/liquid_filters.rb +58 -0
- data/app/services/plum/liquid_renderer.rb +49 -0
- data/app/services/plum/theme.rb +160 -0
- data/app/services/plum/theme_asset_path.rb +31 -0
- data/app/services/plum/theme_package_installer.rb +287 -0
- data/app/services/plum/theme_registry.rb +63 -0
- data/app/services/plum/theme_resolver.rb +55 -0
- data/app/services/plum/theme_settings_params.rb +54 -0
- data/app/themes/bagel-boy/assets/screenshot.svg +92 -0
- data/app/themes/bagel-boy/assets/theme.css +184 -0
- data/app/themes/bagel-boy/blocks/hours.liquid +4 -0
- data/app/themes/bagel-boy/blocks/menu_item.liquid +7 -0
- data/app/themes/bagel-boy/layouts/base.liquid +64 -0
- data/app/themes/bagel-boy/templates/collections/posts.liquid +32 -0
- data/app/themes/bagel-boy/templates/entries/_default.liquid +13 -0
- data/app/themes/bagel-boy/templates/entries/landing.liquid +4 -0
- data/app/themes/bagel-boy/templates/entries/pages.liquid +18 -0
- data/app/themes/bagel-boy/templates/entries/posts.liquid +8 -0
- data/app/themes/bagel-boy/templates/index.liquid +10 -0
- data/app/themes/bagel-boy/templates/search.liquid +28 -0
- data/app/themes/bagel-boy/theme.yml +47 -0
- data/app/themes/bagel-shop/assets/screenshot.svg +28 -0
- data/app/themes/bagel-shop/assets/theme.css +7 -0
- data/app/themes/bagel-shop/layouts/base.liquid +507 -0
- data/app/themes/bagel-shop/templates/entries/_default.liquid +15 -0
- data/app/themes/bagel-shop/templates/entries/pages.liquid +12 -0
- data/app/themes/bagel-shop/templates/entries/posts.liquid +19 -0
- data/app/themes/bagel-shop/templates/index.liquid +55 -0
- data/app/themes/bagel-shop/theme.yml +30 -0
- data/app/themes/default/assets/screenshot.svg +24 -0
- data/app/themes/default/assets/theme.css +89 -0
- data/app/themes/default/layouts/base.liquid +117 -0
- data/app/themes/default/templates/collections/_default.liquid +16 -0
- data/app/themes/default/templates/collections/posts.liquid +27 -0
- data/app/themes/default/templates/entries/_default.liquid +26 -0
- data/app/themes/default/templates/entries/landing.liquid +5 -0
- data/app/themes/default/templates/entries/pages.liquid +17 -0
- data/app/themes/default/templates/entries/posts.liquid +20 -0
- data/app/themes/default/templates/index.liquid +22 -0
- data/app/themes/default/templates/search.liquid +28 -0
- data/app/themes/default/templates/taxonomies/_default.liquid +25 -0
- data/app/themes/default/templates/taxonomies/_default_index.liquid +14 -0
- data/app/themes/default/theme.yml +23 -0
- data/app/views/layouts/plum/cp.html.erb +138 -0
- data/app/views/layouts/plum/session.html.erb +43 -0
- data/app/views/plum/cp/assets/_form.html.erb +44 -0
- data/app/views/plum/cp/assets/edit.html.erb +14 -0
- data/app/views/plum/cp/assets/index.html.erb +52 -0
- data/app/views/plum/cp/assets/new.html.erb +9 -0
- data/app/views/plum/cp/content_types/_form.html.erb +113 -0
- data/app/views/plum/cp/content_types/edit.html.erb +8 -0
- data/app/views/plum/cp/content_types/index.html.erb +48 -0
- data/app/views/plum/cp/content_types/new.html.erb +5 -0
- data/app/views/plum/cp/content_types/show.html.erb +59 -0
- data/app/views/plum/cp/dashboard/show.html.erb +50 -0
- data/app/views/plum/cp/entries/_form.html.erb +196 -0
- data/app/views/plum/cp/entries/edit.html.erb +9 -0
- data/app/views/plum/cp/entries/index.html.erb +51 -0
- data/app/views/plum/cp/entries/new.html.erb +5 -0
- data/app/views/plum/cp/form_definitions/_form.html.erb +90 -0
- data/app/views/plum/cp/form_definitions/edit.html.erb +11 -0
- data/app/views/plum/cp/form_definitions/index.html.erb +43 -0
- data/app/views/plum/cp/form_definitions/new.html.erb +5 -0
- data/app/views/plum/cp/form_definitions/show.html.erb +70 -0
- data/app/views/plum/cp/form_submissions/show.html.erb +20 -0
- data/app/views/plum/cp/globals/_form.html.erb +44 -0
- data/app/views/plum/cp/globals/edit.html.erb +11 -0
- data/app/views/plum/cp/globals/index.html.erb +41 -0
- data/app/views/plum/cp/globals/new.html.erb +5 -0
- data/app/views/plum/cp/globals/show.html.erb +14 -0
- data/app/views/plum/cp/nav_items/_form.html.erb +56 -0
- data/app/views/plum/cp/nav_items/edit.html.erb +11 -0
- data/app/views/plum/cp/nav_items/new.html.erb +6 -0
- data/app/views/plum/cp/nav_menus/_form.html.erb +33 -0
- data/app/views/plum/cp/nav_menus/_nav_item.html.erb +24 -0
- data/app/views/plum/cp/nav_menus/edit.html.erb +11 -0
- data/app/views/plum/cp/nav_menus/index.html.erb +41 -0
- data/app/views/plum/cp/nav_menus/new.html.erb +5 -0
- data/app/views/plum/cp/nav_menus/show.html.erb +24 -0
- data/app/views/plum/cp/shared/_icon.html.erb +39 -0
- data/app/views/plum/cp/shared/_image_picker.html.erb +38 -0
- data/app/views/plum/cp/site_settings/edit.html.erb +145 -0
- data/app/views/plum/cp/site_settings/show.html.erb +62 -0
- data/app/views/plum/cp/taxonomies/_form.html.erb +38 -0
- data/app/views/plum/cp/taxonomies/edit.html.erb +2 -0
- data/app/views/plum/cp/taxonomies/index.html.erb +38 -0
- data/app/views/plum/cp/taxonomies/new.html.erb +2 -0
- data/app/views/plum/cp/taxonomies/show.html.erb +46 -0
- data/app/views/plum/cp/terms/_form.html.erb +30 -0
- data/app/views/plum/cp/terms/edit.html.erb +2 -0
- data/app/views/plum/cp/terms/new.html.erb +2 -0
- data/app/views/plum/cp/themes/index.html.erb +160 -0
- data/app/views/plum/form_mailer/submission_notification.html.erb +13 -0
- data/app/views/plum/form_mailer/submission_notification.text.erb +7 -0
- data/app/views/plum/sessions/new.html.erb +20 -0
- data/compat/plum-cms.rb +1 -0
- data/config/locales/en.yml +31 -0
- data/config/plum_importmap.rb +4 -0
- data/config/plum_routes.rb +34 -0
- data/db/engine_migrate/20260101000001_create_plum_tables.rb +154 -0
- data/lib/generators/plum/install/install_generator.rb +54 -0
- data/lib/generators/plum/install/templates/plum_initializer.rb +37 -0
- data/lib/plum/configuration.rb +88 -0
- data/lib/plum/content_source.rb +45 -0
- data/lib/plum/content_source_context.rb +30 -0
- data/lib/plum/content_source_registry.rb +120 -0
- data/lib/plum/engine.rb +30 -0
- data/lib/plum/liquid_tags/form_tag.rb +17 -0
- data/lib/plum/version.rb +7 -0
- data/lib/plum.rb +11 -0
- data/vendor/javascript/lexxy.js +11859 -0
- metadata +405 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<div class="flex items-center justify-between gap-6 mb-8">
|
|
2
|
+
<div>
|
|
3
|
+
<h1 class="text-2xl font-bold text-gray-900">Themes</h1>
|
|
4
|
+
<p class="mt-1 text-sm text-gray-500">
|
|
5
|
+
Active: <span class="font-medium text-gray-700"><%= @active_theme&.name || "Default" %></span>
|
|
6
|
+
</p>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
11
|
+
<section class="lg:col-span-2">
|
|
12
|
+
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
13
|
+
<% @themes.each do |theme| %>
|
|
14
|
+
<% active = theme.handle == @active_theme&.handle %>
|
|
15
|
+
<% screenshot_url = theme_screenshot_url(theme) %>
|
|
16
|
+
|
|
17
|
+
<article class="overflow-hidden bg-white shadow rounded-lg" data-theme-handle="<%= theme.handle %>">
|
|
18
|
+
<div class="border-b border-gray-200 bg-gray-100">
|
|
19
|
+
<% if screenshot_url.present? %>
|
|
20
|
+
<%= image_tag screenshot_url,
|
|
21
|
+
alt: "#{theme.name} screenshot",
|
|
22
|
+
class: "h-44 w-full object-cover" %>
|
|
23
|
+
<% else %>
|
|
24
|
+
<div class="flex h-44 w-full items-center justify-center bg-gray-100 text-gray-500">
|
|
25
|
+
<span class="text-3xl font-bold"><%= theme_initials(theme) %></span>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="p-6 space-y-4">
|
|
31
|
+
<div class="flex items-start justify-between gap-4">
|
|
32
|
+
<div class="min-w-0">
|
|
33
|
+
<h2 class="text-lg font-semibold text-gray-900"><%= theme.name %></h2>
|
|
34
|
+
<p class="mt-1 text-sm text-gray-500"><%= theme.description.presence || "No description provided." %></p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<% if active %>
|
|
38
|
+
<span class="shrink-0 px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Active</span>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<dl class="grid grid-cols-2 gap-3 text-xs text-gray-500">
|
|
43
|
+
<div>
|
|
44
|
+
<dt class="font-medium text-gray-700">Handle</dt>
|
|
45
|
+
<dd class="mt-1 font-mono truncate"><%= theme.handle %></dd>
|
|
46
|
+
</div>
|
|
47
|
+
<div>
|
|
48
|
+
<dt class="font-medium text-gray-700">Version</dt>
|
|
49
|
+
<dd class="mt-1"><%= theme.version.presence || "—" %></dd>
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<dt class="font-medium text-gray-700">Category</dt>
|
|
53
|
+
<dd class="mt-1"><%= theme.category.presence || "General" %></dd>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<dt class="font-medium text-gray-700">Author</dt>
|
|
57
|
+
<dd class="mt-1 truncate"><%= theme.author.presence || "—" %></dd>
|
|
58
|
+
</div>
|
|
59
|
+
</dl>
|
|
60
|
+
|
|
61
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
62
|
+
<%= link_to "Preview", cp_theme_preview_path(theme.handle), target: "_blank", rel: "noopener",
|
|
63
|
+
class: "px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
|
64
|
+
|
|
65
|
+
<% unless active %>
|
|
66
|
+
<%= button_to "Activate", cp_theme_path(theme.handle), method: :patch,
|
|
67
|
+
class: "px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 cursor-pointer" %>
|
|
68
|
+
<% end %>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</article>
|
|
72
|
+
<% end %>
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
75
|
+
|
|
76
|
+
<aside class="space-y-6">
|
|
77
|
+
<section class="bg-white shadow rounded-lg p-6 space-y-4">
|
|
78
|
+
<div>
|
|
79
|
+
<h2 class="text-lg font-semibold text-gray-900">Customize</h2>
|
|
80
|
+
<p class="mt-1 text-sm text-gray-500"><%= @active_theme&.name || "Default" %></p>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<% if @active_theme&.settings_fields&.any? %>
|
|
84
|
+
<%= form_with url: cp_theme_path(@active_theme.handle), method: :patch, class: "space-y-4" do %>
|
|
85
|
+
<% @active_theme.settings_fields.each do |field| %>
|
|
86
|
+
<% field_id = theme_setting_input_id(@active_theme, field) %>
|
|
87
|
+
<% field_name = theme_setting_input_name(field) %>
|
|
88
|
+
<% field_value = theme_setting_value(field) %>
|
|
89
|
+
|
|
90
|
+
<div>
|
|
91
|
+
<label for="<%= field_id %>" class="block text-sm font-medium text-gray-700"><%= theme_setting_label(field) %></label>
|
|
92
|
+
|
|
93
|
+
<% case field["type"].to_s %>
|
|
94
|
+
<% when "color" %>
|
|
95
|
+
<%= color_field_tag field_name, field_value,
|
|
96
|
+
id: field_id,
|
|
97
|
+
class: "mt-1 h-10 w-16 rounded-md border border-gray-300 bg-white p-1" %>
|
|
98
|
+
<% when "textarea" %>
|
|
99
|
+
<%= text_area_tag field_name, field_value, id: field_id, rows: 3,
|
|
100
|
+
class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500" %>
|
|
101
|
+
<% when "boolean" %>
|
|
102
|
+
<div class="mt-2 flex items-center gap-2">
|
|
103
|
+
<%= hidden_field_tag field_name, "0" %>
|
|
104
|
+
<%= check_box_tag field_name, "1", theme_setting_checked?(field),
|
|
105
|
+
id: field_id,
|
|
106
|
+
class: "h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" %>
|
|
107
|
+
</div>
|
|
108
|
+
<% when "select" %>
|
|
109
|
+
<%= select_tag field_name,
|
|
110
|
+
options_for_select(theme_setting_options(field), field_value),
|
|
111
|
+
id: field_id,
|
|
112
|
+
class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500" %>
|
|
113
|
+
<% else %>
|
|
114
|
+
<%= text_field_tag field_name, field_value, id: field_id,
|
|
115
|
+
class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500" %>
|
|
116
|
+
<% end %>
|
|
117
|
+
|
|
118
|
+
<% if field["help"].present? %>
|
|
119
|
+
<p class="mt-1 text-sm text-gray-500"><%= field["help"] %></p>
|
|
120
|
+
<% end %>
|
|
121
|
+
</div>
|
|
122
|
+
<% end %>
|
|
123
|
+
|
|
124
|
+
<div class="flex items-center justify-between gap-3 pt-2">
|
|
125
|
+
<%= link_to "Preview", cp_theme_preview_path(@active_theme.handle), target: "_blank", rel: "noopener",
|
|
126
|
+
class: "px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
|
127
|
+
<%= submit_tag "Save Settings",
|
|
128
|
+
class: "px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 cursor-pointer" %>
|
|
129
|
+
</div>
|
|
130
|
+
<% end %>
|
|
131
|
+
<% else %>
|
|
132
|
+
<p class="text-sm text-gray-500">This theme does not define editable settings.</p>
|
|
133
|
+
<% end %>
|
|
134
|
+
</section>
|
|
135
|
+
|
|
136
|
+
<section class="bg-white shadow rounded-lg p-6 space-y-4">
|
|
137
|
+
<h2 class="text-lg font-semibold text-gray-900">Install Theme</h2>
|
|
138
|
+
|
|
139
|
+
<% if @theme_package_errors.present? %>
|
|
140
|
+
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
|
141
|
+
<ul class="list-disc list-inside">
|
|
142
|
+
<% @theme_package_errors.each do |error| %>
|
|
143
|
+
<li><%= error %></li>
|
|
144
|
+
<% end %>
|
|
145
|
+
</ul>
|
|
146
|
+
</div>
|
|
147
|
+
<% end %>
|
|
148
|
+
|
|
149
|
+
<%= form_with url: cp_themes_path, multipart: true, class: "space-y-4" do |f| %>
|
|
150
|
+
<div>
|
|
151
|
+
<%= f.label :theme_package, "Theme zip", class: "block text-sm font-medium text-gray-700" %>
|
|
152
|
+
<%= f.file_field :theme_package, accept: ".zip,application/zip", class: "mt-1 block w-full text-sm text-gray-700" %>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<%= f.submit "Install Theme",
|
|
156
|
+
class: "w-full px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 cursor-pointer" %>
|
|
157
|
+
<% end %>
|
|
158
|
+
</section>
|
|
159
|
+
</aside>
|
|
160
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<h1>New <%= @form.name %> submission</h1>
|
|
2
|
+
|
|
3
|
+
<p>A new submission was received<%= " on #{@site_name}" if @site_name.present? %>.</p>
|
|
4
|
+
|
|
5
|
+
<table cellpadding="6" cellspacing="0" border="0">
|
|
6
|
+
<% @fields.each do |field| %>
|
|
7
|
+
<% handle = field["handle"].to_s %>
|
|
8
|
+
<tr>
|
|
9
|
+
<th align="left" valign="top"><%= field["label"].presence || handle.humanize %></th>
|
|
10
|
+
<td><%= @submission.value(handle) %></td>
|
|
11
|
+
</tr>
|
|
12
|
+
<% end %>
|
|
13
|
+
</table>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<%= form_with url: "/login", class: "mt-8 space-y-6" do |f| %>
|
|
2
|
+
<div class="space-y-4">
|
|
3
|
+
<div>
|
|
4
|
+
<%= f.label :email, class: "block text-sm font-medium text-gray-700" %>
|
|
5
|
+
<%= f.email_field :email, required: true, autofocus: true,
|
|
6
|
+
class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500" %>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div>
|
|
10
|
+
<%= f.label :password, class: "block text-sm font-medium text-gray-700" %>
|
|
11
|
+
<%= f.password_field :password, required: true,
|
|
12
|
+
class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500" %>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div>
|
|
17
|
+
<%= f.submit "Sign in",
|
|
18
|
+
class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 cursor-pointer" %>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
data/compat/plum-cms.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "plum"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Files in the config/locales directory are used for internationalization and
|
|
2
|
+
# are automatically loaded by Rails. If you want to use locales other than
|
|
3
|
+
# English, add the necessary files in this directory.
|
|
4
|
+
#
|
|
5
|
+
# To use the locales, use `I18n.t`:
|
|
6
|
+
#
|
|
7
|
+
# I18n.t "hello"
|
|
8
|
+
#
|
|
9
|
+
# In views, this is aliased to just `t`:
|
|
10
|
+
#
|
|
11
|
+
# <%= t("hello") %>
|
|
12
|
+
#
|
|
13
|
+
# To use a different locale, set it with `I18n.locale`:
|
|
14
|
+
#
|
|
15
|
+
# I18n.locale = :es
|
|
16
|
+
#
|
|
17
|
+
# This would use the information in config/locales/es.yml.
|
|
18
|
+
#
|
|
19
|
+
# To learn more about the API, please read the Rails Internationalization guide
|
|
20
|
+
# at https://guides.rubyonrails.org/i18n.html.
|
|
21
|
+
#
|
|
22
|
+
# Be aware that YAML interprets the following case-insensitive strings as
|
|
23
|
+
# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
|
|
24
|
+
# must be quoted to be interpreted as strings. For example:
|
|
25
|
+
#
|
|
26
|
+
# en:
|
|
27
|
+
# "yes": yup
|
|
28
|
+
# enabled: "ON"
|
|
29
|
+
|
|
30
|
+
en:
|
|
31
|
+
hello: "Hello world"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Plum::Engine.routes.draw do
|
|
2
|
+
get "login", to: "sessions#new"
|
|
3
|
+
post "login", to: "sessions#create"
|
|
4
|
+
delete "logout", to: "sessions#destroy"
|
|
5
|
+
|
|
6
|
+
namespace :cp do
|
|
7
|
+
root "dashboard#show"
|
|
8
|
+
resources :content_types do
|
|
9
|
+
resources :entries
|
|
10
|
+
end
|
|
11
|
+
resources :assets, except: [ :show ]
|
|
12
|
+
resources :globals
|
|
13
|
+
resources :nav_menus do
|
|
14
|
+
resources :nav_items, except: [ :index, :show ]
|
|
15
|
+
end
|
|
16
|
+
resources :form_definitions, path: "forms" do
|
|
17
|
+
resources :form_submissions, only: [ :show, :destroy ]
|
|
18
|
+
end
|
|
19
|
+
resource :site_settings, only: [ :show, :edit, :update ]
|
|
20
|
+
resources :themes, only: [ :index, :create, :update ]
|
|
21
|
+
resources :taxonomies do
|
|
22
|
+
resources :terms, except: [ :index, :show ]
|
|
23
|
+
end
|
|
24
|
+
get "theme_previews/:handle", to: "theme_previews#show", as: :theme_preview
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
post "forms/:handle", to: "form_submissions#create", as: :form
|
|
28
|
+
get "theme_assets/:theme_handle/*path", to: "theme_assets#show", as: :theme_asset, format: false
|
|
29
|
+
|
|
30
|
+
get "search", to: "pages#search", as: :search
|
|
31
|
+
|
|
32
|
+
root "pages#home"
|
|
33
|
+
get "*slug", to: "pages#show", constraints: ->(req) { !req.path.start_with?("/rails/") }
|
|
34
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
class CreatePlumTables < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :plum_sites do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.string :domain
|
|
6
|
+
t.string :theme_name, null: false, default: "default"
|
|
7
|
+
t.json :settings
|
|
8
|
+
t.json :theme_settings, default: {}, null: false
|
|
9
|
+
t.text :custom_css
|
|
10
|
+
t.string :owner_type
|
|
11
|
+
t.bigint :owner_id
|
|
12
|
+
t.timestamps
|
|
13
|
+
|
|
14
|
+
t.index [ :owner_type, :owner_id ], unique: true, name: "index_plum_sites_on_owner"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
create_table :plum_users do |t|
|
|
18
|
+
t.string :email
|
|
19
|
+
t.string :password_digest
|
|
20
|
+
t.integer :role
|
|
21
|
+
t.timestamps
|
|
22
|
+
|
|
23
|
+
t.index :email, unique: true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
create_table :plum_site_settings do |t|
|
|
27
|
+
t.references :site, null: false, foreign_key: { to_table: :plum_sites }, index: { unique: true }
|
|
28
|
+
t.string :name
|
|
29
|
+
t.string :tagline
|
|
30
|
+
t.string :logo
|
|
31
|
+
t.string :favicon
|
|
32
|
+
t.string :seo_title
|
|
33
|
+
t.string :seo_description
|
|
34
|
+
t.string :theme_name
|
|
35
|
+
t.string :primary_color
|
|
36
|
+
t.string :support_email
|
|
37
|
+
t.timestamps
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
create_table :plum_content_types do |t|
|
|
41
|
+
t.references :site, null: false, foreign_key: { to_table: :plum_sites }
|
|
42
|
+
t.string :name
|
|
43
|
+
t.string :handle
|
|
44
|
+
t.boolean :singleton
|
|
45
|
+
t.json :blueprint
|
|
46
|
+
t.string :icon
|
|
47
|
+
t.timestamps
|
|
48
|
+
|
|
49
|
+
t.index [ :site_id, :handle ], unique: true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
create_table :plum_entries do |t|
|
|
53
|
+
t.references :site, null: false, foreign_key: { to_table: :plum_sites }
|
|
54
|
+
t.references :content_type, null: false, foreign_key: { to_table: :plum_content_types }
|
|
55
|
+
t.references :author, foreign_key: { to_table: :plum_users }
|
|
56
|
+
t.string :author_name
|
|
57
|
+
t.string :author_email
|
|
58
|
+
t.string :author_gid
|
|
59
|
+
t.string :title
|
|
60
|
+
t.string :slug
|
|
61
|
+
t.integer :status
|
|
62
|
+
t.json :data
|
|
63
|
+
t.datetime :published_at
|
|
64
|
+
t.timestamps
|
|
65
|
+
|
|
66
|
+
t.index [ :site_id, :slug ], unique: true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
create_table :plum_globals do |t|
|
|
70
|
+
t.references :site, null: false, foreign_key: { to_table: :plum_sites }
|
|
71
|
+
t.string :name
|
|
72
|
+
t.string :handle
|
|
73
|
+
t.json :data
|
|
74
|
+
t.timestamps
|
|
75
|
+
|
|
76
|
+
t.index [ :site_id, :handle ], unique: true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
create_table :plum_nav_menus do |t|
|
|
80
|
+
t.references :site, null: false, foreign_key: { to_table: :plum_sites }
|
|
81
|
+
t.string :name
|
|
82
|
+
t.string :handle
|
|
83
|
+
t.timestamps
|
|
84
|
+
|
|
85
|
+
t.index [ :site_id, :handle ], unique: true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
create_table :plum_nav_items do |t|
|
|
89
|
+
t.references :site, null: false, foreign_key: { to_table: :plum_sites }
|
|
90
|
+
t.references :nav_menu, null: false, foreign_key: { to_table: :plum_nav_menus }
|
|
91
|
+
t.references :parent, foreign_key: { to_table: :plum_nav_items }
|
|
92
|
+
t.references :entry, foreign_key: { to_table: :plum_entries }
|
|
93
|
+
t.string :label
|
|
94
|
+
t.string :url
|
|
95
|
+
t.integer :position
|
|
96
|
+
t.timestamps
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
create_table :plum_assets do |t|
|
|
100
|
+
t.references :site, null: false, foreign_key: { to_table: :plum_sites }
|
|
101
|
+
t.string :alt_text
|
|
102
|
+
t.text :caption
|
|
103
|
+
t.string :folder
|
|
104
|
+
t.timestamps
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
create_table :plum_form_definitions do |t|
|
|
108
|
+
t.references :site, null: false, foreign_key: { to_table: :plum_sites }
|
|
109
|
+
t.string :name
|
|
110
|
+
t.string :handle
|
|
111
|
+
t.json :fields
|
|
112
|
+
t.string :notification_email
|
|
113
|
+
t.timestamps
|
|
114
|
+
|
|
115
|
+
t.index [ :site_id, :handle ], unique: true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
create_table :plum_form_submissions do |t|
|
|
119
|
+
t.references :site, null: false, foreign_key: { to_table: :plum_sites }
|
|
120
|
+
t.references :form_definition, null: false, foreign_key: { to_table: :plum_form_definitions }
|
|
121
|
+
t.json :data
|
|
122
|
+
t.timestamps
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
create_table :plum_taxonomies do |t|
|
|
126
|
+
t.references :site, null: false, foreign_key: { to_table: :plum_sites }
|
|
127
|
+
t.string :name, null: false
|
|
128
|
+
t.string :handle, null: false
|
|
129
|
+
t.string :slug, null: false
|
|
130
|
+
t.timestamps
|
|
131
|
+
|
|
132
|
+
t.index [ :site_id, :handle ], unique: true
|
|
133
|
+
t.index [ :site_id, :slug ], unique: true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
create_table :plum_terms do |t|
|
|
137
|
+
t.references :site, null: false, foreign_key: { to_table: :plum_sites }
|
|
138
|
+
t.references :taxonomy, null: false, foreign_key: { to_table: :plum_taxonomies }
|
|
139
|
+
t.string :name, null: false
|
|
140
|
+
t.string :slug, null: false
|
|
141
|
+
t.integer :position, default: 0
|
|
142
|
+
t.timestamps
|
|
143
|
+
|
|
144
|
+
t.index [ :taxonomy_id, :slug ], unique: true
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
create_table :plum_entry_terms do |t|
|
|
148
|
+
t.references :entry, null: false, foreign_key: { to_table: :plum_entries }
|
|
149
|
+
t.references :term, null: false, foreign_key: { to_table: :plum_terms }
|
|
150
|
+
|
|
151
|
+
t.index [ :entry_id, :term_id ], unique: true
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module Plum
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
class_option :mount_path,
|
|
9
|
+
type: :string,
|
|
10
|
+
default: "/cms",
|
|
11
|
+
desc: "Path where Plum::Engine should be mounted"
|
|
12
|
+
|
|
13
|
+
class_option :skip_route,
|
|
14
|
+
type: :boolean,
|
|
15
|
+
default: false,
|
|
16
|
+
desc: "Skip adding the Plum engine mount to config/routes.rb"
|
|
17
|
+
|
|
18
|
+
def copy_initializer
|
|
19
|
+
template "plum_initializer.rb", "config/initializers/plum.rb"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def copy_migrations
|
|
23
|
+
rake "plum:install:migrations"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create_theme_directory
|
|
27
|
+
empty_directory "app/themes"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def mount_engine
|
|
31
|
+
return if options[:skip_route]
|
|
32
|
+
|
|
33
|
+
route %(mount Plum::Engine, at: "#{options[:mount_path]}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def print_next_steps
|
|
37
|
+
say <<~TEXT
|
|
38
|
+
|
|
39
|
+
Plum is installed.
|
|
40
|
+
|
|
41
|
+
Next steps:
|
|
42
|
+
bin/rails active_storage:install # if not already installed
|
|
43
|
+
bin/rails db:migrate
|
|
44
|
+
|
|
45
|
+
Plum is mounted at #{options[:mount_path]}.
|
|
46
|
+
|
|
47
|
+
Migrations are copied to db/migrate/ and can be reviewed
|
|
48
|
+
before running. JavaScript and CSS are served from the
|
|
49
|
+
engine automatically.
|
|
50
|
+
TEXT
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Plum.configure do |config|
|
|
2
|
+
# Standalone/default mode: Plum manages its own site and users.
|
|
3
|
+
config.current_site_resolver = ->(_controller) { Plum::Site.first_or_create_standalone! }
|
|
4
|
+
config.current_user_resolver = lambda { |controller|
|
|
5
|
+
Plum::User.find_by(id: controller.session[:plum_user_id]) if controller.session[:plum_user_id]
|
|
6
|
+
}
|
|
7
|
+
config.authorize_with = :plum
|
|
8
|
+
|
|
9
|
+
# Host themes win first, bundled Plum themes provide the fallback.
|
|
10
|
+
config.theme_paths = [
|
|
11
|
+
Rails.root.join("app/themes"),
|
|
12
|
+
Plum::Engine.root.join("app/themes")
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# White-label the control panel:
|
|
16
|
+
#
|
|
17
|
+
# config.cp_name = "My Brand"
|
|
18
|
+
# config.cp_subtitle = "Website"
|
|
19
|
+
# config.cp_logo_path = "my-logo.svg"
|
|
20
|
+
# config.cp_accent_color = "#7c3aed"
|
|
21
|
+
# config.cp_sidebar_bg = "#241B27"
|
|
22
|
+
# config.cp_sidebar_header_bg = "#1E1621"
|
|
23
|
+
# config.cp_sidebar_text = "#D9CBD4"
|
|
24
|
+
# config.cp_sidebar_muted = "#A8929F"
|
|
25
|
+
# config.cp_back_url = "/dashboard"
|
|
26
|
+
# config.cp_back_label = "← Table Needs"
|
|
27
|
+
|
|
28
|
+
# Embedded host example:
|
|
29
|
+
#
|
|
30
|
+
# config.current_site_resolver = ->(_controller) { Current.restaurant.plum_site }
|
|
31
|
+
# config.current_user_resolver = ->(_controller) { Current.user }
|
|
32
|
+
# config.authorize_with = :host
|
|
33
|
+
# config.host_authorization_resolver = ->(_controller) { Current.user.can_manage_website?(Current.restaurant) }
|
|
34
|
+
# config.register_content_source :restaurant, ->(context) { context.site.owner.to_liquid }
|
|
35
|
+
# config.register_content_source :menu, "TableNeeds::PlumAdapters::Menu"
|
|
36
|
+
# config.register_content_source :hours, ->(context) { context.owner.hours_for_web }
|
|
37
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require_relative "content_source_registry"
|
|
2
|
+
|
|
3
|
+
module Plum
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :authorize_with, :current_site_resolver, :current_user_resolver, :host_authorization_resolver,
|
|
6
|
+
:mailer_sender, :cp_name, :cp_subtitle, :cp_logo_path,
|
|
7
|
+
:cp_accent_color, :cp_sidebar_bg, :cp_sidebar_header_bg, :cp_sidebar_text, :cp_sidebar_muted,
|
|
8
|
+
:cp_back_url, :cp_back_label,
|
|
9
|
+
:powered_by_name, :powered_by_url
|
|
10
|
+
attr_writer :theme_paths
|
|
11
|
+
attr_reader :content_sources
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@authorize_with = :plum
|
|
15
|
+
@mailer_sender = "no-reply@example.com"
|
|
16
|
+
@cp_name = "Plum"
|
|
17
|
+
@cp_subtitle = "CMS"
|
|
18
|
+
@cp_logo_path = "plum-mark.svg"
|
|
19
|
+
@cp_accent_color = "#7c3aed"
|
|
20
|
+
@cp_sidebar_bg = "#241B27"
|
|
21
|
+
@cp_sidebar_header_bg = "#1E1621"
|
|
22
|
+
@cp_sidebar_text = "#D9CBD4"
|
|
23
|
+
@cp_sidebar_muted = "#A8929F"
|
|
24
|
+
@cp_back_url = nil
|
|
25
|
+
@cp_back_label = "← Back"
|
|
26
|
+
@powered_by_name = "Plum"
|
|
27
|
+
@powered_by_url = "https://plumcms.com"
|
|
28
|
+
@content_sources = ContentSourceRegistry.new
|
|
29
|
+
@current_site_resolver = ->(_controller) { Plum::Site.first_or_create_standalone! }
|
|
30
|
+
@current_user_resolver = lambda { |controller|
|
|
31
|
+
Plum::User.find_by(id: controller.session[:plum_user_id]) if controller.session[:plum_user_id]
|
|
32
|
+
}
|
|
33
|
+
@host_authorization_resolver = ->(controller) { Plum.current_user(controller).present? }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def theme_paths
|
|
37
|
+
Array(@theme_paths.presence || default_theme_paths).map { |path| Pathname(path) }.uniq(&:to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def register_content_source(handle, source = nil, &block)
|
|
41
|
+
content_sources.register(handle, source, &block)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def default_theme_paths
|
|
47
|
+
paths = []
|
|
48
|
+
paths << Rails.root.join("app/themes") if defined?(Rails) && Rails.root
|
|
49
|
+
paths << Plum::Engine.root.join("app/themes") if defined?(Plum::Engine)
|
|
50
|
+
paths
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.configuration
|
|
55
|
+
@configuration ||= Configuration.new
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.configure
|
|
59
|
+
yield configuration
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.current_site(controller)
|
|
63
|
+
configuration.current_site_resolver.call(controller)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.current_user(controller)
|
|
67
|
+
configuration.current_user_resolver.call(controller)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.authorized?(controller)
|
|
71
|
+
case configuration.authorize_with
|
|
72
|
+
when :plum
|
|
73
|
+
current_user(controller).present?
|
|
74
|
+
when :host
|
|
75
|
+
configuration.host_authorization_resolver.call(controller)
|
|
76
|
+
else
|
|
77
|
+
configuration.authorize_with.respond_to?(:call) && configuration.authorize_with.call(controller)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.register_content_source(handle, source = nil, &block)
|
|
82
|
+
configuration.register_content_source(handle, source, &block)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.content_sources_for(controller, site:)
|
|
86
|
+
configuration.content_sources.to_liquid_context(controller: controller, site: site)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
class ContentSourceError < StandardError; end
|
|
3
|
+
|
|
4
|
+
class ContentSource
|
|
5
|
+
attr_reader :context
|
|
6
|
+
|
|
7
|
+
def initialize(context)
|
|
8
|
+
@context = context
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_liquid
|
|
12
|
+
raise ContentSourceError, "#{self.class.name} must implement #to_liquid"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def controller
|
|
18
|
+
context.controller
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def site
|
|
22
|
+
context.site
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def owner
|
|
26
|
+
context.owner
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def request
|
|
30
|
+
context.request
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def params
|
|
34
|
+
context.params
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def session
|
|
38
|
+
context.session
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def current_user
|
|
42
|
+
context.current_user
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|