hand written admin improvements pt. 1

This commit is contained in:
sloane 2025-04-12 17:55:17 -04:00
parent 4f3e1d3a8d
commit 1675efb514
Signed by: sloanelybutsurely
SSH key fingerprint: SHA256:8SBnwhl+RY3oEyQxy1a9wByPzxWM0x+/Ejc+sIlY5qQ
12 changed files with 203 additions and 117 deletions

View file

@ -37,6 +37,12 @@ defmodule Core.Posts do
|> Core.Repo.all()
end
def list_posts(kind, params \\ %{}) do
Post.Query.base()
|> Post.Query.where_kind(kind)
|> Flop.validate_and_run(params, for: Schema.Post)
end
def list_published_posts(kind, params \\ %{}) do
Post.Query.base()
|> Post.Query.published()

View file

@ -154,46 +154,6 @@ defmodule Web.CoreComponents do
Module.concat(Timex.Format.DateTime.Formatters, :string.titlecase("#{formatter}"))
end
attr :id, :string, required: true
attr :posts, :any, required: true
slot :inner_block, required: true
def post_list(assigns) do
~H"""
<ol id={@id} phx-update={if is_struct(@posts, Phoenix.LiveView.LiveStream), do: "phx-update"}>
<li
:for={{dom_id, item} <- normalize_posts(@posts)}
id={dom_id}
class="flex flex-row justify-between"
>
<span>{render_slot(@inner_block, item)}</span>
<span>
<%= if item.deleted_at do %>
deleted
<% else %>
<%= if item.published_at do %>
<%= case item.kind do %>
<% :blog -> %>
<.timex value={item.published_at} format="{YYYY}-{0M}-{0D}" />
<% :status -> %>
<.timex value={item.published_at} format="{relative}" formatter={:relative} />
<% end %>
<% else %>
draft
<% end %>
<% end %>
</span>
</li>
</ol>
"""
end
defp normalize_posts(%Phoenix.LiveView.LiveStream{} = stream), do: stream
defp normalize_posts(posts) when is_list(posts),
do: Enum.with_index(posts, &{"#{&1.kind}-#{&2}", &1})
@doc """
Renders markdown content as HTML.
@ -251,4 +211,37 @@ defmodule Web.CoreComponents do
</div>
"""
end
attr :id, :string, required: true
attr :stream, Phoenix.LiveView.LiveStream, required: true
attr :class, :string, default: nil
attr :rest, :global
slot :col do
attr :label, :string
attr :class, :string
end
def table(assigns) do
~H"""
<table id={@id} class={["border-collapse", @class]} {@rest}>
<thead>
<tr>
<%= for col <- @col do %>
<th class="border p-2">{col[:label]}</th>
<% end %>
</tr>
</thead>
<tbody id={"#{@id}-stream"} phx-update="stream">
<%= for {dom_id, item} <- @stream do %>
<tr id={dom_id}>
<%= for col <- @col do %>
<td class={["border p-2", col[:class]]}>{render_slot(col, item)}</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
"""
end
end

View file

@ -8,7 +8,7 @@
<section :if={not is_nil(@current_user)} class="ml-2">
<nav>
<ul class="flex flex-row gap-x-2">
<li><.link navigate={~p"/admin"}>admin</.link></li>
<li><.link navigate={~p"/admin/writing"}>admin</.link></li>
</ul>
</nav>
</section>

View file

@ -2,10 +2,4 @@ defmodule Web.BlogHTML do
use Web, :html
embed_templates "blog_html/*"
def blog_path(%Schema.Post{} = blog) do
if date = Core.Posts.publish_date(blog) do
~p"/blog/#{date.year}/#{date.month}/#{date.day}/#{blog.slug}"
end
end
end

View file

@ -11,7 +11,7 @@
<li>
<article class="h-entry">
<.link
navigate={blog_path(blog)}
navigate={Web.Paths.public_blog_path(blog)}
class="flex justify-between items-center hover:bg-gray-50 -mx-2 px-2 py-1 rounded"
>
<h3 class="p-name u-url">{blog.title}</h3>

View file

@ -13,7 +13,7 @@
<li>
<article class="h-entry">
<.link
navigate={Web.BlogHTML.blog_path(blog)}
navigate={Web.Paths.public_blog_path(blog)}
class="flex justify-between items-center hover:bg-gray-50 -mx-2 px-2 py-1 rounded"
>
<h3 class="p-name u-url">{blog.title}</h3>

View file

@ -2,42 +2,64 @@ defmodule Web.AdminDashboardLive do
use Web, :live_view
def mount(_params, _session, socket) do
statuses = Core.Posts.get_all_recent_statuses()
blogs = Core.Posts.get_all_recent_blogs()
socket =
socket
|> stream(:statuses, statuses)
|> stream(:blogs, blogs)
{:ok, socket}
end
def render(assigns) do
def handle_params(params, _uri, socket) do
kind = socket.assigns.live_action
{:ok, {posts, meta}} = Core.Posts.list_posts(kind, params)
socket =
socket
|> assign(
kind: kind,
meta: meta
)
|> stream(:posts, posts, reset: true)
{:noreply, socket}
end
attr :post, Schema.Post, required: true
defp post_status(%{post: %{published_at: nil, deleted_at: nil}} = assigns) do
~H"""
<div class="flex flex-col gap-y-4">
<h1 class="font-bold text-2xl">dashboard</h1>
draft
"""
end
<section>
<header class="flex flex-row justify-between">
<h2 class="font-bold text-xl">recent statuses</h2>
<.link navigate={~p"/admin/posts/new?kind=status"}>new status</.link>
</header>
<.post_list :let={status} id="recent-statuses" posts={@streams.statuses}>
<.link navigate={~p"/admin/posts/#{status}"}>{status.body}</.link>
</.post_list>
</section>
defp post_status(%{post: %{published_at: _, deleted_at: nil}} = assigns) do
~H"""
published
"""
end
<section>
<header class="flex flex-row justify-between">
<h2 class="font-bold text-xl">recent blogs</h2>
<.link navigate={~p"/admin/posts/new?kind=blog"}>new blog</.link>
</header>
<.post_list :let={blog} id="recent-blogs" posts={@streams.blogs}>
<.link navigate={~p"/admin/posts/#{blog}"}>{blog.title}</.link>
</.post_list>
</section>
defp post_status(assigns) do
~H"""
deleted
"""
end
attr :post, Schema.Post, required: true
defp post_actions(assigns) do
~H"""
<div class="flex flex-row gap-x-1">
<.link navigate={~p"/admin/posts/#{@post}"}>edit</.link>
<.link
:if={@post.published_at && is_nil(@post.deleted_at)}
navigate={Web.Paths.public_post_path(@post)}
>
view
</.link>
</div>
"""
end
defp build_path(:blog, meta),
do: Flop.Phoenix.build_path(~p"/admin/writing", meta, for: Schema.Post)
defp build_path(:status, meta),
do: Flop.Phoenix.build_path(~p"/admin/microblog", meta, for: Schema.Post)
end

View file

@ -0,0 +1,59 @@
<div class="flex flex-col py-4 px-6">
<header class="mb-4">
<nav>
<ul class="flex flex-row gap-x-4">
<li>
<.link class={[@kind == :blog && "underline"]} patch={~p"/admin/writing"}>
writing
</.link>
</li>
<li>
<.link class={[@kind == :status && "underline"]} patch={~p"/admin/microblog"}>
microblog
</.link>
</li>
</ul>
</nav>
</header>
<main class="flex flex-col">
<.link class="mb-4" navigate={~p"/admin/posts/new?kind=#{@kind}"}>new {@kind}</.link>
<%= case @kind do %>
<% :blog -> %>
<.table id="blog-posts" stream={@streams.posts}>
<:col :let={blog} label="title">{blog.title}</:col>
<:col :let={blog} label="status">
<.post_status post={blog} />
</:col>
<:col :let={blog}>
<.post_actions post={blog} />
</:col>
</.table>
<% :status -> %>
<.table id="status-posts" stream={@streams.posts}>
<:col :let={status} label="content">
{status.body}
</:col>
<:col :let={status} label="status">
<.post_status post={status} />
</:col>
<:col :let={status}>
<.post_actions post={status} />
</:col>
</.table>
<% end %>
<footer class="flex flex-row justify-between mt-2">
<%= if @meta.has_previous_page? do %>
<.link patch={build_path(@kind, Flop.to_previous_cursor(@meta))}>prev</.link>
<% else %>
<div />
<% end %>
<%= if @meta.has_next_page? do %>
<.link patch={build_path(@kind, Flop.to_next_cursor(@meta))}>next</.link>
<% else %>
<div />
<% end %>
</footer>
</main>
</div>

View file

@ -116,44 +116,6 @@ defmodule Web.AdminPostLive do
{:noreply, socket}
end
def render(assigns) do
~H"""
<main>
<header>
<h1>{page_title(@post, @live_action)}</h1>
</header>
<.form for={@form} phx-change="validate" phx-submit="save">
<.input :if={@post.kind == :blog} type="text" field={@form[:title]} />
<.input
:if={@post.kind == :blog}
type="text"
field={@form[:slug]}
disabled={not update_slug?(@post)}
/>
<.input type="textarea" field={@form[:body]} />
<.button type="submit">save</.button>
</.form>
<%= if @live_action == :edit do %>
<div>
<%= if @post.published_at do %>
<.button phx-click="unpublish">unpublish</.button>
<% else %>
<.button phx-click="publish">publish</.button>
<% end %>
<%= if @post.deleted_at do %>
<.button phx-click="undelete">undelete</.button>
<% else %>
<.button phx-click="delete">delete</.button>
<% end %>
</div>
<% end %>
</main>
"""
end
defp page_title(%Schema.Post{kind: :blog}, :new), do: "new blog"
defp page_title(%Schema.Post{kind: :status}, :new), do: "new status"
defp page_title(%Schema.Post{kind: :blog}, :edit), do: "edit blog"

View file

@ -0,0 +1,33 @@
<main>
<header>
<h1>{page_title(@post, @live_action)}</h1>
</header>
<.form for={@form} phx-change="validate" phx-submit="save">
<.input :if={@post.kind == :blog} type="text" field={@form[:title]} />
<.input
:if={@post.kind == :blog}
type="text"
field={@form[:slug]}
disabled={not update_slug?(@post)}
/>
<.input type="textarea" field={@form[:body]} />
<.button type="submit">save</.button>
</.form>
<%= if @live_action == :edit do %>
<div>
<%= if @post.published_at do %>
<.button phx-click="unpublish">unpublish</.button>
<% else %>
<.button phx-click="publish">publish</.button>
<% end %>
<%= if @post.deleted_at do %>
<.button phx-click="undelete">undelete</.button>
<% else %>
<.button phx-click="delete">delete</.button>
<% end %>
</div>
<% end %>
</main>

16
lib/web/paths.ex Normal file
View file

@ -0,0 +1,16 @@
defmodule Web.Paths do
use Web, :html
def public_post_path(%Schema.Post{kind: :status} = status), do: public_status_path(status)
def public_post_path(%Schema.Post{kind: :blog} = blog), do: public_blog_path(blog)
def public_status_path(%Schema.Post{kind: :status} = status) do
~p"/status/#{status}"
end
def public_blog_path(%Schema.Post{kind: :blog} = blog) do
if date = Core.Posts.publish_date(blog) do
~p"/blog/#{date.year}/#{date.month}/#{date.day}/#{blog.slug}"
end
end
end

View file

@ -31,7 +31,8 @@ defmodule Web.Router do
live_session :require_authenticated_user, on_mount: [{Web.UserAuth, :ensure_authenticated}] do
live "/users/settings", UserSettingsLive, :edit
live "/", AdminDashboardLive
live "/writing", AdminDashboardLive, :blog
live "/microblog", AdminDashboardLive, :status
live "/posts/new", AdminPostLive, :new
live "/posts/:post_id", AdminPostLive, :edit