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,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class Page < ActiveRecord::Base
5
+ self.table_name = "not_pressed_pages"
6
+
7
+ belongs_to :parent, class_name: "NotPressed::Page", optional: true, inverse_of: :children
8
+ has_many :children, class_name: "NotPressed::Page", foreign_key: :parent_id,
9
+ dependent: :destroy, inverse_of: :parent
10
+ has_many :content_blocks, class_name: "NotPressed::ContentBlock",
11
+ dependent: :destroy, inverse_of: :page
12
+ has_many :page_versions, class_name: "NotPressed::PageVersion",
13
+ dependent: :destroy
14
+ belongs_to :category, class_name: "NotPressed::Category", optional: true, inverse_of: :pages
15
+ has_many :taggings, class_name: "NotPressed::Tagging", foreign_key: :page_id,
16
+ dependent: :destroy, inverse_of: :page
17
+ has_many :tags, through: :taggings
18
+
19
+ attr_accessor :skip_versioning, :page_password
20
+
21
+ # Hook system callbacks
22
+ after_create { NotPressed::Hooks.fire(:after_page_create, self) }
23
+ before_save :fire_before_page_hooks
24
+ after_save :fire_after_page_hooks
25
+ after_destroy { NotPressed::Hooks.fire(:after_page_destroy, self) }
26
+
27
+ enum :status, { draft: 0, published: 1, archived: 2, scheduled: 3 }
28
+ enum :visibility, { public_page: 0, unlisted: 1, password_protected: 2 }, prefix: :visibility
29
+
30
+ validates :title, presence: true
31
+ validates :content_type, inclusion: { in: ->(_) { NotPressed::ContentTypeRegistry.names } }
32
+ validates :slug, presence: true,
33
+ uniqueness: true,
34
+ format: { with: /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/, message: "must be lowercase alphanumeric with hyphens" }
35
+ validates :scheduled_at, presence: true, if: :scheduled?
36
+ validates :layout, inclusion: { in: ->(_) { NotPressed.configuration.available_layouts } }, allow_nil: true
37
+ validates :password_digest, presence: true, if: :visibility_password_protected?
38
+ validate :scheduled_at_must_be_future, if: :scheduled?
39
+ validate :scheduled_at_must_be_nil_unless_scheduled
40
+
41
+ before_validation :generate_slug, on: :create
42
+ before_validation :hash_page_password
43
+ before_save :set_published_at
44
+ after_save :create_version_snapshot, unless: :skip_versioning
45
+ after_save :invalidate_navigation_cache
46
+ after_destroy :invalidate_navigation_cache
47
+
48
+ scope :of_type, ->(type) { where(content_type: type) }
49
+ scope :roots, -> { where(parent_id: nil) }
50
+ scope :ordered, -> { order(position: :asc) }
51
+ scope :visible, -> { where(status: :published) }
52
+ scope :publicly_accessible, -> { where(visibility: [:public_page, :unlisted]) }
53
+ scope :needs_publishing, -> { where(status: :scheduled).where("scheduled_at <= ?", Time.current) }
54
+ scope :blog_posts, -> { of_type("blog_post") }
55
+
56
+ def authenticate_page_password(password)
57
+ return false if password_digest.blank?
58
+
59
+ salt, stored_hash = password_digest.split(":", 2)
60
+ Digest::SHA256.hexdigest("#{salt}:#{password}") == stored_hash
61
+ end
62
+
63
+ def meta_title_or_title
64
+ meta_title.presence || title
65
+ end
66
+
67
+ def effective_canonical_url(request)
68
+ canonical_url.presence || request.original_url
69
+ end
70
+
71
+ def meta_robots_tag
72
+ return meta_robots if meta_robots.present?
73
+ return "noindex" if draft? || archived?
74
+
75
+ nil
76
+ end
77
+
78
+ def self.publish_scheduled!
79
+ needs_publishing.find_each do |page|
80
+ page.update!(status: :published, scheduled_at: nil)
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def fire_before_page_hooks
87
+ NotPressed::Hooks.fire(:before_page_save, self)
88
+ if status_changed? && published?
89
+ @_publishing = true
90
+ NotPressed::Hooks.fire(:before_page_publish, self)
91
+ else
92
+ @_publishing = false
93
+ end
94
+ end
95
+
96
+ def fire_after_page_hooks
97
+ NotPressed::Hooks.fire(:after_page_save, self)
98
+ if @_publishing
99
+ NotPressed::Hooks.fire(:after_page_publish, self)
100
+ @_publishing = false
101
+ end
102
+ end
103
+
104
+ def hash_page_password
105
+ if page_password.present?
106
+ salt = SecureRandom.hex(16)
107
+ hash = Digest::SHA256.hexdigest("#{salt}:#{page_password}")
108
+ self.password_digest = "#{salt}:#{hash}"
109
+ end
110
+ end
111
+
112
+ def set_published_at
113
+ if status_changed? && published?
114
+ self.published_at ||= Time.current
115
+ elsif status_changed? && status_was == "published"
116
+ self.published_at = nil
117
+ end
118
+ end
119
+
120
+ def create_version_snapshot
121
+ event = if previously_new_record?
122
+ "created"
123
+ elsif saved_change_to_status?
124
+ status
125
+ elsif saved_change_to_title? || saved_change_to_slug?
126
+ "updated"
127
+ else
128
+ return
129
+ end
130
+
131
+ snapshot = {
132
+ title: title,
133
+ slug: slug,
134
+ status: status,
135
+ meta_description: meta_description,
136
+ content_type: content_type,
137
+ published_at: published_at,
138
+ blocks: content_blocks.ordered.map { |b| { block_type: b.block_type, content: b.content, position: b.position } }
139
+ }
140
+
141
+ page_versions.create!(
142
+ version_number: (page_versions.maximum(:version_number).to_i + 1),
143
+ snapshot: snapshot,
144
+ event: event
145
+ )
146
+ end
147
+
148
+ def invalidate_navigation_cache
149
+ NotPressed.invalidate_navigation!
150
+ end
151
+
152
+ def generate_slug
153
+ return if slug.present?
154
+ return if title.blank?
155
+
156
+ base_slug = title.encode("UTF-8").parameterize
157
+ candidate = base_slug
158
+ counter = 2
159
+
160
+ while self.class.where(slug: candidate).exists?
161
+ candidate = "#{base_slug}-#{counter}"
162
+ counter += 1
163
+ end
164
+
165
+ self.slug = candidate
166
+ end
167
+
168
+ def scheduled_at_must_be_future
169
+ return if scheduled_at.blank?
170
+
171
+ if scheduled_at <= Time.current
172
+ errors.add(:scheduled_at, "must be in the future")
173
+ end
174
+ end
175
+
176
+ def scheduled_at_must_be_nil_unless_scheduled
177
+ return if scheduled? || scheduled_at.blank?
178
+
179
+ errors.add(:scheduled_at, "must be blank unless status is scheduled")
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class PageVersion < ActiveRecord::Base
5
+ self.table_name = "not_pressed_page_versions"
6
+
7
+ belongs_to :page, class_name: "NotPressed::Page"
8
+
9
+ validates :version_number, presence: true
10
+ validates :snapshot, presence: true
11
+ validates :event, presence: true
12
+
13
+ scope :ordered, -> { order(version_number: :desc) }
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class Setting < ActiveRecord::Base
5
+ self.table_name = "not_pressed_settings"
6
+
7
+ validates :key, presence: true, uniqueness: true
8
+
9
+ def self.get(key)
10
+ find_by(key: key)&.value
11
+ end
12
+
13
+ def self.set(key, value)
14
+ record = find_or_initialize_by(key: key)
15
+ record.value = value
16
+ record.save!
17
+ record
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class Tag < ActiveRecord::Base
5
+ self.table_name = "not_pressed_tags"
6
+
7
+ include NotPressed::Sluggable
8
+
9
+ has_many :taggings, class_name: "NotPressed::Tagging", foreign_key: :tag_id,
10
+ dependent: :destroy, inverse_of: :tag
11
+ has_many :pages, through: :taggings
12
+
13
+ validates :name, presence: true, uniqueness: true
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class Tagging < ActiveRecord::Base
5
+ self.table_name = "not_pressed_taggings"
6
+
7
+ belongs_to :tag, class_name: "NotPressed::Tag", inverse_of: :taggings
8
+ belongs_to :page, class_name: "NotPressed::Page", inverse_of: :taggings
9
+
10
+ validates :tag_id, uniqueness: { scope: :page_id }
11
+ end
12
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class AnalyticsPlugin < Plugin
5
+ name "Analytics"
6
+ version "1.0.0"
7
+ description "Add analytics tracking to your site"
8
+ author "NotPressed"
9
+
10
+ settings do
11
+ string :tracking_id, label: "Tracking ID", required: true
12
+ select :provider, label: "Provider", options: %w[google_analytics plausible umami], default: "google_analytics"
13
+ boolean :anonymize_ip, label: "Anonymize IP", default: true
14
+ end
15
+
16
+ def activate!
17
+ plugin_instance = self
18
+
19
+ @page_content_filter = proc do |html, _page|
20
+ plugin_instance.inject_analytics(html)
21
+ end
22
+
23
+ @after_publish_hook = proc do |page|
24
+ Rails.logger.info "[Analytics] Page published: #{page.respond_to?(:title) ? page.title : page}"
25
+ end
26
+
27
+ NotPressed::Hooks.register_filter(:page_content, &@page_content_filter)
28
+ NotPressed::Hooks.register(:after_page_publish, &@after_publish_hook)
29
+
30
+ self.class.plugin_block_types.each do |bt|
31
+ NotPressed::ContentBlock.register_type(bt[:name].to_s)
32
+ end
33
+ self.class.plugin_admin_menus.each do |menu|
34
+ NotPressed::Admin::MenuRegistry.register(
35
+ menu[:label],
36
+ path: menu[:path],
37
+ icon: menu[:icon],
38
+ section: :main,
39
+ position: 100
40
+ )
41
+ end
42
+
43
+ @active = true
44
+ end
45
+
46
+ def deactivate!
47
+ NotPressed::Hooks.unregister_filter(:page_content, @page_content_filter) if @page_content_filter
48
+ NotPressed::Hooks.unregister(:after_page_publish, @after_publish_hook) if @after_publish_hook
49
+
50
+ self.class.plugin_block_types.each do |bt|
51
+ NotPressed::ContentBlock.custom_types.delete(bt[:name].to_s)
52
+ end
53
+ self.class.plugin_admin_menus.each do |menu|
54
+ NotPressed::Admin::MenuRegistry.unregister(menu[:label], path: menu[:path])
55
+ end
56
+
57
+ @active = false
58
+ end
59
+
60
+ def inject_analytics(html)
61
+ tracking_id = get_setting(:tracking_id)
62
+ return html if tracking_id.nil? || tracking_id.to_s.strip.empty?
63
+
64
+ provider = get_setting(:provider) || "google_analytics"
65
+ script = analytics_script(provider, tracking_id)
66
+ "#{html}#{script}"
67
+ end
68
+
69
+ private
70
+
71
+ def analytics_script(provider, tracking_id)
72
+ case provider.to_s
73
+ when "google_analytics"
74
+ google_analytics_script(tracking_id)
75
+ when "plausible"
76
+ plausible_script(tracking_id)
77
+ when "umami"
78
+ umami_script(tracking_id)
79
+ else
80
+ ""
81
+ end
82
+ end
83
+
84
+ def google_analytics_script(tracking_id)
85
+ anonymize = get_setting(:anonymize_ip)
86
+ anonymize_config = anonymize.to_s == "true" ? ", { 'anonymize_ip': true }" : ""
87
+ <<~HTML
88
+ <script async src="https://www.googletagmanager.com/gtag/js?id=#{ERB::Util.html_escape(tracking_id)}"></script>
89
+ <script>
90
+ window.dataLayer = window.dataLayer || [];
91
+ function gtag(){dataLayer.push(arguments);}
92
+ gtag('js', new Date());
93
+ gtag('config', '#{ERB::Util.html_escape(tracking_id)}'#{anonymize_config});
94
+ </script>
95
+ HTML
96
+ end
97
+
98
+ def plausible_script(tracking_id)
99
+ %(<script defer data-domain="#{ERB::Util.html_escape(tracking_id)}" src="https://plausible.io/js/script.js"></script>\n)
100
+ end
101
+
102
+ def umami_script(tracking_id)
103
+ %(<script async src="https://analytics.umami.is/script.js" data-website-id="#{ERB::Util.html_escape(tracking_id)}"></script>\n)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class CalloutBlockPlugin < Plugin
5
+ name "Callout Block"
6
+ version "1.0.0"
7
+ description "Adds a styled callout/alert block type"
8
+ author "NotPressed"
9
+
10
+ block_type :callout, label: "Callout", icon: "megaphone"
11
+
12
+ STYLES = %w[info warning success danger].freeze
13
+
14
+ DEFAULT_CONTENT = { "title" => "", "text" => "", "style" => "info" }.freeze
15
+
16
+ CALLOUT_RENDERER = lambda do |content, _block|
17
+ style = STYLES.include?(content["style"].to_s) ? content["style"].to_s : "info"
18
+ title = ERB::Util.html_escape(content["title"].to_s)
19
+ text = ERB::Util.html_escape(content["text"].to_s)
20
+
21
+ <<~HTML.strip
22
+ <div class="np-content-callout np-callout-#{style}">
23
+ <div class="np-callout-title">#{title}</div>
24
+ <div class="np-callout-text">#{text}</div>
25
+ </div>
26
+ HTML
27
+ end
28
+
29
+ def activate!
30
+ super
31
+
32
+ NotPressed::Rendering::RendererRegistry.register("callout", CALLOUT_RENDERER)
33
+ NotPressed::Rendering::RendererRegistry.register_editor("callout", "not_pressed/admin/content_blocks/editors/callout")
34
+ NotPressed::Rendering::RendererRegistry.register_default_content("callout", DEFAULT_CONTENT.dup)
35
+ end
36
+
37
+ def deactivate!
38
+ super
39
+
40
+ NotPressed::Rendering::RendererRegistry.unregister("callout")
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NotPressed::StarterTheme < NotPressed::Theme
4
+ name "Starter"
5
+ version "1.0.0"
6
+ description "A clean, minimal theme for NotPressed. Responsive layouts with customizable colors."
7
+ author "NotPressed"
8
+
9
+ layout :default, label: "Default", primary: true
10
+ layout :full_width, label: "Full Width"
11
+ layout :sidebar, label: "With Sidebar"
12
+
13
+ stylesheet "not_pressed/themes/starter.css"
14
+ templates_path File.expand_path("../../../views/themes/starter", __dir__)
15
+
16
+ color_scheme do
17
+ color :primary, "#4f46e5", label: "Primary"
18
+ color :secondary, "#7c3aed", label: "Secondary"
19
+ color :background, "#ffffff", label: "Background"
20
+ color :surface, "#f8fafc", label: "Surface"
21
+ color :text, "#1e293b", label: "Text"
22
+ color :text_muted, "#64748b", label: "Muted Text"
23
+ color :accent, "#06b6d4", label: "Accent"
24
+ color :border, "#e2e8f0", label: "Border"
25
+ end
26
+ end