Packages
phoenix_kit
1.7.74
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
Current section
Files
lib/modules/comments/web/index.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title="Comments"
current_path={@url_path}
project_title={@project_title}
>
<div class="container flex flex-col mx-auto px-4 py-6">
<.admin_page_header
back={Routes.path("/admin")}
title="Comments"
subtitle="Moderate comments across all content"
>
<:actions>
<.link
navigate={Routes.path("/admin/settings/comments")}
class="btn btn-ghost btn-sm"
>
<.icon name="hero-cog-6-tooth" class="w-4 h-4" /> Settings
</.link>
</:actions>
</.admin_page_header>
<%!-- Statistics --%>
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div class="stat bg-base-200 rounded-xl">
<div class="stat-title">Total</div>
<div class="stat-value text-primary text-2xl">{@stats.total}</div>
</div>
<div class="stat bg-base-200 rounded-xl">
<div class="stat-title">Published</div>
<div class="stat-value text-success text-2xl">{@stats.published}</div>
</div>
<div class="stat bg-base-200 rounded-xl">
<div class="stat-title">Pending</div>
<div class="stat-value text-warning text-2xl">{@stats.pending}</div>
</div>
<div class="stat bg-base-200 rounded-xl">
<div class="stat-title">Hidden</div>
<div class="stat-value text-info text-2xl">{@stats.hidden}</div>
</div>
<div class="stat bg-base-200 rounded-xl">
<div class="stat-title">Deleted</div>
<div class="stat-value text-error text-2xl">{@stats.deleted}</div>
</div>
</div>
<%!-- Filters --%>
<div class="bg-base-200 rounded-lg p-4 mb-6">
<.form for={%{}} phx-change="filter" class="flex flex-wrap gap-4 items-end">
<div class="form-control flex-1 min-w-48">
<label class="label"><span class="label-text">Search</span></label>
<input
type="text"
name="search[query]"
value={@search}
placeholder="Search comment content..."
class="input input-bordered input-sm w-full"
phx-debounce="300"
/>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Resource Type</span></label>
<select name="filter[resource_type]" class="select select-bordered select-sm">
<option value="">All Types</option>
<option value="post" selected={@filter_resource_type == "post"}>Post</option>
<option value="entity" selected={@filter_resource_type == "entity"}>Entity</option>
<option value="ticket" selected={@filter_resource_type == "ticket"}>Ticket</option>
</select>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Status</span></label>
<select name="filter[status]" class="select select-bordered select-sm">
<option value="">All Statuses</option>
<option value="published" selected={@filter_status == "published"}>Published</option>
<option value="pending" selected={@filter_status == "pending"}>Pending</option>
<option value="hidden" selected={@filter_status == "hidden"}>Hidden</option>
<option value="deleted" selected={@filter_status == "deleted"}>Deleted</option>
</select>
</div>
<button type="button" phx-click="clear_filters" class="btn btn-ghost btn-sm">
Clear
</button>
</.form>
</div>
<%!-- Bulk Actions --%>
<%= if length(@selected_uuids) > 0 do %>
<div class="flex gap-2 mb-4 items-center">
<span class="text-sm text-base-content/70">{length(@selected_uuids)} selected</span>
<button phx-click="bulk_action" phx-value-action="approve" class="btn btn-success btn-xs">
Approve
</button>
<button phx-click="bulk_action" phx-value-action="hide" class="btn btn-warning btn-xs">
Hide
</button>
<button
phx-click="bulk_action"
phx-value-action="delete"
class="btn btn-error btn-xs"
data-confirm="Delete selected comments?"
>
Delete
</button>
</div>
<% end %>
<%!-- Comments Table --%>
<%= if length(@comments) > 0 do %>
<.table_default
id="comments-table"
variant="zebra"
size="sm"
class="w-full"
toggleable={true}
items={@comments}
card_title={fn comment -> String.slice(comment.content, 0..99) end}
card_fields={
fn comment ->
[
%{
label: gettext("Author"),
value: if(comment.user, do: comment.user.email, else: "Deleted user")
},
%{label: gettext("Resource"), value: comment.resource_type},
%{label: gettext("Status"), value: comment.status},
%{
label: gettext("Date"),
value: Calendar.strftime(comment.inserted_at, "%b %d, %Y")
}
]
end
}
>
<:card_actions :let={comment}>
<%= if comment.status != "published" do %>
<button
phx-click="approve"
phx-value-uuid={comment.uuid}
class="btn btn-xs btn-outline btn-success"
>
{gettext("Approve")}
</button>
<% end %>
<%= if comment.status != "hidden" do %>
<button
phx-click="hide"
phx-value-uuid={comment.uuid}
class="btn btn-xs btn-outline btn-warning"
>
{gettext("Hide")}
</button>
<% end %>
<button
phx-click="delete"
phx-value-uuid={comment.uuid}
class="btn btn-xs btn-outline btn-error"
data-confirm="Delete this comment?"
>
{gettext("Delete")}
</button>
</:card_actions>
<.table_default_header>
<.table_default_row hover={false}>
<.table_default_header_cell class="w-8"></.table_default_header_cell>
<.table_default_header_cell>Content</.table_default_header_cell>
<.table_default_header_cell>Author</.table_default_header_cell>
<.table_default_header_cell>Resource</.table_default_header_cell>
<.table_default_header_cell>Status</.table_default_header_cell>
<.table_default_header_cell>Date</.table_default_header_cell>
<.table_default_header_cell>Actions</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<%= for comment <- @comments do %>
<.table_default_row>
<.table_default_cell>
<input
type="checkbox"
class="checkbox checkbox-xs"
phx-click="toggle_select"
phx-value-uuid={comment.uuid}
checked={comment.uuid in @selected_uuids}
/>
</.table_default_cell>
<.table_default_cell class="max-w-xs">
<%= if comment.depth > 0 do %>
<div class="flex items-center gap-1 text-base-content/50 text-xs mb-0.5">
<.icon name="hero-arrow-uturn-right-mini" class="size-3" />
<span>Reply</span>
<%= if comment.parent do %>
<span class="truncate max-w-[200px]">
— Re: {String.slice(comment.parent.content, 0..39)}
</span>
<% end %>
</div>
<% end %>
<div class="truncate text-sm">{String.slice(comment.content, 0..99)}</div>
</.table_default_cell>
<.table_default_cell class="text-sm">
<%= if comment.user do %>
{comment.user.email}
<% else %>
<span class="text-base-content/50">Deleted user</span>
<% end %>
</.table_default_cell>
<.table_default_cell>
<span class="badge badge-ghost badge-sm h-auto">
{comment.resource_type}
</span>
<%= case resource_info(@resource_context, comment) do %>
<% %{title: title, path: path} -> %>
<.link
navigate={Routes.path(path)}
class="link link-hover text-sm ml-1 truncate max-w-[200px] inline-block align-bottom"
title={title}
>
{String.slice(title, 0..49)}
</.link>
<% _ -> %>
<span
class="text-base-content/40 text-xs ml-1"
title={to_string(comment.resource_uuid)}
>
{String.slice(to_string(comment.resource_uuid), 0..7)}
</span>
<% end %>
</.table_default_cell>
<.table_default_cell>
<span class={status_badge_class(comment.status)}>
{comment.status}
</span>
</.table_default_cell>
<.table_default_cell class="text-sm text-base-content/70">
{Calendar.strftime(comment.inserted_at, "%b %d, %Y")}
</.table_default_cell>
<.table_default_cell>
<div class="flex gap-1">
<%= if comment.status != "published" do %>
<button
phx-click="approve"
phx-value-uuid={comment.uuid}
class="btn btn-xs btn-outline btn-success tooltip tooltip-bottom"
data-tip={gettext("Approve")}
>
<.icon name="hero-check" class="w-4 h-4 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Approve")}</span>
</button>
<% end %>
<%= if comment.status != "hidden" do %>
<button
phx-click="hide"
phx-value-uuid={comment.uuid}
class="btn btn-xs btn-outline btn-warning tooltip tooltip-bottom"
data-tip={gettext("Hide")}
>
<.icon name="hero-eye-slash" class="w-4 h-4 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Hide")}</span>
</button>
<% end %>
<button
phx-click="delete"
phx-value-uuid={comment.uuid}
class="btn btn-xs btn-outline btn-error tooltip tooltip-bottom"
data-tip={gettext("Delete")}
data-confirm="Delete this comment?"
>
<.icon name="hero-trash" class="w-4 h-4 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Delete")}</span>
</button>
</div>
</.table_default_cell>
</.table_default_row>
<% end %>
</.table_default_body>
</.table_default>
<% else %>
<div class="text-center py-12 text-base-content/60">
<.icon name="hero-chat-bubble-left" class="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>No comments found</p>
</div>
<% end %>
<%!-- Pagination --%>
<%= if @total_pages > 1 do %>
<div class="flex justify-center mt-6">
<div class="join">
<%= for page <- max(1, @page - 2)..min(@total_pages, @page + 2) do %>
<.link
patch={
Routes.path(
"/admin/comments?page=#{page}&search=#{@search}&resource_type=#{@filter_resource_type || ""}&status=#{@filter_status || ""}"
)
}
class={["join-item btn btn-sm", if(page == @page, do: "btn-active", else: "")]}
>
{page}
</.link>
<% end %>
</div>
</div>
<% end %>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>