brut 0.5.0 → 0.8.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 (265) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/CHANGELOG.md +7 -0
  4. data/Dockerfile.dx +19 -0
  5. data/Gemfile.lock +1 -1
  6. data/README.md +19 -0
  7. data/assets/YouTubeThumb.pxd +0 -0
  8. data/bin/build +86 -0
  9. data/bin/ci +36 -0
  10. data/bin/docs +39 -9
  11. data/bin/publish +61 -0
  12. data/bin/setup +6 -0
  13. data/brut-css/bin/build +19 -0
  14. data/brut-css/bin/ci +19 -0
  15. data/brut-css/bin/docs +19 -0
  16. data/brut-css/bin/publish +21 -0
  17. data/brut-css/bin/setup +1 -0
  18. data/brut-css/package-lock.json +2 -2
  19. data/brut-css/package.json +1 -1
  20. data/brut-js/bin/build +15 -6
  21. data/brut-js/bin/docs +25 -0
  22. data/brut-js/bin/publish +21 -0
  23. data/brut-js/bin/setup +1 -0
  24. data/brut-js/dx +1 -0
  25. data/brut-js/package-lock.json +2 -2
  26. data/brut-js/package.json +1 -1
  27. data/brut.gemspec +2 -2
  28. data/brutrb.com/bin/setup +1 -0
  29. data/brutrb.com/getting-started.md +3 -0
  30. data/brutrb.com/overview.md +6 -0
  31. data/brutrb.com/tutorial.md +7 -3
  32. data/docs/404.html +2 -2
  33. data/docs/adrs.html +3 -3
  34. data/docs/ai.html +3 -3
  35. data/docs/assets/{app.D6BuVHo9.js → app.DyQLb4Ot.js} +1 -1
  36. data/docs/assets/chunks/@localSearchIndexroot.CmtZyrFA.js +1 -0
  37. data/docs/assets/chunks/{VPLocalSearchBox.BpvHMbx6.js → VPLocalSearchBox.T1iA-eJx.js} +1 -1
  38. data/docs/assets/chunks/{theme.wlAOvi2f.js → theme.ChwsbWjK.js} +2 -2
  39. data/docs/assets/{components.md.iLiv2E9X.js → components.md.DHh-NwKs.js} +3 -3
  40. data/docs/assets/{configuration.md.DmuAdsli.js → configuration.md.D8Wz3oJU.js} +1 -1
  41. data/docs/assets/{forms.md.D8aa_qI-.js → forms.md.BRE85eju.js} +1 -1
  42. data/docs/assets/{getting-started.md.DLplsDUd.js → getting-started.md.2ioiTe-B.js} +6 -3
  43. data/docs/assets/{getting-started.md.DLplsDUd.lean.js → getting-started.md.2ioiTe-B.lean.js} +1 -1
  44. data/docs/assets/overview.md.DlKiRRG_.js +1 -0
  45. data/docs/assets/overview.md.DlKiRRG_.lean.js +1 -0
  46. data/docs/assets/tutorial.md.BIb7XT6j.js +1 -0
  47. data/docs/assets/tutorial.md.BIb7XT6j.lean.js +1 -0
  48. data/docs/assets.html +3 -3
  49. data/docs/brut-js.html +3 -3
  50. data/docs/business-logic.html +3 -3
  51. data/docs/cli.html +3 -3
  52. data/docs/components.html +7 -7
  53. data/docs/configuration.html +5 -5
  54. data/docs/css.html +3 -3
  55. data/docs/custom-element-tests.html +3 -3
  56. data/docs/database-access.html +3 -3
  57. data/docs/database-schema.html +3 -3
  58. data/docs/deployment.html +3 -3
  59. data/docs/dev-environment.html +3 -3
  60. data/docs/dir-structure.html +3 -3
  61. data/docs/doc-conventions.html +3 -3
  62. data/docs/end-to-end-tests.html +3 -3
  63. data/docs/features.html +3 -3
  64. data/docs/flash-and-session.html +3 -3
  65. data/docs/form-constraints.html +3 -3
  66. data/docs/forms.html +5 -5
  67. data/docs/getting-started.html +9 -6
  68. data/docs/handlers.html +3 -3
  69. data/docs/hashmap.json +1 -1
  70. data/docs/hooks.html +3 -3
  71. data/docs/i18n.html +3 -3
  72. data/docs/index.html +3 -3
  73. data/docs/instrumentation.html +3 -3
  74. data/docs/javascript.html +3 -3
  75. data/docs/jobs.html +3 -3
  76. data/docs/keyword-injection.html +3 -3
  77. data/docs/layouts.html +3 -3
  78. data/docs/lsp.html +3 -3
  79. data/docs/markdown-examples.html +3 -3
  80. data/docs/middleware.html +3 -3
  81. data/docs/overview.html +5 -5
  82. data/docs/pages.html +3 -3
  83. data/docs/recipes/alternate-layouts.html +3 -3
  84. data/docs/recipes/authentication.html +3 -3
  85. data/docs/recipes/blank-layouts.html +3 -3
  86. data/docs/recipes/custom-flash.html +3 -3
  87. data/docs/recipes/indexed-forms.html +3 -3
  88. data/docs/recipes/migrations.html +3 -3
  89. data/docs/recipes/text-field-component.html +3 -3
  90. data/docs/roadmap.html +3 -3
  91. data/docs/routes.html +3 -3
  92. data/docs/security.html +3 -3
  93. data/docs/seed-data.html +3 -3
  94. data/docs/space-time-continuum.html +3 -3
  95. data/docs/tutorial.html +5 -5
  96. data/docs/unit-tests.html +3 -3
  97. data/docs/why.html +3 -3
  98. data/lib/brut/framework/mcp.rb +1 -1
  99. data/lib/brut/front_end/components/form_tag.rb +2 -2
  100. data/lib/brut/version.rb +1 -1
  101. data/mkbrut/.gitignore +16 -0
  102. data/mkbrut/CODE_OF_CONDUCT.txt +100 -0
  103. data/mkbrut/Gemfile +3 -0
  104. data/mkbrut/Gemfile.lock +19 -0
  105. data/mkbrut/LICENSE.txt +370 -0
  106. data/mkbrut/README.md +145 -0
  107. data/mkbrut/Rakefile +2 -0
  108. data/mkbrut/bin/build +36 -0
  109. data/mkbrut/bin/ci +19 -0
  110. data/mkbrut/bin/docs +19 -0
  111. data/mkbrut/bin/publish +129 -0
  112. data/mkbrut/bin/rake +16 -0
  113. data/mkbrut/bin/setup +30 -0
  114. data/mkbrut/brut-welcome.png +0 -0
  115. data/mkbrut/deploy/.dockerignore +2 -0
  116. data/mkbrut/deploy/Dockerfile +25 -0
  117. data/mkbrut/exe/mkbrut +5 -0
  118. data/mkbrut/lib/mkbrut/app.rb +79 -0
  119. data/mkbrut/lib/mkbrut/app_id.rb +8 -0
  120. data/mkbrut/lib/mkbrut/app_name.rb +29 -0
  121. data/mkbrut/lib/mkbrut/app_options.rb +36 -0
  122. data/mkbrut/lib/mkbrut/base.rb +57 -0
  123. data/mkbrut/lib/mkbrut/cli.rb +107 -0
  124. data/mkbrut/lib/mkbrut/erb_binding_delegate.rb +20 -0
  125. data/mkbrut/lib/mkbrut/internet_identifier.rb +32 -0
  126. data/mkbrut/lib/mkbrut/invalid_identifier.rb +4 -0
  127. data/mkbrut/lib/mkbrut/ops/add_css_import.rb +42 -0
  128. data/mkbrut/lib/mkbrut/ops/add_i18n_message.rb +74 -0
  129. data/mkbrut/lib/mkbrut/ops/add_method.rb +48 -0
  130. data/mkbrut/lib/mkbrut/ops/append_to_file.rb +20 -0
  131. data/mkbrut/lib/mkbrut/ops/base_op.rb +21 -0
  132. data/mkbrut/lib/mkbrut/ops/copy_file.rb +12 -0
  133. data/mkbrut/lib/mkbrut/ops/insert_code_in_method.rb +58 -0
  134. data/mkbrut/lib/mkbrut/ops/insert_route.rb +52 -0
  135. data/mkbrut/lib/mkbrut/ops/mkdir.rb +13 -0
  136. data/mkbrut/lib/mkbrut/ops/prism_parsing_op.rb +70 -0
  137. data/mkbrut/lib/mkbrut/ops/render_template.rb +26 -0
  138. data/mkbrut/lib/mkbrut/ops/skip_file.rb +10 -0
  139. data/mkbrut/lib/mkbrut/ops.rb +16 -0
  140. data/mkbrut/lib/mkbrut/organization.rb +5 -0
  141. data/mkbrut/lib/mkbrut/prefix.rb +26 -0
  142. data/mkbrut/lib/mkbrut/prefixed_io.rb +16 -0
  143. data/mkbrut/lib/mkbrut/segments/bare_bones.rb +185 -0
  144. data/mkbrut/lib/mkbrut/segments/demo.rb +121 -0
  145. data/mkbrut/lib/mkbrut/segments/heroku.rb +30 -0
  146. data/mkbrut/lib/mkbrut/segments/sidekiq.rb +3 -0
  147. data/mkbrut/lib/mkbrut/segments.rb +8 -0
  148. data/mkbrut/lib/mkbrut/version.rb +3 -0
  149. data/mkbrut/lib/mkbrut/versions.rb +13 -0
  150. data/mkbrut/lib/mkbrut.rb +18 -0
  151. data/mkbrut/mkbrut.gemspec +32 -0
  152. data/mkbrut/templates/Base/.dockerignore +25 -0
  153. data/mkbrut/templates/Base/.env.development.erb +60 -0
  154. data/mkbrut/templates/Base/.env.test.erb +8 -0
  155. data/mkbrut/templates/Base/.gitignore +31 -0
  156. data/mkbrut/templates/Base/.projections.json +59 -0
  157. data/mkbrut/templates/Base/Dockerfile.dx +205 -0
  158. data/mkbrut/templates/Base/Gemfile.erb +53 -0
  159. data/mkbrut/templates/Base/Procfile.development +5 -0
  160. data/mkbrut/templates/Base/Procfile.test +1 -0
  161. data/mkbrut/templates/Base/README.md +4 -0
  162. data/mkbrut/templates/Base/README.md.erb +40 -0
  163. data/mkbrut/templates/Base/app/bootstrap.rb +61 -0
  164. data/mkbrut/templates/Base/app/config/i18n/en/1_defaults.rb +128 -0
  165. data/mkbrut/templates/Base/app/config/i18n/en/2_app.rb +24 -0
  166. data/mkbrut/templates/Base/app/public/static/manifest.json.erb +33 -0
  167. data/mkbrut/templates/Base/app/src/app.rb.erb +37 -0
  168. data/mkbrut/templates/Base/app/src/back_end/data_models/app_data_model.rb +5 -0
  169. data/mkbrut/templates/Base/app/src/back_end/data_models/db.rb +19 -0
  170. data/mkbrut/templates/Base/app/src/back_end/data_models/migrations/20240101130000_citext.rb +6 -0
  171. data/mkbrut/templates/Base/app/src/back_end/data_models/seed/seed_data.rb +9 -0
  172. data/mkbrut/templates/Base/app/src/front_end/components/app_component.rb +8 -0
  173. data/mkbrut/templates/Base/app/src/front_end/components/custom_element_registration.rb.erb +7 -0
  174. data/mkbrut/templates/Base/app/src/front_end/css/index.css +2 -0
  175. data/mkbrut/templates/Base/app/src/front_end/css/svgs.css +12 -0
  176. data/mkbrut/templates/Base/app/src/front_end/forms/app_form.rb +4 -0
  177. data/mkbrut/templates/Base/app/src/front_end/handlers/app_handler.rb +4 -0
  178. data/mkbrut/templates/Base/app/src/front_end/images/LogoPylon.png +0 -0
  179. data/mkbrut/templates/Base/app/src/front_end/images/LogoTransit.png +0 -0
  180. data/mkbrut/templates/Base/app/src/front_end/images/apple-touch-icon-120x120.png +0 -0
  181. data/mkbrut/templates/Base/app/src/front_end/images/apple-touch-icon-152x152.png +0 -0
  182. data/mkbrut/templates/Base/app/src/front_end/images/apple-touch-icon-167x167.png +0 -0
  183. data/mkbrut/templates/Base/app/src/front_end/images/apple-touch-icon-180x180.png +0 -0
  184. data/mkbrut/templates/Base/app/src/front_end/images/favicon.ico +0 -0
  185. data/mkbrut/templates/Base/app/src/front_end/images/icon.png +0 -0
  186. data/mkbrut/templates/Base/app/src/front_end/images/mkicons.sh +6 -0
  187. data/mkbrut/templates/Base/app/src/front_end/js/index.js +6 -0
  188. data/mkbrut/templates/Base/app/src/front_end/layouts/default_layout.rb.erb +73 -0
  189. data/mkbrut/templates/Base/app/src/front_end/pages/app_page.rb +11 -0
  190. data/mkbrut/templates/Base/app/src/front_end/pages/home_page.rb +62 -0
  191. data/mkbrut/templates/Base/app/src/front_end/support/app_session.rb +6 -0
  192. data/mkbrut/templates/Base/app/src/front_end/svgs/README.md +5 -0
  193. data/mkbrut/templates/Base/app/src/front_end/svgs/comment-button.svg +59 -0
  194. data/mkbrut/templates/Base/bin/README.md.erb +5 -0
  195. data/mkbrut/templates/Base/bin/build-assets +7 -0
  196. data/mkbrut/templates/Base/bin/ci +39 -0
  197. data/mkbrut/templates/Base/bin/console +31 -0
  198. data/mkbrut/templates/Base/bin/db +9 -0
  199. data/mkbrut/templates/Base/bin/dbconsole +51 -0
  200. data/mkbrut/templates/Base/bin/dev +25 -0
  201. data/mkbrut/templates/Base/bin/release +26 -0
  202. data/mkbrut/templates/Base/bin/run +86 -0
  203. data/mkbrut/templates/Base/bin/scaffold +9 -0
  204. data/mkbrut/templates/Base/bin/setup +256 -0
  205. data/mkbrut/templates/Base/bin/startup-message +65 -0
  206. data/mkbrut/templates/Base/bin/test +9 -0
  207. data/mkbrut/templates/Base/bin/test-server +29 -0
  208. data/mkbrut/templates/Base/bin/watch-and-build-assets +37 -0
  209. data/mkbrut/templates/Base/config.ru +16 -0
  210. data/mkbrut/templates/Base/docker-compose.dx.yml +92 -0
  211. data/mkbrut/templates/Base/dx/README.md +28 -0
  212. data/mkbrut/templates/Base/dx/bash_customizations +12 -0
  213. data/mkbrut/templates/Base/dx/bash_customizations.local +8 -0
  214. data/mkbrut/templates/Base/dx/build +107 -0
  215. data/mkbrut/templates/Base/dx/docker-compose.env.erb +25 -0
  216. data/mkbrut/templates/Base/dx/dx.sh.lib +137 -0
  217. data/mkbrut/templates/Base/dx/exec +68 -0
  218. data/mkbrut/templates/Base/dx/prune +19 -0
  219. data/mkbrut/templates/Base/dx/show-help-in-app-container-then-wait.sh +38 -0
  220. data/mkbrut/templates/Base/dx/start +30 -0
  221. data/mkbrut/templates/Base/dx/stop +23 -0
  222. data/mkbrut/templates/Base/package.json.erb +37 -0
  223. data/mkbrut/templates/Base/puma.config.rb +53 -0
  224. data/mkbrut/templates/Base/specs/e2e/home_page.spec.rb.erb +23 -0
  225. data/mkbrut/templates/Base/specs/front_end/js/SpecHelper.js +24 -0
  226. data/mkbrut/templates/Base/specs/front_end/pages/home_page.spec.rb +22 -0
  227. data/mkbrut/templates/Base/specs/lint_factories.spec.rb +7 -0
  228. data/mkbrut/templates/Base/specs/spec_helper.rb +78 -0
  229. data/mkbrut/templates/Base/specs/support.rb +2 -0
  230. data/mkbrut/templates/segments/BareBones/app/src/front_end/handlers/trigger_exception_handler.rb +24 -0
  231. data/mkbrut/templates/segments/BareBones/app/src/front_end/js/Example.js.erb +49 -0
  232. data/mkbrut/templates/segments/BareBones/specs/front_end/handlers/trigger_exception_handler.spec.rb +41 -0
  233. data/mkbrut/templates/segments/BareBones/specs/front_end/js/Example.spec.js.erb +38 -0
  234. data/mkbrut/templates/segments/Demo/app/src/back_end/data_models/db/guestbook_message.rb +3 -0
  235. data/mkbrut/templates/segments/Demo/app/src/back_end/data_models/migrations/20250628194124_guestbook.rb +14 -0
  236. data/mkbrut/templates/segments/Demo/app/src/front_end/components/flash_component.rb +36 -0
  237. data/mkbrut/templates/segments/Demo/app/src/front_end/css/constraint-violations.css +18 -0
  238. data/mkbrut/templates/segments/Demo/app/src/front_end/css/fonts.css +19 -0
  239. data/mkbrut/templates/segments/Demo/app/src/front_end/fonts/monaspace-xenon.ttf +0 -0
  240. data/mkbrut/templates/segments/Demo/app/src/front_end/forms/guestbook_message_form.rb +4 -0
  241. data/mkbrut/templates/segments/Demo/app/src/front_end/handlers/guestbook_message_handler.rb +64 -0
  242. data/mkbrut/templates/segments/Demo/app/src/front_end/pages/guestbook_page/message_component.rb +41 -0
  243. data/mkbrut/templates/segments/Demo/app/src/front_end/pages/guestbook_page.rb +43 -0
  244. data/mkbrut/templates/segments/Demo/app/src/front_end/pages/new_guestbook_message_page.rb +64 -0
  245. data/mkbrut/templates/segments/Demo/specs/back_end/data_models/db/guestbook_message.spec.rb +5 -0
  246. data/mkbrut/templates/segments/Demo/specs/e2e/guest_message.spec.rb +54 -0
  247. data/mkbrut/templates/segments/Demo/specs/factories/db/guestbook_message.factory.rb +7 -0
  248. data/mkbrut/templates/segments/Demo/specs/front_end/components/flash_component.spec.rb +5 -0
  249. data/mkbrut/templates/segments/Demo/specs/front_end/handlers/guestbook_message_handler.spec.rb +122 -0
  250. data/mkbrut/templates/segments/Demo/specs/front_end/pages/guestbook_page/message_component.spec.rb +5 -0
  251. data/mkbrut/templates/segments/Demo/specs/front_end/pages/guestbook_page.spec.rb +52 -0
  252. data/mkbrut/templates/segments/Demo/specs/front_end/pages/new_guestbook_message_page.spec.rb +5 -0
  253. data/mkbrut/templates/segments/Heroku/bin/deploy +11 -0
  254. data/mkbrut/templates/segments/Heroku/deploy/Dockerfile +125 -0
  255. data/mkbrut/templates/segments/Heroku/deploy/docker-entrypoint +15 -0
  256. data/mkbrut/templates/segments/Heroku/deploy/heroku_config.rb +26 -0
  257. metadata +185 -21
  258. data/docs/assets/chunks/@localSearchIndexroot.COP2Bcmp.js +0 -1
  259. data/docs/assets/overview.md.iMnwLO4x.js +0 -1
  260. data/docs/assets/overview.md.iMnwLO4x.lean.js +0 -1
  261. data/docs/assets/tutorial.md.BYXj4cOu.js +0 -1
  262. data/docs/assets/tutorial.md.BYXj4cOu.lean.js +0 -1
  263. /data/docs/assets/{components.md.iLiv2E9X.lean.js → components.md.DHh-NwKs.lean.js} +0 -0
  264. /data/docs/assets/{configuration.md.DmuAdsli.lean.js → configuration.md.D8Wz3oJU.lean.js} +0 -0
  265. /data/docs/assets/{forms.md.D8aa_qI-.lean.js → forms.md.BRE85eju.lean.js} +0 -0
@@ -0,0 +1,41 @@
1
+ class GuestbookPage::MessageComponent < AppComponent
2
+ def initialize(message:, index:, current_visitor_message: false)
3
+ @message = message
4
+ @current_visitor_message = current_visitor_message
5
+ @background_color = [
6
+ "bg-gray-800",
7
+ "bg-blue-800",
8
+ "bg-green-800",
9
+ "bg-yellow-800",
10
+ ][index % 4]
11
+ end
12
+
13
+ def view_template
14
+ div_classes = [
15
+ "measure",
16
+ "mh-auto",
17
+ "pa-3",
18
+ "br-4",
19
+ "br-tr-0",
20
+ "br-bl-0",
21
+ "mt-3",
22
+ @background_color,
23
+ ]
24
+ border_class = if @current_visitor_message
25
+ div_classes << "ba" << "bc-gray-600" << "bw-2"
26
+ else
27
+ div_classes << "bn"
28
+ end
29
+ div(class: div_classes, id: @message.external_id) do
30
+ blockquote(class: "ff-cursive p mh-auto i f-4") do
31
+ @message.message
32
+ end
33
+ p(class: "p mh-auto tr f-2") do
34
+ plain("—")
35
+ plain(@message.name)
36
+ plain(" on ")
37
+ TimeTag(date: @message.created_at, format: :date, class: "gray-400 f-1 ttu")
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ class GuestbookPage < AppPage
2
+
3
+ def initialize(session:)
4
+ @session = session
5
+ end
6
+
7
+ def page_template
8
+ div(class: "flex flex-column items-center ff-sans") do
9
+ article(class: "w-50 pv-3 bt bc-gray-700", id: "guestbook") do
10
+ h2(class: "ff-sans ma-0 lh-title f-4 tc") do
11
+ "Check out the Guestbook!"
12
+ end
13
+ if !@session.signed_guestbook?
14
+ a(
15
+ href: NewGuestbookMessagePage.routing,
16
+ class: "db tc mv-3 f-2 fw-5 blue-300"
17
+ ) do
18
+ "Sign it, yourself!"
19
+ end
20
+ else
21
+ a(
22
+ href: "##{@session.guestbook_message.external_id}",
23
+ class: "db tc mv-3 f-2 fw-5 blue-300"
24
+ ) do
25
+ "Check out your message"
26
+ end
27
+
28
+ end
29
+ DB::GuestbookMessage.order(
30
+ Sequel.function(:random)
31
+ ).each_with_index do |message,index|
32
+ render(
33
+ GuestbookPage::MessageComponent.new(
34
+ message:,
35
+ index:,
36
+ current_visitor_message: message == @session.guestbook_message
37
+ )
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,64 @@
1
+ class NewGuestbookMessagePage < AppPage
2
+ def initialize(form: nil, session:)
3
+ @form = form || GuestbookMessageForm.new
4
+ @session = session
5
+ end
6
+
7
+ def before_generate
8
+ if @session.signed_guestbook?
9
+ redirect_to(GuestbookPage)
10
+ end
11
+ end
12
+
13
+ def page_template
14
+ div(class: "flex flex-column items-center ff-sans") do
15
+ render global_component(FlashComponent)
16
+ div(class: "w-50 mv-5") do
17
+ brut_form(class: "w-100") do
18
+ FormTag(for: @form, class: "flex flex-column gap-3 items-end") do
19
+ h1(class: "ff-sans ma-0 lh-title f-5 self-start flex items-center gap-3") do
20
+ img(src: "/static/images/icon.png", class: "w-5")
21
+ span { "Sign The Guestbook" }
22
+ end
23
+ label(class: "flex flex-column gap-1 w-100") do
24
+ Inputs::InputTag(
25
+ form: @form,
26
+ input_name: "name",
27
+ placeholder: "e.g. Pat",
28
+ class: "f-3 pa-2 inset-shadow-1 br-2",
29
+ "data-1p-ignore": true
30
+ )
31
+ div(class: "flex items-start gap-3") do
32
+ span { "Your Message" }
33
+ ConstraintViolations(
34
+ form: @form,
35
+ input_name: "name"
36
+ )
37
+ end
38
+
39
+ end
40
+ label(class: "flex flex-column gap-1 w-100") do
41
+ Inputs::TextareaTag(
42
+ form: @form,
43
+ input_name: "message",
44
+ placeholder: "e.g. What a great site!",
45
+ rows: 6,
46
+ class: "f-3 pa-2 inset-shadow-1 br-2"
47
+ )
48
+ div(class: "flex items-start gap-3") do
49
+ span { "Your Message" }
50
+ ConstraintViolations(
51
+ form: @form,
52
+ input_name: "message"
53
+ )
54
+ end
55
+ end
56
+ button(class: "bg-green-900 green-300 ph-5 pv-3 f-3 ba bc-green-600 br-pill shadow-1 pointer") do
57
+ "Sign!"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe DB::GuestbookMessage do
4
+ implementation_is_trivial
5
+ end
@@ -0,0 +1,54 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe "You can leave a guestbook message" do
4
+ it "accepts your message after validation, then shows it with the others" do
5
+ page.goto(HomePage.routing)
6
+
7
+ link = page.locator("a[href='#{NewGuestbookMessagePage.routing}']")
8
+
9
+ link.click
10
+
11
+ expect(page).to be_page_for(NewGuestbookMessagePage)
12
+
13
+ button = page.locator("form button")
14
+
15
+ button.click
16
+ expect(page).to be_page_for(NewGuestbookMessagePage)
17
+
18
+ name_error_message = page.locator("brut-cv-messages[input-name='name']")
19
+ message_error_message = page.locator("brut-cv-messages[input-name='message']")
20
+
21
+ this_field = t("cv.this_field")
22
+
23
+ expect(name_error_message).to have_text(
24
+ t("cv.cs.valueMissing", field: this_field)
25
+ )
26
+ expect(message_error_message).to have_text(
27
+ t("cv.cs.valueMissing", field: this_field)
28
+ )
29
+
30
+ name_field = page.locator("form input[name='name']")
31
+ message_field = page.locator("form textarea[name='message']")
32
+
33
+ name_field.fill("Pat")
34
+ message_field.fill("FIRSTPOST!")
35
+
36
+ button.click
37
+ expect(page).to be_page_for(NewGuestbookMessagePage)
38
+
39
+ flash = page.locator("[role='alert']")
40
+ expect(flash).to have_text(t(:guestbook_not_saved))
41
+ expect(message_error_message).to have_text(
42
+ t("cv.ss.not_enough_words", field: this_field)
43
+ )
44
+
45
+ message_field.fill("OK, fine, this site is awesome!")
46
+ button.click
47
+
48
+ expect(page).to be_page_for(GuestbookPage)
49
+
50
+ quotes = page.locator("blockquote")
51
+ expect(quotes).to have_text("OK, fine, this site is awesome!")
52
+
53
+ end
54
+ end
@@ -0,0 +1,7 @@
1
+ FactoryBot.define do
2
+ factory :guestbook_message, class: "DB::GuestbookMessage" do
3
+ name { Faker::Name.name }
4
+ message { Faker::Lorem.paragraph }
5
+ ip_address { Faker::Internet.unique.ip_v4_address }
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe FlashComponent do
4
+ implementation_is_trivial
5
+ end
@@ -0,0 +1,122 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe GuestbookMessageHandler do
4
+ describe "#handle!" do
5
+ context "there are client-side constraint violations" do
6
+ it "generates the new guestbook message page" do
7
+ form = GuestbookMessageForm.new(params: {})
8
+ flash = empty_flash
9
+ session = empty_session
10
+ rack_request_ip = Faker::Internet.ip_v4_address
11
+
12
+ confidence_check do
13
+ expect(form).to have_constraint_violation(:name,key: :valueMissing)
14
+ end
15
+
16
+ handler = described_class.new(
17
+ form:,
18
+ flash:,
19
+ session:,
20
+ rack_request_ip:
21
+ )
22
+ result = nil
23
+ expect {
24
+ result = handler.handle
25
+ }.not_to change { DB::GuestbookMessage.count }
26
+
27
+ expect(result).to have_generated(NewGuestbookMessagePage)
28
+ expect(flash.alert).to eq("guestbook_not_saved")
29
+ end
30
+ end
31
+ context "there are server-side constraint violations" do
32
+ it "indicates a server-side error and generates the new guestbook message page" do
33
+ form = GuestbookMessageForm.new(params: {
34
+ name: "Pat",
35
+ message: "FIRSTPOST!!!!",
36
+ })
37
+ flash = empty_flash
38
+ session = empty_session
39
+ rack_request_ip = Faker::Internet.ip_v4_address
40
+
41
+ handler = described_class.new(
42
+ form:,
43
+ flash:,
44
+ session:,
45
+ rack_request_ip:
46
+ )
47
+ result = nil
48
+ expect {
49
+ result = handler.handle
50
+ }.not_to change { DB::GuestbookMessage.count }
51
+
52
+ expect(result).to have_generated(NewGuestbookMessagePage)
53
+ expect(form).to have_constraint_violation(:message, key: :not_enough_words)
54
+ expect(flash.alert).to eq("guestbook_not_saved")
55
+ end
56
+ end
57
+ context "the ip address has already been used" do
58
+
59
+ it "indicates a server-side error and generates the new guestbook message page" do
60
+ existing_message = create(:guestbook_message)
61
+
62
+ form = GuestbookMessageForm.new(params: {
63
+ name: "Pat",
64
+ message: "This website is amazing!",
65
+ })
66
+ flash = empty_flash
67
+ session = empty_session
68
+ rack_request_ip = existing_message.ip_address
69
+
70
+ handler = described_class.new(
71
+ form:,
72
+ flash:,
73
+ session:,
74
+ rack_request_ip:
75
+ )
76
+ result = nil
77
+ expect {
78
+ result = handler.handle
79
+ }.not_to change { DB::GuestbookMessage.count }
80
+
81
+ expect(result).to have_generated(NewGuestbookMessagePage)
82
+ expect(form).to have_constraint_violation(:name, key: :already_posted)
83
+ expect(flash.alert).to eq("guestbook_not_saved")
84
+ end
85
+ end
86
+ context "no constraint violations, unused ip address" do
87
+ it "saves the message and redirects back to the guestbook page" do
88
+ form = GuestbookMessageForm.new(params: {
89
+ name: "Pat",
90
+ message: "This website is amazing!",
91
+ })
92
+ flash = empty_flash
93
+ session = empty_session
94
+ rack_request_ip = Faker::Internet.ip_v4_address
95
+
96
+ handler = described_class.new(
97
+ form:,
98
+ flash:,
99
+ session:,
100
+ rack_request_ip:
101
+ )
102
+ result = nil
103
+ expect {
104
+ result = handler.handle
105
+ }.to change { DB::GuestbookMessage.count }.by(1)
106
+
107
+ message = DB::GuestbookMessage.order(Sequel.desc(:created_at)).first
108
+
109
+ expect(message.name).to eq("Pat")
110
+ expect(message.message).to eq("This website is amazing!")
111
+ expect(message.ip_address).to eq(rack_request_ip)
112
+
113
+ expect(result).to have_redirected_to(GuestbookPage,
114
+ anchor: message.external_id)
115
+ expect(form).not_to have_constraint_violation(:name,
116
+ key: :already_posted)
117
+ expect(flash.notice).to eq("guestbook_saved")
118
+
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,5 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe GuestbookPage::MessageComponent do
4
+ implementation_is_covered_by_other_tests "e2e tests and page's test"
5
+ end
@@ -0,0 +1,52 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe GuestbookPage do
4
+ it "shows current guestbook messages" do
5
+ messages = 5.times.map {
6
+ create(:guestbook_message)
7
+ }
8
+ confidence_check { expect(DB::GuestbookMessage.all.length).to eq(5) }
9
+
10
+ result = generate_and_parse(self.described_class.new(session: empty_session))
11
+
12
+ message_elements = result.css("div:has(> blockquote)")
13
+ expect(message_elements.length).to eq(5)
14
+ messages.each do |message|
15
+ element = message_elements.select {
16
+ it.css("blockquote").text.strip == message.message
17
+ }
18
+ expect(element.size).to eq(1),
19
+ "Found #{element.size} blockquotes for message #{message.external_id}, instead of exactly 1"
20
+ expect(element[0].css("p").text).to include(message.name)
21
+ end
22
+ end
23
+ context "when you've signed the guestbook" do
24
+ it "shows a link to view your message and not a link to make one" do
25
+ message = create(:guestbook_message)
26
+ session = empty_session
27
+
28
+ session.signed_guestbook(message)
29
+
30
+ result = generate_and_parse(self.described_class.new(session:))
31
+
32
+ add_link = result.e("a[href='#{NewGuestbookMessagePage.routing}']")
33
+ expect(add_link).to eq(nil)
34
+
35
+ view_link = result.e("a[href='##{message.external_id}']")
36
+ expect(view_link).not_to eq(nil)
37
+ end
38
+ end
39
+ context "when you have not signed the guestbook" do
40
+ it "shows a link to view your message and not a link to make one" do
41
+ message = create(:guestbook_message)
42
+
43
+ result = generate_and_parse(self.described_class.new(session: empty_session))
44
+
45
+ add_link = result.e("a[href='#{NewGuestbookMessagePage.routing}']")
46
+ expect(add_link).not_to eq(nil)
47
+
48
+ view_link = result.e("a[href='##{message.external_id}']")
49
+ expect(view_link).to eq(nil)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe NewGuestbookMessagePage do
4
+ implementation_is_covered_by_other_tests "e2e tests"
5
+ end
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler"
4
+ Bundler.require
5
+ require "pathname"
6
+ require "brut/cli/apps/heroku_container_based_deploy"
7
+
8
+ exit Brut::CLI.app(
9
+ Brut::CLI::Apps::HerokuContainerBasedDeploy,
10
+ project_root: Pathname($0).dirname / ".."
11
+ )
@@ -0,0 +1,125 @@
1
+ # This is a multi-stage build, meaning one image will be built that
2
+ # has necessarily dev tools in it, and that image is used to create artifacts
3
+ # that will be copied into the second build, which will create the image
4
+ # you deploy to production.
5
+ #
6
+ # # Maintaing this file
7
+ #
8
+ # You own this file now. While there could be a way to create it from
9
+ # the Dockerfile.dx, for now, you'll need to stay on top of it:
10
+ #
11
+ # 1 - ensure the versions of Ruby match
12
+ # 2 - ensure the versions of NodeJS match
13
+ # 3 - ensure that anything you installed in Dockerfile.dx to make
14
+ # the app work or to pre-generate assets is set up here as well.
15
+ # You are advised to make that setup identical.
16
+ #
17
+ # You can test this locally but using the `--plaform` flag to `bin/deploy deploy`
18
+ # and running a container locally. You'll need all infrastructure available
19
+ # but it can be done to test things before you deploy. If you need to.
20
+
21
+ # Use Ruby 3.4 as a base.
22
+ FROM docker.io/library/ruby:3.4 AS base
23
+
24
+ # bin/deploy will inject this value so that your app's GIT SHA1
25
+ # is in the environment in production, thus allowing you to be more
26
+ # sure of what's actually running.
27
+ ARG app_git_sha1
28
+
29
+ WORKDIR /brut-app
30
+
31
+ # Install base packages
32
+ #
33
+ # - ca-certificates is needed for other installs
34
+ # - curl is needed generally for other installs and by Heroku
35
+ # - gnupg is a PGP replacement used in making sure new APT repos work
36
+ # - libjemalloc2 in theory speeds up Ruby
37
+ # - lsb-release is used to generically access information for this OS's version
38
+ # - wget allows us to copy/paste commands from vendors about how to install
39
+ # software even though it does the same thing as curl
40
+ RUN apt-get update --quiet --yes && \
41
+ apt-get install --no-install-recommends --quiet --yes \
42
+ ca-certificates \
43
+ curl \
44
+ gnupg \
45
+ libjemalloc2 \
46
+ lsb-release \
47
+ rsync \
48
+ wget && \
49
+ rm -rf /var/lib/apt/lists /var/cache/apt/archives
50
+
51
+ # Install the PostgreSQL client. The latest version is not available
52
+ # from Debian, so we set up our own. This should match what's in Dockerfile.dx
53
+ # and ideally the version of Postgres used in production.
54
+ #
55
+ # Incancation is based on: https://www.postgresql.org/download/linux/debian/
56
+ RUN sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \
57
+ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
58
+ apt-get update && \
59
+ apt-get --yes --quiet install postgresql-client-16 && \
60
+ rm -rf /var/lib/apt/lists /var/cache/apt/archives
61
+
62
+ # Set basic env vars for production
63
+ ENV RACK_ENV="production" \
64
+ BUNDLE_DEPLOYMENT="1" \
65
+ BUNDLE_PATH="/usr/local/bundle" \
66
+ BUNDLE_WITHOUT="development" \
67
+ APP_GIT_SHA1="${app_git_sha1}"
68
+
69
+ # This makes a new image that we'll throw away after building
70
+ # needed artifacts.
71
+ FROM base AS build
72
+
73
+ # - build-essential is needed for almost any build tool we have to install
74
+ # - git is needed to install some things
75
+ # - libpq-dev is needed by postgres
76
+ # - pkg-config is, I guess, not considered "essential" (as in build-essential),
77
+ # but still needed to install downstream stuff
78
+ RUN apt-get update --quiet --yes && \
79
+ apt-get install --no-install-recommends --quiet --yes \
80
+ build-essential \
81
+ git \
82
+ libpq-dev \
83
+ pkg-config
84
+
85
+ # Install NodeJS, per https://nodejs.org/en/download
86
+ RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash && \
87
+ \. "$HOME/.nvm/nvm.sh" && \
88
+ nvm install 22 && \
89
+ node -v && nvm current && npm -v
90
+
91
+ # Copy the app into the file, excluding the contents of .dockerignore
92
+ COPY . .
93
+
94
+ # Install RubyGems from app's Gemfile
95
+ RUN bundle install --verbose && \
96
+ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git
97
+
98
+ # Install Node Modules from package.json
99
+ RUN \. "$HOME/.nvm/nvm.sh" && \
100
+ nvm use default && \
101
+ npm clean-install --no-audit --no-fund --verbose
102
+
103
+ # Build all assets
104
+ RUN \. "$HOME/.nvm/nvm.sh" && \
105
+ nvm use default && \
106
+ bin/build-assets && \
107
+ rm -rf node_modules
108
+
109
+ # We are now switching back to building the image that will be deployed.
110
+
111
+ FROM base
112
+ # Copy built artifacts from the throwaway image: gems, application
113
+ COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
114
+ COPY --from=build /brut-app /brut-app
115
+
116
+ # For security, set directories that will be written to be owned by non-root
117
+ RUN groupadd --system --gid 1000 brut && \
118
+ useradd brut --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
119
+ chown -R brut:brut logs tmp
120
+ USER 1000:1000
121
+
122
+ # This is used to execute other commands. When the app is run in production,
123
+ # this script is used to run it.
124
+ ENTRYPOINT ["/brut-app/deploy/docker-entrypoint"]
125
+
@@ -0,0 +1,15 @@
1
+ #!/bin/bash -e
2
+
3
+ # This is the Docker entrypoint, mean this script is used to execute
4
+ # the command given to the RUN directive.
5
+ #
6
+ # The reason this exists is to load jemalloc. If you don't want to use
7
+ # jemalloc, you can delete this script, as well as the ENTRYPOINT directive
8
+ # in deploy/Dockerfile.
9
+
10
+ # Enable jemalloc for reduced memory usage and latency.
11
+ if [ -z "${LD_PRELOAD+x}" ] && [ -f /usr/lib/*/libjemalloc.so.2 ]; then
12
+ export LD_PRELOAD="$(echo /usr/lib/*/libjemalloc.so.2)"
13
+ fi
14
+
15
+ exec "${@}"
@@ -0,0 +1,26 @@
1
+ # This contains settings for your Heroku setup.
2
+ # You own and maintain this file. It is required by
3
+ # build/deploy
4
+ class HerokuConfig
5
+
6
+ # Return a Hash oif additional images to run, beyond "web" and "release".
7
+ #
8
+ # The format of this Hash is:
9
+ #
10
+ # {
11
+ # "«image name» => {
12
+ # cmd: "«command line for Dockerfile RUN directive»",
13
+ # }
14
+ # }
15
+ #
16
+ # For example, if you have the Sidekiq segment installed, `bin/run-sidekiq`
17
+ # runs Sidekiq, so you would return this hash:
18
+ #
19
+ # {
20
+ # "sidekiq" => {
21
+ # cmd: "bin/run-sidekiq",
22
+ # }
23
+ # }
24
+ def self.additional_images = {}
25
+ end
26
+