diff --git a/lib/modules/comments/comments.ex b/lib/modules/comments/comments.ex deleted file mode 100644 index ce4dab2d7..000000000 --- a/lib/modules/comments/comments.ex +++ /dev/null @@ -1,700 +0,0 @@ -defmodule PhoenixKit.Modules.Comments do - @moduledoc """ - Standalone, resource-agnostic comments module. - - Provides polymorphic commenting for any resource type (posts, entities, tickets, etc.) - with unlimited threading, likes/dislikes, and moderation support. - - ## Architecture - - Comments are linked to resources via `resource_type` (string) + `resource_uuid` (UUID). - No foreign key constraints on the resource side — any module can use comments. - - ## Resource Handler Callbacks - - Modules that consume comments can register handlers to receive notifications - when comments are created or deleted. Configure in your app: - - config :phoenix_kit, :comment_resource_handlers, %{ - "post" => PhoenixKitPosts - } - - Handler modules should implement `on_comment_created/3` and `on_comment_deleted/3`. - - ## Core Functions - - ### System Management - - `enabled?/0` - Check if Comments module is enabled - - `enable_system/0` - Enable the Comments module - - `disable_system/0` - Disable the Comments module - - `get_config/0` - Get module configuration with statistics - - ### Comment CRUD - - `create_comment/4` - Create a comment on a resource - - `update_comment/2` - Update a comment - - `delete_comment/1` - Delete a comment - - `get_comment/2`, `get_comment!/2` - Get by ID - - `list_comments/3` - Flat list for a resource - - `get_comment_tree/2` - Nested tree for a resource - - `count_comments/3` - Count comments for a resource - - ### Moderation - - `approve_comment/1` - Set status to published - - `hide_comment/1` - Set status to hidden - - `bulk_update_status/2` - Bulk status changes - - `list_all_comments/1` - Cross-resource listing with filters - - `comment_stats/0` - Aggregate statistics - - ### Like/Dislike - - `like_comment/2`, `unlike_comment/2`, `comment_liked_by?/2` - - `dislike_comment/2`, `undislike_comment/2`, `comment_disliked_by?/2` - """ - - use PhoenixKit.Module - - import Ecto.Query, warn: false - require Logger - - alias PhoenixKit.Dashboard.Tab - alias PhoenixKit.Modules.Comments.Comment - alias PhoenixKit.Modules.Comments.CommentDislike - alias PhoenixKit.Modules.Comments.CommentLike - alias PhoenixKit.Settings - alias PhoenixKit.Utils.Date, as: UtilsDate - alias PhoenixKit.Utils.UUID, as: UUIDUtils - - # ============================================================================ - # Module Status - # ============================================================================ - - @impl PhoenixKit.Module - @doc "Checks if the Comments module is enabled." - def enabled? do - Settings.get_boolean_setting("comments_enabled", false) - end - - @impl PhoenixKit.Module - @doc "Enables the Comments module." - def enable_system do - Settings.update_boolean_setting_with_module("comments_enabled", true, "comments") - end - - @impl PhoenixKit.Module - @doc "Disables the Comments module." - def disable_system do - Settings.update_boolean_setting_with_module("comments_enabled", false, "comments") - end - - @impl PhoenixKit.Module - @doc "Gets the Comments module configuration with statistics." - def get_config do - %{ - enabled: enabled?(), - total_comments: count_all_comments(), - published_comments: count_all_comments(status: "published"), - pending_comments: count_all_comments(status: "pending"), - moderation_enabled: Settings.get_boolean_setting("comments_moderation", false), - max_depth: get_max_depth(), - max_length: get_max_length() - } - end - - @doc "Returns the configured maximum comment depth." - def get_max_depth do - Settings.get_setting("comments_max_depth", "10") |> String.to_integer() - end - - @doc "Returns the configured maximum comment length." - def get_max_length do - Settings.get_setting("comments_max_length", "10000") |> String.to_integer() - end - - # ============================================================================ - # Module Behaviour Callbacks - # ============================================================================ - - @impl PhoenixKit.Module - def module_key, do: "comments" - - @impl PhoenixKit.Module - def module_name, do: "Comments" - - @impl PhoenixKit.Module - def permission_metadata do - %{ - key: "comments", - label: "Comments", - icon: "hero-chat-bubble-left-right", - description: "Comment moderation, threading, and reactions across all content types" - } - end - - @impl PhoenixKit.Module - def admin_tabs do - [ - Tab.new!( - id: :admin_comments, - label: "Comments", - icon: "hero-chat-bubble-left-right", - path: "comments", - priority: 590, - level: :admin, - permission: "comments", - match: :prefix, - group: :admin_modules - ) - ] - end - - @impl PhoenixKit.Module - def settings_tabs do - [ - Tab.new!( - id: :admin_settings_comments, - label: "Comments", - icon: "hero-chat-bubble-left-right", - path: "comments", - priority: 924, - level: :admin, - parent: :admin_settings, - permission: "comments" - ) - ] - end - - # ============================================================================ - # Comment CRUD - # ============================================================================ - - @doc """ - Creates a comment on a resource. - - Automatically calculates depth from parent. Invokes resource handler callback - if configured. - - ## Parameters - - - `resource_type` - Type of resource (e.g., "post") - - `resource_uuid` - UUID of the resource - - `user_uuid` - UUID of commenter - - `attrs` - Comment attributes (content, parent_uuid, etc.) - """ - def create_comment(resource_type, resource_uuid, user_uuid, attrs) when is_binary(user_uuid) do - if UUIDUtils.valid?(user_uuid) do - do_create_comment(resource_type, resource_uuid, user_uuid, attrs) - else - {:error, :invalid_user_uuid} - end - end - - defp do_create_comment(resource_type, resource_uuid, user_uuid, attrs) do - repo().transaction(fn -> - attrs = - attrs - |> Map.put(:resource_type, resource_type) - |> Map.put(:resource_uuid, resource_uuid) - |> Map.put(:user_uuid, user_uuid) - |> maybe_calculate_depth() - - case %Comment{} - |> Comment.changeset(attrs) - |> repo().insert() do - {:ok, comment} -> - notify_resource_handler(:on_comment_created, resource_type, resource_uuid, comment) - comment - - {:error, changeset} -> - repo().rollback(changeset) - end - end) - end - - @doc """ - Updates a comment. - - ## Parameters - - - `comment` - Comment to update - - `attrs` - Attributes to update (content, status) - """ - def update_comment(%Comment{} = comment, attrs) do - comment - |> Comment.changeset(attrs) - |> repo().update() - end - - @doc """ - Deletes a comment. - - Cascades to child comments. Invokes resource handler callback if configured. - """ - def delete_comment(%Comment{} = comment) do - repo().transaction(fn -> - case repo().delete(comment) do - {:ok, deleted} -> - notify_resource_handler( - :on_comment_deleted, - comment.resource_type, - comment.resource_uuid, - deleted - ) - - deleted - - {:error, changeset} -> - repo().rollback(changeset) - end - end) - end - - @doc """ - Gets a single comment by ID with optional preloads. - - Returns `nil` if not found. - """ - def get_comment(id, opts \\ []) do - preloads = Keyword.get(opts, :preload, []) - - case repo().get(Comment, id) do - nil -> nil - comment -> repo().preload(comment, preloads) - end - end - - @doc """ - Gets a single comment by ID with optional preloads. - - Raises `Ecto.NoResultsError` if not found. - """ - def get_comment!(id, opts \\ []) do - preloads = Keyword.get(opts, :preload, []) - - Comment - |> repo().get!(id) - |> repo().preload(preloads) - end - - @doc """ - Gets nested comment tree for a resource. - - Returns all published comments organized in a tree structure. - """ - def get_comment_tree(resource_type, resource_uuid) do - comments = - from(c in Comment, - where: - c.resource_type == ^resource_type and - c.resource_uuid == ^resource_uuid and - c.status == "published", - order_by: [asc: c.inserted_at], - preload: [:user] - ) - |> repo().all() - - build_comment_tree(comments) - end - - @doc """ - Lists comments for a resource (flat list). - - ## Options - - - `:preload` - Associations to preload - - `:status` - Filter by status - """ - def list_comments(resource_type, resource_uuid, opts \\ []) do - preloads = Keyword.get(opts, :preload, []) - status = Keyword.get(opts, :status) - - query = - from(c in Comment, - where: c.resource_type == ^resource_type and c.resource_uuid == ^resource_uuid, - order_by: [asc: c.inserted_at] - ) - - query = if status, do: where(query, [c], c.status == ^status), else: query - - query - |> repo().all() - |> repo().preload(preloads) - end - - @doc "Counts comments for a resource." - def count_comments(resource_type, resource_uuid, opts \\ []) do - status = Keyword.get(opts, :status) - - query = - from(c in Comment, - where: c.resource_type == ^resource_type and c.resource_uuid == ^resource_uuid - ) - - query = if status, do: where(query, [c], c.status == ^status), else: query - - repo().aggregate(query, :count) - rescue - _ -> 0 - end - - # ============================================================================ - # Moderation - # ============================================================================ - - @doc "Sets a comment's status to published." - def approve_comment(%Comment{} = comment) do - update_comment(comment, %{status: "published"}) - end - - @doc "Sets a comment's status to hidden." - def hide_comment(%Comment{} = comment) do - update_comment(comment, %{status: "hidden"}) - end - - @doc "Bulk-updates status for multiple comment UUIDs." - def bulk_update_status(comment_uuids, status) - when is_list(comment_uuids) and status in ["published", "hidden", "deleted", "pending"] do - from(c in Comment, where: c.uuid in ^comment_uuids) - |> repo().update_all(set: [status: status, updated_at: UtilsDate.utc_now()]) - end - - @doc """ - Lists all comments across all resource types with filters. - - ## Options - - - `:resource_type` - Filter by resource type - - `:status` - Filter by status - - `:user_uuid` - Filter by user - - `:search` - Search in content - - `:page` - Page number (default: 1) - - `:per_page` - Items per page (default: 20) - """ - def list_all_comments(opts \\ []) do - page = Keyword.get(opts, :page, 1) - per_page = Keyword.get(opts, :per_page, 20) - resource_type = Keyword.get(opts, :resource_type) - status = Keyword.get(opts, :status) - user_uuid = Keyword.get(opts, :user_uuid) - search = Keyword.get(opts, :search) - - query = - from(c in Comment, - order_by: [desc: c.inserted_at], - preload: [:user, :parent] - ) - - query = - if resource_type, do: where(query, [c], c.resource_type == ^resource_type), else: query - - query = if status, do: where(query, [c], c.status == ^status), else: query - query = maybe_filter_by_user(query, user_uuid) - - query = - if search && search != "" do - pattern = "%#{search}%" - where(query, [c], ilike(c.content, ^pattern)) - else - query - end - - total = repo().aggregate(query, :count) - - comments = - query - |> limit(^per_page) - |> offset(^((page - 1) * per_page)) - |> repo().all() - - %{ - comments: comments, - total: total, - page: page, - per_page: per_page, - total_pages: ceil(total / per_page) - } - end - - @doc "Returns aggregate statistics for all comments." - def comment_stats do - %{ - total: count_all_comments(), - published: count_all_comments(status: "published"), - pending: count_all_comments(status: "pending"), - hidden: count_all_comments(status: "hidden"), - deleted: count_all_comments(status: "deleted") - } - end - - # ============================================================================ - # Resource Resolution (for admin UI) - # ============================================================================ - - @doc """ - Resolves resource context (title and admin path) for a list of comments. - - Returns a map of `{resource_type, resource_uuid} => %{title: ..., path: ...}` - by delegating to registered `comment_resource_handlers` that implement - `resolve_comment_resources/1`. - """ - def resolve_resource_context(comments) do - comments - |> Enum.group_by(& &1.resource_type, & &1.resource_uuid) - |> Enum.reduce(%{}, fn {resource_type, ids}, acc -> - resolved = resolve_for_type(resource_type, Enum.uniq(ids)) - - Enum.reduce(resolved, acc, fn {id, info}, inner -> - Map.put(inner, {resource_type, id}, info) - end) - end) - end - - defp resource_handlers do - configured = Application.get_env(:phoenix_kit, :comment_resource_handlers, %{}) - Map.merge(default_resource_handlers(), configured) - end - - defp default_resource_handlers do - handlers = %{} - - handlers = - if Code.ensure_loaded?(PhoenixKitPosts), - do: Map.put(handlers, "post", PhoenixKitPosts), - else: handlers - - handlers - end - - defp resolve_for_type(resource_type, resource_uuids) do - handlers = resource_handlers() - - case Map.get(handlers, resource_type) do - nil -> - %{} - - mod -> - if Code.ensure_loaded?(mod) and function_exported?(mod, :resolve_comment_resources, 1) do - mod.resolve_comment_resources(resource_uuids) - else - %{} - end - end - rescue - e -> - Logger.warning("Comment resource resolver error: #{inspect(e)}") - %{} - end - - # ============================================================================ - # Like Operations - # ============================================================================ - - @doc "User likes a comment. Creates like record and increments counter." - def like_comment(comment_uuid, user_uuid) when is_binary(user_uuid) do - repo().transaction(fn -> - case %CommentLike{} - |> CommentLike.changeset(%{ - comment_uuid: comment_uuid, - user_uuid: user_uuid - }) - |> repo().insert() do - {:ok, like} -> - increment_comment_like_count(comment_uuid) - like - - {:error, changeset} -> - repo().rollback(changeset) - end - end) - end - - @doc "User unlikes a comment. Deletes like record and decrements counter." - def unlike_comment(comment_uuid, user_uuid) when is_binary(user_uuid) do - repo().transaction(fn -> - case repo().get_by(CommentLike, comment_uuid: comment_uuid, user_uuid: user_uuid) do - nil -> - repo().rollback(:not_found) - - like -> - {:ok, _} = repo().delete(like) - decrement_comment_like_count(comment_uuid) - like - end - end) - end - - @doc "Checks if a user has liked a comment." - def comment_liked_by?(comment_uuid, user_uuid) when is_binary(user_uuid) do - repo().exists?( - from(l in CommentLike, where: l.comment_uuid == ^comment_uuid and l.user_uuid == ^user_uuid) - ) - end - - @doc "Lists all likes for a comment." - def list_comment_likes(comment_uuid, opts \\ []) do - preloads = Keyword.get(opts, :preload, []) - - from(l in CommentLike, - where: l.comment_uuid == ^comment_uuid, - order_by: [desc: l.inserted_at] - ) - |> repo().all() - |> repo().preload(preloads) - end - - # ============================================================================ - # Dislike Operations - # ============================================================================ - - @doc "User dislikes a comment. Creates dislike record and increments counter." - def dislike_comment(comment_uuid, user_uuid) when is_binary(user_uuid) do - repo().transaction(fn -> - case %CommentDislike{} - |> CommentDislike.changeset(%{ - comment_uuid: comment_uuid, - user_uuid: user_uuid - }) - |> repo().insert() do - {:ok, dislike} -> - increment_comment_dislike_count(comment_uuid) - dislike - - {:error, changeset} -> - repo().rollback(changeset) - end - end) - end - - @doc "User removes dislike from a comment. Deletes dislike record and decrements counter." - def undislike_comment(comment_uuid, user_uuid) when is_binary(user_uuid) do - repo().transaction(fn -> - case repo().get_by(CommentDislike, comment_uuid: comment_uuid, user_uuid: user_uuid) do - nil -> - repo().rollback(:not_found) - - dislike -> - {:ok, _} = repo().delete(dislike) - decrement_comment_dislike_count(comment_uuid) - dislike - end - end) - end - - @doc "Checks if a user has disliked a comment." - def comment_disliked_by?(comment_uuid, user_uuid) when is_binary(user_uuid) do - repo().exists?( - from(d in CommentDislike, - where: d.comment_uuid == ^comment_uuid and d.user_uuid == ^user_uuid - ) - ) - end - - @doc "Lists all dislikes for a comment." - def list_comment_dislikes(comment_uuid, opts \\ []) do - preloads = Keyword.get(opts, :preload, []) - - from(d in CommentDislike, - where: d.comment_uuid == ^comment_uuid, - order_by: [desc: d.inserted_at] - ) - |> repo().all() - |> repo().preload(preloads) - end - - # ============================================================================ - # Private Helpers - # ============================================================================ - - defp maybe_calculate_depth(attrs) do - case Map.get(attrs, :parent_uuid) do - nil -> - Map.put(attrs, :depth, 0) - - parent_uuid -> - case repo().get(Comment, parent_uuid) do - nil -> Map.put(attrs, :depth, 0) - parent -> Map.put(attrs, :depth, (parent.depth || 0) + 1) - end - end - end - - defp build_comment_tree(comments) do - comment_map = Map.new(comments, &{&1.uuid, &1}) - - comments - |> Enum.filter(&(&1.parent_uuid == nil)) - |> Enum.map(&add_children(&1, comment_map)) - end - - defp add_children(comment, comment_map) do - children = - comment_map - |> Map.values() - |> Enum.filter(&(&1.parent_uuid == comment.uuid)) - |> Enum.map(&add_children(&1, comment_map)) - - Map.put(comment, :children, children) - end - - defp increment_comment_like_count(comment_uuid) do - from(c in Comment, where: c.uuid == ^comment_uuid) - |> repo().update_all(inc: [like_count: 1]) - end - - defp decrement_comment_like_count(comment_uuid) do - from(c in Comment, where: c.uuid == ^comment_uuid and c.like_count > 0) - |> repo().update_all(inc: [like_count: -1]) - end - - defp increment_comment_dislike_count(comment_uuid) do - from(c in Comment, where: c.uuid == ^comment_uuid) - |> repo().update_all(inc: [dislike_count: 1]) - end - - defp decrement_comment_dislike_count(comment_uuid) do - from(c in Comment, where: c.uuid == ^comment_uuid and c.dislike_count > 0) - |> repo().update_all(inc: [dislike_count: -1]) - end - - defp count_all_comments(opts \\ []) do - status = Keyword.get(opts, :status) - query = from(c in Comment) - query = if status, do: where(query, [c], c.status == ^status), else: query - repo().aggregate(query, :count) - rescue - _ -> 0 - end - - defp maybe_filter_by_user(query, nil), do: query - - defp maybe_filter_by_user(query, user_uuid) when is_binary(user_uuid) do - if UUIDUtils.valid?(user_uuid) do - where(query, [c], c.user_uuid == ^user_uuid) - else - query - end - end - - defp notify_resource_handler(callback, resource_type, resource_uuid, comment) do - handlers = resource_handlers() - - case Map.get(handlers, resource_type) do - nil -> - :ok - - handler_module -> - if Code.ensure_loaded?(handler_module) and - function_exported?(handler_module, callback, 3) do - apply(handler_module, callback, [resource_type, resource_uuid, comment]) - else - :ok - end - end - rescue - error -> - Logger.warning("Comment resource handler error: #{inspect(error)}") - :ok - end - - defp repo do - PhoenixKit.RepoHelper.repo() - end -end diff --git a/lib/modules/comments/schemas/comment.ex b/lib/modules/comments/schemas/comment.ex deleted file mode 100644 index dd1139d44..000000000 --- a/lib/modules/comments/schemas/comment.ex +++ /dev/null @@ -1,122 +0,0 @@ -defmodule PhoenixKit.Modules.Comments.Comment do - @moduledoc """ - Schema for polymorphic comments with unlimited threading depth. - - Supports nested comment threads (Reddit-style) with self-referencing parent/child - relationships. Can be attached to any resource type via `resource_type` + `resource_uuid`. - - ## Comment Status - - - `published` - Comment is visible - - `hidden` - Comment is hidden by moderator - - `deleted` - Comment deleted (soft delete) - - `pending` - Awaiting moderation approval - - ## Fields - - - `resource_type` - Type of resource (e.g., "post", "entity", "ticket") - - `resource_uuid` - UUID of the resource - - `user_uuid` - Reference to the commenter - - `parent_uuid` - Reference to parent comment (nil for top-level) - - `content` - Comment text - - `status` - published/hidden/deleted/pending - - `depth` - Nesting level (0=top, 1=reply, 2=reply-to-reply, etc.) - - `like_count` - Denormalized like counter - - `dislike_count` - Denormalized dislike counter - - `metadata` - Arbitrary JSONB data (giphy reactions, custom flags, rich embeds, etc.) - """ - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:uuid, UUIDv7, autogenerate: true} - - @type t :: %__MODULE__{ - uuid: UUIDv7.t() | nil, - resource_type: String.t(), - resource_uuid: Ecto.UUID.t(), - user_uuid: UUIDv7.t() | nil, - parent_uuid: UUIDv7.t() | nil, - content: String.t(), - status: String.t(), - depth: integer(), - like_count: integer(), - dislike_count: integer(), - metadata: map(), - user: PhoenixKit.Users.Auth.User.t() | Ecto.Association.NotLoaded.t() | nil, - parent: t() | Ecto.Association.NotLoaded.t() | nil, - children: [t()] | Ecto.Association.NotLoaded.t(), - inserted_at: DateTime.t() | nil, - updated_at: DateTime.t() | nil - } - - schema "phoenix_kit_comments" do - field :resource_type, :string - field :resource_uuid, Ecto.UUID - field :content, :string - field :status, :string, default: "published" - field :depth, :integer, default: 0 - field :like_count, :integer, default: 0 - field :dislike_count, :integer, default: 0 - field :metadata, :map, default: %{} - - belongs_to :user, PhoenixKit.Users.Auth.User, - foreign_key: :user_uuid, - references: :uuid, - type: UUIDv7 - - belongs_to :parent, __MODULE__, - foreign_key: :parent_uuid, - references: :uuid, - type: UUIDv7 - - has_many :children, __MODULE__, foreign_key: :parent_uuid - - timestamps(type: :utc_datetime) - end - - @doc """ - Changeset for creating or updating a comment. - - ## Required Fields - - - `resource_type` - Type of resource being commented on - - `resource_uuid` - UUID of the resource - - `user_uuid` - Reference to commenter - - `content` - Comment text - """ - def changeset(comment, attrs) do - comment - |> cast(attrs, [ - :resource_type, - :resource_uuid, - :user_uuid, - :parent_uuid, - :content, - :status, - :depth, - :metadata - ]) - |> validate_required([:resource_type, :resource_uuid, :user_uuid, :content]) - |> validate_inclusion(:status, ["published", "hidden", "deleted", "pending"]) - |> validate_length(:content, min: 1, max: 10_000) - |> validate_length(:resource_type, max: 50) - |> foreign_key_constraint(:user_uuid) - |> foreign_key_constraint(:parent_uuid) - end - - @doc "Check if comment is a reply (has parent)." - def reply?(%__MODULE__{parent_uuid: nil}), do: false - def reply?(%__MODULE__{}), do: true - - @doc "Check if comment is top-level (no parent)." - def top_level?(%__MODULE__{parent_uuid: nil}), do: true - def top_level?(%__MODULE__{}), do: false - - @doc "Check if comment is published." - def published?(%__MODULE__{status: "published"}), do: true - def published?(_), do: false - - @doc "Check if comment is deleted." - def deleted?(%__MODULE__{status: "deleted"}), do: true - def deleted?(_), do: false -end diff --git a/lib/modules/comments/schemas/comment_dislike.ex b/lib/modules/comments/schemas/comment_dislike.ex deleted file mode 100644 index 39c1ca193..000000000 --- a/lib/modules/comments/schemas/comment_dislike.ex +++ /dev/null @@ -1,52 +0,0 @@ -defmodule PhoenixKit.Modules.Comments.CommentDislike do - @moduledoc """ - Schema for comment dislikes in the standalone Comments module. - - Tracks which users have disliked which comments. Enforces one dislike per user per comment. - """ - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:uuid, UUIDv7, autogenerate: true} - - @type t :: %__MODULE__{ - uuid: UUIDv7.t() | nil, - comment_uuid: UUIDv7.t(), - user_uuid: UUIDv7.t() | nil, - comment: PhoenixKit.Modules.Comments.Comment.t() | Ecto.Association.NotLoaded.t(), - user: PhoenixKit.Users.Auth.User.t() | Ecto.Association.NotLoaded.t(), - inserted_at: DateTime.t() | nil, - updated_at: DateTime.t() | nil - } - - schema "phoenix_kit_comments_dislikes" do - belongs_to :comment, PhoenixKit.Modules.Comments.Comment, - foreign_key: :comment_uuid, - references: :uuid, - type: UUIDv7 - - belongs_to :user, PhoenixKit.Users.Auth.User, - foreign_key: :user_uuid, - references: :uuid, - type: UUIDv7 - - timestamps(type: :utc_datetime) - end - - @doc """ - Changeset for creating a comment dislike. - - Unique constraint on (comment_uuid, user_uuid) — one dislike per user per comment. - """ - def changeset(dislike, attrs) do - dislike - |> cast(attrs, [:comment_uuid, :user_uuid]) - |> validate_required([:comment_uuid, :user_uuid]) - |> foreign_key_constraint(:comment_uuid) - |> foreign_key_constraint(:user_uuid) - |> unique_constraint([:comment_uuid, :user_uuid], - name: :uq_comments_dislikes_comment_user, - message: "you have already disliked this comment" - ) - end -end diff --git a/lib/modules/comments/schemas/comment_like.ex b/lib/modules/comments/schemas/comment_like.ex deleted file mode 100644 index a36a6793a..000000000 --- a/lib/modules/comments/schemas/comment_like.ex +++ /dev/null @@ -1,52 +0,0 @@ -defmodule PhoenixKit.Modules.Comments.CommentLike do - @moduledoc """ - Schema for comment likes in the standalone Comments module. - - Tracks which users have liked which comments. Enforces one like per user per comment. - """ - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:uuid, UUIDv7, autogenerate: true} - - @type t :: %__MODULE__{ - uuid: UUIDv7.t() | nil, - comment_uuid: UUIDv7.t(), - user_uuid: UUIDv7.t() | nil, - comment: PhoenixKit.Modules.Comments.Comment.t() | Ecto.Association.NotLoaded.t(), - user: PhoenixKit.Users.Auth.User.t() | Ecto.Association.NotLoaded.t(), - inserted_at: DateTime.t() | nil, - updated_at: DateTime.t() | nil - } - - schema "phoenix_kit_comments_likes" do - belongs_to :comment, PhoenixKit.Modules.Comments.Comment, - foreign_key: :comment_uuid, - references: :uuid, - type: UUIDv7 - - belongs_to :user, PhoenixKit.Users.Auth.User, - foreign_key: :user_uuid, - references: :uuid, - type: UUIDv7 - - timestamps(type: :utc_datetime) - end - - @doc """ - Changeset for creating a comment like. - - Unique constraint on (comment_uuid, user_uuid) — one like per user per comment. - """ - def changeset(like, attrs) do - like - |> cast(attrs, [:comment_uuid, :user_uuid]) - |> validate_required([:comment_uuid, :user_uuid]) - |> foreign_key_constraint(:comment_uuid) - |> foreign_key_constraint(:user_uuid) - |> unique_constraint([:comment_uuid, :user_uuid], - name: :uq_comments_likes_comment_user, - message: "you have already liked this comment" - ) - end -end diff --git a/lib/modules/comments/web/comments_component.ex b/lib/modules/comments/web/comments_component.ex deleted file mode 100644 index 8b9d0e7f8..000000000 --- a/lib/modules/comments/web/comments_component.ex +++ /dev/null @@ -1,254 +0,0 @@ -defmodule PhoenixKit.Modules.Comments.Web.CommentsComponent do - @moduledoc """ - Reusable LiveComponent for displaying and managing comments on any resource. - - ## Usage - - <.live_component - module={PhoenixKit.Modules.Comments.Web.CommentsComponent} - id={"comments-\#{@post.uuid}"} - resource_type="post" - resource_uuid={@post.uuid} - current_user={@current_user} - /> - - ## Required Attrs - - - `resource_type` - String identifying the resource type (e.g., "post") - - `resource_uuid` - UUID of the resource - - `current_user` - Current authenticated user struct - - `id` - Unique component ID - - ## Optional Attrs - - - `enabled` - Whether comments are enabled (default: true) - - `show_likes` - Show like/dislike buttons (default: false) - - `title` - Section title (default: "Comments") - - ## Parent Notifications - - After create/delete, sends to the parent LiveView: - - {:comments_updated, %{resource_type: "post", resource_uuid: uuid, action: :created | :deleted}} - """ - - use PhoenixKitWeb, :live_component - - import PhoenixKitWeb.Components.Core.Icon - - alias PhoenixKit.Modules.Comments - alias PhoenixKit.Users.Roles - - @impl true - def mount(socket) do - {:ok, - socket - |> assign(:comments, []) - |> assign(:reply_to, nil) - |> assign(:new_comment, "")} - end - - @impl true - def update(assigns, socket) do - socket = - socket - |> assign(assigns) - |> assign_new(:enabled, fn -> true end) - |> assign_new(:show_likes, fn -> false end) - |> assign_new(:title, fn -> "Comments" end) - - socket = - if changed?(socket, :resource_uuid) or socket.assigns.comments == [] do - load_comments(socket) - else - socket - end - - {:ok, socket} - end - - @impl true - def handle_event("add_comment", %{"comment" => comment_text}, socket) do - if comment_text != "" do - parent_uuid = socket.assigns.reply_to - - attrs = %{ - content: comment_text, - parent_uuid: parent_uuid - } - - case Comments.create_comment( - socket.assigns.resource_type, - socket.assigns.resource_uuid, - socket.assigns.current_user.uuid, - attrs - ) do - {:ok, _comment} -> - send( - self(), - {:comments_updated, - %{ - resource_type: socket.assigns.resource_type, - resource_uuid: socket.assigns.resource_uuid, - action: :created - }} - ) - - {:noreply, - socket - |> 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_uuid}, socket) do - {:noreply, assign(socket, :reply_to, comment_uuid)} - 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_uuid}, socket) do - case Comments.get_comment(comment_uuid) do - nil -> - {:noreply, socket |> put_flash(:error, "Comment not found")} - - comment -> - if can_delete_comment?(socket.assigns.current_user, comment) do - case Comments.delete_comment(comment) do - {:ok, _} -> - send( - self(), - {:comments_updated, - %{ - resource_type: socket.assigns.resource_type, - resource_uuid: socket.assigns.resource_uuid, - action: :deleted - }} - ) - - {:noreply, - socket - |> 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 - - defp load_comments(socket) do - comments = - Comments.get_comment_tree(socket.assigns.resource_type, socket.assigns.resource_uuid) - - comment_count = - Comments.count_comments( - socket.assigns.resource_type, - socket.assigns.resource_uuid, - status: "published" - ) - - socket - |> assign(:comments, comments) - |> assign(:comment_count, comment_count) - end - - attr :comment, :map, required: true - attr :current_user, :map, required: true - attr :myself, :any, required: true - - def render_comment(assigns) do - ~H""" -
0, do: "ml-4 border-l-2 border-base-300", else: "") - ]}> -
- <%!-- Comment Header --%> -
-
- <.icon name="hero-user-circle" class="w-5 h-5 text-base-content/60" /> - - <%= if @comment.user do %> - {@comment.user.email} - <% else %> - Unknown - <% end %> - - - - {Calendar.strftime(@comment.inserted_at, "%b %d, %Y %I:%M %p")} - -
- - <%!-- Comment Actions --%> -
- - - <%= if can_delete_comment?(@current_user, @comment) do %> - - <% end %> -
-
- - <%!-- Comment Content --%> -
- {@comment.content} -
- - <%!-- Nested Comments (Replies) --%> - <%= if @comment.children && length(@comment.children) > 0 do %> -
- <%= for child <- @comment.children do %> - <.render_comment - comment={child} - current_user={@current_user} - myself={@myself} - /> - <% end %> -
- <% end %> -
-
- """ - end - - defp can_delete_comment?(user, comment) do - user.uuid == comment.user_uuid 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 -end diff --git a/lib/modules/comments/web/comments_component.html.heex b/lib/modules/comments/web/comments_component.html.heex deleted file mode 100644 index 951f47a21..000000000 --- a/lib/modules/comments/web/comments_component.html.heex +++ /dev/null @@ -1,60 +0,0 @@ -
- <%= if @enabled do %> -
-
-

- <.icon name="hero-chat-bubble-left-right" class="w-6 h-6" /> - {@title} ({@comment_count}) -

- - <%!-- New Comment Form --%> -
- <%= if @reply_to do %> -
-
- Replying to comment -
- -
- <% end %> - - <.form for={%{}} phx-submit="add_comment" phx-target={@myself} class="space-y-2"> - - -
- -
- -
- - <%!-- Comments List --%> - <%= if length(@comments) > 0 do %> -
- <%= for comment <- @comments do %> - <.render_comment - comment={comment} - current_user={@current_user} - myself={@myself} - /> - <% end %> -
- <% else %> -
- <.icon name="hero-chat-bubble-left" class="w-12 h-12 mx-auto mb-2 opacity-50" /> -

No comments yet. Be the first to comment!

-
- <% end %> -
-
- <% end %> -
diff --git a/lib/modules/comments/web/index.ex b/lib/modules/comments/web/index.ex deleted file mode 100644 index 3ee5ad132..000000000 --- a/lib/modules/comments/web/index.ex +++ /dev/null @@ -1,260 +0,0 @@ -defmodule PhoenixKit.Modules.Comments.Web.Index do - @moduledoc """ - LiveView for comment moderation admin page. - - Provides cross-resource comment management with filtering, search, - pagination, and bulk actions. - - ## Route - - Mounted at `{prefix}/admin/comments`. - """ - - use PhoenixKitWeb, :live_view - - alias PhoenixKit.Modules.Comments - alias PhoenixKit.Settings - alias PhoenixKit.Utils.Routes - - @impl true - def mount(_params, _session, socket) do - if Comments.enabled?() do - project_title = Settings.get_project_title() - - socket = - socket - |> assign(:page_title, "Comments") - |> assign(:project_title, project_title) - |> assign(:comments, []) - |> assign(:total, 0) - |> assign(:total_pages, 1) - |> assign(:resource_context, %{}) - |> assign(:stats, Comments.comment_stats()) - |> assign(:selected_uuids, []) - |> assign_filter_defaults() - - {:ok, socket} - else - {:ok, - socket - |> put_flash(:error, "Comments 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_comments() - - {:noreply, socket} - end - - @impl true - def handle_event("filter", params, socket) do - combined_params = %{"page" => "1"} - - combined_params = - case Map.get(params, "search") do - %{"query" => query} -> Map.put(combined_params, "search", String.trim(query || "")) - _ -> combined_params - end - - combined_params = - case Map.get(params, "filter") do - filter_params when is_map(filter_params) -> Map.merge(combined_params, filter_params) - _ -> combined_params - end - - new_params = build_url_params(socket.assigns, combined_params) - {:noreply, push_patch(socket, to: Routes.path("/admin/comments?#{new_params}"))} - end - - @impl true - def handle_event("clear_filters", _params, socket) do - {:noreply, push_patch(socket, to: Routes.path("/admin/comments"))} - end - - @impl true - def handle_event("approve", %{"id" => id}, socket) do - case Comments.get_comment(id) do - nil -> - {:noreply, put_flash(socket, :error, "Comment not found")} - - comment -> - Comments.approve_comment(comment) - - {:noreply, - socket |> load_comments() |> reload_stats() |> put_flash(:info, "Comment approved")} - end - end - - @impl true - def handle_event("hide", %{"id" => id}, socket) do - case Comments.get_comment(id) do - nil -> - {:noreply, put_flash(socket, :error, "Comment not found")} - - comment -> - Comments.hide_comment(comment) - - {:noreply, - socket |> load_comments() |> reload_stats() |> put_flash(:info, "Comment hidden")} - end - end - - @impl true - def handle_event("delete", %{"id" => id}, socket) do - case Comments.get_comment(id) do - nil -> - {:noreply, put_flash(socket, :error, "Comment not found")} - - comment -> - Comments.delete_comment(comment) - - {:noreply, - socket |> load_comments() |> reload_stats() |> put_flash(:info, "Comment deleted")} - end - end - - @impl true - def handle_event("toggle_select", %{"uuid" => uuid}, socket) do - selected = socket.assigns.selected_uuids - - selected = - if uuid in selected, - do: List.delete(selected, uuid), - else: [uuid | selected] - - {:noreply, assign(socket, :selected_uuids, selected)} - end - - @impl true - def handle_event("bulk_action", %{"action" => action}, socket) do - uuids = socket.assigns.selected_uuids - - if uuids == [] do - {:noreply, put_flash(socket, :error, "No comments selected")} - else - case action do - "approve" -> - Comments.bulk_update_status(uuids, "published") - - {:noreply, - socket - |> assign(:selected_uuids, []) - |> load_comments() - |> reload_stats() - |> put_flash(:info, "Comments approved")} - - "hide" -> - Comments.bulk_update_status(uuids, "hidden") - - {:noreply, - socket - |> assign(:selected_uuids, []) - |> load_comments() - |> reload_stats() - |> put_flash(:info, "Comments hidden")} - - "delete" -> - Comments.bulk_update_status(uuids, "deleted") - - {:noreply, - socket - |> assign(:selected_uuids, []) - |> load_comments() - |> reload_stats() - |> put_flash(:info, "Comments deleted")} - - _ -> - {:noreply, socket} - end - end - end - - ## --- Private --- - - defp assign_filter_defaults(socket) do - socket - |> assign(:page, 1) - |> assign(:per_page, 20) - |> assign(:search, "") - |> assign(:filter_resource_type, nil) - |> assign(:filter_status, nil) - end - - defp apply_params(socket, params) do - socket - |> assign(:page, parse_int(params["page"], 1)) - |> assign(:search, params["search"] || "") - |> assign(:filter_resource_type, blank_to_nil(params["resource_type"])) - |> assign(:filter_status, blank_to_nil(params["status"])) - end - - defp load_comments(socket) do - result = - Comments.list_all_comments( - page: socket.assigns.page, - per_page: socket.assigns.per_page, - search: socket.assigns.search, - resource_type: socket.assigns.filter_resource_type, - status: socket.assigns.filter_status - ) - - resource_context = Comments.resolve_resource_context(result.comments) - - socket - |> assign(:comments, result.comments) - |> assign(:total, result.total) - |> assign(:total_pages, result.total_pages) - |> assign(:resource_context, resource_context) - end - - defp reload_stats(socket) do - assign(socket, :stats, Comments.comment_stats()) - end - - defp build_url_params(assigns, overrides) do - params = - %{} - |> maybe_put("page", Map.get(overrides, "page", to_string(assigns.page))) - |> maybe_put("search", Map.get(overrides, "search", assigns.search)) - |> maybe_put( - "resource_type", - Map.get(overrides, "resource_type", assigns.filter_resource_type) - ) - |> maybe_put("status", Map.get(overrides, "status", assigns.filter_status)) - - URI.encode_query(params) - end - - defp maybe_put(map, _key, nil), do: map - defp maybe_put(map, _key, ""), do: map - defp maybe_put(map, key, value), do: Map.put(map, key, value) - - defp parse_int(nil, default), do: default - - defp parse_int(str, default) when is_binary(str) do - case Integer.parse(str) do - {n, _} -> max(n, 1) - :error -> default - end - end - - defp blank_to_nil(nil), do: nil - defp blank_to_nil(""), do: nil - defp blank_to_nil(val), do: val - - defp resource_info(resource_context, comment) do - Map.get(resource_context, {comment.resource_type, comment.resource_uuid}) - end - - defp status_badge_class("published"), do: "badge badge-success badge-sm" - defp status_badge_class("pending"), do: "badge badge-warning badge-sm" - defp status_badge_class("hidden"), do: "badge badge-info badge-sm" - defp status_badge_class("deleted"), do: "badge badge-error badge-sm" - defp status_badge_class(_), do: "badge badge-ghost badge-sm" -end diff --git a/lib/modules/comments/web/index.html.heex b/lib/modules/comments/web/index.html.heex deleted file mode 100644 index c867af9fb..000000000 --- a/lib/modules/comments/web/index.html.heex +++ /dev/null @@ -1,301 +0,0 @@ -
- <.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 - - - - - <%!-- Statistics --%> -
-
-
Total
-
{@stats.total}
-
-
-
Published
-
{@stats.published}
-
-
-
Pending
-
{@stats.pending}
-
-
-
Hidden
-
{@stats.hidden}
-
-
-
Deleted
-
{@stats.deleted}
-
-
- - <%!-- Filters --%> -
- <.form for={%{}} phx-change="filter" class="flex flex-wrap gap-4 items-end"> -
- - -
- -
- - -
- -
- - -
- - - -
- - <%!-- Bulk Actions --%> - <%= if length(@selected_uuids) > 0 do %> -
- {length(@selected_uuids)} selected - - - -
- <% 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 %> - - <% end %> - <%= if comment.status != "hidden" do %> - - <% end %> - - - - <.table_default_header> - <.table_default_row hover={false}> - <.table_default_header_cell class="w-8"> - <.table_default_header_cell>Content - <.table_default_header_cell>Author - <.table_default_header_cell>Resource - <.table_default_header_cell>Status - <.table_default_header_cell>Date - <.table_default_header_cell>Actions - - - - <.table_default_body> - <%= for comment <- @comments do %> - <.table_default_row> - <.table_default_cell> - - - <.table_default_cell class="max-w-xs"> - <%= if comment.depth > 0 do %> -
- <.icon name="hero-arrow-uturn-right-mini" class="size-3" /> - Reply - <%= if comment.parent do %> - - — Re: {String.slice(comment.parent.content, 0..39)} - - <% end %> -
- <% end %> -
{String.slice(comment.content, 0..99)}
- - <.table_default_cell class="text-sm"> - <%= if comment.user do %> - {comment.user.email} - <% else %> - Deleted user - <% end %> - - <.table_default_cell> - - {comment.resource_type} - - <%= 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)} - - <% _ -> %> - - {String.slice(to_string(comment.resource_uuid), 0..7)} - - <% end %> - - <.table_default_cell> - - {comment.status} - - - <.table_default_cell class="text-sm text-base-content/70"> - {Calendar.strftime(comment.inserted_at, "%b %d, %Y")} - - <.table_default_cell> -
- <%= if comment.status != "published" do %> - - <% end %> - <%= if comment.status != "hidden" do %> - - <% end %> - -
- - - <% end %> - - - <% else %> -
- <.icon name="hero-chat-bubble-left" class="w-12 h-12 mx-auto mb-2 opacity-50" /> -

No comments found

-
- <% end %> - - <%!-- Pagination --%> - <%= if @total_pages > 1 do %> -
-
- <%= 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} - - <% end %> -
-
- <% end %> -
diff --git a/lib/modules/comments/web/settings.ex b/lib/modules/comments/web/settings.ex deleted file mode 100644 index 695f1b935..000000000 --- a/lib/modules/comments/web/settings.ex +++ /dev/null @@ -1,95 +0,0 @@ -defmodule PhoenixKit.Modules.Comments.Web.Settings do - @moduledoc """ - LiveView for Comments module settings management. - - Manages: - - `comments_enabled` toggle - - `comments_moderation` toggle - - `comments_max_depth` input - - `comments_max_length` input - - ## Route - - Mounted at `{prefix}/admin/settings/comments`. - """ - - use PhoenixKitWeb, :live_view - - alias PhoenixKit.Settings - - @impl true - def mount(_params, _session, socket) do - project_title = Settings.get_project_title() - - socket = - socket - |> assign(:page_title, "Comments Settings") - |> assign(:project_title, project_title) - |> assign(:saving, false) - |> load_settings() - - {:ok, socket} - end - - @impl true - def handle_event("save", params, socket) do - socket = assign(socket, :saving, true) - settings = Map.get(params, "settings", %{}) - - try do - results = - Enum.map(settings, fn {key, value} -> - Settings.update_setting(key, value) - end) - - socket = - if Enum.all?(results, fn - {:ok, _} -> true - _ -> false - end) do - socket - |> put_flash(:info, "Settings saved successfully") - |> load_settings() - else - put_flash(socket, :error, "Failed to save some settings") - end - - {:noreply, assign(socket, :saving, false)} - rescue - e -> - require Logger - Logger.error("Comment settings save failed: #{Exception.message(e)}") - - {:noreply, - assign(socket, :saving, false) - |> put_flash(:error, "Something went wrong. Please try again.")} - end - end - - @impl true - def handle_event("reset_defaults", _params, socket) do - defaults = %{ - "comments_enabled" => "false", - "comments_moderation" => "false", - "comments_max_depth" => "10", - "comments_max_length" => "10000" - } - - Enum.each(defaults, fn {key, value} -> - Settings.update_setting(key, value) - end) - - {:noreply, - socket - |> put_flash(:info, "Settings reset to defaults") - |> load_settings()} - end - - defp load_settings(socket) do - socket - |> assign(:comments_enabled, Settings.get_setting("comments_enabled", "false")) - |> assign(:comments_moderation, Settings.get_setting("comments_moderation", "false")) - |> assign(:comments_max_depth, Settings.get_setting("comments_max_depth", "10")) - |> assign(:comments_max_length, Settings.get_setting("comments_max_length", "10000")) - end -end diff --git a/lib/modules/comments/web/settings.html.heex b/lib/modules/comments/web/settings.html.heex deleted file mode 100644 index ebc7bbedf..000000000 --- a/lib/modules/comments/web/settings.html.heex +++ /dev/null @@ -1,135 +0,0 @@ -
- <.admin_page_header - back={PhoenixKit.Utils.Routes.path("/admin/settings")} - title="Comments Settings" - subtitle="Configure the standalone comments module" - /> - - <%!-- Settings Form --%> -
- <.form for={%{}} phx-submit="save"> - <%!-- Module Status --%> -
-
-

- <.icon name="hero-cog-6-tooth" class="w-6 h-6" /> Module Status -

- -
- -
-
-
- - <%!-- Moderation --%> -
-
-

- <.icon name="hero-shield-check" class="w-6 h-6" /> Moderation -

- -
- -
-
-
- - <%!-- Limits --%> -
-
-

- <.icon name="hero-adjustments-horizontal" class="w-6 h-6" /> Limits -

- -
-
- - - -
- -
- - - -
-
-
-
- - <%!-- Action Buttons --%> -
- - -
- -
-
diff --git a/lib/phoenix_kit/module_registry.ex b/lib/phoenix_kit/module_registry.ex index 3593dc2e1..53b05c815 100644 --- a/lib/phoenix_kit/module_registry.ex +++ b/lib/phoenix_kit/module_registry.ex @@ -402,7 +402,6 @@ defmodule PhoenixKit.ModuleRegistry do defp internal_modules do [ PhoenixKit.Modules.Billing, - PhoenixKit.Modules.Comments, PhoenixKit.Modules.Connections, PhoenixKit.Modules.DB, PhoenixKit.Modules.Languages, diff --git a/lib/phoenix_kit_web/components/admin_nav.ex b/lib/phoenix_kit_web/components/admin_nav.ex index 0f7cec474..e9b7a5ee9 100644 --- a/lib/phoenix_kit_web/components/admin_nav.ex +++ b/lib/phoenix_kit_web/components/admin_nav.ex @@ -136,8 +136,6 @@ defmodule PhoenixKitWeb.Components.AdminNav do <.icon name="hero-cube" class="w-5 h-5" /> <% "ticket" -> %> <.icon name="hero-chat-bubble-left-right" class="w-5 h-5" /> - <% "comments" -> %> - <.icon name="hero-chat-bubble-left-right" class="w-5 h-5" /> <% "ai" -> %> <.icon name="hero-cpu-chip" class="w-5 h-5" /> <% "language" -> %> diff --git a/lib/phoenix_kit_web/integration.ex b/lib/phoenix_kit_web/integration.ex index 947d2abd6..a2363346e 100644 --- a/lib/phoenix_kit_web/integration.ex +++ b/lib/phoenix_kit_web/integration.ex @@ -559,13 +559,6 @@ defmodule PhoenixKitWeb.Integration do live "/admin/db/:schema/:table", PhoenixKit.Modules.DB.Web.Show, :show, as: :db_show - # Comments module routes - live "/admin/comments", PhoenixKit.Modules.Comments.Web.Index, :index, - as: :comments_index - - live "/admin/settings/comments", PhoenixKit.Modules.Comments.Web.Settings, :settings, - as: :comments_settings - # Shop admin routes live "/admin/shop", PhoenixKit.Modules.Shop.Web.Dashboard, :index, as: :shop_dashboard diff --git a/mix.lock b/mix.lock index 63a96f9c6..75d677df3 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "bandit": {:hex, :bandit, "1.10.3", "1e5d168fa79ec8de2860d1b4d878d97d4fbbe2fdbe7b0a7d9315a4359d1d4bb9", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "99a52d909c48db65ca598e1962797659e3c0f1d06e825a50c3d75b74a5e2db18"}, + "bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, "beamlab_countries": {:hex, :beamlab_countries, "1.0.6", "c6366b518f6b3d21e9b872e2e4c375d4cbb12fe57b84d348429b770da25a2315", [:mix], [{:yaml_elixir, "~> 2.12", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "91305fb9c294674b2ad241a0a3f3ccf334417657363fd16d5a527ddbffcd215d"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, @@ -36,10 +36,10 @@ "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "httpoison": {:hex, :httpoison, "2.3.0", "10eef046405bc44ba77dc5b48957944df8952cc4966364b3cf6aa71dce6de587", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d388ee70be56d31a901e333dbcdab3682d356f651f93cf492ba9f06056436a2c"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "igniter": {:hex, :igniter, "0.7.6", "687d622c735e020f13cf480c83d0fce1cc899f4fbed547f5254b960ea82d3525", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "424f41a41273fce0f7424008405ee073b5bd06359ca9396e841f83a669c01619"}, + "igniter": {:hex, :igniter, "0.7.7", "08bae07b7b610100bc7c676e6b18130fe12bb90617982023cc798346879c2c5f", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "caeb1227887362b22038ff8419a7e6ddd3888f3d7e6cffacb14c73abbce17600"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"}, - "leaf": {:hex, :leaf, "0.2.5", "e5fa6053a8a1f50681943310a92ae12c8b6f23ca6aba1d1d0fe14b25abe31957", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "1b50cbcab097ae735c201e93f1d404ec2203356800b3f9fbe37d2bd6ca4ca796"}, + "leaf": {:hex, :leaf, "0.2.6", "eb8e56e5d08ae3d982719f0694ffefa37e3120f88ce4a7068e86cc90f8bed4d2", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "08bcd9cd2851d6fbe5eb5f4fafa2a08403972e3fe135f1b89fac7f6e3e58d6f4"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, diff --git a/test/phoenix_kit/module_registry_test.exs b/test/phoenix_kit/module_registry_test.exs index f35f78811..31f56b4a6 100644 --- a/test/phoenix_kit/module_registry_test.exs +++ b/test/phoenix_kit/module_registry_test.exs @@ -19,7 +19,6 @@ defmodule PhoenixKit.ModuleRegistryTest do # so this test doesn't break when modules are extracted or added. expected = [ PhoenixKit.Modules.Billing, - PhoenixKit.Modules.Comments, PhoenixKit.Modules.Connections, PhoenixKit.Modules.DB, PhoenixKit.Modules.Languages, @@ -163,7 +162,7 @@ defmodule PhoenixKit.ModuleRegistryTest do test "returns a list of permission metadata maps" do metadata = ModuleRegistry.all_permission_metadata() assert is_list(metadata) - assert length(metadata) >= 14 + assert length(metadata) >= 13 for meta <- metadata do assert is_map(meta) @@ -183,10 +182,10 @@ defmodule PhoenixKit.ModuleRegistryTest do end describe "all_feature_keys/0" do - test "returns sorted list of 14 feature keys" do + test "returns sorted list of 13 feature keys" do keys = ModuleRegistry.all_feature_keys() assert is_list(keys) - assert length(keys) == 14 + assert length(keys) == 13 assert keys == Enum.sort(keys) end @@ -212,7 +211,7 @@ defmodule PhoenixKit.ModuleRegistryTest do test "returns a map of key => {module, :enabled?}" do checks = ModuleRegistry.feature_enabled_checks() assert is_map(checks) - assert map_size(checks) >= 14 + assert map_size(checks) >= 13 for {key, {mod, fun}} <- checks do assert is_binary(key) diff --git a/test/phoenix_kit/module_test.exs b/test/phoenix_kit/module_test.exs index e3509c85a..d2c6a9990 100644 --- a/test/phoenix_kit/module_test.exs +++ b/test/phoenix_kit/module_test.exs @@ -5,7 +5,6 @@ defmodule PhoenixKit.ModuleTest do @all_internal_modules [ PhoenixKit.Modules.Billing, - PhoenixKit.Modules.Comments, PhoenixKit.Modules.Connections, PhoenixKit.Modules.DB, PhoenixKit.Modules.Languages,