lib/literature/live/blog_live.ex

defmodule Literature.BlogLive do
  use Literature.Web, :live_view

  import Literature.Helpers,
    only: [atomize_keys_to_string: 1, literature_image_url: 2]

  alias Literature.{Author, Post, Repo, Tag}

  @layout {Literature.LayoutView, :live}

  @impl Phoenix.LiveView
  def mount(%{"slug" => slug} = params, session, socket) do
    [&Literature.get_post!/1, &Literature.get_tag!/1, &Literature.get_author!/1]
    |> Enum.map(fn fun -> fun.(slug: slug, publication_slug: session["publication_slug"]) end)
    |> Enum.find(&is_struct/1)
    |> case do
      %Post{status: "published"} = post ->
        assign_to_socket(socket, :post, build_post(post, session["publication_slug"]))

      %Tag{visibility: true} = tag ->
        assign_to_socket(socket, :tag, preload_tag(tag))

      %Author{} = author ->
        assign_to_socket(socket, :author, preload_author(author))

      _ ->
        socket
    end
    |> assign(%{
      application_router: session["application_router"],
      locale: params["locale"],
      publication_slug: session["publication_slug"],
      view_module: session["view_module"]
    })
    |> then(&{:ok, &1, layout: @layout})
  end

  @impl Phoenix.LiveView
  def mount(params, session, socket) do
    socket
    |> assign(%{
      locale: params["locale"],
      publication_slug: session["publication_slug"],
      view_module: session["view_module"]
    })
    |> then(&{:ok, &1, layout: @layout})
  end

  @impl Phoenix.LiveView
  def render(%{view_module: view_module, live_action: live_action} = assigns) do
    live_action
    |> case do
      :show ->
        [
          {assigns[:post], "post.html"},
          {assigns[:tag], "tag.html"},
          {assigns[:author], "author.html"}
        ]
        |> Enum.find(fn {assign, _} -> is_map(assign) end)
        |> elem(1)

      action ->
        "#{to_string(action)}.html"
    end
    |> then(&Phoenix.View.render(view_module, &1, assigns))
  rescue
    _ ->
      reraise Literature.PageNotFound,
              [
                conn: %{path_info: assigns[:path_info], method: "GET"},
                router: assigns[:application_router]
              ],
              __STACKTRACE__
  end

  @impl Phoenix.LiveView
  def handle_params(params, url, socket) do
    %{path: path} = URI.parse(url)

    cond do
      is_nil(params["page"]) ->
        do_handle_params(params, url, socket)

      params["page"] == "1" ->
        path
        |> String.replace("/?page=#{params["page"]}", "")
        |> then(&{:noreply, push_navigate(socket, to: &1, replace: true)})

      Integer.parse(params["page"]) == :error ->
        raise Literature.PageNotFound

      true ->
        do_handle_params(params, url, socket)
    end
  end

  defp do_handle_params(params, url, socket) do
    path_info = String.split(URI.parse(url).path, "/") |> Enum.reject(&(&1 == ""))

    socket
    |> assign(:current_url, url)
    |> assign(:path_info, path_info)
    |> apply_action(socket.assigns.live_action, socket.assigns.publication_slug, params)
    |> then(&{:noreply, &1})
  end

  defp apply_action(socket, :index, slug, params) do
    publication = Literature.get_publication!(slug: slug)
    page = paginate_posts(socket, params)

    socket
    |> assign_meta_tags(publication)
    |> override_title_with_page(page)
    |> assign(:publication, publication)
    |> assign(:page, page)
    |> assign(:posts, page.entries)
    |> path_not_found_when_page_number_exceeds_from_total_pages(params, page.total_pages)
  end

  defp apply_action(socket, :tags, slug, _params) do
    publication = Literature.get_publication!(slug: slug)
    meta_tags = get_meta_tags_from_view_module(socket, :tags, publication)

    socket
    |> assign_meta_tags(meta_tags)
    |> assign(:publication, publication)
    |> assign(:tags, list_tags(socket))
  end

  defp apply_action(socket, :authors, slug, _params) do
    publication = Literature.get_publication!(slug: slug)
    meta_tags = get_meta_tags_from_view_module(socket, :authors, publication)

    socket
    |> assign_meta_tags(meta_tags)
    |> assign(:publication, publication)
    |> assign(:authors, list_authors(socket))
  end

  defp apply_action(socket, _, slug, _) do
    publication = Literature.get_publication!(slug: slug)
    assign(socket, :publication, publication)
  end

  defp paginate_posts(%{assigns: %{publication_slug: slug}}, params) do
    %{
      "publication_slug" => slug,
      "status" => "published",
      "preload" => ~w(authors tags)a,
      "page" => params["page"],
      "page_size" => 10
    }
    |> Literature.paginate_posts()
  end

  defp list_tags(%{assigns: %{publication_slug: slug}}) do
    %{"publication_slug" => slug, "status" => "public"}
    |> Literature.list_tags()
    |> preload_tag()
  end

  defp list_authors(%{assigns: %{publication_slug: slug}}) do
    %{"publication_slug" => slug}
    |> Literature.list_authors()
    |> preload_author()
  end

  defp build_post(post, slug) do
    publication =
      Literature.get_publication!(slug: slug) |> Repo.preload(published_posts: ~w(authors tags)a)

    post
    |> Post.resolve_prev_and_next_post(publication)
    |> Post.resolve_similar_posts(publication)
  end

  defp preload_tag(tag),
    do: Repo.preload(tag, ~w(published_posts)a)

  defp preload_author(author),
    do: Repo.preload(author, ~w(published_posts)a)

  defp assign_to_socket(socket, name, struct) do
    socket
    |> assign(name, struct_to_map(struct))
    |> assign_meta_tags(struct)
  end

  defp assign_meta_tags(socket, struct) do
    struct
    |> struct_to_map()
    |> convert_name_to_title()
    |> convert_excerpt_to_description()
    |> convert_image_to_url()
    |> atomize_keys_to_string()
    |> then(&assign(socket, :meta_tags, &1))
  end

  defp struct_to_map(struct) when is_struct(struct),
    do: Map.from_struct(struct)

  defp struct_to_map(map), do: map

  defp convert_name_to_title(author_or_tag),
    do: Map.put_new(author_or_tag, :title, author_or_tag[:name])

  defp convert_excerpt_to_description(post),
    do: Map.put_new(post, :description, post[:excerpt])

  defp convert_image_to_url(author_or_tag_or_post) do
    image =
      literature_image_url(author_or_tag_or_post, :feature_image) ||
        literature_image_url(author_or_tag_or_post, :profile_image)

    author_or_tag_or_post
    |> Map.put(:image, image)
    |> Map.put(:og_image, literature_image_url(author_or_tag_or_post, :og_image))
    |> Map.put(:twitter_image, literature_image_url(author_or_tag_or_post, :twitter_image))
  end

  defp get_meta_tags_from_view_module(socket, action, publication) do
    socket.assigns.view_module.meta_tags(action, publication) || %{}
  rescue
    _ -> %{}
  end

  defp override_title_with_page(socket, %{page_number: 1}), do: socket

  defp override_title_with_page(%{assigns: %{meta_tags: meta_tags}} = socket, page) do
    %{
      meta_tags
      | "title" => meta_tags["title"] <> " - Page #{page.page_number} of #{page.total_pages}"
    }
    |> then(&assign(socket, :meta_tags, &1))
  end

  defp path_not_found_when_page_number_exceeds_from_total_pages(
         socket,
         %{"page" => page},
         total_pages
       ) do
    if String.to_integer(page) > total_pages do
      raise Literature.PageNotFound
    else
      socket
    end
  end

  defp path_not_found_when_page_number_exceeds_from_total_pages(socket, _, _), do: socket
end

defmodule Literature.PageNotFound do
  @moduledoc """
    Exception raised when no route is found.
  """
  defexception plug_status: 404, message: "no route found", conn: nil, router: nil

  if Mix.env() == :dev do
    def exception(opts) do
      conn = Keyword.fetch!(opts, :conn)
      router = Keyword.fetch!(opts, :router)
      path = "/" <> Enum.join(conn.path_info, "/")

      %Phoenix.Router.NoRouteError{
        message: "no route found for #{conn.method} #{path} (#{inspect(router)})",
        conn: conn,
        router: router
      }
    end
  end
end