Packages
phoenix_kit
1.7.51
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/index.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 --%>
<.admin_page_header
back={Routes.path("/admin")}
title={gettext("Publishing")}
subtitle={
gettext(
"Monitor publishing activity, jump into group workspaces, and guide your team toward new content opportunities."
)
}
>
<:actions>
<% needs_migration =
(@fs_group_count > 0 and @empty_state?) or
Enum.any?(@dashboard_insights, & &1.needs_import) %>
<%= if needs_migration do %>
<button
type="button"
phx-click="import_all_to_db"
class="btn btn-info btn-sm"
title={gettext("Migrate all groups and posts from filesystem to database storage")}
data-confirm={
gettext(
"Migrate all publishing groups and their posts to database storage? Existing DB records will be updated."
)
}
>
<.icon name="hero-circle-stack" class="w-4 h-4 mr-1" />
{gettext("Migrate All to Database")}
</button>
<% else %>
<.link
navigate={Routes.path("/admin/settings/publishing/new")}
class="btn btn-primary btn-sm"
>
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> {gettext("Create Group")}
</.link>
<% end %>
</:actions>
</.admin_page_header>
<div class="space-y-8">
<%= if @empty_state? do %>
<div class="card bg-base-100 border border-dashed border-base-300 shadow-sm">
<div class="card-body items-center text-center space-y-4">
<div class="rounded-full bg-base-200 p-3 text-primary">
<.icon name="hero-document-text" class="w-8 h-8" />
</div>
<h2 class="text-2xl font-semibold text-base-content">
{gettext("No publishing groups in database")}
</h2>
<%= if @fs_group_count > 0 do %>
<p class="text-base text-base-content/70 max-w-xl">
{ngettext(
"%{count} publishing group found in filesystem. Migrate to database to get started.",
"%{count} publishing groups found in filesystem. Migrate to database to get started.",
@fs_group_count,
count: @fs_group_count
)}
</p>
<div class="flex flex-wrap justify-center gap-3">
<button
type="button"
phx-click="import_all_to_db"
class="btn btn-info"
data-confirm={
gettext("Migrate all publishing groups and their posts to database storage?")
}
>
<.icon name="hero-circle-stack" class="w-4 h-4 mr-1" />
{ngettext(
"Migrate %{count} Group to Database",
"Migrate %{count} Groups to Database",
@fs_group_count,
count: @fs_group_count
)}
</button>
</div>
<% else %>
<p class="text-base text-base-content/70 max-w-xl">
{gettext("Create your first publishing group to start drafting posts.")}
</p>
<div class="flex flex-wrap justify-center gap-3">
<.link
href={Routes.path("/admin/settings/publishing/new")}
class="btn btn-primary btn-sm"
>
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> {gettext(
"Create Publishing Group"
)}
</.link>
</div>
<% end %>
</div>
</div>
<% else %>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
<div class="stat bg-base-100 shadow-sm rounded-lg border border-base-200">
<div class="stat-title text-sm text-base-content/70">
{gettext("Publishing Groups")}
</div>
<div class="stat-value text-3xl text-base-content">
{@dashboard_summary.total_groups}
</div>
</div>
<div class="stat bg-base-100 shadow-sm rounded-lg border border-base-200">
<div class="stat-title text-sm text-base-content/70">{gettext("Total Posts")}</div>
<div class="stat-value text-3xl text-base-content">
{@dashboard_summary.total_posts}
</div>
</div>
<div class="stat bg-base-100 shadow-sm rounded-lg border border-base-200">
<div class="stat-title text-sm text-base-content/70">{gettext("Published")}</div>
<div class="stat-value text-3xl text-base-content">
{@dashboard_summary.published_posts}
</div>
</div>
<div class="stat bg-base-100 shadow-sm rounded-lg border border-base-200">
<div class="stat-title text-sm text-base-content/70">{gettext("Drafts")}</div>
<div class="stat-value text-3xl text-base-content">
{@dashboard_summary.draft_posts}
</div>
</div>
<div class="stat bg-base-100 shadow-sm rounded-lg border border-base-200">
<div class="stat-title text-sm text-base-content/70">{gettext("Archived Posts")}</div>
<div class="stat-value text-3xl text-base-content">
{@dashboard_summary.archived_posts}
</div>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<%= for insight <- @dashboard_insights do %>
<div class="card bg-base-100 shadow-sm border border-base-200 hover:border-primary/60 transition h-full">
<div class="card-body space-y-4 h-full flex flex-col">
<div class="flex items-start justify-between gap-3">
<div class="space-y-2">
<h3 class="text-xl font-semibold text-base-content">
<.link
navigate={Routes.path("/admin/publishing/#{insight.slug}")}
class="hover:text-primary transition-colors"
>
{insight.name}
</.link>
</h3>
<p class="text-xs text-base-content/60">
{gettext("Slug")}: <code class="font-mono">{insight.slug}</code>
</p>
<p class="text-xs text-base-content/60">
<span class="whitespace-nowrap">{gettext("Last published")}: </span>
<%= if insight.last_published_at_text do %>
<span class="whitespace-nowrap">{insight.last_published_at_text}</span>
<% else %>
{gettext("No published posts yet")}
<% end %>
</p>
</div>
<span class="badge badge-ghost badge-lg text-xs px-4 py-3 whitespace-nowrap">
<%= if insight.mode == "slug" do %>
{gettext("Slug-based")}
<% else %>
{gettext("Timestamp-based")}
<% end %>
</span>
</div>
<div class="grid grid-cols-3 gap-3">
<div class="rounded-lg bg-base-200/60 px-3 py-2 text-center">
<p class="text-xs uppercase tracking-wide text-base-content/60 min-h-[1.25rem] flex items-center justify-center">
{gettext("Posts")}
</p>
<p class="font-semibold text-base-content">{insight.posts_count}</p>
</div>
<div class="rounded-lg bg-success/10 px-3 py-2 text-center">
<p class="text-xs uppercase tracking-wide text-success min-h-[1.25rem] flex items-center justify-center">
{gettext("Published")}
</p>
<p class="font-semibold text-success">{insight.published_count}</p>
</div>
<div class="rounded-lg bg-warning/10 px-3 py-2 text-center">
<p class="text-xs uppercase tracking-wide text-warning min-h-[1.25rem] flex items-center justify-center">
{gettext("Drafts")}
</p>
<p class="font-semibold text-warning">{insight.draft_count}</p>
</div>
</div>
<div class="min-h-[2rem]">
<%= if insight.languages != [] do %>
<div class="flex flex-wrap gap-2">
<span class="text-xs font-medium uppercase text-base-content/60">
{gettext("Languages")}:
</span>
<%= for language <- insight.languages do %>
<span class="badge badge-ghost badge-sm px-2 py-1">
{language
|> PhoenixKit.Modules.Languages.DialectMapper.extract_base()
|> String.upcase()}
</span>
<% end %>
</div>
<% else %>
<p class="text-xs text-base-content/60">
{gettext("Languages")}:
<span class="badge badge-ghost badge-sm px-2 py-1 uppercase">
{gettext("N/A")}
</span>
</p>
<% end %>
</div>
<%!-- Version Structure Migration Progress/Warning --%>
<% version_migration_progress =
Map.get(@version_migrations_in_progress, insight.slug) %>
<%= if version_migration_progress do %>
<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-info/10 border border-info/20">
<span class="loading loading-spinner loading-xs text-info"></span>
<div class="flex-1">
<span class="text-xs text-info">
{gettext("Migrating to versioned structure...")}
</span>
<progress
class="progress progress-info w-full h-1 mt-1"
value={version_migration_progress.current}
max={version_migration_progress.total}
>
</progress>
</div>
</div>
<% else %>
<%= if insight.needs_version_migration do %>
<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-info/10 border border-info/20">
<.icon name="hero-arrow-up-circle" class="w-4 h-4 text-info shrink-0" />
<span class="text-xs text-info flex-1">
{ngettext(
"%{count} post needs version migration",
"%{count} posts need version migration",
insight.legacy_count,
count: insight.legacy_count
)}
</span>
<button
type="button"
class="btn btn-info btn-xs"
phx-click="show_version_migration_modal"
phx-value-slug={insight.slug}
phx-value-name={insight.name}
phx-value-count={insight.legacy_count}
>
{gettext("Migrate")}
</button>
</div>
<% end %>
<% end %>
<%!-- Primary Language Migration Progress/Warning --%>
<% migration_progress = Map.get(@migrations_in_progress, insight.slug) %>
<%= if migration_progress do %>
<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-info/10 border border-info/20">
<span class="loading loading-spinner loading-xs text-info"></span>
<div class="flex-1">
<span class="text-xs text-info">{gettext("Updating...")}</span>
<progress
class="progress progress-info w-full h-1 mt-1"
value={migration_progress.current}
max={migration_progress.total}
>
</progress>
</div>
</div>
<% else %>
<%= if insight.needs_primary_language_migration do %>
<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-warning/10 border border-warning/20">
<.icon name="hero-language" class="w-4 h-4 text-warning shrink-0" />
<span class="text-xs text-warning flex-1">
{ngettext(
"%{count} post needs primary language update",
"%{count} posts need primary language update",
insight.needs_migration_count,
count: insight.needs_migration_count
)}
</span>
<button
type="button"
class="btn btn-warning btn-xs"
phx-click="show_migration_modal"
phx-value-slug={insight.slug}
phx-value-name={insight.name}
phx-value-count={insight.needs_migration_count}
>
{gettext("Update")}
</button>
</div>
<% end %>
<% end %>
<div class="flex flex-wrap gap-2 mt-auto">
<%= if insight.needs_import do %>
<button
type="button"
phx-click="import_to_db"
phx-value-slug={insight.slug}
class="btn btn-info btn-sm"
title={gettext("Migrate posts from filesystem to database storage")}
data-confirm={
gettext(
"Migrate all posts from \"%{name}\" to database storage? Existing DB records will be updated.",
name: insight.name
)
}
>
<.icon name="hero-circle-stack" class="w-4 h-4 mr-1" />
{gettext("Migrate to Database")}
</button>
<% end %>
<.link
navigate={Routes.path("/admin/publishing/#{insight.slug}")}
class="btn btn-outline btn-sm"
>
<.icon name="hero-arrow-right" class="w-4 h-4 mr-1" />
{gettext("Open")}
</.link>
<.link
navigate={Routes.path("/admin/settings/publishing/#{insight.slug}/edit")}
class="btn btn-outline btn-sm"
>
<.icon name="hero-cog-6-tooth" class="w-4 h-4 mr-1" />
{gettext("Settings")}
</.link>
<% group_slug = insight.slug %>
<% url_prefix =
PhoenixKit.Config.get_url_prefix()
|> case do
"/" -> ""
prefix -> prefix
end %>
<%!-- Use first enabled language (default) for public URL, not UI locale --%>
<% default_language = List.first(@enabled_languages) %>
<% public_url =
if length(@enabled_languages) == 1 do
@endpoint_url <> url_prefix <> "/" <> group_slug
else
@endpoint_url <> url_prefix <> "/#{default_language}/" <> group_slug
end %>
<a
href={public_url}
target="_blank"
rel="noopener"
class="btn btn-outline btn-sm"
aria-label={gettext("View public site")}
>
<.icon name="hero-eye" class="w-4 h-4 mr-1" />
{gettext("View")}
</a>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<%!-- Version Structure Migration Confirmation Modal --%>
<%= if @show_version_migration_modal do %>
<div class="modal modal-open">
<div class="modal-box" phx-click-away="close_version_migration_modal">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
<.icon name="hero-arrow-up-circle" class="w-5 h-5 text-info" />
{gettext("Migrate to Versioned Structure")}
</h3>
<div class="space-y-4">
<p class="text-base-content/80">
{gettext(
"This will migrate %{count} posts in \"%{name}\" to the versioned structure.",
count: @version_migration_modal_count,
name: @version_migration_modal_name
)}
</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("Publishing Group")}</span>
<span class="font-medium">{@version_migration_modal_name}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-base-content/70">{gettext("Posts to Migrate")}</span>
<span class="font-medium">{@version_migration_modal_count}</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("Creates a v1/ directory for each post")}</li>
<li>{gettext("Moves post files into the v1/ directory")}</li>
<li>{gettext("Enables version history and safe editing of published content")}</li>
<li>{gettext("Does not modify post content")}</li>
</ul>
</div>
<%= if @version_migration_modal_count > 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 migrated."
)}
</p>
</div>
<% end %>
</div>
<div class="modal-action">
<button type="button" class="btn" phx-click="close_version_migration_modal">
{gettext("Cancel")}
</button>
<button
type="button"
class="btn btn-info"
phx-click="confirm_migrate_to_versioned"
phx-disable-with={gettext("Migrating...")}
>
<.icon name="hero-arrow-up-circle" class="w-4 h-4" />
{gettext("Migrate %{count} Posts", count: @version_migration_modal_count)}
</button>
</div>
</div>
</div>
<% end %>
<%!-- Primary Language Migration Confirmation Modal --%>
<%= if @show_migration_modal do %>
<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 \"%{name}\".",
"This will update the primary language setting for %{count} posts in \"%{name}\".",
@migration_modal_count,
name: @migration_modal_name
)}
</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("Publishing Group")}</span>
<span class="font-medium">{@migration_modal_name}</span>
</div>
<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">{@migration_modal_count}</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 @migration_modal_count > 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", @migration_modal_count)}
</button>
</div>
</div>
</div>
<% end %>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>