diff --git a/config/config.exs b/config/config.exs index 4a3f784..1532e6a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -51,4 +51,6 @@ config :tailwind, cd: Path.expand("../assets", __DIR__) ] +config :flop, repo: Core.Repo + import_config "#{config_env()}.exs" diff --git a/lib/core/posts.ex b/lib/core/posts.ex index 5a00dc6..7594db5 100644 --- a/lib/core/posts.ex +++ b/lib/core/posts.ex @@ -37,6 +37,13 @@ defmodule Core.Posts do |> Core.Repo.all() end + def list_published_posts(kind, params \\ %{}) do + Post.Query.base() + |> Post.Query.published() + |> Post.Query.where_kind(kind) + |> Flop.validate_and_run(params, for: Schema.Post) + end + def publish_date(%Schema.Post{published_at: nil}), do: nil def publish_date(%Schema.Post{published_at: published_at}) do diff --git a/lib/schema/post.ex b/lib/schema/post.ex index 4e54648..033c38c 100644 --- a/lib/schema/post.ex +++ b/lib/schema/post.ex @@ -2,6 +2,20 @@ defmodule Schema.Post do @moduledoc false use Schema + @derive { + Flop.Schema, + filterable: [], + sortable: [:published_at, :id], + pagination_types: [:first, :last], + default_order: %{ + order_by: [:published_at, :id], + order_directions: [:desc, :desc] + }, + default_pagination_type: :first, + default_limit: 20, + max_limit: 50 + } + @post_kinds ~w[status blog]a schema "posts" do diff --git a/lib/web/components/core_components.ex b/lib/web/components/core_components.ex index a90fa48..52e8986 100644 --- a/lib/web/components/core_components.ex +++ b/lib/web/components/core_components.ex @@ -211,4 +211,44 @@ defmodule Web.CoreComponents do </div> """ end + + @doc """ + Renders pagination controls for navigating through a paginated list. + + ## Examples + + <.pagination meta={@meta} path={~p"/blog"} schema={Schema.Post} /> + """ + attr :meta, :map, required: true, doc: "the pagination metadata from Flop" + attr :path, :string, required: true, doc: "the base path for pagination links" + attr :schema, :atom, required: true, doc: "the schema module for Flop.Phoenix.build_path" + attr :class, :string, default: "mt-4", doc: "additional CSS classes" + + def pagination(assigns) do + ~H""" + <div class={["flex justify-between", @class]}> + <%= if @meta.has_previous_page? do %> + <.link + navigate={Flop.Phoenix.build_path(@path, Flop.to_previous_cursor(@meta), for: @schema)} + class="text-gray-500 hover:text-gray-800" + > + Newer posts + </.link> + <% else %> + <div></div> + <% end %> + + <%= if @meta.has_next_page? do %> + <.link + navigate={Flop.Phoenix.build_path(@path, Flop.to_next_cursor(@meta), for: @schema)} + class="text-gray-500 hover:text-gray-800" + > + Older posts + </.link> + <% else %> + <div></div> + <% end %> + </div> + """ + end end diff --git a/lib/web/controllers/blog_controller.ex b/lib/web/controllers/blog_controller.ex index 3c039ee..56214f6 100644 --- a/lib/web/controllers/blog_controller.ex +++ b/lib/web/controllers/blog_controller.ex @@ -1,8 +1,8 @@ defmodule Web.BlogController do use Web, :controller - def index(conn, _params) do - blogs = Core.Posts.get_published_recent_posts(:blog) + def index(conn, params) do + {:ok, {blogs, meta}} = Core.Posts.list_published_posts(:blog, params) blogs_by_year = blogs @@ -12,7 +12,7 @@ defmodule Web.BlogController do end) |> Enum.sort_by(fn {year, _} -> year end, :desc) - render(conn, :index, blogs_by_year: blogs_by_year) + render(conn, :index, blogs_by_year: blogs_by_year, meta: meta) end def show(conn, %{"year" => year, "month" => month, "day" => day, "slug" => slug}) do diff --git a/lib/web/controllers/blog_html/index.html.heex b/lib/web/controllers/blog_html/index.html.heex index 1671f0c..4c06abb 100644 --- a/lib/web/controllers/blog_html/index.html.heex +++ b/lib/web/controllers/blog_html/index.html.heex @@ -26,5 +26,7 @@ </div> <% end %> </div> + + <.pagination meta={@meta} path={~p"/blog"} schema={Schema.Post} class="my-2" /> </div> </div> diff --git a/lib/web/controllers/status_controller.ex b/lib/web/controllers/status_controller.ex index e16afca..b03795b 100644 --- a/lib/web/controllers/status_controller.ex +++ b/lib/web/controllers/status_controller.ex @@ -1,9 +1,9 @@ defmodule Web.StatusController do use Web, :controller - def index(conn, _params) do - statuses = Core.Posts.get_published_recent_posts(:status) - render(conn, :index, statuses: statuses) + def index(conn, params) do + {:ok, {statuses, meta}} = Core.Posts.list_published_posts(:status, params) + render(conn, :index, statuses: statuses, meta: meta) end def show(conn, %{"status_id" => status_id}) do diff --git a/lib/web/controllers/status_html/index.html.heex b/lib/web/controllers/status_html/index.html.heex index 5817bf5..f276573 100644 --- a/lib/web/controllers/status_html/index.html.heex +++ b/lib/web/controllers/status_html/index.html.heex @@ -8,4 +8,6 @@ <.status_entry status={status} /> <% end %> </div> + + <.pagination meta={@meta} path={~p"/microblog"} schema={Schema.Post} class="my-2" /> </div> diff --git a/mix.exs b/mix.exs index 25ede0d..18337c0 100644 --- a/mix.exs +++ b/mix.exs @@ -62,6 +62,8 @@ defmodule SlaonelyButSurely.MixProject do {:slugify, "~> 1.3"}, {:timex, "~> 3.7"}, {:mdex, "~> 0.5.0"}, + {:flop, "~> 0.26.1"}, + {:flop_phoenix, "~> 0.24.1"}, # Added dev and/or test dependencies {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index f23d922..04eb2af 100644 --- a/mix.lock +++ b/mix.lock @@ -19,6 +19,8 @@ "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"}, + "flop": {:hex, :flop, "0.26.1", "f0e9c6895cf876f667e9ff1c0398e53df87087fcd82d9cea8989332b9c0e1358", [:mix], [{:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}], "hexpm", "5fcab8a1ee78111159fc4752dc9823862343b6d6bd527ff947ec1e1c27018485"}, + "flop_phoenix": {:hex, :flop_phoenix, "0.24.1", "0eee8721e984cd9cbbfc90357c355fcf5c57da9e0617159f432d35843d01b671", [:mix], [{:flop, ">= 0.23.0 and < 0.27.0", [hex: :flop, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6.0 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "543c8eb70a29c0255b778df855f0de303290f88159fc3e008ce0ac4ace48e6ea"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, "hackney": {:hex, :hackney, "1.22.0", "4efc68df70322d4d2e3d2744e9bd191a39a0cb8d08c35379a08d9fb0f040d595", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "628569e451820950382be3d3e6481d7c59997e606c7823bddb4ce5d10812dfcb"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},