Packages
phoenix_kit
1.7.63
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/modules/publishing/web/listing.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={@page_title}
current_path={@current_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="container flex-col mx-auto px-4 py-6">
<%!-- Header Section --%>
<% group_slug = (@current_group && @current_group["slug"]) || @group_slug || "your-group" %>
<% url_prefix =
PhoenixKit.Config.get_url_prefix()
|> case do
"/" -> ""
prefix -> prefix
end %>
<% path_hint = @group_path_prefix <> group_slug <> "/…" %>
<%!-- Use primary language for public URL preview --%>
<% public_url =
if length(@enabled_languages) == 1 do
@endpoint_url <> url_prefix <> "/" <> group_slug
else
@endpoint_url <> url_prefix <> "/#{@primary_language}/" <> group_slug
end %>
<.admin_page_header back={Routes.path("/admin/publishing")}>
<h1 class="text-xl sm:text-2xl lg:text-3xl font-bold text-base-content">
{(@current_group && @current_group["name"]) || @group_slug || gettext("Group")}
</h1>
<div class="text-sm text-base-content/60 space-y-1 mt-0.5">
<p>
<span class="font-medium text-base-content">{gettext("Path")}: </span>
<code class="font-mono break-all">{path_hint}</code>
</p>
<p>
<span class="font-medium text-base-content">{gettext("Public URL")}: </span>
<a
href={public_url}
target="_blank"
class="link link-hover font-mono break-all text-xs"
>
{public_url}
</a>
</p>
</div>
</.admin_page_header>
<%!-- Cache Status Section (Collapsible) --%>
<%= if @cache_info do %>
<div class="collapse collapse-arrow bg-base-100 shadow-sm border border-base-200 mb-6">
<input type="checkbox" class="peer" />
<%!-- Collapse Title - Always visible --%>
<div class="collapse-title p-4 min-h-0 flex items-center">
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
<.icon name="hero-archive-box" class="w-5 h-5" />
<span class="font-semibold">{gettext("Caching")}</span>
</div>
<%= if @cache_info.memory_enabled || (@cache_info.render_enabled && @cache_info.render_global_enabled) do %>
<span class="badge badge-success badge-sm gap-1 mr-6">
<.icon name="hero-check" class="w-3 h-3" />
{gettext("ON")}
</span>
<% else %>
<span class="badge badge-ghost badge-sm mr-6">
{gettext("OFF")}
</span>
<% end %>
</div>
</div>
<%!-- Collapse Content - Expandable details --%>
<div class="collapse-content !p-0">
<div class="px-4 pb-4">
<div class="space-y-3">
<%!-- Memory Cache Row --%>
<div>
<div class="flex items-center justify-between p-2 rounded-lg bg-base-200">
<div class="flex items-center gap-2">
<.icon name="hero-cpu-chip" class="w-4 h-4 text-base-content/70" />
<span class="text-sm font-medium">{gettext("Memory")}</span>
<%= if @cache_info.memory_enabled do %>
<%= if @cache_info.in_memory do %>
<span class="badge badge-success badge-xs">{gettext("loaded")}</span>
<%= if @cache_info.cache_generated_at do %>
<span class="text-xs text-base-content/50">
{gettext("from DB %{time}",
time: format_cache_time(@cache_info.cache_generated_at)
)}
</span>
<% end %>
<% else %>
<span class="badge badge-ghost badge-xs">{gettext("empty")}</span>
<% end %>
<% else %>
<span class="badge badge-ghost badge-xs">{gettext("off")}</span>
<% end %>
</div>
<div class="flex items-center gap-1">
<%= if @cache_info.memory_enabled do %>
<%= if @cache_info.in_memory do %>
<button
type="button"
phx-click="invalidate_memory_cache"
class="btn btn-ghost btn-xs text-error tooltip tooltip-bottom"
data-tip={gettext("Clear memory cache")}
>
<.icon name="hero-trash" class="w-3 h-3 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">
{gettext("Clear memory cache")}
</span>
</button>
<% end %>
<button
type="button"
phx-click="load_memory_cache"
class="btn btn-ghost btn-xs tooltip tooltip-bottom"
data-tip={gettext("Load into memory")}
>
<.icon name="hero-arrow-path" class="w-3 h-3 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">
{gettext("Load into memory")}
</span>
</button>
<% end %>
<input
type="checkbox"
class="toggle toggle-primary toggle-xs"
checked={@cache_info.memory_enabled}
phx-click="toggle_memory_cache"
/>
</div>
</div>
<p class="text-xs text-base-content/50 mt-1 ml-1">
<%= cond do %>
<% @cache_info.memory_enabled && @cache_info.in_memory -> %>
<span class="text-success">
{gettext("Listing requests served in sub-milliseconds.")}
</span>
<% @cache_info.memory_enabled && !@cache_info.in_memory -> %>
{gettext("Not loaded. Will load from database on first request.")}
<% true -> %>
{gettext("Disabled. Listing queries database on each request.")}
<% end %>
</p>
</div>
<%!-- Post Render Cache Row --%>
<div>
<div class="flex items-center justify-between p-2 bg-base-200 rounded-lg">
<div class="flex items-center gap-2">
<.icon name="hero-document-text" class="w-4 h-4 text-base-content/70" />
<span class="text-sm font-medium">{gettext("Posts")}</span>
<%= if @cache_info.render_enabled && @cache_info.render_global_enabled do %>
<span class="badge badge-success badge-xs">{gettext("on")}</span>
<% else %>
<%= if !@cache_info.render_global_enabled do %>
<span class="badge badge-ghost badge-xs">{gettext("global off")}</span>
<% else %>
<span class="badge badge-ghost badge-xs">{gettext("off")}</span>
<% end %>
<% end %>
</div>
<div class="flex items-center gap-1">
<%= if @cache_info.render_enabled && @cache_info.render_global_enabled do %>
<button
type="button"
phx-click="clear_render_cache"
class="btn btn-ghost btn-xs text-error tooltip tooltip-bottom"
data-tip={gettext("Clear render cache")}
>
<.icon name="hero-trash" class="w-3 h-3 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">
{gettext("Clear render cache")}
</span>
</button>
<% end %>
<input
type="checkbox"
class="toggle toggle-primary toggle-xs"
checked={@cache_info.render_enabled}
phx-click="toggle_render_cache"
disabled={!@cache_info.render_global_enabled}
title={
if !@cache_info.render_global_enabled,
do: gettext("Enable global render cache first in Publishing Settings"),
else: nil
}
/>
</div>
</div>
<p class="text-xs text-base-content/50 mt-1 ml-1">
<%= if @cache_info.render_enabled && @cache_info.render_global_enabled do %>
{gettext(
"Rendered HTML cached in ETS. Auto-invalidates when content changes."
)}
<% else %>
{gettext("Disabled. Markdown rendered fresh on each view.")}
<% end %>
</p>
</div>
</div>
</div>
</div>
</div>
<% end %>
<%!-- Primary Language Migration Progress Banner --%>
<%= if @migration_in_progress do %>
<div class="flex items-center gap-3 px-4 py-3 rounded-lg bg-gradient-to-r from-info/10 to-primary/10 border border-info/30 mb-6">
<div class="flex items-center justify-center w-10 h-10 rounded-full bg-gradient-to-br from-info to-primary text-white shadow-sm">
<span class="loading loading-spinner loading-sm"></span>
</div>
<div class="flex-1">
<span class="text-sm font-medium text-info">
{gettext("Migration in progress...")}
</span>
<%= if @migration_progress do %>
<div class="flex items-center gap-2 mt-1">
<progress
class="progress progress-info w-48"
value={@migration_progress.current}
max={@migration_progress.total}
>
</progress>
<span class="text-xs text-base-content/60">
{@migration_progress.current} / {@migration_progress.total}
</span>
</div>
<% end %>
</div>
</div>
<% end %>
<%!-- Primary Language Migration Banner --%>
<%= if not @migration_in_progress and @primary_language_status && (@primary_language_status.needs_backfill > 0 or @primary_language_status.needs_migration > 0) do %>
<% total_needing =
@primary_language_status.needs_backfill + @primary_language_status.needs_migration %>
<div class="flex items-center gap-3 px-4 py-3 rounded-lg bg-gradient-to-r from-warning/10 to-amber-500/10 border border-warning/30 mb-6">
<div class="flex items-center justify-center w-10 h-10 rounded-full bg-gradient-to-br from-warning to-amber-500 text-white shadow-sm">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
</div>
<div class="flex-1">
<span class="text-sm font-medium text-warning">
{ngettext(
"1 post needs primary language update",
"%{count} posts need primary language update",
total_needing,
count: total_needing
)}
</span>
<p class="text-xs text-base-content/60">
{gettext(
"Save the current language setting (%{lang}) to these posts to ensure they remain accessible if the global setting changes.",
lang: @primary_language_name
)}
</p>
</div>
<button
type="button"
class="btn btn-warning btn-sm"
phx-click="show_migration_modal"
>
<.icon name="hero-arrow-down-tray" class="w-4 h-4" />
{gettext("Update All")}
</button>
</div>
<% end %>
<div class="space-y-6">
<div class="flex-1">
<div class="flex flex-col md:flex-row gap-3 md:items-center md:justify-between mb-4">
<span class="text-sm text-base-content/70">
{gettext("Showing %{count} posts", count: length(@posts))}
</span>
<div class="flex gap-2">
<button type="button" class="btn btn-outline btn-sm shadow-none" phx-click="refresh">
<.icon name="hero-arrow-path" class="w-4 h-4 mr-1" /> {gettext("Refresh")}
</button>
<button type="button" class="btn btn-primary btn-sm" phx-click="create_post">
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> {gettext("Create Post")}
</button>
</div>
</div>
<%= if @loading do %>
<div class="flex justify-center items-center py-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
<% else %>
<%= if @posts == [] do %>
<div class="card bg-base-100 shadow-sm border border-base-200">
<div class="card-body">
<h3 class="text-lg font-semibold text-base-content">
{gettext("No posts yet")}
</h3>
<p class="text-sm text-base-content/70">
{gettext("Create your first post to start sharing content.")}
</p>
</div>
</div>
<% else %>
<div class="grid gap-4">
<%= for post <- @posts do %>
<% group_slug =
post.group || (@current_group && @current_group["slug"]) || @group_slug ||
"group" %>
<%!-- Use post's stored primary language, falling back to global only if not stored --%>
<% post_primary_lang = post[:primary_language] || @primary_language %>
<% default_lang_path =
Path.join([
Path.dirname(post.path),
"#{post_primary_lang}.phk"
]) %>
<% post_path = @group_path_prefix <> post.path %>
<%!-- Use post's primary language for the public URL --%>
<% public_url =
PublishingHTML.build_post_url(group_slug, post, post_primary_lang) %>
<% full_public_url = @endpoint_url <> public_url %>
<%!-- Version info (only for slug-mode posts) --%>
<% version_count = length(Map.get(post, :available_versions) || []) %>
<% is_versioned = post.mode == :slug %>
<div class="card bg-base-100 shadow-sm border border-base-200 overflow-hidden">
<div class="card-body p-4 md:p-5 min-w-0">
<%!-- Post info section --%>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 text-xs uppercase tracking-wide text-base-content/60">
<span>
{format_datetime(post, @phoenix_kit_current_user, @date_time_settings)}
</span>
<%!-- Version info (show published version and count) --%>
<%= if is_versioned do %>
<% {display_version, display_status, _label} = get_display_version(post) %>
<span class={[
"badge badge-xs gap-1",
display_status == "published" && "badge-success",
display_status != "published" && "badge-ghost"
]}>
<%= if version_count > 1 do %>
{ngettext("%{count} version", "%{count} versions", version_count,
count: version_count
)}
<span class="opacity-50">|</span>
<% end %>
{gettext("showing")} v{display_version}
</span>
<% end %>
</div>
<h3 class="text-xl font-semibold text-base-content mt-1 truncate">
<a
href={
if post[:uuid] do
PhoenixKit.Utils.Routes.path(
"/admin/publishing/#{group_slug}/#{post.uuid}"
)
else
PhoenixKit.Utils.Routes.path(
"/admin/publishing/#{group_slug}/edit?path=#{URI.encode(default_lang_path)}"
)
end
}
class="hover:text-primary transition-colors"
>
{post.metadata.title || gettext("Untitled post")}
</a>
</h3>
<%= unless post[:uuid] do %>
<p class="text-xs text-base-content/50 mt-1">
<span class="font-medium text-base-content">
{gettext("Path")}:
</span>
<code class="font-mono break-all">{post_path}</code>
</p>
<% end %>
<p class="text-xs text-base-content/50">
<span class="font-medium text-base-content">
{gettext("Public URL")}:
</span>
<a
href={full_public_url}
target="_blank"
class="link link-hover font-mono break-all text-xs"
>
{full_public_url}
</a>
</p>
</div>
<%!-- Languages section --%>
<div class="border-t border-base-200 pt-3 mt-3 space-y-2">
<%!-- Build all languages and separate primary from others --%>
<% all_languages =
build_post_languages(
post,
@group_slug,
@enabled_languages,
@current_locale
) %>
<% primary_lang = Enum.find(all_languages, & &1.is_primary) %>
<% other_languages = Enum.reject(all_languages, & &1.is_primary) %>
<%!-- Primary language (clickable, prominent) --%>
<%= if primary_lang do %>
<button
type="button"
phx-click="language_action"
phx-value-language={primary_lang.code}
phx-value-path={primary_lang.path}
phx-value-post_path={primary_lang.post_path}
phx-value-uuid={post[:uuid]}
phx-value-status={primary_lang.status}
class="inline-flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-primary/10 hover:bg-primary/20 border border-primary/20 cursor-pointer transition-colors"
title={primary_lang.name}
>
<span class="text-xs text-primary/70 font-medium">
{gettext("Primary")}
</span>
<span class={[
"rounded-full inline-block w-2.5 h-2.5",
cond do
not primary_lang.exists -> "bg-base-content/20"
primary_lang.status == "published" -> "bg-success"
primary_lang.status == "draft" -> "bg-warning"
primary_lang.status == "archived" -> "bg-base-content/40"
true -> "bg-base-content/20"
end
]}>
</span>
<span class={[
"text-sm font-bold uppercase",
cond do
not primary_lang.exists -> "text-base-content/20"
primary_lang.status == "published" -> "text-success"
primary_lang.status == "draft" -> "text-warning"
primary_lang.status == "archived" -> "text-base-content/40"
true -> "text-base-content/20"
end
]}>
{String.upcase(primary_lang.display_code)}
</span>
</button>
<% end %>
<%!-- Other translations (excluding primary) --%>
<.publishing_language_switcher
languages={other_languages}
show_status={true}
show_add={true}
on_click="language_action"
size={:sm}
/>
</div>
<%!-- Translation Progress Bar --%>
<%= if translation_status = Map.get(@translating_posts, post.slug) do %>
<div class="border-t border-base-200 pt-3 mt-3">
<%= if translation_status.status == :in_progress do %>
<div class="flex items-center justify-between text-xs mb-1">
<span class="text-base-content/70 flex items-center gap-1">
<span class="loading loading-spinner loading-xs"></span>
{gettext("Translating...")}
</span>
<span class="font-medium">
{translation_status.completed} / {translation_status.total}
</span>
</div>
<progress
class="progress progress-primary w-full h-2"
value={translation_status.completed}
max={translation_status.total}
>
</progress>
<% else %>
<div class="flex items-center gap-2 text-xs text-success">
<.icon name="hero-check-circle" class="w-4 h-4" />
{ngettext(
"Translation complete - %{count} language",
"Translation complete - %{count} languages",
translation_status.success_count,
count: translation_status.success_count
)}
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
<%!-- Primary Language Migration Confirmation Modal --%>
<%= if @show_migration_modal do %>
<% total_needing =
@primary_language_status.needs_backfill + @primary_language_status.needs_migration %>
<div class="modal modal-open">
<div class="modal-box" phx-click-away="close_migration_modal">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
<.icon name="hero-language" class="w-5 h-5 text-warning" />
{gettext("Update Primary Language")}
</h3>
<div class="space-y-4">
<p class="text-base-content/80">
{ngettext(
"This will update the primary language setting for %{count} post in this publishing group.",
"This will update the primary language setting for %{count} posts in this publishing group.",
total_needing,
count: total_needing
)}
</p>
<div class="bg-base-200 rounded-lg p-4 space-y-2">
<div class="flex justify-between text-sm">
<span class="text-base-content/70">{gettext("New Primary Language")}</span>
<span class="font-medium">{@primary_language_name}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-base-content/70">{gettext("Posts to Update")}</span>
<span class="font-medium">{total_needing}</span>
</div>
</div>
<div class="text-sm text-base-content/70 space-y-2">
<p>
<strong>{gettext("What this does:")}</strong>
</p>
<ul class="list-disc list-inside space-y-1 pl-2">
<li>{gettext("Saves the current primary language to each post's metadata")}</li>
<li>{gettext("Keeps posts accessible if the global language changes")}</li>
<li>{gettext("Does not modify post content or translations")}</li>
</ul>
</div>
<%= if total_needing > 20 do %>
<div class="flex items-start gap-2 p-3 rounded-lg bg-info/10 border border-info/20">
<.icon name="hero-information-circle" class="w-4 h-4 text-info shrink-0 mt-0.5" />
<p class="text-xs text-info">
{gettext(
"This migration will run in the background. You can continue working while posts are updated."
)}
</p>
</div>
<% end %>
</div>
<div class="modal-action">
<button type="button" class="btn" phx-click="close_migration_modal">
{gettext("Cancel")}
</button>
<button
type="button"
class="btn btn-warning"
phx-click="confirm_migrate_primary_language"
phx-disable-with={gettext("Updating...")}
>
<.icon name="hero-arrow-path" class="w-4 h-4" />
{ngettext("Update %{count} Post", "Update %{count} Posts", total_needing,
count: total_needing
)}
</button>
</div>
</div>
</div>
<% end %>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>