firehose/blogex/lib/blogex/components.ex
2026-03-19 22:14:19 +00:00

168 lines
4.1 KiB
Elixir

defmodule Blogex.Components do
@moduledoc """
Phoenix function components for rendering blog content.
These are unstyled building blocks — the host app wraps them
in its own layout and applies its own CSS.
## Usage in a LiveView or template:
import Blogex.Components
<.post_index blog={@blog_module} posts={@posts} />
<.post_show post={@post} />
<.tag_list tags={@tags} base_path={@base_path} />
"""
use Phoenix.Component
@doc """
Renders a list of post previews.
## Attributes
* `:posts` - list of `%Blogex.Post{}` structs (required)
* `:base_path` - base URL path for post links (required)
"""
attr :posts, :list, required: true
attr :base_path, :string, required: true
def post_index(assigns) do
~H"""
<div class="blogex-post-index">
<article :for={post <- @posts} class="blogex-post-preview">
<header>
<h2>
<a href={"#{@base_path}/#{post.id}"}>{post.title}</a>
</h2>
<.post_meta post={post} base_path={@base_path} />
</header>
<p class="blogex-post-description">{post.description}</p>
</article>
</div>
"""
end
@doc """
Renders a full blog post.
## Attributes
* `:post` - a `%Blogex.Post{}` struct (required)
* `:base_path` - base URL path for tag links (required)
"""
attr :post, :map, required: true
attr :base_path, :string, required: true
def post_show(assigns) do
~H"""
<article class="blogex-post">
<header class="blogex-post-header">
<h1>{@post.title}</h1>
<.post_meta post={@post} base_path={@base_path} />
</header>
<div class="blogex-post-body">
{Phoenix.HTML.raw(@post.body)}
</div>
</article>
"""
end
@doc """
Renders post metadata (date, author, tags).
## Attributes
* `:post` - a `%Blogex.Post{}` struct (required)
* `:base_path` - base URL path for tag links (required)
* `:current_tag` - currently selected tag for highlighting (optional)
"""
attr :post, :map, required: true
attr :base_path, :string, required: true
attr :current_tag, :string, default: nil
def post_meta(assigns) do
~H"""
<div class="blogex-post-meta">
<time datetime={Date.to_iso8601(@post.date)}>
{Calendar.strftime(@post.date, "%B %d, %Y")}
</time>
<span :if={@post.author} class="blogex-post-author">
by {@post.author}
</span>
<a
:for={tag <- @post.tags}
href={"#{@base_path}/tag/#{tag}"}
class={["blogex-tag-link", tag == @current_tag && "blogex-tag-active"]}
>
{tag}
</a>
</div>
"""
end
@doc """
Renders a tag cloud / tag list with links.
## Attributes
* `:tags` - list of tag strings (required)
* `:base_path` - base URL path (required)
* `:current_tag` - currently selected tag for highlighting (optional)
"""
attr :tags, :list, required: true
attr :base_path, :string, required: true
attr :current_tag, :string, default: nil
def tag_list(assigns) do
~H"""
<nav class="blogex-tag-list">
<a
:for={tag <- @tags}
href={"#{@base_path}/tag/#{tag}"}
class={["blogex-tag-link", tag == @current_tag && "blogex-tag-active"]}
>
{tag}
</a>
</nav>
"""
end
@doc """
Renders pagination controls.
## Attributes
* `:page` - current page number (required)
* `:total_pages` - total number of pages (required)
* `:base_path` - base URL path (required)
"""
attr :page, :integer, required: true
attr :total_pages, :integer, required: true
attr :base_path, :string, required: true
def pagination(assigns) do
~H"""
<nav :if={@total_pages > 1} class="blogex-pagination">
<a
:if={@page > 1}
href={"#{@base_path}?page=#{@page - 1}"}
class="blogex-pagination-prev"
>
&larr; Newer
</a>
<span class="blogex-pagination-info">
Page {@page} of {@total_pages}
</span>
<a
:if={@page < @total_pages}
href={"#{@base_path}?page=#{@page + 1}"}
class="blogex-pagination-next"
>
Older &rarr;
</a>
</nav>
"""
end
end