Skip to content

Commit

Permalink
feat: link scheduling (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
AfonsoMartins26 authored Feb 27, 2025
1 parent 39a59b1 commit a5df8d4
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 13 deletions.
2 changes: 2 additions & 0 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
35 changes: 35 additions & 0 deletions assets/js/hooks/timer.js
Original file line number Diff line number Diff line change
@@ -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();
},
};
6 changes: 5 additions & 1 deletion lib/cesium_link/links.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
3 changes: 2 additions & 1 deletion lib/cesium_link/links/link.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions lib/cesium_link_web/live/link_live/form_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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</.button>
</:actions>
Expand Down Expand Up @@ -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)}
Expand All @@ -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)}
Expand Down
21 changes: 20 additions & 1 deletion lib/cesium_link_web/live/link_live/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
39 changes: 31 additions & 8 deletions lib/cesium_link_web/live/link_live/index.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,53 @@
</.header>

<.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>
<: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>

<:col :let={{_id, link}} label="Name">
<p class="font-semibold text-zinc-900"><%= link.name %></p>
<p class={"font-semibold text-zinc-900 #{if link.in_future, do: "opacity-50"}"}><%= link.name %></p>
</:col>

<:col :let={{_id, link}} label="Emoji">
<.emoji code={link.emoji} />
<span class={if link.in_future, do: "opacity-50"}>
<.emoji code={link.emoji} />
</span>
</:col>

<: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) %>
</.link>
</:col>
<:col :let={{_id, link}} label="Clicks"><%= link.visits %></:col>

<:col :let={{_id, link}} label="Time to publish">
<%= if link.in_future do %>
<div id="timer-countdown" phx-hook="Timer" data-finish-time={DateTime.to_unix(link.publish_at)} class="opacity-50">
00:00:00
</div>
<% end %>
</:col>

<:col :let={{_id, link}} label="Clicks">
<p class={if link.in_future, do: "opacity-50"}>
<%= link.visits %>
</p>
</:col>

<:col :let={{_id, link}} label="Attention">
<input type="checkbox" disabled={true} checked={link.attention} class="self-center block rounded-md text-gray-600" />
<input type="checkbox" disabled={true} checked={link.attention} class={"self-center block rounded-md text-gray-600 #{if link.in_future, do: "opacity-50"}"} />
</:col>

<: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"}"} />
</.link>
</:action>

<: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"}"} />
</.link>
</:action>
</.table>
Expand Down
9 changes: 9 additions & 0 deletions priv/repo/migrations/20240918181533_add_link_scheduling.exs
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit a5df8d4

Please sign in to comment.