postnhost 0.0.0 → 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 (497) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/{LICENSE.txt → LICENSE} +6 -6
  4. data/README.md +341 -15
  5. data/Rakefile +38 -6
  6. data/app/assets/builds/postnhost/application.css +2 -0
  7. data/app/assets/builds/postnhost/application.js +50190 -0
  8. data/app/assets/config/postnhost_manifest.js +2 -0
  9. data/app/assets/images/postnhost/demo.webp +0 -0
  10. data/app/assets/images/postnhost/icons/alert-circle.svg +3 -0
  11. data/app/assets/images/postnhost/icons/arrow-left-long.svg +3 -0
  12. data/app/assets/images/postnhost/icons/arrow-left.svg +3 -0
  13. data/app/assets/images/postnhost/icons/arrow-right-end-on-rectangle.svg +3 -0
  14. data/app/assets/images/postnhost/icons/arrow-right-long.svg +3 -0
  15. data/app/assets/images/postnhost/icons/arrow-right.svg +3 -0
  16. data/app/assets/images/postnhost/icons/bars.svg +3 -0
  17. data/app/assets/images/postnhost/icons/bluesky.svg +3 -0
  18. data/app/assets/images/postnhost/icons/check-circle.svg +3 -0
  19. data/app/assets/images/postnhost/icons/chevron-down-small.svg +3 -0
  20. data/app/assets/images/postnhost/icons/chevron-down.svg +3 -0
  21. data/app/assets/images/postnhost/icons/chevron-left.svg +3 -0
  22. data/app/assets/images/postnhost/icons/chevron-right.svg +3 -0
  23. data/app/assets/images/postnhost/icons/clock.svg +3 -0
  24. data/app/assets/images/postnhost/icons/close.svg +3 -0
  25. data/app/assets/images/postnhost/icons/code-bracket.svg +3 -0
  26. data/app/assets/images/postnhost/icons/document-text.svg +3 -0
  27. data/app/assets/images/postnhost/icons/drag-handle.svg +8 -0
  28. data/app/assets/images/postnhost/icons/edit.svg +3 -0
  29. data/app/assets/images/postnhost/icons/external-link.svg +3 -0
  30. data/app/assets/images/postnhost/icons/eye.svg +4 -0
  31. data/app/assets/images/postnhost/icons/facebook.svg +3 -0
  32. data/app/assets/images/postnhost/icons/folder.svg +3 -0
  33. data/app/assets/images/postnhost/icons/image.svg +3 -0
  34. data/app/assets/images/postnhost/icons/info-circle.svg +3 -0
  35. data/app/assets/images/postnhost/icons/instagram.svg +3 -0
  36. data/app/assets/images/postnhost/icons/language.svg +3 -0
  37. data/app/assets/images/postnhost/icons/layers.svg +3 -0
  38. data/app/assets/images/postnhost/icons/lightning.svg +3 -0
  39. data/app/assets/images/postnhost/icons/linkedin.svg +3 -0
  40. data/app/assets/images/postnhost/icons/logout.svg +3 -0
  41. data/app/assets/images/postnhost/icons/magnifying-glass.svg +3 -0
  42. data/app/assets/images/postnhost/icons/mastodon.svg +3 -0
  43. data/app/assets/images/postnhost/icons/menu.svg +3 -0
  44. data/app/assets/images/postnhost/icons/plus.svg +3 -0
  45. data/app/assets/images/postnhost/icons/schedule.svg +3 -0
  46. data/app/assets/images/postnhost/icons/settings.svg +4 -0
  47. data/app/assets/images/postnhost/icons/sparkles.svg +3 -0
  48. data/app/assets/images/postnhost/icons/spinner.svg +4 -0
  49. data/app/assets/images/postnhost/icons/threads.svg +3 -0
  50. data/app/assets/images/postnhost/icons/tiktok.svg +3 -0
  51. data/app/assets/images/postnhost/icons/tiptap/align-center.svg +3 -0
  52. data/app/assets/images/postnhost/icons/tiptap/align-left.svg +3 -0
  53. data/app/assets/images/postnhost/icons/tiptap/align-right.svg +3 -0
  54. data/app/assets/images/postnhost/icons/tiptap/apply-link.svg +3 -0
  55. data/app/assets/images/postnhost/icons/tiptap/blockquote.svg +3 -0
  56. data/app/assets/images/postnhost/icons/tiptap/bold.svg +3 -0
  57. data/app/assets/images/postnhost/icons/tiptap/bullet-list.svg +3 -0
  58. data/app/assets/images/postnhost/icons/tiptap/code-block.svg +3 -0
  59. data/app/assets/images/postnhost/icons/tiptap/code.svg +3 -0
  60. data/app/assets/images/postnhost/icons/tiptap/heading-2.svg +3 -0
  61. data/app/assets/images/postnhost/icons/tiptap/heading-3.svg +3 -0
  62. data/app/assets/images/postnhost/icons/tiptap/image-add.svg +3 -0
  63. data/app/assets/images/postnhost/icons/tiptap/italic.svg +3 -0
  64. data/app/assets/images/postnhost/icons/tiptap/link.svg +3 -0
  65. data/app/assets/images/postnhost/icons/tiptap/ordered-list.svg +3 -0
  66. data/app/assets/images/postnhost/icons/tiptap/redo.svg +3 -0
  67. data/app/assets/images/postnhost/icons/tiptap/remove-link.svg +3 -0
  68. data/app/assets/images/postnhost/icons/tiptap/strike.svg +3 -0
  69. data/app/assets/images/postnhost/icons/tiptap/table.svg +3 -0
  70. data/app/assets/images/postnhost/icons/tiptap/underline.svg +3 -0
  71. data/app/assets/images/postnhost/icons/tiptap/undo.svg +3 -0
  72. data/app/assets/images/postnhost/icons/tiptap/youtube.svg +4 -0
  73. data/app/assets/images/postnhost/icons/translate.svg +3 -0
  74. data/app/assets/images/postnhost/icons/trash.svg +3 -0
  75. data/app/assets/images/postnhost/icons/user.svg +4 -0
  76. data/app/assets/images/postnhost/icons/users.svg +3 -0
  77. data/app/assets/images/postnhost/icons/warning.svg +3 -0
  78. data/app/assets/images/postnhost/icons/x.svg +3 -0
  79. data/app/assets/images/postnhost/icons/youtube.svg +3 -0
  80. data/app/assets/images/postnhost/logo.webp +0 -0
  81. data/app/assets/images/postnhost/og-image.webp +0 -0
  82. data/app/assets/images/postnhost/sample-docs/08-how-to-control-suggested-articles-and-top-picks-01-discovery-controls.webp +0 -0
  83. data/app/assets/images/postnhost/sample-docs/09-all-about-authors-in-postnhost-01-author-profile.webp +0 -0
  84. data/app/assets/images/postnhost/sample-docs/09-all-about-authors-in-postnhost-02-multiple-authors.webp +0 -0
  85. data/app/assets/images/postnhost/sample-docs/11-how-to-schedule-an-article-for-publication-01-timezone.webp +0 -0
  86. data/app/assets/images/postnhost/sample-docs/11-how-to-schedule-an-article-for-publication-02-scheduled-article.webp +0 -0
  87. data/app/assets/images/postnhost/sample-docs/12-how-article-version-history-and-restores-work-01-version-history.webp +0 -0
  88. data/app/assets/images/postnhost/sample-docs/13-how-to-add-and-manage-languages-01-language-management.webp +0 -0
  89. data/app/assets/images/postnhost/sample-docs/14-how-to-translate-articles-pages-and-categories-01-translation-options.webp +0 -0
  90. data/app/assets/images/postnhost/sample-docs/14-how-to-translate-articles-pages-and-categories-02-ai-language-selection.webp +0 -0
  91. data/app/assets/images/postnhost/sample-docs/16-how-to-add-navigation-links-01-navigation-builder.webp +0 -0
  92. data/app/assets/images/postnhost/sample-docs/16-how-to-add-navigation-links-02-external-link-settings.webp +0 -0
  93. data/app/assets/images/postnhost/sample-docs/17-how-to-add-a-logo-and-default-og-image-01-assets-settings.webp +0 -0
  94. data/app/assets/images/postnhost/sample-docs/19-how-to-customize-public-text-for-each-language-01-i18n-overrides.webp +0 -0
  95. data/app/assets/images/postnhost/sample-docs/20-how-to-configure-structured-data-and-sitemap-seo-01-site-schema.webp +0 -0
  96. data/app/assets/images/postnhost/sample-docs/20-how-to-configure-structured-data-and-sitemap-seo-02-author-schema-defaults.webp +0 -0
  97. data/app/assets/images/postnhost/sample-docs/20-how-to-configure-structured-data-and-sitemap-seo-03-author-schema-advanced-fields.webp +0 -0
  98. data/app/assets/images/postnhost/sample-docs/covers/08-suggested-articles-and-top-picks.webp +0 -0
  99. data/app/assets/images/postnhost/sample-docs/covers/09-authors.webp +0 -0
  100. data/app/assets/images/postnhost/sample-docs/covers/11-scheduling.webp +0 -0
  101. data/app/assets/images/postnhost/sample-docs/covers/12-version-history.webp +0 -0
  102. data/app/assets/images/postnhost/sample-docs/covers/13-languages.webp +0 -0
  103. data/app/assets/images/postnhost/sample-docs/covers/14-translations.webp +0 -0
  104. data/app/assets/images/postnhost/sample-docs/covers/16-navigation-links.webp +0 -0
  105. data/app/assets/images/postnhost/sample-docs/covers/17-logo-and-default-og-image.webp +0 -0
  106. data/app/assets/images/postnhost/sample-docs/covers/19-public-text-for-each-language.webp +0 -0
  107. data/app/assets/images/postnhost/sample-docs/covers/20-structured-data-and-sitemap-seo.webp +0 -0
  108. data/app/assets/stylesheets/application.tailwind.css +343 -0
  109. data/app/assets/stylesheets/postnhost/application.css +15 -0
  110. data/app/assets/xsl/sitemap.xsl +64 -0
  111. data/app/controllers/concerns/postnhost/authentication.rb +59 -0
  112. data/app/controllers/concerns/postnhost/canonical_first_page_redirect.rb +14 -0
  113. data/app/controllers/concerns/postnhost/language_switcher.rb +42 -0
  114. data/app/controllers/concerns/postnhost/onboarding/access_control.rb +57 -0
  115. data/app/controllers/concerns/postnhost/onboarding/step_assignment.rb +55 -0
  116. data/app/controllers/concerns/postnhost/public_footer_data.rb +103 -0
  117. data/app/controllers/concerns/postnhost/public_http_caching.rb +35 -0
  118. data/app/controllers/concerns/postnhost/public_page_size.rb +17 -0
  119. data/app/controllers/concerns/postnhost/public_template_resolvable.rb +44 -0
  120. data/app/controllers/concerns/postnhost/settings/i18n_manageable.rb +61 -0
  121. data/app/controllers/concerns/postnhost/settings/navigation_manageable.rb +118 -0
  122. data/app/controllers/concerns/postnhost/settings/schema_manageable.rb +140 -0
  123. data/app/controllers/concerns/postnhost/users/schema_manageable.rb +119 -0
  124. data/app/controllers/postnhost/application_controller.rb +37 -0
  125. data/app/controllers/postnhost/article_variants_controller.rb +138 -0
  126. data/app/controllers/postnhost/articles/cover_images_controller.rb +35 -0
  127. data/app/controllers/postnhost/articles/images_controller.rb +28 -0
  128. data/app/controllers/postnhost/articles/translations_controller.rb +26 -0
  129. data/app/controllers/postnhost/articles_controller.rb +130 -0
  130. data/app/controllers/postnhost/categories/translations_controller.rb +24 -0
  131. data/app/controllers/postnhost/categories_controller.rb +59 -0
  132. data/app/controllers/postnhost/category_variants_controller.rb +69 -0
  133. data/app/controllers/postnhost/languages_controller.rb +66 -0
  134. data/app/controllers/postnhost/onboarding_controller.rb +70 -0
  135. data/app/controllers/postnhost/page_variants_controller.rb +136 -0
  136. data/app/controllers/postnhost/pages/images_controller.rb +28 -0
  137. data/app/controllers/postnhost/pages/translations_controller.rb +26 -0
  138. data/app/controllers/postnhost/pages_controller.rb +95 -0
  139. data/app/controllers/postnhost/public/articles_controller.rb +135 -0
  140. data/app/controllers/postnhost/public/authors_controller.rb +57 -0
  141. data/app/controllers/postnhost/public/categories_controller.rb +61 -0
  142. data/app/controllers/postnhost/public/static_pages_controller.rb +77 -0
  143. data/app/controllers/postnhost/public_controller.rb +32 -0
  144. data/app/controllers/postnhost/sessions_controller.rb +37 -0
  145. data/app/controllers/postnhost/settings/site_scripts_controller.rb +61 -0
  146. data/app/controllers/postnhost/settings_controller.rb +63 -0
  147. data/app/controllers/postnhost/sitemap_controller.rb +17 -0
  148. data/app/controllers/postnhost/templates_controller.rb +26 -0
  149. data/app/controllers/postnhost/users_controller.rb +112 -0
  150. data/app/helpers/postnhost/application_helper.rb +247 -0
  151. data/app/helpers/postnhost/article_helper.rb +41 -0
  152. data/app/helpers/postnhost/category_helper.rb +52 -0
  153. data/app/helpers/postnhost/common_css_helper.rb +97 -0
  154. data/app/helpers/postnhost/navigation_helper.rb +148 -0
  155. data/app/helpers/postnhost/pagy_helper.rb +103 -0
  156. data/app/helpers/postnhost/public_pages_helper.rb +287 -0
  157. data/app/helpers/postnhost/schema_helper.rb +102 -0
  158. data/app/helpers/postnhost/seo_helper.rb +428 -0
  159. data/app/helpers/postnhost/settings_helper.rb +79 -0
  160. data/app/javascript/application.js +3 -0
  161. data/app/javascript/controllers/ai_translation_controller.js +42 -0
  162. data/app/javascript/controllers/application.js +9 -0
  163. data/app/javascript/controllers/author_manager_controller.js +96 -0
  164. data/app/javascript/controllers/auto_save_controller.js +244 -0
  165. data/app/javascript/controllers/bulk_selection_controller.js +74 -0
  166. data/app/javascript/controllers/burger_controller.js +20 -0
  167. data/app/javascript/controllers/category_manager_controller.js +99 -0
  168. data/app/javascript/controllers/collapsible_sidebar_controller.js +87 -0
  169. data/app/javascript/controllers/cover_image_upload_controller.js +164 -0
  170. data/app/javascript/controllers/dropdown_controller.js +38 -0
  171. data/app/javascript/controllers/editor_mobile_sidebars_controller.js +36 -0
  172. data/app/javascript/controllers/file_auto_submit_controller.js +23 -0
  173. data/app/javascript/controllers/flash_message_controller.js +40 -0
  174. data/app/javascript/controllers/index.js +71 -0
  175. data/app/javascript/controllers/navigation_builder_controller.js +463 -0
  176. data/app/javascript/controllers/public_mobile_nav_controller.js +26 -0
  177. data/app/javascript/controllers/publish_guard_controller.js +47 -0
  178. data/app/javascript/controllers/reading_progress_controller.js +38 -0
  179. data/app/javascript/controllers/schedule_manager_controller.js +87 -0
  180. data/app/javascript/controllers/suggestion_manager_controller.js +99 -0
  181. data/app/javascript/controllers/template_preview_controller.js +34 -0
  182. data/app/javascript/controllers/tiptap_editor_controller.js +812 -0
  183. data/app/javascript/controllers/title_field_controller.js +17 -0
  184. data/app/javascript/helpers/auto_save_state.js +47 -0
  185. data/app/javascript/helpers/code_highlighting.js +48 -0
  186. data/app/javascript/helpers/dom_helpers.js +109 -0
  187. data/app/javascript/helpers/style_helpers.js +168 -0
  188. data/app/jobs/postnhost/application_job.rb +9 -0
  189. data/app/jobs/postnhost/scheduled_article_job.rb +13 -0
  190. data/app/jobs/postnhost/translation/article_variant_job.rb +17 -0
  191. data/app/jobs/postnhost/translation/category_variant_job.rb +17 -0
  192. data/app/jobs/postnhost/translation/page_variant_job.rb +17 -0
  193. data/app/models/concerns/postnhost/article/image_uploadable.rb +11 -0
  194. data/app/models/concerns/postnhost/excerpt_optimizable.rb +57 -0
  195. data/app/models/concerns/postnhost/inline_image_uploadable.rb +40 -0
  196. data/app/models/concerns/postnhost/page/image_uploadable.rb +11 -0
  197. data/app/models/concerns/postnhost/primary_language_guard.rb +18 -0
  198. data/app/models/concerns/postnhost/public_revision_touch.rb +15 -0
  199. data/app/models/concerns/postnhost/schema_localizable.rb +28 -0
  200. data/app/models/postnhost/application_record.rb +5 -0
  201. data/app/models/postnhost/article.rb +142 -0
  202. data/app/models/postnhost/article_author.rb +20 -0
  203. data/app/models/postnhost/article_category.rb +8 -0
  204. data/app/models/postnhost/article_suggestion.rb +19 -0
  205. data/app/models/postnhost/article_variant.rb +28 -0
  206. data/app/models/postnhost/category.rb +34 -0
  207. data/app/models/postnhost/category_variant.rb +24 -0
  208. data/app/models/postnhost/language.rb +38 -0
  209. data/app/models/postnhost/navigation.rb +76 -0
  210. data/app/models/postnhost/navigation_item.rb +100 -0
  211. data/app/models/postnhost/page.rb +63 -0
  212. data/app/models/postnhost/page_variant.rb +26 -0
  213. data/app/models/postnhost/public_site_revision.rb +24 -0
  214. data/app/models/postnhost/published_cover_image.rb +28 -0
  215. data/app/models/postnhost/setting.rb +116 -0
  216. data/app/models/postnhost/site_script.rb +11 -0
  217. data/app/models/postnhost/snapshot/article.rb +142 -0
  218. data/app/models/postnhost/snapshot/article_author.rb +11 -0
  219. data/app/models/postnhost/snapshot/article_category.rb +10 -0
  220. data/app/models/postnhost/snapshot/article_suggestion.rb +11 -0
  221. data/app/models/postnhost/snapshot/article_variant.rb +60 -0
  222. data/app/models/postnhost/snapshot/page.rb +33 -0
  223. data/app/models/postnhost/snapshot/page_variant.rb +41 -0
  224. data/app/models/postnhost/template.rb +20 -0
  225. data/app/models/postnhost/user.rb +63 -0
  226. data/app/models/postnhost/version.rb +7 -0
  227. data/app/services/postnhost/base_service.rb +17 -0
  228. data/app/services/postnhost/constraints/public_locale_constraint.rb +9 -0
  229. data/app/services/postnhost/constraints/public_route_constraint.rb +19 -0
  230. data/app/services/postnhost/onboarding/step_processor.rb +27 -0
  231. data/app/services/postnhost/onboarding/steps/base.rb +52 -0
  232. data/app/services/postnhost/onboarding/steps/choose_languages.rb +66 -0
  233. data/app/services/postnhost/onboarding/steps/complete_post_install_checklist.rb +12 -0
  234. data/app/services/postnhost/onboarding/steps/create_admin_account.rb +28 -0
  235. data/app/services/postnhost/onboarding/steps/finish_setup.rb +27 -0
  236. data/app/services/postnhost/onboarding/steps/set_site_metadata.rb +70 -0
  237. data/app/services/postnhost/public_navigation_targets.rb +67 -0
  238. data/app/services/postnhost/public_request_context.rb +101 -0
  239. data/app/services/postnhost/publishing/article_variants/publish.rb +26 -0
  240. data/app/services/postnhost/publishing/article_variants/publish_many.rb +23 -0
  241. data/app/services/postnhost/publishing/article_variants/snapshot_writer.rb +44 -0
  242. data/app/services/postnhost/publishing/article_variants/unpublish.rb +27 -0
  243. data/app/services/postnhost/publishing/article_variants/unpublish_many.rb +23 -0
  244. data/app/services/postnhost/publishing/articles/publish.rb +25 -0
  245. data/app/services/postnhost/publishing/articles/publish_scheduled.rb +42 -0
  246. data/app/services/postnhost/publishing/articles/snapshot_writer.rb +84 -0
  247. data/app/services/postnhost/publishing/articles/unpublish.rb +26 -0
  248. data/app/services/postnhost/publishing/bulk_transition.rb +63 -0
  249. data/app/services/postnhost/publishing/error.rb +12 -0
  250. data/app/services/postnhost/publishing/page_variants/publish.rb +26 -0
  251. data/app/services/postnhost/publishing/page_variants/publish_many.rb +23 -0
  252. data/app/services/postnhost/publishing/page_variants/snapshot_writer.rb +41 -0
  253. data/app/services/postnhost/publishing/page_variants/unpublish.rb +27 -0
  254. data/app/services/postnhost/publishing/page_variants/unpublish_many.rb +23 -0
  255. data/app/services/postnhost/publishing/pages/publish.rb +25 -0
  256. data/app/services/postnhost/publishing/pages/snapshot_writer.rb +38 -0
  257. data/app/services/postnhost/publishing/pages/unpublish.rb +26 -0
  258. data/app/services/postnhost/publishing/revision.rb +37 -0
  259. data/app/services/postnhost/publishing/route_validator.rb +41 -0
  260. data/app/services/postnhost/publishing/versions/restore_draft.rb +64 -0
  261. data/app/services/postnhost/service_result.rb +7 -0
  262. data/app/services/postnhost/sitemap_builder.rb +395 -0
  263. data/app/services/postnhost/translation/article_variant_translator.rb +102 -0
  264. data/app/services/postnhost/translation/category_variant_translator.rb +92 -0
  265. data/app/services/postnhost/translation/content_translator.rb +119 -0
  266. data/app/services/postnhost/translation/openai_llm_client.rb +30 -0
  267. data/app/services/postnhost/translation/page_variant_translator.rb +55 -0
  268. data/app/uploaders/base_uploader.rb +26 -0
  269. data/app/uploaders/cover_image_uploader.rb +31 -0
  270. data/app/uploaders/postnhost/avatar_uploader.rb +17 -0
  271. data/app/uploaders/postnhost/inline_article_image_uploader.rb +38 -0
  272. data/app/uploaders/postnhost/inline_page_image_uploader.rb +38 -0
  273. data/app/uploaders/postnhost/setting_asset_uploader.rb +18 -0
  274. data/app/views/layouts/postnhost/_favicon.html.erb +6 -0
  275. data/app/views/layouts/postnhost/_footer.html.erb +67 -0
  276. data/app/views/layouts/postnhost/_header.html.erb +89 -0
  277. data/app/views/layouts/postnhost/_sidebar.html.erb +72 -0
  278. data/app/views/layouts/postnhost/application.html.erb +23 -0
  279. data/app/views/layouts/postnhost/articles.html.erb +34 -0
  280. data/app/views/layouts/postnhost/product.html.erb +36 -0
  281. data/app/views/layouts/postnhost/public/templates/default.html.erb +110 -0
  282. data/app/views/layouts/postnhost/public/templates/swiss-editorial.html.erb +213 -0
  283. data/app/views/layouts/postnhost/public/templates/workspace-journal.html.erb +231 -0
  284. data/app/views/layouts/postnhost/public_header/_dashboard_sign_in.html.erb +14 -0
  285. data/app/views/layouts/postnhost/public_header/_language_switcher.html.erb +43 -0
  286. data/app/views/layouts/postnhost/public_header/_logo.html.erb +9 -0
  287. data/app/views/postnhost/article_variants/_variants_list.html.erb +82 -0
  288. data/app/views/postnhost/article_variants/edit.html.erb +48 -0
  289. data/app/views/postnhost/article_variants/index.html.erb +128 -0
  290. data/app/views/postnhost/article_variants/publish.turbo_stream.erb +3 -0
  291. data/app/views/postnhost/article_variants/unpublish.turbo_stream.erb +3 -0
  292. data/app/views/postnhost/article_variants/update.turbo_stream.erb +3 -0
  293. data/app/views/postnhost/articles/_author_manager.html.erb +38 -0
  294. data/app/views/postnhost/articles/_category_manager.html.erb +42 -0
  295. data/app/views/postnhost/articles/_cover_image_upload.html.erb +82 -0
  296. data/app/views/postnhost/articles/_suggestion_manager.html.erb +43 -0
  297. data/app/views/postnhost/articles/_version_snapshot.html.erb +10 -0
  298. data/app/views/postnhost/articles/edit.html.erb +51 -0
  299. data/app/views/postnhost/articles/index.html.erb +132 -0
  300. data/app/views/postnhost/articles/publish.turbo_stream.erb +3 -0
  301. data/app/views/postnhost/articles/unpublish.turbo_stream.erb +3 -0
  302. data/app/views/postnhost/articles/update.turbo_stream.erb +3 -0
  303. data/app/views/postnhost/articles/version_preview.html.erb +17 -0
  304. data/app/views/postnhost/articles/versions.html.erb +68 -0
  305. data/app/views/postnhost/categories/edit.html.erb +77 -0
  306. data/app/views/postnhost/categories/index.html.erb +80 -0
  307. data/app/views/postnhost/categories/new.html.erb +50 -0
  308. data/app/views/postnhost/categories/show.html.erb +99 -0
  309. data/app/views/postnhost/category_variants/_category_variants_list.html.erb +67 -0
  310. data/app/views/postnhost/category_variants/_form.html.erb +29 -0
  311. data/app/views/postnhost/category_variants/edit.html.erb +21 -0
  312. data/app/views/postnhost/category_variants/index.html.erb +82 -0
  313. data/app/views/postnhost/category_variants/new.html.erb +21 -0
  314. data/app/views/postnhost/languages/edit.html.erb +45 -0
  315. data/app/views/postnhost/languages/index.html.erb +86 -0
  316. data/app/views/postnhost/languages/new.html.erb +45 -0
  317. data/app/views/postnhost/languages/show.html.erb +156 -0
  318. data/app/views/postnhost/onboarding/_post_install_checklist.html.erb +40 -0
  319. data/app/views/postnhost/onboarding/_step_1.html.erb +42 -0
  320. data/app/views/postnhost/onboarding/_step_2.html.erb +38 -0
  321. data/app/views/postnhost/onboarding/_step_3.html.erb +62 -0
  322. data/app/views/postnhost/onboarding/_step_4.html.erb +26 -0
  323. data/app/views/postnhost/onboarding/show.html.erb +51 -0
  324. data/app/views/postnhost/page_variants/_variants_list.html.erb +82 -0
  325. data/app/views/postnhost/page_variants/edit.html.erb +50 -0
  326. data/app/views/postnhost/page_variants/index.html.erb +128 -0
  327. data/app/views/postnhost/page_variants/publish.turbo_stream.erb +3 -0
  328. data/app/views/postnhost/page_variants/unpublish.turbo_stream.erb +3 -0
  329. data/app/views/postnhost/page_variants/update.turbo_stream.erb +3 -0
  330. data/app/views/postnhost/pages/edit.html.erb +50 -0
  331. data/app/views/postnhost/pages/index.html.erb +110 -0
  332. data/app/views/postnhost/pages/publish.turbo_stream.erb +3 -0
  333. data/app/views/postnhost/pages/unpublish.turbo_stream.erb +3 -0
  334. data/app/views/postnhost/public/articles/content_for/_index.html.erb +30 -0
  335. data/app/views/postnhost/public/articles/content_for/_preview.html.erb +9 -0
  336. data/app/views/postnhost/public/articles/content_for/_show.html.erb +24 -0
  337. data/app/views/postnhost/public/authors/content_for/_show.html.erb +24 -0
  338. data/app/views/postnhost/public/categories/content_for/_index.html.erb +29 -0
  339. data/app/views/postnhost/public/static_pages/content_for/_show.html.erb +19 -0
  340. data/app/views/postnhost/public/templates/default/articles/_article_card.html.erb +37 -0
  341. data/app/views/postnhost/public/templates/default/articles/_search_form.html.erb +12 -0
  342. data/app/views/postnhost/public/templates/default/articles/_suggestions.html.erb +44 -0
  343. data/app/views/postnhost/public/templates/default/articles/_top_picks.html.erb +42 -0
  344. data/app/views/postnhost/public/templates/default/articles/index.html.erb +53 -0
  345. data/app/views/postnhost/public/templates/default/articles/preview.html.erb +102 -0
  346. data/app/views/postnhost/public/templates/default/articles/show.html.erb +111 -0
  347. data/app/views/postnhost/public/templates/default/authors/_article_card.html.erb +28 -0
  348. data/app/views/postnhost/public/templates/default/authors/_profile_block.html.erb +29 -0
  349. data/app/views/postnhost/public/templates/default/authors/show.html.erb +36 -0
  350. data/app/views/postnhost/public/templates/default/categories/_article_card.html.erb +34 -0
  351. data/app/views/postnhost/public/templates/default/categories/index.html.erb +48 -0
  352. data/app/views/postnhost/public/templates/default/shared/_article_authors.html.erb +24 -0
  353. data/app/views/postnhost/public/templates/default/shared/_author_avatar.html.erb +9 -0
  354. data/app/views/postnhost/public/templates/default/shared/_hero_cover_image.html.erb +11 -0
  355. data/app/views/postnhost/public/templates/default/shared/_pagination.html.erb +35 -0
  356. data/app/views/postnhost/public/templates/default/shared/_thumbnail_cover_image.html.erb +9 -0
  357. data/app/views/postnhost/public/templates/default/static_pages/show.html.erb +22 -0
  358. data/app/views/postnhost/public/templates/swiss-editorial/articles/_article_card.html.erb +28 -0
  359. data/app/views/postnhost/public/templates/swiss-editorial/articles/_search_form.html.erb +19 -0
  360. data/app/views/postnhost/public/templates/swiss-editorial/articles/_suggestions.html.erb +42 -0
  361. data/app/views/postnhost/public/templates/swiss-editorial/articles/_top_picks.html.erb +38 -0
  362. data/app/views/postnhost/public/templates/swiss-editorial/articles/index.html.erb +133 -0
  363. data/app/views/postnhost/public/templates/swiss-editorial/articles/preview.html.erb +42 -0
  364. data/app/views/postnhost/public/templates/swiss-editorial/articles/show.html.erb +100 -0
  365. data/app/views/postnhost/public/templates/swiss-editorial/authors/_article_card.html.erb +27 -0
  366. data/app/views/postnhost/public/templates/swiss-editorial/authors/_profile_block.html.erb +32 -0
  367. data/app/views/postnhost/public/templates/swiss-editorial/authors/show.html.erb +31 -0
  368. data/app/views/postnhost/public/templates/swiss-editorial/categories/_article_card.html.erb +28 -0
  369. data/app/views/postnhost/public/templates/swiss-editorial/categories/index.html.erb +88 -0
  370. data/app/views/postnhost/public/templates/swiss-editorial/shared/_article_authors.html.erb +26 -0
  371. data/app/views/postnhost/public/templates/swiss-editorial/shared/_author_avatar.html.erb +9 -0
  372. data/app/views/postnhost/public/templates/swiss-editorial/shared/_hero_cover_image.html.erb +11 -0
  373. data/app/views/postnhost/public/templates/swiss-editorial/shared/_pagination.html.erb +35 -0
  374. data/app/views/postnhost/public/templates/swiss-editorial/shared/_thumbnail_cover_image.html.erb +9 -0
  375. data/app/views/postnhost/public/templates/swiss-editorial/static_pages/show.html.erb +17 -0
  376. data/app/views/postnhost/public/templates/workspace-journal/articles/_article_card.html.erb +44 -0
  377. data/app/views/postnhost/public/templates/workspace-journal/articles/_search_form.html.erb +19 -0
  378. data/app/views/postnhost/public/templates/workspace-journal/articles/_suggestions.html.erb +49 -0
  379. data/app/views/postnhost/public/templates/workspace-journal/articles/_top_picks.html.erb +34 -0
  380. data/app/views/postnhost/public/templates/workspace-journal/articles/index.html.erb +69 -0
  381. data/app/views/postnhost/public/templates/workspace-journal/articles/preview.html.erb +113 -0
  382. data/app/views/postnhost/public/templates/workspace-journal/articles/show.html.erb +120 -0
  383. data/app/views/postnhost/public/templates/workspace-journal/authors/_article_card.html.erb +1 -0
  384. data/app/views/postnhost/public/templates/workspace-journal/authors/_profile_block.html.erb +32 -0
  385. data/app/views/postnhost/public/templates/workspace-journal/authors/show.html.erb +33 -0
  386. data/app/views/postnhost/public/templates/workspace-journal/categories/_article_card.html.erb +1 -0
  387. data/app/views/postnhost/public/templates/workspace-journal/categories/index.html.erb +58 -0
  388. data/app/views/postnhost/public/templates/workspace-journal/shared/_article_authors.html.erb +27 -0
  389. data/app/views/postnhost/public/templates/workspace-journal/shared/_article_card.html.erb +44 -0
  390. data/app/views/postnhost/public/templates/workspace-journal/shared/_author_avatar.html.erb +9 -0
  391. data/app/views/postnhost/public/templates/workspace-journal/shared/_category_navigation.html.erb +10 -0
  392. data/app/views/postnhost/public/templates/workspace-journal/shared/_hero_cover_image.html.erb +11 -0
  393. data/app/views/postnhost/public/templates/workspace-journal/shared/_pagination.html.erb +35 -0
  394. data/app/views/postnhost/public/templates/workspace-journal/shared/_thumbnail_cover_image.html.erb +9 -0
  395. data/app/views/postnhost/public/templates/workspace-journal/static_pages/show.html.erb +17 -0
  396. data/app/views/postnhost/sessions/new.html.erb +59 -0
  397. data/app/views/postnhost/settings/edit.html.erb +68 -0
  398. data/app/views/postnhost/settings/sections/_assets.html.erb +40 -0
  399. data/app/views/postnhost/settings/sections/_feature_flags.html.erb +43 -0
  400. data/app/views/postnhost/settings/sections/_i18n.html.erb +57 -0
  401. data/app/views/postnhost/settings/sections/_navigation.html.erb +98 -0
  402. data/app/views/postnhost/settings/sections/_schema.html.erb +253 -0
  403. data/app/views/postnhost/settings/sections/_scripts.html.erb +79 -0
  404. data/app/views/postnhost/settings/sections/_site.html.erb +41 -0
  405. data/app/views/postnhost/settings/sections/_timezone.html.erb +19 -0
  406. data/app/views/postnhost/shared/_ai_translation_modal.html.erb +85 -0
  407. data/app/views/postnhost/shared/_flash_messages.html.erb +27 -0
  408. data/app/views/postnhost/shared/_form_errors.html.erb +22 -0
  409. data/app/views/postnhost/shared/_seo_meta_tags.html.erb +70 -0
  410. data/app/views/postnhost/shared/_structured_data.html.erb +5 -0
  411. data/app/views/postnhost/shared/articles/_action_buttons.html.erb +87 -0
  412. data/app/views/postnhost/shared/articles/_cover_image_preview.html.erb +30 -0
  413. data/app/views/postnhost/shared/articles/_editor_column.html.erb +15 -0
  414. data/app/views/postnhost/shared/articles/_editor_column_preview.html.erb +33 -0
  415. data/app/views/postnhost/shared/articles/_error_messages.html.erb +23 -0
  416. data/app/views/postnhost/shared/articles/_meta_sidebar.html.erb +322 -0
  417. data/app/views/postnhost/shared/articles/_meta_sidebar_preview.html.erb +29 -0
  418. data/app/views/postnhost/shared/articles/_post_actions.html.erb +105 -0
  419. data/app/views/postnhost/shared/articles/_schedule_actions.html.erb +51 -0
  420. data/app/views/postnhost/shared/articles/_tiptap_editor.html.erb +225 -0
  421. data/app/views/postnhost/shared/articles/_title_field.html.erb +9 -0
  422. data/app/views/postnhost/shared/articles/_version_action_buttons.html.erb +35 -0
  423. data/app/views/postnhost/shared/elements/_button.html.erb +20 -0
  424. data/app/views/postnhost/shared/elements/_button_link.html.erb +24 -0
  425. data/app/views/postnhost/shared/elements/_empty_state.html.erb +22 -0
  426. data/app/views/postnhost/shared/elements/_error_messages.html.erb +28 -0
  427. data/app/views/postnhost/shared/elements/_form_card.html.erb +10 -0
  428. data/app/views/postnhost/shared/elements/_form_page_header.html.erb +21 -0
  429. data/app/views/postnhost/shared/elements/_icon_button.html.erb +17 -0
  430. data/app/views/postnhost/shared/elements/_page_header.html.erb +40 -0
  431. data/app/views/postnhost/shared/elements/_table_container.html.erb +23 -0
  432. data/app/views/postnhost/shared/pages/_action_buttons.html.erb +84 -0
  433. data/app/views/postnhost/shared/pages/_meta_sidebar.html.erb +132 -0
  434. data/app/views/postnhost/shared/pages/_post_actions.html.erb +69 -0
  435. data/app/views/postnhost/templates/edit.html.erb +83 -0
  436. data/app/views/postnhost/users/_avatar.html.erb +9 -0
  437. data/app/views/postnhost/users/_form.html.erb +160 -0
  438. data/app/views/postnhost/users/edit.html.erb +26 -0
  439. data/app/views/postnhost/users/index.html.erb +91 -0
  440. data/app/views/postnhost/users/new.html.erb +9 -0
  441. data/app/views/postnhost/users/schema_edit.html.erb +175 -0
  442. data/config/locales/de.yml +55 -0
  443. data/config/locales/en.yml +88 -0
  444. data/config/locales/es.yml +58 -0
  445. data/config/locales/fr.yml +55 -0
  446. data/config/locales/ja.yml +55 -0
  447. data/config/locales/ko.yml +55 -0
  448. data/config/locales/pl.yml +55 -0
  449. data/config/locales/pt.yml +55 -0
  450. data/config/locales/ru.yml +55 -0
  451. data/config/routes.rb +112 -0
  452. data/db/migrate/20250822213820_create_users.rb +29 -0
  453. data/db/migrate/20250822214046_create_categories.rb +15 -0
  454. data/db/migrate/20250822214106_create_languages.rb +16 -0
  455. data/db/migrate/20250822214148_create_articles.rb +34 -0
  456. data/db/migrate/20250822214217_create_article_variants.rb +24 -0
  457. data/db/migrate/20250929091137_create_article_categories.rb +12 -0
  458. data/db/migrate/20251013204200_create_versions.rb +40 -0
  459. data/db/migrate/20260106074532_create_article_suggestions.rb +13 -0
  460. data/db/migrate/20260108110506_create_category_variants.rb +15 -0
  461. data/db/migrate/20260308130000_create_settings.rb +23 -0
  462. data/db/migrate/20260323113000_create_pages.rb +19 -0
  463. data/db/migrate/20260414100000_create_article_authors.rb +14 -0
  464. data/db/migrate/20260512100000_create_templates.rb +9 -0
  465. data/db/migrate/20260520110001_create_navigations.rb +9 -0
  466. data/db/migrate/20260520110002_create_navigation_items.rb +22 -0
  467. data/db/migrate/20260528000002_create_page_variants.rb +20 -0
  468. data/db/migrate/20260709090000_create_snapshots.rb +154 -0
  469. data/db/migrate/20260726130000_create_site_scripts.rb +11 -0
  470. data/lib/generators/postnhost/install/install_generator.rb +56 -0
  471. data/lib/generators/postnhost/install/templates/carrierwave.rb +17 -0
  472. data/lib/generators/postnhost/install/templates/postnhost.rb +36 -0
  473. data/lib/generators/postnhost/locale/locale_generator.rb +36 -0
  474. data/lib/generators/postnhost/static_pages/static_pages_generator.rb +47 -0
  475. data/lib/generators/postnhost/tailwindcss/install_generator.rb +97 -0
  476. data/lib/generators/postnhost/tailwindcss/templates/host.tailwind.css +7 -0
  477. data/lib/generators/postnhost/tailwindcss/templates/tailwind.config.js +8 -0
  478. data/lib/generators/postnhost/user/user_generator.rb +36 -0
  479. data/lib/generators/postnhost/views/views_generator.rb +142 -0
  480. data/lib/postnhost/configuration.rb +64 -0
  481. data/lib/postnhost/engine.rb +57 -0
  482. data/lib/postnhost/sample_data.rb +865 -0
  483. data/lib/postnhost/settings/i18n_overrides.rb +137 -0
  484. data/lib/postnhost/settings/i18n_patch.rb +18 -0
  485. data/lib/postnhost/version.rb +1 -3
  486. data/lib/postnhost.rb +26 -3
  487. data/lib/tasks/postnhost_snapshots.rake +77 -0
  488. data/lib/tasks/postnhost_tasks.rake +96 -0
  489. metadata +662 -21
  490. data/.github/workflows/main.yml +0 -27
  491. data/.gitignore +0 -11
  492. data/.rspec +0 -3
  493. data/.rubocop.yml +0 -8
  494. data/Gemfile +0 -13
  495. data/bin/console +0 -11
  496. data/bin/setup +0 -8
  497. data/postnhost.gemspec +0 -31
@@ -0,0 +1,82 @@
1
+ <% variants.each do |variant| %>
2
+ <div class="px-6 py-4 hover:bg-gray-50 transition-colors duration-200">
3
+ <div class="flex items-center gap-2 md:gap-4">
4
+ <div class="flex items-center justify-center w-6 md:w-8 flex-shrink-0">
5
+ <input
6
+ type="checkbox"
7
+ name="ids[]"
8
+ value="<%= variant.id %>"
9
+ data-bulk-selection-target="checkbox"
10
+ data-action="change->bulk-selection#toggle"
11
+ <%= "disabled" if variant.generating? %>
12
+ class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 <%= variant.generating? ? 'cursor-not-allowed opacity-50' : 'cursor-pointer' %>"
13
+ >
14
+ </div>
15
+ <div class="flex-1 min-w-0">
16
+ <% if variant.generating? %>
17
+ <div class="block">
18
+ <div class="font-medium text-gray-900">
19
+ <%= variant.title.present? ? variant.title : 'Untitled Translation' %>
20
+ </div>
21
+ <% if variant.meta_description.present? %>
22
+ <div class="text-sm text-gray-500 mt-1 line-clamp-2">
23
+ <%= variant.meta_description %>
24
+ </div>
25
+ <% end %>
26
+ </div>
27
+ <% else %>
28
+ <%= link_to edit_article_variant_path(variant.article, variant), class: "block group" do %>
29
+ <div class="font-medium text-gray-900 group-hover:text-gray-600 transition-colors">
30
+ <%= variant.title.present? ? variant.title : 'Untitled Translation' %>
31
+ </div>
32
+ <% if variant.meta_description.present? %>
33
+ <div class="text-sm text-gray-500 mt-1 line-clamp-2">
34
+ <%= variant.meta_description %>
35
+ </div>
36
+ <% end %>
37
+ <% end %>
38
+ <% end %>
39
+ </div>
40
+
41
+ <div class="hidden md:block md:w-32 flex-shrink-0">
42
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
43
+ <%= variant.language.name %>
44
+ </span>
45
+ </div>
46
+
47
+ <div class="hidden md:block md:w-28 flex-shrink-0">
48
+ <% if variant.generating? %>
49
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
50
+ <%= icon("spinner", css_class: "animate-spin -ml-1 mr-1.5 h-3 w-3 text-purple-800") %>
51
+ Translating
52
+ </span>
53
+ <% else %>
54
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= variant.published? ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' %>">
55
+ <%= variant.published? ? 'Published' : 'Draft' %>
56
+ </span>
57
+ <% end %>
58
+ </div>
59
+
60
+ <div class="w-16 md:w-24 flex items-center justify-end space-x-2 flex-shrink-0">
61
+ <% if variant.generating? %>
62
+ <span class="text-gray-300 cursor-not-allowed inline-flex" title="Editing is available when translation finishes">
63
+ <%= icon("edit", css_class: "w-4 h-4") %>
64
+ </span>
65
+ <% else %>
66
+ <%= link_to edit_article_variant_path(variant.article, variant), class: "text-gray-400 hover:text-gray-600 transition-colors cursor-pointer" do %>
67
+ <%= icon("edit", css_class: "w-4 h-4") %>
68
+ <% end %>
69
+ <% end %>
70
+
71
+ <%= link_to article_variant_path(variant.article, variant), method: :delete,
72
+ class: "text-gray-400 hover:text-red-600 transition-colors cursor-pointer",
73
+ data: {
74
+ turbo_method: :delete,
75
+ turbo_confirm: "Are you sure you want to delete this translation? This action cannot be undone."
76
+ } do %>
77
+ <%= icon("trash", css_class: "w-4 h-4") %>
78
+ <% end %>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ <% end %>
@@ -0,0 +1,48 @@
1
+ <% content_for :title, "Editing #{@article.title}" %>
2
+
3
+ <div data-controller="auto-save publish-guard editor-mobile-sidebars">
4
+ <%= form_with model: @article_variant, url: article_variant_path(@article, @article_variant), local: true, data: { auto_save_target: "form" } do |form| %>
5
+ <%= render 'postnhost/shared/articles/error_messages', resource: @article_variant, resource_type: "variant" %>
6
+
7
+ <div class="md:hidden px-4 py-3 bg-white border-b border-gray-200">
8
+ <div class="grid grid-cols-2 gap-2">
9
+ <button type="button"
10
+ data-action="click->editor-mobile-sidebars#toggleMeta"
11
+ class="w-full bg-white border border-gray-300 text-gray-700 py-2.5 px-4 rounded-xl hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition duration-200 text-sm font-medium cursor-pointer">
12
+ Details
13
+ </button>
14
+ <button type="button"
15
+ data-action="click->editor-mobile-sidebars#toggleActions"
16
+ class="w-full bg-white border border-gray-300 text-gray-700 py-2.5 px-4 rounded-xl hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition duration-200 text-sm font-medium cursor-pointer">
17
+ Actions
18
+ </button>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="flex flex-col md:flex-row min-h-screen">
23
+ <%= render 'postnhost/shared/articles/meta_sidebar',
24
+ form: form,
25
+ resource: @article_variant,
26
+ article: @article,
27
+ is_variant: true,
28
+ languages: nil,
29
+ categories: nil,
30
+ mobile_target: "metaSidebar" %>
31
+
32
+ <%= render 'postnhost/shared/articles/editor_column',
33
+ form: form,
34
+ article: @article,
35
+ content: @article_variant.content,
36
+ is_variant: true,
37
+ placeholder: "Translation Title" %>
38
+
39
+ <%= render 'postnhost/shared/articles/action_buttons',
40
+ form: form,
41
+ resource: @article_variant,
42
+ resource_type: "variant",
43
+ article: @article,
44
+ is_variant: true,
45
+ mobile_target: "actionsSidebar" %>
46
+ </div>
47
+ <% end %>
48
+ </div>
@@ -0,0 +1,128 @@
1
+ <% content_for :title, "Translations for #{@article.title}" %>
2
+
3
+ <%= turbo_stream_from "article_variants_#{@article.id}" %>
4
+ <div class="space-y-8" data-controller="ai-translation dialog" data-action="click->dialog#backdropClose">
5
+ <%= render 'postnhost/shared/elements/page_header',
6
+ title: 'Article Translations',
7
+ description: "Manage translations for \"#{@article.title.present? ? @article.title : 'Untitled Article'}\"",
8
+ stats: [
9
+ { text: "#{@pagy.count} translations", class: '' },
10
+ { text: "#{@available_languages.count} available languages", class: '' }
11
+ ] do %>
12
+ <%= render 'postnhost/shared/elements/button_link',
13
+ text: 'Back to Article',
14
+ url: edit_article_path(@article),
15
+ variant: :secondary,
16
+ icon: 'chevron-left',
17
+ extra_class: 'w-full md:w-auto' %>
18
+
19
+ <% if @available_languages.any? %>
20
+ <button
21
+ data-action="dialog#open"
22
+ class="<%= button_classes %> w-full md:w-auto">
23
+ <%= icon("sparkles", css_class: "w-4 h-4 mr-2") %>
24
+ Translate with AI
25
+ </button>
26
+
27
+ <div class="relative w-full md:w-auto" data-controller="dropdown">
28
+ <button
29
+ data-action="click->dropdown#toggle"
30
+ class="<%= button_classes %> w-full md:w-auto">
31
+ <%= icon("language", css_class: "w-4 h-4 mr-2") %>
32
+ Manual Translation
33
+ <%= icon("chevron-down", css_class: "w-4 h-4 ml-2") %>
34
+ </button>
35
+
36
+ <div data-dropdown-target="menu" class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 overflow-hidden z-20 hidden">
37
+ <div class="py-1">
38
+ <% @available_languages.each do |language| %>
39
+ <%= link_to new_article_variant_path(@article, language_id: language.id), data: { turbo_prefetch: false}, class: "flex items-center px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-150" do %>
40
+ <div class="flex-1">
41
+ <div class="font-medium"><%= language.name %></div>
42
+ <div class="text-xs text-gray-500"><%= language.html_lang %></div>
43
+ </div>
44
+ <% end %>
45
+ <% end %>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ <% else %>
50
+ <span class="<%= button_classes(variant: :secondary, disabled: true) %> w-full md:w-auto">
51
+ All languages translated
52
+ </span>
53
+ <% end %>
54
+ <% end %>
55
+
56
+ <% if @variants.any? %>
57
+ <%= form_with url: "#", method: :patch, data: { controller: "bulk-selection" } do |form| %>
58
+ <div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
59
+ <div data-bulk-selection-target="bulkActions" class="hidden px-6 py-4 bg-gray-50 border-b border-gray-200">
60
+ <div class="flex items-center justify-between">
61
+ <span class="text-sm font-medium text-gray-700">
62
+ <span data-bulk-selection-target="selectedCount">0</span> translation(s) selected
63
+ </span>
64
+ <div class="flex space-x-3">
65
+ <%= form.button type: "submit",
66
+ formaction: bulk_publish_article_variants_path(@article),
67
+ data: { turbo_confirm: "Publish selected translation(s)?" },
68
+ class: "#{button_classes} bg-green-600 hover:bg-green-700" do %>
69
+ Publish
70
+ <% end %>
71
+ <%= form.button type: "submit",
72
+ formaction: bulk_unpublish_article_variants_path(@article),
73
+ data: { turbo_confirm: "Unpublish selected translation(s)?" },
74
+ class: "#{button_classes(variant: :secondary)} border border-gray-300" do %>
75
+ Unpublish
76
+ <% end %>
77
+ <%= form.button type: "submit",
78
+ formaction: bulk_destroy_article_variants_path(@article),
79
+ formmethod: "delete",
80
+ data: { turbo_confirm: "Are you sure you want to delete the selected translation(s)? This action cannot be undone." },
81
+ class: "#{button_classes} bg-red-600 hover:bg-red-700" do %>
82
+ Delete
83
+ <% end %>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
89
+ <div class="flex items-center gap-2 md:gap-4 text-xs font-medium text-gray-500 uppercase tracking-wider">
90
+ <div class="flex items-center justify-center w-6 md:w-8">
91
+ <input
92
+ type="checkbox"
93
+ data-bulk-selection-target="checkAll"
94
+ data-action="change->bulk-selection#toggleAll"
95
+ class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 cursor-pointer"
96
+ >
97
+ </div>
98
+ <div class="flex-1">Translation</div>
99
+ <div class="hidden md:block md:w-32">Language</div>
100
+ <div class="hidden md:block md:w-28">Status</div>
101
+ <div class="w-16 md:w-24 text-right">Actions</div>
102
+ </div>
103
+ </div>
104
+
105
+ <div id="variants_list" class="divide-y divide-gray-200">
106
+ <%= render partial: "variants_list", locals: { variants: @variants } %>
107
+ </div>
108
+ </div>
109
+ <% end %>
110
+
111
+ <!-- Pagination -->
112
+ <% if @pagy.pages > 1 %>
113
+ <div class="mt-8 flex justify-center">
114
+ <%== pagination_nav(@pagy) %>
115
+ </div>
116
+ <% end %>
117
+ <% else %>
118
+ <%= render 'postnhost/shared/elements/empty_state',
119
+ icon: 'translate',
120
+ title: 'No translations',
121
+ description: 'Use "Translate with AI" to automatically create translations, or "Manual Translation" to create them manually.' %>
122
+ <% end %>
123
+
124
+ <%= render "postnhost/shared/ai_translation_modal",
125
+ subject_title: (@article.title.present? ? @article.title : "this article"),
126
+ form_url: article_translations_path(@article),
127
+ available_languages: @available_languages %>
128
+ </div>
@@ -0,0 +1,3 @@
1
+ <%= turbo_stream.replace "article_variant_actions" do %>
2
+ <%= render "postnhost/shared/articles/post_actions", resource: @article_variant, resource_type: "variant", article: @article %>
3
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_stream.replace "article_variant_actions" do %>
2
+ <%= render "postnhost/shared/articles/post_actions", resource: @article_variant, resource_type: "variant", article: @article %>
3
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_stream.replace "article_variant_actions" do %>
2
+ <%= render "postnhost/shared/articles/post_actions", resource: @article_variant, resource_type: "variant", article: @article %>
3
+ <% end %>
@@ -0,0 +1,38 @@
1
+ <div data-controller="author-manager">
2
+ <%= hidden_field_tag "#{form.object_name}[author_ids][]", "" %>
3
+ <div data-author-manager-target="authorList" class="space-y-2 mb-3">
4
+ <% article.authors.each do |author| %>
5
+ <div class="flex items-center justify-between bg-gray-50 px-3 py-2 rounded-xl" data-author-id="<%= author.id %>">
6
+ <span class="text-sm text-gray-900" data-author-name><%= author.name.presence || author.email %></span>
7
+ <button type="button" class="text-gray-400 hover:text-red-600 transition-colors cursor-pointer" data-action="click->author-manager#removeAuthor">
8
+ <%= icon("close", css_class: "w-4 h-4") %>
9
+ </button>
10
+ <%= form.hidden_field :author_ids, value: author.id, multiple: true %>
11
+ </div>
12
+ <% end %>
13
+ </div>
14
+
15
+ <button type="button" data-author-manager-target="addButton" data-action="click->author-manager#showSelector" class="w-full text-left px-3 py-2 border border-dashed border-gray-300 rounded-xl text-sm text-gray-500 hover:border-gray-400 hover:text-gray-700 transition-colors cursor-pointer">
16
+ + Add author
17
+ </button>
18
+
19
+ <div data-author-manager-target="selector" class="hidden mt-2">
20
+ <select data-action="change->author-manager#addAuthor" class="w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm bg-white">
21
+ <option value="">Select an author to add...</option>
22
+ <% users.each do |user| %>
23
+ <option value="<%= user.id %>"><%= user.name.presence || user.email %></option>
24
+ <% end %>
25
+ </select>
26
+ <button type="button" data-action="click->author-manager#hideSelector" class="mt-2 text-xs text-gray-500 hover:text-gray-700 cursor-pointer">Cancel</button>
27
+ </div>
28
+
29
+ <template data-author-manager-target="template">
30
+ <div class="flex items-center justify-between bg-gray-50 px-3 py-2 rounded-xl" data-author-id="">
31
+ <span class="text-sm text-gray-900" data-author-name></span>
32
+ <button type="button" class="text-gray-400 hover:text-red-600 transition-colors cursor-pointer" data-action="click->author-manager#removeAuthor">
33
+ <%= icon("close", css_class: "w-4 h-4") %>
34
+ </button>
35
+ <%= form.hidden_field :author_ids, value: "", multiple: true %>
36
+ </div>
37
+ </template>
38
+ </div>
@@ -0,0 +1,42 @@
1
+ <div data-controller="category-manager">
2
+ <%= hidden_field_tag "#{form.object_name}[category_ids][]", "" %>
3
+ <!-- Current Categories -->
4
+ <div data-category-manager-target="categoryList" class="space-y-2 mb-3">
5
+ <% article.categories.each do |category| %>
6
+ <div class="flex items-center justify-between bg-gray-50 px-3 py-2 rounded-xl" data-category-id="<%= category.id %>">
7
+ <span class="text-sm text-gray-900" data-category-name><%= category.name %></span>
8
+ <button type="button" class="text-gray-400 hover:text-red-600 transition-colors cursor-pointer" data-action="click->category-manager#removeCategory">
9
+ <%= icon("close", css_class: "w-4 h-4") %>
10
+ </button>
11
+ <%= form.hidden_field :category_ids, value: category.id, multiple: true %>
12
+ </div>
13
+ <% end %>
14
+ </div>
15
+
16
+ <!-- Add Category Button -->
17
+ <button type="button" data-category-manager-target="addButton" data-action="click->category-manager#showSelector" class="w-full text-left px-3 py-2 border border-dashed border-gray-300 rounded-xl text-sm text-gray-500 hover:border-gray-400 hover:text-gray-700 transition-colors cursor-pointer">
18
+ + Add category
19
+ </button>
20
+
21
+ <!-- Category Selector (Hidden by default) -->
22
+ <div data-category-manager-target="selector" class="hidden mt-2">
23
+ <select data-action="change->category-manager#addCategory" class="w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm bg-white">
24
+ <option value="">Select a category to add...</option>
25
+ <% categories.each do |category| %>
26
+ <option value="<%= category.id %>"><%= category.name %></option>
27
+ <% end %>
28
+ </select>
29
+ <button type="button" data-action="click->category-manager#hideSelector" class="mt-2 text-xs text-gray-500 hover:text-gray-700 cursor-pointer">Cancel</button>
30
+ </div>
31
+
32
+ <!-- Template for new category items -->
33
+ <template data-category-manager-target="template">
34
+ <div class="flex items-center justify-between bg-gray-50 px-3 py-2 rounded-xl" data-category-id="">
35
+ <span class="text-sm text-gray-900" data-category-name></span>
36
+ <button type="button" class="text-gray-400 hover:text-red-600 transition-colors cursor-pointer" data-action="click->category-manager#removeCategory">
37
+ <%= icon("close", css_class: "w-4 h-4") %>
38
+ </button>
39
+ <%= form.hidden_field :category_ids, value: "", multiple: true %>
40
+ </div>
41
+ </template>
42
+ </div>
@@ -0,0 +1,82 @@
1
+ <div class="px-6 py-4 border-b border-gray-100"
2
+ data-controller="cover-image-upload collapsible-sidebar"
3
+ data-cover-image-upload-article-id-value="<%= article.id %>"
4
+ data-cover-image-upload-mount-path-value="<%= postnhost.root_path %>"
5
+ data-collapsible-sidebar-storage-key-value="article-cover-image-<%= article.id %>">
6
+ <div data-collapsible-sidebar-target="section" data-section-id="cover-image">
7
+ <div class="flex items-center justify-between">
8
+ <button type="button"
9
+ class="inline-flex items-center text-left cursor-pointer"
10
+ data-role="section-toggle"
11
+ data-action="click->collapsible-sidebar#toggle"
12
+ data-collapsible-sidebar-id-param="cover-image"
13
+ aria-expanded="true"
14
+ aria-controls="cover-image-section-<%= article.id %>">
15
+ <span class="text-sm font-medium text-gray-900">Cover Image (ogimage)</span>
16
+ <span data-role="section-icon" class="ml-1 inline-flex text-gray-400 transition-transform duration-200 rotate-0">
17
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
18
+ </span>
19
+ </button>
20
+ <button type="button"
21
+ data-role="section-accessory"
22
+ class="mb-2 inline-flex items-center px-2.5 py-1 bg-gray-200 text-gray-400 text-xs font-medium rounded-lg cursor-not-allowed opacity-60"
23
+ disabled
24
+ title="AI generation coming soon">
25
+ <%= icon("sparkles", css_class: "w-3 h-3 mr-1") %>
26
+ <span>Generate</span>
27
+ </button>
28
+ </div>
29
+
30
+ <div data-role="section-content" id="cover-image-section-<%= article.id %>">
31
+ <!-- Preview with Alt Text -->
32
+ <div data-cover-image-upload-target="preview" class="<%= 'hidden' unless article.cover_image? %> space-y-2">
33
+ <div class="relative">
34
+ <%= image_tag(article.cover_image? ? article.cover_image.url : '',
35
+ alt: article.cover_image_alt,
36
+ width: 1200,
37
+ height: 630,
38
+ data: { cover_image_upload_target: "previewImage" },
39
+ class: "w-full object-cover") %>
40
+ <button type="button" data-action="click->cover-image-upload#removeImage" class="absolute top-2 right-2 bg-white/90 hover:bg-red-50 text-gray-600 hover:text-red-600 p-1.5 rounded-lg shadow transition-colors cursor-pointer">
41
+ <%= icon("trash", css_class: "w-4 h-4") %>
42
+ </button>
43
+ </div>
44
+ <%= form.text_field :cover_image_alt,
45
+ data: { cover_image_upload_target: "altField" },
46
+ class: "w-full px-3 py-1.5 rounded-lg focus:outline-none text-sm text-gray-600",
47
+ placeholder: "Add alt text...",
48
+ autocomplete: "off" %>
49
+ </div>
50
+
51
+ <!-- Compact Upload Area -->
52
+ <div data-cover-image-upload-target="uploadSection" class="<%= 'hidden' if article.cover_image? %>">
53
+ <div data-cover-image-upload-target="uploadArea"
54
+ data-action="dragover->cover-image-upload#dragOver dragleave->cover-image-upload#dragLeave drop->cover-image-upload#drop"
55
+ class="border-2 border-dashed border-gray-300 rounded-lg p-4 text-center hover:border-gray-400 transition-colors">
56
+
57
+ <%= form.file_field :cover_image,
58
+ data: { cover_image_upload_target: "input", action: "change->cover-image-upload#selectFile" },
59
+ accept: "image/jpeg,image/png,image/gif,image/webp,image/heic",
60
+ class: "hidden" %>
61
+
62
+ <!-- Placeholder -->
63
+ <div data-cover-image-upload-target="placeholder">
64
+ <div class="w-8 h-8 bg-gray-100 rounded-lg flex items-center justify-center mx-auto mb-2">
65
+ <%= icon("image", css_class: "w-4 h-4 text-gray-400") %>
66
+ </div>
67
+ <p class="text-xs text-gray-600 mb-1">Drag and drop or <button type="button" data-action="click->cover-image-upload#triggerFileInput" class="text-gray-900 font-medium hover:underline">browse</button></p>
68
+ <p class="text-xs text-gray-400">1200x630 • Max 25MB</p>
69
+ </div>
70
+
71
+ <!-- Loading Spinner -->
72
+ <div data-cover-image-upload-target="spinner" class="hidden flex items-center justify-center">
73
+ <span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
74
+ <%= icon("spinner", css_class: "animate-spin -ml-0.5 mr-1.5 h-3 w-3 text-purple-800") %>
75
+ Uploading...
76
+ </span>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
@@ -0,0 +1,43 @@
1
+ <div data-controller="suggestion-manager">
2
+ <%= hidden_field_tag "#{form.object_name}[suggested_article_ids][]", "" %>
3
+ <!-- Current Suggestions -->
4
+ <div data-suggestion-manager-target="suggestionList" class="space-y-2 mb-3">
5
+ <% article.suggested_articles.each do |suggested| %>
6
+ <div class="flex items-center justify-between bg-gray-50 px-3 py-2 rounded-xl" data-suggestion-id="<%= suggested.id %>">
7
+ <span class="text-sm text-gray-900 truncate" data-suggestion-name><%= suggested.title %></span>
8
+ <button type="button" class="text-gray-400 hover:text-red-600 transition-colors cursor-pointer flex-shrink-0 ml-2" data-action="click->suggestion-manager#removeSuggestion">
9
+ <%= icon("close", css_class: "w-4 h-4") %>
10
+ </button>
11
+ <%= form.hidden_field :suggested_article_ids, value: suggested.id, multiple: true %>
12
+ </div>
13
+ <% end %>
14
+ </div>
15
+
16
+ <!-- Add Suggestion Button -->
17
+ <button type="button" data-suggestion-manager-target="addButton" data-action="click->suggestion-manager#showSelector" class="w-full text-left px-3 py-2 border border-dashed border-gray-300 rounded-xl text-sm text-gray-500 hover:border-gray-400 hover:text-gray-700 transition-colors cursor-pointer">
18
+ + Add suggestion
19
+ </button>
20
+
21
+ <!-- Suggestion Selector (Hidden by default) -->
22
+ <div data-suggestion-manager-target="selector" class="hidden mt-2">
23
+ <select data-action="change->suggestion-manager#addSuggestion" class="w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm bg-white">
24
+ <option value="">Select an article to suggest...</option>
25
+ <% articles.each do |a| %>
26
+ <% next if a.id == article.id %>
27
+ <option value="<%= a.id %>"><%= a.title %></option>
28
+ <% end %>
29
+ </select>
30
+ <button type="button" data-action="click->suggestion-manager#hideSelector" class="mt-2 text-xs text-gray-500 hover:text-gray-700 cursor-pointer">Cancel</button>
31
+ </div>
32
+
33
+ <!-- Template for new suggestion items -->
34
+ <template data-suggestion-manager-target="template">
35
+ <div class="flex items-center justify-between bg-gray-50 px-3 py-2 rounded-xl" data-suggestion-id="">
36
+ <span class="text-sm text-gray-900 truncate" data-suggestion-name></span>
37
+ <button type="button" class="text-gray-400 hover:text-red-600 transition-colors cursor-pointer flex-shrink-0 ml-2" data-action="click->suggestion-manager#removeSuggestion">
38
+ <%= icon("close", css_class: "w-4 h-4") %>
39
+ </button>
40
+ <%= form.hidden_field :suggested_article_ids, value: "", multiple: true %>
41
+ </div>
42
+ </template>
43
+ </div>
@@ -0,0 +1,10 @@
1
+ <%# locals: versioned (Article snapshot from PaperTrail#reify, or nil) %>
2
+ <% if versioned %>
3
+ <h3 class="text-lg font-medium text-gray-900 mb-1 break-words"><%= versioned.title %></h3>
4
+ <% if versioned.meta_description.present? %>
5
+ <p class="text-sm text-gray-600 mb-2 break-words"><%= truncate(versioned.meta_description, length: 120) %></p>
6
+ <% end %>
7
+ <div class="text-xs text-gray-500 break-words">
8
+ Content: <%= truncate(strip_tags(versioned.content), length: 100) %>
9
+ </div>
10
+ <% end %>
@@ -0,0 +1,51 @@
1
+ <% content_for :title, "Editing #{@article.title}" %>
2
+
3
+ <div data-controller="auto-save publish-guard editor-mobile-sidebars">
4
+ <%= form_with model: @article, local: true, data: { auto_save_target: "form" } do |form| %>
5
+ <%= render 'postnhost/shared/articles/error_messages', resource: @article, resource_type: "article" %>
6
+
7
+ <div class="md:hidden px-4 py-3 bg-white border-b border-gray-200">
8
+ <div class="grid grid-cols-2 gap-2">
9
+ <button type="button"
10
+ data-action="click->editor-mobile-sidebars#toggleMeta"
11
+ class="w-full bg-white border border-gray-300 text-gray-700 py-2.5 px-4 rounded-xl hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition duration-200 text-sm font-medium cursor-pointer">
12
+ Details
13
+ </button>
14
+ <button type="button"
15
+ data-action="click->editor-mobile-sidebars#toggleActions"
16
+ class="w-full bg-white border border-gray-300 text-gray-700 py-2.5 px-4 rounded-xl hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition duration-200 text-sm font-medium cursor-pointer">
17
+ Actions
18
+ </button>
19
+ </div>
20
+ </div>
21
+
22
+ <!-- 3-Column Layout -->
23
+ <div class="flex flex-col md:flex-row min-h-screen">
24
+ <%= render 'postnhost/shared/articles/meta_sidebar',
25
+ form: form,
26
+ resource: @article,
27
+ article: @article,
28
+ is_variant: false,
29
+ languages: @languages,
30
+ users: @users,
31
+ categories: @categories,
32
+ articles: @articles,
33
+ mobile_target: "metaSidebar" %>
34
+
35
+ <%= render 'postnhost/shared/articles/editor_column',
36
+ form: form,
37
+ article: @article,
38
+ content: @article.content,
39
+ is_variant: false,
40
+ placeholder: "Untitled Article" %>
41
+
42
+ <%= render 'postnhost/shared/articles/action_buttons',
43
+ form: form,
44
+ resource: @article,
45
+ resource_type: "article",
46
+ article: @article,
47
+ is_variant: false,
48
+ mobile_target: "actionsSidebar" %>
49
+ </div>
50
+ <% end %>
51
+ </div>