Packages

phoenix_kit

1.7.58
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 posts web posts.ex
Raw

lib/modules/posts/web/posts.ex

defmodule PhoenixKitWeb.Live.Modules.Posts.Posts do
@moduledoc """
LiveView for displaying and managing posts in PhoenixKit admin panel.
Provides comprehensive post management interface with filtering, searching,
and quick actions for social posts system.
## Features
- **Real-time Post List**: Live updates of posts
- **Advanced Filtering**: By type, status, group, tag, date range
- **Search Functionality**: Search across titles and content
- **Pagination**: Handle large volumes of posts
- **Bulk Actions**: Publish, delete multiple posts
- **Quick Actions**: Edit, delete, change status
- **Statistics Summary**: Key metrics (total, drafts, scheduled, public)
## Route
This LiveView is mounted at `{prefix}/admin/posts` and requires
appropriate admin permissions.
Note: `{prefix}` is your configured PhoenixKit URL prefix (default: `/phoenix_kit`).
## Usage
# In your Phoenix router
live "/admin/posts", PhoenixKitWeb.Live.Modules.Posts.Posts, :index
## Permissions
Access is restricted to users with admin or owner roles in PhoenixKit.
"""
use PhoenixKitWeb, :live_view
require Logger
alias PhoenixKit.Modules.Posts
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Routes
@max_per_page 100
## --- Lifecycle Callbacks ---
@impl true
def mount(_params, _session, socket) do
# Check if posts module is enabled
if posts_enabled?() do
# Get project title from settings cache
project_title = Settings.get_project_title()
# Get current user
current_user = socket.assigns[:phoenix_kit_current_user]
socket =
socket
|> assign(:page_title, "Posts")
|> assign(:project_title, project_title)
|> assign(:current_user, current_user)
|> assign(:posts, [])
|> assign(:total_count, 0)
|> assign(:stats, %{total: 0, drafts: 0, public: 0, scheduled: 0})
|> assign(:loading, true)
|> assign(:selected_posts, [])
|> assign_filter_defaults()
|> assign_pagination_defaults()
{:ok, socket}
else
{:ok,
socket
|> put_flash(:error, "Posts module is not enabled")
|> push_navigate(to: Routes.path("/admin"))}
end
end
@impl true
def handle_params(params, _url, socket) do
socket =
socket
|> apply_params(params)
|> load_posts()
|> load_stats()
{:noreply, socket}
end
## --- Event Handlers ---
@impl true
def handle_event("filter", params, socket) do
# Handle search and filter parameters
combined_params = %{}
# Extract search parameters
combined_params =
case Map.get(params, "search") do
%{"query" => query} -> Map.put(combined_params, "search", String.trim(query || ""))
_ -> combined_params
end
# Extract filter parameters
combined_params =
case Map.get(params, "filter") do
filter_params when is_map(filter_params) -> Map.merge(combined_params, filter_params)
_ -> combined_params
end
# Reset to first page when filtering
combined_params = Map.put(combined_params, "page", "1")
# Build new URL parameters
new_params = build_url_params(socket.assigns, combined_params)
{:noreply, socket |> push_patch(to: Routes.path("/admin/posts?#{new_params}"))}
end
@impl true
def handle_event("clear_filters", _params, socket) do
{:noreply, socket |> push_patch(to: Routes.path("/admin/posts"))}
end
@impl true
def handle_event("view_post", %{"id" => post_uuid}, socket) do
{:noreply, socket |> push_navigate(to: Routes.path("/admin/posts/#{post_uuid}"))}
end
@impl true
def handle_event("edit_post", %{"id" => post_uuid}, socket) do
{:noreply, socket |> push_navigate(to: Routes.path("/admin/posts/#{post_uuid}/edit"))}
end
@impl true
def handle_event("delete_post", %{"id" => post_uuid}, socket) do
case Posts.get_post!(post_uuid) do
nil ->
{:noreply, socket |> put_flash(:error, "Post not found")}
post ->
case Posts.delete_post(post) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Post deleted successfully")
|> load_posts()
|> load_stats()}
{:error, _changeset} ->
{:noreply, socket |> put_flash(:error, "Failed to delete post")}
end
end
end
@impl true
def handle_event("publish_post", %{"id" => post_uuid}, socket) do
case Posts.get_post!(post_uuid) do
nil ->
{:noreply, socket |> put_flash(:error, "Post not found")}
post ->
case Posts.publish_post(post) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Post published successfully")
|> load_posts()
|> load_stats()}
{:error, _changeset} ->
{:noreply, socket |> put_flash(:error, "Failed to publish post")}
end
end
end
@impl true
def handle_event("draft_post", %{"id" => post_uuid}, socket) do
case Posts.get_post!(post_uuid) do
nil ->
{:noreply, socket |> put_flash(:error, "Post not found")}
post ->
case Posts.draft_post(post) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Post moved to drafts")
|> load_posts()
|> load_stats()}
{:error, _changeset} ->
{:noreply, socket |> put_flash(:error, "Failed to update post")}
end
end
end
@impl true
def handle_event("select_post", %{"id" => post_uuid, "value" => value}, socket) do
selected_posts =
if value == "on" do
[post_uuid | socket.assigns.selected_posts] |> Enum.uniq()
else
Enum.reject(socket.assigns.selected_posts, &(&1 == post_uuid))
end
{:noreply, assign(socket, :selected_posts, selected_posts)}
end
@impl true
def handle_event("select_all", %{"value" => value}, socket) do
selected_posts =
if value == "on" do
Enum.map(socket.assigns.posts, & &1.uuid)
else
[]
end
{:noreply, assign(socket, :selected_posts, selected_posts)}
end
@impl true
def handle_event("bulk_publish", _params, socket) do
count =
Enum.reduce(socket.assigns.selected_posts, 0, fn post_uuid, acc ->
case Posts.get_post!(post_uuid) do
nil ->
acc
post ->
case Posts.publish_post(post) do
{:ok, _} -> acc + 1
_ -> acc
end
end
end)
{:noreply,
socket
|> put_flash(:info, "Published #{count} post(s)")
|> assign(:selected_posts, [])
|> load_posts()
|> load_stats()}
end
@impl true
def handle_event("bulk_delete", _params, socket) do
count =
Enum.reduce(socket.assigns.selected_posts, 0, fn post_uuid, acc ->
case Posts.get_post!(post_uuid) do
nil ->
acc
post ->
case Posts.delete_post(post) do
{:ok, _} -> acc + 1
_ -> acc
end
end
end)
{:noreply,
socket
|> put_flash(:info, "Deleted #{count} post(s)")
|> assign(:selected_posts, [])
|> load_posts()
|> load_stats()}
end
@impl true
def handle_event("refresh", _params, socket) do
{:noreply,
socket
|> assign(:loading, true)
|> load_posts()
|> load_stats()
|> put_flash(:info, "Posts refreshed")}
end
## --- Private Helper Functions ---
defp posts_enabled? do
Settings.get_setting_cached("posts_enabled", "true") == "true"
end
defp assign_filter_defaults(socket) do
socket
|> assign(:filter_type, "all")
|> assign(:filter_status, "all")
|> assign(:filter_group, "all")
|> assign(:filter_tag, "all")
|> assign(:search_query, "")
end
defp assign_pagination_defaults(socket) do
per_page =
Settings.get_setting_cached("posts_per_page", "20")
|> String.to_integer()
|> min(@max_per_page)
socket
|> assign(:page, 1)
|> assign(:per_page, per_page)
end
defp apply_params(socket, params) do
socket
|> assign(:filter_type, Map.get(params, "type", "all"))
|> assign(:filter_status, Map.get(params, "status", "all"))
|> assign(:filter_group, Map.get(params, "group", "all"))
|> assign(:filter_tag, Map.get(params, "tag", "all"))
|> assign(:search_query, Map.get(params, "search", ""))
|> assign(:page, String.to_integer(Map.get(params, "page", "1")))
end
defp load_posts(socket) do
opts = [
page: socket.assigns.page,
per_page: socket.assigns.per_page,
preload: [:user, :media, :tags, :groups]
]
# Add filters
opts =
opts
|> maybe_add_filter(:type, socket.assigns.filter_type)
|> maybe_add_filter(:status, socket.assigns.filter_status)
|> maybe_add_search(socket.assigns.search_query)
posts = Posts.list_posts(opts)
total_count = length(posts)
socket
|> assign(:posts, posts)
|> assign(:total_count, total_count)
|> assign(:loading, false)
end
defp load_stats(socket) do
# Load post statistics
stats = %{
total: count_posts([]),
drafts: count_posts(status: "draft"),
public: count_posts(status: "public"),
scheduled: count_posts(status: "scheduled"),
unlisted: count_posts(status: "unlisted")
}
assign(socket, :stats, stats)
end
defp count_posts(opts) do
Posts.list_posts(opts) |> length()
end
defp maybe_add_filter(opts, _key, "all"), do: opts
defp maybe_add_filter(opts, key, value) when value != "" and value != nil do
Keyword.put(opts, key, value)
end
defp maybe_add_filter(opts, _key, _value), do: opts
defp maybe_add_search(opts, "") do
opts
end
defp maybe_add_search(opts, query) when is_binary(query) and query != "" do
Keyword.put(opts, :search, query)
end
defp maybe_add_search(opts, _), do: opts
defp build_url_params(assigns, new_params) do
params = %{
"type" => new_params["type"] || assigns.filter_type,
"status" => new_params["status"] || assigns.filter_status,
"group" => new_params["group"] || assigns.filter_group,
"tag" => new_params["tag"] || assigns.filter_tag,
"search" => new_params["search"] || assigns.search_query,
"page" => new_params["page"] || to_string(assigns.page)
}
# Remove default values to keep URL clean
params
|> Enum.reject(fn {_k, v} -> v == "all" or v == "" end)
|> URI.encode_query()
end
defp format_post_type("post"), do: "Post"
defp format_post_type("snippet"), do: "Snippet"
defp format_post_type("repost"), do: "Repost"
defp format_post_type(_), do: "Unknown"
defp format_status("draft"), do: "Draft"
defp format_status("public"), do: "Public"
defp format_status("unlisted"), do: "Unlisted"
defp format_status("scheduled"), do: "Scheduled"
defp format_status(_), do: "Unknown"
defp status_badge_class("draft"), do: "badge-neutral"
defp status_badge_class("public"), do: "badge-success"
defp status_badge_class("unlisted"), do: "badge-warning"
defp status_badge_class("scheduled"), do: "badge-info"
defp status_badge_class(_), do: "badge-ghost"
defp type_badge_class("post"), do: "badge-primary"
defp type_badge_class("snippet"), do: "badge-secondary"
defp type_badge_class("repost"), do: "badge-accent"
defp type_badge_class(_), do: "badge-ghost"
end