diff --git a/.credo.exs b/.credo.exs index dbc5ffe..dfc685d 100644 --- a/.credo.exs +++ b/.credo.exs @@ -29,9 +29,9 @@ "apps/*/lib/", "apps/*/src/", "apps/*/test/", - "apps/*/web/", + "apps/*/web/" ], - excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] }, # # Load and configure plugins here: @@ -132,7 +132,7 @@ {Credo.Check.Refactor.NegatedConditionsWithElse, []}, {Credo.Check.Refactor.Nesting, [ - max_nesting: 3, + max_nesting: 3 ]}, {Credo.Check.Refactor.RedundantWithClauseResult, []}, {Credo.Check.Refactor.RejectReject, []}, @@ -162,7 +162,7 @@ {Credo.Check.Warning.UnusedRegexOperation, []}, {Credo.Check.Warning.UnusedStringOperation, []}, {Credo.Check.Warning.UnusedTupleOperation, []}, - {Credo.Check.Warning.WrongTestFileExtension, []}, + {Credo.Check.Warning.WrongTestFileExtension, []} ], disabled: [ # @@ -208,13 +208,13 @@ {Credo.Check.Warning.UnsafeToAtom, []}, # {Credo.Check.Refactor.MapInto, []}, - {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleDoc, []} # # Custom checks can be created using `mix credo.gen.check`. # - ], - }, - }, - ], + ] + } + } + ] } diff --git a/.formatter.exs b/.formatter.exs index d55bf35..c61d166 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,6 +1,8 @@ [ import_deps: [:phoenix], - plugins: [Quokka, FreedomFormatter, Phoenix.LiveView.HTMLFormatter], + plugins: [Quokka, Phoenix.LiveView.HTMLFormatter], inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"], - trailing_comma: true, + line_length: 98, + heex_line_length: 98, + trailing_comma: true ] diff --git a/README.md b/README.md index e31ed72..63066c5 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ As a markdown parser, [Earmark](https://github.com/pragdave/earmark) is used bec For styling, [daisyUI](https://github.com/saadeghi/daisyui) allows easy change of themes and creation of your own style using [Tailwind](https://github.com/tailwindlabs/tailwindcss). The site is fully responsive for mobile and desktop resolutions, and supports themes for dark and light mode. Code blocks are automatically highlighted using [highlight.js](https://github.com/highlightjs/highlight.js). +With [LiveDeck](priv/pages/LiveDeck.md), AlchemyPub comes with an interactive presentation framework. It can turn any markdown page into a dynamic, interactive presentation. + Page visits are tracked anonymously. Thanks to [Phoenix Presence](https://hexdocs.pm/phoenix/Phoenix.Presence.html), it can keep track of navigation and the duration of each page visit. This also powers the live online counter in the navigation bar. The tracking data is stored in a file based SQLite database using Ecto. No external database or configuration is required. The tracking data can be viewed through the [Phoenix LiveDashboard](https://github.com/phoenixframework/phoenix_live_dashboard). ## Getting Started @@ -56,8 +58,8 @@ AlchemyPub is originally brought to you by [j4nk.dev](https://j4nk.dev). It is p - [x] Deployment - [x] Live viewer count - [x] Live tracking -- [ ] Static content updates (images) -- [ ] Slide support +- [x] Slide support +- [x] Keyboard navigation - [ ] Tracker visualization -- [ ] Keyboard navigation +- [ ] Static content updates (images) - [ ] LiveView patch tag navigation support diff --git a/assets/css/app.css b/assets/css/app.css index 945b76b..6af6c50 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -16,6 +16,7 @@ Make sure to look at the daisyUI changelog: https://daisyui.com/docs/changelog/ */ @plugin "../vendor/daisyui" {} +/* whitelisted classes */ @source inline("mr-1"); @source inline("mb-2"); @source inline("pl-2"); @@ -24,18 +25,6 @@ @source inline("no-underline"); @source inline("bg-accent"); @source inline("accent"); -@source inline("hero-megaphone"); -@source inline("hero-newspaper"); -@source inline("hero-home-modern"); -@source inline("hero-scale"); -@source inline("hero-beaker"); -@source inline("hero-document"); -@source inline("hero-identification"); -@source inline("hero-musical-note"); -@source inline("hero-code-bracket"); -@source inline("hero-envelope"); -@source inline("hero-globe-alt"); -@source inline("hero-calendar-days"); @source inline("prose"); @source inline("not-prose"); @source inline("whitespace-nowrap"); @@ -59,6 +48,22 @@ @source inline("print:border-0"); @source inline("print:w-[50rem]"); @source inline("print:break-after-page"); +@source inline("kbd"); + +/* whitelisted heroicons */ +@source inline("hero-megaphone"); +@source inline("hero-newspaper"); +@source inline("hero-home-modern"); +@source inline("hero-scale"); +@source inline("hero-beaker"); +@source inline("hero-document"); +@source inline("hero-identification"); +@source inline("hero-musical-note"); +@source inline("hero-code-bracket"); +@source inline("hero-envelope"); +@source inline("hero-globe-alt"); +@source inline("hero-calendar-days"); +@source inline("hero-presentation-chart-bar"); /* Add variants based on LiveView classes */ @custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &); @@ -99,7 +104,7 @@ } p { - @apply text-[3cqw] max-w-[90cqw] flex + @apply text-[3cqw] max-w-[90cqw] } pre { @@ -122,6 +127,10 @@ @apply text-[3cqw] } + a { + @apply text-primary link + } + img { @apply grow shrink justify-self-center } diff --git a/config/config.exs b/config/config.exs index 782a279..e7d781c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -25,7 +25,7 @@ config :alchemy_pub, AlchemyPubWeb.Endpoint, adapter: Bandit.PhoenixAdapter, render_errors: [ formats: [html: AlchemyPubWeb.ErrorHTML, json: AlchemyPubWeb.ErrorJSON], - layout: false, + layout: false ], pubsub_server: AlchemyPub.PubSub, live_view: [signing_salt: "pWWRs/C1"] @@ -48,7 +48,7 @@ config :esbuild, args: ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/*), cd: Path.expand("../assets", __DIR__), - env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}, + env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} ] # Configures Elixir's Logger @@ -57,7 +57,7 @@ config :logger, :default_formatter, metadata: [:request_id] config :mime, :types, %{ - "image/svg+xml" => ["svg"], + "image/svg+xml" => ["svg"] } # Use Jason for JSON parsing in Phoenix @@ -73,7 +73,7 @@ config :tailwind, ), # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. - cd: Path.expand("..", __DIR__), + cd: Path.expand("..", __DIR__) ] import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index 9f60117..c55fdb1 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -10,7 +10,7 @@ config :alchemy_pub, AlchemyPubWeb.Endpoint, secret_key_base: "QTJv7Qn0zOcczi48xHo5HfEyr3tRwA6Gz6QFCM1J6przHoAuJdUlJwqHBdQxQR55", watchers: [ esbuild: {Esbuild, :install_and_run, [:alchemy_pub, ~w(--sourcemap=inline --watch)]}, - tailwind: {Tailwind, :install_and_run, [:alchemy_pub, ~w(--watch)]}, + tailwind: {Tailwind, :install_and_run, [:alchemy_pub, ~w(--watch)]} ] # Watch static and templates for browser reloading. @@ -25,10 +25,12 @@ config :alchemy_pub, AlchemyPubWeb.Endpoint, # In order to use HTTPS in development, a self-signed # certificate can be generated by running the following # Mix task: - ~r"lib/alchemy_pub_web/(controllers|live|components)/.*(ex|heex)$", - ], + ~r"lib/alchemy_pub_web/(controllers|live|components)/.*(ex|heex)$" + ] ] +config :alchemy_pub, admin_secret: "local_admin" + # Enable dev routes for dashboard and mailbox # # mix phx.gen.cert diff --git a/config/runtime.exs b/config/runtime.exs index 6ff4dfd..9b3e4f1 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -29,8 +29,17 @@ if System.get_env("PHX_SERVER") do You can generate one by calling: mix phx.gen.secret """ + random_secret = fn key -> + require Logger + + secret = :rand.bytes(63) |> :base64.encode_to_string(%{mode: :urlsafe}) |> to_string() + Logger.info("Using random secret for #{key}: #{secret}") + secret + end + host = System.get_env("PHX_HOST") || "example.com" port = String.to_integer(System.get_env("PORT") || "4000") + admin_secret = System.get_env("ADMIN_SECRET") || random_secret.("ADMIN_SECRET") config :alchemy_pub, AlchemyPubWeb.Endpoint, server: true @@ -42,11 +51,12 @@ if System.get_env("PHX_SERVER") do # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 # for details about using IPv6 vs IPv4 and loopback vs public addresses. ip: {0, 0, 0, 0, 0, 0, 0, 0}, - port: port, + port: port ], secret_key_base: secret_key_base config :alchemy_pub, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") + config :alchemy_pub, admin_secret: admin_secret # ## SSL Support # diff --git a/lib/alchemy_pub/application.ex b/lib/alchemy_pub/application.ex index 152bcc4..7bd33e1 100644 --- a/lib/alchemy_pub/application.ex +++ b/lib/alchemy_pub/application.ex @@ -15,13 +15,13 @@ defmodule AlchemyPub.Application do {Finch, name: AlchemyPub.Finch}, # Start a worker by calling: AlchemyPub.Worker.start_link(arg) # {AlchemyPub.Worker, arg}, + {Registry, [keys: :unique, name: AlchemyPub.Registry]}, + {DynamicSupervisor, strategy: :one_for_one, name: AlchemyPub.DeckSupervisor}, {AlchemyPub.Engine, base_path: "priv/pages"}, AlchemyPub.Presence, AlchemyPub.Repo, - {Registry, [keys: :unique, name: AlchemyPub.Registry]}, - {DynamicSupervisor, strategy: :one_for_one, name: AlchemyPub.DynamicSupervisor}, # Start to serve requests, typically the last entry - AlchemyPubWeb.Endpoint, + AlchemyPubWeb.Endpoint ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/alchemy_pub/deck_server.ex b/lib/alchemy_pub/deck_server.ex index d8da23d..81c0a25 100644 --- a/lib/alchemy_pub/deck_server.ex +++ b/lib/alchemy_pub/deck_server.ex @@ -15,9 +15,10 @@ defmodule AlchemyPub.DeckSupervisor do end def remove_child(name) when name |> is_binary() do - [{pid, _}] = Registry.lookup(AlchemyPub.Registry, name) - :ok = DynamicSupervisor.terminate_child(__MODULE__, pid) - Registry.unregister(AlchemyPub.Registry, name) + for {pid, _} <- Registry.lookup(AlchemyPub.Registry, name) do + :ok = DynamicSupervisor.terminate_child(__MODULE__, pid) + Registry.unregister(AlchemyPub.Registry, name) + end end def child_process(name), do: {:via, Registry, {AlchemyPub.Registry, name}} @@ -44,15 +45,19 @@ defmodule AlchemyPub.DeckServer do GenServer.call({:via, Registry, {AlchemyPub.Registry, name}}, :get_page) end + def get_count(name) do + GenServer.call({:via, Registry, {AlchemyPub.Registry, name}}, :get_count) + end + @impl true def init(name: {:via, Registry, {AlchemyPub.Registry, name}}, pages: pages) do - {:ok, %{%DeckState{} | name: name, pages: pages}} + {:ok, %{%DeckState{} | name: name, pages: pages, slide: pages - 1}} end @impl true def handle_call({:page, page}, _from, state) do new_state = %{state | slide: min(state.pages - 1, max(0, page))} - PubSub.broadcast(AlchemyPub.PubSub, new_state.name, new_state) + PubSub.broadcast(AlchemyPub.PubSub, "deck_state", new_state) {:reply, new_state.slide, new_state} end @@ -60,4 +65,9 @@ defmodule AlchemyPub.DeckServer do def handle_call(:get_page, _from, state) do {:reply, state.slide, state} end + + @impl true + def handle_call(:get_count, _from, state) do + {:reply, state.pages, state} + end end diff --git a/lib/alchemy_pub/engine.ex b/lib/alchemy_pub/engine.ex index 4977840..17d1354 100644 --- a/lib/alchemy_pub/engine.ex +++ b/lib/alchemy_pub/engine.ex @@ -46,8 +46,8 @@ defmodule AlchemyPub.Engine do {"href", "##{id}"}, {"class", "mr-1 opacity-20 hover:opacity-60 no-underline"}, {"aria-hidden", "true"}, - {"tabindex", "-1"}, - ], ["#"], meta}, + {"tabindex", "-1"} + ], ["#"], meta} ], meta}} end @@ -154,6 +154,12 @@ defmodule AlchemyPub.Engine do {h1, content} = compile(md, is_deck) filename = path |> Path.basename() |> Path.rootname() title = title(filename, h1, frontmatter) + url = urlify(filename) + + if is_deck do + AlchemyPub.DeckSupervisor.remove_child(url) + AlchemyPub.DeckSupervisor.add_child({url, length(content)}) + end meta = frontmatter @@ -162,10 +168,10 @@ defmodule AlchemyPub.Engine do "date" => date, "rank" => rank, "tags" => tags, - "qr" => qr, + "qr" => qr }) - post = {urlify(filename), rank, date, meta, content} + post = {url, rank, date, meta, content} :ets.insert(@ets, post) post end diff --git a/lib/alchemy_pub/presence.ex b/lib/alchemy_pub/presence.ex index 4673266..0552e73 100644 --- a/lib/alchemy_pub/presence.ex +++ b/lib/alchemy_pub/presence.ex @@ -18,7 +18,7 @@ defmodule AlchemyPub.Presence do path: path, source: source, joined: joined, - path_joined: path_joined, + path_joined: path_joined } <- metas do now = DateTime.utc_now() duration = DateTime.diff(now, path_joined) diff --git a/lib/alchemy_pub/visit.ex b/lib/alchemy_pub/visit.ex index 4232716..585812a 100644 --- a/lib/alchemy_pub/visit.ex +++ b/lib/alchemy_pub/visit.ex @@ -32,7 +32,7 @@ defmodule AlchemyPub.Repo.Visit do duration: duration, duration_total: duration_total, date: date, - hour: now.hour, + hour: now.hour } |> Repo.insert() end diff --git a/lib/alchemy_pub_web/components/layouts/root.html.heex b/lib/alchemy_pub_web/components/layouts/root.html.heex index 5c04264..a782a10 100644 --- a/lib/alchemy_pub_web/components/layouts/root.html.heex +++ b/lib/alchemy_pub_web/components/layouts/root.html.heex @@ -1,5 +1,5 @@ - + @@ -22,7 +22,7 @@ - + {@inner_content} diff --git a/lib/alchemy_pub_web/endpoint.ex b/lib/alchemy_pub_web/endpoint.ex index 4d7506d..0dbf992 100644 --- a/lib/alchemy_pub_web/endpoint.ex +++ b/lib/alchemy_pub_web/endpoint.ex @@ -11,7 +11,7 @@ defmodule AlchemyPubWeb.Endpoint do store: :cookie, key: "_alchemy_pub_key", signing_salt: "1URcYgso", - same_site: "Lax", + same_site: "Lax" ] socket "/live", Socket, diff --git a/lib/alchemy_pub_web/page_analytics.ex b/lib/alchemy_pub_web/page_analytics.ex index 4e19bb7..dd3f485 100644 --- a/lib/alchemy_pub_web/page_analytics.ex +++ b/lib/alchemy_pub_web/page_analytics.ex @@ -71,7 +71,7 @@ defmodule AlchemyPubWeb.DevAnalytics do orientation: :vertical, phx_event_handler: "chart_bar_clicked", colour_palette: :pastel1, - select_item: highlight, + select_item: highlight ] Plot.new(dataset, BarChart, 800, 500, options) diff --git a/lib/alchemy_pub_web/page_live.ex b/lib/alchemy_pub_web/page_live.ex index 6e1b60f..3154e9c 100644 --- a/lib/alchemy_pub_web/page_live.ex +++ b/lib/alchemy_pub_web/page_live.ex @@ -1,6 +1,7 @@ defmodule AlchemyPubWeb.PageLive do use AlchemyPubWeb, :live_view + alias AlchemyPub.DeckServer alias AlchemyPub.Engine alias AlchemyPub.Presence alias Phoenix.PubSub @@ -18,12 +19,13 @@ defmodule AlchemyPubWeb.PageLive do if connected?(socket) do PubSub.subscribe(AlchemyPub.PubSub, "page_update") PubSub.subscribe(AlchemyPub.PubSub, @topic) + PubSub.subscribe(AlchemyPub.PubSub, "deck_state") Presence.track(self(), @topic, socket.id, %{ joined: DateTime.utc_now(), source: referrer, session: session_id, - admin: admin, + admin: admin }) end @@ -35,14 +37,17 @@ defmodule AlchemyPubWeb.PageLive do copyright: %{ name: Keyword.get(copyright, :name), url: Keyword.get(copyright, :url), - license: Keyword.get(copyright, :license), + license: Keyword.get(copyright, :license) }, post_title: "Loading", viewers: 0, track_valid: false, - subpage: nil, + slide: 0, + follow: false, + page_count: nil, fullscreen: false, mute: false, + all_pages: false, admin: admin ) |> stream_configure(:deck, []) @@ -62,26 +67,40 @@ defmodule AlchemyPubWeb.PageLive do {:noreply, socket |> assign(url: uri)} end - defp apply_keypress(param, %{a: a, f: f, m: m, p: p} = params) do + defp apply_keypress(param, assigns) do + %{a: a, f: f, m: m} = params = get_params(assigns) + case param["key"] do "a" -> %{params | a: not a} "f" -> %{params | f: not f} "m" -> %{params | m: not m} - "ArrowRight" -> %{params | p: p + 1} - "ArrowLeft" -> %{params | p: max(0, p - 1)} + "Home" -> %{params | p: 0} + "End" when assigns.admin -> %{params | p: assigns.page_count} + "End" -> %{params | p: -1} + "ArrowRight" -> %{params | p: assigns.slide + 1} + "ArrowLeft" -> %{params | p: max(0, assigns.slide - 1)} "Escape" -> %{params | f: false} _ -> params end end + def get_params(assigns) do + p = + if assigns.follow do + -1 + else + get_in(assigns.slide) || 0 + end + + f = get_in(assigns.fullscreen) || false + m = get_in(assigns.mute) || false + a = get_in(assigns.all_pages) || false + %{a: a, p: p, f: f, m: m} + end + @impl true def handle_event("key", param, socket) do - p = get_in(socket.assigns.subpage) || 0 - f = get_in(socket.assigns.fullscreen) || false - m = get_in(socket.assigns.mute) || false - a = get_in(socket.assigns.all_pages) || false - - params = apply_keypress(param, %{a: a, p: p, f: f, m: m}) + params = apply_keypress(param, socket.assigns) socket = if params && socket.assigns.meta["rank"] == :deck do @@ -97,6 +116,17 @@ defmodule AlchemyPubWeb.PageLive do {:noreply, socket} end + @impl true + def handle_event("jump", %{"slide" => num}, socket) do + params = get_params(socket.assigns) |> Map.merge(%{p: num, f: true, a: false}) + + socket = + socket + |> push_patch(to: "/#{socket.assigns.title}?#{URI.encode_query(params)}", replace: true) + + {:noreply, socket} + end + @impl true def handle_info({:add, _title}, socket) do {:noreply, socket |> rebuild(socket.assigns.params)} @@ -124,11 +154,26 @@ defmodule AlchemyPubWeb.PageLive do {:noreply, socket |> assign(:viewers, viewers)} end + @impl true + def handle_info(%AlchemyPub.DeckState{name: name}, socket) do + title = socket.assigns.title + params = get_params(socket.assigns) + + socket = + if title == name && !socket.assigns.admin && socket.assigns.follow do + socket + |> push_patch(to: "/#{title}?#{URI.encode_query(%{params | p: -1})}", replace: true) + else + socket + end + + {:noreply, socket} + end + defp rebuild(socket, params) do all = Engine.find_sorted() {pages, posts} = all |> Enum.split_with(fn [_, rank | _] -> rank != nil end) pages = pages |> Enum.reverse() - tags = get_unique_tags(all) page = @@ -137,61 +182,10 @@ defmodule AlchemyPubWeb.PageLive do m -> m end - subpage = match_subpage(params) - socket = case page do {title, :deck, _date, meta, content} -> - direction = - cond do - socket.assigns.subpage == nil -> nil - socket.assigns.subpage > subpage -> :left - socket.assigns.subpage < subpage -> :right - true -> nil - end - - fullscreen = params["f"] == "true" - mute = params["m"] == "true" - all_pages = params["a"] == "true" - page_count = Enum.count(content) - - socket = - socket - |> assign( - page_title: meta |> Map.get("title"), - meta: meta, - title: title, - content: "", - tag: nil, - track_valid: true, - subpage: subpage, - fullscreen: fullscreen && !all_pages, - mute: mute && !all_pages, - all_pages: all_pages, - page_count: page_count - ) - - if all_pages do - Enum.reduce(content, socket, fn p, socket -> - stream_insert( - socket, - :deck, - %{ - id: "deck-page-#{Ulid.generate()}", - slide: p, - animation: [], - }, - at: -1, - limit: -page_count - ) - end) - else - socket - |> stream_insert(:deck, paginate(content, subpage, direction, mute), - at: -1, - limit: -2 - ) - end + build_deck(socket, params, {title, meta, content}) {title, _rank, _date, meta, content} -> socket @@ -247,6 +241,55 @@ defmodule AlchemyPubWeb.PageLive do socket |> assign(posts: posts, pages: pages, decks: decks, tags: tags, params: params) end + defp build_deck(socket, params, {title, meta, content}) do + page_count = DeckServer.get_count(title) + %{admin: admin, slide: slide} = socket.assigns + {slide, direction} = match_slide(params, title, slide, page_count, admin) + + fullscreen = params["f"] == "true" + mute = params["m"] == "true" + all_pages = params["a"] == "true" && admin + follow = params["p"] == "-1" && !admin + + socket = + socket + |> assign( + page_title: meta |> Map.get("title"), + meta: meta, + title: title, + content: "", + tag: nil, + track_valid: true, + slide: slide, + follow: follow, + fullscreen: fullscreen && !all_pages, + mute: mute && !all_pages, + all_pages: all_pages, + page_count: page_count + ) + + if all_pages do + content + |> Enum.with_index() + |> Enum.reduce(socket, fn {p, num}, socket -> + stream_insert( + socket, + :deck, + %{ + id: "deck-page-#{Ulid.generate()}", + num: num, + slide: p, + animation: "" + }, + at: -1, + limit: -page_count + ) + end) + else + socket |> stream_insert(:deck, paginate(content, slide, direction, mute), at: -1, limit: -2) + end + end + defp filter_rank(pages, type) when is_atom(type) do pages |> Enum.filter(fn [_, rank | _] -> rank == type end) end @@ -269,13 +312,14 @@ defmodule AlchemyPubWeb.PageLive do defp paginate(deck, page, direction, mute) do animation = case direction do - :left -> ["-translate-x-1/4", "scale-90", "opacity-0"] - :right -> ["translate-x-1/4", "scale-110", "opacity-0"] - nil -> [] + :left -> "-translate-x-1/4 scale-90 opacity-0" + :right -> "translate-x-1/4 scale-110 opacity-0" + nil -> "" end %{ id: "deck-page-#{Ulid.generate()}", + num: page, slide: if mute do "" @@ -284,10 +328,10 @@ defmodule AlchemyPubWeb.PageLive do end, animation: if mute do - ["opacity-0"] + "opacity-0" else animation - end, + end } end @@ -314,14 +358,33 @@ defmodule AlchemyPubWeb.PageLive do "Tag not found" end - defp match_subpage(%{"p" => p}) do - case Integer.parse(p) do - {page, _} -> page - _ -> 0 - end + defp match_slide(%{"p" => p}, title, prev_page, page_count, admin) do + page = + case Integer.parse(p) do + {page, _} -> page + _ -> 0 + end + + page = + case {admin, page} do + {true, -1} -> DeckServer.set_page(title, page_count) + {true, p} -> DeckServer.set_page(title, p) + {_, -1} -> DeckServer.get_page(title) + {_, p} -> min(p, DeckServer.get_page(title)) + end + + direction = + cond do + prev_page == nil -> nil + prev_page > page -> :left + prev_page < page -> :right + true -> nil + end + + {page, direction} end - defp match_subpage(_), do: 0 + defp match_slide(_, _, _, _, _), do: {0, nil} defp match_params(params, pages) do case params do @@ -349,9 +412,7 @@ defmodule AlchemyPubWeb.PageLive do |> Enum.uniq_by(fn {t, _} -> t end) end - defp remove_animation(js \\ %JS{}, classes, id) do - Enum.reduce(classes, js, fn c, js -> - JS.remove_class(js, c, to: "##{id}") - end) + defp animation(classes) do + JS.transition({"ease-in-out duration-150", classes, "opacity-100 scale-100 translate-0"}) end end diff --git a/lib/alchemy_pub_web/page_live.html.heex b/lib/alchemy_pub_web/page_live.html.heex index 8d8c5a7..c5bb0d0 100644 --- a/lib/alchemy_pub_web/page_live.html.heex +++ b/lib/alchemy_pub_web/page_live.html.heex @@ -19,11 +19,37 @@ class="size-5" /> {raw(meta["title"])} - <%= if Map.get(meta, "tags" , []) |> Enum.any?(fn t -> Engine.urlify(t) == @tag end) do %> - - <% end %> + Enum.any?(fn t -> Engine.urlify(t) == @tag end) + } + class="badge badge-xs badge-info" + /> +
  • + +
      +
    • + <.link + class={(@title == title && active_class()) || ""} + patch={"/#{title}"} + phx-click={JS.dispatch("click", to: "#menu-open")} + > + {raw(meta["title"])} + Enum.any?(fn t -> Engine.urlify(t) == @tag end) + } + class="badge badge-xs badge-info" + /> + +
    • +
    +
  • <%= for {{year, _}, posts} <- Enum.group_by(@posts, fn [_, _, date | _] -> Date.year_of_era(date) end) |> Enum.sort(:desc) do %> @@ -48,22 +74,6 @@ <% end %>
  • -
  • - -
      -
    • - <.link - class={(@title == title && active_class()) || ""} - patch={"/#{title}"} - phx-click={JS.dispatch("click", to: "#menu-open")} - > - {raw(meta["title"])} - -
    • -
    -