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