Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
radicle-explorer src views nodes ReposView.svelte
<script lang="ts">
  import type { BaseUrl, NodeStats } from "@http-client";
  import type { RepoInfo } from "@app/components/RepoCard";

  import { onDestroy } from "svelte";

  import * as router from "@app/lib/router";
  import {
    fetchRepoInfos,
    sortRepoInfosByActivity,
  } from "@app/components/RepoCard";
  import { handleError } from "@app/views/nodes/error";

  import Loading from "@app/components/Loading.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
  import RepoCard from "@app/components/RepoCard.svelte";

  export let baseUrl: BaseUrl;
  export let stats: NodeStats;

  let listState: "pinned" | "all" = "pinned";
  let page = 0;
  let hasPinnedRepos = true;
  let sortByActivity = false;
  let sorting = false;
  let displayedRepos: RepoInfo[] = [];

  let activityAbort: AbortController | undefined;

  function newActivitySession(): AbortSignal {
    activityAbort?.abort();
    activityAbort = new AbortController();
    return activityAbort.signal;
  }

  onDestroy(() => activityAbort?.abort());

  // Reset state when baseUrl changes
  $: if (baseUrl) {
    listState = "pinned";
    page = 0;
    hasPinnedRepos = true;
    sortByActivity = false;
  }

  $: perPage = listState === "pinned" ? stats.repos.total : 24;
  $: totalPages = Math.ceil(stats.repos.total / perPage);

  function showPinned() {
    listState = "pinned";
    page = 0;
    sortByActivity = false;
  }

  function showAll() {
    listState = "all";
    page = 0;
    sortByActivity = false;
  }

  async function fetchRepos(
    show: "pinned" | "all",
    perPage: number,
    page: number,
  ) {
    const repos = await fetchRepoInfos(
      baseUrl,
      { show, perPage, page },
      undefined,
      newActivitySession(),
    );

    if (
      hasPinnedRepos &&
      show === "pinned" &&
      page === 0 &&
      repos.length === 0
    ) {
      hasPinnedRepos = false;
      listState = "all";
      return [];
    }

    sortByActivity = false;
    displayedRepos = repos;
    return repos;
  }

  async function toggleSortByActivity(repos: RepoInfo[]) {
    if (sortByActivity) {
      sortByActivity = false;
      displayedRepos = repos;
      return;
    }
    sorting = true;
    try {
      displayedRepos = await sortRepoInfosByActivity(repos);
      sortByActivity = true;
    } finally {
      sorting = false;
    }
  }
</script>

<style>
  .subtitle,
  .pagination {
    font: var(--txt-body-m-regular);
    color: var(--color-text-tertiary);
  }
  .pagination {
    display: flex;
    gap: 0.25rem;
    margin-left: auto;
  }
  .repos {
    margin-top: 0;
  }
  .repo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(32rem, 1fr));
    gap: 0;
  }
  .container {
    display: grid;
    place-items: center;
    min-height: calc(100vh - var(--global-header-height));
    font: var(--txt-body-m-regular);
  }
  .text-button {
    background: none;
    border: none;
    font: inherit;
    color: inherit;
    margin: 0;
    padding: 0;
  }
  .text-button:not(:disabled) {
    cursor: pointer;
  }
  .text-button:hover:not(:disabled) {
    text-decoration: underline;
  }
  .current-page {
    text-decoration: underline;
  }
  .footer {
    display: flex;
    gap: 0.5rem 1rem;
    margin: 1rem;
  }

  @media (max-width: 1010.98px) {
    .repo-grid {
      grid-template-columns: 1fr;
    }
    .footer {
      flex-direction: column;
    }
    .pagination {
      margin-left: 0;
    }
  }
</style>

<div class="repos">
  {#await fetchRepos(listState, perPage, page)}
    <div class="container">
      <Loading small center />
    </div>
  {:then repoInfos}
    {#if repoInfos.length > 0}
      <div class="repo-grid">
        {#each displayedRepos as repoInfo (repoInfo.repo.rid)}
          <RepoCard {baseUrl} {repoInfo} />
        {/each}
      </div>
      <div class="footer">
        {#if listState === "pinned"}
          <div class="subtitle">
            {repoInfos.length}
            pinned {repoInfos.length === 1 ? "repository" : "repositories"} ·
            <button
              class="text-button"
              disabled={sorting}
              on:click={() => toggleSortByActivity(repoInfos)}>
              {sortByActivity ? "Default order" : "Sort by activity"}
            </button>
            ·
            <button class="text-button" on:click={showAll}>Browse all</button>
          </div>
        {:else}
          <div class="subtitle">
            {stats.repos.total.toLocaleString()}
            seeded {stats.repos.total === 1 ? "repository" : "repositories"}
            {#if hasPinnedRepos}
              ·
              <button class="text-button" on:click={showPinned}>
                See pinned
              </button>
            {/if}
          </div>

          {#if totalPages > 1}
            <div class="pagination">
              {#if page !== 0}
                <button class="text-button" on:click={() => (page = page - 1)}>
                  Previous
                </button>
                ·
              {/if}

              {#each Array.from({ length: Math.min(totalPages, 7) }) as _, i}
                {@const startPage = Math.max(page - 3, 0)}
                {@const pageNumber = startPage + i}
                <button
                  class="text-button"
                  class:current-page={page === pageNumber}
                  on:click={() => (page = pageNumber)}
                  disabled={page === pageNumber}>
                  {pageNumber + 1}
                </button>
              {/each}

              {#if page !== totalPages - 1}
                ·
                <button class="text-button" on:click={() => (page = page + 1)}>
                  Next
                </button>
              {/if}
            </div>
          {/if}
        {/if}
      </div>
    {:else}
      <div class="container">
        {#if listState === "pinned"}
          <Placeholder
            iconName="desert"
            caption="This node doesn't have any pinned repositories." />
        {:else}
          <Placeholder
            iconName="desert"
            caption="This node doesn't seed any repositories." />
        {/if}
      </div>
    {/if}
  {:catch error}
    {router.push(handleError(error, baseUrl))}
  {/await}
</div>