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,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ class MediaAttachmentsController < BaseController
6
+ PER_PAGE = 25
7
+
8
+ before_action :set_media_attachment, only: %i[edit update destroy]
9
+
10
+ def picker
11
+ @media = NotPressed::MediaAttachment.images.recent.limit(50)
12
+ render partial: "picker", layout: false
13
+ end
14
+
15
+ def index
16
+ page_title "Media Library"
17
+ @media = filtered_media
18
+ .recent
19
+ .limit(PER_PAGE)
20
+ .offset(page_offset)
21
+ @total_count = filtered_media.count
22
+ @current_page = current_page
23
+ @total_pages = (@total_count.to_f / PER_PAGE).ceil
24
+ end
25
+
26
+ def create
27
+ files = Array(params[:files])
28
+
29
+ files.each do |file|
30
+ attachment = NotPressed::MediaAttachment.new
31
+ attachment.file.attach(file)
32
+ attachment.title = file.original_filename.sub(/\.[^.]+\z/, "").titleize
33
+ attachment.save
34
+ end
35
+
36
+ redirect_to admin_media_attachments_path, notice: "Media uploaded successfully."
37
+ end
38
+
39
+ def edit
40
+ page_title "Edit Media: #{@media_attachment.title}"
41
+ end
42
+
43
+ def update
44
+ if @media_attachment.update(media_attachment_params)
45
+ redirect_to edit_admin_media_attachment_path(@media_attachment), notice: "Media updated successfully."
46
+ else
47
+ page_title "Edit Media: #{@media_attachment.title}"
48
+ render :edit, status: :unprocessable_entity
49
+ end
50
+ end
51
+
52
+ def destroy
53
+ @media_attachment.destroy
54
+ redirect_to admin_media_attachments_path, notice: "Media deleted successfully."
55
+ end
56
+
57
+ private
58
+
59
+ def filtered_media
60
+ scope = NotPressed::MediaAttachment.all
61
+
62
+ case params[:type]
63
+ when "images"
64
+ scope = scope.images
65
+ when "documents"
66
+ scope = scope.documents
67
+ end
68
+
69
+ if params[:q].present?
70
+ query = "%#{params[:q]}%"
71
+ scope = scope.where("title LIKE :q OR alt_text LIKE :q", q: query)
72
+ end
73
+
74
+ scope
75
+ end
76
+
77
+ def current_page
78
+ [(params[:page] || 1).to_i, 1].max
79
+ end
80
+
81
+ def page_offset
82
+ (current_page - 1) * PER_PAGE
83
+ end
84
+
85
+ def set_media_attachment
86
+ @media_attachment = NotPressed::MediaAttachment.find(params[:id])
87
+ end
88
+
89
+ def media_attachment_params
90
+ params.require(:media_attachment).permit(:title, :alt_text)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ class PagesController < BaseController
6
+ before_action :set_page, only: %i[edit update destroy preview duplicate]
7
+
8
+ def index
9
+ page_title "Pages"
10
+ @pages = NotPressed::Page.roots.ordered.includes(children: :children)
11
+ end
12
+
13
+ def new
14
+ page_title "New Page"
15
+ @page = NotPressed::Page.new
16
+ end
17
+
18
+ def create
19
+ @page = NotPressed::Page.new(page_params)
20
+
21
+ if @page.save
22
+ redirect_to edit_admin_page_path(@page), notice: "Page created successfully."
23
+ else
24
+ page_title "New Page"
25
+ render :new, status: :unprocessable_entity
26
+ end
27
+ end
28
+
29
+ def edit
30
+ page_title "Edit: #{@page.title}"
31
+ @blocks = @page.content_blocks.ordered
32
+ @versions = @page.page_versions.ordered.limit(10)
33
+ end
34
+
35
+ def update
36
+ if @page.update(page_params)
37
+ redirect_to edit_admin_page_path(@page), notice: "Page updated successfully."
38
+ else
39
+ page_title "Edit: #{@page.title}"
40
+ @blocks = @page.content_blocks.ordered
41
+ render :edit, status: :unprocessable_entity
42
+ end
43
+ end
44
+
45
+ def preview
46
+ render layout: false
47
+ end
48
+
49
+ def duplicate
50
+ new_page = NotPressed::Page.new(
51
+ title: "#{@page.title} (Copy)",
52
+ status: :draft,
53
+ content_type: @page.content_type,
54
+ meta_description: @page.meta_description,
55
+ parent_id: @page.parent_id,
56
+ position: @page.position + 1
57
+ )
58
+
59
+ if new_page.save
60
+ @page.content_blocks.ordered.each do |block|
61
+ new_page.content_blocks.create!(
62
+ block_type: block.block_type,
63
+ content: block.content,
64
+ position: block.position
65
+ )
66
+ end
67
+
68
+ redirect_to edit_admin_page_path(new_page), notice: "Page duplicated successfully."
69
+ else
70
+ redirect_to admin_pages_path, alert: "Failed to duplicate page."
71
+ end
72
+ end
73
+
74
+ def reorder
75
+ entries = params.permit(pages: [:id, :position, :parent_id]).fetch(:pages, [])
76
+
77
+ ActiveRecord::Base.transaction do
78
+ entries.each do |entry|
79
+ page = NotPressed::Page.find_by(id: entry[:id])
80
+ next unless page
81
+
82
+ new_parent_id = entry[:parent_id].presence
83
+ if new_parent_id.present?
84
+ next if new_parent_id.to_i == page.id
85
+ next if descendant_ids(page).include?(new_parent_id.to_i)
86
+ end
87
+
88
+ page.update!(position: entry[:position], parent_id: new_parent_id)
89
+ end
90
+ end
91
+
92
+ head :ok
93
+ end
94
+
95
+ def destroy
96
+ @page.destroy
97
+ redirect_to admin_pages_path, notice: "Page deleted successfully."
98
+ end
99
+
100
+ private
101
+
102
+ def set_page
103
+ @page = NotPressed::Page.find(params[:id])
104
+ end
105
+
106
+ def descendant_ids(page)
107
+ ids = []
108
+ children = page.children.to_a
109
+ while children.any?
110
+ child = children.shift
111
+ ids << child.id
112
+ children.concat(child.children.to_a)
113
+ end
114
+ ids
115
+ end
116
+
117
+ def page_params
118
+ params.require(:page).permit(:title, :slug, :status, :meta_description, :content_type, :parent_id, :position, :scheduled_at, :meta_title, :og_image_url, :layout, :visibility, :page_password, :canonical_url, :meta_robots, :og_type, :twitter_card, :head_code, :body_code, :category_id, tag_ids: [])
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ class PluginsController < BaseController
6
+ before_action :set_plugin, only: [:show, :toggle, :update_settings]
7
+
8
+ def index
9
+ page_title "Plugins"
10
+ @plugins = NotPressed::PluginManager.registered
11
+ end
12
+
13
+ def export
14
+ info = NotPressed::PluginManager.find(params[:id])
15
+ plugin_name = info[:klass].plugin_name
16
+
17
+ temp_dir = Dir.mktmpdir("np_plugin_export")
18
+ zip_path = NotPressed::PluginPackager.new(plugin_name).package(temp_dir)
19
+
20
+ send_file zip_path,
21
+ type: "application/zip",
22
+ disposition: "attachment",
23
+ filename: File.basename(zip_path)
24
+
25
+ # Schedule cleanup after response is sent
26
+ response.headers["X-Temp-Dir"] = temp_dir
27
+ rescue NotPressed::PluginManager::NotFound
28
+ redirect_to admin_plugins_path, alert: "Plugin not found."
29
+ end
30
+
31
+ def import
32
+ unless params[:plugin_archive].present?
33
+ redirect_to admin_plugins_path, alert: "Please select a plugin archive to import."
34
+ return
35
+ end
36
+
37
+ temp_file = Tempfile.new(["plugin_import", ".zip"])
38
+ temp_file.binmode
39
+ temp_file.write(params[:plugin_archive].read)
40
+ temp_file.close
41
+
42
+ result = NotPressed::PluginImporter.new(temp_file.path).import!
43
+ redirect_to admin_plugins_path, notice: "Plugin '#{result[:name]}' v#{result[:version]} imported successfully."
44
+ rescue NotPressed::PluginImporter::InvalidArchive => e
45
+ redirect_to admin_plugins_path, alert: "Import failed: #{e.message}"
46
+ rescue NotPressed::PluginImporter::DuplicatePlugin => e
47
+ redirect_to admin_plugins_path, alert: "Import failed: #{e.message}"
48
+ ensure
49
+ temp_file&.unlink
50
+ end
51
+
52
+ def show
53
+ page_title @plugin_class.plugin_name
54
+ end
55
+
56
+ def toggle
57
+ name = @plugin_class.plugin_name
58
+ if NotPressed::PluginManager.enabled?(name)
59
+ NotPressed::PluginManager.disable(name)
60
+ redirect_to admin_plugin_path(plugin_name: name), notice: "Plugin '#{name}' has been disabled."
61
+ else
62
+ NotPressed::PluginManager.enable(name)
63
+ redirect_to admin_plugin_path(plugin_name: name), notice: "Plugin '#{name}' has been enabled."
64
+ end
65
+ rescue NotPressed::PluginManager::DependencyError => e
66
+ redirect_to admin_plugin_path(plugin_name: name), alert: e.message
67
+ end
68
+
69
+ def update_settings
70
+ name = @plugin_class.plugin_name
71
+ schema = @plugin_class.plugin_settings_schema || []
72
+ instance = plugin_instance
73
+
74
+ errors = []
75
+ schema.each do |field|
76
+ key = field[:key].to_s
77
+ value = params.dig(:plugin_settings, key)
78
+
79
+ if field[:required] && (value.nil? || value.to_s.strip.empty?)
80
+ errors << "#{field[:label] || key.humanize} is required"
81
+ next
82
+ end
83
+
84
+ value = cast_setting_value(value, field[:type])
85
+ instance.set_setting(key, value)
86
+ end
87
+
88
+ if errors.any?
89
+ redirect_to admin_plugin_path(plugin_name: name), alert: errors.join(", ")
90
+ else
91
+ redirect_to admin_plugin_path(plugin_name: name), notice: "Settings saved successfully."
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def set_plugin
98
+ info = NotPressed::PluginManager.find(params[:plugin_name])
99
+ @plugin_class = info[:klass]
100
+ @plugin_info = info
101
+ rescue NotPressed::PluginManager::NotFound
102
+ redirect_to admin_plugins_path, alert: "Plugin not found."
103
+ end
104
+
105
+ def plugin_instance
106
+ @plugin_info[:instance] || @plugin_class.new
107
+ end
108
+
109
+ def cast_setting_value(value, type)
110
+ case type
111
+ when :boolean
112
+ value == "1" || value == "true"
113
+ when :integer
114
+ value.to_s.strip.empty? ? nil : value.to_i
115
+ else
116
+ value
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ class SettingsController < BaseController
6
+ def code_injection
7
+ page_title "Code Injection"
8
+ @global_head_code = NotPressed::Setting.get("global_head_code") || ""
9
+ @global_body_code = NotPressed::Setting.get("global_body_code") || ""
10
+ end
11
+
12
+ def update_code_injection
13
+ NotPressed::Setting.set("global_head_code", params[:global_head_code])
14
+ NotPressed::Setting.set("global_body_code", params[:global_body_code])
15
+ redirect_to code_injection_admin_settings_path, notice: "Code injection settings saved."
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ class TagsController < BaseController
6
+ def index
7
+ page_title "Tags"
8
+ @tags = NotPressed::Tag.order(:name)
9
+ @tag = NotPressed::Tag.new
10
+ end
11
+
12
+ def create
13
+ @tag = NotPressed::Tag.new(tag_params)
14
+
15
+ if @tag.save
16
+ redirect_to admin_tags_path, notice: "Tag created successfully."
17
+ else
18
+ page_title "Tags"
19
+ @tags = NotPressed::Tag.order(:name)
20
+ render :index, status: :unprocessable_entity
21
+ end
22
+ end
23
+
24
+ def destroy
25
+ @tag = NotPressed::Tag.find(params[:id])
26
+ @tag.destroy
27
+ redirect_to admin_tags_path, notice: "Tag deleted successfully."
28
+ end
29
+
30
+ private
31
+
32
+ def tag_params
33
+ params.require(:tag).permit(:name)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ module Admin
5
+ class ThemesController < BaseController
6
+ before_action :set_theme, only: [:show, :activate, :deactivate, :update_colors]
7
+
8
+ def index
9
+ page_title "Themes"
10
+ @themes = NotPressed::ThemeRegistry.available.map { |name| [name, NotPressed::ThemeRegistry.find(name)] }
11
+ @active_theme = NotPressed::ThemeRegistry.active
12
+ end
13
+
14
+ def show
15
+ page_title @theme_class.plugin_name
16
+ @colors = @theme_class.theme_color_scheme
17
+ @color_values = load_color_values
18
+ end
19
+
20
+ def activate
21
+ name = @theme_class.plugin_name
22
+ NotPressed::ThemeRegistry.activate(name)
23
+ redirect_to admin_themes_path, notice: "Theme '#{name}' has been activated."
24
+ end
25
+
26
+ def deactivate
27
+ NotPressed::ThemeRegistry.deactivate
28
+ redirect_to admin_themes_path, notice: "Theme has been deactivated."
29
+ end
30
+
31
+ def update_colors
32
+ name = @theme_class.plugin_name
33
+ colors = @theme_class.theme_color_scheme
34
+
35
+ colors.each do |color|
36
+ key = "theme.#{name}.color_#{color[:key]}"
37
+ value = params.dig(:theme_colors, color[:key].to_s)
38
+ if value.present?
39
+ NotPressed::Setting.set(key, value)
40
+ end
41
+ end
42
+
43
+ redirect_to admin_theme_path(theme_name: name), notice: "Colors saved successfully."
44
+ end
45
+
46
+ def export
47
+ info = NotPressed::ThemeRegistry.find(params[:id])
48
+ theme_name = info[:klass].plugin_name
49
+
50
+ temp_dir = Dir.mktmpdir("np_theme_export")
51
+ zip_path = NotPressed::ThemePackager.new(theme_name).package(temp_dir)
52
+
53
+ send_file zip_path,
54
+ type: "application/zip",
55
+ disposition: "attachment",
56
+ filename: File.basename(zip_path)
57
+
58
+ # Schedule cleanup after response is sent
59
+ response.headers["X-Temp-Dir"] = temp_dir
60
+ rescue NotPressed::ThemeRegistry::NotFound
61
+ redirect_to admin_themes_path, alert: "Theme not found."
62
+ end
63
+
64
+ def import
65
+ unless params[:theme_archive].present?
66
+ redirect_to admin_themes_path, alert: "Please select a theme archive to import."
67
+ return
68
+ end
69
+
70
+ temp_file = Tempfile.new(["theme_import", ".zip"])
71
+ temp_file.binmode
72
+ temp_file.write(params[:theme_archive].read)
73
+ temp_file.close
74
+
75
+ result = NotPressed::ThemeImporter.new(temp_file.path).import!
76
+ redirect_to admin_themes_path, notice: "Theme '#{result[:name]}' v#{result[:version]} imported successfully."
77
+ rescue NotPressed::ThemeImporter::InvalidArchive => e
78
+ redirect_to admin_themes_path, alert: "Import failed: #{e.message}"
79
+ rescue NotPressed::ThemeImporter::DuplicateTheme => e
80
+ redirect_to admin_themes_path, alert: "Import failed: #{e.message}"
81
+ ensure
82
+ temp_file&.unlink
83
+ end
84
+
85
+ private
86
+
87
+ def set_theme
88
+ info = NotPressed::ThemeRegistry.find(params[:theme_name])
89
+ @theme_class = info[:klass]
90
+ @theme_info = info
91
+ rescue NotPressed::ThemeRegistry::NotFound
92
+ redirect_to admin_themes_path, alert: "Theme not found."
93
+ end
94
+
95
+ def load_color_values
96
+ name = @theme_class.plugin_name
97
+ @theme_class.theme_color_scheme.each_with_object({}) do |color, hash|
98
+ key = "theme.#{name}.color_#{color[:key]}"
99
+ hash[color[:key]] = NotPressed::Setting.get(key) || color[:default]
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class ApplicationController < ActionController::Base
5
+ end
6
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class BlogController < ApplicationController
5
+ include NotPressed::ContentHelper
6
+
7
+ layout :resolve_layout
8
+
9
+ def index
10
+ @page_num = [params.fetch(:page, 1).to_i, 1].max
11
+ @posts = blog_posts.order(published_at: :desc).limit(12).offset((@page_num - 1) * 12)
12
+ @total_count = blog_posts.count
13
+ end
14
+
15
+ def show
16
+ @post = blog_posts.find_by!(slug: params[:slug])
17
+ @prev_post = blog_posts.where("published_at < ?", @post.published_at).order(published_at: :desc).first
18
+ @next_post = blog_posts.where("published_at > ?", @post.published_at).order(published_at: :asc).first
19
+ rescue ActiveRecord::RecordNotFound
20
+ redirect_to not_pressed.blog_path
21
+ end
22
+
23
+ def category
24
+ @category = NotPressed::Category.find_by!(slug: params[:slug])
25
+ @page_num = [params.fetch(:page, 1).to_i, 1].max
26
+ @posts = blog_posts.where(category: @category).order(published_at: :desc).limit(12).offset((@page_num - 1) * 12)
27
+ @total_count = blog_posts.where(category: @category).count
28
+ render :index
29
+ rescue ActiveRecord::RecordNotFound
30
+ redirect_to not_pressed.blog_path
31
+ end
32
+
33
+ def tag
34
+ @tag = NotPressed::Tag.find_by!(slug: params[:slug])
35
+ @page_num = [params.fetch(:page, 1).to_i, 1].max
36
+ @posts = blog_posts.joins(:taggings).where(not_pressed_taggings: { tag_id: @tag.id }).order(published_at: :desc).limit(12).offset((@page_num - 1) * 12)
37
+ @total_count = blog_posts.joins(:taggings).where(not_pressed_taggings: { tag_id: @tag.id }).count
38
+ render :index
39
+ rescue ActiveRecord::RecordNotFound
40
+ redirect_to not_pressed.blog_path
41
+ end
42
+
43
+ def archive
44
+ @year = params[:year].to_i
45
+ @month = params[:month]&.to_i
46
+ @page_num = [params.fetch(:page, 1).to_i, 1].max
47
+
48
+ scope = blog_posts.where("EXTRACT(YEAR FROM published_at) = ?", @year)
49
+ scope = scope.where("EXTRACT(MONTH FROM published_at) = ?", @month) if @month
50
+
51
+ @posts = scope.order(published_at: :desc).limit(12).offset((@page_num - 1) * 12)
52
+ @total_count = scope.count
53
+ render :index
54
+ end
55
+
56
+ def feed
57
+ @posts = blog_posts.order(published_at: :desc).limit(20)
58
+ respond_to do |format|
59
+ format.rss { render layout: false }
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def blog_posts
66
+ NotPressed::Page.blog_posts.published
67
+ end
68
+
69
+ def resolve_layout
70
+ return false if action_name == "feed"
71
+
72
+ theme = NotPressed::ThemeRegistry.active
73
+ if theme
74
+ primary = theme.primary_layout
75
+ primary ? "not_pressed/#{primary[:name]}" : "not_pressed/page"
76
+ else
77
+ "not_pressed/page"
78
+ end
79
+ rescue ActionView::MissingTemplate
80
+ "not_pressed/page"
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class FormSubmissionsController < ApplicationController
5
+ def create
6
+ @form = NotPressed::Form.find_by(slug: params[:form_slug])
7
+
8
+ unless @form
9
+ render plain: "Form not found", status: :not_found
10
+ return
11
+ end
12
+
13
+ unless @form.status_active?
14
+ render plain: "This form is not currently accepting submissions", status: :gone
15
+ return
16
+ end
17
+
18
+ data = build_submission_data
19
+ @submission = @form.form_submissions.build(data: data)
20
+
21
+ if @submission.save
22
+ send_notification_email(@submission)
23
+
24
+ if @form.redirect_url.present?
25
+ redirect_to @form.redirect_url, allow_other_host: true
26
+ else
27
+ render inline: thank_you_html(@form), layout: false
28
+ end
29
+ else
30
+ render plain: "There was an error processing your submission", status: :unprocessable_entity
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def build_submission_data
37
+ submission_params = params[:submission] || {}
38
+ data = {}
39
+
40
+ @form.form_fields.ordered.each do |field|
41
+ key = field.label.parameterize(separator: "_")
42
+ value = submission_params[key]
43
+ data[field.label] = value.is_a?(Array) ? value.join(", ") : value.to_s
44
+ end
45
+
46
+ data
47
+ end
48
+
49
+ def send_notification_email(submission)
50
+ return unless submission.form.email_recipient.present?
51
+
52
+ NotPressed::FormMailer.submission_notification(submission).deliver_later
53
+ end
54
+
55
+ def thank_you_html(form)
56
+ message = ERB::Util.html_escape(form.success_message)
57
+ <<~HTML
58
+ <!DOCTYPE html>
59
+ <html>
60
+ <head><title>Thank You</title></head>
61
+ <body>
62
+ <div class="np-form-thank-you">
63
+ <p>#{message}</p>
64
+ </div>
65
+ </body>
66
+ </html>
67
+ HTML
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotPressed
4
+ class PagesController < ApplicationController
5
+ layout :resolve_layout
6
+
7
+ def show
8
+ @page = NotPressed::Page.published.find_by!(slug: params[:slug])
9
+ rescue ActiveRecord::RecordNotFound
10
+ render plain: "Not Found", status: :not_found
11
+ end
12
+
13
+ private
14
+
15
+ def resolve_layout
16
+ theme = NotPressed::ThemeRegistry.active
17
+ if theme
18
+ page_layout = @page&.layout
19
+ if page_layout.present? && page_layout != "default"
20
+ "not_pressed/#{page_layout}"
21
+ else
22
+ primary = theme.primary_layout
23
+ primary ? "not_pressed/#{primary[:name]}" : "not_pressed/page"
24
+ end
25
+ else
26
+ if @page&.layout.present? && @page.layout != "default"
27
+ "not_pressed/#{@page.layout}"
28
+ else
29
+ "not_pressed/page"
30
+ end
31
+ end
32
+ rescue ActionView::MissingTemplate
33
+ "not_pressed/page"
34
+ end
35
+ end
36
+ end