Packages

phoenix_kit

1.7.28
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
phoenix_kit lib modules publishing web index.html.heex
Raw

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 --%>
<header class="w-full relative mb-6">
<%!-- Back Button (Left aligned) --%>
<.link
navigate={Routes.path("/admin")}
class="btn btn-outline btn-primary btn-sm absolute left-0 top-0 -mb-12"
>
<.icon name="hero-arrow-left" class="w-4 h-4 mr-2" /> Back to Dashboard
</.link>
<%!-- Create Group Button (Right aligned) --%>
<.link
navigate={Routes.path("/admin/settings/publishing/new")}
class="btn btn-primary btn-sm absolute right-0 top-0 -mb-12"
>
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> {gettext("Create Group")}
</.link>
<%!-- Title Section --%>
<div class="text-center">
<h1 class="text-4xl font-bold text-base-content mb-3">{gettext("Publishing")}</h1>
<p class="text-lg text-base-content">
{gettext(
"Monitor publishing activity, jump into blog workspaces, and guide your team toward new content opportunities."
)}
</p>
</div>
</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 yet")}
</h2>
<p class="text-base text-base-content/70 max-w-xl">
{gettext(
"Publishing lets you organize content into themed workspaces with their own publishing flows. Set up 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>
<.link
navigate={Routes.path("/admin/settings/publishing")}
class="btn btn-outline btn-sm"
>
<.icon name="hero-information-circle" class="w-4 h-4 mr-1" /> {gettext(
"Learn More"
)}
</.link>
</div>
</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_blogs}
</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 gap-2 mt-auto">
<.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>
<% blog_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 <> "/" <> blog_slug
else
@endpoint_url <> url_prefix <> "/#{default_language}/" <> blog_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">
{gettext(
"This will update the primary language setting for %{count} posts in \"%{name}\".",
count: @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("Ensures posts remain accessible if the global language setting 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-down-tray" class="w-4 h-4" />
{gettext("Update %{count} Posts", count: @migration_modal_count)}
</button>
</div>
</div>
</div>
<% end %>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>