From a7a270101d63150b43a14882b89da1296bd597e7 Mon Sep 17 00:00:00 2001
From: sloane <git@sloanelybutsurely.com>
Date: Mon, 31 Mar 2025 09:54:31 -0400
Subject: [PATCH] starting to get public pages working

---
 lib/core/posts.ex                             | 14 ++++
 lib/core/posts/post.ex                        |  7 ++
 lib/web/components/core_components.ex         | 81 +++++++++++++++++++
 lib/web/components/layouts/app.html.heex      |  4 +-
 lib/web/controllers/post_controller.ex        |  6 ++
 lib/web/controllers/post_html.ex              |  6 ++
 lib/web/controllers/post_html/index.html.heex |  8 ++
 lib/web/live/admin_dashboard_live.ex          | 19 +----
 lib/web/live/admin_post_live.ex               | 81 ++++++++++++++++++-
 lib/web/router.ex                             |  4 +-
 mix.exs                                       |  1 +
 11 files changed, 209 insertions(+), 22 deletions(-)
 create mode 100644 lib/web/controllers/post_html/index.html.heex

diff --git a/lib/core/posts.ex b/lib/core/posts.ex
index 5380144..5a00dc6 100644
--- a/lib/core/posts.ex
+++ b/lib/core/posts.ex
@@ -31,6 +31,20 @@ defmodule Core.Posts do
     |> Core.Repo.all()
   end
 
+  def get_published_recent_posts(kind) do
+    Post.Query.recent_posts(kind, @recent_posts_count)
+    |> Post.Query.published()
+    |> Core.Repo.all()
+  end
+
+  def publish_date(%Schema.Post{published_at: nil}), do: nil
+
+  def publish_date(%Schema.Post{published_at: published_at}) do
+    published_at
+    |> DateTime.shift_zone!("America/New_York")
+    |> DateTime.to_date()
+  end
+
   def change_post(%Schema.Post{} = post \\ %Schema.Post{}, attrs) do
     Post.content_changeset(post, attrs)
   end
diff --git a/lib/core/posts/post.ex b/lib/core/posts/post.ex
index 2a3b3e3..38b51bd 100644
--- a/lib/core/posts/post.ex
+++ b/lib/core/posts/post.ex
@@ -110,6 +110,13 @@ defmodule Core.Posts.Post do
       |> limit(^count)
     end
 
+    def recent_posts(query \\ base(), kind, count) do
+      query
+      |> where_kind(kind)
+      |> default_order()
+      |> limit(^count)
+    end
+
     def where_publish_date_and_slug(query \\ current(), publish_date, slug) do
       where(
         query,
diff --git a/lib/web/components/core_components.ex b/lib/web/components/core_components.ex
index 77b5ebf..ad9d967 100644
--- a/lib/web/components/core_components.ex
+++ b/lib/web/components/core_components.ex
@@ -112,4 +112,85 @@ defmodule Web.CoreComponents do
     </button>
     """
   end
+
+  attr :format, :string, required: true
+  attr :value, :any, default: nil
+  attr :formatter, :atom, default: :default
+  attr :timezone, :string, default: "America/New_York"
+  attr :global, :global
+
+  def timex(%{value: nil} = assigns) do
+    ~H"""
+    <time datetime="">--</time>
+    """
+  end
+
+  def timex(%{value: value, timezone: timezone} = assigns) do
+    assigns =
+      assign_new(assigns, :local_value, fn ->
+        case value do
+          %DateTime{} = datetime ->
+            datetime
+
+          %NaiveDateTime{} = naive ->
+            naive
+            |> DateTime.from_naive!("Etc/UTC")
+            |> DateTime.shift_zone!(timezone)
+        end
+      end)
+
+    ~H"""
+    <time
+      datetime={Timex.format!(@local_value, "{ISO:Extended}")}
+      title={Timex.format!(@local_value, "{Mshort} {D}, {YYYY}, {h12}:{m} {AM} {Zabbr}")}
+      {@global}
+    >
+      {Timex.format!(@local_value, @format, timex_formatter(@formatter))}
+    </time>
+    """
+  end
+
+  defp timex_formatter(formatter) 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})
 end
diff --git a/lib/web/components/layouts/app.html.heex b/lib/web/components/layouts/app.html.heex
index b5e9694..2ca9351 100644
--- a/lib/web/components/layouts/app.html.heex
+++ b/lib/web/components/layouts/app.html.heex
@@ -6,8 +6,8 @@
       </.link>
       <nav>
         <ul class="flex flex-row gap-x-2">
-          <li><.link href="#">writing</.link></li>
-          <li><.link href="#">microblog</.link></li>
+          <li><.link href={~p"/blog"}>writing</.link></li>
+          <li><.link href={~p"/microblog"}>microblog</.link></li>
         </ul>
       </nav>
     </section>
diff --git a/lib/web/controllers/post_controller.ex b/lib/web/controllers/post_controller.ex
index 5d28516..9765928 100644
--- a/lib/web/controllers/post_controller.ex
+++ b/lib/web/controllers/post_controller.ex
@@ -22,6 +22,12 @@ defmodule Web.PostController do
     |> render_post(status)
   end
 
+  def index(%{assigns: %{kind: kind}} = conn, _params) when kind in ~w[blog status]a do
+    posts = Core.Posts.get_published_recent_posts(kind)
+
+    render(conn, :index, posts: posts)
+  end
+
   defp render_post(conn, %Schema.Post{kind: :blog} = blog) do
     render(conn, :show_blog, blog: blog)
   end
diff --git a/lib/web/controllers/post_html.ex b/lib/web/controllers/post_html.ex
index 0fcc3f9..0183532 100644
--- a/lib/web/controllers/post_html.ex
+++ b/lib/web/controllers/post_html.ex
@@ -2,4 +2,10 @@ defmodule Web.PostHTML do
   use Web, :html
 
   embed_templates "post_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/post_html/index.html.heex b/lib/web/controllers/post_html/index.html.heex
new file mode 100644
index 0000000..8e61040
--- /dev/null
+++ b/lib/web/controllers/post_html/index.html.heex
@@ -0,0 +1,8 @@
+<.post_list :let={post} id="recent-posts" posts={@posts}>
+  <%= case post.kind do %>
+    <% :blog -> %>
+      <.link navigate={blog_path(post)}>{post.title}</.link>
+    <% :status -> %>
+      {post.body}
+  <% end %>
+</.post_list>
diff --git a/lib/web/live/admin_dashboard_live.ex b/lib/web/live/admin_dashboard_live.ex
index be54573..8c20527 100644
--- a/lib/web/live/admin_dashboard_live.ex
+++ b/lib/web/live/admin_dashboard_live.ex
@@ -23,7 +23,7 @@ defmodule Web.AdminDashboardLive do
           <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" stream={@streams.statuses}>
+        <.post_list :let={status} id="recent-statuses" posts={@streams.statuses}>
           <.link navigate={~p"/admin/posts/#{status}"}>{status.body}</.link>
         </.post_list>
       </section>
@@ -33,26 +33,11 @@ defmodule Web.AdminDashboardLive do
           <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}>
+        <.post_list :let={blog} id="recent-blogs" posts={@streams.blogs}>
           <.link navigate={~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
diff --git a/lib/web/live/admin_post_live.ex b/lib/web/live/admin_post_live.ex
index 99175f9..41dcc1c 100644
--- a/lib/web/live/admin_post_live.ex
+++ b/lib/web/live/admin_post_live.ex
@@ -9,7 +9,12 @@ defmodule Web.AdminPostLive do
     post = Core.Posts.get!(post_id)
     form = Core.Posts.change_post(post, %{}) |> to_form()
 
-    socket = assign(socket, post: post, form: form)
+    socket =
+      assign(socket,
+        post: post,
+        form: form,
+        page_title: page_title(post, socket.assigns.live_action)
+      )
 
     {:noreply, socket}
   end
@@ -19,7 +24,12 @@ defmodule Web.AdminPostLive 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)
+    socket =
+      assign(socket,
+        post: post,
+        form: form,
+        page_title: page_title(post, socket.assigns.live_action)
+      )
 
     {:noreply, socket}
   end
@@ -54,6 +64,58 @@ defmodule Web.AdminPostLive do
     {:noreply, socket}
   end
 
+  def handle_event("publish", _, %{assigns: %{post: post}} = socket) do
+    socket =
+      case Core.Posts.publish_post(post) do
+        {:ok, post} ->
+          assign(socket, post: post)
+
+        _ ->
+          socket
+      end
+
+    {:noreply, socket}
+  end
+
+  def handle_event("unpublish", _, %{assigns: %{post: post}} = socket) do
+    socket =
+      case Core.Posts.unpublish_post(post) do
+        {:ok, post} ->
+          assign(socket, post: post)
+
+        _ ->
+          socket
+      end
+
+    {:noreply, socket}
+  end
+
+  def handle_event("delete", _, %{assigns: %{post: post}} = socket) do
+    socket =
+      case Core.Posts.delete_post(post) do
+        {:ok, post} ->
+          assign(socket, post: post)
+
+        _ ->
+          socket
+      end
+
+    {:noreply, socket}
+  end
+
+  def handle_event("undelete", _, %{assigns: %{post: post}} = socket) do
+    socket =
+      case Core.Posts.undelete_post(post) do
+        {:ok, post} ->
+          assign(socket, post: post)
+
+        _ ->
+          socket
+      end
+
+    {:noreply, socket}
+  end
+
   def render(assigns) do
     ~H"""
     <main>
@@ -73,6 +135,21 @@ defmodule Web.AdminPostLive do
 
         <.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
diff --git a/lib/web/router.ex b/lib/web/router.ex
index a4b51b4..1d23a76 100644
--- a/lib/web/router.ex
+++ b/lib/web/router.ex
@@ -45,7 +45,9 @@ defmodule Web.Router do
 
     delete "/admin/users/log_out", UserSessionController, :delete
 
-    get "/:year/:month/:day/:slug", PostController, :show
+    get "/blog", PostController, :index, assigns: %{kind: :blog}
+    get "/blog/:year/:month/:day/:slug", PostController, :show
+    get "/microblog", PostController, :index, assigns: %{kind: :status}
     get "/status/:status_id", PostController, :show
 
     # live_session :current_user, on_mount: [{Web.UserAuth, :mount_current_user}] do
diff --git a/mix.exs b/mix.exs
index c9524f2..e0285f1 100644
--- a/mix.exs
+++ b/mix.exs
@@ -60,6 +60,7 @@ defmodule SlaonelyButSurely.MixProject do
       {:boundary, "~> 0.10.4"},
       {:tzdata, "~> 1.1"},
       {:slugify, "~> 1.3"},
+      {:timex, "~> 3.7"},
 
       # Added dev and/or test dependencies
       {:credo, "~> 1.7", only: [:dev, :test], runtime: false},