Packages

phoenix_kit

1.7.78
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/publishing/new-group")}
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-2 sm:space-y-3">
<%= 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/publishing/new-group")}
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 %>
<%!-- Active / Trashed Tabs — only show if there are trashed groups --%>
<%= if @trashed_count > 0 or @view_mode == "trashed" do %>
<div class="flex items-center gap-0.5 border-b border-base-200">
<button
type="button"
phx-click="switch_view"
phx-value-mode="active"
class={"px-3 py-1 text-xs font-medium border-b-2 transition-colors cursor-pointer #{if @view_mode == "active", do: "border-primary text-primary", else: "border-transparent text-base-content/50 hover:text-base-content"}"}
>
{gettext("Active")}
</button>
<button
type="button"
phx-click="switch_view"
phx-value-mode="trashed"
class={"px-3 py-1 text-xs font-medium border-b-2 transition-colors cursor-pointer #{if @view_mode == "trashed", do: "border-error text-error", else: "border-transparent text-base-content/50 hover:text-base-content"}"}
>
{gettext("Trash")}
</button>
</div>
<% end %>
<%= if @loading do %>
<div class="grid gap-3 sm:gap-4 md:grid-cols-2 xl:grid-cols-3 animate-pulse">
<%= for _i <- 1..3 do %>
<div class="card bg-base-100 shadow-sm border border-base-200 h-full">
<div class="card-body p-3 sm:p-6 space-y-3 sm:space-y-4">
<div class="flex items-center justify-between">
<div class="bg-base-200 h-6 w-40 rounded"></div>
<div class="bg-base-200 h-5 w-20 rounded-full"></div>
</div>
<div class="bg-base-200 h-3 w-32 rounded"></div>
<div class="grid grid-cols-2 gap-3">
<div class="bg-base-200 h-14 rounded-lg"></div>
<div class="bg-base-200 h-14 rounded-lg"></div>
</div>
<div class="flex gap-2">
<div class="bg-base-200 h-8 w-20 rounded-lg"></div>
<div class="bg-base-200 h-8 w-20 rounded-lg"></div>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<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 transition h-full #{if @view_mode == "trashed", do: "border-base-200 opacity-60", else: "border-base-200 hover:border-primary/60"}"}>
<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">
<%= if @view_mode == "active" do %>
<.link
navigate={Routes.path("/admin/publishing/#{insight.slug}")}
class="hover:text-primary transition-colors"
>
{insight.name}
</.link>
<% else %>
{insight.name}
<% end %>
</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 badge-sm text-[10px] px-2 py-1 whitespace-nowrap">
<%= if insight.mode == "slug" do %>
{gettext("Slug-based")}
<% else %>
{gettext("Timestamp-based")}
<% end %>
</span>
</div>
<div class="grid grid-cols-2 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>
<div class="hidden sm:block">
<details class="group">
<summary class="text-xs font-medium uppercase text-base-content/60 cursor-pointer select-none list-none flex items-center gap-1 hover:text-base-content transition-colors">
<.icon
name="hero-chevron-right"
class="w-3 h-3 transition-transform group-open:rotate-90"
/>
{gettext("Languages used")}:
<span class="font-semibold text-base-content/80">
{length(insight.languages)}
</span>
</summary>
<%= if insight.languages != [] do %>
<div class="flex flex-wrap gap-1.5 mt-1.5 pl-4">
<%= 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/50 mt-1 pl-4">{gettext("None")}</p>
<% end %>
</details>
</div>
<%!-- Primary Language Update Warning --%>
<%= 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="update_primary_language"
phx-value-slug={insight.slug}
data-confirm={
gettext(
"Update %{count} posts to use the current primary language?",
count: insight.needs_migration_count
)
}
>
{gettext("Update")}
</button>
</div>
<% end %>
<div class="flex flex-wrap gap-1.5 sm:gap-2 mt-auto">
<%= if @view_mode == "active" do %>
<.link
navigate={Routes.path("/admin/publishing/#{insight.slug}")}
class="btn btn-outline btn-sm btn-xs sm:btn-sm flex-1 sm:flex-none min-w-0"
>
<.icon name="hero-arrow-right" class="w-4 h-4 sm:mr-1" />
<span class="hidden sm:inline">{gettext("Open")}</span>
</.link>
<.link
navigate={Routes.path("/admin/publishing/edit-group/#{insight.slug}")}
class="btn btn-outline btn-sm btn-xs sm:btn-sm flex-1 sm:flex-none min-w-0"
>
<.icon name="hero-cog-6-tooth" class="w-4 h-4 sm:mr-1" />
<span class="hidden sm:inline">{gettext("Settings")}</span>
</.link>
<%= if insight.published_count > 0 do %>
<% group_slug = insight.slug %>
<% url_prefix =
PhoenixKit.Config.get_url_prefix()
|> case do
"/" -> ""
prefix -> prefix
end %>
<% 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 btn-xs sm:btn-sm flex-1 sm:flex-none min-w-0"
aria-label={gettext("View public site")}
>
<.icon name="hero-eye" class="w-4 h-4 sm:mr-1" />
<span class="hidden sm:inline">{gettext("Public")}</span>
</a>
<% end %>
<button
type="button"
phx-click="trash_group"
phx-value-slug={insight.slug}
class="btn btn-outline btn-sm btn-xs sm:btn-sm min-w-0 text-error hover:bg-error hover:text-error-content"
data-confirm={gettext("Move this group to trash?")}
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
<% else %>
<button
type="button"
phx-click="restore_group"
phx-value-slug={insight.slug}
class="btn btn-outline btn-sm btn-xs sm:btn-sm flex-1 sm:flex-none min-w-0 text-success"
>
<.icon name="hero-arrow-uturn-left" class="w-4 h-4 sm:mr-1" />
<span class="hidden sm:inline">{gettext("Restore")}</span>
</button>
<button
type="button"
phx-click="delete_group"
phx-value-slug={insight.slug}
class="btn btn-outline btn-sm btn-xs sm:btn-sm flex-1 sm:flex-none min-w-0 text-error hover:bg-error hover:text-error-content"
data-confirm={
gettext(
"Permanently delete this group and all its posts? This cannot be undone."
)
}
>
<.icon name="hero-trash" class="w-4 h-4 sm:mr-1" />
<span class="hidden sm:inline">{gettext("Delete Forever")}</span>
</button>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
<%= if not @loading and @dashboard_insights == [] and @view_mode == "trashed" do %>
<div class="text-center py-8 text-base-content/60">
<.icon name="hero-trash" class="w-8 h-8 mx-auto mb-2 opacity-40" />
<p class="text-sm">{gettext("Trash is empty")}</p>
</div>
<% end %>
<% end %>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>