lean_cms 0.2.12
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 +235 -0
- data/LICENSE +21 -0
- data/README.md +107 -0
- data/app/assets/images/lean_cms/sloth-404.png +0 -0
- data/app/assets/images/lean_cms/sloth-500.png +0 -0
- data/app/assets/images/lean_cms/sloth-favicon-16.png +0 -0
- data/app/assets/images/lean_cms/sloth-favicon-32.png +0 -0
- data/app/assets/images/lean_cms/sloth-favicon-64.png +0 -0
- data/app/assets/images/lean_cms/sloth-logo.png +0 -0
- data/app/assets/lean_cms/actiontext.css +440 -0
- data/app/assets/lean_cms/cms_edit_controls.css +548 -0
- data/app/assets/tailwind/lean_cms/engine.css +14 -0
- data/app/components/lean_cms/base_component.rb +61 -0
- data/app/components/lean_cms/bullets_section_component.html.erb +23 -0
- data/app/components/lean_cms/bullets_section_component.rb +54 -0
- data/app/components/lean_cms/cards_section_component.html.erb +237 -0
- data/app/components/lean_cms/cards_section_component.rb +71 -0
- data/app/components/lean_cms/editable_content_component.html.erb +15 -0
- data/app/components/lean_cms/editable_content_component.rb +53 -0
- data/app/components/lean_cms/section_component.html.erb +18 -0
- data/app/components/lean_cms/section_component.rb +35 -0
- data/app/controllers/concerns/lean_cms/authentication.rb +60 -0
- data/app/controllers/concerns/lean_cms/authorization.rb +60 -0
- data/app/controllers/lean_cms/activity_controller.rb +16 -0
- data/app/controllers/lean_cms/application_controller.rb +48 -0
- data/app/controllers/lean_cms/dashboard_controller.rb +13 -0
- data/app/controllers/lean_cms/form_submissions_controller.rb +37 -0
- data/app/controllers/lean_cms/notification_settings_controller.rb +145 -0
- data/app/controllers/lean_cms/notifications_controller.rb +26 -0
- data/app/controllers/lean_cms/page_contents_controller.rb +403 -0
- data/app/controllers/lean_cms/password_setup_controller.rb +65 -0
- data/app/controllers/lean_cms/passwords_controller.rb +42 -0
- data/app/controllers/lean_cms/posts_controller.rb +78 -0
- data/app/controllers/lean_cms/sessions_controller.rb +50 -0
- data/app/controllers/lean_cms/settings_controller.rb +124 -0
- data/app/controllers/lean_cms/users_controller.rb +113 -0
- data/app/helpers/lean_cms/activity_helper.rb +190 -0
- data/app/helpers/lean_cms/application_helper.rb +43 -0
- data/app/helpers/lean_cms/content_helper.rb +34 -0
- data/app/helpers/lean_cms/page_content_helper.rb +359 -0
- data/app/javascript/controllers/cards_editor_controller.js +317 -0
- data/app/javascript/controllers/cms_sticky_overlay_controller.js +59 -0
- data/app/javascript/controllers/field_editor_form_controller.js +68 -0
- data/app/javascript/controllers/field_editor_modal_controller.js +79 -0
- data/app/javascript/controllers/inline_edit_controller.js +414 -0
- data/app/javascript/controllers/inline_edit_toggle_controller.js +81 -0
- data/app/javascript/controllers/notifications_controller.js +19 -0
- data/app/javascript/controllers/settings_inline_edit_sync_controller.js +38 -0
- data/app/javascript/controllers/settings_override_controller.js +45 -0
- data/app/mailers/lean_cms/application_mailer.rb +6 -0
- data/app/mailers/lean_cms/passwords_mailer.rb +8 -0
- data/app/mailers/lean_cms/users_mailer.rb +39 -0
- data/app/models/lean_cms/current.rb +6 -0
- data/app/models/lean_cms/form_submission.rb +45 -0
- data/app/models/lean_cms/magic_link.rb +76 -0
- data/app/models/lean_cms/meta_tag.rb +30 -0
- data/app/models/lean_cms/notification_setting.rb +69 -0
- data/app/models/lean_cms/page.rb +23 -0
- data/app/models/lean_cms/page_content.rb +245 -0
- data/app/models/lean_cms/post.rb +65 -0
- data/app/models/lean_cms/session.rb +7 -0
- data/app/models/lean_cms/setting.rb +156 -0
- data/app/policies/lean_cms/application_policy.rb +35 -0
- data/app/policies/lean_cms/page_content_policy.rb +31 -0
- data/app/policies/lean_cms/post_policy.rb +37 -0
- data/app/policies/lean_cms/setting_policy.rb +17 -0
- data/app/views/layouts/lean_cms/application.html.erb +114 -0
- data/app/views/layouts/lean_cms/auth.html.erb +200 -0
- data/app/views/lean_cms/activity/index.html.erb +79 -0
- data/app/views/lean_cms/dashboard/index.html.erb +180 -0
- data/app/views/lean_cms/form_submissions/index.html.erb +104 -0
- data/app/views/lean_cms/form_submissions/show.html.erb +157 -0
- data/app/views/lean_cms/notification_settings/edit.html.erb +192 -0
- data/app/views/lean_cms/notifications/index.html.erb +72 -0
- data/app/views/lean_cms/notifications/show.html.erb +39 -0
- data/app/views/lean_cms/page_contents/_field_editor.html.erb +174 -0
- data/app/views/lean_cms/page_contents/edit.html.erb +428 -0
- data/app/views/lean_cms/page_contents/index.html.erb +113 -0
- data/app/views/lean_cms/password_setup/show.html.erb +35 -0
- data/app/views/lean_cms/passwords/edit.html.erb +26 -0
- data/app/views/lean_cms/passwords/new.html.erb +21 -0
- data/app/views/lean_cms/passwords_mailer/reset.html.erb +6 -0
- data/app/views/lean_cms/passwords_mailer/reset.text.erb +4 -0
- data/app/views/lean_cms/posts/_form.html.erb +118 -0
- data/app/views/lean_cms/posts/edit.html.erb +31 -0
- data/app/views/lean_cms/posts/index.html.erb +100 -0
- data/app/views/lean_cms/posts/new.html.erb +16 -0
- data/app/views/lean_cms/sessions/new.html.erb +28 -0
- data/app/views/lean_cms/settings/edit.html.erb +384 -0
- data/app/views/lean_cms/shared/_admin_bar.html.erb +85 -0
- data/app/views/lean_cms/shared/_header.html.erb +86 -0
- data/app/views/lean_cms/shared/_notifications_bell.html.erb +84 -0
- data/app/views/lean_cms/shared/_sidebar.html.erb +102 -0
- data/app/views/lean_cms/users/_form.html.erb +105 -0
- data/app/views/lean_cms/users/edit.html.erb +8 -0
- data/app/views/lean_cms/users/index.html.erb +99 -0
- data/app/views/lean_cms/users/new.html.erb +8 -0
- data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.html.erb +13 -0
- data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.text.erb +11 -0
- data/app/views/lean_cms/users_mailer/invitation.html.erb +13 -0
- data/app/views/lean_cms/users_mailer/invitation.text.erb +11 -0
- data/app/views/lean_cms/users_mailer/reactivation.html.erb +13 -0
- data/app/views/lean_cms/users_mailer/reactivation.text.erb +11 -0
- data/config/importmap.rb +8 -0
- data/config/routes.rb +78 -0
- data/db/migrate/20251112034030_create_lean_cms_tables.rb +131 -0
- data/db/migrate/20260513000001_create_lean_cms_auth_tables.rb +31 -0
- data/db/migrate/20260514000001_create_paper_trail_versions.rb +16 -0
- data/db/migrate/20260514000002_create_action_text_tables.rb +18 -0
- data/db/migrate/20260514000003_create_active_storage_tables.rb +45 -0
- data/db/migrate/20260514000004_create_noticed_tables.rb +27 -0
- data/lib/generators/lean_cms/demo/demo_generator.rb +54 -0
- data/lib/generators/lean_cms/demo/templates/lean_cms_structure.yml +129 -0
- data/lib/generators/lean_cms/demo/templates/pages_controller.rb +30 -0
- data/lib/generators/lean_cms/demo/templates/views/pages/about.html.erb +40 -0
- data/lib/generators/lean_cms/demo/templates/views/pages/contact.html.erb +55 -0
- data/lib/generators/lean_cms/demo/templates/views/pages/home.html.erb +31 -0
- data/lib/generators/lean_cms/install/install_generator.rb +317 -0
- data/lib/generators/lean_cms/install/templates/add_lean_cms_columns_to_users.rb.tt +7 -0
- data/lib/generators/lean_cms/install/templates/lean_cms.rb +11 -0
- data/lib/generators/lean_cms/install/templates/lean_cms_structure.yml +29 -0
- data/lib/lean_cms/configuration.rb +32 -0
- data/lib/lean_cms/engine.rb +93 -0
- data/lib/lean_cms/loader.rb +217 -0
- data/lib/lean_cms/sync_helper.rb +182 -0
- data/lib/lean_cms/version.rb +3 -0
- data/lib/lean_cms.rb +26 -0
- data/lib/tasks/lean_cms.rake +390 -0
- metadata +313 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<% cache cache_key, expires_in: 1.hour do %>
|
|
2
|
+
<% cards_data = cards %>
|
|
3
|
+
<% if cards_data.empty? %>
|
|
4
|
+
<% return %>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<% if can_edit_cms? && field %>
|
|
8
|
+
<%= content_tag(:div, class: 'cms-inline-field relative', data: data_attributes) do %>
|
|
9
|
+
<div class="grid md:grid-cols-<%= grid_cols %> gap-<%= gap %> <%= container_class %>"<% if scroll_animate %> data-controller="scroll-animate" data-scroll-animate-stagger-value="<%= stagger_delay %>"<% end %>>
|
|
10
|
+
<% cards_data.each do |card| %>
|
|
11
|
+
<%
|
|
12
|
+
# Parse card data (handle both string and symbol keys)
|
|
13
|
+
icon = card['icon'] || card[:icon]
|
|
14
|
+
icon_color = card['icon_color'] || card[:icon_color]
|
|
15
|
+
bg_color = card['bg_color'] || card[:bg_color]
|
|
16
|
+
heading = card['heading'] || card[:heading]
|
|
17
|
+
text = card['text'] || card[:text]
|
|
18
|
+
alignment = text_align || card['alignment'] || card[:alignment] || 'center'
|
|
19
|
+
use_image = card['use_image'] == true || card['use_image'] == 'true'
|
|
20
|
+
image_id = card['image_id'] || card[:image_id]
|
|
21
|
+
card_image = nil
|
|
22
|
+
|
|
23
|
+
# Get image attachment if use_image is true and image_id is present
|
|
24
|
+
if use_image && image_id.present? && field
|
|
25
|
+
card_image = field.card_image(image_id)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Determine styling classes
|
|
29
|
+
text_align_class = case alignment
|
|
30
|
+
when 'left' then 'text-left'
|
|
31
|
+
when 'right' then 'text-right'
|
|
32
|
+
else 'text-center'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Handle gradient backgrounds (skip if ignore_card_bg is true)
|
|
36
|
+
has_gradient = !ignore_card_bg && bg_color&.include?('gradient')
|
|
37
|
+
bg_gradient_class = if ignore_card_bg
|
|
38
|
+
nil
|
|
39
|
+
else
|
|
40
|
+
case bg_color
|
|
41
|
+
when 'gradient-red'
|
|
42
|
+
'bg-gradient-to-br from-[#b82025] to-[#a01c20] text-white'
|
|
43
|
+
when 'gradient-blue'
|
|
44
|
+
'bg-gradient-to-br from-[#2563eb] to-[#1e40af] text-white'
|
|
45
|
+
else
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Regular background color (skip if ignore_card_bg is true)
|
|
51
|
+
bg_style = nil
|
|
52
|
+
is_dark_bg = false
|
|
53
|
+
if !ignore_card_bg && !has_gradient && bg_color&.start_with?('#')
|
|
54
|
+
bg_style = "background-color: #{bg_color};"
|
|
55
|
+
# Detect actual brightness so light colors (e.g. #dbeafe) get dark text
|
|
56
|
+
hex_match = bg_color.match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i)
|
|
57
|
+
if hex_match
|
|
58
|
+
r, g, b = hex_match[1].to_i(16), hex_match[2].to_i(16), hex_match[3].to_i(16)
|
|
59
|
+
brightness = (r * 299 + g * 587 + b * 114) / 1000
|
|
60
|
+
is_dark_bg = brightness < 128
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Determine if we need light text (for dark backgrounds or gradients)
|
|
65
|
+
use_light_text = !ignore_card_bg && (has_gradient || is_dark_bg)
|
|
66
|
+
%>
|
|
67
|
+
|
|
68
|
+
<div class="<%= card_class %> <%= bg_gradient_class %> <%= text_align_class %>"
|
|
69
|
+
<% if bg_style %>style="<%= bg_style %>"<% end %>>
|
|
70
|
+
|
|
71
|
+
<%# Render image or icon if present %>
|
|
72
|
+
<% if use_image && card_image %>
|
|
73
|
+
<%# Display uploaded image - resize to 512x512 for retina displays, CSS will scale down %>
|
|
74
|
+
<div class="w-<%= icon_size %> h-<%= icon_size %>
|
|
75
|
+
<%= alignment == 'center' ? 'mx-auto' : '' %>
|
|
76
|
+
mb-<%= alignment == 'center' ? '4' : '6' %>
|
|
77
|
+
flex items-center justify-center">
|
|
78
|
+
<%= image_tag card_image.variant(resize_to_limit: [512, 512]),
|
|
79
|
+
class: "w-full h-full object-contain",
|
|
80
|
+
alt: heading.presence || 'Card image' %>
|
|
81
|
+
</div>
|
|
82
|
+
<% elsif icon.present? %>
|
|
83
|
+
<% if icon_bg_gradient %>
|
|
84
|
+
<%# Red gradient icon box (like landing page service cards) %>
|
|
85
|
+
<div class="w-<%= icon_size %> h-<%= icon_size %>
|
|
86
|
+
rounded-xl
|
|
87
|
+
flex items-center justify-center
|
|
88
|
+
<%= alignment == 'center' ? 'mx-auto' : '' %>
|
|
89
|
+
mb-6"
|
|
90
|
+
style="background: linear-gradient(135deg, #b82025, #d92028);">
|
|
91
|
+
<%= helpers.render_card_icon(icon, 'white', true) %>
|
|
92
|
+
</div>
|
|
93
|
+
<% else %>
|
|
94
|
+
<div class="w-<%= icon_size %> h-<%= icon_size %>
|
|
95
|
+
<%= use_light_text ? 'bg-white/20' : (bg_color&.start_with?('#') ? '' : "bg-#{bg_color}") %>
|
|
96
|
+
rounded-<%= icon_shape %>
|
|
97
|
+
flex items-center justify-center
|
|
98
|
+
<%= alignment == 'center' ? 'mx-auto' : '' %>
|
|
99
|
+
mb-<%= alignment == 'center' ? '4' : '6' %>">
|
|
100
|
+
<%= helpers.render_card_icon(icon, icon_color, use_light_text) %>
|
|
101
|
+
</div>
|
|
102
|
+
<% end %>
|
|
103
|
+
<% end %>
|
|
104
|
+
|
|
105
|
+
<%# Render heading %>
|
|
106
|
+
<% if heading.present? %>
|
|
107
|
+
<h3 class="text-<%= alignment == 'center' ? 'xl' : '2xl' %> font-bold mb-<%= alignment == 'center' ? '2' : '4' %>
|
|
108
|
+
<%= use_light_text ? 'text-white' : 'text-slate-800' %>">
|
|
109
|
+
<%= heading %>
|
|
110
|
+
</h3>
|
|
111
|
+
<% end %>
|
|
112
|
+
|
|
113
|
+
<%# Render text/description %>
|
|
114
|
+
<% if text.present? %>
|
|
115
|
+
<p class="leading-relaxed <%= use_light_text ? 'text-white opacity-90' : 'text-slate-500' %>">
|
|
116
|
+
<%= text %>
|
|
117
|
+
</p>
|
|
118
|
+
<% end %>
|
|
119
|
+
</div>
|
|
120
|
+
<% end %>
|
|
121
|
+
<% end %>
|
|
122
|
+
<% else %>
|
|
123
|
+
<div class="grid md:grid-cols-<%= grid_cols %> gap-<%= gap %> <%= container_class %>"<% if scroll_animate %> data-controller="scroll-animate" data-scroll-animate-stagger-value="<%= stagger_delay %>"<% end %>>
|
|
124
|
+
<% cards_data.each do |card| %>
|
|
125
|
+
<%
|
|
126
|
+
# Parse card data (handle both string and symbol keys)
|
|
127
|
+
icon = card['icon'] || card[:icon]
|
|
128
|
+
icon_color = card['icon_color'] || card[:icon_color]
|
|
129
|
+
bg_color = card['bg_color'] || card[:bg_color]
|
|
130
|
+
heading = card['heading'] || card[:heading]
|
|
131
|
+
text = card['text'] || card[:text]
|
|
132
|
+
alignment = text_align || card['alignment'] || card[:alignment] || 'center'
|
|
133
|
+
use_image = card['use_image'] == true || card['use_image'] == 'true'
|
|
134
|
+
image_id = card['image_id'] || card[:image_id]
|
|
135
|
+
card_image = nil
|
|
136
|
+
|
|
137
|
+
# Get image attachment if use_image is true and image_id is present
|
|
138
|
+
if use_image && image_id.present? && field
|
|
139
|
+
card_image = field.card_image(image_id)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Determine styling classes
|
|
143
|
+
text_align_class = case alignment
|
|
144
|
+
when 'left' then 'text-left'
|
|
145
|
+
when 'right' then 'text-right'
|
|
146
|
+
else 'text-center'
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Handle gradient backgrounds (skip if ignore_card_bg is true)
|
|
150
|
+
has_gradient = !ignore_card_bg && bg_color&.include?('gradient')
|
|
151
|
+
bg_gradient_class = if ignore_card_bg
|
|
152
|
+
nil
|
|
153
|
+
else
|
|
154
|
+
case bg_color
|
|
155
|
+
when 'gradient-red'
|
|
156
|
+
'bg-gradient-to-br from-[#b82025] to-[#a01c20] text-white'
|
|
157
|
+
when 'gradient-blue'
|
|
158
|
+
'bg-gradient-to-br from-blue-600 to-blue-800 text-white'
|
|
159
|
+
else
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Regular background color (skip if ignore_card_bg is true)
|
|
165
|
+
bg_style = nil
|
|
166
|
+
is_dark_bg = false
|
|
167
|
+
if !ignore_card_bg && !has_gradient && bg_color&.start_with?('#')
|
|
168
|
+
bg_style = "background-color: #{bg_color};"
|
|
169
|
+
# Detect actual brightness so light colors (e.g. #dbeafe) get dark text
|
|
170
|
+
hex_match = bg_color.match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i)
|
|
171
|
+
if hex_match
|
|
172
|
+
r, g, b = hex_match[1].to_i(16), hex_match[2].to_i(16), hex_match[3].to_i(16)
|
|
173
|
+
brightness = (r * 299 + g * 587 + b * 114) / 1000
|
|
174
|
+
is_dark_bg = brightness < 128
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Determine if we need light text (for dark backgrounds or gradients)
|
|
179
|
+
use_light_text = !ignore_card_bg && (has_gradient || is_dark_bg)
|
|
180
|
+
%>
|
|
181
|
+
|
|
182
|
+
<div class="<%= card_class %> <%= bg_gradient_class %> <%= text_align_class %>"
|
|
183
|
+
<% if bg_style %>style="<%= bg_style %>"<% end %>>
|
|
184
|
+
|
|
185
|
+
<%# Render image or icon if present %>
|
|
186
|
+
<% if use_image && card_image %>
|
|
187
|
+
<%# Display uploaded image - resize to 512x512 for retina displays, CSS will scale down %>
|
|
188
|
+
<div class="w-<%= icon_size %> h-<%= icon_size %>
|
|
189
|
+
<%= alignment == 'center' ? 'mx-auto' : '' %>
|
|
190
|
+
mb-<%= alignment == 'center' ? '4' : '6' %>
|
|
191
|
+
flex items-center justify-center">
|
|
192
|
+
<%= image_tag card_image.variant(resize_to_limit: [512, 512]),
|
|
193
|
+
class: "w-full h-full object-contain",
|
|
194
|
+
alt: heading.presence || 'Card image' %>
|
|
195
|
+
</div>
|
|
196
|
+
<% elsif icon.present? %>
|
|
197
|
+
<% if icon_bg_gradient %>
|
|
198
|
+
<%# Red gradient icon box (like landing page service cards) %>
|
|
199
|
+
<div class="w-<%= icon_size %> h-<%= icon_size %>
|
|
200
|
+
rounded-xl
|
|
201
|
+
flex items-center justify-center
|
|
202
|
+
<%= alignment == 'center' ? 'mx-auto' : '' %>
|
|
203
|
+
mb-6"
|
|
204
|
+
style="background: linear-gradient(135deg, #b82025, #a01c20);">
|
|
205
|
+
<%= helpers.render_card_icon(icon, 'white', true) %>
|
|
206
|
+
</div>
|
|
207
|
+
<% else %>
|
|
208
|
+
<div class="w-<%= icon_size %> h-<%= icon_size %>
|
|
209
|
+
<%= use_light_text ? 'bg-white/20' : (bg_color&.start_with?('#') ? '' : "bg-#{bg_color}") %>
|
|
210
|
+
rounded-<%= icon_shape %>
|
|
211
|
+
flex items-center justify-center
|
|
212
|
+
<%= alignment == 'center' ? 'mx-auto' : '' %>
|
|
213
|
+
mb-<%= alignment == 'center' ? '4' : '6' %>">
|
|
214
|
+
<%= helpers.render_card_icon(icon, icon_color, use_light_text) %>
|
|
215
|
+
</div>
|
|
216
|
+
<% end %>
|
|
217
|
+
<% end %>
|
|
218
|
+
|
|
219
|
+
<%# Render heading %>
|
|
220
|
+
<% if heading.present? %>
|
|
221
|
+
<h3 class="text-<%= alignment == 'center' ? 'xl' : '2xl' %> font-bold mb-<%= alignment == 'center' ? '2' : '4' %>
|
|
222
|
+
<%= use_light_text ? 'text-white' : 'text-slate-800' %>">
|
|
223
|
+
<%= heading %>
|
|
224
|
+
</h3>
|
|
225
|
+
<% end %>
|
|
226
|
+
|
|
227
|
+
<%# Render text/description %>
|
|
228
|
+
<% if text.present? %>
|
|
229
|
+
<p class="leading-relaxed <%= use_light_text ? 'text-white opacity-90' : 'text-slate-500' %>">
|
|
230
|
+
<%= text %>
|
|
231
|
+
</p>
|
|
232
|
+
<% end %>
|
|
233
|
+
</div>
|
|
234
|
+
<% end %>
|
|
235
|
+
</div>
|
|
236
|
+
<% end %>
|
|
237
|
+
<% end %>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class CardsSectionComponent < BaseComponent
|
|
3
|
+
attr_reader :section, :grid_cols, :gap, :container_class, :card_class, :icon_size,
|
|
4
|
+
:icon_shape, :icon_bg_gradient, :text_align, :ignore_card_bg,
|
|
5
|
+
:scroll_animate, :stagger_delay
|
|
6
|
+
|
|
7
|
+
def initialize(section:, page: nil, grid_cols: 3, gap: 8, container_class: '',
|
|
8
|
+
card_class: 'bg-white rounded-xl p-8 shadow-sm hover:shadow-md transition-shadow',
|
|
9
|
+
icon_size: 12, icon_shape: 'lg', icon_bg_gradient: false,
|
|
10
|
+
text_align: nil, ignore_card_bg: false, scroll_animate: false,
|
|
11
|
+
stagger_delay: 150, **options)
|
|
12
|
+
super(page: page)
|
|
13
|
+
@section = section
|
|
14
|
+
@grid_cols = grid_cols
|
|
15
|
+
@gap = gap
|
|
16
|
+
@container_class = container_class
|
|
17
|
+
@card_class = card_class
|
|
18
|
+
@icon_size = icon_size
|
|
19
|
+
@icon_shape = icon_shape
|
|
20
|
+
@icon_bg_gradient = icon_bg_gradient
|
|
21
|
+
@text_align = text_align
|
|
22
|
+
@ignore_card_bg = ignore_card_bg
|
|
23
|
+
@scroll_animate = scroll_animate
|
|
24
|
+
@stagger_delay = stagger_delay
|
|
25
|
+
@options = options
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def cache_key
|
|
31
|
+
super("#{section}_cards")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def cards
|
|
35
|
+
if page.is_a?(LeanCms::Page) && page.page_contents.loaded?
|
|
36
|
+
content_record = page.page_contents.find { |pc| pc.section == section.to_s && pc.key == 'cards' }
|
|
37
|
+
return [] unless content_record&.cards?
|
|
38
|
+
content_record.display_value
|
|
39
|
+
else
|
|
40
|
+
page_key = page.is_a?(LeanCms::Page) ? page.slug : page.to_s
|
|
41
|
+
Rails.cache.fetch("page_cards/#{page_key}/#{section}", expires_in: 1.hour) do
|
|
42
|
+
content_record = if page.is_a?(LeanCms::Page)
|
|
43
|
+
LeanCms::PageContent.find_by(page_id: page.id, section: section, key: 'cards')
|
|
44
|
+
else
|
|
45
|
+
LeanCms::PageContent.find_by("page = ? AND section = ? AND key = ?", page.to_s, section.to_s, 'cards')
|
|
46
|
+
end
|
|
47
|
+
return [] unless content_record&.cards?
|
|
48
|
+
content_record.display_value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def field
|
|
54
|
+
@field ||= find_field(section, 'cards')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def data_attributes
|
|
58
|
+
return {} unless can_edit_cms? && field
|
|
59
|
+
|
|
60
|
+
{
|
|
61
|
+
controller: 'inline-edit',
|
|
62
|
+
inline_edit_field_id_value: field.id,
|
|
63
|
+
inline_edit_type_value: 'cards',
|
|
64
|
+
inline_edit_inline_value: false,
|
|
65
|
+
inline_edit_page_value: page_slug,
|
|
66
|
+
inline_edit_section_value: section,
|
|
67
|
+
inline_edit_key_value: 'cards'
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<% if field&.rich_text? %>
|
|
2
|
+
<%# Rich text content %>
|
|
3
|
+
<% if can_edit_cms? && field %>
|
|
4
|
+
<%= content_tag(:div, raw(value), html_options.merge(class: css_classes, data: data_attributes)) %>
|
|
5
|
+
<% else %>
|
|
6
|
+
<%= raw(value) %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% else %>
|
|
9
|
+
<%# Regular text content %>
|
|
10
|
+
<% if can_edit_cms? && field %>
|
|
11
|
+
<%= content_tag(tag, value, html_options.merge(class: css_classes, data: data_attributes)) %>
|
|
12
|
+
<% else %>
|
|
13
|
+
<%= content_tag(tag, value, html_options) %>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% end %>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class EditableContentComponent < BaseComponent
|
|
3
|
+
attr_reader :section, :key, :tag, :default, :html_options
|
|
4
|
+
|
|
5
|
+
def initialize(section:, key:, tag: :span, default: nil, page: nil, **html_options)
|
|
6
|
+
super(page: page)
|
|
7
|
+
@section = section
|
|
8
|
+
@key = key
|
|
9
|
+
@tag = tag
|
|
10
|
+
@default = default
|
|
11
|
+
@html_options = html_options
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
attr_reader :field
|
|
17
|
+
|
|
18
|
+
def field
|
|
19
|
+
@field ||= find_field(section, key)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def value
|
|
23
|
+
if field&.rich_text?
|
|
24
|
+
helpers.page_content_html(page, section, key, default: default)
|
|
25
|
+
else
|
|
26
|
+
field_value(section, key, default: default)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def inline_editable?
|
|
31
|
+
field&.text? || field&.url? || field&.color?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def data_attributes
|
|
35
|
+
return {} unless can_edit_cms? && field
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
controller: 'inline-edit',
|
|
39
|
+
inline_edit_field_id_value: field.id,
|
|
40
|
+
inline_edit_type_value: field.content_type,
|
|
41
|
+
inline_edit_inline_value: inline_editable?,
|
|
42
|
+
inline_edit_page_value: page_slug,
|
|
43
|
+
inline_edit_section_value: section,
|
|
44
|
+
inline_edit_key_value: key
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def css_classes
|
|
49
|
+
base_classes = html_options[:class] || ''
|
|
50
|
+
can_edit_cms? && field ? "#{base_classes} cms-inline-field".strip : base_classes
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<% cache cache_key, expires_in: 1.hour do %>
|
|
2
|
+
<% if can_edit_cms? %>
|
|
3
|
+
<div class="cms-editable-section"
|
|
4
|
+
data-cms-section="<%= page_slug %>/<%= section %>"
|
|
5
|
+
data-controller="cms-sticky-overlay"
|
|
6
|
+
data-action="mouseenter->cms-sticky-overlay#mouseEnter mouseleave->cms-sticky-overlay#mouseLeave">
|
|
7
|
+
<%= content %>
|
|
8
|
+
<div class="cms-edit-overlay" data-cms-sticky-overlay-target="overlay">
|
|
9
|
+
<div class="cms-edit-controls">
|
|
10
|
+
<span class="cms-section-title"><%= full_title %></span>
|
|
11
|
+
<%= link_to 'Edit', edit_url, target: '_blank', class: 'cms-edit-button', data: { turbo: false } %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<% else %>
|
|
16
|
+
<%= content %>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class SectionComponent < BaseComponent
|
|
3
|
+
attr_reader :section, :title
|
|
4
|
+
|
|
5
|
+
def initialize(section:, title: nil, page: nil, **options)
|
|
6
|
+
super(page: page)
|
|
7
|
+
@section = section
|
|
8
|
+
@title = title
|
|
9
|
+
@options = options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def cache_key
|
|
15
|
+
super(section)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def display_title
|
|
19
|
+
return title if title.present?
|
|
20
|
+
section.humanize
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def page_title
|
|
24
|
+
page.is_a?(LeanCms::Page) ? page.title : page_slug.titleize
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def full_title
|
|
28
|
+
"#{page_title} - #{display_title}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def edit_url
|
|
32
|
+
@view_context.lean_cms_edit_page_content_path(page: page_slug, section: section)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
module Authentication
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
before_action :resume_session
|
|
7
|
+
before_action :require_authentication
|
|
8
|
+
helper_method :authenticated?, :current_user
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
def allow_unauthenticated_access(**options)
|
|
13
|
+
skip_before_action :require_authentication, **options
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def authenticated?
|
|
20
|
+
LeanCms::Current.session.present?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def current_user
|
|
24
|
+
LeanCms::Current.user
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def require_authentication
|
|
28
|
+
authenticated? || request_authentication
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def resume_session
|
|
32
|
+
LeanCms::Current.session ||= find_session_by_cookie
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def find_session_by_cookie
|
|
36
|
+
LeanCms::Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def request_authentication
|
|
40
|
+
session[:return_to_after_authenticating] = request.url
|
|
41
|
+
redirect_to lean_cms_new_session_path
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def after_authentication_url
|
|
45
|
+
session.delete(:return_to_after_authenticating) || root_url
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def start_new_session_for(user)
|
|
49
|
+
LeanCms::Session.create!(user: user, user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
|
|
50
|
+
LeanCms::Current.session = session
|
|
51
|
+
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def terminate_session
|
|
56
|
+
LeanCms::Current.session.destroy
|
|
57
|
+
cookies.delete(:session_id)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
module Authorization
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
before_action :require_cms_access
|
|
7
|
+
helper_method :current_user
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# Base access check - user must have at least one CMS permission
|
|
13
|
+
def require_cms_access
|
|
14
|
+
unless LeanCms::Current.user&.has_any_cms_permission?
|
|
15
|
+
redirect_to root_path, alert: "You do not have access to the CMS."
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Specific permission checks for controllers that need them
|
|
20
|
+
def require_page_editing
|
|
21
|
+
unless LeanCms::Current.user&.can_edit_pages?
|
|
22
|
+
redirect_to lean_cms_root_path, alert: "You do not have permission to edit pages."
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def require_blog_editing
|
|
27
|
+
unless LeanCms::Current.user&.can_edit_blog?
|
|
28
|
+
redirect_to lean_cms_root_path, alert: "You do not have permission to edit blog posts."
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def require_user_management
|
|
33
|
+
unless LeanCms::Current.user&.can_manage_users?
|
|
34
|
+
redirect_to lean_cms_root_path, alert: "You do not have permission to manage users."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def require_settings_access
|
|
39
|
+
unless LeanCms::Current.user&.can_access_settings?
|
|
40
|
+
redirect_to lean_cms_root_path, alert: "You do not have permission to access settings."
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check if current user can edit a specific record
|
|
45
|
+
def can_edit?(record)
|
|
46
|
+
return true if LeanCms::Current.user&.is_super_admin?
|
|
47
|
+
return true if record.respond_to?(:author) && record.author == LeanCms::Current.user
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def set_paper_trail_whodunnit
|
|
52
|
+
PaperTrail.request.whodunnit = LeanCms::Current.user&.id
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Helper method for views
|
|
56
|
+
def current_user
|
|
57
|
+
LeanCms::Current.user
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class ActivityController < ApplicationController
|
|
3
|
+
skip_before_action :check_content_lock
|
|
4
|
+
before_action :require_settings_access
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@versions = PaperTrail::Version
|
|
8
|
+
.order(created_at: :desc)
|
|
9
|
+
.page(params[:page])
|
|
10
|
+
.per(20)
|
|
11
|
+
|
|
12
|
+
@versions = @versions.where(item_type: params[:item_type]) if params[:item_type].present?
|
|
13
|
+
@versions = @versions.where(whodunnit: params[:whodunnit]) if params[:whodunnit].present?
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class ApplicationController < ::ApplicationController
|
|
3
|
+
# Pundit is included here so the gem's admin controllers (UsersController
|
|
4
|
+
# most prominently) can call `authorize` / `policy_scope` without
|
|
5
|
+
# requiring the host's ApplicationController to include it. Hosts that
|
|
6
|
+
# want Pundit available in their own non-CMS controllers should still
|
|
7
|
+
# `include Pundit::Authorization` in their own ApplicationController.
|
|
8
|
+
include Pundit::Authorization
|
|
9
|
+
|
|
10
|
+
include LeanCms::Authorization
|
|
11
|
+
|
|
12
|
+
layout 'lean_cms/application'
|
|
13
|
+
|
|
14
|
+
before_action :set_paper_trail_whodunnit
|
|
15
|
+
before_action :check_content_lock, only: [:create, :update]
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def can_edit?(resource)
|
|
20
|
+
return true if current_user&.is_super_admin?
|
|
21
|
+
return true if resource.respond_to?(:author) && resource.author_id == current_user&.id
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
helper_method :can_edit?
|
|
25
|
+
|
|
26
|
+
def content_locked?
|
|
27
|
+
LeanCms::Setting.content_locked?
|
|
28
|
+
end
|
|
29
|
+
helper_method :content_locked?
|
|
30
|
+
|
|
31
|
+
def content_lock_info
|
|
32
|
+
LeanCms::Setting.content_lock_info
|
|
33
|
+
end
|
|
34
|
+
helper_method :content_lock_info
|
|
35
|
+
|
|
36
|
+
def check_content_lock
|
|
37
|
+
return unless content_locked?
|
|
38
|
+
|
|
39
|
+
lock_info = content_lock_info
|
|
40
|
+
message = "Content editing is temporarily locked: #{lock_info[:reason]}"
|
|
41
|
+
|
|
42
|
+
respond_to do |format|
|
|
43
|
+
format.html { redirect_back fallback_location: lean_cms_dashboard_path, alert: message }
|
|
44
|
+
format.json { render json: { error: message, locked: true }, status: :locked }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class DashboardController < LeanCms::ApplicationController
|
|
3
|
+
skip_before_action :check_content_lock
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
@recent_posts = LeanCms::Post.recent.limit(5)
|
|
7
|
+
@recent_submissions = LeanCms::FormSubmission.recent.limit(10)
|
|
8
|
+
@unread_submissions_count = LeanCms::FormSubmission.unread.count
|
|
9
|
+
@draft_posts_count = LeanCms::Post.draft.count
|
|
10
|
+
@published_posts_count = LeanCms::Post.published.count
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class FormSubmissionsController < LeanCms::ApplicationController
|
|
3
|
+
skip_before_action :check_content_lock
|
|
4
|
+
before_action :set_form_submission, only: [:show, :mark_as_read, :mark_as_replied, :destroy]
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@form_submissions = LeanCms::FormSubmission.recent
|
|
8
|
+
@form_submissions = @form_submissions.where(form_type: params[:form_type]) if params[:form_type].present?
|
|
9
|
+
@form_submissions = @form_submissions.where(status: params[:status]) if params[:status].present?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def show
|
|
13
|
+
@form_submission.mark_as_read! if @form_submission.unread?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def mark_as_read
|
|
17
|
+
@form_submission.mark_as_read!
|
|
18
|
+
redirect_to lean_cms_form_submissions_path, notice: 'Submission marked as read.'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def mark_as_replied
|
|
22
|
+
@form_submission.mark_as_replied!
|
|
23
|
+
redirect_to lean_cms_form_submissions_path, notice: 'Submission marked as replied.'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def destroy
|
|
27
|
+
@form_submission.destroy
|
|
28
|
+
redirect_to lean_cms_form_submissions_path, notice: 'Submission deleted successfully.'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def set_form_submission
|
|
34
|
+
@form_submission = LeanCms::FormSubmission.find(params[:id])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|