Packages
phoenix_kit
1.7.27
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
Current section
Files
lib/modules/posts/web/details.ex
defmodule PhoenixKitWeb.Live.Modules.Posts.Details do
@moduledoc """
LiveView for displaying a single post with all details, comments, and interactions.
Displays:
- Post content (title, subtitle, content, media)
- Author information
- Post statistics (views, likes, comments)
- Tags and groups
- Comments with unlimited threading
- Like/unlike functionality
- Admin actions (edit, delete, status changes)
## Route
This LiveView is mounted at `{prefix}/admin/posts/:id` and requires
appropriate permissions.
Note: `{prefix}` is your configured PhoenixKit URL prefix (default: `/phoenix_kit`).
"""
use PhoenixKitWeb, :live_view
require Logger
alias PhoenixKit.Modules.Posts
alias PhoenixKit.Modules.Publishing.Renderer
alias PhoenixKit.Settings
alias PhoenixKit.Users.Roles
alias PhoenixKit.Utils.Routes
@impl true
def mount(%{"id" => post_id}, _session, socket) do
# Get current user
current_user = socket.assigns[:phoenix_kit_current_user]
# Get project title
project_title = Settings.get_project_title()
# Load post with all associations
case Posts.get_post!(post_id, preload: [:user, [media: :file], :tags, :groups, :mentions]) do
nil ->
{:ok,
socket
|> put_flash(:error, "Post not found")
|> push_navigate(to: Routes.path("/admin/posts"))}
post ->
# Increment view count
Posts.increment_view_count(post)
# Check if current user liked this post
liked_by_user = Posts.post_liked_by?(post.id, current_user.id)
# Load settings
comments_enabled = Settings.get_setting("posts_comments_enabled", "true") == "true"
likes_enabled = Settings.get_setting("posts_likes_enabled", "true") == "true"
show_view_count = Settings.get_setting("posts_show_view_count", "true") == "true"
socket =
socket
|> assign(:page_title, post.title)
|> assign(:project_title, project_title)
|> assign(:post, post)
|> assign(:rendered_content, render_markdown_content(post.content))
|> assign(:current_user, current_user)
|> assign(:liked_by_user, liked_by_user)
|> assign(:comments_enabled, comments_enabled)
|> assign(:likes_enabled, likes_enabled)
|> assign(:show_view_count, show_view_count)
|> assign(:new_comment, "")
|> assign(:reply_to, nil)
|> load_comments()
{:ok, socket}
end
end
@impl true
def handle_event("like_post", _params, socket) do
post = socket.assigns.post
current_user = socket.assigns.current_user
if socket.assigns.liked_by_user do
# Unlike
Posts.unlike_post(post.id, current_user.id)
updated_post =
Posts.get_post!(post.id, preload: [:user, [media: :file], :tags, :groups, :mentions])
{:noreply,
socket
|> assign(:post, updated_post)
|> assign(:liked_by_user, false)}
else
# Like
Posts.like_post(post.id, current_user.id)
updated_post =
Posts.get_post!(post.id, preload: [:user, [media: :file], :tags, :groups, :mentions])
{:noreply,
socket
|> assign(:post, updated_post)
|> assign(:liked_by_user, true)}
end
end
@impl true
def handle_event("add_comment", %{"comment" => comment_text}, socket) do
if comment_text != "" do
parent_id = socket.assigns.reply_to
attrs = %{
content: comment_text,
parent_id: parent_id
}
case Posts.create_comment(socket.assigns.post.id, socket.assigns.current_user.id, attrs) do
{:ok, _comment} ->
updated_post =
Posts.get_post!(socket.assigns.post.id,
preload: [:user, :media, :tags, :groups, :mentions]
)
{:noreply,
socket
|> assign(:post, updated_post)
|> assign(:new_comment, "")
|> assign(:reply_to, nil)
|> load_comments()
|> put_flash(:info, "Comment added")}
{:error, _changeset} ->
{:noreply, socket |> put_flash(:error, "Failed to add comment")}
end
else
{:noreply, socket}
end
end
@impl true
def handle_event("reply_to", %{"id" => comment_id}, socket) do
{:noreply, assign(socket, :reply_to, comment_id)}
end
@impl true
def handle_event("cancel_reply", _params, socket) do
{:noreply, assign(socket, :reply_to, nil)}
end
@impl true
def handle_event("delete_comment", %{"id" => comment_id}, socket) do
case Posts.get_comment(comment_id) do
nil ->
{:noreply, socket |> put_flash(:error, "Comment not found")}
comment ->
# Check if user can delete (owns comment or is admin)
if can_delete_comment?(socket.assigns.current_user, comment) do
case Posts.delete_comment(comment) do
{:ok, _} ->
updated_post =
Posts.get_post!(socket.assigns.post.id,
preload: [:user, :media, :tags, :groups, :mentions]
)
{:noreply,
socket
|> assign(:post, updated_post)
|> load_comments()
|> put_flash(:info, "Comment deleted")}
{:error, _} ->
{:noreply, socket |> put_flash(:error, "Failed to delete comment")}
end
else
{:noreply,
socket |> put_flash(:error, "You don't have permission to delete this comment")}
end
end
end
@impl true
def handle_event("edit_post", _params, socket) do
{:noreply,
push_navigate(socket, to: Routes.path("/admin/posts/#{socket.assigns.post.id}/edit"))}
end
@impl true
def handle_event("delete_post", _params, socket) do
case Posts.delete_post(socket.assigns.post) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Post deleted successfully")
|> push_navigate(to: Routes.path("/admin/posts"))}
{:error, _} ->
{:noreply, socket |> put_flash(:error, "Failed to delete post")}
end
end
@impl true
def handle_event("change_status", %{"status" => status}, socket) do
case Posts.update_post(socket.assigns.post, %{status: status}) do
{:ok, updated_post} ->
{:noreply,
socket
|> assign(:post, updated_post)
|> put_flash(:info, "Post status updated to #{status}")}
{:error, _} ->
{:noreply, socket |> put_flash(:error, "Failed to update post status")}
end
end
## --- Private Helper Functions ---
defp load_comments(socket) do
comments = Posts.get_comment_tree(socket.assigns.post.id)
assign(socket, :comments, comments)
end
defp can_delete_comment?(user, comment) do
user.id == comment.user_id or user_is_admin?(user)
end
defp user_is_admin?(user) do
Roles.user_has_role_owner?(user) or Roles.user_has_role_admin?(user)
end
defp can_edit_post?(user, post) do
user.id == post.user_id or user_is_admin?(user)
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 format_type_badge_class("post"), do: "badge badge-primary"
defp format_type_badge_class("snippet"), do: "badge badge-secondary"
defp format_type_badge_class("repost"), do: "badge badge-accent"
defp format_type_badge_class(_), do: "badge badge-ghost"
defp format_status_badge_class("draft"), do: "badge badge-neutral"
defp format_status_badge_class("public"), do: "badge badge-success"
defp format_status_badge_class("unlisted"), do: "badge badge-warning"
defp format_status_badge_class("scheduled"), do: "badge badge-info"
defp format_status_badge_class(_), do: "badge badge-ghost"
defp render_markdown_content(content) when is_binary(content) and content != "" do
Renderer.render_markdown(content)
end
defp render_markdown_content(_), do: ""
defp render_comment(comment, current_user, assigns) do
assigns = Map.merge(assigns, %{comment: comment, current_user: current_user})
~H"""
<div class={[
"pl-#{@comment.depth * 4}",
if(@comment.depth > 0, do: "ml-4 border-l-2 border-base-300", else: "")
]}>
<div class="bg-base-200 rounded-lg p-4">
<%!-- Comment Header --%>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2 text-sm">
<.icon name="hero-user-circle" class="w-5 h-5 text-base-content/60" />
<span class="font-semibold">
<%= if @comment.user do %>
{@comment.user.email}
<% else %>
Unknown
<% end %>
</span>
<span class="text-base-content/60">•</span>
<span class="text-base-content/60">
{Calendar.strftime(@comment.inserted_at, "%b %d, %Y %I:%M %p")}
</span>
</div>
<%!-- Comment Actions --%>
<div class="flex gap-2">
<button
phx-click="reply_to"
phx-value-id={@comment.id}
class="btn btn-ghost btn-xs"
>
<.icon name="hero-arrow-uturn-left" class="w-4 h-4" /> Reply
</button>
<%= if can_delete_comment?(@current_user, @comment) do %>
<button
phx-click="delete_comment"
phx-value-id={@comment.id}
class="btn btn-ghost btn-xs text-error"
data-confirm="Are you sure you want to delete this comment?"
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
<% end %>
</div>
</div>
<%!-- Comment Content --%>
<div class="text-base-content">
{@comment.content}
</div>
<%!-- Nested Comments (Replies) --%>
<%= if @comment.children && length(@comment.children) > 0 do %>
<div class="mt-4 space-y-3">
<%= for child <- @comment.children do %>
{render_comment(child, @current_user, assigns)}
<% end %>
</div>
<% end %>
</div>
</div>
"""
end
end