journeybook 0.1.6.pre.alpha
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +354 -0
- data/Rakefile +6 -0
- data/app/assets/images/journeybook/.keep +0 -0
- data/app/assets/images/journeybook/logo.svg +5 -0
- data/app/assets/javascripts/journeybook/sortable.js +96 -0
- data/app/assets/stylesheets/journeybook/application.css +1556 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/concerns/journeybook/public_pages.rb +31 -0
- data/app/controllers/journeybook/admin/ai_pages_controller.rb +65 -0
- data/app/controllers/journeybook/admin/ai_style_prompts_controller.rb +51 -0
- data/app/controllers/journeybook/admin/components_controller.rb +57 -0
- data/app/controllers/journeybook/admin/dashboard_controller.rb +13 -0
- data/app/controllers/journeybook/admin/media_assets_controller.rb +59 -0
- data/app/controllers/journeybook/admin/navigation_items_controller.rb +91 -0
- data/app/controllers/journeybook/admin/navigation_menus_controller.rb +59 -0
- data/app/controllers/journeybook/admin/pages_controller.rb +304 -0
- data/app/controllers/journeybook/admin/redirects_controller.rb +51 -0
- data/app/controllers/journeybook/admin/sites_controller.rb +51 -0
- data/app/controllers/journeybook/application_controller.rb +52 -0
- data/app/controllers/journeybook/form_submissions_controller.rb +55 -0
- data/app/helpers/journeybook/application_helper.rb +4 -0
- data/app/helpers/journeybook/navigation_helper.rb +11 -0
- data/app/javascript/journeybook/application.js +5 -0
- data/app/javascript/journeybook/component_search_controller.js +13 -0
- data/app/jobs/journeybook/application_job.rb +4 -0
- data/app/models/concerns/.keep +0 -0
- data/app/models/journeybook/ai_style_prompt.rb +6 -0
- data/app/models/journeybook/application_record.rb +5 -0
- data/app/models/journeybook/media_asset.rb +26 -0
- data/app/models/journeybook/navigation_item.rb +45 -0
- data/app/models/journeybook/navigation_menu.rb +23 -0
- data/app/models/journeybook/page.rb +96 -0
- data/app/models/journeybook/redirect.rb +31 -0
- data/app/models/journeybook/site.rb +62 -0
- data/app/services/journeybook/ai/markdown_generator.rb +15 -0
- data/app/services/journeybook/ai/openai_client.rb +54 -0
- data/app/services/journeybook/ai/prompt_builder.rb +154 -0
- data/app/services/journeybook/ai/template_validator.rb +17 -0
- data/app/services/journeybook/component_parser.rb +53 -0
- data/app/services/journeybook/component_renderer.rb +108 -0
- data/app/services/journeybook/component_tag_updater.rb +132 -0
- data/app/services/journeybook/component_validator.rb +111 -0
- data/app/services/journeybook/form_mailer.rb +69 -0
- data/app/services/journeybook/html_sanitizer.rb +46 -0
- data/app/services/journeybook/letter_opener_client.rb +14 -0
- data/app/services/journeybook/markdown_renderer.rb +29 -0
- data/app/services/journeybook/page_renderer.rb +58 -0
- data/app/services/journeybook/page_resolver.rb +55 -0
- data/app/services/journeybook/postmark_client.rb +48 -0
- data/app/services/journeybook/storage/s3_client.rb +96 -0
- data/app/views/journeybook/admin/ai_pages/_draft_form.html.erb +73 -0
- data/app/views/journeybook/admin/ai_pages/new.html.erb +43 -0
- data/app/views/journeybook/admin/ai_style_prompts/_form.html.erb +16 -0
- data/app/views/journeybook/admin/ai_style_prompts/edit.html.erb +2 -0
- data/app/views/journeybook/admin/ai_style_prompts/index.html.erb +26 -0
- data/app/views/journeybook/admin/ai_style_prompts/new.html.erb +2 -0
- data/app/views/journeybook/admin/components/_component.html.erb +17 -0
- data/app/views/journeybook/admin/components/_reference.html.erb +9 -0
- data/app/views/journeybook/admin/components/index.html.erb +46 -0
- data/app/views/journeybook/admin/components/preview.html.erb +13 -0
- data/app/views/journeybook/admin/dashboard/show.html.erb +34 -0
- data/app/views/journeybook/admin/media_assets/_reference.html.erb +16 -0
- data/app/views/journeybook/admin/media_assets/index.html.erb +43 -0
- data/app/views/journeybook/admin/media_assets/new.html.erb +41 -0
- data/app/views/journeybook/admin/navigation_items/_form.html.erb +46 -0
- data/app/views/journeybook/admin/navigation_items/edit.html.erb +2 -0
- data/app/views/journeybook/admin/navigation_items/new.html.erb +2 -0
- data/app/views/journeybook/admin/navigation_menus/_form.html.erb +10 -0
- data/app/views/journeybook/admin/navigation_menus/edit.html.erb +38 -0
- data/app/views/journeybook/admin/navigation_menus/index.html.erb +28 -0
- data/app/views/journeybook/admin/navigation_menus/new.html.erb +2 -0
- data/app/views/journeybook/admin/pages/_collection_field.html.erb +58 -0
- data/app/views/journeybook/admin/pages/_form.html.erb +77 -0
- data/app/views/journeybook/admin/pages/_image_picker_modal.html.erb +23 -0
- data/app/views/journeybook/admin/pages/_table.html.erb +37 -0
- data/app/views/journeybook/admin/pages/_url_field.html.erb +25 -0
- data/app/views/journeybook/admin/pages/edit.html.erb +547 -0
- data/app/views/journeybook/admin/pages/errors.html.erb +25 -0
- data/app/views/journeybook/admin/pages/index.html.erb +52 -0
- data/app/views/journeybook/admin/pages/new.html.erb +27 -0
- data/app/views/journeybook/admin/pages/new_component.html.erb +70 -0
- data/app/views/journeybook/admin/pages/public_preview.html.erb +27 -0
- data/app/views/journeybook/admin/pages/source.html.erb +23 -0
- data/app/views/journeybook/admin/redirects/_form.html.erb +9 -0
- data/app/views/journeybook/admin/redirects/edit.html.erb +2 -0
- data/app/views/journeybook/admin/redirects/index.html.erb +15 -0
- data/app/views/journeybook/admin/redirects/new.html.erb +2 -0
- data/app/views/journeybook/admin/shared/_drag_handle.html.erb +3 -0
- data/app/views/journeybook/admin/shared/_errors.html.erb +10 -0
- data/app/views/journeybook/admin/sites/_form.html.erb +12 -0
- data/app/views/journeybook/admin/sites/edit.html.erb +2 -0
- data/app/views/journeybook/admin/sites/index.html.erb +26 -0
- data/app/views/journeybook/admin/sites/new.html.erb +2 -0
- data/app/views/layouts/journeybook/application.html.erb +57 -0
- data/config/importmap.rb +3 -0
- data/config/routes.rb +39 -0
- data/db/migrate/20260627000001_create_journeybook_sites.rb +13 -0
- data/db/migrate/20260627000002_create_journeybook_pages.rb +23 -0
- data/db/migrate/20260627000003_create_journeybook_redirects.rb +13 -0
- data/db/migrate/20260628000004_add_context_fields_to_journeybook_sites.rb +38 -0
- data/db/migrate/20260628000005_create_journeybook_media_assets.rb +24 -0
- data/db/migrate/20260629000006_create_journeybook_navigation.rb +28 -0
- data/db/migrate/20260731000007_add_open_in_new_window_to_journeybook_navigation_items.rb +5 -0
- data/db/migrate/20260731000008_create_journeybook_ai_style_prompts.rb +12 -0
- data/db/migrate/20260801000009_add_blog_fields_to_journeybook_pages.rb +9 -0
- data/db/migrate/20260801000010_add_knowledge_base_fields_to_journeybook_pages.rb +8 -0
- data/db/migrate/20260801000011_add_position_to_journeybook_pages.rb +6 -0
- data/lib/generators/journeybook/install/install_generator.rb +72 -0
- data/lib/generators/journeybook/install/templates/add_blog_fields_to_journeybook_pages.rb +9 -0
- data/lib/generators/journeybook/install/templates/add_context_fields_to_journeybook_sites.rb +38 -0
- data/lib/generators/journeybook/install/templates/add_knowledge_base_fields_to_journeybook_pages.rb +8 -0
- data/lib/generators/journeybook/install/templates/add_open_in_new_window_to_journeybook_navigation_items.rb +5 -0
- data/lib/generators/journeybook/install/templates/add_position_to_journeybook_pages.rb +6 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_ai_style_prompts.rb +12 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_media_assets.rb +24 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_navigation.rb +28 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_pages.rb +33 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_redirects.rb +13 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_sites.rb +13 -0
- data/lib/generators/journeybook/install/templates/journeybook.rb +42 -0
- data/lib/generators/journeybook/install/templates/pages_controller.rb +3 -0
- data/lib/generators/journeybook/install/templates/show.html.erb +10 -0
- data/lib/journeybook/component_registry.rb +136 -0
- data/lib/journeybook/configuration.rb +55 -0
- data/lib/journeybook/engine.rb +19 -0
- data/lib/journeybook/page_template_registry.rb +67 -0
- data/lib/journeybook/public_page_constraint.rb +23 -0
- data/lib/journeybook/version.rb +3 -0
- data/lib/journeybook.rb +62 -0
- data/lib/tasks/journeybook_tasks.rake +8 -0
- metadata +266 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<% content_for :main_class, "jb-main-wide" %>
|
|
2
|
+
|
|
3
|
+
<div class="jb-component-add-page">
|
|
4
|
+
<aside class="jb-card jb-component-picker" aria-label="Available components">
|
|
5
|
+
<p class="jb-eyebrow">Add component</p>
|
|
6
|
+
<h1 class="jb-card-title"><%= @page.title %></h1>
|
|
7
|
+
|
|
8
|
+
<% if @components.any? %>
|
|
9
|
+
<div data-controller="journeybook-component-search">
|
|
10
|
+
<label class="jb-component-search">
|
|
11
|
+
Search components
|
|
12
|
+
<input type="search" placeholder="Search" autocomplete="off" autofocus data-journeybook-component-search-target="input" data-action="input->journeybook-component-search#filter">
|
|
13
|
+
</label>
|
|
14
|
+
<nav class="jb-component-nav">
|
|
15
|
+
<% @components.each do |component| %>
|
|
16
|
+
<div class="jb-component-nav-item" data-journeybook-component-search-target="item" data-component-search-name="<%= component.name %>">
|
|
17
|
+
<%= link_to new_component_admin_page_path(@page, site_id: @page.site_id, component: component.name), class: ("is-active" if component == @selected_component), "aria-current": ("page" if component == @selected_component) do %>
|
|
18
|
+
<strong><%= component.name %></strong>
|
|
19
|
+
<% if component.metadata[:description].present? %>
|
|
20
|
+
<span><%= component.metadata[:description] %></span>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
23
|
+
<%= button_to "+", add_component_admin_page_path(@page, site_id: @page.site_id), method: :post, params: { component_name: component.name }, class: "jb-component-add-button", title: "Add #{component.name}", aria: { label: "Add #{component.name}" } %>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
26
|
+
</nav>
|
|
27
|
+
</div>
|
|
28
|
+
<% else %>
|
|
29
|
+
<p class="jb-subtitle">No components are registered for this site.</p>
|
|
30
|
+
<% end %>
|
|
31
|
+
|
|
32
|
+
<div class="jb-component-add-sidebar-footer">
|
|
33
|
+
<%= link_to "Back to editor", edit_admin_page_path(@page, site_id: @page.site_id, tab: "layout"), class: "jb-button jb-button-secondary" %>
|
|
34
|
+
</div>
|
|
35
|
+
</aside>
|
|
36
|
+
|
|
37
|
+
<main class="jb-card jb-component-add-preview">
|
|
38
|
+
<% if @selected_component %>
|
|
39
|
+
<header class="jb-header">
|
|
40
|
+
<div>
|
|
41
|
+
<p class="jb-eyebrow">Preview</p>
|
|
42
|
+
<h2 class="jb-title"><code>{{ <%= @selected_component.name %> }}</code></h2>
|
|
43
|
+
<% if @selected_component.metadata[:description].present? %>
|
|
44
|
+
<p class="jb-subtitle"><%= @selected_component.metadata[:description] %></p>
|
|
45
|
+
<% end %>
|
|
46
|
+
</div>
|
|
47
|
+
</header>
|
|
48
|
+
|
|
49
|
+
<iframe class="jb-preview jb-component-add-preview-frame" src="<%= admin_component_preview_path(@selected_component.name, site_id: @page.site_id) %>" title="<%= @selected_component.name %> preview"></iframe>
|
|
50
|
+
|
|
51
|
+
<% else %>
|
|
52
|
+
<p class="jb-subtitle">Select a component to preview it.</p>
|
|
53
|
+
<% end %>
|
|
54
|
+
</main>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<%= javascript_tag nonce: true do %>
|
|
58
|
+
(() => {
|
|
59
|
+
const search = document.querySelector("[data-component-search]");
|
|
60
|
+
const items = document.querySelectorAll("[data-component-search-item]");
|
|
61
|
+
if (!search || !items.length) return;
|
|
62
|
+
|
|
63
|
+
search.addEventListener("input", () => {
|
|
64
|
+
const query = search.value.trim().toLowerCase();
|
|
65
|
+
items.forEach((item) => {
|
|
66
|
+
item.hidden = Boolean(query) && !item.dataset.componentSearchItem.toLowerCase().includes(query);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
})();
|
|
70
|
+
<% end %>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<% content_for :title, @journeybook_page.meta_title.presence || @journeybook_page.title %>
|
|
2
|
+
<% content_for :head do %>
|
|
3
|
+
<% if @journeybook_page.meta_description.present? %>
|
|
4
|
+
<meta name="description" content="<%= @journeybook_page.meta_description %>">
|
|
5
|
+
<% end %>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<article class="journeybook-page"<% if params[:component].present? %> style="padding-top: 6rem"<% end %>>
|
|
9
|
+
<%= @journeybook_html.html_safe %>
|
|
10
|
+
</article>
|
|
11
|
+
|
|
12
|
+
<%= javascript_tag nonce: true do %>
|
|
13
|
+
window.addEventListener("message", (event) => {
|
|
14
|
+
if (event.origin !== window.location.origin || event.data?.type !== "journeybook:scroll-preview") return;
|
|
15
|
+
|
|
16
|
+
const index = Number(event.data.componentIndex);
|
|
17
|
+
if (!Number.isInteger(index)) return;
|
|
18
|
+
|
|
19
|
+
const anchor = document.getElementById(`journeybook-preview-component-${index}`);
|
|
20
|
+
if (!anchor) return;
|
|
21
|
+
|
|
22
|
+
window.scrollTo({
|
|
23
|
+
top: Math.max(0, anchor.getBoundingClientRect().top + window.scrollY - 48),
|
|
24
|
+
behavior: "smooth"
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
<% end %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<% content_for :title, "Edit source: #{@page.title}" %>
|
|
2
|
+
|
|
3
|
+
<header class="jb-header">
|
|
4
|
+
<div>
|
|
5
|
+
<p class="jb-eyebrow">Page source</p>
|
|
6
|
+
<h1 class="jb-title"><%= @page.title %></h1>
|
|
7
|
+
<p class="jb-subtitle">Edit the complete Markdown source for this page.</p>
|
|
8
|
+
</div>
|
|
9
|
+
<%= link_to "Back to editor", edit_admin_page_path(@page, site_id: @page.site_id, tab: "layout"), class: "jb-button jb-button-secondary" %>
|
|
10
|
+
</header>
|
|
11
|
+
|
|
12
|
+
<%= form_with model: [:admin, @page], url: source_admin_page_path(@page, site_id: @page.site_id), method: :patch, class: "jb-form jb-source-editor" do |form| %>
|
|
13
|
+
<%= render "journeybook/admin/shared/errors", record: @page %>
|
|
14
|
+
|
|
15
|
+
<div class="jb-field jb-markdown">
|
|
16
|
+
<%= form.label :markdown, "Markdown source" %>
|
|
17
|
+
<%= form.text_area :markdown, required: true, spellcheck: false, autofocus: true %>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="jb-actions">
|
|
21
|
+
<%= form.submit "Save source", class: "jb-button" %>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<%= form_with model: [:admin, redirect], class: "jb-form jb-card" do |form| %>
|
|
2
|
+
<%= render "journeybook/admin/shared/errors", record: redirect %>
|
|
3
|
+
<div class="jb-fields">
|
|
4
|
+
<div class="jb-field"><%= form.label :from %><%= form.text_field :from, required: true, placeholder: "/old-page" %></div>
|
|
5
|
+
<div class="jb-field"><%= form.label :to %><%= form.text_field :to, required: true, placeholder: "/new-page" %></div>
|
|
6
|
+
<div class="jb-field"><%= form.label :status_code %><%= form.select :status_code, Journeybook::Redirect::VALID_STATUS_CODES %></div>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="jb-actions"><%= form.submit class: "jb-button" %><%= link_to "Back", admin_redirects_path, class: "jb-button jb-button-secondary" %></div>
|
|
9
|
+
<% end %>
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
<header class="jb-header"><div><p class="jb-eyebrow">Redirects</p><h1 class="jb-title">Edit redirect</h1></div><%= button_to "Delete redirect", admin_redirect_path(@redirect), method: :delete, class: "jb-button jb-button-danger", data: { turbo_confirm: "Delete this redirect?" } %></header>
|
|
2
|
+
<%= render "form", redirect: @redirect %>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<header class="jb-header">
|
|
2
|
+
<div><p class="jb-eyebrow">Redirects</p><h1 class="jb-title">URL redirects</h1></div>
|
|
3
|
+
<%= link_to "New redirect", new_admin_redirect_path, class: "jb-button" %>
|
|
4
|
+
</header>
|
|
5
|
+
|
|
6
|
+
<% if @redirects.any? %>
|
|
7
|
+
<section class="jb-card jb-card-flush">
|
|
8
|
+
<table class="jb-table">
|
|
9
|
+
<thead><tr><th>From</th><th>To</th><th>Status</th><th></th></tr></thead>
|
|
10
|
+
<tbody><% @redirects.each do |redirect| %><tr><td><code><%= redirect.from %></code></td><td><code><%= redirect.to %></code></td><td><%= redirect.status_code %></td><td><%= link_to "Edit", edit_admin_redirect_path(redirect), class: "jb-button jb-button-secondary" %></td></tr><% end %></tbody>
|
|
11
|
+
</table>
|
|
12
|
+
</section>
|
|
13
|
+
<% else %>
|
|
14
|
+
<p class="jb-subtitle">No redirects have been configured.</p>
|
|
15
|
+
<% end %>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<span class="jb-drag-handle" data-sortable-handle aria-label="Drag to reorder" role="img">
|
|
2
|
+
<svg viewBox="0 0 12 18" aria-hidden="true"><circle cx="3" cy="3" r="1.25"/><circle cx="9" cy="3" r="1.25"/><circle cx="3" cy="9" r="1.25"/><circle cx="9" cy="9" r="1.25"/><circle cx="3" cy="15" r="1.25"/><circle cx="9" cy="15" r="1.25"/></svg>
|
|
3
|
+
</span>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<% if record.errors.any? %>
|
|
2
|
+
<div class="jb-errors">
|
|
3
|
+
<strong><%= pluralize(record.errors.count, "error") %> prevented saving:</strong>
|
|
4
|
+
<ul>
|
|
5
|
+
<% record.errors.full_messages.each do |message| %>
|
|
6
|
+
<li><%= message %></li>
|
|
7
|
+
<% end %>
|
|
8
|
+
</ul>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<%= form_with model: [:admin, site], class: "jb-form jb-card" do |form| %>
|
|
2
|
+
<%= render "journeybook/admin/shared/errors", record: site %>
|
|
3
|
+
<div class="jb-fields">
|
|
4
|
+
<div class="jb-field"><%= form.label :name %><%= form.text_field :name, required: true %></div>
|
|
5
|
+
<div class="jb-field"><%= form.label :key %><%= form.text_field :key, required: true, placeholder: "marketing" %></div>
|
|
6
|
+
<div class="jb-field"><%= form.label :host %><%= form.text_field :host, required: true %></div>
|
|
7
|
+
<div class="jb-field"><%= form.label :path_prefix %><%= form.text_field :path_prefix, required: true, placeholder: "/blog" %></div>
|
|
8
|
+
<div class="jb-field"><%= form.label :layout %><%= form.text_field :layout, placeholder: "application" %></div>
|
|
9
|
+
<div class="jb-field"><%= form.label :default_locale %><%= form.text_field :default_locale, required: true %></div>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="jb-actions"><%= form.submit "Save site", class: "jb-button" %><%= link_to "Back", admin_sites_path, class: "jb-button jb-button-secondary" %></div>
|
|
12
|
+
<% end %>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<header class="jb-header">
|
|
2
|
+
<div>
|
|
3
|
+
<p class="jb-eyebrow">Sites</p>
|
|
4
|
+
<h1 class="jb-title">Sites</h1>
|
|
5
|
+
</div>
|
|
6
|
+
<%= link_to "New site", new_admin_site_path, class: "jb-button" %>
|
|
7
|
+
</header>
|
|
8
|
+
|
|
9
|
+
<section class="jb-card jb-card-flush">
|
|
10
|
+
<table class="jb-table">
|
|
11
|
+
<thead><tr><th>Name</th><th>Key</th><th>Host</th><th>Prefix</th><th>Layout</th><th>Locale</th><th></th></tr></thead>
|
|
12
|
+
<tbody>
|
|
13
|
+
<% @sites.each do |site| %>
|
|
14
|
+
<tr>
|
|
15
|
+
<td><strong><%= site.name %></strong></td>
|
|
16
|
+
<td><code><%= site.key %></code></td>
|
|
17
|
+
<td><code><%= site.host %></code></td>
|
|
18
|
+
<td><code><%= site.path_prefix %></code></td>
|
|
19
|
+
<td><code><%= site.layout_name %></code></td>
|
|
20
|
+
<td><%= site.default_locale %></td>
|
|
21
|
+
<td><div class="jb-actions"><%= link_to "Edit", edit_admin_site_path(site), class: "jb-button jb-button-secondary" %></div></td>
|
|
22
|
+
</tr>
|
|
23
|
+
<% end %>
|
|
24
|
+
</tbody>
|
|
25
|
+
</table>
|
|
26
|
+
</section>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<title>Journeybook</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<%= csp_meta_tag %>
|
|
8
|
+
|
|
9
|
+
<%= yield :head %>
|
|
10
|
+
|
|
11
|
+
<%= stylesheet_link_tag "journeybook/application", media: "all" %>
|
|
12
|
+
<%= javascript_importmap_tags "journeybook" %>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div class="jb-shell">
|
|
16
|
+
<aside class="jb-sidebar">
|
|
17
|
+
<a class="jb-brand" href="<%= root_path %>">
|
|
18
|
+
<%= image_tag "journeybook/logo.svg", class: "jb-brand-logo", alt: "" %>
|
|
19
|
+
<strong>JOURNEYBOOK</strong>
|
|
20
|
+
</a>
|
|
21
|
+
<nav class="jb-nav" aria-label="Admin navigation">
|
|
22
|
+
<%= link_to "Dashboard", root_path, class: ("is-active" if admin_nav_active?(:dashboard)), "aria-current": ("page" if admin_nav_active?(:dashboard)) %>
|
|
23
|
+
<%= link_to "Pages", admin_pages_path, class: ("is-active" if admin_nav_active?(:pages)), "aria-current": ("page" if admin_nav_active?(:pages)) %>
|
|
24
|
+
<%= link_to "Navigation", admin_navigation_menus_path, class: ("is-active" if admin_nav_active?(:navigation)), "aria-current": ("page" if admin_nav_active?(:navigation)) %>
|
|
25
|
+
<%= link_to "Sites", admin_sites_path, class: ("is-active" if admin_nav_active?(:sites)), "aria-current": ("page" if admin_nav_active?(:sites)) %>
|
|
26
|
+
<%= link_to "Redirects", admin_redirects_path, class: ("is-active" if admin_nav_active?(:redirects)), "aria-current": ("page" if admin_nav_active?(:redirects)) %>
|
|
27
|
+
<%= link_to "Components", admin_components_path, class: ("is-active" if admin_nav_active?(:components)), "aria-current": ("page" if admin_nav_active?(:components)) %>
|
|
28
|
+
<%= link_to "Library", admin_media_assets_path, class: ("is-active" if admin_nav_active?(:library)), "aria-current": ("page" if admin_nav_active?(:library)) %>
|
|
29
|
+
<%= link_to "AI Builder", new_admin_ai_page_path, class: ("is-active" if admin_nav_active?(:ai)), "aria-current": ("page" if admin_nav_active?(:ai)) %>
|
|
30
|
+
</nav>
|
|
31
|
+
<% if Journeybook::Site.exists? %>
|
|
32
|
+
<div class="jb-context">
|
|
33
|
+
<label for="journeybook_site_context">Site context</label>
|
|
34
|
+
<select id="journeybook_site_context" onchange="if (this.value) window.location = this.value">
|
|
35
|
+
<% Journeybook::Site.order(:name).each do |site| %>
|
|
36
|
+
<option value="<%= site_context_path(site) %>" <%= "selected" if site == current_site %>><%= site.name %></option>
|
|
37
|
+
<% end %>
|
|
38
|
+
</select>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
</aside>
|
|
42
|
+
|
|
43
|
+
<main class="jb-main <%= content_for(:main_class) %>">
|
|
44
|
+
<%= yield %>
|
|
45
|
+
</main>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="jb-flashes" aria-live="polite" aria-atomic="true">
|
|
48
|
+
<% if notice.present? %>
|
|
49
|
+
<p class="jb-notice" data-flash><%= notice %></p>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% if alert.present? %>
|
|
52
|
+
<p class="jb-alert" data-flash><%= alert %></p>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
<%= javascript_include_tag "journeybook/sortable", defer: true %>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
data/config/importmap.rb
ADDED
data/config/routes.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Journeybook::Engine.routes.draw do
|
|
2
|
+
root "admin/dashboard#show"
|
|
3
|
+
post "form", to: "form_submissions#create", as: :form_submission
|
|
4
|
+
|
|
5
|
+
namespace :admin, path: "/" do
|
|
6
|
+
resource :dashboard, only: :show, controller: "dashboard"
|
|
7
|
+
resources :sites, except: :show
|
|
8
|
+
resources :redirects, except: :show
|
|
9
|
+
resources :pages, except: :show do
|
|
10
|
+
member do
|
|
11
|
+
get :source
|
|
12
|
+
patch :source, action: :update_source
|
|
13
|
+
get :preview
|
|
14
|
+
get :errors
|
|
15
|
+
get "component/new", action: :new_component, as: :new_component
|
|
16
|
+
post "component", action: :add_component, as: :add_component
|
|
17
|
+
patch :component
|
|
18
|
+
patch "component/move", action: :move_component, as: :move_component
|
|
19
|
+
patch "component/visibility", action: :toggle_component_visibility, as: :toggle_component_visibility
|
|
20
|
+
patch "components/reorder", action: :reorder_components, as: :reorder_components
|
|
21
|
+
delete "component", action: :delete_component, as: :delete_component
|
|
22
|
+
patch :publish
|
|
23
|
+
patch :draft
|
|
24
|
+
post :duplicate
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
resources :navigation_menus, path: "navigation", except: :show do
|
|
28
|
+
resources :navigation_items, only: %i[new create edit update destroy] do
|
|
29
|
+
patch :move, on: :member
|
|
30
|
+
patch :reorder, on: :collection
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
resources :components, only: :index
|
|
34
|
+
resources :media_assets, path: "library", except: %i[show edit update]
|
|
35
|
+
get "components/:component/preview", to: "components#preview", as: :component_preview
|
|
36
|
+
resources :ai_style_prompts, path: "ai/styles"
|
|
37
|
+
resource :ai_page, only: %i[new create], path: "ai"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateJourneybookSites < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :journeybook_sites do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.string :host, null: false
|
|
6
|
+
t.string :default_locale, null: false, default: "en"
|
|
7
|
+
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index :journeybook_sites, :host, unique: true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class CreateJourneybookPages < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :journeybook_pages do |t|
|
|
4
|
+
t.references :site, null: false, foreign_key: { to_table: :journeybook_sites }
|
|
5
|
+
t.string :title, null: false
|
|
6
|
+
t.string :slug, null: false
|
|
7
|
+
t.string :path, null: false
|
|
8
|
+
t.text :markdown, null: false
|
|
9
|
+
t.string :meta_title
|
|
10
|
+
t.text :meta_description
|
|
11
|
+
t.string :status, null: false, default: "draft"
|
|
12
|
+
t.datetime :published_at
|
|
13
|
+
t.string :layout
|
|
14
|
+
t.string :locale, null: false, default: "en"
|
|
15
|
+
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :journeybook_pages, %i[site_id locale slug], unique: true
|
|
20
|
+
add_index :journeybook_pages, %i[site_id locale path], unique: true
|
|
21
|
+
add_index :journeybook_pages, :status
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateJourneybookRedirects < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :journeybook_redirects do |t|
|
|
4
|
+
t.string :from, null: false
|
|
5
|
+
t.string :to, null: false
|
|
6
|
+
t.integer :status_code, null: false, default: 301
|
|
7
|
+
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index :journeybook_redirects, :from, unique: true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class AddContextFieldsToJourneybookSites < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
remove_index :journeybook_sites, :host if index_exists?(:journeybook_sites, :host, unique: true)
|
|
4
|
+
|
|
5
|
+
add_column :journeybook_sites, :key, :string
|
|
6
|
+
add_column :journeybook_sites, :path_prefix, :string, null: false, default: "/"
|
|
7
|
+
add_column :journeybook_sites, :layout, :string
|
|
8
|
+
|
|
9
|
+
reversible do |dir|
|
|
10
|
+
dir.up do
|
|
11
|
+
say_with_time "Backfilling Journeybook site keys" do
|
|
12
|
+
site_class = Class.new(ActiveRecord::Base) do
|
|
13
|
+
self.table_name = "journeybook_sites"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
used_keys = {}
|
|
17
|
+
site_class.order(:id).find_each do |site|
|
|
18
|
+
base_key = site.name.to_s.parameterize(separator: "_").presence || "site"
|
|
19
|
+
key = base_key
|
|
20
|
+
suffix = 2
|
|
21
|
+
|
|
22
|
+
while used_keys.key?(key)
|
|
23
|
+
key = "#{base_key}_#{suffix}"
|
|
24
|
+
suffix += 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
used_keys[key] = true
|
|
28
|
+
site.update_columns(key: key)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
add_index :journeybook_sites, :key, unique: true
|
|
35
|
+
add_index :journeybook_sites, :host
|
|
36
|
+
add_index :journeybook_sites, %i[host path_prefix], unique: true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class CreateJourneybookMediaAssets < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :journeybook_media_assets do |t|
|
|
4
|
+
t.string :title, null: false
|
|
5
|
+
t.string :alt_text
|
|
6
|
+
t.text :description
|
|
7
|
+
t.string :tags
|
|
8
|
+
t.string :storage_key, null: false
|
|
9
|
+
t.string :original_filename, null: false
|
|
10
|
+
t.string :content_type, null: false
|
|
11
|
+
t.integer :byte_size, null: false
|
|
12
|
+
t.string :checksum
|
|
13
|
+
t.integer :width
|
|
14
|
+
t.integer :height
|
|
15
|
+
t.json :metadata
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :journeybook_media_assets, :storage_key, unique: true
|
|
21
|
+
add_index :journeybook_media_assets, :title
|
|
22
|
+
add_index :journeybook_media_assets, :content_type
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class CreateJourneybookNavigation < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :journeybook_navigation_menus do |t|
|
|
4
|
+
t.references :site, null: false, foreign_key: { to_table: :journeybook_sites }
|
|
5
|
+
t.string :key, null: false
|
|
6
|
+
t.string :title, null: false
|
|
7
|
+
t.integer :position, null: false, default: 0
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
add_index :journeybook_navigation_menus, %i[site_id key], unique: true
|
|
13
|
+
|
|
14
|
+
create_table :journeybook_navigation_items do |t|
|
|
15
|
+
t.references :navigation_menu, null: false, foreign_key: { to_table: :journeybook_navigation_menus }
|
|
16
|
+
t.references :page, foreign_key: { to_table: :journeybook_pages }
|
|
17
|
+
t.string :label, null: false
|
|
18
|
+
t.text :description
|
|
19
|
+
t.string :external_url
|
|
20
|
+
t.integer :position, null: false, default: 0
|
|
21
|
+
t.boolean :visible, null: false, default: true
|
|
22
|
+
|
|
23
|
+
t.timestamps
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
add_index :journeybook_navigation_items, %i[navigation_menu_id position]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateJourneybookAiStylePrompts < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :journeybook_ai_style_prompts do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.text :prompt, null: false
|
|
6
|
+
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
add_index :journeybook_ai_style_prompts, :name, unique: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddBlogFieldsToJourneybookPages < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :journeybook_pages, :page_type, :string, null: false, default: "page"
|
|
4
|
+
add_column :journeybook_pages, :excerpt, :text
|
|
5
|
+
add_column :journeybook_pages, :author_name, :string
|
|
6
|
+
|
|
7
|
+
add_index :journeybook_pages, :page_type
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class AddKnowledgeBaseFieldsToJourneybookPages < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_reference :journeybook_pages, :parent, foreign_key: { to_table: :journeybook_pages }
|
|
4
|
+
add_column :journeybook_pages, :topic, :string
|
|
5
|
+
|
|
6
|
+
add_index :journeybook_pages, %i[site_id page_type topic]
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record/migration"
|
|
3
|
+
|
|
4
|
+
module Journeybook
|
|
5
|
+
module Generators
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
include ActiveRecord::Generators::Migration
|
|
8
|
+
|
|
9
|
+
MIGRATION_TEMPLATES = %w[
|
|
10
|
+
create_journeybook_sites.rb
|
|
11
|
+
create_journeybook_pages.rb
|
|
12
|
+
create_journeybook_redirects.rb
|
|
13
|
+
add_context_fields_to_journeybook_sites.rb
|
|
14
|
+
create_journeybook_media_assets.rb
|
|
15
|
+
create_journeybook_navigation.rb
|
|
16
|
+
add_open_in_new_window_to_journeybook_navigation_items.rb
|
|
17
|
+
create_journeybook_ai_style_prompts.rb
|
|
18
|
+
add_blog_fields_to_journeybook_pages.rb
|
|
19
|
+
add_knowledge_base_fields_to_journeybook_pages.rb
|
|
20
|
+
add_position_to_journeybook_pages.rb
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
source_root File.expand_path("templates", __dir__)
|
|
24
|
+
|
|
25
|
+
def self.next_migration_number(dirname)
|
|
26
|
+
@migration_number ||= Time.current.utc.strftime("%Y%m%d%H%M%S").to_i
|
|
27
|
+
(@migration_number += 1).to_s
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def copy_migrations
|
|
31
|
+
MIGRATION_TEMPLATES.each do |migration|
|
|
32
|
+
migration_template migration, "db/migrate/#{migration}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def copy_initializer
|
|
37
|
+
template "journeybook.rb", "config/initializers/journeybook.rb"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def copy_public_page_files
|
|
41
|
+
template "pages_controller.rb", "app/controllers/pages_controller.rb"
|
|
42
|
+
copy_file "show.html.erb", "app/views/pages/show.html.erb"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ensure_routes_file
|
|
46
|
+
create_file "config/routes.rb", "Rails.application.routes.draw do\nend\n" unless File.exist?(File.join(destination_root, "config/routes.rb"))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def mount_admin_routes
|
|
50
|
+
route 'mount Journeybook::Engine => "/journeybook"'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def add_public_routes
|
|
54
|
+
route 'root "pages#show"'
|
|
55
|
+
route <<~RUBY
|
|
56
|
+
constraints Journeybook::PublicPageConstraint.new do
|
|
57
|
+
get "*path" => "pages#show", as: :journeybook_page
|
|
58
|
+
end
|
|
59
|
+
RUBY
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_component_directories
|
|
63
|
+
empty_directory "app/components"
|
|
64
|
+
empty_directory "app/components/previews"
|
|
65
|
+
empty_directory "test/components/previews"
|
|
66
|
+
create_file "app/components/.keep"
|
|
67
|
+
create_file "app/components/previews/.keep"
|
|
68
|
+
create_file "test/components/previews/.keep"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddBlogFieldsToJourneybookPages < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :journeybook_pages, :page_type, :string, null: false, default: "page"
|
|
4
|
+
add_column :journeybook_pages, :excerpt, :text
|
|
5
|
+
add_column :journeybook_pages, :author_name, :string
|
|
6
|
+
|
|
7
|
+
add_index :journeybook_pages, :page_type
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class AddContextFieldsToJourneybookSites < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
remove_index :journeybook_sites, :host if index_exists?(:journeybook_sites, :host, unique: true)
|
|
4
|
+
|
|
5
|
+
add_column :journeybook_sites, :key, :string
|
|
6
|
+
add_column :journeybook_sites, :path_prefix, :string, null: false, default: "/"
|
|
7
|
+
add_column :journeybook_sites, :layout, :string
|
|
8
|
+
|
|
9
|
+
reversible do |dir|
|
|
10
|
+
dir.up do
|
|
11
|
+
say_with_time "Backfilling Journeybook site keys" do
|
|
12
|
+
site_class = Class.new(ActiveRecord::Base) do
|
|
13
|
+
self.table_name = "journeybook_sites"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
used_keys = {}
|
|
17
|
+
site_class.order(:id).find_each do |site|
|
|
18
|
+
base_key = site.name.to_s.parameterize(separator: "_").presence || "site"
|
|
19
|
+
key = base_key
|
|
20
|
+
suffix = 2
|
|
21
|
+
|
|
22
|
+
while used_keys.key?(key)
|
|
23
|
+
key = "#{base_key}_#{suffix}"
|
|
24
|
+
suffix += 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
used_keys[key] = true
|
|
28
|
+
site.update_columns(key: key)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
add_index :journeybook_sites, :key, unique: true
|
|
35
|
+
add_index :journeybook_sites, :host
|
|
36
|
+
add_index :journeybook_sites, %i[host path_prefix], unique: true
|
|
37
|
+
end
|
|
38
|
+
end
|