Skip to content

Commit a5df8d4

Browse files
feat: link scheduling (#75)
1 parent 39a59b1 commit a5df8d4

File tree

8 files changed

+109
-13
lines changed

8 files changed

+109
-13
lines changed

assets/js/app.js

+2
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ import topbar from "../vendor/topbar"
2424
import { Sorting } from "./hooks/sorting"
2525
import { EmojiPicker } from "./hooks/emoji"
2626
import { QRCodeGenerator } from "./hooks/qrcode-generator"
27+
import { Timer } from "./hooks/timer"
2728

2829
// JS Hooks
2930
let Hooks = {}
3031
Hooks.Sorting = Sorting;
3132
Hooks.EmojiPicker = EmojiPicker;
3233
Hooks.QRCodeGenerator = QRCodeGenerator;
34+
Hooks.Timer = Timer;
3335

3436
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
3537
let liveSocket = new LiveSocket("/live", Socket, {

assets/js/hooks/timer.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export const Timer = {
2+
mounted() {
3+
const finishTime = parseInt(this.el.dataset.finishTime, 10);
4+
const timerElement = this.el;
5+
6+
const updateTimer = () => {
7+
const now = Math.floor(Date.now() / 1000);
8+
const remainingTime = Math.max(0, finishTime - now);
9+
10+
if (remainingTime <= 0) {
11+
clearInterval(timerInterval);
12+
timerElement.textContent = "00:00:00";
13+
this.pushEvent("end-time", {});
14+
return;
15+
}
16+
17+
const hours = Math.floor(remainingTime / 3600);
18+
const minutes = Math.floor((remainingTime % 3600) / 60);
19+
const seconds = remainingTime % 60;
20+
21+
timerElement.textContent = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
22+
};
23+
24+
const timerInterval = setInterval(updateTimer, 1000);
25+
updateTimer();
26+
27+
this.cleanup = () => {
28+
clearInterval(timerInterval);
29+
};
30+
},
31+
32+
destroyed() {
33+
this.cleanup();
34+
},
35+
};

lib/cesium_link/links.ex

+5-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ defmodule CesiumLink.Links do
6464
6565
"""
6666
def list_unarchived_links_by_index do
67-
Repo.all(from l in Link, where: l.archived == false, order_by: [asc: l.index])
67+
Link
68+
|> where([l], fragment("? <= now() OR ? IS NULL", l.publish_at, l.publish_at))
69+
|> where([l], not l.archived)
70+
|> order_by([l], asc: l.index)
71+
|> Repo.all()
6872
end
6973

7074
@doc """

lib/cesium_link/links/link.ex

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule CesiumLink.Links.Link do
55
use CesiumLink.Schema
66

77
@required_fields ~w(name emoji url attention edited_at)a
8-
@optional_fields ~w(index archived visits)a
8+
@optional_fields ~w(index archived visits publish_at)a
99

1010
schema "links" do
1111
field :archived, :boolean, default: false
@@ -16,6 +16,7 @@ defmodule CesiumLink.Links.Link do
1616
field :url, :string
1717
field :visits, :integer, default: 0
1818
field :edited_at, :utc_datetime
19+
field :publish_at, :utc_datetime
1920

2021
timestamps(type: :utc_datetime)
2122
end

lib/cesium_link_web/live/link_live/form_component.ex

+5-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ defmodule CesiumLinkWeb.LinkLive.FormComponent do
1919
<.input field={@form[:emoji]} type="emoji" label="Emoji" />
2020
<.input field={@form[:url]} type="text" label="URL" />
2121
<.input field={@form[:attention]} type="checkbox" label="Attention" />
22+
<%= if @action == :new do %>
23+
<.input field={@form[:publish_at]} type="datetime-local" label="Publish At" />
24+
<% end %>
2225
<:actions>
2326
<.button phx-disable-with="Saving...">Save Link</.button>
2427
</:actions>
@@ -62,7 +65,7 @@ defmodule CesiumLinkWeb.LinkLive.FormComponent do
6265
{:noreply,
6366
socket
6467
|> put_flash(:info, "Link updated successfully")
65-
|> push_patch(to: socket.assigns.patch)}
68+
|> push_navigate(to: ~p"/admin/links")}
6669

6770
{:error, %Ecto.Changeset{} = changeset} ->
6871
{:noreply, assign_form(socket, changeset)}
@@ -81,7 +84,7 @@ defmodule CesiumLinkWeb.LinkLive.FormComponent do
8184
{:noreply,
8285
socket
8386
|> put_flash(:info, "Link created successfully")
84-
|> push_patch(to: socket.assigns.patch)}
87+
|> push_navigate(to: ~p"/admin/links")}
8588

8689
{:error, %Ecto.Changeset{} = changeset} ->
8790
{:noreply, assign_form(socket, changeset)}

lib/cesium_link_web/live/link_live/index.ex

+20-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ defmodule CesiumLinkWeb.LinkLive.Index do
66

77
@impl true
88
def mount(_params, _session, socket) do
9-
{:ok, stream(socket, :links, Links.list_unarchived_links())}
9+
links = Links.list_unarchived_links()
10+
11+
enriched_links =
12+
Enum.map(links, fn link ->
13+
in_future = publish_in_future?(link)
14+
Map.put(link, :in_future, in_future)
15+
end)
16+
17+
{:ok, stream(socket, :links, enriched_links)}
1018
end
1119

1220
@impl true
@@ -66,4 +74,15 @@ defmodule CesiumLinkWeb.LinkLive.Index do
6674

6775
{:noreply, socket}
6876
end
77+
78+
def handle_event("end-time", _, socket) do
79+
{:noreply, socket |> push_navigate(to: ~p"/admin/links")}
80+
end
81+
82+
def published?(%Link{publish_at: nil}), do: true
83+
84+
def published?(%Link{publish_at: publish_at}),
85+
do: DateTime.compare(publish_at, DateTime.utc_now()) == :lt
86+
87+
defp publish_in_future?(link), do: not published?(link)
6988
end

lib/cesium_link_web/live/link_live/index.html.heex

+31-8
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,53 @@
1515
</.header>
1616

1717
<.table id="links" rows={@streams.links} phx-hook="Sorting">
18-
<:col :let={{_id, _link}}><.icon name="hero-bars-3 cursor-pointer ml-4" class="handle w-5 h-5" /></:col>
18+
<:col :let={{_id, link}}>
19+
<.icon name="hero-bars-3 cursor-pointer ml-4" class={"handle w-5 h-5 #{if link.in_future, do: "opacity-50"}"} />
20+
</:col>
21+
1922
<:col :let={{_id, link}} label="Name">
20-
<p class="font-semibold text-zinc-900"><%= link.name %></p>
23+
<p class={"font-semibold text-zinc-900 #{if link.in_future, do: "opacity-50"}"}><%= link.name %></p>
2124
</:col>
25+
2226
<:col :let={{_id, link}} label="Emoji">
23-
<.emoji code={link.emoji} />
27+
<span class={if link.in_future, do: "opacity-50"}>
28+
<.emoji code={link.emoji} />
29+
</span>
2430
</:col>
31+
2532
<:col :let={{_id, link}} label="URL">
26-
<.link target="_blank" class="hover:text-brand hover:underline" navigate={link.url}>
33+
<.link target="_blank" class={"hover:text-brand hover:underline #{if link.in_future, do: "opacity-50"}"} navigate={link.url}>
2734
<%= truncate_elipsis(link.url, 50) %>
2835
</.link>
2936
</:col>
30-
<:col :let={{_id, link}} label="Clicks"><%= link.visits %></:col>
37+
38+
<:col :let={{_id, link}} label="Time to publish">
39+
<%= if link.in_future do %>
40+
<div id="timer-countdown" phx-hook="Timer" data-finish-time={DateTime.to_unix(link.publish_at)} class="opacity-50">
41+
00:00:00
42+
</div>
43+
<% end %>
44+
</:col>
45+
46+
<:col :let={{_id, link}} label="Clicks">
47+
<p class={if link.in_future, do: "opacity-50"}>
48+
<%= link.visits %>
49+
</p>
50+
</:col>
51+
3152
<:col :let={{_id, link}} label="Attention">
32-
<input type="checkbox" disabled={true} checked={link.attention} class="self-center block rounded-md text-gray-600" />
53+
<input type="checkbox" disabled={true} checked={link.attention} class={"self-center block rounded-md text-gray-600 #{if link.in_future, do: "opacity-50"}"} />
3354
</:col>
55+
3456
<:action :let={{_id, link}}>
3557
<.link patch={~p"/admin/links/#{link}/edit"}>
36-
<.icon name="hero-pencil" class="w-5 h-5" />
58+
<.icon name="hero-pencil" class={"w-5 h-5 #{if link.in_future, do: "opacity-50"}"} />
3759
</.link>
3860
</:action>
61+
3962
<:action :let={{_id, link}}>
4063
<.link patch={~p"/admin/links/#{link}/archive"}>
41-
<.icon name="hero-archive-box" class="w-5 h-5" />
64+
<.icon name="hero-archive-box" class={"w-5 h-5 #{if link.in_future, do: "opacity-50"}"} />
4265
</.link>
4366
</:action>
4467
</.table>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
defmodule CesiumLink.Repo.Migrations.AddLinkScheduling do
2+
use Ecto.Migration
3+
4+
def change do
5+
alter table(:links) do
6+
add :publish_at, :utc_datetime
7+
end
8+
end
9+
end

0 commit comments

Comments
 (0)