really basic new/edit post page and dashboard

This commit is contained in:
sloane 2025-03-29 16:15:28 -04:00
parent 2e7a81c121
commit 05f9dce1e5
Signed by: sloanelybutsurely
SSH key fingerprint: SHA256:8SBnwhl+RY3oEyQxy1a9wByPzxWM0x+/Ejc+sIlY5qQ
8 changed files with 205 additions and 1 deletions

View file

@ -19,6 +19,18 @@ defmodule Core.Posts do
|> Core.Repo.get!(id)
end
@recent_posts_count 10
def get_all_recent_blogs do
Post.Query.recent_blogs(@recent_posts_count)
|> Core.Repo.all()
end
def get_all_recent_statuses do
Post.Query.recent_statuses(@recent_posts_count)
|> Core.Repo.all()
end
def change_post(%Schema.Post{} = post \\ %Schema.Post{}, attrs) do
Post.content_changeset(post, attrs)
end
@ -35,6 +47,12 @@ defmodule Core.Posts do
|> Core.Repo.update()
end
def create_or_update_post(%Schema.Post{} = post, attrs) do
post
|> change_post(attrs)
|> Core.Repo.insert_or_update()
end
def publish_post(%Schema.Post{} = post, published_at \\ DateTime.utc_now()) do
post
|> Post.publish_changeset(published_at)

View file

@ -88,6 +88,28 @@ defmodule Core.Posts.Post do
where(query, [posts: p], p.kind == ^kind)
end
def blogs(query \\ base()) do
where_kind(query, :blog)
end
def statuses(query \\ base()) do
where_kind(query, :status)
end
def recent_blogs(query \\ base(), count) do
query
|> blogs()
|> default_order()
|> limit(^count)
end
def recent_statuses(query \\ base(), count) do
query
|> statuses()
|> default_order()
|> limit(^count)
end
def where_publish_date_and_slug(query \\ current(), publish_date, slug) do
where(
query,

View file

@ -10,7 +10,7 @@ defmodule Web.CoreComponents do
attr :name, :any
attr :label, :string, default: nil
attr :value, :any
attr :type, :string, default: "text", values: ~w[text password]
attr :type, :string, default: "text", values: ~w[text password textarea]
attr :field, FormField
attr :errors, :list, default: []
attr :rest, :global, include: ~w[disabled form pattern placeholder readonly required]
@ -34,6 +34,16 @@ defmodule Web.CoreComponents do
|> input()
end
def input(%{type: "textarea"} = assigns) do
~H"""
<div>
<.label for={@id}>{@label}</.label>
<textarea id={@id} name={@name}>{Phoenix.HTML.Form.normalize_value(@type, @value)}</textarea>
<.error :for={error <- @errors}>{error}</.error>
</div>
"""
end
def input(assigns) do
~H"""
<div>

View file

@ -0,0 +1,58 @@
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
~H"""
<div class="flex flex-col gap-y-4">
<h1 class="font-bold text-2xl">dashboard</h1>
<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>
<ol id="recent-statuses" phx-update="stream">
<li :for={{dom_id, status} <- @streams.statuses} id={dom_id}>{status.body}</li>
</ol>
</section>
<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" stream={@streams.blogs}>
<.link href={~p"/admin/posts/#{blog}"}>{blog.title}</.link>
</.post_list>
</section>
</div>
"""
end
attr :id, :string, required: true
attr :stream, :any, required: true
slot :inner_block, required: true
def post_list(assigns) do
~H"""
<ol id={@id} phx-update="stream">
<li :for={{dom_id, item} <- @stream} id={dom_id}>
{render_slot(@inner_block, item)}
</li>
</ol>
"""
end
end

View file

@ -0,0 +1,89 @@
defmodule Web.AdminPostLive do
use Web, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
def handle_params(%{"post_id" => post_id}, _uri, socket) do
post = Core.Posts.get!(post_id)
form = Core.Posts.change_post(post, %{}) |> to_form()
socket = assign(socket, post: post, form: form)
{:noreply, socket}
end
def handle_params(%{"kind" => kind}, _uri, %{assigns: %{live_action: :new}} = socket)
when kind in ~w[blog status] do
post = %Schema.Post{kind: String.to_existing_atom(kind)}
form = Core.Posts.change_post(post, %{}) |> to_form()
socket = assign(socket, post: post, form: form)
{:noreply, socket}
end
def handle_event("validate", %{"post" => attrs}, %{assigns: %{post: post}} = socket) do
attrs =
if update_slug?(post) and attrs["_unused_slug"] == "" do
Map.put(attrs, "slug", Slug.slugify(attrs["title"]))
else
attrs
end
form = Core.Posts.change_post(post, attrs) |> to_form()
socket = assign(socket, form: form)
{:noreply, socket}
end
def handle_event("save", %{"post" => attrs}, %{assigns: %{post: post}} = socket) do
socket =
case Core.Posts.create_or_update_post(post, attrs) do
{:ok, post} ->
socket
|> put_flash(:info, "post saved")
|> push_patch(to: ~p"/admin/posts/#{post}", replace: true)
{:error, changest} ->
assign(socket, form: to_form(changest))
end
{: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>
</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"
defp page_title(%Schema.Post{kind: :status}, :edit), do: "edit status"
defp update_slug?(%Schema.Post{kind: :blog, published_at: published_at}),
do: is_nil(published_at)
defp update_slug?(_), do: true
end

View file

@ -30,6 +30,11 @@ defmodule Web.Router do
live_session :require_authenticated_user, on_mount: [{Web.UserAuth, :ensure_authenticated}] do
live "/users/settings", UserSettingsLive, :edit
live "/", AdminDashboardLive
live "/posts/new", AdminPostLive, :new
live "/posts/:post_id", AdminPostLive, :edit
end
end

View file

@ -59,6 +59,7 @@ defmodule SlaonelyButSurely.MixProject do
# Added dependencies
{:boundary, "~> 0.10.4"},
{:tzdata, "~> 1.1"},
{:slugify, "~> 1.3"},
# Added dev and/or test dependencies
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},

View file

@ -40,6 +40,7 @@
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"styler": {:hex, :styler, "1.4.0", "5944723d08afe4d38210b674d7e97dd1137a75968a85a633983cc308e86dc5f2", [:mix], [], "hexpm", "07de0e89c27490c8e469bb814d77ddaaa3283d7d8038501021d80a7705cf13e9"},
"tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"},