Packages
phoenix_kit
1.6.4
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/live/modules/blogging/editor.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={@page_title}
current_path={
PhoenixKit.Utils.Routes.path("/admin/blogging/#{@blog_slug}/edit", locale: @current_locale)
}
project_title={@project_title}
current_locale={@current_locale}
>
<script>
(function() {
// Track unsaved changes state
window.editorUnsavedChanges = false;
// Browser exit protection
window.addEventListener('beforeunload', function(e) {
if (window.editorUnsavedChanges) {
e.preventDefault();
e.returnValue = '';
return '';
}
});
// Intercept navigation links
document.addEventListener('click', function(e) {
if (window.editorUnsavedChanges) {
const link = e.target.closest('a[href], a[data-phx-link]');
if (link && !link.hasAttribute('data-confirm')) {
const href = link.getAttribute('href') || link.getAttribute('data-phx-link');
// Don't block external links or hash links
if (href && !href.startsWith('http') && !href.startsWith('#')) {
e.preventDefault();
e.stopPropagation();
document.getElementById('confirm-cancel-btn').click();
}
}
}
}, true);
// Listen for changes status from LiveView
window.addEventListener('phx:changes-status', function(e) {
window.editorUnsavedChanges = e.detail.has_changes;
});
// Listen for confirm navigation event
window.addEventListener('phx:confirm-navigation', function(e) {
if (confirm('You have unsaved changes. Are you sure you want to leave?')) {
document.getElementById('confirm-cancel-btn').click();
}
});
// Listen for slug update event from LiveView
window.addEventListener('phx:update-slug', function(e) {
const slugInput = document.getElementById('slug-input');
if (slugInput && e.detail.slug) {
slugInput.value = e.detail.slug;
}
});
})();
</script>
<%!-- Hidden confirmation button for JavaScript --%>
<button
id="confirm-cancel-btn"
type="button"
phx-click="cancel"
class="hidden"
aria-hidden="true"
>
</button>
<div class="container mx-auto px-4 py-6 space-y-6">
<div class="flex items-center justify-between">
<button type="button" class="btn btn-outline btn-sm shadow-none" phx-click="back_to_list">
<.icon name="hero-arrow-left" class="w-4 h-4 mr-1" /> {gettext("Back to %{blog}",
blog: @blog_name || gettext("Blog")
)}
</button>
<div class="flex items-center gap-3">
<%!-- Language Switcher --%>
<%= if length(@all_enabled_languages) > 1 do %>
<div class="flex items-center gap-2" id="language-switcher">
<span class="text-sm text-base-content/70">{gettext("Language")}:</span>
<div class="join">
<%= for lang_code <- @all_enabled_languages do %>
<% lang_info = PhoenixKitWeb.Live.Modules.Blogging.get_language_info(lang_code)
flag = if lang_info, do: lang_info.flag, else: ""
is_current = @current_language == lang_code
file_exists = lang_code in @available_languages %>
<button
type="button"
class={[
"join-item btn btn-sm shadow-none",
cond do
is_current -> "btn-primary"
file_exists -> "btn-outline"
true -> "btn-success btn-outline"
end
]}
phx-click="switch_language"
phx-value-language={lang_code}
title={
cond do
is_current && lang_info -> "Editing #{lang_info.name}"
file_exists && lang_info -> "Switch to #{lang_info.name}"
lang_info -> "Create #{lang_info.name} translation"
true -> lang_code
end
}
>
<%= cond do %>
<% is_current -> %>
<.icon name="hero-check" class="w-4 h-4" />
<% not file_exists -> %>
<.icon name="hero-plus" class="w-3 h-3" />
<% true -> %>
<% end %>
<span class="text-lg phx-click-loading:opacity-50">{flag}</span>
<span class="phx-click-loading:hidden">{String.upcase(lang_code)}</span>
<span class="hidden phx-click-loading:inline-block loading loading-spinner loading-xs">
</span>
</button>
<% end %>
</div>
</div>
<% end %>
<button
type="button"
class="btn btn-outline btn-sm shadow-none"
phx-click="preview"
>
<.icon name="hero-eye" class="w-4 h-4 mr-1" /> {gettext("Preview")}
</button>
<%= if @form["status"] == "published" && @public_url do %>
<a
href={if @has_pending_changes, do: "#", else: @public_url}
target="_blank"
class={[
"btn btn-outline btn-sm shadow-none",
!@has_pending_changes && "btn-success",
@has_pending_changes && "btn-disabled pointer-events-none opacity-60"
]}
aria-disabled={@has_pending_changes}
tabindex={if @has_pending_changes, do: "-1", else: "0"}
title={
if(@has_pending_changes,
do: "Save the post before viewing the public page",
else: "View this post on the public site"
)
}
>
<.icon name="hero-globe-alt" class="w-4 h-4 mr-1" /> View Public
</a>
<% end %>
</div>
</div>
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body space-y-6">
<div class="flex flex-col lg:flex-row gap-6">
<div class="lg:w-80 space-y-4">
<.form for={@form} id="blogging-meta" phx-change="update_meta" phx-submit="noop">
<div class="space-y-4">
<%!-- Title is extracted from markdown content (first # Heading) --%>
<%= if @blog_mode == "slug" do %>
<div>
<label class="label">
<span class="label-text text-sm font-semibold text-base-content">
{gettext("Slug")}
</span>
</label>
<input
type="text"
name="slug"
id="slug-input"
value={@form["slug"]}
pattern="[a-z0-9]+(-[a-z0-9]+)*"
class="input input-bordered w-full lowercase"
placeholder={gettext("auto-generated from first heading")}
title={
gettext(
"Use lowercase letters, numbers, and hyphens only. No spaces or special characters."
)
}
/>
<p class="text-xs text-base-content/60 mt-1">
{gettext("Use lowercase letters, numbers, and hyphens only.")}
</p>
</div>
<% end %>
<div>
<label class="label">
<span class="label-text text-sm font-semibold text-base-content">
{gettext("Featured image file ID")}
</span>
</label>
<input
type="text"
name="featured_image_id"
value={@form["featured_image_id"]}
class="input input-bordered w-full font-mono"
placeholder={gettext("Paste a PhoenixKit Storage file ID")}
/>
<p class="text-xs text-base-content/60 mt-1 space-x-1">
<span>
{gettext("Use the Media Manager to upload or copy a file ID.")}
</span>
<a
href={
PhoenixKit.Utils.Routes.path("/admin/users/media",
locale: @current_locale
)
}
target="_blank"
class="link link-hover"
>
{gettext("Open Media Manager")}
</a>
</p>
<%= if preview_url = featured_image_preview_url(@form["featured_image_id"]) do %>
<div class="mt-3 space-y-2">
<img
src={preview_url}
alt={@post.metadata.title || gettext("Featured image preview")}
class="w-full rounded-lg border border-base-300 object-cover max-h-48"
loading="lazy"
/>
<p class="text-xs text-base-content/60">
{gettext("Clear the field above to remove the featured image.")}
</p>
<p class="text-xs text-success">
{gettext("Valid image ID")}
</p>
</div>
<% else %>
<% trimmed_id = @form["featured_image_id"] |> to_string() |> String.trim() %>
<%= if trimmed_id == "" do %>
<p class="text-xs text-base-content/60 mt-3">
{gettext("No featured image selected.")}
</p>
<% else %>
<p class="text-xs text-error mt-3">
{gettext("Invalid image ID or file not found.")}
</p>
<% end %>
<% end %>
</div>
<div>
<label class="label">
<span class="label-text text-sm font-semibold text-base-content">
{gettext("Status")}
</span>
</label>
<select
class="select select-bordered w-full"
name="status"
>
<option value="draft" selected={@form["status"] == "draft"}>
{gettext("Draft")}
</option>
<option value="published" selected={@form["status"] == "published"}>
{gettext("Published")}
</option>
<option value="archived" selected={@form["status"] == "archived"}>
{gettext("Archived")}
</option>
</select>
</div>
<div>
<label class="label">
<span class="label-text text-sm font-semibold text-base-content">
{gettext("Publication Date & Time (UTC)")}
</span>
</label>
<input
type="datetime-local"
name="published_at"
value={datetime_local_value(@form["published_at"])}
class="input input-bordered w-full"
/>
<p class="text-xs text-base-content/60 mt-1">
{gettext(
"Updating the publication time moves the file to the matching folder."
)}
</p>
</div>
</div>
</.form>
</div>
<div class="flex-1 space-y-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<h2 class="text-xl font-semibold text-base-content">{gettext("Content")}</h2>
<div class="flex items-center gap-2">
<%= if @has_pending_changes do %>
<span class="badge badge-warning badge-sm">{gettext("Unsaved changes")}</span>
<% end %>
<button
type="button"
class={[
"btn btn-sm inline-flex items-center justify-center gap-2 px-3",
if(@has_pending_changes, do: "btn-primary", else: "btn-disabled")
]}
phx-click="save"
disabled={!@has_pending_changes}
>
<.icon name="hero-check" class="w-4 h-4" />
<span class="whitespace-nowrap">{gettext("Save Post")}</span>
</button>
</div>
</div>
<.form for={%{}} id="blogging-content" phx-change="update_content" phx-submit="noop">
<textarea
name="content"
class="textarea textarea-bordered w-full h-[480px] font-mono text-sm leading-6"
>{@content}</textarea>
</.form>
</div>
</div>
</div>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>