rails_site_engine 0.6.2

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 (110) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +137 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/stylesheets/rails_site_engine/application.css +15 -0
  6. data/app/assets/stylesheets/rails_site_engine/engine.css +2 -0
  7. data/app/assets/stylesheets/rails_site_engine/engine.tailwind.css +5 -0
  8. data/app/controllers/rails_site_engine/application_controller.rb +27 -0
  9. data/app/controllers/rails_site_engine/contacts_controller.rb +35 -0
  10. data/app/controllers/rails_site_engine/meta_controller.rb +20 -0
  11. data/app/controllers/rails_site_engine/pages_controller.rb +18 -0
  12. data/app/helpers/rails_site_engine/application_helper.rb +266 -0
  13. data/app/javascript/controllers/rails_site_engine/color_mode_controller.js +83 -0
  14. data/app/javascript/controllers/rails_site_engine/form_submit_controller.js +22 -0
  15. data/app/javascript/controllers/rails_site_engine/mobile_nav_controller.js +45 -0
  16. data/app/javascript/rails_site_engine/application.js +8 -0
  17. data/app/jobs/rails_site_engine/application_job.rb +4 -0
  18. data/app/mailers/rails_site_engine/application_mailer.rb +6 -0
  19. data/app/mailers/rails_site_engine/contact_mailer.rb +25 -0
  20. data/app/models/rails_site_engine/application_record.rb +5 -0
  21. data/app/models/rails_site_engine/contact_message.rb +16 -0
  22. data/app/views/layouts/mailer.html.erb +13 -0
  23. data/app/views/layouts/mailer.text.erb +1 -0
  24. data/app/views/layouts/rails_site_engine/application.html.erb +54 -0
  25. data/app/views/rails_site_engine/contact_mailer/notify.text.erb +9 -0
  26. data/app/views/rails_site_engine/contacts/new.html.erb +1 -0
  27. data/app/views/rails_site_engine/pages/show.html.erb +15 -0
  28. data/app/views/rails_site_engine/sections/_contact_form.html.erb +112 -0
  29. data/app/views/rails_site_engine/sections/_cta_band.html.erb +27 -0
  30. data/app/views/rails_site_engine/sections/_faq.html.erb +29 -0
  31. data/app/views/rails_site_engine/sections/_feature_cards.html.erb +30 -0
  32. data/app/views/rails_site_engine/sections/_hero.html.erb +39 -0
  33. data/app/views/rails_site_engine/sections/_pricing_cards.html.erb +53 -0
  34. data/app/views/rails_site_engine/sections/_process_steps.html.erb +35 -0
  35. data/app/views/rails_site_engine/sections/_project_cards.html.erb +48 -0
  36. data/app/views/rails_site_engine/sections/_proof_strip.html.erb +16 -0
  37. data/app/views/rails_site_engine/sections/_rich_text.html.erb +15 -0
  38. data/app/views/rails_site_engine/sections/_stats.html.erb +34 -0
  39. data/app/views/rails_site_engine/sections/_testimonials.html.erb +40 -0
  40. data/app/views/rails_site_engine/sections/_unknown.html.erb +6 -0
  41. data/app/views/rails_site_engine/shared/_flash.html.erb +18 -0
  42. data/app/views/rails_site_engine/shared/_footer.html.erb +41 -0
  43. data/app/views/rails_site_engine/shared/_navbar.html.erb +118 -0
  44. data/config/importmap.rb +5 -0
  45. data/config/locales/en.yml +27 -0
  46. data/config/routes.rb +9 -0
  47. data/config/site_profiles.yml +23 -0
  48. data/lib/generators/rails_site_engine/install/install_generator.rb +454 -0
  49. data/lib/generators/rails_site_engine/install/templates/config/theme.yml +3 -0
  50. data/lib/generators/rails_site_engine/install/templates/content/pages/about.md +12 -0
  51. data/lib/generators/rails_site_engine/install/templates/content/pages/contact.md +13 -0
  52. data/lib/generators/rails_site_engine/install/templates/content/pages/home.md +39 -0
  53. data/lib/generators/rails_site_engine/install/templates/content/pages/privacy.md +43 -0
  54. data/lib/generators/rails_site_engine/install/templates/content/pages/services.md +25 -0
  55. data/lib/generators/rails_site_engine/install/templates/content/site.yml +36 -0
  56. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/config/theme.yml +3 -0
  57. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/about.md +14 -0
  58. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/contact.md +12 -0
  59. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/home.md +55 -0
  60. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/privacy.md +43 -0
  61. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/pages/services.md +29 -0
  62. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-carpentry/content/site.yml +36 -0
  63. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/config/theme.yml +3 -0
  64. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/about.md +14 -0
  65. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/contact.md +12 -0
  66. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/home.md +55 -0
  67. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/privacy.md +43 -0
  68. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/pages/services.md +29 -0
  69. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-electrician/content/site.yml +36 -0
  70. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/config/theme.yml +3 -0
  71. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/about.md +14 -0
  72. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/contact.md +12 -0
  73. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/home.md +55 -0
  74. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/privacy.md +43 -0
  75. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/pages/services.md +29 -0
  76. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-general/content/site.yml +36 -0
  77. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/config/theme.yml +3 -0
  78. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/about.md +14 -0
  79. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/contact.md +12 -0
  80. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/home.md +55 -0
  81. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/privacy.md +43 -0
  82. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/pages/services.md +29 -0
  83. data/lib/generators/rails_site_engine/install/templates/site_profiles/home-services-plumbing/content/site.yml +36 -0
  84. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/config/theme.yml +3 -0
  85. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/about.md +26 -0
  86. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/contact.md +12 -0
  87. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/home.md +43 -0
  88. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/privacy.md +43 -0
  89. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/pages/work.md +27 -0
  90. data/lib/generators/rails_site_engine/install/templates/site_profiles/portfolio/content/site.yml +36 -0
  91. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/config/theme.yml +3 -0
  92. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/about.md +14 -0
  93. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/contact.md +12 -0
  94. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/home.md +48 -0
  95. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/pricing.md +57 -0
  96. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/privacy.md +43 -0
  97. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/pages/projects.md +27 -0
  98. data/lib/generators/rails_site_engine/install/templates/site_profiles/software-contracting/content/site.yml +38 -0
  99. data/lib/rails_site_engine/content/store.rb +297 -0
  100. data/lib/rails_site_engine/engine.rb +15 -0
  101. data/lib/rails_site_engine/markdown_renderer.rb +37 -0
  102. data/lib/rails_site_engine/profile_defaults.rb +83 -0
  103. data/lib/rails_site_engine/site_engine_config.rb +78 -0
  104. data/lib/rails_site_engine/site_profiles.rb +166 -0
  105. data/lib/rails_site_engine/sitemap_xml.rb +26 -0
  106. data/lib/rails_site_engine/theme.rb +54 -0
  107. data/lib/rails_site_engine/version.rb +3 -0
  108. data/lib/rails_site_engine.rb +13 -0
  109. data/lib/tasks/rails_site_engine_tasks.rake +7 -0
  110. metadata +179 -0
@@ -0,0 +1,112 @@
1
+ <%
2
+ @contact_message ||= RailsSiteEngine::ContactMessage.new
3
+ heading = section["heading"].to_s.strip.presence || t("rails_site_engine.contact.form_title")
4
+ intro = section["body"].to_s.strip.presence
5
+ errors = @contact_message.errors
6
+
7
+ name_errors = errors[:name]
8
+ email_errors = errors[:email]
9
+ phone_errors = errors[:phone]
10
+ message_errors = errors[:message]
11
+ %>
12
+
13
+ <section class="mb-14">
14
+ <div class="card border border-base-200 bg-base-100 shadow-sm">
15
+ <div class="card-body">
16
+ <% if heading.present? %>
17
+ <%= tag.public_send(heading_tag, heading, class: "card-title text-2xl") %>
18
+ <% end %>
19
+
20
+ <% if intro.present? %>
21
+ <p class="text-base-content/80"><%= intro %></p>
22
+ <% end %>
23
+
24
+ <%= form_with model: @contact_message,
25
+ scope: :rails_site_engine_contact_message,
26
+ url: contact_page_path,
27
+ method: :post,
28
+ class: "space-y-5",
29
+ data: { controller: "rails-site-engine--form-submit", action: "submit->rails-site-engine--form-submit#submit" } do |f| %>
30
+ <% if errors.any? %>
31
+ <div class="alert alert-error alert-soft" role="alert" aria-live="assertive">
32
+ <div>
33
+ <p class="font-semibold"><%= t("rails_site_engine.contact.errors_title") %></p>
34
+ <ul class="mt-2 list-disc pl-5 text-sm">
35
+ <% errors.full_messages.each do |error_message| %>
36
+ <li><%= error_message %></li>
37
+ <% end %>
38
+ </ul>
39
+ </div>
40
+ </div>
41
+ <% end %>
42
+
43
+ <div class="grid gap-5 md:grid-cols-2">
44
+ <div class="fieldset">
45
+ <%= f.label :name, t("rails_site_engine.contact.fields.name"), class: "fieldset-legend" %>
46
+ <%= f.text_field :name,
47
+ class: "input input-bordered w-full #{'input-error' if name_errors.any?}",
48
+ autocomplete: "name",
49
+ required: true,
50
+ "aria-invalid": name_errors.any?,
51
+ "aria-describedby": ("contact-name-error" if name_errors.any?) %>
52
+ <% if name_errors.any? %>
53
+ <p id="contact-name-error" class="mt-1 text-sm text-error"><%= name_errors.first %></p>
54
+ <% end %>
55
+ </div>
56
+
57
+ <div class="fieldset">
58
+ <%= f.label :email, t("rails_site_engine.contact.fields.email"), class: "fieldset-legend" %>
59
+ <%= f.email_field :email,
60
+ class: "input input-bordered w-full #{'input-error' if email_errors.any?}",
61
+ autocomplete: "email",
62
+ required: true,
63
+ "aria-invalid": email_errors.any?,
64
+ "aria-describedby": ("contact-email-error" if email_errors.any?) %>
65
+ <% if email_errors.any? %>
66
+ <p id="contact-email-error" class="mt-1 text-sm text-error"><%= email_errors.first %></p>
67
+ <% end %>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="fieldset">
72
+ <%= f.label :phone, t("rails_site_engine.contact.fields.phone"), class: "fieldset-legend" %>
73
+ <%= f.telephone_field :phone,
74
+ class: "input input-bordered w-full #{'input-error' if phone_errors.any?}",
75
+ autocomplete: "tel",
76
+ "aria-invalid": phone_errors.any?,
77
+ "aria-describedby": ("contact-phone-error" if phone_errors.any?) %>
78
+ <% if phone_errors.any? %>
79
+ <p id="contact-phone-error" class="mt-1 text-sm text-error"><%= phone_errors.first %></p>
80
+ <% end %>
81
+ </div>
82
+
83
+ <div class="fieldset">
84
+ <%= f.label :message, t("rails_site_engine.contact.fields.message"), class: "fieldset-legend" %>
85
+ <%= f.text_area :message,
86
+ class: "textarea textarea-bordered min-h-36 w-full #{'textarea-error' if message_errors.any?}",
87
+ required: true,
88
+ "aria-invalid": message_errors.any?,
89
+ "aria-describedby": ("contact-message-error" if message_errors.any?) %>
90
+ <% if message_errors.any? %>
91
+ <p id="contact-message-error" class="mt-1 text-sm text-error"><%= message_errors.first %></p>
92
+ <% end %>
93
+ </div>
94
+
95
+ <div class="hidden" aria-hidden="true">
96
+ <%= f.label :website %>
97
+ <%= f.text_field :website, tabindex: "-1", autocomplete: "off" %>
98
+ </div>
99
+
100
+ <div class="card-actions justify-end">
101
+ <button type="submit" class="btn btn-primary" data-rails-site-engine--form-submit-target="submitButton">
102
+ <span data-rails-site-engine--form-submit-target="submitText"><%= t("rails_site_engine.contact.submit") %></span>
103
+ <span class="hidden items-center gap-2" data-rails-site-engine--form-submit-target="submittingText">
104
+ <span class="loading loading-spinner loading-xs"></span>
105
+ <%= t("rails_site_engine.contact.submitting") %>
106
+ </span>
107
+ </button>
108
+ </div>
109
+ <% end %>
110
+ </div>
111
+ </div>
112
+ </section>
@@ -0,0 +1,27 @@
1
+ <%
2
+ heading = section["heading"].to_s.strip.presence || section["title"].to_s.strip.presence
3
+ body = section["body"].to_s.strip.presence
4
+ cta = section["cta"].is_a?(Hash) ? section["cta"] : {}
5
+ cta_href = resolve_link_href(cta)
6
+ %>
7
+
8
+ <% if heading.present? || body.present? || cta_href.present? %>
9
+ <section class="mb-14">
10
+ <div class="rounded-box border border-primary/20 bg-primary px-6 py-8 text-primary-content sm:px-8 sm:py-10">
11
+ <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
12
+ <div>
13
+ <% if heading.present? %>
14
+ <h2 class="text-2xl font-bold tracking-tight"><%= heading %></h2>
15
+ <% end %>
16
+ <% if body.present? %>
17
+ <p class="mt-2 max-w-2xl text-primary-content/90"><%= body %></p>
18
+ <% end %>
19
+ </div>
20
+
21
+ <% if cta["label"].to_s.strip.present? && cta_href.present? %>
22
+ <%= link_to cta["label"], cta_href, class: "btn btn-neutral" %>
23
+ <% end %>
24
+ </div>
25
+ </div>
26
+ </section>
27
+ <% end %>
@@ -0,0 +1,29 @@
1
+ <%
2
+ heading = section["heading"].to_s.strip.presence
3
+ items = section["items"]
4
+ items = items.is_a?(Array) ? items.select { |item| item.is_a?(Hash) } : []
5
+ %>
6
+
7
+ <% if items.any? %>
8
+ <section class="mb-14">
9
+ <% if heading.present? %>
10
+ <%= tag.public_send(heading_tag, heading, class: "mb-6 text-3xl font-bold tracking-tight") %>
11
+ <% end %>
12
+
13
+ <div class="space-y-3">
14
+ <% items.each_with_index do |item, index| %>
15
+ <% question = item["question"].to_s.strip %>
16
+ <% answer = item["answer"].to_s.strip %>
17
+ <% next if question.blank? && answer.blank? %>
18
+
19
+ <div class="collapse collapse-plus border border-base-200 bg-base-100">
20
+ <input type="checkbox" name="faq-<%= page.id %>-<%= index %>">
21
+ <div class="collapse-title pr-10 text-base font-semibold"><%= question %></div>
22
+ <div class="collapse-content text-sm leading-relaxed text-base-content/80">
23
+ <%= answer %>
24
+ </div>
25
+ </div>
26
+ <% end %>
27
+ </div>
28
+ </section>
29
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <%
2
+ heading = section["heading"].to_s.strip.presence || section["title"].to_s.strip.presence
3
+ items = section["items"]
4
+ items = items.is_a?(Array) ? items.select { |item| item.is_a?(Hash) } : []
5
+ %>
6
+
7
+ <section class="mb-14">
8
+ <% if heading.present? %>
9
+ <%= tag.public_send(heading_tag, heading, class: "mb-6 text-3xl font-bold tracking-tight") %>
10
+ <% end %>
11
+
12
+ <div class="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
13
+ <% items.each do |item| %>
14
+ <% item_title = item["title"].to_s.strip %>
15
+ <% item_description = item["description"].to_s.strip %>
16
+ <% next if item_title.blank? && item_description.blank? %>
17
+
18
+ <article class="card border border-base-200 bg-base-100 shadow-sm">
19
+ <div class="card-body">
20
+ <% if item_title.present? %>
21
+ <h3 class="card-title text-lg"><%= item_title %></h3>
22
+ <% end %>
23
+ <% if item_description.present? %>
24
+ <p class="text-base-content/80"><%= item_description %></p>
25
+ <% end %>
26
+ </div>
27
+ </article>
28
+ <% end %>
29
+ </div>
30
+ </section>
@@ -0,0 +1,39 @@
1
+ <%
2
+ heading = section["heading"].to_s.strip.presence || section["headline"].to_s.strip.presence || page.title.to_s
3
+ subheading = section["subheading"].to_s.strip.presence || section["subhead"].to_s.strip.presence
4
+ kicker = section["kicker"].to_s.strip.presence
5
+ primary = section["primary_cta"].is_a?(Hash) ? section["primary_cta"] : {}
6
+ secondary = section["secondary_cta"].is_a?(Hash) ? section["secondary_cta"] : {}
7
+ primary_href = resolve_link_href(primary)
8
+ secondary_href = resolve_link_href(secondary)
9
+ %>
10
+
11
+ <section class="mb-14">
12
+ <div class="hero rounded-box border border-base-200 bg-base-200/60">
13
+ <div class="hero-content w-full py-12 sm:py-16">
14
+ <div class="max-w-3xl">
15
+ <% if kicker.present? %>
16
+ <p class="mb-4"><span class="badge badge-primary badge-outline"><%= kicker %></span></p>
17
+ <% end %>
18
+
19
+ <%= tag.public_send(heading_tag, heading, class: "text-4xl font-extrabold tracking-tight sm:text-5xl") if heading.present? %>
20
+
21
+ <% if subheading.present? %>
22
+ <p class="mt-5 max-w-2xl text-lg text-base-content/80"><%= subheading %></p>
23
+ <% end %>
24
+
25
+ <% if primary_href.present? || secondary_href.present? %>
26
+ <div class="mt-8 flex flex-wrap gap-3">
27
+ <% if primary["label"].to_s.strip.present? && primary_href.present? %>
28
+ <%= link_to primary["label"], primary_href, class: "btn btn-primary btn-wide sm:btn-md" %>
29
+ <% end %>
30
+
31
+ <% if secondary["label"].to_s.strip.present? && secondary_href.present? %>
32
+ <%= link_to secondary["label"], secondary_href, class: "btn btn-ghost" %>
33
+ <% end %>
34
+ </div>
35
+ <% end %>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </section>
@@ -0,0 +1,53 @@
1
+ <%
2
+ heading = section["heading"].to_s.strip.presence
3
+ items = section["items"]
4
+ items = items.is_a?(Array) ? items.select { |item| item.is_a?(Hash) } : []
5
+ %>
6
+
7
+ <% if items.any? %>
8
+ <section class="mb-14">
9
+ <% if heading.present? %>
10
+ <%= tag.public_send(heading_tag, heading, class: "mb-6 text-3xl font-bold tracking-tight") %>
11
+ <% end %>
12
+
13
+ <div class="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
14
+ <% items.each do |item| %>
15
+ <% name = item["name"].to_s.strip %>
16
+ <% starting_price = item["starting_price"].to_s.strip %>
17
+ <% description = item["description"].to_s.strip %>
18
+ <% includes = item["includes"] %>
19
+ <% includes = includes.is_a?(Array) ? includes.map { |entry| entry.to_s.strip }.reject(&:blank?) : [] %>
20
+ <% cta = item["cta"].is_a?(Hash) ? item["cta"] : {} %>
21
+ <% cta_href = resolve_link_href(cta) %>
22
+ <% next if name.blank? && starting_price.blank? && description.blank? && includes.empty? && cta_href.blank? %>
23
+
24
+ <article class="card border border-base-200 bg-base-100 shadow-sm">
25
+ <div class="card-body">
26
+ <% if name.present? %>
27
+ <h3 class="card-title text-lg"><%= name %></h3>
28
+ <% end %>
29
+ <% if starting_price.present? %>
30
+ <p class="text-2xl font-bold text-primary"><%= starting_price %></p>
31
+ <% end %>
32
+ <% if description.present? %>
33
+ <p class="text-base-content/80"><%= description %></p>
34
+ <% end %>
35
+ <% if includes.any? %>
36
+ <ul class="mt-2 list-disc space-y-1 pl-5 text-sm text-base-content/80">
37
+ <% includes.each do |entry| %>
38
+ <li><%= entry %></li>
39
+ <% end %>
40
+ </ul>
41
+ <% end %>
42
+
43
+ <% if cta["label"].to_s.strip.present? && cta_href.present? %>
44
+ <div class="card-actions mt-4 justify-start">
45
+ <%= link_to cta["label"], cta_href, class: "btn btn-primary btn-sm" %>
46
+ </div>
47
+ <% end %>
48
+ </div>
49
+ </article>
50
+ <% end %>
51
+ </div>
52
+ </section>
53
+ <% end %>
@@ -0,0 +1,35 @@
1
+ <%
2
+ heading = section["heading"].to_s.strip.presence
3
+ items = section["items"]
4
+ items = items.is_a?(Array) ? items.select { |item| item.is_a?(Hash) } : []
5
+ %>
6
+
7
+ <% if items.any? %>
8
+ <section class="mb-14">
9
+ <% if heading.present? %>
10
+ <%= tag.public_send(heading_tag, heading, class: "mb-6 text-3xl font-bold tracking-tight") %>
11
+ <% end %>
12
+
13
+ <ol class="grid gap-4 md:grid-cols-3">
14
+ <% items.each_with_index do |item, index| %>
15
+ <% title = item["title"].to_s.strip %>
16
+ <% body = item["description"].to_s.strip %>
17
+ <% next if title.blank? && body.blank? %>
18
+
19
+ <li class="card border border-base-200 bg-base-100 shadow-sm">
20
+ <div class="card-body">
21
+ <div class="mb-2">
22
+ <span class="badge badge-primary badge-outline"><%= format("%02d", index + 1) %></span>
23
+ </div>
24
+ <% if title.present? %>
25
+ <h3 class="text-lg font-semibold"><%= title %></h3>
26
+ <% end %>
27
+ <% if body.present? %>
28
+ <p class="text-base-content/80"><%= body %></p>
29
+ <% end %>
30
+ </div>
31
+ </li>
32
+ <% end %>
33
+ </ol>
34
+ </section>
35
+ <% end %>
@@ -0,0 +1,48 @@
1
+ <%
2
+ heading = section["heading"].to_s.strip.presence
3
+ items = section["items"]
4
+ items = items.is_a?(Array) ? items.select { |item| item.is_a?(Hash) } : []
5
+ %>
6
+
7
+ <% if items.any? %>
8
+ <section class="mb-14">
9
+ <% if heading.present? %>
10
+ <%= tag.public_send(heading_tag, heading, class: "mb-6 text-3xl font-bold tracking-tight") %>
11
+ <% end %>
12
+
13
+ <div class="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
14
+ <% items.each do |item| %>
15
+ <% title = item["title"].to_s.strip %>
16
+ <% summary = item["summary"].to_s.strip %>
17
+ <% stack = item["stack"].to_s.strip %>
18
+ <% outcome = item["outcome"].to_s.strip %>
19
+ <% url = item["url"].to_s.strip %>
20
+ <% next if title.blank? && summary.blank? && stack.blank? && outcome.blank? && url.blank? %>
21
+
22
+ <article class="card border border-base-200 bg-base-100 shadow-sm">
23
+ <div class="card-body">
24
+ <% if title.present? %>
25
+ <h3 class="card-title text-lg"><%= title %></h3>
26
+ <% end %>
27
+ <% if summary.present? %>
28
+ <p class="text-base-content/80"><%= summary %></p>
29
+ <% end %>
30
+ <% if stack.present? %>
31
+ <p class="mt-2 text-sm"><span class="font-semibold">Stack:</span> <%= stack %></p>
32
+ <% end %>
33
+ <% if outcome.present? %>
34
+ <p class="mt-1 text-sm"><span class="font-semibold">Outcome:</span> <%= outcome %></p>
35
+ <% end %>
36
+
37
+ <% if url.present? %>
38
+ <% external = url.start_with?("http://", "https://") %>
39
+ <div class="card-actions mt-4 justify-start">
40
+ <%= link_to "View project", url, class: "btn btn-sm btn-outline", target: (external ? "_blank" : nil), rel: (external ? "noopener noreferrer" : nil) %>
41
+ </div>
42
+ <% end %>
43
+ </div>
44
+ </article>
45
+ <% end %>
46
+ </div>
47
+ </section>
48
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <%
2
+ items = section["items"]
3
+ items = items.is_a?(Array) ? items.map { |item| item.to_s.strip }.reject(&:blank?) : []
4
+ %>
5
+
6
+ <% if items.any? %>
7
+ <section class="mb-12">
8
+ <div class="rounded-box border border-base-200 bg-base-100 px-4 py-4 sm:px-6">
9
+ <ul class="flex flex-wrap items-center gap-3 sm:gap-4">
10
+ <% items.each do |item| %>
11
+ <li class="badge badge-lg badge-outline"><%= item %></li>
12
+ <% end %>
13
+ </ul>
14
+ </div>
15
+ </section>
16
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <%
2
+ heading = section["heading"].to_s.strip.presence
3
+ markdown = section["markdown"].to_s.strip.presence || page.body_markdown.to_s
4
+ html = section["markdown"].to_s.strip.present? ? RailsSiteEngine::MarkdownRenderer.render(markdown) : page.body_html
5
+ %>
6
+
7
+ <section class="mb-14">
8
+ <% if heading.present? %>
9
+ <%= tag.public_send(heading_tag, heading, class: "mb-6 text-3xl font-bold tracking-tight") %>
10
+ <% end %>
11
+
12
+ <div class="prose max-w-none">
13
+ <%= html %>
14
+ </div>
15
+ </section>
@@ -0,0 +1,34 @@
1
+ <%
2
+ heading = section["heading"].to_s.strip.presence
3
+ items = section["items"]
4
+ items = items.is_a?(Array) ? items.select { |item| item.is_a?(Hash) } : []
5
+ %>
6
+
7
+ <% if items.any? %>
8
+ <section class="mb-14">
9
+ <% if heading.present? %>
10
+ <%= tag.public_send(heading_tag, heading, class: "mb-6 text-3xl font-bold tracking-tight") %>
11
+ <% end %>
12
+
13
+ <div class="stats stats-vertical w-full border border-base-200 bg-base-100 shadow-sm lg:stats-horizontal">
14
+ <% items.each do |item| %>
15
+ <% value = item["value"].to_s.strip %>
16
+ <% label = item["label"].to_s.strip %>
17
+ <% detail = item["description"].to_s.strip %>
18
+ <% next if value.blank? && label.blank? && detail.blank? %>
19
+
20
+ <div class="stat">
21
+ <% if label.present? %>
22
+ <div class="stat-title"><%= label %></div>
23
+ <% end %>
24
+ <% if value.present? %>
25
+ <div class="stat-value text-primary"><%= value %></div>
26
+ <% end %>
27
+ <% if detail.present? %>
28
+ <div class="stat-desc"><%= detail %></div>
29
+ <% end %>
30
+ </div>
31
+ <% end %>
32
+ </div>
33
+ </section>
34
+ <% end %>
@@ -0,0 +1,40 @@
1
+ <%
2
+ heading = section["heading"].to_s.strip.presence || section["title"].to_s.strip.presence
3
+ items = section["items"]
4
+ items = items.is_a?(Array) ? items.select { |item| item.is_a?(Hash) } : []
5
+ %>
6
+
7
+ <% if items.any? %>
8
+ <section class="mb-14">
9
+ <% if heading.present? %>
10
+ <%= tag.public_send(heading_tag, heading, class: "mb-6 text-3xl font-bold tracking-tight") %>
11
+ <% end %>
12
+
13
+ <div class="grid gap-5 md:grid-cols-2">
14
+ <% items.each do |item| %>
15
+ <% quote = item["quote"].to_s.strip %>
16
+ <% author = item["author"].to_s.strip %>
17
+ <% role = item["role"].to_s.strip %>
18
+ <% next if quote.blank? && author.blank? && role.blank? %>
19
+
20
+ <figure class="card border border-base-200 bg-base-100 shadow-sm">
21
+ <blockquote class="card-body">
22
+ <% if quote.present? %>
23
+ <p class="text-lg leading-relaxed">“<%= quote %>”</p>
24
+ <% end %>
25
+ <% if author.present? || role.present? %>
26
+ <figcaption class="mt-4 text-sm text-base-content/70">
27
+ <% if author.present? %>
28
+ <span class="font-medium text-base-content"><%= author %></span>
29
+ <% end %>
30
+ <% if role.present? %>
31
+ <span><%= author.present? ? " · #{role}" : role %></span>
32
+ <% end %>
33
+ </figcaption>
34
+ <% end %>
35
+ </blockquote>
36
+ </figure>
37
+ <% end %>
38
+ </div>
39
+ </section>
40
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <div class="alert alert-warning alert-soft mb-8" role="alert">
2
+ <div>
3
+ <div class="font-semibold">Unknown section type</div>
4
+ <div class="text-sm opacity-80"><%= section.inspect %></div>
5
+ </div>
6
+ </div>
@@ -0,0 +1,18 @@
1
+ <% flash.each do |key, message| %>
2
+ <% next if message.blank? %>
3
+
4
+ <% alert_class, role, live =
5
+ case key.to_s
6
+ when "notice"
7
+ [ "alert-success", "status", "polite" ]
8
+ when "alert"
9
+ [ "alert-error", "alert", "assertive" ]
10
+ else
11
+ [ "alert-info", "status", "polite" ]
12
+ end
13
+ %>
14
+
15
+ <div class="alert sm:alert-horizontal <%= alert_class %> mb-6 shadow-sm" role="<%= role %>" aria-live="<%= live %>">
16
+ <span><%= message %></span>
17
+ </div>
18
+ <% end %>
@@ -0,0 +1,41 @@
1
+ <footer class="border-t border-base-200 bg-base-200/30">
2
+ <div class="mx-auto w-full max-w-6xl px-4 py-10">
3
+ <div class="grid gap-8 md:grid-cols-[2fr_1fr_1fr]">
4
+ <section>
5
+ <h2 class="text-base font-semibold"><%= site_name %></h2>
6
+ <% if (description = site_config.dig("seo", "default_description").to_s.strip.presence) %>
7
+ <p class="mt-3 max-w-md text-sm text-base-content/80"><%= description %></p>
8
+ <% end %>
9
+ </section>
10
+
11
+ <nav aria-label="<%= t("rails_site_engine.footer.navigation") %>">
12
+ <h2 class="text-sm font-semibold uppercase tracking-wide text-base-content/70"><%= t("rails_site_engine.footer.quick_links") %></h2>
13
+ <ul class="menu menu-sm mt-2 w-fit gap-1 p-0">
14
+ <% links = footer_nav_items.presence || primary_nav_items %>
15
+ <% links.each do |item| %>
16
+ <li>
17
+ <%= link_to item[:label], item[:href], **nav_link_options(item) %>
18
+ </li>
19
+ <% end %>
20
+ </ul>
21
+ </nav>
22
+
23
+ <section>
24
+ <h2 class="text-sm font-semibold uppercase tracking-wide text-base-content/70"><%= t("rails_site_engine.footer.contact") %></h2>
25
+ <div class="mt-2 space-y-2 text-sm">
26
+ <% if (phone = contact_channels[:phone]).present? %>
27
+ <p><%= link_to phone, "tel:#{phone}", class: "link link-hover" %></p>
28
+ <% end %>
29
+ <% if (email = contact_channels[:email]).present? %>
30
+ <p><%= link_to email, "mailto:#{email}", class: "link link-hover" %></p>
31
+ <% end %>
32
+ </div>
33
+ </section>
34
+ </div>
35
+
36
+ <div class="mt-8 flex flex-col gap-2 border-t border-base-300 pt-4 text-sm text-base-content/70 sm:flex-row sm:items-center sm:justify-between">
37
+ <div>© <%= Time.current.year %> <%= site_name %></div>
38
+ <a href="#" class="link link-hover"><%= t("rails_site_engine.footer.back_to_top") %></a>
39
+ </div>
40
+ </div>
41
+ </footer>