From 27a8c22e9f5d1801eb25f62b93ddffac9cf540d0 Mon Sep 17 00:00:00 2001
From: Adam Piontek <adam@73k.us>
Date: Mon, 5 Apr 2021 21:40:43 -0400
Subject: [PATCH] numerous fixes, blog index, paging, tag index, post show -
 all liveview

---
 assets/css/app.scss                           |  15 ++-
 lib/home73k/blog.ex                           |  24 ++--
 lib/home73k_web.ex                            |   3 -
 lib/home73k_web/live/blog_live.ex             | 111 +++++++++++++-----
 lib/home73k_web/live/blog_live.html.leex      |  84 +++++++++----
 lib/home73k_web/live/post_live.ex             |  58 ---------
 lib/home73k_web/live/post_live.html.leex      |  36 ------
 lib/home73k_web/router.ex                     |   8 +-
 lib/home73k_web/views/date_helpers.ex         |   9 --
 ...lana-del-arr-ultrapirates-parody-lyrics.md |   7 +-
 10 files changed, 178 insertions(+), 177 deletions(-)
 delete mode 100644 lib/home73k_web/live/post_live.ex
 delete mode 100644 lib/home73k_web/live/post_live.html.leex
 delete mode 100644 lib/home73k_web/views/date_helpers.ex

diff --git a/assets/css/app.scss b/assets/css/app.scss
index 7754710..bb6ce2d 100644
--- a/assets/css/app.scss
+++ b/assets/css/app.scss
@@ -47,12 +47,21 @@ body {
   height: 100%;
 }
 a {
-  color: $secondary;
+  color: $gray-100;
+  border-bottom: $secondary 2px solid;
+  text-decoration: none;
+  &.navbar-brand {
+    border-bottom: none;
+  }
   &:visited {
     color: $info;
+    .post-title & {
+      color: $gray-100;
+    }
   }
   &:hover {
     color: $primary;
+    text-decoration: none;
   }
 }
 .border-gray-900 {
@@ -94,6 +103,7 @@ a {
 
 /* social icons */
 #social-icons .link-light {
+  border-bottom: none;
   color: $gray-100;
   &:hover {
     color: $primary;
@@ -128,9 +138,10 @@ a {
 .post-title a {
   color: $gray-100;
   text-decoration: none;
+  border-bottom: none;
   &:hover {
     color: $primary;
-    text-decoration: underline;
+    border-bottom: $secondary 3px solid;
   }
 }
 .post-lede,
diff --git a/lib/home73k/blog.ex b/lib/home73k/blog.ex
index 7f002f6..db9ba4b 100644
--- a/lib/home73k/blog.ex
+++ b/lib/home73k/blog.ex
@@ -3,21 +3,29 @@ defmodule Home73k.Blog do
 
   Application.ensure_all_started(:earmark)
 
-  posts_paths = "#{Home73k.app_blog_content()}/**/*.md" |> Path.wildcard()
+  post_paths = "#{Home73k.app_blog_content()}/**/*.md" |> Path.wildcard()
+  post_paths_hash = :erlang.md5(post_paths)
 
   posts =
-    for post_path <- posts_paths do
+    for post_path <- post_paths do
       @external_resource Path.relative_to_cwd(post_path)
       Post.parse!(post_path)
     end
 
+  def __mix_recompile__?() do
+    Path.wildcard("#{Home73k.app_blog_content()}/**/*.md") |> :erlang.md5() != unquote(post_paths_hash)
+  end
+
   @posts Enum.sort_by(posts, & &1.date, {:desc, Date})
+  @post_count length(@posts)
 
   @tags posts |> Stream.flat_map(& &1.tags) |> Stream.uniq() |> Enum.sort()
 
   def list_posts, do: @posts
   def list_tags, do: @tags
 
+  def post_count, do: @post_count
+
   defmodule NotFoundError do
     defexception [:message, plug_status: 404]
   end
@@ -29,10 +37,10 @@ defmodule Home73k.Blog do
     end
   end
 
-  # def get_posts_by_tag!(tag) do
-  #   case Enum.filter(list_posts(), &(tag in &1.tags)) do
-  #     [] -> raise NotFoundError, "posts with tag=#{tag} not found"
-  #     posts -> posts
-  #   end
-  # end
+  def get_posts_by_tag!(tag) do
+    case Enum.filter(list_posts(), &(tag in &1.tags)) do
+      [] -> raise NotFoundError, "posts with tag=#{tag} not found"
+      posts -> posts
+    end
+  end
 end
diff --git a/lib/home73k_web.ex b/lib/home73k_web.ex
index 6da50d5..29f5403 100644
--- a/lib/home73k_web.ex
+++ b/lib/home73k_web.ex
@@ -93,9 +93,6 @@ defmodule Home73kWeb do
       # Import SVG Icon helper
       import Home73kWeb.IconHelpers
 
-      # Import Date formatter helper
-      import Home73kWeb.DateHelpers
-
       import Home73kWeb.ErrorHelpers
       import Home73kWeb.Gettext
       alias Home73kWeb.Router.Helpers, as: Routes
diff --git a/lib/home73k_web/live/blog_live.ex b/lib/home73k_web/live/blog_live.ex
index 6100d73..ac160be 100644
--- a/lib/home73k_web/live/blog_live.ex
+++ b/lib/home73k_web/live/blog_live.ex
@@ -3,49 +3,96 @@ defmodule Home73kWeb.BlogLive do
 
   alias Home73k.Blog
 
+  @page_size 7
+
   @impl true
   def mount(_params, _session, socket) do
-    socket
-    |> live_okreply()
+    {:ok, socket}
   end
 
   @impl true
-  def handle_params(_params, _url, socket) do
-    socket
-    |> assign(:page_title, "Blog")
-    |> assign(:posts, Blog.list_posts())
+  def handle_params(params, _url, socket) do
+    socket.assigns.live_action
+    |> init_per_live_action(socket, params)
     |> live_noreply()
   end
 
-  # @impl true
-  # def handle_event("suggest", %{"q" => query}, socket) do
-  #   {:noreply, assign(socket, results: search(query), query: query)}
-  # end
+  defp page_param_as_int(page) do
+    try do
+      String.to_integer(page)
+    rescue
+      _ -> nil
+    end
+  end
 
-  # @impl true
-  # def handle_event("search", %{"q" => query}, socket) do
-  #   case search(query) do
-  #     %{^query => vsn} ->
-  #       {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
+  defp raise_not_found(msg), do: raise Home73k.Blog.NotFoundError, msg
 
-  #     _ ->
-  #       {:noreply,
-  #        socket
-  #        |> put_flash(:error, "No dependencies found matching \"#{query}\"")
-  #        |> assign(results: %{}, query: query)}
-  #   end
-  # end
+  defp init_per_live_action(:index, socket, _params) do
+    socket
+    |> assign(:page_title, "Blog")
+    |> assign(:posts, get_posts_for_page!(1))
+    |> assign(:page_count, get_page_count())
+    |> assign_prev_next(1)
+  end
 
-  # defp search(query) do
-  #   if not Home73kWeb.Endpoint.config(:code_reloader) do
-  #     raise "action disabled when not in development"
-  #   end
+  defp init_per_live_action(:page, socket, %{"page" => page}) do
+    page_int = page_param_as_int(page)
+    page_count = get_page_count()
 
-  #   for {app, desc, vsn} <- Application.started_applications(),
-  #       app = to_string(app),
-  #       String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
-  #       into: %{},
-  #       do: {app, vsn}
-  # end
+    cond do
+      is_nil(page_int) || page_int <= 1 ->
+        push_patch(socket, to: Routes.blog_path(socket, :index))
 
+      page_int > page_count ->
+        raise_not_found("there are only #{page_count} pages of posts")
+
+      true ->
+        posts = get_posts_for_page!(page_int)
+
+        socket
+        |> assign(:page_title, "Blog \\ Page #{page}")
+        |> assign(:posts, posts)
+        |> assign(:page_count, page_count)
+        |> assign_prev_next(page_int)
+    end
+  end
+
+  defp init_per_live_action(:show, socket, %{"id" => id}) do
+    post = Blog.get_post_by_id!(id)
+    socket
+    |> assign(:page_title, "Blog \\ post.title")
+    |> assign(:posts, [post])
+    |> assign(:page_count, nil)
+    |> assign_prev_next(0)
+  end
+
+  defp init_per_live_action(:tag, socket, %{"tag" => tag}) do
+    socket
+    |> assign(:page_title, "Blog \\ ##{tag}")
+    |> assign(:posts, Blog.get_posts_by_tag!(tag))
+    |> assign(:page_count, get_page_count())
+    |> assign_prev_next(1)
+  end
+
+
+  defp get_posts_for_page!(1), do: Blog.list_posts() |> Enum.take(@page_size)
+
+  defp get_posts_for_page!(page_int) do
+    Blog.list_posts()
+    |> Stream.chunk_every(@page_size)
+    |> Enum.at(page_int - 1)
+  end
+
+  defp get_page_count, do: Integer.floor_div(Blog.post_count(), @page_size) + rem(Blog.post_count(), @page_size)
+
+  defp assign_prev_next(socket, page_int) do
+    socket
+    |> assign(:page_prev, page_int < socket.assigns.page_count && page_int + 1 || nil)
+    |> assign(:page_next, page_int > 1 && page_int - 1 || nil)
+  end
+
+
+  def format_date(date) do
+    Calendar.strftime(date, "%B %-d, %Y")
+  end
 end
diff --git a/lib/home73k_web/live/blog_live.html.leex b/lib/home73k_web/live/blog_live.html.leex
index 5dd8a55..f41c5af 100644
--- a/lib/home73k_web/live/blog_live.html.leex
+++ b/lib/home73k_web/live/blog_live.html.leex
@@ -2,38 +2,74 @@
 
   <div class="col-12 col-md-10 col-lg-9 col-xl-8 col-xxl-7 pb-2 mb-4 mt-3">
 
-    <%= for post <- @posts do %>
+    <%= if is_nil(@posts) do %>
 
-      <div class="post border-bottom border-gray pb-4 mb-3">
+        <div class="post border-bottom border-gray pb-4 mb-3">
 
-        <h2 class="post-title fs-2 fw-600 mb-2">
-          <%=  live_redirect "#{post.title}", to: Routes.post_path(@socket, :show, post) %>
-        </h2>
+          <h2 class="post-title fs-2 fw-600 mb-2">Nothing found.</h2>
 
-        <div class="post-date font-monospace text-gray-400 <%= if length(post.tags) == 0, do: "mb-3" %>">
-          <%= icon_div @socket, "mdi-calendar-clock", [class: "icon baseline me-2"] %><%= post.date |> format_date() %>
-          by <%= icon_div @socket, "mdi-account", [class: "icon baseline me-1"] %>Adam Piontek
         </div>
 
-        <%= if length(post.tags) > 0 do %>
-          <div class="post-tags fs-smaller mb-3">
-          <%= icon_div @socket, "mdi-tag-multiple", [class: "icon baseline"] %>
-          <%= for {tag, i} <- Enum.with_index(post.tags) do %>
-            #<%= tag %><%= i < (length(post.tags) - 1) && "," || "" %>
-          <% end %>
+    <% else %>
+
+      <%= for post <- @posts do %>
+
+        <div class="post border-bottom border-gray pb-4 mb-3">
+
+          <h2 class="post-title fs-2 fw-600 mb-2">
+            <%=  live_redirect "#{post.title}", to: Routes.blog_path(@socket, :show, post) %>
+          </h2>
+
+          <div class="post-date font-monospace text-gray-400 <%= if length(post.tags) == 0, do: "mb-3" %>">
+            <%= icon_div @socket, "mdi-calendar-clock", [class: "icon baseline me-2"] %><%= format_date(post.date) %>
+            by <%= icon_div @socket, "mdi-account", [class: "icon baseline me-1"] %>Adam Piontek
           </div>
+
+          <%= if length(post.tags) > 0 do %>
+            <div class="post-tags fs-smaller mb-3">
+            <%= icon_div @socket, "mdi-tag-multiple", [class: "icon baseline text-gray-400"] %>
+            <%= for {tag, i} <- Enum.with_index(post.tags) do %>
+              <span class="text-gray-400">#</span><%= live_redirect tag, to: Routes.blog_path(@socket, :tag, tag) %><%= i < (length(post.tags) - 1) && "," || "" %>
+            <% end %>
+            </div>
+          <% end %>
+
+          <div class="post-lede">
+            <%= raw post.lede %>
+          </div>
+
+          <%= if @live_action == :show do %>
+            <div class="post-body">
+              <%= raw post.body %>
+            </div>
+          <% else %>
+            <p>
+              <%=  live_redirect raw("Read more&hellip;"), to: Routes.blog_path(@socket, :show, post), class: "fs-6" %>
+            </p>
+          <% end %>
+
+        </div>
+
+      <% end %>
+
+    <% end %>
+
+    <%= if @live_action in [:index, :page] do %>
+      <nav class="d-flex justify-content-between" aria-label="Page navigation">
+        <%= if @page_prev do %>
+          <%= live_patch to: Routes.blog_path(@socket, :page, @page_prev) do %>
+            &larr; Older
+          <% end %>
+        <% else %>
+          <div class="d-block"></div>
         <% end %>
 
-        <div class="post-lede">
-          <%= raw post.lede %>
-        </div>
-
-        <p>
-        <%=  live_redirect raw("Read more&hellip;"), to: Routes.post_path(@socket, :show, post), class: "fs-6" %>
-        </p>
-
-      </div>
-
+        <%= if @page_next do %>
+          <%= live_patch to: @page_next == 1 && Routes.blog_path(@socket, :index) || Routes.blog_path(@socket, :page, @page_next) do %>
+            Newer &rarr;
+          <% end %>
+        <% end %>
+      </nav>
     <% end %>
 
   </div>
diff --git a/lib/home73k_web/live/post_live.ex b/lib/home73k_web/live/post_live.ex
deleted file mode 100644
index 781322f..0000000
--- a/lib/home73k_web/live/post_live.ex
+++ /dev/null
@@ -1,58 +0,0 @@
-defmodule Home73kWeb.PostLive do
-  use Home73kWeb, :live_view
-
-  alias Home73k.Blog
-
-  @impl true
-  def mount(%{"id" => id}, session, socket) do
-    # IO.inspect(params, label: "postlive params")
-    IO.inspect(session, label: "postlive session")
-
-    post = Blog.get_post_by_id!(id)
-
-    socket
-    |> assign(:page_title, "Blog \\ #{post.title}")
-    |> assign(:post, post)
-    |> live_okreply()
-  end
-
-  # @impl true
-  # def handle_params(params, _url, socket) do
-  #   socket
-  #   |> assign(:page_title, "Blog")
-  #   |> assign(:posts, Blog.list_posts())
-  #   |> live_noreply()
-  # end
-
-  # @impl true
-  # def handle_event("suggest", %{"q" => query}, socket) do
-  #   {:noreply, assign(socket, results: search(query), query: query)}
-  # end
-
-  # @impl true
-  # def handle_event("search", %{"q" => query}, socket) do
-  #   case search(query) do
-  #     %{^query => vsn} ->
-  #       {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
-
-  #     _ ->
-  #       {:noreply,
-  #        socket
-  #        |> put_flash(:error, "No dependencies found matching \"#{query}\"")
-  #        |> assign(results: %{}, query: query)}
-  #   end
-  # end
-
-  # defp search(query) do
-  #   if not Home73kWeb.Endpoint.config(:code_reloader) do
-  #     raise "action disabled when not in development"
-  #   end
-
-  #   for {app, desc, vsn} <- Application.started_applications(),
-  #       app = to_string(app),
-  #       String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
-  #       into: %{},
-  #       do: {app, vsn}
-  # end
-
-end
diff --git a/lib/home73k_web/live/post_live.html.leex b/lib/home73k_web/live/post_live.html.leex
deleted file mode 100644
index 3b2b9d1..0000000
--- a/lib/home73k_web/live/post_live.html.leex
+++ /dev/null
@@ -1,36 +0,0 @@
-<main class="container d-flex justify-content-center">
-
-  <div class="col-12 col-md-10 col-lg-9 col-xl-8 col-xxl-7 pb-2 mb-4 mt-3">
-
-      <div class="post border-bottom border-gray pb-4 mb-3">
-
-        <h2 class="post-title fs-2 fw-normal mb-2"><%= raw @post.title %></h2>
-
-        <div class="post-date font-monospace text-gray-400 <%= if length(@post.tags) == 0, do: "mb-3" %>">
-          <%= icon_div @socket, "mdi-calendar-clock", [class: "icon baseline me-2"] %><%= @post.date |> format_date() %>
-          by <%= icon_div @socket, "mdi-account", [class: "icon baseline me-1"] %>Adam Piontek
-        </div>
-
-        <%= if length(@post.tags) > 0 do %>
-          <div class="post-tags  fs-smaller mb-3">
-          <%= icon_div @socket, "mdi-tag-multiple", [class: "icon baseline"] %>
-          <%= for {tag, i} <- Enum.with_index(@post.tags) do %>
-            #<%= tag %><%= i < (length(@post.tags) - 1) && "," || "" %>
-          <% end %>
-          </div>
-        <% end %>
-
-        <div class="post-lede">
-        <%= raw @post.lede %>
-        </div>
-
-        <div class="post-body">
-        <%= raw @post.body %>
-        </div>
-
-
-      </div>
-
-  </div>
-
-</main>
diff --git a/lib/home73k_web/router.ex b/lib/home73k_web/router.ex
index 352684e..2f1be2b 100644
--- a/lib/home73k_web/router.ex
+++ b/lib/home73k_web/router.ex
@@ -17,13 +17,17 @@ defmodule Home73kWeb.Router do
   scope "/", Home73kWeb do
     pipe_through :browser
 
+    # Pages
     get "/", HomeController, :index
     get "/about", HomeController, :about
     get "/resume", HomeController, :resume
     get "/folio", HomeController, :folio
+
+    # Blog
     live "/blog", BlogLive, :index
-    # live "/blog/page/:page", BlogLive, :older
-    live "/blog/:id", PostLive, :show
+    live "/blog/page/:page", BlogLive, :page
+    live "/blog/tag/:tag", BlogLive, :tag
+    live "/blog/:id", BlogLive, :show
   end
 
   # Other scopes may use custom stacks.
diff --git a/lib/home73k_web/views/date_helpers.ex b/lib/home73k_web/views/date_helpers.ex
deleted file mode 100644
index c795cff..0000000
--- a/lib/home73k_web/views/date_helpers.ex
+++ /dev/null
@@ -1,9 +0,0 @@
-defmodule Home73kWeb.DateHelpers do
-  @moduledoc """
-  Formatters for dates
-  """
-
-  def format_date(date) do
-    Calendar.strftime(date, "%B %-d, %Y")
-  end
-end
diff --git a/priv/content/2018/11/2018-11-09_lana-del-arr-ultrapirates-parody-lyrics.md b/priv/content/2018/11/2018-11-09_lana-del-arr-ultrapirates-parody-lyrics.md
index c67bbfd..b383bb2 100644
--- a/priv/content/2018/11/2018-11-09_lana-del-arr-ultrapirates-parody-lyrics.md
+++ b/priv/content/2018/11/2018-11-09_lana-del-arr-ultrapirates-parody-lyrics.md
@@ -21,10 +21,11 @@ Naturally I ended up writing some alternate lyrics. There's only two; I never go
 - ["Florida Cargo" (to the tune of "Florida Kilos")](#florida-cargo)
 - ["The Other Schooner" (to the tune of "The Other Woman")](#other-schooner)
 
-#### "Florida Cargo" (to the tune of "Florida Kilos")
 
 <a name="florida-cargo"></a>
 
+#### "Florida Cargo" (to the tune of "Florida Kilos")
+
 White shine, royal sailor, doubloons,   
 Don't you see them gleam,   
 They're special, just for you.   
@@ -103,10 +104,10 @@ White shine, royal sailor,
 Gold teeth, royal sailor, yeah,   
 Drink the night away.
 
-#### "The Other Schooner" (to the tune of "The Other Woman")
-
 <a name="other-schooner"></a>
 
+#### "The Other Schooner" (to the tune of "The Other Woman")
+
 The other schooner has time to trim and furl her sails   
 The other schooner is perfect where her rival fails   
 And she's never seen with caulking in her hull anywhere