# `PhoenixKit.Modules.Posts`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1)

Context for managing posts, likes, tags, and groups.

Provides complete API for the social posts system including CRUD operations,
counter cache management, tag assignment, and group organization.
Comments are now handled by the standalone `PhoenixKit.Modules.Comments` module.

## Features

- **Post Management**: Create, update, delete, publish, schedule posts
- **Like System**: Like/unlike posts, check like status
- **Comment System**: Nested threaded comments with unlimited depth
- **Tag System**: Hashtag categorization with auto-slugification
- **Group System**: User collections for organizing posts
- **Media Attachments**: Multiple images per post with ordering
- **Publishing**: Draft/public/unlisted/scheduled status management
- **Analytics**: View tracking (future feature)

## Examples

    # Create a post
    {:ok, post} = Posts.create_post(user_uuid, %{
      title: "My First Post",
      content: "Hello world!",
      type: "post",
      status: "draft"
    })

    # Publish a post
    {:ok, post} = Posts.publish_post(post)

    # Like a post
    {:ok, like} = Posts.like_post(post.uuid, user_uuid)

    # Add a comment
    {:ok, comment} = Posts.create_comment(post.uuid, user_uuid, %{
      content: "Great post!"
    })

    # Create a group
    {:ok, group} = Posts.create_group(user_uuid, %{
      name: "Travel Photos",
      description: "My adventures"
    })

# `add_mention_to_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1576)

Adds a mention to a post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `user_uuid` - User UUID (UUIDv7 string) to mention
- `mention_type` - "contributor" or "mention" (default: "mention")

## Examples

    iex> add_mention_to_post("018e3c4a-...", "019145a1-...", "contributor")
    {:ok, %PostMention{}}

# `add_post_to_group`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1355)

Adds a post to a group.

Increments the group's post counter.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `group_uuid` - Group UUID (UUIDv7 string)
- `opts` - Options
  - `:position` - Display position (default: 0)

## Examples

    iex> add_post_to_group("018e3c4a-...", "018e3c4a-...")
    {:ok, %PostGroupAssignment{}}

# `add_posts_to_group`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1396)

Adds multiple posts to a group in a single transaction.

Uses `on_conflict: :nothing` to skip duplicates, then increments the group's
`post_count` by the actual number of newly inserted rows.

## Parameters

- `post_uuids` - List of Post UUIDs (UUIDv7 strings)
- `group_uuid` - Group UUID (UUIDv7 string)
- `opts` - Options
  - `:position` - Position in group (default: 0)

## Examples

    iex> add_posts_to_group(["018e3c4a-...", "018e3c4b-..."], "018e3c4a-...")
    {:ok, 2}

# `add_tags_to_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1132)

Adds tags to a post.

Creates tags if they don't exist, then assigns them to the post.
Updates usage counters for tags.

## Parameters

- `post` - Post to tag
- `tag_names` - List of tag names

## Examples

    iex> add_tags_to_post(post, ["elixir", "phoenix"])
    {:ok, [%PostTag{}, %PostTag{}]}

# `attach_media`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1662)

Attaches media to a post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `file_uuid` - File UUID (UUIDv7 string, from PhoenixKit.Modules.Storage)
- `opts` - Options
  - `:position` - Display position (default: 1)
  - `:caption` - Image caption

## Examples

    iex> attach_media("018e3c4a-...", "018e3c4a-...", position: 1)
    {:ok, %PostMedia{}}

# `create_group`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1222)

Creates a user group.

## Parameters

- `user_uuid` - Owner UUID (UUIDv7 string)
- `attrs` - Group attributes (name, description, etc.)

## Examples

    iex> create_group("019145a1-...", %{name: "Travel Photos"})
    {:ok, %PostGroup{}}

# `create_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L245)

Creates a new post.

## Parameters

- `user_uuid` - Owner UUID (UUIDv7 string)
- `attrs` - Post attributes (title, content, type, status, etc.)

## Examples

    iex> create_post("019145a1-...", %{title: "Test", content: "Content", type: "post"})
    {:ok, %Post{}}

    iex> create_post("019145a1-...", %{title: "", content: ""})
    {:error, %Ecto.Changeset{}}

# `decrement_comment_count`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L721)

Decrements the comment counter for a post.

## Examples

    iex> decrement_comment_count(post)
    {1, nil}

# `decrement_dislike_count`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L695)

Decrements the dislike counter for a post.

## Examples

    iex> decrement_dislike_count(post)
    {1, nil}

# `decrement_like_count`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L669)

Decrements the like counter for a post.

## Examples

    iex> decrement_like_count(post)
    {1, nil}

# `delete_group`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1273)

Deletes a group.

## Parameters

- `group` - Group to delete

## Examples

    iex> delete_group(group)
    {:ok, %PostGroup{}}

# `delete_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L302)

Deletes a post and all related data (cascades to media, likes, comments, etc.).

## Parameters

- `post` - Post struct to delete

## Examples

    iex> delete_post(post)
    {:ok, %Post{}}

    iex> delete_post(post)
    {:error, %Ecto.Changeset{}}

# `detach_media`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1689)

Detaches media from a post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `file_uuid` - File UUID (UUIDv7 string)

## Examples

    iex> detach_media("018e3c4a-...", "018e3c4a-...")
    {:ok, %PostMedia{}}

# `detach_media_by_uuid`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1708)

Detaches media from a post by PostMedia ID.

## Parameters

- `media_uuid` - PostMedia record UUID (UUIDv7 string)

## Examples

    iex> detach_media_by_uuid("018e3c4a-...")
    {:ok, %PostMedia{}}

# `disable_system`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L113)

Disables the Posts module.

## Examples

    iex> disable_system()
    {:ok, %Setting{}}

# `dislike_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L903)

User dislikes a post.

Creates a dislike record and increments the post's dislike counter.
Returns error if user has already disliked the post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `user_uuid` - User UUID (UUIDv7 string)

## Examples

    iex> dislike_post("018e3c4a-...", "019145a1-...")
    {:ok, %PostDislike{}}

    iex> dislike_post("018e3c4a-...", "019145a1-...")  # Already disliked
    {:error, %Ecto.Changeset{}}

# `draft_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L611)

Reverts a post to draft status.

## Examples

    iex> draft_post(post)
    {:ok, %Post{status: "draft"}}

# `enable_system`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L100)

Enables the Posts module.

## Examples

    iex> enable_system()
    {:ok, %Setting{}}

# `enabled?`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L87)

Checks if the Posts module is enabled.

## Examples

    iex> enabled?()
    true

# `find_or_create_tag`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1082)

Finds or creates a tag by name.

Automatically generates slug from name. Returns existing tag if slug already exists.

## Parameters

- `name` - Tag name (e.g., "Web Development")

## Examples

    iex> find_or_create_tag("Web Development")
    {:ok, %PostTag{name: "Web Development", slug: "web-development"}}

    iex> find_or_create_tag("web development")  # Same slug
    {:ok, %PostTag{name: "Web Development"}}  # Returns existing

# `get_config`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L126)

Gets the current Posts module configuration and stats.

## Examples

    iex> get_config()
    %{enabled: true, total_posts: 42, published_posts: 30, ...}

# `get_featured_image`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1819)

Gets the featured image for a post (PostMedia with position 1).

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)

## Examples

    iex> get_featured_image("018e3c4a-...")
    %PostMedia{position: 1}

    iex> get_featured_image("018e3c4a-...")
    nil

# `get_group`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1299)

Gets a single group by ID with optional preloads.

Returns `nil` if group not found.

## Parameters

- `id` - Group ID (UUIDv7)
- `opts` - Options
  - `:preload` - List of associations to preload (e.g., [:user, :posts])

## Examples

    iex> get_group("018e3c4a-...")
    %PostGroup{}

    iex> get_group("018e3c4a-...", preload: [:user])
    %PostGroup{user: %User{}}

    iex> get_group("nonexistent")
    nil

# `get_group!`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1330)

Gets a single group by ID with optional preloads.

Raises `Ecto.NoResultsError` if group not found.

## Parameters

- `id` - Group ID (UUIDv7)
- `opts` - Options
  - `:preload` - List of associations to preload (e.g., [:user, :posts])

## Examples

    iex> get_group!("018e3c4a-...")
    %PostGroup{}

    iex> get_group!("018e3c4a-...", preload: [:user])
    %PostGroup{user: %User{}}

    iex> get_group!("nonexistent")
    ** (Ecto.NoResultsError)

# `get_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L358)

Gets a single post by ID with optional preloads.

Returns `nil` if post not found.

## Parameters

- `id` - Post ID (UUIDv7)
- `opts` - Options
  - `:preload` - List of associations to preload

## Examples

    iex> get_post("018e3c4a-...")
    %Post{}

    iex> get_post("018e3c4a-...", preload: [:user, :media, :tags])
    %Post{user: %User{}, media: [...], tags: [...]}

    iex> get_post("nonexistent")
    nil

# `get_post!`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L328)

Gets a single post by ID with optional preloads.

Raises `Ecto.NoResultsError` if post not found.

## Parameters

- `id` - Post ID (UUIDv7)
- `opts` - Options
  - `:preload` - List of associations to preload

## Examples

    iex> get_post!("018e3c4a-...")
    %Post{}

    iex> get_post!("018e3c4a-...", preload: [:user, :media, :tags])
    %Post{user: %User{}, media: [...], tags: [...]}

    iex> get_post!("nonexistent")
    ** (Ecto.NoResultsError)

# `get_post_by_slug`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L384)

Gets a single post by slug.

## Parameters

- `slug` - Post slug (e.g., "my-first-post")
- `opts` - Options
  - `:preload` - List of associations to preload

## Examples

    iex> get_post_by_slug("my-first-post")
    %Post{}

    iex> get_post_by_slug("nonexistent")
    nil

# `increment_comment_count`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L708)

Increments the comment counter for a post.

## Examples

    iex> increment_comment_count(post)
    {1, nil}

# `increment_dislike_count`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L682)

Increments the dislike counter for a post.

## Examples

    iex> increment_dislike_count(post)
    {1, nil}

# `increment_like_count`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L656)

Increments the like counter for a post.

## Examples

    iex> increment_like_count(post)
    {1, nil}

# `increment_view_count`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L734)

Increments the view counter for a post.

## Examples

    iex> increment_view_count(post)
    {1, nil}

# `like_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L762)

User likes a post.

Creates a like record and increments the post's like counter.
Returns error if user already liked the post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `user_uuid` - User UUID (UUIDv7 string)

## Examples

    iex> like_post("018e3c4a-...", "019145a1-...")
    {:ok, %PostLike{}}

    iex> like_post("018e3c4a-...", "019145a1-...")  # Already liked
    {:error, %Ecto.Changeset{}}

# `list_groups`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1497)

Lists all groups ordered by name.

## Parameters

- `opts` - Options
  - `:preload` - Associations to preload

## Examples

    iex> list_groups()
    [%PostGroup{}, ...]

# `list_popular_tags`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1200)

Lists popular tags by usage count.

## Parameters

- `limit` - Number of tags to return (default: 20)

## Examples

    iex> list_popular_tags(10)
    [%PostTag{usage_count: 150}, %PostTag{usage_count: 120}, ...]

# `list_post_dislikes`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1013)

Lists all dislikes for a post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `opts` - Options
  - `:preload` - Associations to preload

## Examples

    iex> list_post_dislikes("018e3c4a-...")
    [%PostDislike{}, ...]

    iex> list_post_dislikes("018e3c4a-...", preload: [:user])
    [%PostDislike{user: %User{}}, ...]

# `list_post_likes`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L872)

Lists all likes for a post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `opts` - Options
  - `:preload` - Associations to preload

## Examples

    iex> list_post_likes("018e3c4a-...")
    [%PostLike{}, ...]

    iex> list_post_likes("018e3c4a-...", preload: [:user])
    [%PostLike{user: %User{}}, ...]

# `list_post_media`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1729)

Lists media for a post (ordered by position).

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `opts` - Options
  - `:preload` - Associations to preload

## Examples

    iex> list_post_media("018e3c4a-...")
    [%PostMedia{}, ...]

# `list_post_mentions`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1634)

Lists mentioned users in a post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `opts` - Options
  - `:preload` - Associations to preload

## Examples

    iex> list_post_mentions("018e3c4a-...")
    [%PostMention{}, ...]

# `list_posts`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L421)

Lists posts with optional filtering and pagination.

## Parameters

- `opts` - Options
  - `:user_uuid` - Filter by user
  - `:status` - Filter by status (draft/public/unlisted/scheduled)
  - `:type` - Filter by type (post/snippet/repost)
  - `:search` - Search in title and content
  - `:page` - Page number (default: 1)
  - `:per_page` - Items per page (default: 20)
  - `:preload` - Associations to preload

## Examples

    iex> list_posts()
    [%Post{}, ...]

    iex> list_posts(status: "public", page: 1, per_page: 10)
    [%Post{}, ...]

    iex> list_posts(user_uuid: "018e3c4a-9f6b-7890-abcd-ef1234567890", type: "post")
    [%Post{}, ...]

# `list_posts_by_group`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1519)

Lists posts in a group.

## Parameters

- `group_uuid` - Group UUID (UUIDv7 string)
- `opts` - Options
  - `:preload` - Associations to preload

## Examples

    iex> list_posts_by_group("018e3c4a-...")
    [%Post{}, ...]

# `list_public_posts`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L469)

Lists public posts only.

## Parameters

- `opts` - See `list_posts/1` for options

## Examples

    iex> list_public_posts()
    [%Post{}, ...]

# `list_user_groups`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1470)

Lists user's groups.

## Parameters

- `user_uuid` - User UUID (UUIDv7 string)
- `opts` - Options
  - `:preload` - Associations to preload

## Examples

    iex> list_user_groups("019145a1-...")
    [%PostGroup{}, ...]

# `list_user_posts`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L451)

Lists user's posts.

## Parameters

- `user_uuid` - User UUID (UUIDv7 string)
- `opts` - See `list_posts/1` for options

## Examples

    iex> list_user_posts("019145a1-...")
    [%Post{}, ...]

# `on_comment_created`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1029)

Callback invoked by the Comments module when a comment is created on a post.
Increments the post's denormalized comment_count.

# `on_comment_deleted`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1040)

Callback invoked by the Comments module when a comment is deleted from a post.
Decrements the post's denormalized comment_count.

# `parse_hashtags`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1109)

Parses hashtags from text.

Extracts all hashtags (#word) from text and returns list of tag names.

## Parameters

- `text` - Text to parse

## Examples

    iex> parse_hashtags("Check out #elixir and #phoenix!")
    ["elixir", "phoenix"]

    iex> parse_hashtags("No tags here")
    []

# `post_disliked_by?`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L986)

Checks if a user has disliked a post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `user_uuid` - User UUID (UUIDv7 string)

## Examples

    iex> post_disliked_by?("018e3c4a-...", "019145a1-...")
    true

    iex> post_disliked_by?("018e3c4a-...", "019145a2-...")
    false

# `post_liked_by?`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L845)

Checks if a user has liked a post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `user_uuid` - User UUID (UUIDv7 string)

## Examples

    iex> post_liked_by?("018e3c4a-...", "019145a1-...")
    true

    iex> post_liked_by?("018e3c4a-...", "019145a2-...")
    false

# `process_scheduled_posts`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L628)

Processes scheduled posts that are ready to be published.

Finds all posts with status "scheduled" where scheduled_at <= now,
and publishes them. Returns list of published posts.

Should be called periodically (e.g., via Oban job every minute).

## Examples

    iex> process_scheduled_posts()
    {:ok, 2}

# `publish_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L488)

Publishes a post (makes it public).

Sets status to "public" and published_at to current time.

## Examples

    iex> publish_post(post)
    {:ok, %Post{status: "public"}}

# `remove_featured_image`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1842)

Removes the featured image from a post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)

## Examples

    iex> remove_featured_image("018e3c4a-...")
    {:ok, 1}

    iex> remove_featured_image("018e3c4a-...")
    {:ok, 0}

# `remove_mention_from_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1605)

Removes a mention from a post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `user_uuid` - User UUID (UUIDv7 string)

## Examples

    iex> remove_mention_from_post("018e3c4a-...", "019145a1-...")
    {:ok, %PostMention{}}

# `remove_post_from_group`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1439)

Removes a post from a group.

Decrements the group's post counter.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `group_uuid` - Group UUID (UUIDv7 string)

## Examples

    iex> remove_post_from_group("018e3c4a-...", "018e3c4a-...")
    {:ok, %PostGroupAssignment{}}

# `remove_tag_from_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1170)

Removes a tag from a post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `tag_uuid` - Tag UUID (UUIDv7 string)

## Examples

    iex> remove_tag_from_post("018e3c4a-...", "018e3c4a-...")
    {:ok, %PostTagAssignment{}}

# `reorder_groups`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1547)

Reorders user's groups.

Updates position field for multiple groups.

## Parameters

- `user_uuid` - User UUID (UUIDv7 string)
- `group_uuid_positions` - Map of group_uuid => position

## Examples

    iex> reorder_groups("019145a1-...", %{"group1" => 0, "group2" => 1})
    :ok

# `reorder_media`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1750)

Reorders media in a post.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `file_uuid_positions` - Map of file_uuid => position

## Examples

    iex> reorder_media("018e3c4a-...", %{"file1" => 1, "file2" => 2})
    :ok

# `resolve_comment_resources`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1053)

Resolves post titles and admin paths for a list of resource IDs.

Called by the Comments module to display resource context in the admin UI.
Returns a map of `resource_uuid => %{title: ..., path: ...}`.

# `schedule_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L517)

Schedules a post for future publishing.

Updates the post status to "scheduled" and creates an entry in the
scheduled jobs table for execution by the cron worker.

## Parameters

- `post` - Post to schedule
- `scheduled_at` - DateTime to publish at (must be in future)
- `attrs` - Additional attributes to update (title, content, etc.)
- `opts` - Options
  - `:created_by_uuid` - UUID of user scheduling the post

## Examples

    iex> schedule_post(post, ~U[2025-12-31 09:00:00Z])
    {:ok, %Post{status: "scheduled"}}

    iex> schedule_post(post, ~U[2025-12-31 09:00:00Z], %{title: "New Title"})
    {:ok, %Post{status: "scheduled", title: "New Title"}}

# `set_featured_image`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1784)

Sets the featured image for a post (PostMedia with position 1).

Replaces any existing featured image (position 1) with the new one.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `file_uuid` - File UUID (UUIDv7 string, from PhoenixKit.Modules.Storage)

## Examples

    iex> set_featured_image("018e3c4a-...", "018e3c4a-...")
    {:ok, %PostMedia{position: 1}}

# `undislike_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L948)

User removes dislike from a post.

Deletes the dislike record and decrements the post's dislike counter.
Returns error if dislike doesn't exist.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `user_uuid` - User UUID (UUIDv7 string)

## Examples

    iex> undislike_post("018e3c4a-...", "019145a1-...")
    {:ok, %PostDislike{}}

    iex> undislike_post("018e3c4a-...", "019145a1-...")  # Not disliked
    {:error, :not_found}

# `unlike_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L807)

User unlikes a post.

Deletes the like record and decrements the post's like counter.
Returns error if like doesn't exist.

## Parameters

- `post_uuid` - Post UUID (UUIDv7 string)
- `user_uuid` - User UUID (UUIDv7 string)

## Examples

    iex> unlike_post("018e3c4a-...", "019145a1-...")
    {:ok, %PostLike{}}

    iex> unlike_post("018e3c4a-...", "019145a1-...")  # Not liked
    {:error, :not_found}

# `unschedule_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L590)

Unschedules a post, reverting it to draft status.

Cancels any pending scheduled jobs for this post.

## Parameters

- `post` - Post to unschedule

## Examples

    iex> unschedule_post(post)
    {:ok, %Post{status: "draft"}}

# `update_group`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L1255)

Updates a group.

## Parameters

- `group` - Group to update
- `attrs` - Attributes to update

## Examples

    iex> update_group(group, %{name: "New Name"})
    {:ok, %PostGroup{}}

# `update_post`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.71/lib/modules/posts/posts.ex#L281)

Updates an existing post.

## Parameters

- `post` - Post struct to update
- `attrs` - Attributes to update

## Examples

    iex> update_post(post, %{title: "Updated Title"})
    {:ok, %Post{}}

    iex> update_post(post, %{title: ""})
    {:error, %Ecto.Changeset{}}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
