sincerely 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/app/assets/config/sincerely.js +2 -0
  4. data/app/assets/images/sincerely/arrow_left.svg +1 -0
  5. data/app/assets/images/sincerely/check.svg +3 -0
  6. data/app/assets/images/sincerely/chevron_left.svg +1 -0
  7. data/app/assets/images/sincerely/clock.svg +1 -0
  8. data/app/assets/images/sincerely/code.svg +1 -0
  9. data/app/assets/images/sincerely/eye.svg +1 -0
  10. data/app/assets/images/sincerely/logout.svg +1 -0
  11. data/app/assets/images/sincerely/menu.svg +1 -0
  12. data/app/assets/images/sincerely/moon.svg +1 -0
  13. data/app/assets/images/sincerely/pencil.svg +4 -0
  14. data/app/assets/images/sincerely/plus.svg +3 -0
  15. data/app/assets/images/sincerely/progress_spinner.svg +3 -0
  16. data/app/assets/images/sincerely/spinner.svg +3 -0
  17. data/app/assets/images/sincerely/sun.svg +1 -0
  18. data/app/assets/images/sincerely/warning_circle.svg +4 -0
  19. data/app/assets/javascripts/sincerely/charts.js +115 -0
  20. data/app/assets/javascripts/sincerely/clickable_rows.js +29 -0
  21. data/app/assets/javascripts/sincerely/layout.js +104 -0
  22. data/app/assets/javascripts/sincerely/send_form.js +243 -0
  23. data/app/assets/javascripts/sincerely/template_form.js +44 -0
  24. data/app/assets/javascripts/sincerely/template_preview.js +12 -0
  25. data/app/assets/javascripts/sincerely/theme_init.js +9 -0
  26. data/app/assets/stylesheets/sincerely/charts.css +250 -0
  27. data/app/assets/stylesheets/sincerely/components.css +632 -0
  28. data/app/assets/stylesheets/sincerely/layout.css +233 -0
  29. data/app/assets/stylesheets/sincerely/navigation.css +198 -0
  30. data/app/assets/stylesheets/sincerely/send_form.css +287 -0
  31. data/app/assets/stylesheets/sincerely/template_form.css +74 -0
  32. data/app/assets/stylesheets/sincerely/template_preview.css +75 -0
  33. data/app/assets/stylesheets/sincerely/variables.css +88 -0
  34. data/app/controllers/sincerely/application_controller.rb +3 -72
  35. data/app/controllers/sincerely/concerns/authorization.rb +21 -0
  36. data/app/controllers/sincerely/concerns/delivery_metrics.rb +51 -0
  37. data/app/controllers/sincerely/concerns/event_filtering.rb +58 -0
  38. data/app/controllers/sincerely/concerns/event_timeline.rb +41 -0
  39. data/app/controllers/sincerely/concerns/notification_timeline.rb +73 -0
  40. data/app/controllers/sincerely/concerns/notifications.rb +47 -0
  41. data/app/controllers/sincerely/concerns/pagination.rb +39 -0
  42. data/app/controllers/sincerely/concerns/periods.rb +40 -0
  43. data/app/controllers/sincerely/dashboard_controller.rb +30 -88
  44. data/app/controllers/sincerely/delivery_events_controller.rb +13 -21
  45. data/app/controllers/sincerely/engagement_events_controller.rb +13 -22
  46. data/app/controllers/sincerely/notifications_controller.rb +68 -49
  47. data/app/controllers/sincerely/send_controller.rb +82 -67
  48. data/app/controllers/sincerely/templates_controller.rb +37 -8
  49. data/app/helpers/sincerely/application_helper.rb +22 -0
  50. data/app/helpers/sincerely/chart_helper.rb +79 -0
  51. data/app/views/layouts/sincerely/application.html.erb +24 -528
  52. data/app/views/sincerely/dashboard/index.html.erb +25 -256
  53. data/app/views/sincerely/delivery_events/index.html.erb +6 -38
  54. data/app/views/sincerely/engagement_events/index.html.erb +6 -38
  55. data/app/views/sincerely/notifications/index.html.erb +7 -23
  56. data/app/views/sincerely/notifications/show.html.erb +4 -4
  57. data/app/views/sincerely/send/new.html.erb +8 -522
  58. data/app/views/sincerely/shared/_event.html.erb +21 -0
  59. data/app/views/sincerely/shared/_line_chart.html.erb +38 -0
  60. data/app/views/sincerely/shared/_notification_type_filter.html.erb +9 -0
  61. data/app/views/sincerely/shared/_status_option.html.erb +1 -0
  62. data/app/views/sincerely/shared/_template_option.html.erb +3 -0
  63. data/app/views/sincerely/templates/_form.html.erb +8 -107
  64. data/app/views/sincerely/templates/edit.html.erb +2 -2
  65. data/app/views/sincerely/templates/index.html.erb +7 -15
  66. data/app/views/sincerely/templates/new.html.erb +2 -2
  67. data/app/views/sincerely/templates/preview.html.erb +7 -26
  68. data/app/views/sincerely/templates/show.html.erb +3 -3
  69. data/lib/config/sincerely_config.rb +2 -2
  70. data/lib/sincerely/delivery_systems/email_aws_ses.rb +2 -2
  71. data/lib/sincerely/engine.rb +4 -0
  72. data/lib/sincerely/mixins/notification_model.rb +5 -1
  73. data/lib/sincerely/services/process_delivery_event.rb +9 -7
  74. data/lib/sincerely/templates/notification_template.rb +1 -1
  75. data/lib/sincerely/version.rb +1 -1
  76. metadata +52 -4
  77. data/sincerely.gemspec +0 -37
@@ -1,17 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <script>
5
- (function() {
6
- var theme = localStorage.getItem('sincerely-theme');
7
- if (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches) {
8
- theme = 'dark';
9
- }
10
- if (theme) {
11
- document.documentElement.setAttribute('data-theme', theme);
12
- }
13
- })();
14
- </script>
4
+ <%= javascript_include_tag 'sincerely/theme_init' %>
15
5
  <meta charset="UTF-8">
16
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
17
7
  <title>Sincerely Dashboard</title>
@@ -19,420 +9,29 @@
19
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
20
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21
11
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Jersey+10&display=swap" rel="stylesheet">
22
- <style>
23
- :root {
24
- --primary: #D4183D;
25
- --primary-dark: #70041E;
26
- --success: #8ACB88;
27
- --warning: #78c6cc;
28
- --danger: #D4183D;
29
- --gray-50: #f9fafb;
30
- --gray-100: #f3f4f6;
31
- --gray-200: #e5e7eb;
32
- --gray-300: #d1d5db;
33
- --gray-500: #6b7280;
34
- --gray-700: #374151;
35
- --gray-900: #111827;
36
- --bg-primary: #ffffff;
37
- --bg-secondary: #E6E6E6;
38
- --text-primary: #70041E;
39
- --text-secondary: #717182;
40
- --border-color: #e5e7eb;
41
- }
42
-
43
- [data-theme="dark"] {
44
- --primary: #D4183D;
45
- --primary-dark: #490C13;
46
- --gray-50: #1f2937;
47
- --gray-100: #374151;
48
- --gray-200: #4b5563;
49
- --gray-300: #6b7280;
50
- --gray-500: #9ca3af;
51
- --gray-700: #d1d5db;
52
- --gray-900: #f9fafb;
53
- --bg-primary: #1E1E1E;
54
- /* --bg-primary: #9393A1; */
55
- --bg-secondary: #303030;
56
- /* --bg-secondary: #1E1E1E; */
57
- --text-primary: #f9fafb;
58
- --text-secondary: #9ca3af;
59
- --border-color: #374151;
60
- }
61
-
62
- * { box-sizing: border-box; margin: 0; padding: 0; }
63
-
64
- html, body { height: 100%; }
65
-
66
- body {
67
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
68
- background: var(--bg-secondary);
69
- color: var(--text-primary);
70
- line-height: 1.5;
71
- transition: background-color 0.3s, color 0.3s;
72
- display: flex;
73
- flex-direction: column;
74
- overflow: hidden;
75
- }
76
-
77
- @media (max-width: 768px) {
78
- body { overflow: auto; }
79
- }
80
-
81
- a { color: var(--primary); }
82
- .container { max-width: 1280px; margin: 0 auto; padding: 0 1rem; }
83
-
84
- /* Navigation */
85
- .nav { background: var(--bg-primary); border-bottom: 1px solid var(--border-color); padding: 1rem 0; transition: background-color 0.3s; z-index: 50; box-shadow: 0 1px 3px rgba(0,0,0,0.08); flex-shrink: 0; }
86
- .nav-inner { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; position: relative; }
87
- .nav-brand { font-family: 'Jersey 10', cursive; font-weight: 400; font-size: 2.25rem; color: #717182; text-decoration: none; }
88
- [data-theme="dark"] .nav-brand { color: #9393A1; }
89
- .nav-toggle { display: none; background: none; border: 1px solid var(--border-color); border-radius: 0.375rem; padding: 0.5rem; cursor: pointer; color: var(--text-secondary); height: 2.25rem; align-items: center; justify-content: center; }
90
- .nav-toggle svg { width: 1.25rem; height: 1.25rem; display: block; }
91
- .nav-links { display: flex; gap: 1.5rem; align-items: center; margin-left: 2rem; }
92
- .nav-links a { color: var(--text-secondary); text-decoration: none; font-weight: 500; padding: 0.5rem 0; border-bottom: 2px solid transparent; }
93
- .nav-links a:hover, .nav-links a.active { color: var(--primary); border-bottom-color: var(--primary); }
94
- .nav-utils { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
95
-
96
- /* Theme Toggle */
97
- .theme-toggle { background: none; border: 1px solid var(--border-color); border-radius: 0.375rem; padding: 0.5rem; cursor: pointer; color: var(--text-secondary); display: flex; align-items: center; justify-content: center; transition: all 0.2s; height: 2.25rem; }
98
- .theme-toggle:hover { background: var(--gray-100); color: var(--text-primary); }
99
- .theme-toggle svg { width: 1.25rem; height: 1.25rem; }
100
- .theme-toggle .icon-sun { display: none; }
101
- .theme-toggle .icon-moon { display: block; }
102
- [data-theme="dark"] .theme-toggle .icon-sun { display: block; }
103
- [data-theme="dark"] .theme-toggle .icon-moon { display: none; }
104
-
105
- /* Main */
106
- .main { padding: 2rem 0; flex: 1; overflow-y: auto; }
107
- @media (max-width: 768px) { .main { overflow-y: visible; } }
108
- .page-header { margin-bottom: 2rem; display: none; flex-direction: column; gap: 0.25rem; }
109
- .page-header--visible { display: flex; }
110
- .page-header-top { display: flex; align-items: center; gap: 0.75rem; }
111
- .btn-back { display: flex; align-items: center; justify-content: center; width: 2.25rem; height: 2.25rem; background: none; border: 1px solid var(--border-color); border-radius: 0.375rem; cursor: pointer; color: var(--text-secondary); transition: all 0.2s; }
112
- .btn-back:hover { background: var(--gray-100); color: var(--text-primary); }
113
- .page-title { font-family: 'Jersey 10', cursive; font-size: 2rem; font-weight: 400; color: var(--gray-900); }
114
- .page-subtitle { color: var(--gray-500); }
115
-
116
- /* Cards */
117
- .card { background: var(--bg-primary); border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); padding: 1.5rem; margin-bottom: 1.5rem; transition: background-color 0.3s; overflow: hidden; min-width: 0; }
118
- .card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border-color); }
119
- .card-title { font-family: 'Jersey 10', cursive; font-weight: 400; font-size: 1.5rem; }
120
-
121
- /* Stats */
122
- .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
123
- .stat-card { background: var(--bg-primary); border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); padding: 1.5rem; transition: background-color 0.3s; min-width: 0; overflow: hidden; }
124
- .stat-label { color: var(--gray-500); font-size: 0.875rem; }
125
- .stat-value { font-size: 2rem; font-weight: 700; margin-top: 0.25rem; }
126
- .stat-value.primary { color: #8ACB88; }
127
- .stat-value.success { color: var(--success); }
128
- .stat-value.warning { color: var(--warning); }
129
- .stat-value.danger { color: var(--danger); }
130
-
131
- /* Tables */
132
- .table-wrapper { overflow-x: auto; -webkit-overflow-scrolling: touch; }
133
- table { width: 100%; border-collapse: collapse; }
134
- th, td { text-align: left; padding: 0.75rem 1rem; border-bottom: 1px solid var(--border-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
135
- th { background: var(--gray-100); font-weight: 600; font-size: 0.875rem; color: var(--text-secondary); transition: background-color 0.3s; }
136
- tr:hover { background: var(--gray-100); }
137
- tr.clickable-row { cursor: pointer; transition: background-color 0.15s; }
138
- tr.clickable-row:hover { background: var(--gray-200); }
139
- tr.clickable-row td a { color: inherit; text-decoration: none; }
140
-
141
- /* Badges */
142
- .badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 500; }
143
- .badge-draft { background: var(--gray-100); color: var(--gray-700); }
144
- .badge-accepted { background: #dbeafe; color: #1d4ed8; }
145
- .badge-delivered { background: #d1fae5; color: #047857; }
146
- .badge-opened { background: #fef3c7; color: #b45309; }
147
- .badge-clicked { background: #ddd6fe; color: #7c3aed; }
148
- .badge-bounced, .badge-rejected, .badge-complained { background: #fee2e2; color: #dc2626; }
149
- .badge-delayed { background: #fef3c7; color: #b45309; }
150
- .badge-delivery { background: #d1fae5; color: #047857; }
151
- .badge-bounce { background: #fee2e2; color: #dc2626; }
152
- .badge-open { background: #fef3c7; color: #b45309; }
153
- .badge-click { background: #ddd6fe; color: #7c3aed; }
154
- .badge-complaint { background: #fee2e2; color: #dc2626; }
155
-
156
- /* Buttons */
157
- .btn { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; border-radius: 0.375rem; font-weight: 500; font-size: 0.875rem; text-decoration: none; cursor: pointer; border: none; transition: background-color 0.2s; }
158
- .btn-primary { background: var(--primary); color: white; }
159
- .btn-primary:hover { background: var(--primary-dark); }
160
- .btn-secondary { background: var(--bg-primary); color: var(--text-secondary); border: 1px solid var(--border-color); }
161
- .btn-secondary:hover { background: var(--gray-100); }
162
- .btn-sm { padding: 0.375rem 0.75rem; font-size: 0.75rem; height: 2rem; }
163
- .btn-icon { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; border-radius: 0.375rem; color: var(--text-secondary); transition: all 0.15s; }
164
- .btn-icon:hover { background: var(--gray-100); color: var(--primary); }
165
- .actions-cell { display: flex; gap: 0.25rem; }
166
-
167
- /* Forms */
168
- .form-group { margin-bottom: 1rem; }
169
- .form-label { display: block; font-weight: 500; margin-bottom: 0.5rem; font-size: 0.875rem; }
170
- .form-input, .form-select, .form-textarea { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--gray-300); border-radius: 0.375rem; font-size: 0.875rem; }
171
- .form-input:focus, .form-select:focus, .form-textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); }
172
- .form-textarea { min-height: 150px; font-family: monospace; }
173
-
174
- /* Filters */
175
- .filters { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-end; margin-bottom: 1.5rem; padding: 1rem; background: white; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
176
- .filter-group { flex: 1; min-width: 150px; }
177
- .filter-group label { display: block; font-size: 0.75rem; color: var(--gray-500); margin-bottom: 0.25rem; }
178
-
179
- /* Timeline */
180
- .timeline { position: relative; padding-left: 2rem; }
181
- .timeline::before { content: ''; position: absolute; left: 0.5rem; top: 0; bottom: 0; width: 2px; background: var(--gray-200); }
182
- .timeline-item { position: relative; padding-bottom: 1.5rem; }
183
- .timeline-item::before { content: ''; position: absolute; left: -1.5rem; top: 0.25rem; width: 0.75rem; height: 0.75rem; border-radius: 50%; background: var(--primary); border: 2px solid white; }
184
- .timeline-item.delivery::before { background: var(--success); }
185
- .timeline-item.engagement::before { background: var(--warning); }
186
- .timeline-item.error::before { background: var(--danger); }
187
- .timeline-time { font-size: 0.75rem; color: var(--gray-500); }
188
- .timeline-content { margin-top: 0.25rem; }
189
-
190
- /* Detail */
191
- .detail-row { display: flex; padding: 0.75rem 0; border-bottom: 1px solid var(--gray-100); }
192
- .detail-label { width: 150px; font-weight: 500; color: var(--gray-500); }
193
- .detail-value { flex: 1; }
194
-
195
- /* Grid */
196
- .grid { display: grid; gap: 1.5rem; }
197
- .grid-2 { grid-template-columns: repeat(2, 1fr); }
198
- .grid > * { min-width: 0; }
199
-
200
- /* Pagination */
201
- .pagination { display: flex; justify-content: center; align-items: center; gap: 0.5rem; margin-top: 1.5rem; padding: 1rem; }
202
- .pagination-link { padding: 0.5rem 1rem; border-radius: 0.375rem; text-decoration: none; color: var(--text-secondary); background: var(--bg-primary); border: 1px solid var(--border-color); font-size: 0.875rem; transition: background-color 0.2s; }
203
- .pagination-link:hover:not(.disabled) { background: var(--gray-100); }
204
- .pagination-link.disabled { color: var(--gray-300); cursor: not-allowed; }
205
- .pagination-info { font-size: 0.875rem; color: var(--text-secondary); }
206
-
207
- /* Filters Form */
208
- .filters-form { padding: 1rem; }
209
- .filters-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1rem; }
210
- .filter-group label { display: block; font-size: 0.75rem; font-weight: 500; color: var(--gray-500); margin-bottom: 0.25rem; text-transform: uppercase; letter-spacing: 0.05em; }
211
- .filter-group input, .filter-group select { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--border-color); border-radius: 0.375rem; font-size: 0.875rem; background: var(--bg-primary); color: var(--text-primary); transition: background-color 0.3s, border-color 0.2s; }
212
- .filter-group input:focus, .filter-group select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); }
213
- .filter-actions { display: flex; gap: 0.5rem; }
214
-
215
- /* Details Grid */
216
- .details-grid { padding: 0.5rem 0; }
217
- .detail-row { display: flex; padding: 0.75rem 0; border-bottom: 1px solid var(--gray-100); }
218
- .detail-row:last-child { border-bottom: none; }
219
- .detail-label { width: 140px; flex-shrink: 0; font-size: 0.875rem; font-weight: 500; color: var(--gray-500); }
220
- .detail-value { flex: 1; font-size: 0.875rem; }
221
-
222
- /* Code Block */
223
- .code-block { background: var(--gray-50); border-radius: 0.375rem; padding: 1rem; overflow-x: auto; }
224
- .code-block pre { margin: 0; font-family: 'SF Mono', Consolas, monospace; font-size: 0.8125rem; white-space: pre-wrap; word-break: break-all; }
225
- code { background: var(--gray-100); padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-family: 'SF Mono', Consolas, monospace; font-size: 0.8125rem; }
226
-
227
- /* Timeline */
228
- .timeline { position: relative; padding: 1rem; }
229
- .timeline-item { display: flex; gap: 1rem; padding-bottom: 1.5rem; position: relative; }
230
- .timeline-item:not(:last-child)::after { content: ''; position: absolute; left: 6px; top: 24px; bottom: 0; width: 2px; background: var(--gray-200); }
231
- .timeline-marker { width: 14px; height: 14px; border-radius: 50%; background: var(--gray-300); flex-shrink: 0; margin-top: 4px; }
232
- .timeline-marker.badge-delivered, .timeline-marker.badge-delivery { background: var(--success); }
233
- .timeline-marker.badge-opened, .timeline-marker.badge-open { background: var(--warning); }
234
- .timeline-marker.badge-clicked, .timeline-marker.badge-click { background: #7c3aed; }
235
- .timeline-marker.badge-bounced, .timeline-marker.badge-bounce, .timeline-marker.badge-complained, .timeline-marker.badge-complaint { background: var(--danger); }
236
- .timeline-content { flex: 1; }
237
- .timeline-header { display: flex; align-items: center; gap: 0.75rem; }
238
- .timeline-time { font-size: 0.75rem; color: var(--gray-500); }
239
- .timeline-details { margin-top: 0.5rem; font-size: 0.875rem; color: var(--gray-700); }
240
-
241
- /* Form Styles */
242
- .form-content { padding: 0.5rem 0; }
243
- .form-group { margin-bottom: 1.25rem; }
244
- .form-group label { display: block; font-weight: 500; margin-bottom: 0.375rem; font-size: 0.875rem; color: var(--gray-700); }
245
- .form-control { width: 100%; padding: 0.625rem 0.875rem; border: 1px solid var(--border-color); border-radius: 0.375rem; font-size: 0.875rem; transition: border-color 0.15s, box-shadow 0.15s, background-color 0.3s; background: var(--bg-primary); color: var(--text-primary); }
246
- .form-control:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); }
247
- .form-hint { display: block; margin-top: 0.375rem; font-size: 0.75rem; color: var(--gray-500); }
248
- .code-editor { font-family: 'SF Mono', Consolas, monospace; font-size: 0.8125rem; min-height: 200px; }
249
- .form-actions { display: flex; gap: 0.75rem; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--gray-200); }
250
-
251
- /* Alert */
252
- .alert { padding: 1rem; border-radius: 0.375rem; margin-bottom: 1.25rem; }
253
- .alert-danger { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
254
- .alert ul { margin: 0.5rem 0 0 1.25rem; }
255
-
256
- /* Tips */
257
- .tips-content { padding: 0.5rem 0; font-size: 0.875rem; color: var(--gray-700); }
258
- .tips-content h4 { font-size: 0.8125rem; font-weight: 600; color: var(--gray-900); margin-bottom: 0.25rem; }
259
- .tips-content p { margin-bottom: 0.5rem; }
260
- .tips-content code { font-size: 0.75rem; }
261
- .tips-content pre { background: var(--gray-50); padding: 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; overflow-x: auto; }
262
-
263
- /* Stats Mini */
264
- .stats-mini { display: flex; gap: 2rem; padding: 1rem 0; }
265
- .stat-mini { text-align: center; }
266
- .stat-mini-value { font-size: 2rem; font-weight: 700; color: var(--primary); }
267
- .stat-mini-label { font-size: 0.75rem; color: var(--gray-500); margin-top: 0.25rem; }
268
-
269
- /* Header Actions */
270
- .header-actions { display: flex; align-items: center; gap: 0.5rem; }
271
- /* Event Details */
272
- .event-details { margin-top: 0.5rem; background: var(--gray-50); padding: 0.75rem; border-radius: 0.25rem; font-size: 0.75rem; max-height: 200px; overflow: auto; }
273
-
274
- /* Empty State */
275
- .empty-state { text-align: center; padding: 2rem; color: var(--gray-500); }
276
-
277
- /* Donut Chart */
278
- .donut-chart-container { display: flex; flex-direction: column; align-items: center; gap: 1.5rem; padding: 1rem 0; }
279
- .donut-chart-wrapper { position: relative; width: 300px; height: 300px; flex-shrink: 0; }
280
- .donut-chart { width: 100%; height: 100%; transform: rotate(-90deg); }
281
- .donut-segment { fill: none; stroke-width: 32; stroke-linecap: butt; transition: opacity 0.2s, filter 0.2s; cursor: pointer; }
282
- .donut-segment.dimmed { opacity: 0.3; filter: grayscale(60%); }
283
- .donut-segment.highlighted { filter: brightness(1.1); }
284
- .donut-center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; pointer-events: none; }
285
- .donut-center-value { font-size: 2rem; font-weight: 700; color: var(--text-primary); line-height: 1; }
286
- .donut-center-label { font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem; }
287
- .donut-legend { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem; width: 100%; }
288
- .donut-legend-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.625rem; border-radius: 0.375rem; cursor: pointer; transition: background-color 0.15s, opacity 0.2s; }
289
- .donut-legend-item:hover { background: var(--gray-100); }
290
- .donut-legend-item.dimmed { opacity: 0.4; }
291
- .donut-legend-item.highlighted { background: var(--gray-100); }
292
- .donut-legend-color { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
293
- .donut-legend-text { font-size: 0.8125rem; font-weight: 500; color: var(--text-primary); }
294
- .donut-legend-count { font-size: 0.8125rem; font-weight: 600; color: var(--text-secondary); }
295
- .donut-empty { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--text-secondary); font-size: 0.875rem; }
296
-
297
- /* Line Chart */
298
- .line-chart-container { padding: 1rem 0; }
299
- .line-chart-wrapper { position: relative; width: 100%; height: 280px; }
300
- .line-chart { width: 100%; height: 100%; }
301
- .line-chart-line { fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; transition: opacity 0.2s, stroke-width 0.2s; cursor: pointer; }
302
- .line-chart-line.dimmed { opacity: 0.15; }
303
- .line-chart-line.highlighted { stroke-width: 4; }
304
- .line-chart-area { opacity: 0.1; transition: opacity 0.2s; }
305
- .line-chart-area.dimmed { opacity: 0.02; }
306
- .line-chart-area.highlighted { opacity: 0.2; }
307
- .line-chart-axis { stroke: var(--border-color); stroke-width: 1; }
308
- .line-chart-grid { stroke: var(--border-color); stroke-width: 0.5; opacity: 0.5; }
309
- .line-chart-label { font-size: 0.625rem; fill: var(--text-secondary); }
310
- .line-chart-label-y { text-anchor: end; }
311
- .line-chart-label-x { text-anchor: middle; }
312
- .line-chart-legend { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem; margin-top: 1rem; }
313
- .line-chart-legend-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.625rem; border-radius: 0.375rem; cursor: pointer; transition: background-color 0.15s, opacity 0.2s; }
314
- .line-chart-legend-item:hover { background: var(--gray-100); }
315
- .line-chart-legend-item.dimmed { opacity: 0.4; }
316
- .line-chart-legend-item.highlighted { background: var(--gray-100); }
317
- .line-chart-empty { display: flex; align-items: center; justify-content: center; height: 280px; color: var(--text-secondary); font-size: 0.875rem; }
318
-
319
- /* Time Filter Dropdown */
320
- .time-dropdown { position: relative; }
321
- .time-dropdown-btn { display: flex; align-items: center; gap: 0.375rem; background: none; border: 1px solid var(--border-color); border-radius: 0.375rem; padding: 0 0.625rem; cursor: pointer; color: var(--text-secondary); font-size: 0.8125rem; font-weight: 500; transition: all 0.2s; height: 2.25rem; }
322
- .time-dropdown-btn:hover { background: var(--gray-100); color: var(--text-primary); }
323
- .time-dropdown-btn svg { width: 1rem; height: 1rem; }
324
- .time-dropdown-menu { position: absolute; top: 100%; right: 0; margin-top: 0.25rem; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 0.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15); min-width: 140px; z-index: 100; display: none; }
325
- .time-dropdown.open .time-dropdown-menu { display: block; }
326
- .time-dropdown-item { display: block; width: 100%; padding: 0.5rem 0.75rem; border: none; background: none; text-align: left; font-size: 0.8125rem; color: var(--text-secondary); cursor: pointer; transition: all 0.15s; }
327
- .time-dropdown-item:first-child { border-radius: 0.5rem 0.5rem 0 0; }
328
- .time-dropdown-item:last-child { border-radius: 0 0 0.5rem 0.5rem; }
329
- .time-dropdown-item:hover { background: var(--gray-100); color: var(--text-primary); }
330
- .time-dropdown-item.active { background: var(--primary); color: white; }
331
-
332
- /* Flash */
333
- .flash { padding: 1rem; border-radius: 0.375rem; margin-bottom: 1rem; }
334
- .flash-notice { background: #d1fae5; color: #047857; }
335
- .flash-alert { background: #fee2e2; color: #dc2626; }
336
-
337
- /* Responsive */
338
- @media (max-width: 768px) {
339
- .grid-2 { grid-template-columns: 1fr; }
340
- .page-header { display: flex; justify-content: space-between; align-items: flex-start; }
341
- .nav-toggle { display: block; order: 1; position: relative; }
342
- .nav-brand { order: 2; }
343
- .nav-utils { order: 3; margin-left: auto; }
344
- .nav-links {
345
- display: none;
346
- flex-direction: column;
347
- position: absolute;
348
- top: 100%;
349
- left: 1rem;
350
- margin-top: 0.25rem;
351
- background: var(--bg-primary);
352
- border: 1px solid var(--border-color);
353
- border-radius: 0.5rem;
354
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
355
- min-width: 180px;
356
- z-index: 100;
357
- margin-left: 0;
358
- gap: 0;
359
- padding: 0;
360
- }
361
- .nav-links.open { display: flex; }
362
- .nav-links a {
363
- padding: 0.625rem 1rem;
364
- border-bottom: none;
365
- color: var(--text-secondary);
366
- transition: all 0.15s;
367
- }
368
- .nav-links a:first-child { border-radius: 0.5rem 0.5rem 0 0; }
369
- .nav-links a:last-child { border-radius: 0 0 0.5rem 0.5rem; }
370
- .nav-links a:hover { color: var(--text-primary); }
371
- .nav-links a.active { color: var(--primary); font-weight: 600; }
372
- .stats-grid { grid-template-columns: repeat(2, 1fr); }
373
- .stat-value { font-size: 1.5rem; }
374
- .donut-chart-wrapper { width: 220px; height: 220px; }
375
- .line-chart-wrapper { height: 200px; }
376
- .card { padding: 1rem; }
377
- .card-header { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
378
- .main { padding: 1rem 0; }
379
- }
380
-
381
- @media (max-width: 480px) {
382
- .stats-grid { grid-template-columns: 1fr; }
383
- .donut-chart-wrapper { width: 180px; height: 180px; }
384
- .donut-center-value { font-size: 1.5rem; }
385
- }
386
-
387
- /* Return Footer */
388
- .return-footer {
389
- position: fixed;
390
- bottom: 0;
391
- left: 0;
392
- right: 0;
393
- background: var(--bg-primary);
394
- border-top: 1px solid var(--border-color);
395
- padding: 0.5rem 1rem;
396
- z-index: 40;
397
- opacity: 0.85;
398
- transition: opacity 0.2s, background-color 0.3s;
399
- }
400
- .return-footer:hover { opacity: 1; }
401
- .return-footer-link {
402
- display: inline-flex;
403
- align-items: center;
404
- gap: 0.375rem;
405
- color: var(--text-secondary);
406
- text-decoration: none;
407
- font-size: 0.8125rem;
408
- transition: color 0.15s;
409
- }
410
- .return-footer-link:hover { color: var(--primary); }
411
- .return-footer-link svg { width: 0.875rem; height: 0.875rem; }
412
- .main.has-return-footer { padding-bottom: 3.5rem; }
413
- </style>
12
+ <%= stylesheet_link_tag 'sincerely/variables', 'sincerely/navigation', 'sincerely/layout', 'sincerely/components', 'sincerely/charts' %>
414
13
  </head>
415
- <body>
14
+ <body data-controller="<%= controller_name %>">
416
15
  <nav class="nav">
417
16
  <div class="container nav-inner">
418
17
  <button class="nav-toggle" onclick="toggleNav()" aria-label="Toggle navigation">
419
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
18
+ <%= inline_svg 'sincerely/menu' %>
420
19
  </button>
421
- <a href="<%= sincerely.root_path %>" class="nav-brand">Sincerely</a>
20
+ <a href="<%= root_path %>" class="nav-brand">Sincerely</a>
422
21
  <div class="nav-links" id="navLinks">
423
- <a href="<%= sincerely.root_path %>" class="<%= 'active' if controller_name == 'dashboard' %>">Dashboard</a>
424
- <a href="<%= sincerely.notifications_path %>" class="<%= 'active' if controller_name == 'notifications' %>">Notifications</a>
425
- <a href="<%= sincerely.delivery_events_path %>" class="<%= 'active' if controller_name == 'delivery_events' %>">Delivery</a>
426
- <a href="<%= sincerely.engagement_events_path %>" class="<%= 'active' if controller_name == 'engagement_events' %>">Engagement</a>
427
- <a href="<%= sincerely.templates_path %>" class="<%= 'active' if controller_name == 'templates' %>">Templates</a>
428
- <a href="<%= sincerely.send_new_path %>" class="<%= 'active' if controller_name == 'send' %>">Send</a>
22
+ <a href="<%= root_path %>" class="<%= 'active' if controller_name == 'dashboard' %>">Dashboard</a>
23
+ <a href="<%= notifications_path %>" class="<%= 'active' if controller_name == 'notifications' %>">Notifications</a>
24
+ <a href="<%= delivery_events_path %>" class="<%= 'active' if controller_name == 'delivery_events' %>">Delivery</a>
25
+ <a href="<%= engagement_events_path %>" class="<%= 'active' if controller_name == 'engagement_events' %>">Engagement</a>
26
+ <a href="<%= templates_path %>" class="<%= 'active' if controller_name == 'templates' %>">Templates</a>
27
+ <a href="<%= send_new_path %>" class="<%= 'active' if controller_name == 'send' %>">Send</a>
429
28
  </div>
430
29
  <div class="nav-utils">
431
30
  <% if !%w[templates send].include?(controller_name) %>
432
31
  <% time_labels = { '1h' => '1h', '24h' => '24h', '7d' => '7d', '30d' => '30d', '3m' => '3m', 'all' => 'All' } %>
433
32
  <div class="time-dropdown" id="timeDropdown">
434
33
  <button class="time-dropdown-btn" onclick="toggleTimeDropdown()" title="Time filter">
435
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
34
+ <%= inline_svg 'sincerely/clock' %>
436
35
  <span id="timeLabel"><%= time_labels[params[:period]] || '24h' %></span>
437
36
  </button>
438
37
  <div class="time-dropdown-menu">
@@ -446,13 +45,17 @@
446
45
  </div>
447
46
  <% end %>
448
47
  <button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">
449
- <svg class="icon-moon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>
450
- <svg class="icon-sun" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
48
+ <%= inline_svg 'sincerely/moon' %>
49
+ <%= inline_svg 'sincerely/sun' %>
451
50
  </button>
452
51
  <% if Sincerely.authenticate_with.present? && Sincerely.config.logout_url.present? %>
453
- <a href="<%= Sincerely.config.logout_url %>" class="theme-toggle" title="<%= Sincerely.config.logout_label.presence || 'Logout' %>"<%= " data-turbo-method=\"#{Sincerely.config.logout_method}\"".html_safe if Sincerely.config.logout_method.present? %>>
454
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg>
455
- </a>
52
+ <%= button_to Sincerely.config.logout_url,
53
+ method: Sincerely.config.logout_method.presence || :get,
54
+ class: 'theme-toggle',
55
+ title: Sincerely.config.logout_label.presence || 'Logout',
56
+ form: { style: 'display:contents' } do %>
57
+ <%= inline_svg 'sincerely/logout' %>
58
+ <% end %>
456
59
  <% end %>
457
60
  </div>
458
61
  </div>
@@ -474,120 +77,13 @@
474
77
  <% if Sincerely.config.return_url.present? %>
475
78
  <footer class="return-footer">
476
79
  <a href="<%= Sincerely.config.return_url %>" class="return-footer-link">
477
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
80
+ <%= inline_svg 'sincerely/arrow_left' %>
478
81
  <%= Sincerely.config.return_label.presence || 'Back to app' %>
479
82
  </a>
480
83
  </footer>
481
84
  <% end %>
482
85
 
483
- <script>
484
- const TIME_LABELS = { '1h': '1h', '24h': '24h', '7d': '7d', '30d': '30d', '3m': '3m', 'all': 'All' };
485
- const FILTERED_PAGES = ['dashboard', 'notifications', 'delivery_events', 'engagement_events'];
486
- const CURRENT_PAGE = '<%= controller_name %>';
487
-
488
- function toggleNav() {
489
- const navLinks = document.getElementById('navLinks');
490
- if (navLinks) navLinks.classList.toggle('open');
491
- }
492
-
493
- function toggleTheme() {
494
- const html = document.documentElement;
495
- const currentTheme = html.getAttribute('data-theme');
496
- const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
497
- html.setAttribute('data-theme', newTheme);
498
- localStorage.setItem('sincerely-theme', newTheme);
499
- }
500
-
501
- function toggleTimeDropdown() {
502
- const dropdown = document.getElementById('timeDropdown');
503
- if (dropdown) dropdown.classList.toggle('open');
504
- }
505
-
506
- function setTimePeriod(period) {
507
- localStorage.setItem('sincerely-period', period);
508
- const label = document.getElementById('timeLabel');
509
- if (label) label.textContent = TIME_LABELS[period];
510
- const dropdown = document.getElementById('timeDropdown');
511
- if (dropdown) dropdown.classList.remove('open');
512
- updateActiveItem(period);
513
-
514
- const url = new URL(window.location);
515
- url.searchParams.set('period', period);
516
- window.location = url;
517
- }
518
-
519
- function updateActiveItem(period) {
520
- document.querySelectorAll('.time-dropdown-item').forEach(item => {
521
- item.classList.toggle('active', item.dataset.period === period);
522
- });
523
- }
524
-
525
- function getSavedPeriod() {
526
- return localStorage.getItem('sincerely-period') || '24h';
527
- }
528
-
529
- document.addEventListener('click', function(e) {
530
- // Close time dropdown when clicking outside
531
- const dropdown = document.getElementById('timeDropdown');
532
- if (dropdown && !dropdown.contains(e.target)) {
533
- dropdown.classList.remove('open');
534
- }
535
-
536
- // Close nav menu when clicking outside
537
- const navLinks = document.getElementById('navLinks');
538
- const navToggle = document.querySelector('.nav-toggle');
539
- if (navLinks && !navLinks.contains(e.target) && !navToggle.contains(e.target)) {
540
- navLinks.classList.remove('open');
541
- }
542
- });
543
-
544
- document.querySelectorAll('.time-dropdown-item').forEach(item => {
545
- item.addEventListener('click', () => setTimePeriod(item.dataset.period));
546
- });
547
-
548
- document.querySelectorAll('.nav-links a').forEach(link => {
549
- link.addEventListener('click', function(e) {
550
- // Close mobile nav on link click
551
- const navLinks = document.getElementById('navLinks');
552
- if (navLinks) navLinks.classList.remove('open');
553
-
554
- const href = this.getAttribute('href');
555
- const isFilteredPage = FILTERED_PAGES.some(page =>
556
- href.includes('/' + page) || href.endsWith('/sincerely') || href.endsWith('/sincerely/')
557
- );
558
-
559
- if (isFilteredPage) {
560
- e.preventDefault();
561
- const url = new URL(href, window.location.origin);
562
- url.searchParams.set('period', getSavedPeriod());
563
- window.location = url;
564
- }
565
- });
566
- });
567
-
568
- (function() {
569
- const savedTheme = localStorage.getItem('sincerely-theme');
570
- if (savedTheme) {
571
- document.documentElement.setAttribute('data-theme', savedTheme);
572
- } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
573
- document.documentElement.setAttribute('data-theme', 'dark');
574
- }
575
-
576
- const urlParams = new URLSearchParams(window.location.search);
577
- const urlPeriod = urlParams.get('period');
578
- const savedPeriod = getSavedPeriod();
579
- const period = urlPeriod || savedPeriod;
580
-
581
- const label = document.getElementById('timeLabel');
582
- if (label) label.textContent = TIME_LABELS[period] || '24h';
583
- updateActiveItem(period);
584
-
585
- if (!urlPeriod && FILTERED_PAGES.includes(CURRENT_PAGE)) {
586
- const url = new URL(window.location);
587
- url.searchParams.set('period', savedPeriod);
588
- window.location.replace(url);
589
- }
590
- })();
591
- </script>
86
+ <%= javascript_include_tag 'sincerely/layout' %>
87
+ <%= javascript_include_tag 'sincerely/clickable_rows' %>
592
88
  </body>
593
89
  </html>