Packages

phoenix_kit

1.7.69
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 --%>
<.admin_page_header
back={Routes.path("/admin")}
title={gettext("Publishing")}
>
<:actions>
<.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>
</:actions>
</.admin_page_header>
<div class="space-y-4 sm: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 p-4 sm:p-8 items-center text-center space-y-3 sm: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-xl sm:text-2xl font-semibold text-base-content">
{gettext("No publishing groups yet")}
</h2>
<p class="text-sm sm: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>
</div>
</div>
<% else %>
<%!-- "Publishing Groups" stat: hidden on mobile, visible from sm up --%>
<div class="hidden sm:grid grid-cols-2 gap-4 md:grid-cols-3 xl:grid-cols-5">
<div class="stat bg-base-100 shadow-sm rounded-lg border border-base-200 px-6 py-4">
<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 px-6 py-4">
<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 px-6 py-4">
<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 px-6 py-4">
<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 px-6 py-4">
<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>
<%!-- Mobile: compact 4-stat row without "Publishing Groups" --%>
<div class="grid grid-cols-4 gap-1.5 sm:hidden">
<div class="stat bg-base-100 shadow-sm rounded-lg border border-base-200 px-1 py-2 text-center">
<div class="stat-title text-[10px] text-base-content/70">{gettext("Posts")}</div>
<div class="stat-value text-base tabular-nums truncate text-base-content">
{@dashboard_summary.total_posts}
</div>
</div>
<div class="stat bg-base-100 shadow-sm rounded-lg border border-base-200 px-1 py-2 text-center">
<div class="stat-title text-[10px] text-base-content/70">{gettext("Published")}</div>
<div class="stat-value text-base tabular-nums truncate text-base-content">
{@dashboard_summary.published_posts}
</div>
</div>
<div class="stat bg-base-100 shadow-sm rounded-lg border border-base-200 px-1 py-2 text-center">
<div class="stat-title text-[10px] text-base-content/70">{gettext("Drafts")}</div>
<div class="stat-value text-base tabular-nums truncate text-base-content">
{@dashboard_summary.draft_posts}
</div>
</div>
<div class="stat bg-base-100 shadow-sm rounded-lg border border-base-200 px-1 py-2 text-center">
<div class="stat-title text-[10px] text-base-content/70">{gettext("Archived")}</div>
<div class="stat-value text-base tabular-nums truncate text-base-content">
{@dashboard_summary.archived_posts}
</div>
</div>
</div>
<div class="grid gap-3 sm: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 p-3 sm:p-6 space-y-1.5 sm:space-y-4 h-full flex flex-col">
<div class="flex flex-wrap items-center sm:items-start justify-between gap-1 sm:gap-3">
<div class="sm:space-y-2 min-w-0 flex-1">
<h3 class="text-lg sm: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="hidden sm:block text-xs text-base-content/60 truncate">
{gettext("Slug")}: <code class="font-mono">{insight.slug}</code>
</p>
<p class="hidden sm:block 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 text-xs px-3 py-2 sm:badge-lg sm:px-4 sm: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-1.5 sm:gap-3">
<div class="rounded-lg bg-base-200/60 px-1.5 py-1 sm:px-3 sm:py-2 text-center">
<p class="text-[10px] sm:text-xs uppercase tracking-wide text-base-content/60">
{gettext("Posts")}
</p>
<p class="text-sm sm:text-base font-semibold text-base-content">
{insight.posts_count}
</p>
</div>
<div class="rounded-lg bg-success/10 px-1.5 py-1 sm:px-3 sm:py-2 text-center">
<p class="text-[10px] sm:text-xs uppercase tracking-wide text-success">
{gettext("Published")}
</p>
<p class="text-sm sm:text-base font-semibold text-success">
{insight.published_count}
</p>
</div>
<div class="rounded-lg bg-warning/10 px-1.5 py-1 sm:px-3 sm:py-2 text-center">
<p class="text-[10px] sm:text-xs uppercase tracking-wide text-warning">
{gettext("Drafts")}
</p>
<p class="text-sm sm:text-base font-semibold text-warning">
{insight.draft_count}
</p>
</div>
</div>
<div class="hidden sm:block 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>
<%!-- 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 flex-wrap 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 min-w-0">
{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">
<.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>
<%!-- 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>