diff --git a/assets/js/app.js b/assets/js/app.js index 9e122be..faede14 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -24,12 +24,14 @@ import topbar from "../vendor/topbar" import { Sorting } from "./hooks/sorting" import { EmojiPicker } from "./hooks/emoji" import { QRCodeGenerator } from "./hooks/qrcode-generator" +import { Timer } from "./hooks/timer" // JS Hooks let Hooks = {} Hooks.Sorting = Sorting; Hooks.EmojiPicker = EmojiPicker; Hooks.QRCodeGenerator = QRCodeGenerator; +Hooks.Timer = Timer; let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let liveSocket = new LiveSocket("/live", Socket, { diff --git a/assets/js/hooks/timer.js b/assets/js/hooks/timer.js new file mode 100644 index 0000000..8c6cf1c --- /dev/null +++ b/assets/js/hooks/timer.js @@ -0,0 +1,35 @@ +export const Timer = { + mounted() { + const finishTime = parseInt(this.el.dataset.finishTime, 10); + const timerElement = this.el; + + const updateTimer = () => { + const now = Math.floor(Date.now() / 1000); + const remainingTime = Math.max(0, finishTime - now); + + if (remainingTime <= 0) { + clearInterval(timerInterval); + timerElement.textContent = "00:00:00"; + this.pushEvent("end-time", {}); + return; + } + + const hours = Math.floor(remainingTime / 3600); + const minutes = Math.floor((remainingTime % 3600) / 60); + const seconds = remainingTime % 60; + + timerElement.textContent = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + }; + + const timerInterval = setInterval(updateTimer, 1000); + updateTimer(); + + this.cleanup = () => { + clearInterval(timerInterval); + }; + }, + + destroyed() { + this.cleanup(); + }, +}; diff --git a/lib/cesium_link/links.ex b/lib/cesium_link/links.ex index 9d05aa4..b2c8e13 100644 --- a/lib/cesium_link/links.ex +++ b/lib/cesium_link/links.ex @@ -64,7 +64,11 @@ defmodule CesiumLink.Links do """ def list_unarchived_links_by_index do - Repo.all(from l in Link, where: l.archived == false, order_by: [asc: l.index]) + Link + |> where([l], fragment("? <= now() OR ? IS NULL", l.publish_at, l.publish_at)) + |> where([l], not l.archived) + |> order_by([l], asc: l.index) + |> Repo.all() end @doc """ diff --git a/lib/cesium_link/links/link.ex b/lib/cesium_link/links/link.ex index e30d9d5..c9f8d39 100644 --- a/lib/cesium_link/links/link.ex +++ b/lib/cesium_link/links/link.ex @@ -5,7 +5,7 @@ defmodule CesiumLink.Links.Link do use CesiumLink.Schema @required_fields ~w(name emoji url attention edited_at)a - @optional_fields ~w(index archived visits)a + @optional_fields ~w(index archived visits publish_at)a schema "links" do field :archived, :boolean, default: false @@ -16,6 +16,7 @@ defmodule CesiumLink.Links.Link do field :url, :string field :visits, :integer, default: 0 field :edited_at, :utc_datetime + field :publish_at, :utc_datetime timestamps(type: :utc_datetime) end diff --git a/lib/cesium_link_web/live/link_live/form_component.ex b/lib/cesium_link_web/live/link_live/form_component.ex index dff7386..33adef4 100644 --- a/lib/cesium_link_web/live/link_live/form_component.ex +++ b/lib/cesium_link_web/live/link_live/form_component.ex @@ -19,6 +19,9 @@ defmodule CesiumLinkWeb.LinkLive.FormComponent do <.input field={@form[:emoji]} type="emoji" label="Emoji" /> <.input field={@form[:url]} type="text" label="URL" /> <.input field={@form[:attention]} type="checkbox" label="Attention" /> + <%= if @action == :new do %> + <.input field={@form[:publish_at]} type="datetime-local" label="Publish At" /> + <% end %> <:actions> <.button phx-disable-with="Saving...">Save Link @@ -62,7 +65,7 @@ defmodule CesiumLinkWeb.LinkLive.FormComponent do {:noreply, socket |> put_flash(:info, "Link updated successfully") - |> push_patch(to: socket.assigns.patch)} + |> push_navigate(to: ~p"/admin/links")} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign_form(socket, changeset)} @@ -81,7 +84,7 @@ defmodule CesiumLinkWeb.LinkLive.FormComponent do {:noreply, socket |> put_flash(:info, "Link created successfully") - |> push_patch(to: socket.assigns.patch)} + |> push_navigate(to: ~p"/admin/links")} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign_form(socket, changeset)} diff --git a/lib/cesium_link_web/live/link_live/index.ex b/lib/cesium_link_web/live/link_live/index.ex index a1fa7c3..319977d 100644 --- a/lib/cesium_link_web/live/link_live/index.ex +++ b/lib/cesium_link_web/live/link_live/index.ex @@ -6,7 +6,15 @@ defmodule CesiumLinkWeb.LinkLive.Index do @impl true def mount(_params, _session, socket) do - {:ok, stream(socket, :links, Links.list_unarchived_links())} + links = Links.list_unarchived_links() + + enriched_links = + Enum.map(links, fn link -> + in_future = publish_in_future?(link) + Map.put(link, :in_future, in_future) + end) + + {:ok, stream(socket, :links, enriched_links)} end @impl true @@ -66,4 +74,15 @@ defmodule CesiumLinkWeb.LinkLive.Index do {:noreply, socket} end + + def handle_event("end-time", _, socket) do + {:noreply, socket |> push_navigate(to: ~p"/admin/links")} + end + + def published?(%Link{publish_at: nil}), do: true + + def published?(%Link{publish_at: publish_at}), + do: DateTime.compare(publish_at, DateTime.utc_now()) == :lt + + defp publish_in_future?(link), do: not published?(link) end diff --git a/lib/cesium_link_web/live/link_live/index.html.heex b/lib/cesium_link_web/live/link_live/index.html.heex index 3be9618..be0d9a1 100644 --- a/lib/cesium_link_web/live/link_live/index.html.heex +++ b/lib/cesium_link_web/live/link_live/index.html.heex @@ -15,30 +15,53 @@ <.table id="links" rows={@streams.links} phx-hook="Sorting"> - <:col :let={{_id, _link}}><.icon name="hero-bars-3 cursor-pointer ml-4" class="handle w-5 h-5" /> + <:col :let={{_id, link}}> + <.icon name="hero-bars-3 cursor-pointer ml-4" class={"handle w-5 h-5 #{if link.in_future, do: "opacity-50"}"} /> + + <:col :let={{_id, link}} label="Name"> -

<%= link.name %>

+

<%= link.name %>

+ <:col :let={{_id, link}} label="Emoji"> - <.emoji code={link.emoji} /> + + <.emoji code={link.emoji} /> + + <:col :let={{_id, link}} label="URL"> - <.link target="_blank" class="hover:text-brand hover:underline" navigate={link.url}> + <.link target="_blank" class={"hover:text-brand hover:underline #{if link.in_future, do: "opacity-50"}"} navigate={link.url}> <%= truncate_elipsis(link.url, 50) %> - <:col :let={{_id, link}} label="Clicks"><%= link.visits %> + + <:col :let={{_id, link}} label="Time to publish"> + <%= if link.in_future do %> +
+ 00:00:00 +
+ <% end %> + + + <:col :let={{_id, link}} label="Clicks"> +

+ <%= link.visits %> +

+ + <:col :let={{_id, link}} label="Attention"> - + + <:action :let={{_id, link}}> <.link patch={~p"/admin/links/#{link}/edit"}> - <.icon name="hero-pencil" class="w-5 h-5" /> + <.icon name="hero-pencil" class={"w-5 h-5 #{if link.in_future, do: "opacity-50"}"} /> + <:action :let={{_id, link}}> <.link patch={~p"/admin/links/#{link}/archive"}> - <.icon name="hero-archive-box" class="w-5 h-5" /> + <.icon name="hero-archive-box" class={"w-5 h-5 #{if link.in_future, do: "opacity-50"}"} /> diff --git a/priv/repo/migrations/20240918181533_add_link_scheduling.exs b/priv/repo/migrations/20240918181533_add_link_scheduling.exs new file mode 100644 index 0000000..2fd3685 --- /dev/null +++ b/priv/repo/migrations/20240918181533_add_link_scheduling.exs @@ -0,0 +1,9 @@ +defmodule CesiumLink.Repo.Migrations.AddLinkScheduling do + use Ecto.Migration + + def change do + alter table(:links) do + add :publish_at, :utc_datetime + end + end +end