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.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +235 -0
  3. data/LICENSE +21 -0
  4. data/README.md +107 -0
  5. data/app/assets/images/lean_cms/sloth-404.png +0 -0
  6. data/app/assets/images/lean_cms/sloth-500.png +0 -0
  7. data/app/assets/images/lean_cms/sloth-favicon-16.png +0 -0
  8. data/app/assets/images/lean_cms/sloth-favicon-32.png +0 -0
  9. data/app/assets/images/lean_cms/sloth-favicon-64.png +0 -0
  10. data/app/assets/images/lean_cms/sloth-logo.png +0 -0
  11. data/app/assets/lean_cms/actiontext.css +440 -0
  12. data/app/assets/lean_cms/cms_edit_controls.css +548 -0
  13. data/app/assets/tailwind/lean_cms/engine.css +14 -0
  14. data/app/components/lean_cms/base_component.rb +61 -0
  15. data/app/components/lean_cms/bullets_section_component.html.erb +23 -0
  16. data/app/components/lean_cms/bullets_section_component.rb +54 -0
  17. data/app/components/lean_cms/cards_section_component.html.erb +237 -0
  18. data/app/components/lean_cms/cards_section_component.rb +71 -0
  19. data/app/components/lean_cms/editable_content_component.html.erb +15 -0
  20. data/app/components/lean_cms/editable_content_component.rb +53 -0
  21. data/app/components/lean_cms/section_component.html.erb +18 -0
  22. data/app/components/lean_cms/section_component.rb +35 -0
  23. data/app/controllers/concerns/lean_cms/authentication.rb +60 -0
  24. data/app/controllers/concerns/lean_cms/authorization.rb +60 -0
  25. data/app/controllers/lean_cms/activity_controller.rb +16 -0
  26. data/app/controllers/lean_cms/application_controller.rb +48 -0
  27. data/app/controllers/lean_cms/dashboard_controller.rb +13 -0
  28. data/app/controllers/lean_cms/form_submissions_controller.rb +37 -0
  29. data/app/controllers/lean_cms/notification_settings_controller.rb +145 -0
  30. data/app/controllers/lean_cms/notifications_controller.rb +26 -0
  31. data/app/controllers/lean_cms/page_contents_controller.rb +403 -0
  32. data/app/controllers/lean_cms/password_setup_controller.rb +65 -0
  33. data/app/controllers/lean_cms/passwords_controller.rb +42 -0
  34. data/app/controllers/lean_cms/posts_controller.rb +78 -0
  35. data/app/controllers/lean_cms/sessions_controller.rb +50 -0
  36. data/app/controllers/lean_cms/settings_controller.rb +124 -0
  37. data/app/controllers/lean_cms/users_controller.rb +113 -0
  38. data/app/helpers/lean_cms/activity_helper.rb +190 -0
  39. data/app/helpers/lean_cms/application_helper.rb +43 -0
  40. data/app/helpers/lean_cms/content_helper.rb +34 -0
  41. data/app/helpers/lean_cms/page_content_helper.rb +359 -0
  42. data/app/javascript/controllers/cards_editor_controller.js +317 -0
  43. data/app/javascript/controllers/cms_sticky_overlay_controller.js +59 -0
  44. data/app/javascript/controllers/field_editor_form_controller.js +68 -0
  45. data/app/javascript/controllers/field_editor_modal_controller.js +79 -0
  46. data/app/javascript/controllers/inline_edit_controller.js +414 -0
  47. data/app/javascript/controllers/inline_edit_toggle_controller.js +81 -0
  48. data/app/javascript/controllers/notifications_controller.js +19 -0
  49. data/app/javascript/controllers/settings_inline_edit_sync_controller.js +38 -0
  50. data/app/javascript/controllers/settings_override_controller.js +45 -0
  51. data/app/mailers/lean_cms/application_mailer.rb +6 -0
  52. data/app/mailers/lean_cms/passwords_mailer.rb +8 -0
  53. data/app/mailers/lean_cms/users_mailer.rb +39 -0
  54. data/app/models/lean_cms/current.rb +6 -0
  55. data/app/models/lean_cms/form_submission.rb +45 -0
  56. data/app/models/lean_cms/magic_link.rb +76 -0
  57. data/app/models/lean_cms/meta_tag.rb +30 -0
  58. data/app/models/lean_cms/notification_setting.rb +69 -0
  59. data/app/models/lean_cms/page.rb +23 -0
  60. data/app/models/lean_cms/page_content.rb +245 -0
  61. data/app/models/lean_cms/post.rb +65 -0
  62. data/app/models/lean_cms/session.rb +7 -0
  63. data/app/models/lean_cms/setting.rb +156 -0
  64. data/app/policies/lean_cms/application_policy.rb +35 -0
  65. data/app/policies/lean_cms/page_content_policy.rb +31 -0
  66. data/app/policies/lean_cms/post_policy.rb +37 -0
  67. data/app/policies/lean_cms/setting_policy.rb +17 -0
  68. data/app/views/layouts/lean_cms/application.html.erb +114 -0
  69. data/app/views/layouts/lean_cms/auth.html.erb +200 -0
  70. data/app/views/lean_cms/activity/index.html.erb +79 -0
  71. data/app/views/lean_cms/dashboard/index.html.erb +180 -0
  72. data/app/views/lean_cms/form_submissions/index.html.erb +104 -0
  73. data/app/views/lean_cms/form_submissions/show.html.erb +157 -0
  74. data/app/views/lean_cms/notification_settings/edit.html.erb +192 -0
  75. data/app/views/lean_cms/notifications/index.html.erb +72 -0
  76. data/app/views/lean_cms/notifications/show.html.erb +39 -0
  77. data/app/views/lean_cms/page_contents/_field_editor.html.erb +174 -0
  78. data/app/views/lean_cms/page_contents/edit.html.erb +428 -0
  79. data/app/views/lean_cms/page_contents/index.html.erb +113 -0
  80. data/app/views/lean_cms/password_setup/show.html.erb +35 -0
  81. data/app/views/lean_cms/passwords/edit.html.erb +26 -0
  82. data/app/views/lean_cms/passwords/new.html.erb +21 -0
  83. data/app/views/lean_cms/passwords_mailer/reset.html.erb +6 -0
  84. data/app/views/lean_cms/passwords_mailer/reset.text.erb +4 -0
  85. data/app/views/lean_cms/posts/_form.html.erb +118 -0
  86. data/app/views/lean_cms/posts/edit.html.erb +31 -0
  87. data/app/views/lean_cms/posts/index.html.erb +100 -0
  88. data/app/views/lean_cms/posts/new.html.erb +16 -0
  89. data/app/views/lean_cms/sessions/new.html.erb +28 -0
  90. data/app/views/lean_cms/settings/edit.html.erb +384 -0
  91. data/app/views/lean_cms/shared/_admin_bar.html.erb +85 -0
  92. data/app/views/lean_cms/shared/_header.html.erb +86 -0
  93. data/app/views/lean_cms/shared/_notifications_bell.html.erb +84 -0
  94. data/app/views/lean_cms/shared/_sidebar.html.erb +102 -0
  95. data/app/views/lean_cms/users/_form.html.erb +105 -0
  96. data/app/views/lean_cms/users/edit.html.erb +8 -0
  97. data/app/views/lean_cms/users/index.html.erb +99 -0
  98. data/app/views/lean_cms/users/new.html.erb +8 -0
  99. data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.html.erb +13 -0
  100. data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.text.erb +11 -0
  101. data/app/views/lean_cms/users_mailer/invitation.html.erb +13 -0
  102. data/app/views/lean_cms/users_mailer/invitation.text.erb +11 -0
  103. data/app/views/lean_cms/users_mailer/reactivation.html.erb +13 -0
  104. data/app/views/lean_cms/users_mailer/reactivation.text.erb +11 -0
  105. data/config/importmap.rb +8 -0
  106. data/config/routes.rb +78 -0
  107. data/db/migrate/20251112034030_create_lean_cms_tables.rb +131 -0
  108. data/db/migrate/20260513000001_create_lean_cms_auth_tables.rb +31 -0
  109. data/db/migrate/20260514000001_create_paper_trail_versions.rb +16 -0
  110. data/db/migrate/20260514000002_create_action_text_tables.rb +18 -0
  111. data/db/migrate/20260514000003_create_active_storage_tables.rb +45 -0
  112. data/db/migrate/20260514000004_create_noticed_tables.rb +27 -0
  113. data/lib/generators/lean_cms/demo/demo_generator.rb +54 -0
  114. data/lib/generators/lean_cms/demo/templates/lean_cms_structure.yml +129 -0
  115. data/lib/generators/lean_cms/demo/templates/pages_controller.rb +30 -0
  116. data/lib/generators/lean_cms/demo/templates/views/pages/about.html.erb +40 -0
  117. data/lib/generators/lean_cms/demo/templates/views/pages/contact.html.erb +55 -0
  118. data/lib/generators/lean_cms/demo/templates/views/pages/home.html.erb +31 -0
  119. data/lib/generators/lean_cms/install/install_generator.rb +317 -0
  120. data/lib/generators/lean_cms/install/templates/add_lean_cms_columns_to_users.rb.tt +7 -0
  121. data/lib/generators/lean_cms/install/templates/lean_cms.rb +11 -0
  122. data/lib/generators/lean_cms/install/templates/lean_cms_structure.yml +29 -0
  123. data/lib/lean_cms/configuration.rb +32 -0
  124. data/lib/lean_cms/engine.rb +93 -0
  125. data/lib/lean_cms/loader.rb +217 -0
  126. data/lib/lean_cms/sync_helper.rb +182 -0
  127. data/lib/lean_cms/version.rb +3 -0
  128. data/lib/lean_cms.rb +26 -0
  129. data/lib/tasks/lean_cms.rake +390 -0
  130. metadata +313 -0
@@ -0,0 +1,102 @@
1
+ <aside class="w-64 bg-white border-r border-gray-200 flex-shrink-0 overflow-y-auto">
2
+ <!-- Logo / Brand -->
3
+ <div class="h-16 flex items-center px-6 border-b border-gray-200">
4
+ <div class="flex items-center space-x-3">
5
+ <% if LeanCms.site_logo_path.present? %>
6
+ <%= image_tag LeanCms.site_logo_path, alt: LeanCms.site_name, class: "h-10 w-auto" %>
7
+ <% else %>
8
+ <div class="h-10 w-10 rounded-lg flex items-center justify-center text-white font-bold text-lg" style="background-color: var(--cms-primary);">
9
+ <%= LeanCms.site_name[0..1].upcase %>
10
+ </div>
11
+ <% end %>
12
+ </div>
13
+ </div>
14
+
15
+ <!-- Navigation -->
16
+ <nav class="mt-6 px-3">
17
+ <!-- Dashboard -->
18
+ <%= link_to lean_cms_root_path, class: "flex items-center px-3 py-2 text-sm font-medium rounded-lg mb-1 transition-colors #{current_page?(lean_cms_root_path) ? 'text-white' : 'text-gray-700 hover:bg-gray-100'}", style: current_page?(lean_cms_root_path) ? "background-color: var(--cms-primary);" : "" do %>
19
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
20
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
21
+ </svg>
22
+ Dashboard
23
+ <% end %>
24
+
25
+ <% if current_user&.can_edit_blog? || current_user&.can_edit_pages? %>
26
+ <!-- Content Section -->
27
+ <div class="mt-6 mb-2 px-3">
28
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Content</h3>
29
+ </div>
30
+
31
+ <% if current_user&.can_edit_blog? %>
32
+ <!-- Blog Posts -->
33
+ <%= link_to lean_cms_posts_path, class: "flex items-center px-3 py-2 text-sm font-medium rounded-lg mb-1 transition-colors #{controller_name == 'posts' ? 'text-white' : 'text-gray-700 hover:bg-gray-100'}", style: controller_name == 'posts' ? "background-color: var(--cms-primary);" : "" do %>
34
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
35
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/>
36
+ </svg>
37
+ Blog & Portfolio
38
+ <% if (count = LeanCms::Post.draft.count) > 0 %>
39
+ <span class="ml-auto bg-yellow-100 text-yellow-800 text-xs font-medium px-2 py-0.5 rounded-full"><%= count %></span>
40
+ <% end %>
41
+ <% end %>
42
+ <% end %>
43
+
44
+ <% if current_user&.can_edit_pages? %>
45
+ <!-- Page Content -->
46
+ <%= link_to lean_cms_page_contents_path, class: "flex items-center px-3 py-2 text-sm font-medium rounded-lg mb-1 transition-colors #{controller_name == 'page_contents' ? 'text-white' : 'text-gray-700 hover:bg-gray-100'}", style: controller_name == 'page_contents' ? "background-color: var(--cms-primary);" : "" do %>
47
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
48
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
49
+ </svg>
50
+ Page Content
51
+ <% end %>
52
+ <% end %>
53
+ <% end %>
54
+
55
+ <!-- Form Submissions -->
56
+ <%= link_to lean_cms_form_submissions_path, class: "flex items-center px-3 py-2 text-sm font-medium rounded-lg mb-1 transition-colors #{controller_name == 'form_submissions' ? 'text-white' : 'text-gray-700 hover:bg-gray-100'}", style: controller_name == 'form_submissions' ? "background-color: var(--cms-primary);" : "" do %>
57
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
58
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
59
+ </svg>
60
+ Form Submissions
61
+ <% if (count = LeanCms::FormSubmission.unread.count) > 0 %>
62
+ <span class="ml-auto bg-red-100 text-red-800 text-xs font-medium px-2 py-0.5 rounded-full"><%= count %></span>
63
+ <% end %>
64
+ <% end %>
65
+
66
+ <% if current_user&.can_manage_users? %>
67
+ <!-- Administration Section -->
68
+ <div class="mt-6 mb-2 px-3">
69
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Administration</h3>
70
+ </div>
71
+
72
+ <!-- User Management -->
73
+ <%= link_to lean_cms_users_path, class: "flex items-center px-3 py-2 text-sm font-medium rounded-lg mb-1 transition-colors #{controller_name == 'users' ? 'text-white' : 'text-gray-700 hover:bg-gray-100'}", style: controller_name == 'users' ? "background-color: var(--cms-primary);" : "" do %>
74
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
75
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
76
+ </svg>
77
+ Users
78
+ <% end %>
79
+
80
+ <!-- Notification Settings -->
81
+ <% if current_user&.can_access_settings? %>
82
+ <%= link_to edit_lean_cms_notification_settings_path, class: "flex items-center px-3 py-2 text-sm font-medium rounded-lg mb-1 transition-colors #{controller_name == 'notification_settings' ? 'text-white' : 'text-gray-700 hover:bg-gray-100'}", style: controller_name == 'notification_settings' ? "background-color: var(--cms-primary);" : "" do %>
83
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
84
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
85
+ </svg>
86
+ Notification Settings
87
+ <% end %>
88
+ <% end %>
89
+
90
+ <!-- Activity Log (super admins only) -->
91
+ <% if current_user&.is_super_admin? %>
92
+ <%= link_to lean_cms_activity_path, class: "flex items-center px-3 py-2 text-sm font-medium rounded-lg mb-1 transition-colors #{controller_name == 'activity' ? 'text-white' : 'text-gray-700 hover:bg-gray-100'}", style: controller_name == 'activity' ? "background-color: var(--cms-primary);" : "" do %>
93
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
94
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
95
+ </svg>
96
+ Activity Log
97
+ <% end %>
98
+ <% end %>
99
+ <% end %>
100
+
101
+ </nav>
102
+ </aside>
@@ -0,0 +1,105 @@
1
+ <%= form_with model: [:lean_cms, @user], class: "space-y-6" do |f| %>
2
+ <% if @user.errors.any? %>
3
+ <div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg">
4
+ <h3 class="font-medium">Please correct the following errors:</h3>
5
+ <ul class="mt-2 list-disc list-inside">
6
+ <% @user.errors.full_messages.each do |message| %>
7
+ <li><%= message %></li>
8
+ <% end %>
9
+ </ul>
10
+ </div>
11
+ <% end %>
12
+
13
+ <!-- Basic Information -->
14
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
15
+ <h2 class="text-xl font-semibold text-gray-900 mb-4">User Information</h2>
16
+
17
+ <div class="space-y-4">
18
+ <div>
19
+ <%= f.label :name, class: "block text-sm font-medium text-gray-700 mb-1" %>
20
+ <%= f.text_field :name, class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#b82025]/20 focus:border-[#b82025] outline-none", placeholder: "Full name" %>
21
+ </div>
22
+
23
+ <div>
24
+ <%= f.label :email_address, class: "block text-sm font-medium text-gray-700 mb-1" %>
25
+ <%= f.email_field :email_address, class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#b82025]/20 focus:border-[#b82025] outline-none", placeholder: "email@example.com" %>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ <!-- Permissions -->
31
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
32
+ <h2 class="text-xl font-semibold text-gray-900 mb-4">Permissions</h2>
33
+ <p class="text-sm text-gray-600 mb-4">Select what this user can do in the CMS.</p>
34
+
35
+ <div class="space-y-4">
36
+ <div class="flex items-start">
37
+ <div class="flex items-center h-5">
38
+ <%= f.check_box :can_edit_pages, class: "w-4 h-4 text-[#b82025] bg-gray-100 border-gray-300 rounded focus:ring-[#b82025] focus:ring-2" %>
39
+ </div>
40
+ <div class="ml-3">
41
+ <%= f.label :can_edit_pages, class: "font-medium text-gray-900" do %>
42
+ Can Edit Pages
43
+ <% end %>
44
+ <p class="text-sm text-gray-600">Edit page content and sections</p>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="flex items-start">
49
+ <div class="flex items-center h-5">
50
+ <%= f.check_box :can_edit_blog, class: "w-4 h-4 text-[#b82025] bg-gray-100 border-gray-300 rounded focus:ring-[#b82025] focus:ring-2" %>
51
+ </div>
52
+ <div class="ml-3">
53
+ <%= f.label :can_edit_blog, class: "font-medium text-gray-900" do %>
54
+ Can Edit Blog
55
+ <% end %>
56
+ <p class="text-sm text-gray-600">Create and edit blog posts and portfolio items</p>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="flex items-start">
61
+ <div class="flex items-center h-5">
62
+ <%= f.check_box :can_manage_users, class: "w-4 h-4 text-[#b82025] bg-gray-100 border-gray-300 rounded focus:ring-[#b82025] focus:ring-2" %>
63
+ </div>
64
+ <div class="ml-3">
65
+ <%= f.label :can_manage_users, class: "font-medium text-gray-900" do %>
66
+ Can Manage Users
67
+ <% end %>
68
+ <p class="text-sm text-gray-600">Add, edit, and deactivate other users (except super admins)</p>
69
+ </div>
70
+ </div>
71
+
72
+ <% if current_user.is_super_admin? %>
73
+ <div class="flex items-start border-t border-gray-200 pt-4">
74
+ <div class="flex items-center h-5">
75
+ <%= f.check_box :can_access_settings, class: "w-4 h-4 text-[#b82025] bg-gray-100 border-gray-300 rounded focus:ring-[#b82025] focus:ring-2" %>
76
+ </div>
77
+ <div class="ml-3">
78
+ <%= f.label :can_access_settings, class: "font-medium text-gray-900" do %>
79
+ Can Access Settings
80
+ <% end %>
81
+ <p class="text-sm text-gray-600">Access and modify system settings</p>
82
+ </div>
83
+ </div>
84
+
85
+ <div class="flex items-start pt-2">
86
+ <div class="flex items-center h-5">
87
+ <%= f.check_box :is_super_admin, class: "w-4 h-4 text-purple-600 bg-gray-100 border-gray-300 rounded focus:ring-purple-500 focus:ring-2" %>
88
+ </div>
89
+ <div class="ml-3">
90
+ <%= f.label :is_super_admin, class: "font-medium text-purple-900" do %>
91
+ Super Admin
92
+ <% end %>
93
+ <p class="text-sm text-purple-600">Has all permissions and cannot be modified by regular admins. Use sparingly.</p>
94
+ </div>
95
+ </div>
96
+ <% end %>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- Submit -->
101
+ <div class="flex items-center justify-between pt-4">
102
+ <%= link_to "Cancel", lean_cms_users_path, class: "px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors" %>
103
+ <%= f.submit @user.new_record? ? "Send Invitation" : "Update User", class: "px-6 py-2 bg-[#b82025] hover:bg-[#a01c20] text-white font-semibold rounded-lg transition-colors cursor-pointer" %>
104
+ </div>
105
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <div class="max-w-2xl">
2
+ <div class="mb-8">
3
+ <h1 class="text-3xl font-bold text-gray-900">Edit User</h1>
4
+ <p class="text-gray-600 mt-2">Update user information and permissions</p>
5
+ </div>
6
+
7
+ <%= render "form" %>
8
+ </div>
@@ -0,0 +1,99 @@
1
+ <div class="max-w-6xl">
2
+ <div class="flex justify-between items-center mb-8">
3
+ <div>
4
+ <h1 class="text-3xl font-bold text-gray-900">User Management</h1>
5
+ <p class="text-gray-600 mt-2">Manage CMS user accounts and permissions</p>
6
+ </div>
7
+ <%= link_to new_lean_cms_user_path, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white transition-colors hover:opacity-90", style: "background-color: var(--cms-primary, #b82025);" do %>
8
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
9
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
10
+ </svg>
11
+ Add User
12
+ <% end %>
13
+ </div>
14
+
15
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
16
+ <table class="min-w-full divide-y divide-gray-200">
17
+ <thead class="bg-gray-50">
18
+ <tr>
19
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
20
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Permissions</th>
21
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
22
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Login</th>
23
+ <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
24
+ </tr>
25
+ </thead>
26
+ <tbody class="bg-white divide-y divide-gray-200">
27
+ <% @users.each do |user| %>
28
+ <tr class="<%= user.active? ? '' : 'bg-gray-50 opacity-60' %>">
29
+ <td class="px-6 py-4 whitespace-nowrap">
30
+ <div class="flex items-center">
31
+ <div class="h-10 w-10 rounded-full flex items-center justify-center text-white text-sm font-medium" style="background-color: var(--cms-primary, #b82025);">
32
+ <%= user.display_name[0].upcase %>
33
+ </div>
34
+ <div class="ml-4">
35
+ <div class="text-sm font-medium text-gray-900">
36
+ <%= user.display_name %>
37
+ <% if user.is_super_admin? %>
38
+ <span class="ml-2 px-2 py-0.5 text-xs bg-purple-100 text-purple-800 rounded-full">Super Admin</span>
39
+ <% end %>
40
+ <% if user == current_user %>
41
+ <span class="ml-1 px-2 py-0.5 text-xs bg-blue-100 text-blue-800 rounded-full">You</span>
42
+ <% end %>
43
+ </div>
44
+ <div class="text-sm text-gray-500"><%= user.email_address %></div>
45
+ </div>
46
+ </div>
47
+ </td>
48
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
49
+ <%= user.permissions_summary %>
50
+ </td>
51
+ <td class="px-6 py-4 whitespace-nowrap">
52
+ <% if user.active? %>
53
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Active</span>
54
+ <% else %>
55
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Inactive</span>
56
+ <% end %>
57
+ </td>
58
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
59
+ <%= user.last_login_at ? time_ago_in_words(user.last_login_at) + " ago" : "Never" %>
60
+ </td>
61
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
62
+ <% if policy(user).update? %>
63
+ <%= link_to "Edit", edit_lean_cms_user_path(user), class: "text-[#b82025] hover:text-[#a01c20] mr-3" %>
64
+ <% end %>
65
+
66
+ <% if user.active? && policy(user).deactivate? && user != current_user %>
67
+ <%= button_to "Deactivate", deactivate_lean_cms_user_path(user), method: :patch, class: "text-gray-600 hover:text-gray-900 mr-3", form: { data: { turbo_confirm: "Are you sure you want to deactivate this user? They will no longer be able to log in." } } %>
68
+ <% elsif !user.active? && policy(user).activate? %>
69
+ <%= button_to "Activate", activate_lean_cms_user_path(user), method: :patch, class: "text-green-600 hover:text-green-900 mr-3" %>
70
+ <% end %>
71
+
72
+ <% if policy(user).send_password_reset? && user.active? %>
73
+ <%= button_to "Reset Password", send_password_reset_lean_cms_user_path(user), method: :post, class: "text-blue-600 hover:text-blue-900", form: { data: { turbo_confirm: "Send password reset email to #{user.email_address}?" } } %>
74
+ <% end %>
75
+ </td>
76
+ </tr>
77
+ <% end %>
78
+ </tbody>
79
+ </table>
80
+
81
+ <% if @users.empty? %>
82
+ <div class="text-center py-12">
83
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
84
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
85
+ </svg>
86
+ <h3 class="mt-2 text-sm font-medium text-gray-900">No users</h3>
87
+ <p class="mt-1 text-sm text-gray-500">Get started by inviting a new user.</p>
88
+ <div class="mt-6">
89
+ <%= link_to new_lean_cms_user_path, class: "inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-[#b82025] hover:bg-[#a01c20]" do %>
90
+ <svg class="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
91
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
92
+ </svg>
93
+ Add User
94
+ <% end %>
95
+ </div>
96
+ </div>
97
+ <% end %>
98
+ </div>
99
+ </div>
@@ -0,0 +1,8 @@
1
+ <div class="max-w-2xl">
2
+ <div class="mb-8">
3
+ <h1 class="text-3xl font-bold text-gray-900">Invite New User</h1>
4
+ <p class="text-gray-600 mt-2">The user will receive an email to set their password</p>
5
+ </div>
6
+
7
+ <%= render "form" %>
8
+ </div>
@@ -0,0 +1,13 @@
1
+ <h1>Password Reset Request</h1>
2
+
3
+ <p>Hello <%= @user.display_name %>,</p>
4
+
5
+ <p>An administrator has requested a password reset for your account on <%= @site_name %> CMS.</p>
6
+
7
+ <p>Click the link below to set a new password:</p>
8
+
9
+ <p><%= link_to "Reset Your Password", @setup_url %></p>
10
+
11
+ <p>This link will expire in 2 hours.</p>
12
+
13
+ <p>If you did not expect this email, please contact your administrator.</p>
@@ -0,0 +1,11 @@
1
+ Password Reset Request
2
+
3
+ Hello <%= @user.display_name %>,
4
+
5
+ An administrator has requested a password reset for your account on <%= @site_name %> CMS.
6
+
7
+ Reset your password by visiting: <%= @setup_url %>
8
+
9
+ This link will expire in 2 hours.
10
+
11
+ If you did not expect this email, please contact your administrator.
@@ -0,0 +1,13 @@
1
+ <h1>Welcome to <%= @site_name %> CMS</h1>
2
+
3
+ <p>Hello <%= @user.display_name %>,</p>
4
+
5
+ <p>You've been invited to manage content on <%= @site_name %>.</p>
6
+
7
+ <p>Click the link below to set your password and activate your account:</p>
8
+
9
+ <p><%= link_to "Set Your Password", @setup_url %></p>
10
+
11
+ <p>This link will expire in 24 hours.</p>
12
+
13
+ <p>If you did not expect this invitation, you can safely ignore this email.</p>
@@ -0,0 +1,11 @@
1
+ Welcome to <%= @site_name %> CMS
2
+
3
+ Hello <%= @user.display_name %>,
4
+
5
+ You've been invited to manage content on <%= @site_name %>.
6
+
7
+ Set your password by visiting: <%= @setup_url %>
8
+
9
+ This link will expire in 24 hours.
10
+
11
+ If you did not expect this invitation, you can safely ignore this email.
@@ -0,0 +1,13 @@
1
+ <h1>Your Account Has Been Reactivated</h1>
2
+
3
+ <p>Hello <%= @user.display_name %>,</p>
4
+
5
+ <p>Your account on <%= @site_name %> CMS has been reactivated.</p>
6
+
7
+ <p>For security, you'll need to set a new password. Click the link below:</p>
8
+
9
+ <p><%= link_to "Set Your Password", @setup_url %></p>
10
+
11
+ <p>This link will expire in 2 hours.</p>
12
+
13
+ <p>If you did not expect this email, please contact your administrator.</p>
@@ -0,0 +1,11 @@
1
+ Your Account Has Been Reactivated
2
+
3
+ Hello <%= @user.display_name %>,
4
+
5
+ Your account on <%= @site_name %> CMS has been reactivated.
6
+
7
+ For security, you'll need to set a new password. Visit: <%= @setup_url %>
8
+
9
+ This link will expire in 2 hours.
10
+
11
+ If you did not expect this email, please contact your administrator.
@@ -0,0 +1,8 @@
1
+ pin_all_from File.expand_path("../app/javascript/controllers", __dir__), under: "controllers"
2
+
3
+ # Action Text editor (Trix). The field-editor modal renders <trix-editor>
4
+ # for any rich_text field, which needs both `trix` and `@rails/actiontext`
5
+ # loaded. Pin them here so hosts get them automatically — host pins win
6
+ # on conflict if a host wants a different version.
7
+ pin "trix"
8
+ pin "@rails/actiontext", to: "actiontext.esm.js"
data/config/routes.rb ADDED
@@ -0,0 +1,78 @@
1
+ Rails.application.routes.draw do
2
+ namespace :lean_cms, path: 'lean-cms' do
3
+ # Authentication
4
+ get 'login', to: 'sessions#new', as: :new_session
5
+ post 'login', to: 'sessions#create', as: :session
6
+ delete 'login', to: 'sessions#destroy'
7
+
8
+ # Password reset (email-driven, sets a signed token on the user)
9
+ get 'reset-password', to: 'passwords#new', as: :new_password
10
+ post 'reset-password', to: 'passwords#create', as: :passwords
11
+ get 'reset-password/:token/edit', to: 'passwords#edit', as: :edit_password
12
+ patch 'reset-password/:token', to: 'passwords#update', as: :password
13
+ put 'reset-password/:token', to: 'passwords#update'
14
+
15
+ # Magic-link password setup (new user invitations + admin-triggered resets)
16
+ get 'setup-password/:token', to: 'password_setup#show', as: :password_setup
17
+ patch 'setup-password/:token', to: 'password_setup#update'
18
+
19
+ root to: 'dashboard#index'
20
+
21
+ # User Management
22
+ resources :users, except: [:destroy] do
23
+ member do
24
+ patch :deactivate
25
+ patch :activate
26
+ post :send_password_reset
27
+ end
28
+ end
29
+
30
+ resources :posts
31
+
32
+ # Page Contents - section-based editing
33
+ get 'page-contents', to: 'page_contents#index', as: :page_contents
34
+
35
+ # Page Contents - inline field editing (MUST come before section routes)
36
+ get 'page-contents/field/:id/edit', to: 'page_contents#edit_field', as: :edit_page_content_field
37
+ patch 'page-contents/field/:id', to: 'page_contents#update_field', as: :update_page_content_field
38
+ get 'page-contents/field/:id/undo/preview', to: 'page_contents#preview_undo_field', as: :preview_undo_page_content_field
39
+ post 'page-contents/field/:id/undo', to: 'page_contents#undo_field', as: :undo_page_content_field
40
+
41
+ # Page Contents - section routes
42
+ get 'page-contents/:page/:section/edit', to: 'page_contents#edit', as: :edit_page_content
43
+ patch 'page-contents/:page/:section', to: 'page_contents#update', as: :page_content
44
+
45
+ resources :form_submissions, only: [:index, :show, :destroy] do
46
+ member do
47
+ patch :mark_as_read
48
+ patch :mark_as_replied
49
+ end
50
+ end
51
+
52
+ # Activity Log
53
+ get 'activity', to: 'activity#index', as: :activity
54
+
55
+ # Settings
56
+ get 'settings', to: 'settings#edit', as: :settings
57
+ patch 'settings', to: 'settings#update'
58
+ patch 'settings/update_override', to: 'settings#update_override'
59
+ post 'settings/lock', to: 'settings#lock', as: :lock_content
60
+ post 'settings/unlock', to: 'settings#unlock', as: :unlock_content
61
+
62
+ # Notification Settings
63
+ resource :notification_settings, only: [:edit, :update] do
64
+ post :test_email, on: :collection
65
+ post :test_sms, on: :collection
66
+ end
67
+
68
+ # In-app Notifications
69
+ resources :notifications, only: [:index, :show] do
70
+ member do
71
+ patch :mark_as_read
72
+ end
73
+ collection do
74
+ patch :mark_all_as_read
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,131 @@
1
+ class CreateLeanCmsTables < ActiveRecord::Migration[8.1]
2
+ def change
3
+ # Pages (must come before page_contents for FK)
4
+ create_table :lean_cms_pages, if_not_exists: true do |t|
5
+ t.string :slug, null: false
6
+ t.string :parent_slug
7
+ t.string :title, null: false
8
+ t.string :meta_title
9
+ t.text :meta_description
10
+ t.boolean :published, default: false, null: false
11
+ t.integer :position, default: 0
12
+ t.timestamps
13
+
14
+ t.index [:slug, :parent_slug], unique: true
15
+ end
16
+
17
+ # Posts
18
+ create_table :lean_cms_posts, if_not_exists: true do |t|
19
+ # author / last_edited_by reference the host's user table. We intentionally
20
+ # don't add a database-level FK here — the host owns its user table
21
+ # (default `users`, but configurable via LeanCms.user_class) and SQLite's
22
+ # FK validation on later table rebuilds breaks if the host's users table
23
+ # name doesn't match. Belongs-to in the model gives us app-level integrity.
24
+ t.references :author, null: false
25
+ t.references :last_edited_by
26
+ t.string :title, null: false
27
+ t.string :slug, null: false
28
+ t.text :excerpt
29
+ t.integer :content_type, default: 0, null: false
30
+ t.integer :status, default: 0, null: false
31
+ t.datetime :published_at
32
+ t.timestamps
33
+
34
+ t.index :slug, unique: true
35
+ t.index [:status, :published_at]
36
+ t.index :content_type
37
+ end
38
+
39
+ # Page Contents
40
+ create_table :lean_cms_page_contents, if_not_exists: true do |t|
41
+ t.references :last_edited_by, null: false
42
+ t.integer :page_id
43
+ # FK constraint added at the bottom of this migration after both tables
44
+ # exist, so we can use add_foreign_key with an explicit column.
45
+ t.string :page, null: false
46
+ t.string :section, null: false
47
+ t.string :key, null: false
48
+ t.string :label
49
+ t.text :content
50
+ t.text :value
51
+ t.integer :content_type, default: 0
52
+ t.json :options
53
+ t.integer :position, default: 0
54
+ t.string :display_title
55
+ t.string :page_display_title
56
+ t.integer :page_order, default: 0
57
+ t.integer :section_order, default: 0
58
+ t.timestamps
59
+
60
+ t.index [:page, :section, :key], unique: true, name: "index_page_contents_on_page_section_key"
61
+ t.index [:page, :section], name: "index_page_contents_on_page_and_section"
62
+ t.index [:page_order, :section_order, :position]
63
+ t.index :page_id
64
+ end
65
+
66
+ # Settings (key-value store)
67
+ create_table :lean_cms_settings, if_not_exists: true do |t|
68
+ t.string :key, null: false
69
+ t.text :value
70
+ t.timestamps
71
+
72
+ t.index :key, unique: true
73
+ end
74
+
75
+ # Notification Settings
76
+ create_table :lean_cms_notification_settings, if_not_exists: true do |t|
77
+ t.string :email_provider
78
+ t.text :sendgrid_api_key
79
+ t.text :mailgun_api_key
80
+ t.string :mailgun_domain
81
+ t.text :twilio_account_sid
82
+ t.text :twilio_auth_token
83
+ t.string :twilio_from_number
84
+ t.text :notification_emails
85
+ t.text :notification_phones
86
+ t.boolean :email_enabled, default: false
87
+ t.boolean :sms_enabled, default: false
88
+ t.boolean :in_app_enabled, default: true
89
+ t.timestamps
90
+ end
91
+
92
+ # Meta Tags (polymorphic)
93
+ create_table :lean_cms_meta_tags, if_not_exists: true do |t|
94
+ t.references :taggable, polymorphic: true, null: false
95
+ t.string :title
96
+ t.text :description
97
+ t.string :og_image_url
98
+ t.string :canonical_url
99
+ t.json :structured_data
100
+ t.timestamps
101
+
102
+ t.index [:taggable_type, :taggable_id]
103
+ end
104
+
105
+ # Form Submissions
106
+ create_table :lean_cms_form_submissions, if_not_exists: true do |t|
107
+ t.string :form_type, null: false
108
+ t.string :name
109
+ t.string :email
110
+ t.string :phone
111
+ t.string :company_name
112
+ t.string :city
113
+ t.string :state
114
+ t.string :zip
115
+ t.text :message
116
+ t.json :additional_data
117
+ t.string :ip_address
118
+ t.string :user_agent
119
+ t.integer :status, default: 0, null: false
120
+ t.timestamps
121
+
122
+ t.index :form_type
123
+ t.index :status
124
+ t.index :created_at
125
+ end
126
+
127
+ unless foreign_key_exists?(:lean_cms_page_contents, :lean_cms_pages, column: :page_id)
128
+ add_foreign_key :lean_cms_page_contents, :lean_cms_pages, column: :page_id
129
+ end
130
+ end
131
+ end