diff --git a/assets/js/app.js b/assets/js/app.js index 2ca06a5..aada76a 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -25,9 +25,18 @@ import "phoenix_html" import {Socket} from "phoenix" import {LiveSocket} from "phoenix_live_view" import topbar from "../vendor/topbar" +import InfinityScroll from "./infinity-scroll" + +let Hooks = {} +Hooks.InfinityScroll = InfinityScroll; let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") -let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) +let liveSocket = new LiveSocket("/live", Socket, { + params: { _csrf_token: csrfToken }, + hooks: Hooks, +}) + + // Show progress bar on live navigation and form submits topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) @@ -42,4 +51,3 @@ liveSocket.connect() // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.disableLatencySim() window.liveSocket = liveSocket - diff --git a/assets/js/infinity-scroll.js b/assets/js/infinity-scroll.js new file mode 100644 index 0000000..8febfa5 --- /dev/null +++ b/assets/js/infinity-scroll.js @@ -0,0 +1,29 @@ +/*** + * Hook to send push event "load-more" to server on window scroll events + */ +export default { + rootElement() { + return ( + document.documentElement || document.body.parentNode || document.body + ); + }, + scrollPosition() { + const { scrollTop, clientHeight, scrollHeight } = this.rootElement(); + return ((scrollTop + clientHeight) / scrollHeight) * 100; + }, + + mounted() { + this.threshold = 95; + this.lastScrollPosition = 0; + + window.addEventListener("scroll", () => { + const currentScrollPosition = this.scrollPosition(); + const isCloseToBottom = + currentScrollPosition > this.threshold && + this.lastScrollPosition <= this.threshold; + + if (isCloseToBottom) this.pushEvent("load-more", {}); + this.lastScrollPosition = currentScrollPosition; + }); + }, +}; diff --git a/lib/ascend/hills.ex b/lib/ascend/hills.ex index bfdd84e..3cf49b4 100644 --- a/lib/ascend/hills.ex +++ b/lib/ascend/hills.ex @@ -16,13 +16,28 @@ defmodule Ascend.Hills do [%Hill{}, ...] """ - def list_hills(opts) do + def list_hills(opts \\ %{}) do from(h in Hill) |> filter(opts) |> sort(opts) |> Repo.all() end + @doc """ + Returns a count of hills. + + ## Examples + + iex> hill_count() + 10 + + """ + def hill_count(opts \\ %{}) do + from(h in Hill) + |> filter(opts) + |> Repo.aggregate(:count) + end + @doc """ Returns the list of hills with a total count @@ -32,7 +47,7 @@ defmodule Ascend.Hills do %{hills: [%Hill{}, ...], total_count: 2} """ - def list_hills_with_total_count(opts) do + def list_hills_with_total_count(opts \\ %{}) do query = from(h in Hill) |> filter(opts) total_count = Repo.aggregate(query, :count) @@ -46,6 +61,14 @@ defmodule Ascend.Hills do %{hills: result, total_count: total_count} end + def list_hills_with_pagination(offset, limit, opts \\ %{}) do + from(h in Hill) + |> filter(opts) + |> limit(^limit) + |> offset(^offset) + |> Repo.all() + end + defp paginate(query, %{page: page, page_size: page_size}) when is_integer(page) and is_integer(page_size) do offset = max(page - 1, 0) * page_size diff --git a/lib/ascend_web/live/filter_component.ex b/lib/ascend_web/live/filter_component.ex index b56f786..981d50d 100644 --- a/lib/ascend_web/live/filter_component.ex +++ b/lib/ascend_web/live/filter_component.ex @@ -27,7 +27,6 @@ defmodule AscendWeb.Live.FilterComponent do def handle_event("search", %{"filter" => filter}, socket) do case FilterForm.parse(filter) do {:ok, opts} -> - IO.puts("Send self 1") send(self(), {:update, opts}) {:noreply, socket} diff --git a/lib/ascend_web/live/infinity_live.ex b/lib/ascend_web/live/infinity_live.ex new file mode 100644 index 0000000..68584d9 --- /dev/null +++ b/lib/ascend_web/live/infinity_live.ex @@ -0,0 +1,117 @@ +defmodule AscendWeb.InfinityLive do + use AscendWeb, :live_view + + alias Ascend.Hills + alias AscendWeb.Forms.FilterForm + + def render(assigns) do + ~H""" +

Listing <%= @count %> Hills

+ <.live_component + module={AscendWeb.Live.FilterComponent} + id="filter" + filter={@filter} + /> + + + <%= for hill <- @hills do %> + + + + + + + + + + + <% end %> + +
+ <%= live_redirect "#{hill.name}", to: Routes.hill_show_path(@socket, :show, hill) %> + <%= hill.dobih_id %><%= hill.metres %><%= hill.feet %><%= hill.grid_ref %><%= hill.classification %><%= hill.region %><%= hill.area %>
+ """ + end + + def mount(_params, _session, socket) do + count = Hills.hill_count() + + socket = + socket + |> assign(offset: 0, limit: 25, count: count) + + {:ok, socket, temporary_assigns: [hills: []]} + end + + def handle_params(params, _url, socket) do + socket = + socket + |> assign(:page_title, "Listing Hills") + |> parse_params(params) + |> load_hills() + + {:noreply, socket} + end + + def handle_event("load-more", _params, socket) do + %{offset: offset, limit: limit, count: count} = socket.assigns + + socket = + if offset < count do + socket + |> assign(offset: offset + limit) + |> load_hills() + else + socket + end + + {:noreply, socket} + end + + def handle_info({:update, opts}, socket) do + params = merge_and_sanitize_params(socket, opts) + path = Routes.live_path(socket, AscendWeb.InfinityLive, params) + + socket = + socket + |> assign(offset: 0) + |> assign(limit: 25) + + {:noreply, push_patch(socket, to: path, replace: true)} + end + + defp merge_and_sanitize_params(socket, overrides \\ %{}) do + %{filter: filter} = socket.assigns + + %{} + |> Map.merge(filter) + |> Map.merge(overrides) + |> Enum.reject(fn {_key, value} -> is_nil(value) end) + |> Map.new() + end + + defp parse_params(socket, params) do + with {:ok, filter_opts} <- FilterForm.parse(params) do + socket + |> assign_filter(filter_opts) + else + _error -> + socket + |> assign_filter() + end + end + + defp assign_filter(socket, overrides \\ %{}) do + assign(socket, :filter, FilterForm.default_values(overrides)) + end + + defp load_hills(socket) do + %{offset: offset, limit: limit} = socket.assigns + + params = merge_and_sanitize_params(socket) + + socket + |> assign(:hills, Hills.list_hills_with_pagination(offset, limit, params)) + |> assign(:count, Hills.hill_count(params)) + end +end diff --git a/lib/ascend_web/router.ex b/lib/ascend_web/router.ex index 07ab2d0..b7bf2cf 100644 --- a/lib/ascend_web/router.ex +++ b/lib/ascend_web/router.ex @@ -18,6 +18,7 @@ defmodule AscendWeb.Router do pipe_through :browser live "/", HillLive.Index, :index + live "/infinity", InfinityLive live "/hills/:id", HillLive.Show, :show end diff --git a/test/ascend/hills_test.exs b/test/ascend/hills_test.exs index 64dd4e5..e0d3fbe 100644 --- a/test/ascend/hills_test.exs +++ b/test/ascend/hills_test.exs @@ -73,6 +73,29 @@ defmodule Ascend.HillsTest do } end + test "hill_count/1 returns a count" do + insert(:hill) + assert Hills.hill_count(%{}) == 1 + end + + test "hill_count/1 returns a filtered count" do + insert(:hill, name: "Z Hill") + insert(:hill, name: "A Hill1") + insert(:hill, name: "A Hill2") + insert(:hill, name: "A Hill3") + + assert Hills.hill_count(%{name: "a"}) == 3 + end + + test "list_hills_with_pagination/3" do + hill1 = insert(:hill, name: "A Hill1") + hill2 = insert(:hill, name: "A Hill2") + hill3 = insert(:hill, name: "A Hill3") + + assert Hills.list_hills_with_pagination(0, 1, %{}) == [hill1] + assert Hills.list_hills_with_pagination(1, 2, %{}) == [hill2, hill3] + end + test "get_hill!/1 returns the hill with given id" do hill = insert(:hill) assert Hills.get_hill!(hill.id) == hill