Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Simplify the ProjectCard component
Rūdolfs Ošiņš committed 3 years ago
commit 26419a74e26ec8a538b7c6ba54957ba252c86249
parent d80c46e085f0ace6f5921c422b60a17c9a2a3bc0
7 files changed +307 -312
added src/components/ProjectCard.svelte
@@ -0,0 +1,131 @@
+
<script lang="ts">
+
  import type { WeeklyActivity } from "@app/lib/commit";
+

+
  import { formatCommit, twemoji } from "@app/lib/utils";
+

+
  import ActivityDiagram from "@app/views/projects/ActivityDiagram.svelte";
+

+
  export let compact = false;
+
  export let description: string;
+
  export let head: string;
+
  export let id: string;
+
  export let name: string;
+

+
  export let activity: WeeklyActivity[];
+
</script>
+

+
<style>
+
  .project {
+
    display: flex;
+
    flex-direction: row;
+
    justify-content: space-between;
+
    padding: 1rem;
+
    border: 1px solid var(--color-secondary-5);
+
    border-radius: var(--border-radius-small);
+
    min-width: 36rem;
+
    cursor: pointer;
+
    background: var(--color-background-1);
+
  }
+
  .right {
+
    display: flex;
+
    flex-direction: column;
+
    justify-content: space-between;
+
    align-items: flex-end;
+
  }
+
  .left {
+
    width: 50%;
+
  }
+
  .description {
+
    overflow-x: hidden;
+
    overflow-y: hidden;
+
    text-overflow: ellipsis;
+
  }
+
  .compact {
+
    min-width: 16rem;
+
    height: 9rem;
+
  }
+
  .compact .left {
+
    width: 100%;
+
  }
+
  .compact .right {
+
    display: none;
+
  }
+
  .compact .description {
+
    white-space: nowrap;
+
  }
+
  .activity {
+
    width: 100%;
+
    max-width: 14rem;
+
  }
+
  .project:hover {
+
    border-color: var(--color-secondary);
+
    background-color: var(--color-secondary-1);
+
  }
+
  .description {
+
    margin-bottom: 0.25rem;
+
    font-size: var(--font-size-tiny);
+
  }
+
  .stateHash {
+
    color: var(--color-secondary);
+
    font-size: var(--font-size-tiny);
+
    font-family: var(--font-family-monospace);
+
    min-height: 2rem;
+
    display: flex;
+
    align-items: center;
+
  }
+
  .id {
+
    display: flex;
+
    justify-content: space-between;
+
    font-size: var(--font-size-regular);
+
    font-weight: var(--font-weight-medium);
+
    margin-bottom: 0.5rem;
+
  }
+
  .rid {
+
    visibility: hidden;
+
    color: var(--color-foreground-5);
+
    font-weight: var(--font-weight-normal);
+
    font-family: var(--font-family-monospace);
+
    font-size: var(--font-size-tiny);
+
  }
+
  .project:hover .rid {
+
    visibility: visible;
+
  }
+
  @media (max-width: 720px) {
+
    .project {
+
      min-width: 0;
+
    }
+
  }
+
</style>
+

+
<!-- svelte-ignore a11y-click-events-have-key-events -->
+
<div class="project" on:click class:compact>
+
  <div class="left">
+
    <div class="id">
+
      <span class="name">{name}</span>
+
    </div>
+
    <div class="description" use:twemoji>{description}</div>
+
    <div class="stateHash">
+
      {#if compact}
+
        {formatCommit(head)}
+
      {:else}
+
        {head}
+
      {/if}
+
    </div>
+
    {#if compact}
+
      <div class="activity">
+
        <ActivityDiagram {activity} viewBoxHeight={70} />
+
      </div>
+
    {/if}
+
  </div>
+

+
  {#if !compact}
+
    <div class="right">
+
      <div class="id">
+
        <span class="rid layout-desktop">{id}</span>
+
      </div>
+
      <div class="layout-desktop activity">
+
        <ActivityDiagram {activity} viewBoxHeight={100} />
+
      </div>
+
    </div>
+
  {/if}
+
</div>
modified src/lib/commit.ts
@@ -1,6 +1,7 @@
-
import type { CommitHeader } from "@httpd-client";
+
import type { BaseUrl, CommitHeader } from "@httpd-client";

import { getDaysPassed } from "@app/lib/utils";
+
import { HttpdClient } from "@httpd-client";

// A set of commits grouped by time.
interface CommitGroup {
@@ -70,7 +71,7 @@ export function groupCommits(commits: CommitHeader[]): CommitGroup[] {
  }
}

-
export function groupCommitsByWeek(commits: number[]): WeeklyActivity[] {
+
function groupCommitsByWeek(commits: number[]): WeeklyActivity[] {
  const groupedCommits: WeeklyActivity[] = [];
  let groupDate: Date | undefined = undefined;

@@ -114,3 +115,10 @@ export function groupCommitsByWeek(commits: number[]): WeeklyActivity[] {

  return groupedCommits;
}
+

+
export async function loadProjectActivity(id: string, baseUrl: BaseUrl) {
+
  const api = new HttpdClient(baseUrl);
+
  const commits = await api.project.getActivity(id);
+

+
  return groupCommitsByWeek(commits.activity);
+
}
modified src/views/home/Index.svelte
@@ -4,11 +4,12 @@
  import * as router from "@app/lib/router";
  import { config } from "@app/lib/config";
  import { getProjectsFromSeeds } from "@app/lib/search";
+
  import { loadProjectActivity } from "@app/lib/commit";
  import { twemoji } from "@app/lib/utils";

  import Loading from "@app/components/Loading.svelte";
  import Message from "@app/components/Message.svelte";
-
  import Widget from "@app/views/projects/Widget.svelte";
+
  import ProjectCard from "@app/components/ProjectCard.svelte";

  function goToProject(project: Project, baseUrl: BaseUrl) {
    router.push({
@@ -94,13 +95,18 @@

      <div class="projects">
        {#each results as result}
-
          <div class="project">
-
            <Widget
-
              compact
-
              project={result.project}
-
              baseUrl={result.baseUrl}
-
              on:click={() => goToProject(result.project, result.baseUrl)} />
-
          </div>
+
          {#await loadProjectActivity(result.project.id, result.baseUrl) then activity}
+
            <div class="project">
+
              <ProjectCard
+
                compact
+
                description={result.project.description}
+
                head={result.project.head}
+
                id={result.project.id}
+
                name={result.project.name}
+
                {activity}
+
                on:click={() => goToProject(result.project, result.baseUrl)} />
+
            </div>
+
          {/await}
        {/each}
      </div>
    {/if}
added src/views/projects/ActivityDiagram.svelte
@@ -0,0 +1,139 @@
+
<script lang="ts">
+
  import type { WeeklyActivity } from "@app/lib/commit";
+

+
  import { onMount } from "svelte";
+

+
  export let activity: WeeklyActivity[];
+
  export let viewBoxHeight: number;
+

+
  const strokeWidth = 3;
+
  const viewBoxWidth = 600;
+

+
  // The path strings to be inserted into the svg <path>.
+
  let path = "";
+
  let areaPath = "";
+

+
  const heightWithPadding = viewBoxHeight + 16;
+

+
  // The latest point on the x axis, starting at 0 until `viewBoxWidth`.
+
  let lastWidthPoint = viewBoxWidth;
+

+
  // The amount of points on the x axis.
+
  const widthIteration = viewBoxWidth / 52;
+

+
  // The highest value on the y axis.
+
  const commitCountArray: number[] = [];
+

+
  // The minimal amplitude shown e.g. commitCount = 1 => `minimalHeight`
+
  // points of height in the SVG.
+
  const minimalHeight = 5;
+

+
  let week = 0;
+

+
  for (const point of activity) {
+
    if (point.week - week > 1) {
+
      commitCountArray.push(...new Array(point.week - week).fill(0));
+
    }
+
    commitCountArray.push(point.commits.length);
+
    week = point.week;
+
  }
+

+
  // Formats the points passed in, into a svg path string, without closing
+
  // the area.
+
  function createPath() {
+
    let i = 1;
+

+
    if (commitCountArray.length < 52) {
+
      commitCountArray.push(...new Array(52 - commitCountArray.length).fill(0));
+
    }
+

+
    const maxValue = Math.max(...commitCountArray);
+
    const minValue = Math.min(...commitCountArray);
+

+
    // Normalizes the values to the viewBox dimensions.
+
    const normalizedArray = commitCountArray.map(c => {
+
      // If we are not crossing the `viewBoxHeight` we want to return the
+
      // actual value, and don't want to normalize <`minimalHeight` commit
+
      // counts as huge spikes.
+
      if (maxValue < viewBoxHeight && c >= minimalHeight) {
+
        return c;
+
      }
+
      // If the value is 0..minimalHeight though we don't want to set it to
+
      // the minimalHeight.
+
      else if (c > 0 && c < minimalHeight) {
+
        return minimalHeight;
+
      }
+
      // If the count is 0 we have to make sure the normalization is not being
+
      // run since it would return NaN.
+
      else {
+
        return c === 0
+
          ? 0
+
          : ((viewBoxHeight - 0) * (c - minValue)) / (maxValue - minValue);
+
      }
+
    });
+

+
    const path = normalizedArray.slice(1).reduce(
+
      (acc, curr) => {
+
        const s = `${viewBoxWidth - widthIteration * i},${
+
          viewBoxHeight - curr
+
        }`;
+
        lastWidthPoint = viewBoxWidth - widthIteration * i;
+
        i += 1;
+
        return acc.concat(s);
+
      },
+
      [`M${viewBoxWidth},${viewBoxHeight - normalizedArray[0]}`],
+
    );
+
    return path.join();
+
  }
+

+
  onMount(() => {
+
    // Creates the stroke path with the array of points.
+
    path = createPath();
+
    // Concats a path closing for it to be the area under the stroke.
+
    areaPath = path.concat(
+
      `L${lastWidthPoint},${viewBoxHeight}L${viewBoxWidth},${viewBoxHeight}Z`,
+
    );
+
  });
+
</script>
+

+
<svg
+
  viewBox="0 0 {viewBoxWidth} {heightWithPadding}"
+
  xmlns="http://www.w3.org/2000/svg">
+
  <svg style="height: 0; width: 0;" xmlns="http://www.w3.org/2000/svg">
+
    <defs>
+
      <linearGradient id="fillGradient" x1="0" y1="1" x2="0" y2="0">
+
        <stop offset="0%" stop-color="#ff55ff" stop-opacity="0" />
+
        <stop offset="100%" stop-color="#ff55ff" stop-opacity="0.2" />
+
      </linearGradient>
+
    </defs>
+
  </svg>
+
  <svg style="height: 0; width: 0;" xmlns="http://www.w3.org/2000/svg">
+
    <defs>
+
      <linearGradient id="gradient" x1="0" y1="1" x2="0" y2="0">
+
        <stop offset="0%" stop-color="#ff55ff" stop-opacity="0.2" />
+
        <stop offset="50%" stop-color="#ff55ff" stop-opacity="0.8" />
+
        <stop offset="100%" stop-color="#ff55ff" stop-opacity="1" />
+
      </linearGradient>
+
    </defs>
+
  </svg>
+
  {#if activity.length > 0}
+
    <g>
+
      <path
+
        fill="transparent"
+
        stroke="url(#gradient)"
+
        stroke-width={strokeWidth}
+
        stroke-linejoin="round"
+
        d={path} />
+
      <path fill="url(#fillGradient)" stroke="transparent" d={areaPath} />
+
    </g>
+
  {:else}
+
    <!-- If no commits have been made in a year, we show a straight line -->
+
    <line
+
      x1="0"
+
      y1={viewBoxHeight}
+
      x2="600"
+
      y2={viewBoxHeight}
+
      stroke="#ff55ff"
+
      stroke-width={1} />
+
  {/if}
+
</svg>
deleted src/views/projects/Diagram.svelte
@@ -1,132 +0,0 @@
-
<script lang="ts">
-
  import { onMount } from "svelte";
-
  import type { WeeklyActivity } from "@app/lib/commit";
-

-
  export let strokeWidth: number;
-
  export let points: WeeklyActivity[];
-
  export let viewBoxWidth: number;
-
  export let viewBoxHeight: number;
-

-
  // The path strings to be inserted into the svg <path>
-
  let path = "";
-
  let areaPath = "";
-

-
  const heightWithPadding = viewBoxHeight + 16;
-

-
  // The latest point on the x axis, starting at 0 until `viewBoxWidth`
-
  let lastWidthPoint = viewBoxWidth;
-

-
  // The amount of points on the x axis
-
  const widthIteration = viewBoxWidth / 52;
-

-
  // The highest value on the y axis
-
  const commitCountArray: number[] = [];
-

-
  // The minimal amplitude shown e.g. commitCount = 1 => `minimalHeight` points of height in the SVG.
-
  const minimalHeight = 5;
-

-
  let week = 0;
-

-
  for (const point of points) {
-
    if (point.week - week > 1) {
-
      commitCountArray.push(...new Array(point.week - week).fill(0));
-
    }
-
    commitCountArray.push(point.commits.length);
-
    week = point.week;
-
  }
-

-
  // Formats the points passed in, into a svg path string, without closing the area
-
  function createPath() {
-
    let i = 1;
-

-
    if (commitCountArray.length < 52) {
-
      commitCountArray.push(...new Array(52 - commitCountArray.length).fill(0));
-
    }
-

-
    const maxValue = Math.max(...commitCountArray);
-
    const minValue = Math.min(...commitCountArray);
-

-
    // Normalizes the values to the viewBox dimensions
-
    const normalizedArray = commitCountArray.map(c => {
-
      // If we are not crossing the `viewBoxHeight` we want to return the actual value,
-
      // and don't want to normalize <`minimalHeight` commit counts as huge spikes.
-
      if (maxValue < viewBoxHeight && c >= minimalHeight) {
-
        return c;
-
      }
-
      // If the value is 0..minimalHeight though we don't want to set it to the minimalHeight.
-
      else if (c > 0 && c < minimalHeight) {
-
        return minimalHeight;
-
      }
-
      // If the count is 0 we have to make sure the normalization is not being run since it would return NaN
-
      else {
-
        return c === 0
-
          ? 0
-
          : ((viewBoxHeight - 0) * (c - minValue)) / (maxValue - minValue);
-
      }
-
    });
-

-
    const path = normalizedArray.slice(1).reduce(
-
      (acc, curr) => {
-
        const s = `${viewBoxWidth - widthIteration * i},${
-
          viewBoxHeight - curr
-
        }`;
-
        lastWidthPoint = viewBoxWidth - widthIteration * i;
-
        i += 1;
-
        return acc.concat(s);
-
      },
-
      [`M${viewBoxWidth},${viewBoxHeight - normalizedArray[0]}`],
-
    );
-
    return path.join();
-
  }
-

-
  onMount(() => {
-
    // Creates the stroke path with the array of points
-
    path = createPath();
-
    // Concats a path closing for it to be the area under the stroke
-
    areaPath = path.concat(
-
      `L${lastWidthPoint},${viewBoxHeight}L${viewBoxWidth},${viewBoxHeight}Z`,
-
    );
-
  });
-
</script>
-

-
<svg
-
  viewBox="0 0 {viewBoxWidth} {heightWithPadding}"
-
  xmlns="http://www.w3.org/2000/svg">
-
  <svg style="height: 0; width: 0;" xmlns="http://www.w3.org/2000/svg">
-
    <defs>
-
      <linearGradient id="fillGradient" x1="0" y1="1" x2="0" y2="0">
-
        <stop offset="0%" stop-color="#ff55ff" stop-opacity="0" />
-
        <stop offset="100%" stop-color="#ff55ff" stop-opacity="0.2" />
-
      </linearGradient>
-
    </defs>
-
  </svg>
-
  <svg style="height: 0; width: 0;" xmlns="http://www.w3.org/2000/svg">
-
    <defs>
-
      <linearGradient id="gradient" x1="0" y1="1" x2="0" y2="0">
-
        <stop offset="0%" stop-color="#ff55ff" stop-opacity="0.2" />
-
        <stop offset="50%" stop-color="#ff55ff" stop-opacity="0.8" />
-
        <stop offset="100%" stop-color="#ff55ff" stop-opacity="1" />
-
      </linearGradient>
-
    </defs>
-
  </svg>
-
  {#if points.length > 0}
-
    <g>
-
      <path
-
        fill="transparent"
-
        stroke="url(#gradient)"
-
        stroke-width={strokeWidth}
-
        stroke-linejoin="round"
-
        d={path} />
-
      <path fill="url(#fillGradient)" stroke="transparent" d={areaPath} />
-
    </g>
-
  {:else}
-
    <!-- If no commits have been made in a year, we show a straight line -->
-
    <line
-
      x1="0"
-
      y1={viewBoxHeight}
-
      x2="600"
-
      y2={viewBoxHeight}
-
      stroke="#ff55ff"
-
      stroke-width={1} />
-
  {/if}
-
</svg>
deleted src/views/projects/Widget.svelte
@@ -1,165 +0,0 @@
-
<script lang="ts">
-
  import type { BaseUrl, Project } from "@httpd-client";
-

-
  import Diagram from "@app/views/projects/Diagram.svelte";
-
  import { HttpdClient } from "@httpd-client";
-
  import { formatCommit, twemoji } from "@app/lib/utils";
-
  import { groupCommitsByWeek } from "@app/lib/commit";
-

-
  export let project: Project;
-
  export let baseUrl: BaseUrl;
-
  export let faded = false;
-
  export let compact = false;
-

-
  const loadCommits = async () => {
-
    const api = new HttpdClient(baseUrl);
-
    const commits = await api.project.getActivity(project.id);
-

-
    return groupCommitsByWeek(commits.activity);
-
  };
-
</script>
-

-
<style>
-
  article {
-
    display: flex;
-
    flex-direction: row;
-
    justify-content: space-between;
-
    padding: 1rem;
-
    border: 1px solid var(--color-secondary-5);
-
    border-radius: var(--border-radius-small);
-
    min-width: 36rem;
-
    cursor: pointer;
-
    background: var(--color-background-1);
-
  }
-
  article .right {
-
    display: flex;
-
    flex-direction: column;
-
    justify-content: space-between;
-
    align-items: flex-end;
-
  }
-
  article .left {
-
    width: 50%;
-
  }
-
  div .description {
-
    overflow-x: hidden;
-
    overflow-y: hidden;
-
    text-overflow: ellipsis;
-
  }
-
  article.compact {
-
    min-width: 16rem;
-
    height: 9rem;
-
  }
-
  article.compact .left {
-
    width: 100%;
-
  }
-
  article.compact .right {
-
    display: none;
-
  }
-
  article.compact .description {
-
    white-space: nowrap;
-
  }
-
  article.project-faded {
-
    border: 1px dashed var(--color-foreground-4);
-
    cursor: not-allowed;
-
  }
-
  .activity {
-
    width: 100%;
-
    max-width: 14rem;
-
  }
-
  article:hover {
-
    border-color: var(--color-secondary);
-
    background-color: var(--color-secondary-1);
-
  }
-
  article.project-faded:hover {
-
    border-color: var(--color-foreground-5);
-
  }
-
  article .id {
-
    font-size: var(--font-size-regular);
-
    font-weight: var(--font-weight-medium);
-
    margin-bottom: 0.5rem;
-
  }
-
  article .description {
-
    margin-bottom: 0.25rem;
-
    font-size: var(--font-size-tiny);
-
  }
-
  article .stateHash {
-
    color: var(--color-secondary);
-
    font-size: var(--font-size-tiny);
-
    font-family: var(--font-family-monospace);
-
    min-height: 2rem;
-
    display: flex;
-
    align-items: center;
-
  }
-
  article .id {
-
    display: flex;
-
    justify-content: space-between;
-
  }
-
  article .id .rid {
-
    visibility: hidden;
-
    color: var(--color-foreground-5);
-
    font-weight: var(--font-weight-normal);
-
    font-family: var(--font-family-monospace);
-
    font-size: var(--font-size-tiny);
-
  }
-
  article:hover .id .rid {
-
    visibility: visible;
-
  }
-
  @media (max-width: 720px) {
-
    article {
-
      min-width: 0;
-
    }
-
  }
-
</style>
-

-
<!-- svelte-ignore a11y-click-events-have-key-events -->
-
<article on:click class:project-faded={faded} class:compact>
-
  <div class="left">
-
    <div class="id">
-
      <span class="name">{project.name}</span>
-
    </div>
-
    {#if project.description}
-
      <div class="description" use:twemoji>{project.description}</div>
-
    {:else}
-
      <div class="description txt-missing">No description</div>
-
    {/if}
-
    <div class="stateHash">
-
      {#if project.head}
-
        {#if compact}
-
          {formatCommit(project.head)}
-
        {:else}
-
          {project.head}
-
        {/if}
-
      {:else}
-
        <span class="txt-missing">✗ No head</span>
-
      {/if}
-
    </div>
-
    {#if compact}
-
      {#await loadCommits() then points}
-
        <div class="activity">
-
          <Diagram
-
            {points}
-
            strokeWidth={3}
-
            viewBoxHeight={70}
-
            viewBoxWidth={600} />
-
        </div>
-
      {/await}
-
    {/if}
-
  </div>
-

-
  {#if !compact}
-
    <div class="right">
-
      <div class="id">
-
        <span class="rid layout-desktop">{project.id}</span>
-
      </div>
-
      {#await loadCommits() then points}
-
        <div class="layout-desktop activity">
-
          <Diagram
-
            {points}
-
            strokeWidth={3}
-
            viewBoxHeight={100}
-
            viewBoxWidth={600} />
-
        </div>
-
      {/await}
-
    </div>
-
  {/if}
-
</article>
modified src/views/seeds/View/Projects.svelte
@@ -2,10 +2,12 @@
  import type { BaseUrl, Project, NodeStats } from "@httpd-client";

  import * as router from "@app/lib/router";
-
  import List from "@app/components/List.svelte";
-
  import Widget from "@app/views/projects/Widget.svelte";
  import { HttpdClient } from "@httpd-client";
  import { config } from "@app/lib/config";
+
  import { loadProjectActivity } from "@app/lib/commit";
+

+
  import List from "@app/components/List.svelte";
+
  import ProjectCard from "@app/components/ProjectCard.svelte";

  export let baseUrl: BaseUrl;
  export let projects: Project[];
@@ -69,11 +71,17 @@
    query={fetchMoreProjects}>
    <svelte:fragment slot="list" let:items>
      {#each items as project}
-
        {#if project.head}
+
        {#await loadProjectActivity(project.id, baseUrl) then activity}
          <div class="project">
-
            <Widget {project} {baseUrl} on:click={() => onClick(project)} />
+
            <ProjectCard
+
              {activity}
+
              id={project.id}
+
              name={project.name}
+
              description={project.description}
+
              head={project.head}
+
              on:click={() => onClick(project)} />
          </div>
-
        {/if}
+
        {/await}
      {/each}
    </svelte:fragment>
  </List>