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,384 @@
1
+ <div class="max-w-4xl">
2
+ <div class="mb-8">
3
+ <h1 class="text-3xl font-bold text-gray-900">CMS Settings</h1>
4
+ <p class="text-gray-600 mt-2">Configure your Lean CMS preferences</p>
5
+ </div>
6
+
7
+ <!-- Content Sync -->
8
+ <div class="bg-white rounded-lg shadow-sm border <%= content_locked? ? 'border-amber-300' : 'border-gray-200' %> p-6 mb-6">
9
+ <div class="flex items-start justify-between">
10
+ <div>
11
+ <h2 class="text-xl font-semibold text-gray-900 mb-1">Content Sync</h2>
12
+ <p class="text-sm text-gray-600">Lock content editing while syncing the database between environments.</p>
13
+ </div>
14
+ <% if content_locked? %>
15
+ <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-semibold bg-amber-100 text-amber-800 border border-amber-300">
16
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
17
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
18
+ </svg>
19
+ Locked
20
+ </span>
21
+ <% else %>
22
+ <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-semibold bg-green-100 text-green-800 border border-green-300">
23
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
24
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"/>
25
+ </svg>
26
+ Unlocked
27
+ </span>
28
+ <% end %>
29
+ </div>
30
+
31
+ <% if content_locked? %>
32
+ <% lock_info = content_lock_info %>
33
+ <div class="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-800">
34
+ <div><span class="font-medium">Reason:</span> <%= lock_info[:reason] %></div>
35
+ <% if lock_info[:locked_at].present? %>
36
+ <div class="mt-1"><span class="font-medium">Locked:</span> <%= Time.parse(lock_info[:locked_at]).strftime('%b %-d at %-I:%M %p') %></div>
37
+ <% end %>
38
+ </div>
39
+ <div class="mt-4">
40
+ <%= button_to 'Unlock Content', lean_cms_unlock_content_path, method: :post,
41
+ class: "px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white font-semibold rounded-lg transition-colors" %>
42
+ </div>
43
+ <% else %>
44
+ <div class="mt-4">
45
+ <%= form_with url: lean_cms_lock_content_path, method: :post, class: "flex items-end gap-3" do |lf| %>
46
+ <div class="flex-1">
47
+ <%= lf.label :reason, "Reason (optional)", class: "block text-sm font-medium text-gray-700 mb-1" %>
48
+ <%= lf.text_field :reason, placeholder: "e.g. Syncing database for new feature",
49
+ class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500 outline-none" %>
50
+ </div>
51
+ <%= lf.submit "Lock Content", class: "px-4 py-2 bg-gray-800 hover:bg-gray-900 text-white font-semibold rounded-lg transition-colors cursor-pointer" %>
52
+ <% end %>
53
+ </div>
54
+ <div class="mt-4 pt-4 border-t border-gray-100">
55
+ <p class="text-xs text-gray-500 font-medium mb-2">Sync workflow from your terminal:</p>
56
+ <pre class="bg-gray-50 border border-gray-200 rounded p-3 text-xs text-gray-700 overflow-x-auto"><code>bin/rails lean_cms:sync:start # lock + pull prod DB
57
+ # make changes locally
58
+ bin/rails lean_cms:sync:finish # push DB + unlock</code></pre>
59
+ </div>
60
+ <% end %>
61
+ </div>
62
+
63
+ <%= form_with url: lean_cms_settings_path, method: :patch, multipart: true, class: "space-y-6" do |f| %>
64
+
65
+ <!-- Display Options -->
66
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
67
+ <h2 class="text-xl font-semibold text-gray-900 mb-4">Display Options</h2>
68
+
69
+ <div class="space-y-4" data-controller="settings-inline-edit-sync">
70
+ <!-- In-Context Editing Toggle -->
71
+ <div class="flex items-start">
72
+ <div class="flex items-center h-5">
73
+ <%= check_box_tag 'in_context_editing', '1', @in_context_editing_enabled,
74
+ class: "w-4 h-4 text-[#b82025] bg-gray-100 border-gray-300 rounded focus:ring-[#b82025] focus:ring-2",
75
+ data: {
76
+ settings_inline_edit_sync_target: "checkbox",
77
+ action: "change->settings-inline-edit-sync#toggle"
78
+ } %>
79
+ </div>
80
+ <div class="ml-3">
81
+ <label for="in_context_editing" class="font-medium text-gray-900">
82
+ Enable In-Context Editing
83
+ </label>
84
+ <p class="text-sm text-gray-600 mt-1">
85
+ Show edit overlays when hovering over sections on the frontend. This allows you to quickly jump to the edit page for any section directly from the website. Changes here sync with the toggle in the admin bar.
86
+ </p>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Show Blog Toggle -->
91
+ <div class="flex items-start">
92
+ <div class="flex items-center h-5">
93
+ <%= check_box_tag 'show_blog', '1', @show_blog_enabled,
94
+ class: "w-4 h-4 text-[#b82025] bg-gray-100 border-gray-300 rounded focus:ring-[#b82025] focus:ring-2" %>
95
+ </div>
96
+ <div class="ml-3 flex-1">
97
+ <label for="show_blog" class="font-medium text-gray-900">
98
+ Show Blog
99
+ </label>
100
+ <p class="text-sm text-gray-600 mt-1">
101
+ Display the Blog link in navigation.
102
+ <span class="font-semibold"><%= @blog_count %> published <%= 'post'.pluralize(@blog_count) %></span>
103
+ <%= link_to "Manage", lean_cms_posts_path(content_type: 'blog'),
104
+ class: "text-[#b82025] hover:underline ml-2", target: "_blank" %>
105
+ </p>
106
+ <div class="ml-7 mt-3 space-y-3 pl-3 border-l-2 border-gray-200">
107
+ <%= label_tag :blog_title, "Page Title", class: "block text-sm font-medium text-gray-700 mb-1" %>
108
+ <%= text_field_tag :blog_title, @blog_title,
109
+ placeholder: "Our Blog",
110
+ 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" %>
111
+ <%= label_tag :blog_subtitle, "Subtitle", class: "block text-sm font-medium text-gray-700 mb-1 mt-2" %>
112
+ <%= text_field_tag :blog_subtitle, @blog_subtitle,
113
+ placeholder: "Insights, updates, and stories from Custom Assembly Services",
114
+ 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" %>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Show Portfolio Toggle -->
120
+ <div class="flex items-start">
121
+ <div class="flex items-center h-5">
122
+ <%= check_box_tag 'show_portfolio', '1', @show_portfolio_enabled,
123
+ class: "w-4 h-4 text-[#b82025] bg-gray-100 border-gray-300 rounded focus:ring-[#b82025] focus:ring-2" %>
124
+ </div>
125
+ <div class="ml-3 flex-1">
126
+ <label for="show_portfolio" class="font-medium text-gray-900">
127
+ Show Portfolio
128
+ </label>
129
+ <p class="text-sm text-gray-600 mt-1">
130
+ Display the Portfolio link in navigation.
131
+ <span class="font-semibold"><%= @portfolio_count %> published <%= 'item'.pluralize(@portfolio_count) %></span>
132
+ <%= link_to "Manage", lean_cms_posts_path(content_type: 'portfolio'),
133
+ class: "text-[#b82025] hover:underline ml-2", target: "_blank" %>
134
+ </p>
135
+ <div class="ml-7 mt-3 space-y-3 pl-3 border-l-2 border-gray-200">
136
+ <%= label_tag :portfolio_title, "Page Title", class: "block text-sm font-medium text-gray-700 mb-1" %>
137
+ <%= text_field_tag :portfolio_title, @portfolio_title,
138
+ placeholder: "Our Portfolio",
139
+ 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" %>
140
+ <%= label_tag :portfolio_subtitle, "Subtitle", class: "block text-sm font-medium text-gray-700 mb-1 mt-2" %>
141
+ <%= text_field_tag :portfolio_subtitle, @portfolio_subtitle,
142
+ placeholder: "Showcasing our industrial assembly and installation projects",
143
+ 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" %>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Branding -->
151
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
152
+ <h2 class="text-xl font-semibold text-gray-900 mb-4">Branding</h2>
153
+ <p class="text-sm text-gray-600 mb-4">The favicon shown in browser tabs for the public site. Leave blank to use the Lean CMS sloth as the default. Recommended: a 32×32 or 64×64 PNG (or .ico).</p>
154
+
155
+ <div class="flex items-center gap-4">
156
+ <% if (current_favicon = LeanCms::Setting.site_favicon_url) %>
157
+ <img src="<%= current_favicon %>" alt="Current favicon" class="w-12 h-12 border border-gray-200 rounded">
158
+ <div class="flex-1">
159
+ <%= label_tag :site_favicon, "Replace favicon", class: "block text-sm font-medium text-gray-700 mb-1" %>
160
+ <%= file_field_tag :site_favicon, accept: "image/png,image/x-icon,image/svg+xml", class: "block text-sm text-gray-700" %>
161
+ </div>
162
+ <label class="flex items-center gap-2 text-sm text-gray-700">
163
+ <%= check_box_tag :remove_site_favicon, "1", false, class: "w-4 h-4 rounded border-gray-300" %>
164
+ Remove
165
+ </label>
166
+ <% else %>
167
+ <img src="<%= asset_path("lean_cms/sloth-favicon-32.png") %>" alt="Default favicon" class="w-12 h-12 border border-gray-200 rounded">
168
+ <div class="flex-1">
169
+ <%= label_tag :site_favicon, "Upload custom favicon", class: "block text-sm font-medium text-gray-700 mb-1" %>
170
+ <%= file_field_tag :site_favicon, accept: "image/png,image/x-icon,image/svg+xml", class: "block text-sm text-gray-700" %>
171
+ <p class="text-xs text-gray-500 mt-1">Currently using the Lean CMS sloth.</p>
172
+ </div>
173
+ <% end %>
174
+ </div>
175
+ </div>
176
+
177
+ <!-- Site Information -->
178
+ <div id="site-info" class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 scroll-mt-6">
179
+ <h2 class="text-xl font-semibold text-gray-900 mb-4">Site Information</h2>
180
+ <p class="text-sm text-gray-600 mb-4">This information is used across the site including the contact page, footer, and schema markup.</p>
181
+
182
+ <% address = LeanCms::Setting.site_address_data %>
183
+ <div class="space-y-4">
184
+ <!-- Address Fields -->
185
+ <div>
186
+ <%= label_tag :site_street1, "Street Address", class: "block text-sm font-medium text-gray-700 mb-1" %>
187
+ <%= text_field_tag :site_street1, address['street1'],
188
+ placeholder: "1234 Industrial Parkway",
189
+ 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" %>
190
+ </div>
191
+
192
+ <div>
193
+ <%= label_tag :site_street2, "Street Address 2", class: "block text-sm font-medium text-gray-700 mb-1" %>
194
+ <%= text_field_tag :site_street2, address['street2'],
195
+ placeholder: "Suite 100 (optional)",
196
+ 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" %>
197
+ </div>
198
+
199
+ <div class="grid grid-cols-6 gap-4">
200
+ <div class="col-span-3">
201
+ <%= label_tag :site_city, "City", class: "block text-sm font-medium text-gray-700 mb-1" %>
202
+ <%= text_field_tag :site_city, address['city'],
203
+ placeholder: "Green Bay",
204
+ 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" %>
205
+ </div>
206
+
207
+ <div class="col-span-2">
208
+ <%= label_tag :site_state, "State", class: "block text-sm font-medium text-gray-700 mb-1" %>
209
+ <%= text_field_tag :site_state, address['state'],
210
+ placeholder: "WI",
211
+ maxlength: 2,
212
+ 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 uppercase" %>
213
+ </div>
214
+
215
+ <div class="col-span-1">
216
+ <%= label_tag :site_zip, "ZIP", class: "block text-sm font-medium text-gray-700 mb-1" %>
217
+ <%= text_field_tag :site_zip, address['zip'],
218
+ placeholder: "54304",
219
+ 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" %>
220
+ </div>
221
+ </div>
222
+
223
+ <!-- Phone and Email -->
224
+ <div class="grid grid-cols-2 gap-4">
225
+ <div>
226
+ <%= label_tag :site_phone, "Phone", class: "block text-sm font-medium text-gray-700 mb-1" %>
227
+ <%= text_field_tag :site_phone, LeanCms::Setting.site_phone,
228
+ placeholder: "(920) 555-0100",
229
+ 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" %>
230
+ </div>
231
+
232
+ <div>
233
+ <%= label_tag :site_email, "Email", class: "block text-sm font-medium text-gray-700 mb-1" %>
234
+ <%= email_field_tag :site_email, LeanCms::Setting.site_email,
235
+ placeholder: "info@example.com",
236
+ 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" %>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+
242
+ <!-- Business Hours -->
243
+ <div id="business-hours" class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 scroll-mt-6"
244
+ data-controller="dynamic-list"
245
+ data-dynamic-list-max-value="10">
246
+ <h2 class="text-xl font-semibold text-gray-900 mb-4">Business Hours</h2>
247
+ <p class="text-sm text-gray-600 mb-4">Add your business hours. Each row can have its own day label (e.g., "Monday - Thursday", "Friday", etc.).</p>
248
+
249
+ <div data-dynamic-list-target="list" class="space-y-3">
250
+ <% hours = LeanCms::Setting.business_hours['hours'] %>
251
+ <% hours = [{ 'label' => '', 'value' => '' }] if hours.empty? %>
252
+ <% hours.each do |hour| %>
253
+ <div data-list-item class="flex gap-3 items-center">
254
+ <%= text_field_tag "business_hours_labels[]", hour['label'],
255
+ placeholder: "e.g., Monday - Friday",
256
+ class: "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#b82025]/20 focus:border-[#b82025] outline-none" %>
257
+ <%= text_field_tag "business_hours_values[]", hour['value'],
258
+ placeholder: "e.g., 8:00 AM - 5:00 PM",
259
+ class: "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#b82025]/20 focus:border-[#b82025] outline-none" %>
260
+ <button type="button" data-action="dynamic-list#remove"
261
+ class="p-2 text-gray-400 hover:text-red-500 transition-colors">
262
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
263
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
264
+ </svg>
265
+ </button>
266
+ </div>
267
+ <% end %>
268
+ </div>
269
+
270
+ <button type="button" data-action="dynamic-list#add"
271
+ class="mt-3 inline-flex items-center text-sm font-medium text-[#b82025] hover:text-[#a01c20] transition-colors">
272
+ <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
273
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
274
+ </svg>
275
+ Add Hours Row
276
+ </button>
277
+
278
+ <template data-dynamic-list-target="template">
279
+ <div data-list-item class="flex gap-3 items-center">
280
+ <%= text_field_tag "business_hours_labels[]", "",
281
+ placeholder: "e.g., Monday - Friday",
282
+ class: "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#b82025]/20 focus:border-[#b82025] outline-none" %>
283
+ <%= text_field_tag "business_hours_values[]", "",
284
+ placeholder: "e.g., 8:00 AM - 5:00 PM",
285
+ class: "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#b82025]/20 focus:border-[#b82025] outline-none" %>
286
+ <button type="button" data-action="dynamic-list#remove"
287
+ class="p-2 text-gray-400 hover:text-red-500 transition-colors">
288
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
289
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
290
+ </svg>
291
+ </button>
292
+ </div>
293
+ </template>
294
+
295
+ <div class="mt-4 pt-4 border-t border-gray-200">
296
+ <%= label_tag :business_hours_note, "Additional Note", class: "block text-sm font-medium text-gray-700 mb-1" %>
297
+ <%= text_area_tag :business_hours_note, LeanCms::Setting.business_hours['note'],
298
+ rows: 2,
299
+ placeholder: "e.g., Emergency services available 24/7 for existing clients.",
300
+ 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 resize-none" %>
301
+ </div>
302
+ </div>
303
+
304
+ <!-- Privacy & Compliance -->
305
+ <div id="privacy-compliance" class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 scroll-mt-6">
306
+ <h2 class="text-xl font-semibold text-gray-900 mb-4">Privacy & Compliance</h2>
307
+
308
+ <div class="space-y-4">
309
+ <!-- Cookie Consent Toggle -->
310
+ <div class="flex items-start">
311
+ <div class="flex items-center h-5">
312
+ <%= hidden_field_tag 'cookie_consent_enabled', '1' if @cookie_consent_forced_on %>
313
+ <%= check_box_tag 'cookie_consent_enabled', '1', @cookie_consent_enabled,
314
+ disabled: @cookie_consent_forced_on,
315
+ class: "w-4 h-4 text-[#b82025] bg-gray-100 border-gray-300 rounded focus:ring-[#b82025] focus:ring-2 #{'opacity-60 cursor-not-allowed' if @cookie_consent_forced_on}" %>
316
+ </div>
317
+ <div class="ml-3 flex-1">
318
+ <label for="cookie_consent_enabled" class="font-medium text-gray-900">
319
+ Enable Cookie Consent Banner
320
+ </label>
321
+ <p class="text-sm text-gray-600 mt-1">
322
+ Show a GDPR/CCPA cookie consent banner at the bottom of the site. Visitors can accept all, reject optional cookies, or manage preferences.
323
+ </p>
324
+ <% if @cookie_consent_forced_on %>
325
+ <p class="text-sm text-amber-700 mt-2 font-medium">
326
+ Cookie consent cannot be disabled while Google Analytics is enabled.
327
+ </p>
328
+ <% end %>
329
+ <div class="ml-7 mt-3 space-y-3 pl-3 border-l-2 border-gray-200">
330
+ <%= label_tag :cookie_consent_message, "Banner Message", class: "block text-sm font-medium text-gray-700 mb-1" %>
331
+ <%= text_area_tag :cookie_consent_message, @cookie_consent_message,
332
+ rows: 3,
333
+ placeholder: "We use cookies to improve your experience and analyze site traffic. You can choose which cookies to allow.",
334
+ 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 resize-none" %>
335
+ </div>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </div>
340
+
341
+ <!-- Analytics -->
342
+ <div id="analytics" class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 scroll-mt-6" data-controller="toggle-fields">
343
+ <h2 class="text-xl font-semibold text-gray-900 mb-4">Analytics</h2>
344
+
345
+ <div class="flex items-start">
346
+ <div class="flex items-center h-5">
347
+ <%= check_box_tag 'google_analytics_enabled', '1', @google_analytics_enabled,
348
+ class: "w-4 h-4 text-[#b82025] bg-gray-100 border-gray-300 rounded focus:ring-[#b82025] focus:ring-2",
349
+ data: { action: "change->toggle-fields#toggle" } %>
350
+ </div>
351
+ <div class="ml-3 flex-1">
352
+ <label for="google_analytics_enabled" class="font-medium text-gray-900">
353
+ Enable Google Analytics 4
354
+ </label>
355
+ <p class="text-sm text-gray-600 mt-1">
356
+ Track page views and events with Google Analytics 4. Enabling GA4 will automatically enable the Cookie Consent banner and it cannot be disabled while GA is active.
357
+ </p>
358
+ <div data-toggle-fields-target="fields" class="<%= 'hidden' unless @google_analytics_enabled %> mt-4 pt-4 border-t border-gray-200 space-y-3">
359
+ <%= label_tag :google_analytics_id, "Measurement ID", class: "block text-sm font-medium text-gray-700 mb-1" %>
360
+ <%= text_field_tag :google_analytics_id, @google_analytics_id,
361
+ placeholder: "G-XXXXXXXXXX",
362
+ pattern: "G-[A-Z0-9]+",
363
+ title: "Must start with G- followed by alphanumeric characters",
364
+ 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 font-mono" %>
365
+ <p class="text-xs text-gray-500">
366
+ Find this in your GA4 property under Admin → Data Streams → your stream. Must start with <code class="bg-gray-100 px-1 rounded">G-</code>.
367
+ </p>
368
+ </div>
369
+ </div>
370
+ </div>
371
+ </div>
372
+
373
+ <!-- Save Button -->
374
+ <div class="flex items-center justify-between pt-4">
375
+ <div class="text-sm text-gray-600">
376
+ Changes take effect immediately after saving
377
+ </div>
378
+ <div class="flex items-center gap-3">
379
+ <%= link_to "Cancel", lean_cms_root_path, class: "px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors" %>
380
+ <%= f.submit "Save Settings", class: "px-6 py-2 bg-[#b82025] hover:bg-[#a01c20] text-white font-semibold rounded-lg transition-colors cursor-pointer" %>
381
+ </div>
382
+ </div>
383
+ <% end %>
384
+ </div>
@@ -0,0 +1,85 @@
1
+ <% return unless respond_to?(:current_user) && current_user&.has_any_cms_permission? %>
2
+
3
+ <%# Trix base + Action Text overrides. Required so the field-editor modal's
4
+ rich_text toolbar renders as styled buttons rather than plain inline
5
+ text. Gated on the same has_any_cms_permission? check as the bar so
6
+ the stylesheet only loads for signed-in editors. %>
7
+ <%= stylesheet_link_tag "actiontext", "data-turbo-track": "reload" %>
8
+
9
+ <div class="fixed top-0 left-0 right-0 bg-gray-900 text-white py-2 px-6 text-sm z-[60]"
10
+ data-controller="inline-edit-toggle">
11
+ <div class="container mx-auto max-w-5xl flex items-center justify-between">
12
+ <div class="flex items-center gap-4">
13
+ <span class="text-gray-400">Logged in as:</span>
14
+ <span class="font-medium"><%= current_user.email_address %></span>
15
+ </div>
16
+ <div class="flex items-center gap-3">
17
+ <!-- Inline Editing Toggle -->
18
+ <div class="flex items-center gap-2 px-3 py-1.5 bg-gray-800 rounded">
19
+ <label for="inline-edit-toggle" class="flex items-center gap-2 cursor-pointer">
20
+ <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
21
+ <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"/>
22
+ </svg>
23
+ <span class="text-gray-300">Inline Editing</span>
24
+ </label>
25
+ <label for="inline-edit-toggle" class="relative cursor-pointer">
26
+ <input type="checkbox"
27
+ id="inline-edit-toggle"
28
+ data-inline-edit-toggle-target="checkbox"
29
+ data-action="change->inline-edit-toggle#toggle"
30
+ class="sr-only peer">
31
+ <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-600"></div>
32
+ </label>
33
+ </div>
34
+
35
+ <%= link_to LeanCms.docs_url, target: "_blank", rel: "noopener",
36
+ title: "Documentation",
37
+ class: "flex items-center gap-2 px-3 py-1.5 bg-gray-800 hover:bg-gray-700 rounded transition-colors" do %>
38
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
39
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093V14m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
40
+ </svg>
41
+ <span>Help</span>
42
+ <% end %>
43
+
44
+ <%= link_to lean_cms_root_path,
45
+ style: "background-color: #{LeanCms.primary_color};",
46
+ class: "flex items-center gap-2 px-4 py-1.5 rounded transition-colors hover:brightness-95" do %>
47
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
48
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
49
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
50
+ </svg>
51
+ Admin Dashboard
52
+ <% end %>
53
+ <%= button_to lean_cms_login_path, method: :delete,
54
+ class: "cursor-pointer flex items-center gap-2 px-4 py-1.5 bg-gray-700 hover:bg-gray-600 rounded transition-colors" do %>
55
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
56
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
57
+ </svg>
58
+ Sign Out
59
+ <% end %>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <!-- CMS field-editor modal. The inline-edit pencil icons (rendered into
65
+ editable regions by the inline-edit Stimulus controller) dispatch
66
+ window.cms:open-field-editor, which this modal controller listens for
67
+ and fetches the editor form via XHR into `content`. Without this DOM
68
+ present, clicks on the pencils fire the event but no controller is
69
+ attached anywhere to handle it. -->
70
+ <div data-controller="field-editor-modal"
71
+ data-field-editor-modal-target="modal"
72
+ class="cms-modal hidden fixed inset-0 z-[10000] flex items-center justify-center">
73
+ <!-- Backdrop. Closing on backdrop-click is wired here directly so it
74
+ fires even though the panel below intercepts clicks within it. -->
75
+ <div class="absolute inset-0 bg-black opacity-50"
76
+ data-action="click->field-editor-modal#close"></div>
77
+
78
+ <div class="relative bg-white rounded-lg shadow-2xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
79
+ <div data-field-editor-modal-target="loader" class="p-12 text-center">
80
+ <div class="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
81
+ <p class="mt-4 text-gray-600">Loading editor...</p>
82
+ </div>
83
+ <div data-field-editor-modal-target="content" class="p-8"></div>
84
+ </div>
85
+ </div>
@@ -0,0 +1,86 @@
1
+ <header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6">
2
+ <div class="flex items-center">
3
+ <h1 class="text-lg font-semibold text-gray-900">
4
+ <%= content_for?(:page_title) ? yield(:page_title) : 'Dashboard' %>
5
+ </h1>
6
+ </div>
7
+
8
+ <div class="flex items-center space-x-4">
9
+ <% if current_user&.can_edit_blog? %>
10
+ <!-- Quick Actions -->
11
+ <%= link_to new_lean_cms_post_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 cursor-pointer", style: "background-color: var(--cms-primary);" do %>
12
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
13
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
14
+ </svg>
15
+ New Post
16
+ <% end %>
17
+ <% end %>
18
+
19
+ <!-- Help -->
20
+ <%= link_to LeanCms.docs_url,
21
+ target: "_blank", rel: "noopener",
22
+ title: "Lean CMS documentation",
23
+ class: "p-2 text-gray-600 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-[var(--cms-primary)] focus:ring-offset-2 rounded-lg transition-colors" do %>
24
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
25
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093V14m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
26
+ </svg>
27
+ <span class="sr-only">Help</span>
28
+ <% end %>
29
+
30
+ <!-- Notifications Bell -->
31
+ <%= render 'lean_cms/shared/notifications_bell' if LeanCms::NotificationSetting.instance.in_app_enabled? %>
32
+
33
+ <!-- User Menu -->
34
+ <div class="relative" x-data="{ open: false }">
35
+ <button @click="open = !open" class="flex items-center space-x-3 text-sm focus:outline-none cursor-pointer">
36
+ <div class="flex-shrink-0">
37
+ <div class="h-8 w-8 rounded-full flex items-center justify-center text-white text-sm font-medium" style="background-color: var(--cms-primary);">
38
+ <%= current_user.email_address[0].upcase %>
39
+ </div>
40
+ </div>
41
+ <div class="hidden md:block text-left">
42
+ <p class="text-sm font-medium text-gray-900">
43
+ <%= current_user.respond_to?(:display_name) ? current_user.display_name : current_user.email_address.split("@").first %>
44
+ </p>
45
+ <p class="text-xs text-gray-500">
46
+ <%= current_user.respond_to?(:permissions_summary) ? current_user.permissions_summary : current_user.email_address %>
47
+ </p>
48
+ </div>
49
+ <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
50
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
51
+ </svg>
52
+ </button>
53
+
54
+ <!-- Dropdown Menu -->
55
+ <div x-show="open" @click.away="open = false" x-cloak class="absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
56
+ <%= link_to root_path, target: "_blank", class: "flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" do %>
57
+ <svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
58
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
59
+ </svg>
60
+ View Site
61
+ <% end %>
62
+ <% if current_user.can_access_settings? %>
63
+ <div class="border-t border-gray-200 my-1"></div>
64
+ <%= link_to lean_cms_settings_path, class: "flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" do %>
65
+ <svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
66
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
67
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
68
+ </svg>
69
+ Settings
70
+ <% end %>
71
+ <% end %>
72
+ <div class="border-t border-gray-200 my-1"></div>
73
+ <%= button_to lean_cms_session_path, method: :delete, class: "w-full flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 text-left cursor-pointer" do %>
74
+ <svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
75
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
76
+ </svg>
77
+ Sign Out
78
+ <% end %>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </header>
83
+
84
+ <style>
85
+ [x-cloak] { display: none !important; }
86
+ </style>
@@ -0,0 +1,84 @@
1
+ <%
2
+ # Host User models don't have to ship a `notifications` association — the
3
+ # bell hides itself when they don't, which lets hosts that don't want
4
+ # Noticed integration opt out by just leaving the association off.
5
+ return unless current_user.respond_to?(:notifications)
6
+ unread_count = current_user.notifications.unread.count
7
+ recent_notifications = current_user.notifications.unread.limit(5).order(created_at: :desc)
8
+ %>
9
+ <div class="relative" x-data="{ open: false }" data-controller="notifications">
10
+ <button @click="open = !open" class="relative p-2 text-gray-600 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-[#b82025] focus:ring-offset-2 rounded-lg transition-colors">
11
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12
+ <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"/>
13
+ </svg>
14
+ <% if unread_count > 0 %>
15
+ <span class="absolute top-0 right-0 block h-4 w-4 rounded-full bg-red-500 text-white text-xs flex items-center justify-center font-bold" style="font-size: 10px;">
16
+ <%= unread_count > 9 ? '9+' : unread_count %>
17
+ </span>
18
+ <% end %>
19
+ </button>
20
+
21
+ <!-- Dropdown -->
22
+ <div x-show="open"
23
+ @click.away="open = false"
24
+ x-cloak
25
+ class="absolute right-0 mt-2 w-80 bg-white rounded-lg shadow-lg border border-gray-200 z-50 max-h-96 overflow-hidden flex flex-col">
26
+ <div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
27
+ <h3 class="text-sm font-semibold text-gray-900">Notifications</h3>
28
+ <% if unread_count > 0 %>
29
+ <%= button_to mark_all_as_read_lean_cms_notifications_path,
30
+ method: :patch,
31
+ class: "text-xs text-[#b82025] hover:text-[#a01c20] font-medium",
32
+ data: { turbo_frame: "_top" } do %>
33
+ Mark all as read
34
+ <% end %>
35
+ <% end %>
36
+ </div>
37
+
38
+ <div class="overflow-y-auto flex-1">
39
+ <% if recent_notifications.any? %>
40
+ <div class="divide-y divide-gray-200">
41
+ <% recent_notifications.each do |notification| %>
42
+ <%= link_to lean_cms_notification_path(notification),
43
+ class: "block px-4 py-3 hover:bg-gray-50 transition-colors",
44
+ data: { turbo_frame: "_top" } do %>
45
+ <div class="flex items-start">
46
+ <div class="flex-shrink-0">
47
+ <div class="h-2 w-2 rounded-full bg-[#b82025] mt-2"></div>
48
+ </div>
49
+ <div class="ml-3 flex-1 min-w-0">
50
+ <p class="text-sm font-medium text-gray-900 truncate">
51
+ <%= notification.params[:title] || 'Contact Form Submission' %>
52
+ </p>
53
+ <p class="text-xs text-gray-500 mt-1">
54
+ <%= notification.params[:message] || notification.message %>
55
+ </p>
56
+ <p class="text-xs text-gray-400 mt-1">
57
+ <%= time_ago_in_words(notification.created_at) %> ago
58
+ </p>
59
+ </div>
60
+ </div>
61
+ <% end %>
62
+ <% end %>
63
+ </div>
64
+ <% else %>
65
+ <div class="px-4 py-8 text-center">
66
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
67
+ <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"/>
68
+ </svg>
69
+ <p class="mt-2 text-sm text-gray-500">No new notifications</p>
70
+ </div>
71
+ <% end %>
72
+ </div>
73
+
74
+ <% if unread_count > 5 %>
75
+ <div class="px-4 py-3 border-t border-gray-200 bg-gray-50">
76
+ <%= link_to lean_cms_notifications_path,
77
+ class: "block text-center text-sm font-medium text-[#b82025] hover:text-[#a01c20]",
78
+ data: { turbo_frame: "_top" } do %>
79
+ View all notifications
80
+ <% end %>
81
+ </div>
82
+ <% end %>
83
+ </div>
84
+ </div>