Packages

phoenix_kit

1.7.19
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 blogging web settings.html.heex
Raw

lib/modules/blogging/web/settings.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 flex-col mx-auto px-4 py-6">
<%!-- Header Section --%>
<header class="w-full relative mb-6">
<%!-- Back Button (Left aligned) --%>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin", locale: @current_locale)}
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>
<%!-- Title Section --%>
<div class="text-center">
<h1 class="text-4xl font-bold text-base-content mb-3">{gettext("Blogging Settings")}</h1>
<p class="text-lg text-base-content">
{gettext(
"Create dedicated blogs your team can publish to. Each blog appears as its own tab in the blogging workspace."
)}
</p>
</div>
</header>
<div class="max-w-2xl mx-auto space-y-6">
<%!-- Language Configuration Information --%>
<div class="border-l-4 border-primary bg-base-200 p-4 rounded-r-lg">
<div class="flex gap-3">
<.icon name="hero-information-circle" class="w-5 h-5 shrink-0 text-primary" />
<div class="text-sm">
<p class="font-semibold mb-1">{gettext("Multi-Language Configuration")}</p>
<p>
{gettext("The default language for blog posts is set by the")}
<.link
navigate={
PhoenixKit.Utils.Routes.path("/admin/settings", locale: @current_locale)
}
class="link link-primary font-medium underline"
>
{gettext("Content Language")}
</.link>
{gettext("in General Settings.")}
<%= if @languages_enabled do %>
{gettext("Available language variants are configured in the")}
<.link
navigate={
PhoenixKit.Utils.Routes.path("/admin/settings/languages",
locale: @current_locale
)
}
class="link link-primary font-medium underline"
>
{gettext("Languages Module")}
</.link>.
<% else %>
{gettext("To enable multi-language support, activate the")}
<.link
navigate={
PhoenixKit.Utils.Routes.path("/admin/modules", locale: @current_locale)
}
class="link link-primary font-medium underline"
>
{gettext("Languages Module")}
</.link>.
<% end %>
</p>
</div>
</div>
</div>
<%!-- Blogging Section --%>
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body space-y-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h2 class="text-2xl font-semibold text-base-content">{gettext("Blogging")}</h2>
<p class="text-sm text-base-content/70">
{gettext(
"Create dedicated blogs like Product Updates, Release Notes, or Company News. Each blog stores its content in structured folders on disk."
)}
</p>
</div>
<.link
href={Routes.path("/admin/settings/blogging/new", locale: @current_locale)}
class="btn btn-primary btn-sm whitespace-nowrap"
>
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> {gettext("Create Blog")}
</.link>
</div>
<%= if @blogs == [] do %>
<div class="alert alert-info">
<.icon name="hero-information-circle" class="w-5 h-5" />
<div>
<h3 class="font-bold">{gettext("No blogs yet")}</h3>
<p class="text-sm">
{gettext("Create your first blog to start publishing content.")}
</p>
</div>
</div>
<% else %>
<div class="space-y-4">
<%= for blog <- @blogs do %>
<div class="border border-base-200 rounded-lg p-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div class="space-y-1">
<p class="flex items-center gap-2 text-base font-semibold text-base-content">
<.link
navigate={
Routes.path("/admin/blogging/#{blog["slug"]}", locale: @current_locale)
}
class="hover:text-primary transition-colors"
>
{blog["name"]}
</.link>
<span class="badge badge-ghost badge-lg text-xs px-4 py-3 whitespace-nowrap">
<%= if blog["mode"] == "slug" do %>
{gettext("Slug-based")}
<% else %>
{gettext("Timestamp-based")}
<% end %>
</span>
</p>
<p class="text-xs text-base-content/60">
{gettext("Slug")}: <code class="font-mono break-all">{blog["slug"]}</code>
</p>
<% blog_mode = blog["mode"] || "timestamp" %>
<% blog_path =
if blog_mode == "slug" do
"priv/blogging/" <> blog["slug"] <> "/<slug>/<locale>.phk"
else
"priv/blogging/" <> blog["slug"] <> "/<YYYY-MM-DD>/<HH:mm>/<locale>.phk"
end %>
<p class="text-xs text-base-content/50">
{gettext("Files")}: <code class="font-mono break-all">{blog_path}</code>
</p>
</div>
<div class="flex items-center gap-2">
<.link
aria-label={gettext("Open Admin")}
navigate={
Routes.path("/admin/blogging/#{blog["slug"]}", locale: @current_locale)
}
class="btn btn-outline btn-sm btn-square"
>
<.icon name="hero-arrow-right" class="w-4 h-4" />
</.link>
<.link
aria-label={gettext("Edit")}
navigate={
Routes.path(
"/admin/settings/blogging/#{blog["slug"]}/edit",
locale: @current_locale
)
}
class="btn btn-outline btn-sm btn-square"
>
<.icon name="hero-pencil-square" class="w-4 h-4" />
</.link>
<button
type="button"
aria-label={gettext("Delete")}
phx-click="remove_blog"
phx-value-slug={blog["slug"]}
class="btn btn-sm btn-square btn-outline text-error hover:bg-error hover:text-error-content"
data-confirm={
gettext(
"Move this blog to trash? The directory will be renamed with a timestamp."
)
}
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<%!-- Cache Management Section --%>
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body space-y-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h2 class="text-2xl font-semibold text-base-content">
<.icon name="hero-bolt" class="w-6 h-6 inline-block mr-2" />
{gettext("Listing Cache")}
</h2>
<p class="text-sm text-base-content/70">
{gettext(
"Cached listing data speeds up blog pages. Cache is automatically updated when posts change."
)}
</p>
</div>
<%= if @blogs != [] and (@file_cache_enabled or @memory_cache_enabled) do %>
<button
type="button"
phx-click="regenerate_all_caches"
class="btn btn-primary btn-sm whitespace-nowrap"
>
<.icon name="hero-arrow-path" class="w-4 h-4 mr-1" />
{gettext("Regenerate All")}
</button>
<% end %>
</div>
<%!-- Cache Settings Toggles --%>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<%!-- File Cache Toggle --%>
<div class="flex items-center justify-between p-4 bg-base-200 rounded-lg">
<div class="flex items-center gap-3">
<.icon name="hero-document" class="w-5 h-5 text-base-content/70" />
<div>
<p class="font-medium">{gettext("File Cache")}</p>
<p class="text-xs text-base-content/60">
{gettext("Persist cache to JSON files for fast startup")}
</p>
</div>
</div>
<input
type="checkbox"
class="toggle toggle-primary"
checked={@file_cache_enabled}
phx-click="toggle_file_cache"
/>
</div>
<%!-- Memory Cache Toggle --%>
<div class="flex items-center justify-between p-4 bg-base-200 rounded-lg">
<div class="flex items-center gap-3">
<.icon name="hero-cpu-chip" class="w-5 h-5 text-base-content/70" />
<div>
<p class="font-medium">{gettext("Memory Cache")}</p>
<p class="text-xs text-base-content/60">
{gettext("Store in :persistent_term for sub-microsecond reads")}
</p>
</div>
</div>
<input
type="checkbox"
class="toggle toggle-primary"
checked={@memory_cache_enabled}
phx-click="toggle_memory_cache"
/>
</div>
</div>
<%= if not @file_cache_enabled and not @memory_cache_enabled do %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<span>
{gettext(
"Both caches are disabled. Blog listing pages will scan the filesystem on every request."
)}
</span>
</div>
<% end %>
<%= if @blogs == [] do %>
<div class="alert">
<.icon name="hero-information-circle" class="w-5 h-5" />
<span>{gettext("Create a blog to manage its cache.")}</span>
</div>
<% else %>
<%= if @file_cache_enabled or @memory_cache_enabled do %>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>{gettext("Blog")}</th>
<th class="text-center">{gettext("Posts")}</th>
<%= if @file_cache_enabled do %>
<th class="text-center">
<span class="flex items-center justify-center gap-1">
<.icon name="hero-document" class="w-4 h-4" />
{gettext("File")}
</span>
</th>
<% end %>
<%= if @memory_cache_enabled do %>
<th class="text-center">
<span class="flex items-center justify-center gap-1">
<.icon name="hero-cpu-chip" class="w-4 h-4" />
{gettext("Memory")}
</span>
</th>
<% end %>
<th class="text-right">{gettext("Actions")}</th>
</tr>
</thead>
<tbody>
<%= for blog <- @blogs do %>
<% cache = @cache_status[blog["slug"]] %>
<tr>
<td class="font-medium">{blog["name"]}</td>
<td class="text-center">
<%= if cache.post_count do %>
<span class="font-mono text-sm">{cache.post_count}</span>
<% else %>
<span class="text-base-content/50">—</span>
<% end %>
</td>
<%= if @file_cache_enabled do %>
<td class="text-center">
<%= if cache.exists do %>
<.icon name="hero-check-circle" class="w-5 h-5 text-success" />
<% else %>
<.icon name="hero-x-circle" class="w-5 h-5 text-base-content/30" />
<% end %>
</td>
<% end %>
<%= if @memory_cache_enabled do %>
<td class="text-center">
<%= if cache.in_memory do %>
<.icon name="hero-check-circle" class="w-5 h-5 text-success" />
<% else %>
<.icon name="hero-x-circle" class="w-5 h-5 text-base-content/30" />
<% end %>
</td>
<% end %>
<td class="text-right">
<div class="flex justify-end gap-2">
<%= if cache.exists or cache.in_memory do %>
<button
type="button"
phx-click="invalidate_cache"
phx-value-slug={blog["slug"]}
class="btn btn-ghost btn-xs text-error"
title={gettext("Clear cache")}
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
<% end %>
<button
type="button"
phx-click="regenerate_cache"
phx-value-slug={blog["slug"]}
class="btn btn-ghost btn-xs"
title={gettext("Regenerate cache")}
>
<.icon name="hero-arrow-path" class="w-4 h-4" />
</button>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div class="text-xs text-base-content/50 space-y-1">
<p>
<.icon name="hero-information-circle" class="w-3 h-3 inline" />
{gettext(
"\"In Memory\" means the cache is loaded into :persistent_term for sub-microsecond reads."
)}
</p>
<p>
<.icon name="hero-arrow-path" class="w-3 h-3 inline" />
{gettext("Regenerate scans all posts and rebuilds the cache file.")}
</p>
</div>
<% end %>
<% end %>
</div>
</div>
<%!-- Render Cache Section --%>
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body space-y-4">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h2 class="text-2xl font-semibold text-base-content">
<.icon name="hero-document-text" class="w-6 h-6 inline-block mr-2" />
{gettext("Render Cache")}
</h2>
<p class="text-sm text-base-content/70">
{gettext(
"Cached rendered HTML for published posts. Uses content-hash keys so edits auto-invalidate."
)}
</p>
</div>
<%= if @render_cache_enabled do %>
<button
type="button"
phx-click="clear_render_cache"
class="btn btn-outline btn-error btn-sm whitespace-nowrap"
data-confirm={
gettext(
"Clear all cached rendered posts? They will be re-rendered on next view."
)
}
>
<.icon name="hero-trash" class="w-4 h-4 mr-1" />
{gettext("Clear All")}
</button>
<% end %>
</div>
<%!-- Global Render Cache Toggle --%>
<div class="flex items-center justify-between p-4 bg-base-200 rounded-lg">
<div class="flex items-center gap-3">
<.icon name="hero-bolt" class="w-5 h-5 text-base-content/70" />
<div>
<p class="font-medium">{gettext("Render Cache")}</p>
<p class="text-xs text-base-content/60">
{gettext("Cache rendered HTML for published posts (6-hour TTL)")}
</p>
</div>
</div>
<input
type="checkbox"
class="toggle toggle-primary"
checked={@render_cache_enabled}
phx-click="toggle_render_cache"
/>
</div>
<%= if not @render_cache_enabled do %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<span>
{gettext("Render cache is disabled. Posts will be rendered on every request.")}
</span>
</div>
<% else %>
<%!-- Stats Display --%>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="stat bg-base-200 rounded-lg p-4">
<div class="stat-title text-xs">{gettext("Cache Hits")}</div>
<div class="stat-value text-lg font-mono">{@render_cache_stats[:hits] || 0}</div>
</div>
<div class="stat bg-base-200 rounded-lg p-4">
<div class="stat-title text-xs">{gettext("Cache Misses")}</div>
<div class="stat-value text-lg font-mono">
{@render_cache_stats[:misses] || 0}
</div>
</div>
<div class="stat bg-base-200 rounded-lg p-4">
<div class="stat-title text-xs">{gettext("Entries Cached")}</div>
<div class="stat-value text-lg font-mono">{@render_cache_stats[:puts] || 0}</div>
</div>
<div class="stat bg-base-200 rounded-lg p-4">
<div class="stat-title text-xs">{gettext("Hit Rate")}</div>
<div class="stat-value text-lg font-mono">
<%= if @render_cache_stats[:hit_rate] do %>
{Float.round(@render_cache_stats[:hit_rate] * 100, 1)}%
<% else %>
0%
<% end %>
</div>
</div>
</div>
<%!-- Per-Blog Render Cache Table --%>
<%= if @blogs != [] do %>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>{gettext("Blog")}</th>
<th class="text-center">{gettext("Enabled")}</th>
<th class="text-right">{gettext("Actions")}</th>
</tr>
</thead>
<tbody>
<%= for blog <- @blogs do %>
<% slug = blog["slug"] %>
<% enabled = @render_cache_per_blog[slug] %>
<tr>
<td class="font-medium">{blog["name"]}</td>
<td class="text-center">
<input
type="checkbox"
class="toggle toggle-primary toggle-sm"
checked={enabled}
phx-click="toggle_blog_render_cache"
phx-value-slug={slug}
/>
</td>
<td class="text-right">
<%= if enabled do %>
<button
type="button"
phx-click="clear_blog_render_cache"
phx-value-slug={slug}
class="btn btn-ghost btn-xs text-error"
title={gettext("Clear cache for this blog")}
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
<% end %>
<div class="text-xs text-base-content/50">
<p>
<.icon name="hero-information-circle" class="w-3 h-3 inline" />
{gettext(
"Render cache stores pre-rendered HTML for published posts. Cache keys include content hashes, so edits automatically use fresh renders. TTL: 6 hours."
)}
</p>
</div>
</div>
</div>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>