formblocks 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 (86) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +57 -0
  3. data/CHANGELOG.md +24 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +472 -0
  6. data/Rakefile +24 -0
  7. data/app/controllers/concerns/formblocks/request_context.rb +58 -0
  8. data/app/controllers/formblocks/application_controller.rb +16 -0
  9. data/app/controllers/formblocks/assets_controller.rb +27 -0
  10. data/app/controllers/formblocks/blocks_controller.rb +81 -0
  11. data/app/controllers/formblocks/dashboard_controller.rb +41 -0
  12. data/app/controllers/formblocks/forms_controller.rb +71 -0
  13. data/app/controllers/formblocks/pages_controller.rb +44 -0
  14. data/app/controllers/formblocks/public/forms_controller.rb +74 -0
  15. data/app/controllers/formblocks/responses_controller.rb +39 -0
  16. data/app/controllers/formblocks/settings_controller.rb +30 -0
  17. data/app/helpers/formblocks/application_helper.rb +93 -0
  18. data/app/models/concerns/formblocks/attachable.rb +43 -0
  19. data/app/models/formblocks/application_record.rb +7 -0
  20. data/app/models/formblocks/block.rb +177 -0
  21. data/app/models/formblocks/blocks/checkbox.rb +22 -0
  22. data/app/models/formblocks/blocks/email.rb +23 -0
  23. data/app/models/formblocks/blocks/heading.rb +13 -0
  24. data/app/models/formblocks/blocks/hidden.rb +25 -0
  25. data/app/models/formblocks/blocks/image.rb +14 -0
  26. data/app/models/formblocks/blocks/input.rb +59 -0
  27. data/app/models/formblocks/blocks/name.rb +9 -0
  28. data/app/models/formblocks/blocks/paragraph.rb +13 -0
  29. data/app/models/formblocks/blocks/phone.rb +13 -0
  30. data/app/models/formblocks/blocks/radio_group.rb +28 -0
  31. data/app/models/formblocks/blocks/text.rb +12 -0
  32. data/app/models/formblocks/blocks/textarea.rb +8 -0
  33. data/app/models/formblocks/blocks/url.rb +26 -0
  34. data/app/models/formblocks/csv_export.rb +47 -0
  35. data/app/models/formblocks/form.rb +197 -0
  36. data/app/models/formblocks/page.rb +79 -0
  37. data/app/models/formblocks/response.rb +48 -0
  38. data/app/models/formblocks/setting.rb +19 -0
  39. data/app/views/formblocks/blocks/_block.html.erb +102 -0
  40. data/app/views/formblocks/blocks/_palette.html.erb +16 -0
  41. data/app/views/formblocks/forms/_header.html.erb +19 -0
  42. data/app/views/formblocks/forms/edit.html.erb +38 -0
  43. data/app/views/formblocks/forms/index.html.erb +48 -0
  44. data/app/views/formblocks/forms/new.html.erb +26 -0
  45. data/app/views/formblocks/forms/published.html.erb +25 -0
  46. data/app/views/formblocks/forms/settings.html.erb +43 -0
  47. data/app/views/formblocks/pages/_page.html.erb +51 -0
  48. data/app/views/formblocks/public/blocks/_checkbox.html.erb +15 -0
  49. data/app/views/formblocks/public/blocks/_field.html.erb +13 -0
  50. data/app/views/formblocks/public/blocks/_heading.html.erb +1 -0
  51. data/app/views/formblocks/public/blocks/_hidden.html.erb +5 -0
  52. data/app/views/formblocks/public/blocks/_image.html.erb +3 -0
  53. data/app/views/formblocks/public/blocks/_paragraph.html.erb +1 -0
  54. data/app/views/formblocks/public/blocks/_radio_group.html.erb +17 -0
  55. data/app/views/formblocks/public/blocks/_text.html.erb +6 -0
  56. data/app/views/formblocks/public/blocks/_textarea.html.erb +5 -0
  57. data/app/views/formblocks/public/forms/not_found.html.erb +3 -0
  58. data/app/views/formblocks/public/forms/show.html.erb +37 -0
  59. data/app/views/formblocks/public/forms/thanks.html.erb +5 -0
  60. data/app/views/formblocks/responses/index.html.erb +45 -0
  61. data/app/views/formblocks/responses/show.html.erb +43 -0
  62. data/app/views/formblocks/settings/show.html.erb +19 -0
  63. data/app/views/formblocks/shared/_admin.html.erb +21 -0
  64. data/app/views/formblocks/shared/_brand_fields.html.erb +29 -0
  65. data/app/views/formblocks/shared/_errors.html.erb +7 -0
  66. data/app/views/layouts/formblocks/application.html.erb +18 -0
  67. data/app/views/layouts/formblocks/public.html.erb +20 -0
  68. data/config/locales/formblocks.en.yml +188 -0
  69. data/config/routes.rb +35 -0
  70. data/lib/formblocks/assets/admin.css +212 -0
  71. data/lib/formblocks/assets/admin.js +215 -0
  72. data/lib/formblocks/assets/public.css +66 -0
  73. data/lib/formblocks/assets/public.js +86 -0
  74. data/lib/formblocks/assets.rb +81 -0
  75. data/lib/formblocks/configuration.rb +93 -0
  76. data/lib/formblocks/engine.rb +33 -0
  77. data/lib/formblocks/seeds.rb +50 -0
  78. data/lib/formblocks/templates.rb +97 -0
  79. data/lib/formblocks/version.rb +5 -0
  80. data/lib/formblocks.rb +74 -0
  81. data/lib/generators/formblocks/install/install_generator.rb +43 -0
  82. data/lib/generators/formblocks/install/templates/create_formblocks_tables.rb.tt +65 -0
  83. data/lib/generators/formblocks/install/templates/initializer.rb.tt +69 -0
  84. data/lib/generators/formblocks/migration_helpers.rb +40 -0
  85. data/lib/tasks/formblocks_tasks.rake +10 -0
  86. metadata +199 -0
@@ -0,0 +1,51 @@
1
+ <section class="fb-page-card" id="<%= dom_id(page) %>">
2
+ <header class="fb-page-card__header">
3
+ <div>
4
+ <h2 class="fb-h2">
5
+ <%= page.thank_you? ? t('formblocks.forms.edit.thank_you') : t('formblocks.forms.edit.page', number: page.number) %>
6
+ </h2>
7
+ <% if page.thank_you? %><p class="fb-muted fb-small"><%= t('formblocks.forms.edit.thank_you_hint') %></p><% end %>
8
+ </div>
9
+ <% if page.step? %>
10
+ <div class="fb-actions">
11
+ <%= button_to move_form_page_path(form, page, direction: 'up'), method: :patch, class: 'fb-iconbtn',
12
+ title: t('formblocks.actions.move_up'), 'aria-label': t('formblocks.actions.move_up'),
13
+ disabled: page.number == 1, form: { data: { turbo_action: 'replace' } } do %><%= fb_icon(:up) %><% end %>
14
+ <%= button_to move_form_page_path(form, page, direction: 'down'), method: :patch, class: 'fb-iconbtn',
15
+ title: t('formblocks.actions.move_down'), 'aria-label': t('formblocks.actions.move_down'),
16
+ disabled: page.last_step?, form: { data: { turbo_action: 'replace' } } do %><%= fb_icon(:down) %><% end %>
17
+ <% if form.steps.size > 1 %>
18
+ <%= button_to form_page_path(form, page), method: :delete, class: 'fb-iconbtn fb-iconbtn--danger',
19
+ title: t('formblocks.actions.delete'), 'aria-label': t('formblocks.actions.delete'),
20
+ form: { data: { turbo_confirm: t('formblocks.actions.confirm_delete_page'), turbo_action: 'replace' } } do %><%= fb_icon(:trash) %><% end %>
21
+ <% end %>
22
+ </div>
23
+ <% end %>
24
+ </header>
25
+
26
+ <div class="fb-blocks" data-controller="fb-sortable" data-fb-sortable-url-value="<%= reorder_form_page_blocks_path(form, page) %>">
27
+ <% page.blocks.each do |block| %>
28
+ <%= render 'formblocks/blocks/block', block: block %>
29
+ <% end %>
30
+ </div>
31
+ <% if page.blocks.empty? %>
32
+ <p class="fb-empty-page fb-muted"><%= t('formblocks.forms.edit.empty_page') %></p>
33
+ <% end %>
34
+
35
+ <%= render 'formblocks/blocks/palette', page: page, form: form %>
36
+
37
+ <% if page.step? %>
38
+ <footer class="fb-page-card__footer">
39
+ <%= form_with model: page, url: form_page_path(form, page), method: :patch, class: 'fb-button-editor',
40
+ data: { controller: 'fb-autosave',
41
+ action: 'input->fb-autosave#queue change->fb-autosave#save turbo:submit-end->fb-autosave#finished' } do |f| %>
42
+ <%= f.text_field :button_text, value: page.button_label, class: 'fb-button-preview', maxlength: 60, autocomplete: 'off',
43
+ 'aria-label': t('formblocks.forms.edit.button_text_label'),
44
+ data: { controller: 'fb-autosize', action: 'input->fb-autosize#resize' } %>
45
+ <span class="fb-autosave-status" data-fb-autosave-target="status" aria-live="polite"></span>
46
+ <button class="fb-btn fb-btn--sm fb-nojs" type="submit"><%= t('formblocks.actions.save') %></button>
47
+ <% end %>
48
+ <p class="fb-muted fb-small"><%= t('formblocks.forms.edit.button_hint') %></p>
49
+ </footer>
50
+ <% end %>
51
+ </section>
@@ -0,0 +1,15 @@
1
+ <% id = "fb_#{block.key}" %>
2
+ <% error = response&.error_for(block) %>
3
+ <div class="fb-field fb-field--check <%= 'fb-field--error' if error %>">
4
+ <label class="fb-checkbox" for="<%= id %>">
5
+ <%= hidden_field_tag "answers[#{block.key}]", '0', id: nil %>
6
+ <%= check_box_tag "answers[#{block.key}]", '1', response&.answer(block) == true, id: id, required: block.required?,
7
+ 'aria-describedby': (error ? "#{id}_error" : nil) %>
8
+ <span>
9
+ <%= block.label %>
10
+ <% if block.required? %><span class="fb-required" aria-hidden="true">*</span><% end %>
11
+ </span>
12
+ </label>
13
+ <% if block.help_text.present? %><p class="fb-help"><%= block.help_text %></p><% end %>
14
+ <% if error %><p class="fb-error" id="<%= id %>_error" role="alert"><%= block.label %> <%= error %></p><% end %>
15
+ </div>
@@ -0,0 +1,13 @@
1
+ <%# Shared frame for an input: label, the control (yielded), help text and error. %>
2
+ <% error = response&.error_for(block) %>
3
+ <% describe = [(block.help_text.present? ? "#{id}_help" : nil), (error ? "#{id}_error" : nil)].compact %>
4
+ <div class="fb-field <%= 'fb-field--error' if error %>">
5
+ <label class="fb-label" for="<%= id %>">
6
+ <%= block.label %>
7
+ <% if block.required? %><span class="fb-required" aria-hidden="true">*</span>
8
+ <% else %><span class="fb-optional"><%= t('formblocks.public.optional') %></span><% end %>
9
+ </label>
10
+ <%= yield(describe.join(' ').presence) %>
11
+ <% if block.help_text.present? %><p class="fb-help" id="<%= id %>_help"><%= block.help_text %></p><% end %>
12
+ <% if error %><p class="fb-error" id="<%= id %>_error" role="alert"><%= block.label %> <%= error %></p><% end %>
13
+ </div>
@@ -0,0 +1 @@
1
+ <h2 class="fb-heading"><%= block.content %></h2>
@@ -0,0 +1,5 @@
1
+ <%# The stored answer on a re-render, else the URL's query parameter, else the block's default. %>
2
+ <% value = if response&.answers&.key?(block.key) then response.answer(block)
3
+ elsif params[block.key].is_a?(String) then params[block.key]
4
+ else block.default_value end %>
5
+ <%= hidden_field_tag "answers[#{block.key}]", value, id: nil %>
@@ -0,0 +1,3 @@
1
+ <% if fb_stored?(block.image) %>
2
+ <figure class="fb-image"><%= image_tag fb_attachment_url(block.image), alt: block.label.to_s %></figure>
3
+ <% end %>
@@ -0,0 +1 @@
1
+ <div class="fb-paragraph"><%= simple_format(block.content) %></div>
@@ -0,0 +1,17 @@
1
+ <% id = "fb_#{block.key}" %>
2
+ <% error = response&.error_for(block) %>
3
+ <fieldset class="fb-field fb-field--group <%= 'fb-field--error' if error %>" <%= "aria-describedby=#{id}_error" if error %>>
4
+ <legend class="fb-label">
5
+ <%= block.label %>
6
+ <% if block.required? %><span class="fb-required" aria-hidden="true">*</span>
7
+ <% else %><span class="fb-optional"><%= t('formblocks.public.optional') %></span><% end %>
8
+ </legend>
9
+ <% Array(block.options).each_with_index do |option, index| %>
10
+ <label class="fb-radio" for="<%= id %>_<%= index %>">
11
+ <%= radio_button_tag "answers[#{block.key}]", option, response&.answer(block) == option, id: "#{id}_#{index}", required: block.required? %>
12
+ <span><%= option %></span>
13
+ </label>
14
+ <% end %>
15
+ <% if block.help_text.present? %><p class="fb-help"><%= block.help_text %></p><% end %>
16
+ <% if error %><p class="fb-error" id="<%= id %>_error" role="alert"><%= block.label %> <%= error %></p><% end %>
17
+ </fieldset>
@@ -0,0 +1,6 @@
1
+ <% id = "fb_#{block.key}" %>
2
+ <%= render layout: 'formblocks/public/blocks/field', locals: { block: block, response: response, id: id } do |describedby| %>
3
+ <%= text_field_tag "answers[#{block.key}]", response&.answer(block), id: id, type: block.html_input_type, class: 'fb-input',
4
+ required: block.required?, placeholder: block.placeholder.presence, autocomplete: block.autocomplete,
5
+ 'aria-describedby': describedby %>
6
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <% id = "fb_#{block.key}" %>
2
+ <%= render layout: 'formblocks/public/blocks/field', locals: { block: block, response: response, id: id } do |describedby| %>
3
+ <%= text_area_tag "answers[#{block.key}]", response&.answer(block), id: id, class: 'fb-input', rows: 4,
4
+ required: block.required?, placeholder: block.placeholder.presence, 'aria-describedby': describedby %>
5
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <div class="fb-thanks">
2
+ <h1 class="fb-heading"><%= t('formblocks.public.not_found') %></h1>
3
+ </div>
@@ -0,0 +1,37 @@
1
+ <% steps = @form.steps %>
2
+ <%= form_with url: main_app.formblocks_form_path(@form.slug, preview: params[:preview].presence), method: :post,
3
+ class: 'fb-form', data: { controller: 'fb-pager', fb_pager_step_label_value: t('formblocks.public.step', current: '%{current}', total: '%{total}') } do %>
4
+ <% if @rate_limited %>
5
+ <p class="fb-alert" role="alert"><%= t('formblocks.errors.rate_limited') %></p>
6
+ <% elsif @response.errors.any? %>
7
+ <p class="fb-alert" role="alert"><%= t('formblocks.public.fix_errors') %></p>
8
+ <% end %>
9
+
10
+ <%# The honeypot: off-screen, unlabelled for assistive tech, and never filled by a person. %>
11
+ <div class="fb-hp" aria-hidden="true">
12
+ <label for="fb_website">Website</label>
13
+ <input type="text" id="fb_website" name="fb_website" tabindex="-1" autocomplete="off">
14
+ </div>
15
+ <%= hidden_field_tag 'fb_referrer', @referrer, id: nil %>
16
+
17
+ <% steps.each_with_index do |page, index| %>
18
+ <% has_errors = page.blocks.any? { |block| block.input? && @response.error_for(block) } %>
19
+ <fieldset class="fb-step" data-fb-pager-target="step" <%= 'data-fb-pager-errors' if has_errors %>>
20
+ <legend class="fb-sr-only"><%= t('formblocks.public.step', current: index + 1, total: steps.size) %></legend>
21
+ <% page.blocks.each do |block| %>
22
+ <%= render partial: block.public_partial, locals: { block: block, response: @response } %>
23
+ <% end %>
24
+ <div class="fb-step__nav">
25
+ <% if index.positive? %>
26
+ <button type="button" class="fb-btn fb-btn--secondary" data-action="fb-pager#back" data-fb-pager-target="nav" hidden><%= t('formblocks.public.back') %></button>
27
+ <% end %>
28
+ <% if page.last_step? %>
29
+ <button type="submit" class="fb-btn fb-btn--primary"><%= page.button_label %></button>
30
+ <% else %>
31
+ <button type="button" class="fb-btn fb-btn--primary" data-action="fb-pager#next" data-fb-pager-target="nav" hidden><%= page.button_label %></button>
32
+ <% end %>
33
+ </div>
34
+ </fieldset>
35
+ <% end %>
36
+ <p class="fb-progress" data-fb-pager-target="progress" hidden></p>
37
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <div class="fb-thanks">
2
+ <% @page.blocks.reject(&:input?).each do |block| %>
3
+ <%= render partial: block.public_partial, locals: { block: block, response: nil } %>
4
+ <% end %>
5
+ </div>
@@ -0,0 +1,45 @@
1
+ <% content_for :title, "#{@form.title} · #{t('.title')}" %>
2
+ <%= fb_admin_shell do %>
3
+ <%= render 'formblocks/forms/header', form: @form, tab: :responses %>
4
+
5
+ <header class="fb-header">
6
+ <h2 class="fb-h2"><%= t('.count', count: @form.responses_count) %></h2>
7
+ <% if @form.responses_count.positive? %>
8
+ <%= link_to form_responses_path(@form, format: :csv), class: 'fb-btn', data: { turbo: false } do %>
9
+ <%= fb_icon(:download) %> <%= t('.download_csv') %>
10
+ <% end %>
11
+ <% end %>
12
+ </header>
13
+
14
+ <% if @responses.empty? %>
15
+ <div class="fb-card fb-empty"><p><%= t('.empty') %></p></div>
16
+ <% else %>
17
+ <div class="fb-card fb-card--flush">
18
+ <table class="fb-table">
19
+ <thead>
20
+ <tr>
21
+ <th><%= t('.submitted') %></th>
22
+ <% @columns.each do |block| %><th><%= block.label %></th><% end %>
23
+ <th><span class="fb-sr-only"><%= t('formblocks.actions.actions') %></span></th>
24
+ </tr>
25
+ </thead>
26
+ <tbody>
27
+ <% @responses.each do |response| %>
28
+ <tr id="<%= dom_id(response) %>">
29
+ <td><%= link_to fb_time(response.created_at), form_response_path(@form, response), class: 'fb-link fb-link--strong' %></td>
30
+ <% @columns.each do |block| %>
31
+ <td class="fb-cell-clip">
32
+ <%= block.display_answer(response.answer(block)).presence || tag.span(t('formblocks.responses.show.empty'), class: 'fb-muted') %>
33
+ </td>
34
+ <% end %>
35
+ <td class="fb-table__actions"><%= link_to t('.view'), form_response_path(@form, response), class: 'fb-btn fb-btn--sm' %></td>
36
+ </tr>
37
+ <% end %>
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ <% if @more %>
42
+ <p class="fb-more"><%= link_to t('.more'), form_responses_path(@form, page: @page + 1), class: 'fb-btn' %></p>
43
+ <% end %>
44
+ <% end %>
45
+ <% end %>
@@ -0,0 +1,43 @@
1
+ <% content_for :title, "#{@form.title} · #{t('.title')}" %>
2
+ <%= fb_admin_shell do %>
3
+ <%= render 'formblocks/forms/header', form: @form, tab: :responses %>
4
+
5
+ <p><%= link_to form_responses_path(@form), class: 'fb-backlink' do %><%= fb_icon(:back) %> <%= t('.all') %><% end %></p>
6
+
7
+ <% blocks = @form.input_blocks %>
8
+ <% orphans = @response.answers.to_h.keys - blocks.map(&:key) %>
9
+ <div class="fb-card">
10
+ <dl class="fb-dl">
11
+ <% blocks.each do |block| %>
12
+ <div class="fb-dl__row">
13
+ <dt><%= block.label.presence || block.key %></dt>
14
+ <dd class="fb-pre"><%= block.display_answer(@response.answer(block)).presence || tag.span(t('.empty'), class: 'fb-muted') %></dd>
15
+ </div>
16
+ <% end %>
17
+ <% orphans.each do |key| %>
18
+ <div class="fb-dl__row">
19
+ <dt class="fb-mono"><%= key %></dt>
20
+ <dd class="fb-pre"><%= @response.answers[key].to_s.presence || tag.span(t('.empty'), class: 'fb-muted') %></dd>
21
+ </div>
22
+ <% end %>
23
+ </dl>
24
+ </div>
25
+
26
+ <div class="fb-card">
27
+ <dl class="fb-dl fb-dl--meta">
28
+ <div class="fb-dl__row"><dt><%= t('.submitted_at') %></dt><dd><%= fb_time(@response.created_at) %></dd></div>
29
+ <% if @response.page_url.present? %>
30
+ <div class="fb-dl__row"><dt><%= t('.page_url') %></dt><dd class="fb-break"><%= @response.page_url %></dd></div>
31
+ <% end %>
32
+ <% if @response.user_agent.present? %>
33
+ <div class="fb-dl__row"><dt><%= t('.user_agent') %></dt><dd class="fb-break fb-small"><%= @response.user_agent %></dd></div>
34
+ <% end %>
35
+ <% if @response.locale.present? %>
36
+ <div class="fb-dl__row"><dt><%= t('.locale') %></dt><dd><%= @response.locale %></dd></div>
37
+ <% end %>
38
+ </dl>
39
+ </div>
40
+
41
+ <%= button_to t('formblocks.actions.delete'), form_response_path(@form, @response), method: :delete, class: 'fb-btn fb-btn--danger',
42
+ form: { data: { turbo_confirm: t('formblocks.actions.confirm_delete') } } %>
43
+ <% end %>
@@ -0,0 +1,19 @@
1
+ <% content_for :title, t('.title') %>
2
+ <%= fb_admin_shell do %>
3
+ <header class="fb-header">
4
+ <h1 class="fb-h1"><%= t('.title') %></h1>
5
+ </header>
6
+ <p class="fb-muted"><%= t('.hint') %></p>
7
+
8
+ <%= form_with model: @setting, url: settings_path, method: :patch, class: 'fb-stack' do |f| %>
9
+ <%= render 'formblocks/shared/errors', record: @setting %>
10
+ <section class="fb-card">
11
+ <%= render 'formblocks/shared/brand_fields', f: f, record: @setting, logo_hint: t('.logo_hint'),
12
+ fallbacks: { primary_color: Formblocks.config.default_primary_color,
13
+ button_text_color: Formblocks.config.default_button_text_color } %>
14
+ </section>
15
+ <div class="fb-form-actions">
16
+ <%= f.submit t('formblocks.actions.save'), class: 'fb-btn fb-btn--primary' %>
17
+ </div>
18
+ <% end %>
19
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <%# The admin's own shell, rendered by the views (through fb_admin_shell) rather
2
+ than the layout. That is deliberate: `config.admin_layout` and
3
+ `config.base_controller_class` let a host replace the layout, and assets
4
+ declared in a layout the host replaces vanish. Declaring them here means
5
+ every layout works, host or gem.
6
+
7
+ The `fb-admin` wrapper is what scopes admin.css, so it encloses the content. %>
8
+ <%= formblocks_assets(:admin) %>
9
+ <div class="fb-admin">
10
+ <nav class="fb-nav" aria-label="<%= t('formblocks.title') %>">
11
+ <%= link_to t('formblocks.nav.forms'), forms_path,
12
+ class: class_names('fb-nav__link', 'is-active': controller_name != 'settings') %>
13
+ <%= link_to t('formblocks.nav.settings'), settings_path,
14
+ class: class_names('fb-nav__link', 'is-active': controller_name == 'settings') %>
15
+ </nav>
16
+ <div class="fb-container">
17
+ <% if notice.present? %><p class="fb-flash fb-flash--notice" role="status"><%= notice %></p><% end %>
18
+ <% if alert.present? %><p class="fb-flash fb-flash--alert" role="alert"><%= alert %></p><% end %>
19
+ <%= content %>
20
+ </div>
21
+ </div>
@@ -0,0 +1,29 @@
1
+ <%# Colors and logo, shared by a form's settings and the global settings.
2
+ `fallbacks` maps each color attribute to the value it inherits when blank. %>
3
+ <div class="fb-grid">
4
+ <% fallbacks.each do |attribute, fallback| %>
5
+ <div class="fb-field" data-controller="fb-color">
6
+ <%= f.label attribute, t("formblocks.fields.#{attribute}"), class: 'fb-label' %>
7
+ <div class="fb-color">
8
+ <input type="color" class="fb-color__picker" value="<%= record.public_send(attribute).presence || fallback %>"
9
+ data-fb-color-target="picker" data-action="input->fb-color#pick"
10
+ aria-label="<%= t("formblocks.fields.#{attribute}") %>">
11
+ <%= f.text_field attribute, class: 'fb-input fb-input--mono', placeholder: fallback, maxlength: 7,
12
+ autocomplete: 'off', spellcheck: false,
13
+ data: { fb_color_target: 'text', action: 'input->fb-color#type' } %>
14
+ </div>
15
+ <p class="fb-hint"><%= t('formblocks.fields.color_inherit', color: fallback) %></p>
16
+ </div>
17
+ <% end %>
18
+ </div>
19
+ <% if Formblocks.attachments? %>
20
+ <div class="fb-field">
21
+ <%= f.label :logo, t('formblocks.fields.logo'), class: 'fb-label' %>
22
+ <% if fb_stored?(record.logo) %>
23
+ <div class="fb-logo-preview"><%= image_tag fb_attachment_url(record.logo), alt: '' %></div>
24
+ <label class="fb-check"><%= f.check_box :remove_logo %> <%= t('formblocks.forms.settings.logo_remove') %></label>
25
+ <% end %>
26
+ <%= f.file_field :logo, accept: 'image/*', class: 'fb-input fb-input--file' %>
27
+ <% if logo_hint.present? %><p class="fb-hint"><%= logo_hint %></p><% end %>
28
+ </div>
29
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <% if record.errors.any? %>
2
+ <ul class="fb-errors" role="alert">
3
+ <% record.errors.full_messages.each do |message| %>
4
+ <li><%= message %></li>
5
+ <% end %>
6
+ </ul>
7
+ <% end %>
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title><%= [content_for(:title), t('formblocks.title'), Formblocks.app_name].compact_blank.join(' · ') %></title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <%# Turbo 8 morphs page refreshes, so a builder change (add a block, move a page) re-renders in place without losing scroll position. %>
8
+ <meta name="turbo-refresh-method" content="morph">
9
+ <meta name="turbo-refresh-scroll" content="preserve">
10
+ <%= csrf_meta_tags %>
11
+ <%= csp_meta_tag %>
12
+ <%# The stylesheet and script are declared by the views (formblocks/shared/_admin),
13
+ not here, so they survive a host replacing this layout via config.admin_layout. %>
14
+ </head>
15
+ <body class="fb-page">
16
+ <%= yield %>
17
+ </body>
18
+ </html>
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title><%= [@form&.title, Formblocks.app_name].compact_blank.join(' · ') %></title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag %>
9
+ <%= formblocks_assets(:public) %>
10
+ <%= formblocks_brand_style(@form) if @form %>
11
+ </head>
12
+ <body class="fb-public fb-brand">
13
+ <main class="fb-public__main">
14
+ <% if @form && (logo = @form.effective_logo) %>
15
+ <div class="fb-public__logo"><%= image_tag fb_attachment_url(logo), alt: Formblocks.app_name %></div>
16
+ <% end %>
17
+ <%= yield %>
18
+ </main>
19
+ </body>
20
+ </html>
@@ -0,0 +1,188 @@
1
+ en:
2
+ formblocks:
3
+ title: Forms
4
+ forbidden: Forbidden. Set Formblocks.config.authorize_admin to grant access.
5
+ nav:
6
+ forms: Forms
7
+ settings: Settings
8
+ status:
9
+ draft: Draft
10
+ published: Published
11
+ actions:
12
+ actions: Actions
13
+ save: Save
14
+ cancel: Cancel
15
+ delete: Delete
16
+ delete_form: Delete this form
17
+ duplicate: Duplicate
18
+ back: Back
19
+ move_up: Move up
20
+ move_down: Move down
21
+ confirm_delete: Delete this? This cannot be undone.
22
+ confirm_delete_form: Delete this form and all of its responses? This cannot be undone.
23
+ confirm_delete_page: Delete this page and its blocks?
24
+ confirm_delete_block: Delete this block?
25
+ flash:
26
+ form_created: Form created.
27
+ form_updated: Settings saved.
28
+ form_deleted: Form deleted.
29
+ form_duplicated: Form duplicated.
30
+ form_unpublished: Form unpublished. It is no longer reachable at its public URL.
31
+ settings_updated: Settings saved.
32
+ response_deleted: Response deleted.
33
+ forms:
34
+ index:
35
+ title: Forms
36
+ new: New form
37
+ empty: No forms yet. Create your first one.
38
+ columns:
39
+ title: Title
40
+ status: Status
41
+ responses: Responses
42
+ updated: Updated
43
+ new:
44
+ title: New form
45
+ blank: Blank form
46
+ blank_description: Start from scratch with one empty page.
47
+ start_blank: Start blank
48
+ use_template: Use template
49
+ untitled: Untitled form
50
+ summary:
51
+ one: "%{pages} page · %{fields} fields"
52
+ other: "%{pages} pages · %{fields} fields"
53
+ edit:
54
+ page: "Page %{number}"
55
+ thank_you: Thank you page
56
+ thank_you_hint: Shown after a successful submission. Content blocks only.
57
+ add_page: Add page
58
+ add_block: Add block
59
+ content_blocks: Content
60
+ input_blocks: Inputs
61
+ empty_page: This page has no blocks yet. Add one below.
62
+ button_text_label: Button text
63
+ button_hint: Click the button to change its text.
64
+ drag: Drag to reorder
65
+ preview: Preview
66
+ publish: Publish
67
+ unpublish: Unpublish
68
+ published_hint: "Live at %{url}"
69
+ draft_hint: Draft — visitors cannot see this form until you publish it.
70
+ tabs:
71
+ builder: Builder
72
+ settings: Settings
73
+ responses: Responses
74
+ settings:
75
+ general: General
76
+ branding: Branding
77
+ branding_hint: Leave a color blank to inherit the global setting.
78
+ slug_hint: Lowercase letters, numbers and dashes. Leave blank to generate one from the title.
79
+ logo_remove: Remove logo
80
+ danger: Danger zone
81
+ delete_hint: Deleting a form also deletes all of its responses.
82
+ published:
83
+ title: Your form is live
84
+ message: Anyone with the link can now fill it in.
85
+ url_label: Public URL
86
+ copy: Copy
87
+ copied: Copied!
88
+ open: Open form
89
+ back_to_builder: Back to builder
90
+ view_responses: View responses
91
+ fields:
92
+ title: Title
93
+ slug: Slug
94
+ primary_color: Primary color
95
+ button_text_color: Button text color
96
+ logo: Logo
97
+ label: Label
98
+ placeholder: Placeholder
99
+ help_text: Help text
100
+ required: Required
101
+ content: Text
102
+ options: Options
103
+ options_hint: One option per line.
104
+ key: Field name
105
+ key_hint: The name the value is stored under. A matching query parameter on the public URL fills it (for example ?utm_source=…).
106
+ value: Default value
107
+ image: Image
108
+ alt_text: Alt text
109
+ remove_image: Remove image
110
+ attachments_off: Image uploads need Active Storage in your app.
111
+ color_inherit: "Blank inherits %{color}"
112
+ blocks:
113
+ heading:
114
+ name: Heading
115
+ paragraph:
116
+ name: Paragraph
117
+ image:
118
+ name: Image
119
+ name:
120
+ name: Name
121
+ email:
122
+ name: Email
123
+ phone:
124
+ name: Phone
125
+ url:
126
+ name: URL
127
+ text:
128
+ name: Short text
129
+ textarea:
130
+ name: Long text
131
+ hidden:
132
+ name: Hidden field
133
+ checkbox:
134
+ name: Checkbox
135
+ radio_group:
136
+ name: Radio group
137
+ defaults:
138
+ heading: Heading
139
+ paragraph: Write something here.
140
+ option: "Option %{n}"
141
+ "yes": "Yes"
142
+ "no": "No"
143
+ responses:
144
+ index:
145
+ title: Responses
146
+ empty: No responses yet.
147
+ download_csv: Download CSV
148
+ submitted: Submitted
149
+ view: View
150
+ more: Load more
151
+ count:
152
+ one: 1 response
153
+ other: "%{count} responses"
154
+ show:
155
+ title: Response
156
+ all: All responses
157
+ submitted_at: Submitted
158
+ page_url: Came from
159
+ user_agent: Browser
160
+ locale: Locale
161
+ empty: (empty)
162
+ csv:
163
+ submitted_at: Submitted at
164
+ settings:
165
+ show:
166
+ title: Settings
167
+ hint: Defaults for every form. A form can override each of these in its own settings.
168
+ logo_hint: Shown at the top of every form that has no logo of its own.
169
+ public:
170
+ next: Next
171
+ back: Back
172
+ submit: Submit
173
+ step: "Step %{current} of %{total}"
174
+ optional: optional
175
+ fix_errors: Please check the highlighted fields.
176
+ thanks_title: Thank you!
177
+ thanks_message: Your response has been recorded.
178
+ not_found: This form is not available.
179
+ errors:
180
+ required: is required
181
+ invalid_email: is not a valid email address
182
+ invalid_url: is not a valid URL (include http:// or https://)
183
+ invalid_option: is not one of the options
184
+ must_be_checked: must be checked
185
+ too_long: is too long
186
+ rate_limited: Too many submissions. Please wait a moment and try again.
187
+ not_an_image: must be an image
188
+ file_too_large: "is too large (max %{size} MB)"
data/config/routes.rb ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ Formblocks::Engine.routes.draw do
4
+ # The engine's own CSS and JavaScript, served same-origin (see Formblocks::Assets).
5
+ get 'assets/:name', to: 'assets#show', as: :engine_asset, format: false, constraints: { name: /[\w.-]+/ }
6
+
7
+ # Global brand settings — declared before the forms so `/settings` is never
8
+ # read as a form id.
9
+ resource :settings, only: %i[show update]
10
+
11
+ # Flat, human URLs: the mount path IS the forms index. /forms is the list,
12
+ # /forms/new the template picker, /forms/12/edit the builder.
13
+ resources :forms, path: '', except: :show do
14
+ member do
15
+ post :duplicate
16
+ post :publish
17
+ post :unpublish
18
+ get :published
19
+ get :settings
20
+ end
21
+
22
+ resources :pages, only: %i[create update destroy] do
23
+ member { patch :move }
24
+
25
+ resources :blocks, only: %i[create update destroy] do
26
+ member { patch :move }
27
+ collection { patch :reorder }
28
+ end
29
+ end
30
+
31
+ resources :responses, only: %i[index show destroy]
32
+ end
33
+
34
+ root to: 'forms#index'
35
+ end