diff --git a/lib/core/posts.ex b/lib/core/posts.ex index 7594db5..832fd0a 100644 --- a/lib/core/posts.ex +++ b/lib/core/posts.ex @@ -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() diff --git a/lib/web/components/core_components.ex b/lib/web/components/core_components.ex index 52e8986..8d1b43a 100644 --- a/lib/web/components/core_components.ex +++ b/lib/web/components/core_components.ex @@ -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 diff --git a/lib/web/components/layouts/app.html.heex b/lib/web/components/layouts/app.html.heex index 94f066a..0330348 100644 --- a/lib/web/components/layouts/app.html.heex +++ b/lib/web/components/layouts/app.html.heex @@ -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> diff --git a/lib/web/controllers/blog_html.ex b/lib/web/controllers/blog_html.ex index 00a01d3..eb220ee 100644 --- a/lib/web/controllers/blog_html.ex +++ b/lib/web/controllers/blog_html.ex @@ -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 diff --git a/lib/web/controllers/blog_html/index.html.heex b/lib/web/controllers/blog_html/index.html.heex index 4c06abb..75e183b 100644 --- a/lib/web/controllers/blog_html/index.html.heex +++ b/lib/web/controllers/blog_html/index.html.heex @@ -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> diff --git a/lib/web/controllers/page_html/home.html.heex b/lib/web/controllers/page_html/home.html.heex index 7d31bfa..76dd544 100644 --- a/lib/web/controllers/page_html/home.html.heex +++ b/lib/web/controllers/page_html/home.html.heex @@ -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> diff --git a/lib/web/live/admin_dashboard_live.ex b/lib/web/live/admin_dashboard_live.ex index 8c20527..707cb35 100644 --- a/lib/web/live/admin_dashboard_live.ex +++ b/lib/web/live/admin_dashboard_live.ex @@ -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 diff --git a/lib/web/live/admin_dashboard_live.html.heex b/lib/web/live/admin_dashboard_live.html.heex new file mode 100644 index 0000000..e07c2df --- /dev/null +++ b/lib/web/live/admin_dashboard_live.html.heex @@ -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> diff --git a/lib/web/live/admin_post_live.ex b/lib/web/live/admin_post_live.ex index 41dcc1c..7970e80 100644 --- a/lib/web/live/admin_post_live.ex +++ b/lib/web/live/admin_post_live.ex @@ -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" diff --git a/lib/web/live/admin_post_live.html.heex b/lib/web/live/admin_post_live.html.heex new file mode 100644 index 0000000..0fcf196 --- /dev/null +++ b/lib/web/live/admin_post_live.html.heex @@ -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> diff --git a/lib/web/paths.ex b/lib/web/paths.ex new file mode 100644 index 0000000..4d3112a --- /dev/null +++ b/lib/web/paths.ex @@ -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 diff --git a/lib/web/router.ex b/lib/web/router.ex index 69bb5bc..d418ae0 100644 --- a/lib/web/router.ex +++ b/lib/web/router.ex @@ -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