not_pressed-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +41 -0
  3. data/README.md +285 -0
  4. data/app/assets/javascripts/not_pressed/lightbox.js +110 -0
  5. data/app/assets/stylesheets/not_pressed/admin.css +1161 -0
  6. data/app/assets/stylesheets/not_pressed/content.css +193 -0
  7. data/app/assets/stylesheets/not_pressed/gallery.css +117 -0
  8. data/app/assets/stylesheets/not_pressed/themes/starter.css +118 -0
  9. data/app/controllers/not_pressed/admin/base_controller.rb +21 -0
  10. data/app/controllers/not_pressed/admin/categories_controller.rb +53 -0
  11. data/app/controllers/not_pressed/admin/content_blocks_controller.rb +73 -0
  12. data/app/controllers/not_pressed/admin/dashboard_controller.rb +19 -0
  13. data/app/controllers/not_pressed/admin/forms_controller.rb +86 -0
  14. data/app/controllers/not_pressed/admin/media_attachments_controller.rb +94 -0
  15. data/app/controllers/not_pressed/admin/pages_controller.rb +122 -0
  16. data/app/controllers/not_pressed/admin/plugins_controller.rb +121 -0
  17. data/app/controllers/not_pressed/admin/settings_controller.rb +19 -0
  18. data/app/controllers/not_pressed/admin/tags_controller.rb +37 -0
  19. data/app/controllers/not_pressed/admin/themes_controller.rb +104 -0
  20. data/app/controllers/not_pressed/application_controller.rb +6 -0
  21. data/app/controllers/not_pressed/blog_controller.rb +83 -0
  22. data/app/controllers/not_pressed/form_submissions_controller.rb +70 -0
  23. data/app/controllers/not_pressed/pages_controller.rb +36 -0
  24. data/app/controllers/not_pressed/robots_controller.rb +34 -0
  25. data/app/controllers/not_pressed/sitemaps_controller.rb +12 -0
  26. data/app/helpers/not_pressed/admin_helper.rb +41 -0
  27. data/app/helpers/not_pressed/application_helper.rb +6 -0
  28. data/app/helpers/not_pressed/code_injection_helper.rb +29 -0
  29. data/app/helpers/not_pressed/content_helper.rb +13 -0
  30. data/app/helpers/not_pressed/form_helper.rb +80 -0
  31. data/app/helpers/not_pressed/media_helper.rb +28 -0
  32. data/app/helpers/not_pressed/seo_helper.rb +69 -0
  33. data/app/helpers/not_pressed/theme_helper.rb +42 -0
  34. data/app/mailers/not_pressed/application_mailer.rb +10 -0
  35. data/app/mailers/not_pressed/form_mailer.rb +15 -0
  36. data/app/models/concerns/not_pressed/sluggable.rb +43 -0
  37. data/app/models/not_pressed/category.rb +16 -0
  38. data/app/models/not_pressed/content_block.rb +46 -0
  39. data/app/models/not_pressed/form.rb +55 -0
  40. data/app/models/not_pressed/form_field.rb +23 -0
  41. data/app/models/not_pressed/form_submission.rb +19 -0
  42. data/app/models/not_pressed/media_attachment.rb +68 -0
  43. data/app/models/not_pressed/page.rb +182 -0
  44. data/app/models/not_pressed/page_version.rb +15 -0
  45. data/app/models/not_pressed/setting.rb +20 -0
  46. data/app/models/not_pressed/tag.rb +15 -0
  47. data/app/models/not_pressed/tagging.rb +12 -0
  48. data/app/plugins/not_pressed/analytics_plugin.rb +106 -0
  49. data/app/plugins/not_pressed/callout_block_plugin.rb +43 -0
  50. data/app/themes/not_pressed/starter_theme.rb +26 -0
  51. data/app/views/layouts/not_pressed/admin.html.erb +745 -0
  52. data/app/views/layouts/not_pressed/application.html.erb +12 -0
  53. data/app/views/layouts/not_pressed/page.html.erb +22 -0
  54. data/app/views/not_pressed/admin/categories/index.html.erb +58 -0
  55. data/app/views/not_pressed/admin/content_blocks/_block.html.erb +18 -0
  56. data/app/views/not_pressed/admin/content_blocks/_block_picker.html.erb +32 -0
  57. data/app/views/not_pressed/admin/content_blocks/create.turbo_stream.erb +3 -0
  58. data/app/views/not_pressed/admin/content_blocks/destroy.turbo_stream.erb +1 -0
  59. data/app/views/not_pressed/admin/content_blocks/editors/_callout.html.erb +38 -0
  60. data/app/views/not_pressed/admin/content_blocks/editors/_code.html.erb +26 -0
  61. data/app/views/not_pressed/admin/content_blocks/editors/_divider.html.erb +4 -0
  62. data/app/views/not_pressed/admin/content_blocks/editors/_form.html.erb +16 -0
  63. data/app/views/not_pressed/admin/content_blocks/editors/_gallery.html.erb +75 -0
  64. data/app/views/not_pressed/admin/content_blocks/editors/_heading.html.erb +26 -0
  65. data/app/views/not_pressed/admin/content_blocks/editors/_html.html.erb +13 -0
  66. data/app/views/not_pressed/admin/content_blocks/editors/_image.html.erb +56 -0
  67. data/app/views/not_pressed/admin/content_blocks/editors/_quote.html.erb +24 -0
  68. data/app/views/not_pressed/admin/content_blocks/editors/_text.html.erb +28 -0
  69. data/app/views/not_pressed/admin/content_blocks/editors/_video.html.erb +25 -0
  70. data/app/views/not_pressed/admin/dashboard/index.html.erb +60 -0
  71. data/app/views/not_pressed/admin/forms/_field_row.html.erb +33 -0
  72. data/app/views/not_pressed/admin/forms/_form.html.erb +75 -0
  73. data/app/views/not_pressed/admin/forms/edit.html.erb +1 -0
  74. data/app/views/not_pressed/admin/forms/index.html.erb +32 -0
  75. data/app/views/not_pressed/admin/forms/new.html.erb +1 -0
  76. data/app/views/not_pressed/admin/forms/submissions.html.erb +34 -0
  77. data/app/views/not_pressed/admin/media_attachments/_media_card.html.erb +21 -0
  78. data/app/views/not_pressed/admin/media_attachments/_picker.html.erb +19 -0
  79. data/app/views/not_pressed/admin/media_attachments/edit.html.erb +57 -0
  80. data/app/views/not_pressed/admin/media_attachments/index.html.erb +48 -0
  81. data/app/views/not_pressed/admin/pages/_form.html.erb +177 -0
  82. data/app/views/not_pressed/admin/pages/_page_tree_node.html.erb +32 -0
  83. data/app/views/not_pressed/admin/pages/edit.html.erb +69 -0
  84. data/app/views/not_pressed/admin/pages/index.html.erb +21 -0
  85. data/app/views/not_pressed/admin/pages/new.html.erb +1 -0
  86. data/app/views/not_pressed/admin/pages/preview.html.erb +17 -0
  87. data/app/views/not_pressed/admin/plugins/_settings_form.html.erb +59 -0
  88. data/app/views/not_pressed/admin/plugins/index.html.erb +48 -0
  89. data/app/views/not_pressed/admin/plugins/show.html.erb +54 -0
  90. data/app/views/not_pressed/admin/settings/code_injection.html.erb +21 -0
  91. data/app/views/not_pressed/admin/shared/_breadcrumbs.html.erb +14 -0
  92. data/app/views/not_pressed/admin/shared/_flash.html.erb +7 -0
  93. data/app/views/not_pressed/admin/shared/_modal.html.erb +9 -0
  94. data/app/views/not_pressed/admin/shared/_sidebar.html.erb +18 -0
  95. data/app/views/not_pressed/admin/tags/index.html.erb +52 -0
  96. data/app/views/not_pressed/admin/themes/index.html.erb +60 -0
  97. data/app/views/not_pressed/admin/themes/show.html.erb +66 -0
  98. data/app/views/not_pressed/blog/_post_card.html.erb +24 -0
  99. data/app/views/not_pressed/blog/feed.rss.builder +22 -0
  100. data/app/views/not_pressed/blog/index.html.erb +56 -0
  101. data/app/views/not_pressed/blog/show.html.erb +41 -0
  102. data/app/views/not_pressed/form_mailer/submission_notification.text.erb +8 -0
  103. data/app/views/not_pressed/pages/show.html.erb +4 -0
  104. data/app/views/themes/starter/layouts/not_pressed/default.html.erb +36 -0
  105. data/app/views/themes/starter/layouts/not_pressed/full_width.html.erb +36 -0
  106. data/app/views/themes/starter/layouts/not_pressed/sidebar.html.erb +41 -0
  107. data/config/routes.rb +81 -0
  108. data/db/migrate/20260310000001_create_not_pressed_pages.rb +20 -0
  109. data/db/migrate/20260310000002_create_not_pressed_content_blocks.rb +17 -0
  110. data/db/migrate/20260310000003_create_not_pressed_media_attachments.rb +14 -0
  111. data/db/migrate/20260310000004_add_content_type_to_not_pressed_pages.rb +8 -0
  112. data/db/migrate/20260310000005_add_publishing_fields_to_not_pressed_pages.rb +8 -0
  113. data/db/migrate/20260310000006_create_not_pressed_page_versions.rb +16 -0
  114. data/db/migrate/20260310000007_add_settings_fields_to_not_pressed_pages.rb +11 -0
  115. data/db/migrate/20260311000001_create_not_pressed_forms.rb +42 -0
  116. data/db/migrate/20260311000002_add_seo_fields_to_not_pressed_pages.rb +10 -0
  117. data/db/migrate/20260311000003_add_code_injection_to_not_pressed_pages.rb +8 -0
  118. data/db/migrate/20260311000004_create_not_pressed_settings.rb +14 -0
  119. data/db/migrate/20260312000001_create_not_pressed_categories.rb +16 -0
  120. data/db/migrate/20260312000002_create_not_pressed_tags.rb +15 -0
  121. data/db/migrate/20260312000003_create_not_pressed_taggings.rb +14 -0
  122. data/db/migrate/20260312000004_add_category_id_to_not_pressed_pages.rb +7 -0
  123. data/lib/generators/not_pressed/install/install_generator.rb +52 -0
  124. data/lib/generators/not_pressed/install/templates/initializer.rb.tt +89 -0
  125. data/lib/generators/not_pressed/install/templates/seeds.rb.tt +131 -0
  126. data/lib/generators/not_pressed/plugin/plugin_generator.rb +37 -0
  127. data/lib/generators/not_pressed/plugin/templates/plugin.rb.tt +23 -0
  128. data/lib/not_pressed/admin/authentication.rb +48 -0
  129. data/lib/not_pressed/admin/menu_registry.rb +100 -0
  130. data/lib/not_pressed/configuration.rb +77 -0
  131. data/lib/not_pressed/content_type.rb +23 -0
  132. data/lib/not_pressed/content_type_builder.rb +51 -0
  133. data/lib/not_pressed/content_type_registry.rb +45 -0
  134. data/lib/not_pressed/engine.rb +132 -0
  135. data/lib/not_pressed/hooks.rb +166 -0
  136. data/lib/not_pressed/navigation/builder.rb +148 -0
  137. data/lib/not_pressed/navigation/menu.rb +54 -0
  138. data/lib/not_pressed/navigation/menu_item.rb +33 -0
  139. data/lib/not_pressed/navigation/node.rb +45 -0
  140. data/lib/not_pressed/navigation/partial_parser.rb +96 -0
  141. data/lib/not_pressed/navigation/route_inspector.rb +98 -0
  142. data/lib/not_pressed/navigation.rb +6 -0
  143. data/lib/not_pressed/plugin.rb +354 -0
  144. data/lib/not_pressed/plugin_importer.rb +133 -0
  145. data/lib/not_pressed/plugin_manager.rb +196 -0
  146. data/lib/not_pressed/plugin_packager.rb +129 -0
  147. data/lib/not_pressed/rendering/block_renderer.rb +222 -0
  148. data/lib/not_pressed/rendering/renderer_registry.rb +154 -0
  149. data/lib/not_pressed/rendering.rb +8 -0
  150. data/lib/not_pressed/seo/sitemap_builder.rb +61 -0
  151. data/lib/not_pressed/theme.rb +191 -0
  152. data/lib/not_pressed/theme_importer.rb +133 -0
  153. data/lib/not_pressed/theme_packager.rb +180 -0
  154. data/lib/not_pressed/theme_registry.rb +123 -0
  155. data/lib/not_pressed/version.rb +5 -0
  156. data/lib/not_pressed.rb +65 -0
  157. metadata +258 -0
@@ -0,0 +1,193 @@
1
+ /* NotPressed Content Styles
2
+ * Styles for rendered block content on public pages and preview.
3
+ * Uses CSS custom properties for theme overrides.
4
+ */
5
+
6
+ :root {
7
+ /* Colors */
8
+ --np-color-primary: #4f46e5;
9
+ --np-color-text: #1e293b;
10
+ --np-color-heading: #0f172a;
11
+ --np-color-background: #fff;
12
+ --np-color-muted: #64748b;
13
+ --np-color-quote-text: #334155;
14
+ --np-color-quote-bg: #f8fafc;
15
+ --np-color-border: #e2e8f0;
16
+ --np-color-code-bg: #1e1e2e;
17
+ --np-color-code-text: #cdd6f4;
18
+
19
+ /* Fonts */
20
+ --np-font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
21
+ --np-font-code: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
22
+
23
+ /* Layout */
24
+ --np-max-width: 48rem;
25
+ }
26
+
27
+ /* Page layout */
28
+ .np-page-body {
29
+ margin: 0;
30
+ font-family: var(--np-font-body);
31
+ color: var(--np-color-text);
32
+ background-color: var(--np-color-background);
33
+ }
34
+
35
+ .np-page-main {
36
+ max-width: var(--np-max-width);
37
+ margin: 0 auto;
38
+ padding: 2rem 1.5rem;
39
+ }
40
+
41
+ .np-page-article h1 {
42
+ font-size: 2.25rem;
43
+ font-weight: 700;
44
+ line-height: 1.2;
45
+ margin: 0 0 2rem;
46
+ color: var(--np-color-heading);
47
+ }
48
+
49
+ .np-preview-body {
50
+ margin: 0;
51
+ font-family: var(--np-font-body);
52
+ color: var(--np-color-text);
53
+ background-color: var(--np-color-background);
54
+ padding: 1.5rem;
55
+ }
56
+
57
+ .np-preview-article h1 {
58
+ font-size: 2rem;
59
+ font-weight: 700;
60
+ line-height: 1.2;
61
+ margin: 0 0 1.5rem;
62
+ color: var(--np-color-heading);
63
+ }
64
+
65
+ /* Text blocks */
66
+ .np-content-text {
67
+ line-height: 1.75;
68
+ max-width: 65ch;
69
+ margin-bottom: 1.5rem;
70
+ }
71
+
72
+ .np-content-text p {
73
+ margin: 0 0 1rem;
74
+ }
75
+
76
+ .np-content-text a {
77
+ color: var(--np-color-primary);
78
+ text-decoration: underline;
79
+ }
80
+
81
+ .np-content-text ul,
82
+ .np-content-text ol {
83
+ padding-left: 1.5rem;
84
+ margin-bottom: 1rem;
85
+ }
86
+
87
+ /* Heading blocks */
88
+ .np-content-heading {
89
+ font-weight: 700;
90
+ line-height: 1.3;
91
+ margin: 2rem 0 1rem;
92
+ color: var(--np-color-heading);
93
+ }
94
+
95
+ h2.np-content-heading { font-size: 1.75rem; }
96
+ h3.np-content-heading { font-size: 1.5rem; }
97
+ h4.np-content-heading { font-size: 1.25rem; }
98
+ h5.np-content-heading { font-size: 1.125rem; }
99
+ h6.np-content-heading { font-size: 1rem; }
100
+
101
+ /* Image blocks */
102
+ .np-content-image {
103
+ margin: 1.5rem 0;
104
+ text-align: center;
105
+ }
106
+
107
+ .np-content-image img {
108
+ max-width: 100%;
109
+ height: auto;
110
+ border-radius: 0.375rem;
111
+ }
112
+
113
+ .np-content-image figcaption {
114
+ font-style: italic;
115
+ font-size: 0.875rem;
116
+ color: var(--np-color-muted);
117
+ margin-top: 0.5rem;
118
+ }
119
+
120
+ /* Video blocks */
121
+ .np-content-video {
122
+ margin: 1.5rem 0;
123
+ }
124
+
125
+ .np-content-video iframe,
126
+ .np-content-video video {
127
+ max-width: 100%;
128
+ width: 100%;
129
+ aspect-ratio: 16 / 9;
130
+ border-radius: 0.375rem;
131
+ }
132
+
133
+ .np-content-video figcaption {
134
+ font-style: italic;
135
+ font-size: 0.875rem;
136
+ color: var(--np-color-muted);
137
+ margin-top: 0.5rem;
138
+ }
139
+
140
+ /* Code blocks */
141
+ .np-content-code {
142
+ background-color: var(--np-color-code-bg);
143
+ color: var(--np-color-code-text);
144
+ padding: 1.25rem;
145
+ border-radius: 0.5rem;
146
+ overflow-x: auto;
147
+ font-family: var(--np-font-code);
148
+ font-size: 0.875rem;
149
+ line-height: 1.6;
150
+ margin: 1.5rem 0;
151
+ }
152
+
153
+ .np-content-code code {
154
+ background: none;
155
+ padding: 0;
156
+ font-size: inherit;
157
+ }
158
+
159
+ /* Quote blocks */
160
+ .np-content-quote {
161
+ border-left: 4px solid var(--np-color-primary);
162
+ padding: 1rem 1.5rem;
163
+ margin: 1.5rem 0;
164
+ font-style: italic;
165
+ color: var(--np-color-quote-text);
166
+ background-color: var(--np-color-quote-bg);
167
+ border-radius: 0 0.375rem 0.375rem 0;
168
+ }
169
+
170
+ .np-content-quote p {
171
+ margin: 0;
172
+ }
173
+
174
+ .np-content-quote cite {
175
+ display: block;
176
+ margin-top: 0.75rem;
177
+ font-size: 0.875rem;
178
+ font-style: normal;
179
+ font-weight: 500;
180
+ color: var(--np-color-muted);
181
+ }
182
+
183
+ /* Divider blocks */
184
+ .np-content-divider {
185
+ border: none;
186
+ border-top: 2px solid var(--np-color-border);
187
+ margin: 2rem 0;
188
+ }
189
+
190
+ /* HTML blocks */
191
+ .np-content-html {
192
+ margin: 1.5rem 0;
193
+ }
@@ -0,0 +1,117 @@
1
+ /* NotPressed Gallery Styles
2
+ * Styles for gallery content blocks and lightbox overlay.
3
+ */
4
+
5
+ /* Gallery grid */
6
+ .np-content-gallery {
7
+ display: grid;
8
+ grid-template-columns: repeat(var(--np-gallery-columns, 3), 1fr);
9
+ gap: 1rem;
10
+ margin: 1.5rem 0;
11
+ }
12
+
13
+ /* Gallery item */
14
+ .np-gallery-item {
15
+ overflow: hidden;
16
+ border-radius: 0.375rem;
17
+ cursor: pointer;
18
+ transition: transform 0.2s ease;
19
+ }
20
+
21
+ .np-gallery-item:hover {
22
+ transform: scale(1.03);
23
+ }
24
+
25
+ .np-gallery-item img {
26
+ width: 100%;
27
+ aspect-ratio: 1 / 1;
28
+ object-fit: cover;
29
+ display: block;
30
+ }
31
+
32
+ /* Gallery caption */
33
+ .np-gallery-caption {
34
+ text-align: center;
35
+ font-size: 0.875rem;
36
+ color: var(--np-color-muted, #64748b);
37
+ padding: 0.5rem 0.25rem;
38
+ }
39
+
40
+ /* Lightbox overlay */
41
+ .np-lightbox-overlay {
42
+ position: fixed;
43
+ top: 0;
44
+ left: 0;
45
+ width: 100%;
46
+ height: 100%;
47
+ background: rgba(0, 0, 0, 0.9);
48
+ z-index: 9999;
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ }
53
+
54
+ .np-lightbox-overlay[hidden] {
55
+ display: none;
56
+ }
57
+
58
+ /* Lightbox image */
59
+ .np-lightbox-image {
60
+ max-width: 90vw;
61
+ max-height: 85vh;
62
+ object-fit: contain;
63
+ }
64
+
65
+ /* Lightbox close button */
66
+ .np-lightbox-close {
67
+ position: absolute;
68
+ top: 1rem;
69
+ right: 1rem;
70
+ background: none;
71
+ border: none;
72
+ color: #fff;
73
+ font-size: 2rem;
74
+ cursor: pointer;
75
+ line-height: 1;
76
+ padding: 0.5rem;
77
+ opacity: 0.8;
78
+ transition: opacity 0.2s;
79
+ }
80
+
81
+ .np-lightbox-close:hover {
82
+ opacity: 1;
83
+ }
84
+
85
+ /* Lightbox navigation */
86
+ .np-lightbox-nav {
87
+ position: absolute;
88
+ top: 50%;
89
+ transform: translateY(-50%);
90
+ background: none;
91
+ border: none;
92
+ color: #fff;
93
+ font-size: 2.5rem;
94
+ cursor: pointer;
95
+ padding: 1rem;
96
+ opacity: 0.7;
97
+ transition: opacity 0.2s;
98
+ }
99
+
100
+ .np-lightbox-nav:hover {
101
+ opacity: 1;
102
+ }
103
+
104
+ .np-lightbox-nav--prev {
105
+ left: 1rem;
106
+ }
107
+
108
+ .np-lightbox-nav--next {
109
+ right: 1rem;
110
+ }
111
+
112
+ /* Responsive: single column on mobile */
113
+ @media (max-width: 639px) {
114
+ .np-content-gallery {
115
+ grid-template-columns: 1fr;
116
+ }
117
+ }
@@ -0,0 +1,118 @@
1
+ /* Starter Theme Styles */
2
+
3
+ /* Header */
4
+ .np-theme-header {
5
+ background-color: var(--np-color-background, #ffffff);
6
+ border-bottom: 1px solid var(--np-color-border, #e2e8f0);
7
+ padding: 1rem 0;
8
+ }
9
+
10
+ .np-theme-header-inner {
11
+ max-width: 1200px;
12
+ margin: 0 auto;
13
+ padding: 0 1.5rem;
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ }
18
+
19
+ .np-theme-site-name {
20
+ font-size: 1.25rem;
21
+ font-weight: 700;
22
+ color: var(--np-color-text, #1e293b);
23
+ text-decoration: none;
24
+ }
25
+
26
+ .np-theme-site-name:hover {
27
+ color: var(--np-color-primary, #4f46e5);
28
+ }
29
+
30
+ /* Main content */
31
+ .np-theme-main {
32
+ min-height: calc(100vh - 140px);
33
+ padding: 2rem 0;
34
+ }
35
+
36
+ .np-theme-content {
37
+ max-width: 720px;
38
+ margin: 0 auto;
39
+ padding: 0 1.5rem;
40
+ }
41
+
42
+ .np-theme-content--full-width {
43
+ max-width: 1200px;
44
+ margin: 0 auto;
45
+ padding: 0 1.5rem;
46
+ }
47
+
48
+ /* Sidebar layout */
49
+ .np-theme-layout-sidebar {
50
+ max-width: 1200px;
51
+ margin: 0 auto;
52
+ padding: 0 1.5rem;
53
+ display: grid;
54
+ grid-template-columns: 1fr 300px;
55
+ gap: 3rem;
56
+ }
57
+
58
+ .np-theme-sidebar {
59
+ border-left: 1px solid var(--np-color-border, #e2e8f0);
60
+ padding-left: 2rem;
61
+ }
62
+
63
+ /* Footer */
64
+ .np-theme-footer {
65
+ background-color: var(--np-color-surface, #f8fafc);
66
+ border-top: 1px solid var(--np-color-border, #e2e8f0);
67
+ padding: 2rem 0;
68
+ }
69
+
70
+ .np-theme-footer-inner {
71
+ max-width: 1200px;
72
+ margin: 0 auto;
73
+ padding: 0 1.5rem;
74
+ text-align: center;
75
+ color: var(--np-color-text_muted, #64748b);
76
+ font-size: 0.875rem;
77
+ }
78
+
79
+ .np-theme-footer-inner p {
80
+ margin: 0;
81
+ }
82
+
83
+ /* Links */
84
+ .np-theme-content a,
85
+ .np-theme-content--full-width a {
86
+ color: var(--np-color-primary, #4f46e5);
87
+ text-decoration: underline;
88
+ text-underline-offset: 2px;
89
+ }
90
+
91
+ .np-theme-content a:hover,
92
+ .np-theme-content--full-width a:hover {
93
+ color: var(--np-color-secondary, #7c3aed);
94
+ }
95
+
96
+ /* Responsive */
97
+ @media (max-width: 768px) {
98
+ .np-theme-layout-sidebar {
99
+ grid-template-columns: 1fr;
100
+ }
101
+
102
+ .np-theme-sidebar {
103
+ border-left: none;
104
+ border-top: 1px solid var(--np-color-border, #e2e8f0);
105
+ padding-left: 0;
106
+ padding-top: 2rem;
107
+ }
108
+
109
+ .np-theme-content,
110
+ .np-theme-content--full-width {
111
+ padding: 0 1rem;
112
+ }
113
+
114
+ .np-theme-header-inner,
115
+ .np-theme-footer-inner {
116
+ padding: 0 1rem;
117
+ }
118
+ }
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ class BaseController < NotPressed::ApplicationController
6
+ include NotPressed::Admin::Authentication
7
+
8
+ helper NotPressed::AdminHelper
9
+
10
+ layout "not_pressed/admin"
11
+
12
+ before_action :authenticate_admin!
13
+
14
+ private
15
+
16
+ def page_title(title)
17
+ @page_title = title
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ class CategoriesController < BaseController
6
+ before_action :set_category, only: %i[update destroy]
7
+
8
+ def index
9
+ page_title "Categories"
10
+ @categories = NotPressed::Category.ordered
11
+ @category = NotPressed::Category.new
12
+ end
13
+
14
+ def create
15
+ @category = NotPressed::Category.new(category_params)
16
+
17
+ if @category.save
18
+ redirect_to admin_categories_path, notice: "Category created successfully."
19
+ else
20
+ page_title "Categories"
21
+ @categories = NotPressed::Category.ordered
22
+ render :index, status: :unprocessable_entity
23
+ end
24
+ end
25
+
26
+ def update
27
+ if @category.update(category_params)
28
+ redirect_to admin_categories_path, notice: "Category updated successfully."
29
+ else
30
+ page_title "Categories"
31
+ @categories = NotPressed::Category.ordered
32
+ @category = NotPressed::Category.new
33
+ render :index, status: :unprocessable_entity
34
+ end
35
+ end
36
+
37
+ def destroy
38
+ @category.destroy
39
+ redirect_to admin_categories_path, notice: "Category deleted successfully."
40
+ end
41
+
42
+ private
43
+
44
+ def set_category
45
+ @category = NotPressed::Category.find(params[:id])
46
+ end
47
+
48
+ def category_params
49
+ params.require(:category).permit(:name, :description)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ class ContentBlocksController < BaseController
6
+ before_action :set_page
7
+ before_action :set_block, only: %i[update destroy]
8
+
9
+ def create
10
+ next_position = @page.content_blocks.maximum(:position).to_i + 1
11
+ @block = @page.content_blocks.build(
12
+ block_type: params[:block_type],
13
+ content: NotPressed::ContentBlock.default_content_for(params[:block_type]),
14
+ position: next_position
15
+ )
16
+
17
+ respond_to do |format|
18
+ if @block.save
19
+ format.turbo_stream
20
+ format.html { redirect_to edit_admin_page_path(@page), notice: "Block added." }
21
+ else
22
+ format.html { redirect_to edit_admin_page_path(@page), alert: "Could not add block." }
23
+ end
24
+ end
25
+ end
26
+
27
+ def update
28
+ respond_to do |format|
29
+ if @block.update(block_params)
30
+ format.turbo_stream { head :ok }
31
+ format.html { redirect_to edit_admin_page_path(@page), notice: "Block updated." }
32
+ else
33
+ format.html { redirect_to edit_admin_page_path(@page), alert: "Could not update block." }
34
+ end
35
+ end
36
+ end
37
+
38
+ def destroy
39
+ @block.destroy
40
+
41
+ respond_to do |format|
42
+ format.turbo_stream
43
+ format.html { redirect_to edit_admin_page_path(@page), notice: "Block removed." }
44
+ end
45
+ end
46
+
47
+ def reorder
48
+ (params[:block_ids] || []).each_with_index do |block_id, index|
49
+ @page.content_blocks.where(id: block_id).update_all(position: index)
50
+ end
51
+
52
+ respond_to do |format|
53
+ format.turbo_stream { head :ok }
54
+ format.html { redirect_to edit_admin_page_path(@page), notice: "Blocks reordered." }
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def set_page
61
+ @page = NotPressed::Page.find(params[:page_id])
62
+ end
63
+
64
+ def set_block
65
+ @block = @page.content_blocks.find(params[:id])
66
+ end
67
+
68
+ def block_params
69
+ params.require(:content_block).permit(:position, content: {})
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ class DashboardController < BaseController
6
+ def index
7
+ @page_title = "Dashboard"
8
+ @total_pages = NotPressed::Page.count
9
+ @published_pages = NotPressed::Page.published.count
10
+ @draft_pages = NotPressed::Page.draft.count
11
+ @total_blocks = NotPressed::ContentBlock.count
12
+ @total_media = NotPressed::MediaAttachment.count
13
+ @total_forms = NotPressed::Form.count
14
+ @active_forms = NotPressed::Form.status_active.count
15
+ @recent_pages = NotPressed::Page.order(updated_at: :desc).limit(10)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ class FormsController < BaseController
6
+ before_action :set_form, only: %i[edit update destroy submissions export_csv]
7
+
8
+ def index
9
+ page_title "Forms"
10
+ @forms = NotPressed::Form.ordered
11
+ end
12
+
13
+ def new
14
+ page_title "New Form"
15
+ @form = NotPressed::Form.new
16
+ end
17
+
18
+ def create
19
+ @form = NotPressed::Form.new(form_params)
20
+
21
+ if @form.save
22
+ redirect_to edit_admin_form_path(@form), notice: "Form created successfully."
23
+ else
24
+ page_title "New Form"
25
+ render :new, status: :unprocessable_entity
26
+ end
27
+ end
28
+
29
+ def edit
30
+ page_title "Edit: #{@form.name}"
31
+ @form_fields = @form.form_fields.ordered
32
+ end
33
+
34
+ def update
35
+ if @form.update(form_params)
36
+ redirect_to edit_admin_form_path(@form), notice: "Form updated successfully."
37
+ else
38
+ page_title "Edit: #{@form.name}"
39
+ render :edit, status: :unprocessable_entity
40
+ end
41
+ end
42
+
43
+ def destroy
44
+ @form.destroy
45
+ redirect_to admin_forms_path, notice: "Form deleted successfully."
46
+ end
47
+
48
+ def submissions
49
+ page_title "Submissions: #{@form.name}"
50
+ @submissions = @form.form_submissions.recent
51
+ @field_labels = @form.form_fields.ordered.pluck(:label)
52
+ end
53
+
54
+ def export_csv
55
+ @submissions = @form.form_submissions.recent
56
+ field_labels = @form.form_fields.ordered.pluck(:label)
57
+
58
+ csv_data = generate_csv(@submissions, field_labels)
59
+ send_data csv_data, filename: "#{@form.slug}-submissions-#{Date.current}.csv", type: "text/csv"
60
+ end
61
+
62
+ private
63
+
64
+ def set_form
65
+ @form = NotPressed::Form.find(params[:id])
66
+ end
67
+
68
+ def generate_csv(submissions, field_labels)
69
+ require "csv"
70
+ CSV.generate(headers: true) do |csv|
71
+ csv << (field_labels + ["Submitted At"])
72
+ submissions.each do |submission|
73
+ row = field_labels.map { |label| submission.data[label] }
74
+ row << submission.submitted_at&.strftime("%Y-%m-%d %H:%M:%S")
75
+ csv << row
76
+ end
77
+ end
78
+ end
79
+
80
+ def form_params
81
+ params.require(:form).permit(:name, :description, :email_recipient, :redirect_url, :success_message, :status,
82
+ form_fields_attributes: [:id, :label, :field_type, :required, :placeholder, :options, :position, :_destroy])
83
+ end
84
+ end
85
+ end
86
+ end