diff --git a/lib/annot_at_web/components/layouts.ex b/lib/annot_at_web/components/layouts.ex index 61978ec..f47a40d 100644 --- a/lib/annot_at_web/components/layouts.ex +++ b/lib/annot_at_web/components/layouts.ex @@ -107,20 +107,6 @@ defmodule AnnotAtWeb.Layouts do > Overview - -
- Publish -
- <.dash_nav_item icon="hero-globe-alt" navigate={~p"/sites"} active={@active == :sites}> - Sites - - -
- Account -
- <.dash_nav_item icon="hero-cog-6-tooth">Settings
diff --git a/lib/annot_at_web/controllers/live/dashboard_live.ex b/lib/annot_at_web/controllers/live/dashboard_live.ex index 6fc328d..dfd659f 100644 --- a/lib/annot_at_web/controllers/live/dashboard_live.ex +++ b/lib/annot_at_web/controllers/live/dashboard_live.ex @@ -3,16 +3,17 @@ defmodule AnnotAtWeb.DashboardLive do import AnnotAtWeb.SiteComponents, only: [site_row: 1] + alias AnnotAt.Atproto.StandardSite alias AnnotAt.Publishing + alias AnnotAt.Publishing.Site + alias Phoenix.LiveView.AsyncResult + + require Logger @impl Phoenix.LiveView def render(assigns) do ~H""" - <.banner class="my-6" icon="hero-exclamation-triangle"> - This app is in active development. - -
@@ -20,8 +21,7 @@ defmodule AnnotAtWeb.DashboardLive do sm:text-4xl"> Hi {@current_scope.user.display_name || @current_scope.user.handle} -

Here's what you're publishing to the - ATmosphere.

+

Here's what you're publishing to the ATmosphere.

<.link navigate={~p"/sites/new"} @@ -33,75 +33,93 @@ defmodule AnnotAtWeb.DashboardLive do <.icon name="hero-plus" class="size-5" /> Add a site
-
- <.stat_card - label="Sites" - value={"#{length(@sites)}"} - sub="in your account" - tint="bg-sky-light" - shadow="shadow-[5px_5px_0px_0px_var(--color-sky-bold)]" - /> - <.stat_card - label="Posts" - value="0" - sub="published" - tint="bg-peach-light" - shadow="shadow-[5px_5px_0px_0px_var(--color-peach-bold)]" - /> - <.stat_card - label="This week" - value="0" - sub="last 7 days" - tint="bg-sky-light" - shadow="shadow-[5px_5px_0px_0px_var(--color-sky-bold)]" - /> - <.stat_card - label="Bluesky" - value="Off" - sub="cross-posting" - tint="bg-peach-light" - shadow="shadow-[5px_5px_0px_0px_var(--color-peach-bold)]" - /> -
-
-

Your sites

- <.link - :if={@sites != []} - navigate={~p"/sites"} - class="text-sm font-bold text-ink/55 hover:text-ink" - > - View all → - -
+ -
-
- <.icon name="hero-globe-alt" class="size-7" /> -
-

You haven't added any sites yet

-

- Connect a blog's RSS feed and we'll publish every new post to the - ATmosphere. -

- <.link - navigate={~p"/sites/new"} - class="mt-5 inline-flex items-center gap-1.5 rounded-xl bg-ink px-5 - py-2.5 text-sm font-bold text-paper transition-all hover:scale-[1.02] - active:scale-[0.98]" - > - <.icon name="hero-plus" class="size-4" /> Add your first site - -
+ <%= case @tab do %> + <% :setup -> %> +
+ +
-
- <.site_row :for={site <- Enum.take(@sites, 5)} site={site} /> -
+
+

No sites here yet.

+ <.site_row :for={site <- @visible} site={site} /> +
+
+
+
+
{site.url}
+
Publication missing from your repo
+
+ + <.button variant="ghost" size="sm" phx-value-id={site.id} phx-click="delete_site"> + <.icon name="hero-trash" class="size-4" /> Delete + +
+
+ <% :discovered -> %> + <.async_result :let={_pubs} assign={@publications}> + <:loading> +
+ <.icon name="hero-arrow-path" class="size-5 animate-spin" /> Reading your repo… +
+ + <:failed :let={_}> +

Couldn't read your repo.

+ + +

+ No new publications found in your repo. +

+ +
+
+
+
{pub.name || pub.url}
+
{pub.url}
+
{pub.rkey}
+
+ + <.link + navigate={~p"/sites/import/#{pub.rkey}"} + class="inline-flex flex-none items-center gap-1.5 rounded-xl border-2 + border-ink bg-ink px-4 py-1.5 text-xs font-bold text-paper transition-all + hover:-translate-y-0.5" + > + Import + +
+
+ + <% end %>
""" end @@ -109,6 +127,140 @@ defmodule AnnotAtWeb.DashboardLive do @impl Phoenix.LiveView def mount(_params, _session, socket) do sites = Publishing.list_sites(socket.assigns.current_scope) - {:ok, assign(socket, page_title: "Dashboard", sites: sites)} + + socket = + socket + |> assign( + page_title: "Sites", + sites: sites, + filter: "all", + visible: sites, + broken: [], + discovered: [] + ) + |> load_publications() + + {:ok, socket} + end + + @impl Phoenix.LiveView + def handle_params(params, _uri, socket) do + tab = + if Map.get(params, "tab") == "discovered" do + :discovered + else + :setup + end + + {:noreply, assign(socket, tab: tab)} + end + + @impl Phoenix.LiveView + def handle_event("filter", %{"status" => status}, socket) do + visible = filter_sites(socket.assigns.sites -- socket.assigns.broken, status) + + {:noreply, + assign(socket, + filter: status, + visible: visible + )} + end + + def handle_event("delete_site", %{"id" => id}, socket) do + scope = socket.assigns.current_scope + site = Publishing.get_site!(scope, id) + {:ok, _} = Publishing.delete_site(scope, site) + + sites = Enum.reject(socket.assigns.sites, &(&1.id == site.id)) + broken = Enum.reject(socket.assigns.broken, &(&1.id == site.id)) + + {:noreply, + assign(socket, + sites: sites, + broken: broken, + visible: filter_sites(sites -- broken, socket.assigns.filter) + )} + end + + @impl Phoenix.LiveView + def handle_async(:load_publications, {:ok, {:ok, publications}}, socket) do + rkeys = MapSet.new(publications, & &1.rkey) + broken = Enum.filter(socket.assigns.sites, &broken?(&1, rkeys)) + + {:noreply, + assign(socket, + publications: AsyncResult.ok(socket.assigns.publications, publications), + broken: broken, + discovered: discovered(publications, socket.assigns.sites), + visible: filter_sites(socket.assigns.sites -- broken, socket.assigns.filter) + )} + end + + def handle_async(:load_publications, {:ok, {:error, reason}}, socket) do + Logger.warning("SitesLive: failed to load publications", reason: inspect(reason)) + + {:noreply, + assign(socket, + publications: + AsyncResult.failed( + socket.assigns.publications, + reason + ) + )} + end + + defp filter_sites(sites, "all"), do: sites + + defp filter_sites(sites, status) do + Enum.filter(sites, &(Site.status(&1) == status)) + end + + defp filter_label("all"), do: "All" + + defp filter_label(status) do + String.capitalize(status) + end + + defp load_publications(socket) do + if connected?(socket) do + user = socket.assigns.current_scope.user + + socket + |> assign(publications: AsyncResult.loading()) + |> start_async(:load_publications, fn -> StandardSite.list_publications(user.id) end) + else + assign(socket, publications: AsyncResult.loading()) + end + end + + defp broken?(site, publication_rkeys) do + is_binary(site.rkey) and not is_nil(site.published_at) and + not MapSet.member?(publication_rkeys, site.rkey) + end + + defp discovered(publications, sites) do + site_rkeys = MapSet.new(sites, & &1.rkey) + Enum.reject(publications, &MapSet.member?(site_rkeys, &1.rkey)) + end + + attr :patch, :string, required: true + attr :active, :boolean, required: true + slot :inner_block, required: true + + defp tab_link(assigns) do + ~H""" + <.link + patch={@patch} + class={[ + "rounded-full border-2 px-3 py-1 text-sm font-bold transition-colors", + if(@active, + do: "border-ink bg-ink text-paper", + else: "border-ink/15 text-ink/55 hover:border-ink/40 hover:text-ink" + ) + ]} + > + {render_slot(@inner_block)} + + """ end end diff --git a/lib/annot_at_web/controllers/live/import_live.ex b/lib/annot_at_web/controllers/live/import_live.ex index f44e66a..c3339b2 100644 --- a/lib/annot_at_web/controllers/live/import_live.ex +++ b/lib/annot_at_web/controllers/live/import_live.ex @@ -14,7 +14,7 @@ defmodule AnnotAtWeb.ImportLive do ~H""" <.link - navigate={~p"/sites?tab=discovered"} + navigate={~p"/?tab=discovered"} class="text-sm font-bold text-ink/50 hover:text-ink" > ← Discovered diff --git a/lib/annot_at_web/controllers/live/site_live.ex b/lib/annot_at_web/controllers/live/site_live.ex index 322a752..d05f02c 100644 --- a/lib/annot_at_web/controllers/live/site_live.ex +++ b/lib/annot_at_web/controllers/live/site_live.ex @@ -237,7 +237,7 @@ defmodule AnnotAtWeb.SiteLive do socket = socket |> put_flash(:info, "Site disconnected.") - |> push_navigate(to: ~p"/sites") + |> push_navigate(to: ~p"/") {:noreply, socket} end diff --git a/lib/annot_at_web/controllers/live/sites_live.ex b/lib/annot_at_web/controllers/live/sites_live.ex deleted file mode 100644 index 47c87e6..0000000 --- a/lib/annot_at_web/controllers/live/sites_live.ex +++ /dev/null @@ -1,262 +0,0 @@ -defmodule AnnotAtWeb.SitesLive do - use AnnotAtWeb, :live_view - - import AnnotAtWeb.SiteComponents, only: [site_row: 1] - - alias AnnotAt.Atproto.StandardSite - alias AnnotAt.Publishing - alias AnnotAt.Publishing.Site - alias Phoenix.LiveView.AsyncResult - - require Logger - - @impl Phoenix.LiveView - def render(assigns) do - ~H""" - -
-

Sites

- <.link - navigate={~p"/sites/new"} - class="inline-flex items-center gap-1.5 self-start rounded-xl border-2 - border-ink bg-ink px-5 py-2.5 text-sm font-bold text-paper - shadow-[4px_4px_0px_0px_var(--color-peach-bold)] transition-all - hover:-translate-y-0.5 active:translate-y-0" - > - <.icon name="hero-plus" class="size-5" /> Add a site - -
- - - - <%= case @tab do %> - <% :setup -> %> -
- -
- -
-

No sites here yet.

- <.site_row :for={site <- @visible} site={site} /> -
-
-
-
-
{site.url}
-
Publication missing from your - repo
-
- - <.button variant="ghost" size="sm" phx-value-id={site.id} phx-click="delete_site"> - <.icon name="hero-trash" class="size-4" /> Delete - -
-
- <% :discovered -> %> - <.async_result :let={_pubs} assign={@publications}> - <:loading> -
- <.icon name="hero-arrow-path" class="size-5 animate-spin" /> Reading your - repo… -
- - <:failed :let={_}> -

Couldn't read your repo.

- - -

- No new publications found in your repo. -

- -
-
-
-
{pub.name || pub.url}
-
{pub.url}
-
{pub.rkey}
-
- - <.link - navigate={~p"/sites/import/#{pub.rkey}"} - class="inline-flex flex-none items-center gap-1.5 rounded-xl border-2 - border-ink bg-ink px-4 py-1.5 text-xs font-bold text-paper transition-all - hover:-translate-y-0.5" - > - Import - -
-
- - <% end %> -
- """ - end - - @impl Phoenix.LiveView - def mount(_params, _session, socket) do - sites = Publishing.list_sites(socket.assigns.current_scope) - - socket = - socket - |> assign( - page_title: "Sites", - sites: sites, - filter: "all", - visible: sites, - broken: [], - discovered: [] - ) - |> load_publications() - - {:ok, socket} - end - - @impl Phoenix.LiveView - def handle_params(params, _uri, socket) do - tab = - if Map.get(params, "tab") == "discovered" do - :discovered - else - :setup - end - - {:noreply, assign(socket, tab: tab)} - end - - @impl Phoenix.LiveView - def handle_event("filter", %{"status" => status}, socket) do - visible = filter_sites(socket.assigns.sites -- socket.assigns.broken, status) - - {:noreply, - assign(socket, - filter: status, - visible: visible - )} - end - - def handle_event("delete_site", %{"id" => id}, socket) do - scope = socket.assigns.current_scope - site = Publishing.get_site!(scope, id) - {:ok, _} = Publishing.delete_site(scope, site) - - sites = Enum.reject(socket.assigns.sites, &(&1.id == site.id)) - broken = Enum.reject(socket.assigns.broken, &(&1.id == site.id)) - - {:noreply, - assign(socket, - sites: sites, - broken: broken, - visible: filter_sites(sites -- broken, socket.assigns.filter) - )} - end - - @impl Phoenix.LiveView - def handle_async(:load_publications, {:ok, {:ok, publications}}, socket) do - rkeys = MapSet.new(publications, & &1.rkey) - broken = Enum.filter(socket.assigns.sites, &broken?(&1, rkeys)) - - {:noreply, - assign(socket, - publications: AsyncResult.ok(socket.assigns.publications, publications), - broken: broken, - discovered: discovered(publications, socket.assigns.sites), - visible: filter_sites(socket.assigns.sites -- broken, socket.assigns.filter) - )} - end - - def handle_async(:load_publications, {:ok, {:error, reason}}, socket) do - Logger.warning("SitesLive: failed to load publications", reason: inspect(reason)) - - {:noreply, - assign(socket, - publications: - AsyncResult.failed( - socket.assigns.publications, - reason - ) - )} - end - - defp filter_sites(sites, "all"), do: sites - - defp filter_sites(sites, status) do - Enum.filter(sites, &(Site.status(&1) == status)) - end - - defp filter_label("all"), do: "All" - - defp filter_label(status) do - String.capitalize(status) - end - - defp load_publications(socket) do - if connected?(socket) do - user = socket.assigns.current_scope.user - - socket - |> assign(publications: AsyncResult.loading()) - |> start_async(:load_publications, fn -> StandardSite.list_publications(user.id) end) - else - assign(socket, publications: AsyncResult.loading()) - end - end - - defp broken?(site, publication_rkeys) do - is_binary(site.rkey) and not is_nil(site.published_at) and - not MapSet.member?(publication_rkeys, site.rkey) - end - - defp discovered(publications, sites) do - site_rkeys = MapSet.new(sites, & &1.rkey) - Enum.reject(publications, &MapSet.member?(site_rkeys, &1.rkey)) - end - - attr :patch, :string, required: true - attr :active, :boolean, required: true - slot :inner_block, required: true - - defp tab_link(assigns) do - ~H""" - <.link - patch={@patch} - class={[ - "rounded-full border-2 px-3 py-1 text-sm font-bold transition-colors", - if(@active, - do: "border-ink bg-ink text-paper", - else: "border-ink/15 text-ink/55 hover:border-ink/40 hover:text-ink" - ) - ]} - > - {render_slot(@inner_block)} - - """ - end -end diff --git a/lib/annot_at_web/controllers/page_html/home.html.heex b/lib/annot_at_web/controllers/page_html/home.html.heex index 2d7f628..0a22b83 100644 --- a/lib/annot_at_web/controllers/page_html/home.html.heex +++ b/lib/annot_at_web/controllers/page_html/home.html.heex @@ -35,10 +35,6 @@
- <.banner class="mt-6" icon="hero-exclamation-triangle"> - This app is in active development. - -
diff --git a/lib/annot_at_web/router.ex b/lib/annot_at_web/router.ex index 278322c..664b2ca 100644 --- a/lib/annot_at_web/router.ex +++ b/lib/annot_at_web/router.ex @@ -42,7 +42,6 @@ defmodule AnnotAtWeb.Router do live_session :authenticated, on_mount: [{AnnotAtWeb.UserAuth, :require_authenticated}] do live "/dashboard", DashboardLive - live "/sites", SitesLive live "/sites/new", SiteNewLive live "/sites/import/:rkey", ImportLive live "/sites/:id", SiteLive diff --git a/test/annot_at_web/live/dashboard_live_test.exs b/test/annot_at_web/live/dashboard_live_test.exs index 3532676..d21dba6 100644 --- a/test/annot_at_web/live/dashboard_live_test.exs +++ b/test/annot_at_web/live/dashboard_live_test.exs @@ -16,7 +16,6 @@ defmodule AnnotAtWeb.DashboardLiveTest do |> live(~p"/dashboard") assert html =~ "Hi Johanna" - assert html =~ "Your sites" end test "redirects to login when not authenticated", %{conn: conn} do diff --git a/test/annot_at_web/live/sites_live_test.exs b/test/annot_at_web/live/sites_live_test.exs deleted file mode 100644 index 3d14b84..0000000 --- a/test/annot_at_web/live/sites_live_test.exs +++ /dev/null @@ -1,65 +0,0 @@ -defmodule AnnotAtWeb.SitesLiveTest do - use AnnotAtWeb.ConnCase, async: true - use Mimic - - import Phoenix.LiveViewTest - - alias AnnotAt.Accounts - alias AnnotAt.Accounts.Scope - alias AnnotAt.Atproto.StandardSite - alias AnnotAt.Publishing - - test "discovered tab lists publications that aren't set up", %{conn: conn} do - user = create_user() - - expect(StandardSite, :list_publications, fn user_id -> - assert user.id == user_id - {:ok, [%{rkey: "3mope7jyypk22", url: "https://new.example", name: "New Blog"}]} - end) - - {:ok, lv, _html} = - conn - |> init_test_session(%{user_id: user.id}) - |> live(~p"/sites?tab=discovered") - - assert render_async(lv, 2000) =~ "New Blog" - assert render(lv) =~ "Import" - end - - test "a site whose publication is gone shows as broken and can be deleted", %{conn: conn} do - user = create_user() - scope = Scope.for_user(user) - {:ok, _site} = create_broken_site(scope) - - expect(StandardSite, :list_publications, fn _user_id -> {:ok, []} end) - - {:ok, lv, _html} = - conn - |> init_test_session(%{user_id: user.id}) - |> live(~p"/sites") - - assert render_async(lv, 2000) =~ "Publication missing" - - lv - |> element("button", "Delete") - |> render_click() - - refute render(lv) =~ "Publication missing" - assert [] = Publishing.list_sites(scope) - end - - defp create_user do - {:ok, user} = - Accounts.upsert_user(%{ - did: "did:plc:abc", - handle: "jola.dev" - }) - - user - end - - defp create_broken_site(scope) do - {:ok, site} = Publishing.create_site(scope, "https://gone.example") - Publishing.use_existing_publication(scope, site, "3mope7jyypk22") - end -end