Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add RepoCard component
Open rudolfs opened 1 year ago
15 files changed +494 -9 4d269511 3edfcf91
modified index.html
@@ -2,7 +2,7 @@
<html lang="en">
  <head>
    <meta charset="UTF-8" />
-
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+
    <link rel="icon" href="/radicle.svg" type="image/svg+xml" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Radicle</title>
    <link
modified package-lock.json
@@ -20,6 +20,7 @@
        "@tsconfig/svelte": "^5.0.4",
        "@types/node": "^20.9.0",
        "baconjs": "^3.0.19",
+
        "bs58": "^6.0.0",
        "eslint": "^9.9.1",
        "eslint-config-prettier": "^9.1.0",
        "eslint-plugin-svelte": "^2.43.0",
@@ -31,7 +32,8 @@
        "tslib": "^2.7.0",
        "typescript": "^5.2.2",
        "typescript-eslint": "^8.4.0",
-
        "vite": "^5.4.2"
+
        "vite": "^5.4.2",
+
        "zod": "^3.23.8"
      },
      "engines": {
        "node": "20.9.0"
@@ -1447,6 +1449,12 @@
      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
      "dev": true
    },
+
    "node_modules/base-x": {
+
      "version": "5.0.0",
+
      "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz",
+
      "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==",
+
      "dev": true
+
    },
    "node_modules/binary-extensions": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -1481,6 +1489,15 @@
        "node": ">=8"
      }
    },
+
    "node_modules/bs58": {
+
      "version": "6.0.0",
+
      "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
+
      "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
+
      "dev": true,
+
      "dependencies": {
+
        "base-x": "^5.0.0"
+
      }
+
    },
    "node_modules/callsites": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -3203,6 +3220,15 @@
      "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
      "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
      "dev": true
+
    },
+
    "node_modules/zod": {
+
      "version": "3.23.8",
+
      "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
+
      "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
+
      "dev": true,
+
      "funding": {
+
        "url": "https://github.com/sponsors/colinhacks"
+
      }
    }
  }
}
modified package.json
@@ -28,6 +28,7 @@
    "@tsconfig/svelte": "^5.0.4",
    "@types/node": "^20.9.0",
    "baconjs": "^3.0.19",
+
    "bs58": "^6.0.0",
    "eslint": "^9.9.1",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-svelte": "^2.43.0",
@@ -39,6 +40,7 @@
    "tslib": "^2.7.0",
    "typescript": "^5.2.2",
    "typescript-eslint": "^8.4.0",
-
    "vite": "^5.4.2"
+
    "vite": "^5.4.2",
+
    "zod": "^3.23.8"
  }
}
modified public/index.css
@@ -15,3 +15,16 @@ html {
  height: 100%;
  width: 100%;
}
+

+
.global-oid {
+
  color: var(--color-foreground-emphasized);
+
  font-size: var(--font-size-small);
+
  font-family: var(--font-family-monospace);
+
  font-weight: var(--font-weight-regular);
+
}
+

+
.global-flex {
+
  display: flex;
+
  align-items: center;
+
  gap: 0.5rem;
+
}
added public/radicle.svg
@@ -0,0 +1,63 @@
+
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
+
  <g shape-rendering="crispEdges">
+
    <rect x="8" y="0" width="4" height="4" fill="#5555FF"/>
+
    <rect x="32" y="0" width="4" height="4" fill="#5555FF"/>
+
    <rect x="12" y="4" width="4" height="4" fill="#5555FF"/>
+
    <rect x="28" y="4" width="4" height="4" fill="#5555FF"/>
+
    <rect x="12" y="8" width="4" height="4" fill="#5555FF"/>
+
    <rect x="16" y="8" width="4" height="4" fill="#3333DD"/>
+
    <rect x="20" y="8" width="4" height="4" fill="#5555FF"/>
+
    <rect x="24" y="8" width="4" height="4" fill="#3333DD"/>
+
    <rect x="28" y="8" width="4" height="4" fill="#5555FF"/>
+
    <rect x="8" y="12" width="4" height="4" fill="#5555FF"/>
+
    <rect x="12" y="12" width="4" height="4" fill="#5555FF"/>
+
    <rect x="16" y="12" width="4" height="4" fill="#5555FF"/>
+
    <rect x="20" y="12" width="4" height="4" fill="#5555FF"/>
+
    <rect x="24" y="12" width="4" height="4" fill="#5555FF"/>
+
    <rect x="28" y="12" width="4" height="4" fill="#5555FF"/>
+
    <rect x="32" y="12" width="4" height="4" fill="#5555FF"/>
+
    <rect x="4" y="16" width="4" height="4" fill="#5555FF"/>
+
    <rect x="8" y="16" width="4" height="4" fill="#5555FF"/>
+
    <rect x="12" y="16" width="4" height="4" fill="#F4F4F4"/>
+
    <rect x="16" y="16" width="4" height="4" fill="#F4F4F4"/>
+
    <rect x="20" y="16" width="4" height="4" fill="#5555FF"/>
+
    <rect x="24" y="16" width="4" height="4" fill="#5555FF"/>
+
    <rect x="28" y="16" width="4" height="4" fill="#F4F4F4"/>
+
    <rect x="32" y="16" width="4" height="4" fill="#F4F4F4"/>
+
    <rect x="36" y="16" width="4" height="4" fill="#5555FF"/>
+
    <rect x="4" y="20" width="4" height="4" fill="#5555FF"/>
+
    <rect x="8" y="20" width="4" height="4" fill="#5555FF"/>
+
    <rect x="12" y="20" width="4" height="4" fill="#F4F4F4"/>
+
    <rect x="16" y="20" width="4" height="4" fill="#FF55FF"/>
+
    <rect x="20" y="20" width="4" height="4" fill="#5555FF"/>
+
    <rect x="24" y="20" width="4" height="4" fill="#5555FF"/>
+
    <rect x="28" y="20" width="4" height="4" fill="#F4F4F4"/>
+
    <rect x="32" y="20" width="4" height="4" fill="#FF55FF"/>
+
    <rect x="36" y="20" width="4" height="4" fill="#5555FF"/>
+
    <rect x="0" y="24" width="4" height="4" fill="#3333DD"/>
+
    <rect x="4" y="24" width="4" height="4" fill="#3333DD"/>
+
    <rect x="8" y="24" width="4" height="4" fill="#3333DD"/>
+
    <rect x="12" y="24" width="4" height="4" fill="#3333DD"/>
+
    <rect x="16" y="24" width="4" height="4" fill="#3333DD"/>
+
    <rect x="20" y="24" width="4" height="4" fill="#3333DD"/>
+
    <rect x="24" y="24" width="4" height="4" fill="#3333DD"/>
+
    <rect x="28" y="24" width="4" height="4" fill="#3333DD"/>
+
    <rect x="32" y="24" width="4" height="4" fill="#3333DD"/>
+
    <rect x="36" y="24" width="4" height="4" fill="#3333DD"/>
+
    <rect x="40" y="24" width="4" height="4" fill="#3333DD"/>
+
    <rect x="8" y="28" width="4" height="4" fill="#3333DD"/>
+
    <rect x="16" y="28" width="4" height="4" fill="#5555FF"/>
+
    <rect x="24" y="28" width="4" height="4" fill="#5555FF"/>
+
    <rect x="32" y="28" width="4" height="4" fill="#3333DD"/>
+
    <rect x="8" y="32" width="4" height="4" fill="#3333DD"/>
+
    <rect x="16" y="32" width="4" height="4" fill="#5555FF"/>
+
    <rect x="24" y="32" width="4" height="4" fill="#5555FF"/>
+
    <rect x="32" y="32" width="4" height="4" fill="#3333DD"/>
+
    <rect x="16" y="36" width="4" height="4" fill="#5555FF"/>
+
    <rect x="24" y="36" width="4" height="4" fill="#5555FF"/>
+
    <rect x="12" y="40" width="4" height="4" fill="#5555FF"/>
+
    <rect x="16" y="40" width="4" height="4" fill="#5555FF"/>
+
    <rect x="24" y="40" width="4" height="4" fill="#5555FF"/>
+
    <rect x="28" y="40" width="4" height="4" fill="#5555FF"/>
+
  </g>
+
</svg>
modified src/components/Border.svelte
@@ -2,6 +2,8 @@
  export let variant: "primary" | "secondary" | "ghost";
  export let stylePadding: string | undefined = undefined;
  export let styleHeight: string | undefined = undefined;
+
  export let styleMinHeight: string | undefined = undefined;
+
  export let styleWidth: string | undefined = undefined;

  $: style = `--button-color-1: var(--color-fill-${variant});`;
</script>
@@ -121,16 +123,27 @@
      "p3-1 p3-2 p3-3 p3-4 p3-5"
      "p4-1 p4-2 p4-3 p4-4 p4-5"
      "p5-1 p5-2 p5-3 p5-4 p5-5";
+
    overflow: hidden;
+
  }
+

+
  .container:hover .p2-3,
+
  .container:hover .p3-2,
+
  .container:hover .p3-3,
+
  .container:hover .p3-4,
+
  .container:hover .p4-3 {
+
    background-color: var(--color-background-float);
  }
</style>

<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
+
  style:width={styleWidth}
  class="container"
  on:click
  role="button"
  tabindex="0"
  {style}
+
  style:min-height={styleMinHeight}
  style:height={styleHeight}>
  <div class="pixel p1-1"></div>
  <div class="pixel p1-2"></div>
@@ -146,7 +159,7 @@

  <div class="pixel p3-1"></div>
  <div class="pixel p3-2"></div>
-
  <div class="pixel p3-3 txt-semibold txt-small" style:padding={stylePadding}>
+
  <div class="pixel p3-3" style:padding={stylePadding}>
    <slot />
  </div>
  <div class="pixel p3-4"></div>
modified src/components/Fill.svelte
@@ -1,7 +1,14 @@
<script lang="ts">
-
  export let variant: "primary" | "secondary" | "ghost" | "transparent";
+
  export let variant:
+
    | "delegate"
+
    | "ghost"
+
    | "primary"
+
    | "private"
+
    | "secondary"
+
    | "transparent";
  export let stylePadding: string | undefined = undefined;
  export let styleHeight: string | undefined = undefined;
+
  export let styleWidth: string | undefined = undefined;

  $: style =
    variant === "transparent"
@@ -34,6 +41,7 @@
    display: flex;
    align-items: center;
    gap: 0.5rem;
+
    justify-content: center;
  }
  .p2-3 {
    grid-area: p2-3;
@@ -78,13 +86,14 @@
  role="button"
  tabindex="0"
  {style}
+
  style:width={styleWidth}
  style:height={styleHeight}>
  <div class="pixel p1-1"></div>
  <div class="pixel p1-2"></div>
  <div class="pixel p1-3"></div>

  <div class="pixel p2-1"></div>
-
  <div class="pixel p2-2 txt-semibold txt-small" style:padding={stylePadding}>
+
  <div class="pixel p2-2" style:padding={stylePadding}>
    <slot />
  </div>
  <div class="pixel p2-3"></div>
modified src/components/Header.svelte
@@ -51,7 +51,7 @@
          }} />
      </div>
      <Fill variant="ghost" stylePadding="0 0.5rem" styleHeight="32px">
-
        {currentPage}
+
        <span class="txt-small txt-semibold">{currentPage}</span>
      </Fill>
      <Border variant="ghost" stylePadding="0 0.25rem" styleHeight="32px">
        <Icon name="plus" />
@@ -60,7 +60,8 @@

    <div class="flex-item" style:gap="0.5rem">
      <Border variant="ghost" stylePadding="0 0.5rem" styleHeight="32px">
-
        <Icon name="offline" /> Offline
+
        <Icon name="offline" />
+
        <span class="txt-small txt-semibold">Offline</span>
      </Border>
      <Popover popoverPositionRight="0" popoverPositionTop="3rem">
        <Border
modified src/components/Icon.svelte
@@ -12,8 +12,11 @@
    | "diff"
    | "file"
    | "inbox"
+
    | "issue"
+
    | "lock"
    | "moon"
    | "offline"
+
    | "patch"
    | "plus"
    | "repo"
    | "seedling"
@@ -133,6 +136,38 @@
    <path d="M6 9H10L10 10H6L6 9Z" />
    <path d="M3 13H13V14H3L3 13Z" />
    <path d="M3 2H13V3H3L3 2Z" />
+
  {:else if name === "issue"}
+
    <path d="M6 13H8V14H6V13Z" />
+
    <path d="M10 13L8 13V14L10 14V13Z" />
+
    <path d="M3 5.99999L3 7.99999H2L2 5.99999H3Z" />
+
    <path d="M13 5.99999V7.99999H14V5.99999H13Z" />
+
    <path d="M4 12H6V13L4 13V12Z" />
+
    <path d="M12 12H10V13L12 13V12Z" />
+
    <path d="M4 3.99999V5.99999H3L3 3.99999H4Z" />
+
    <path d="M12 3.99999V5.99999L13 5.99999V3.99999H12Z" />
+
    <path d="M4 9.99999L4 12H3L3 9.99999H4Z" />
+
    <path d="M12 9.99998V12H13V9.99998H12Z" />
+
    <path d="M6 3.99998L4 3.99999L4 2.99998L6 2.99999V3.99998Z" />
+
    <path d="M10 3.99998L12 3.99999V2.99998L10 2.99998V3.99998Z" />
+
    <path d="M3 7.99999L3 9.99999H2L2 7.99999H3Z" />
+
    <path d="M13 7.99999V9.99998L14 9.99999V7.99999H13Z" />
+
    <path d="M8 2.99998L6 2.99999V1.99998L8 1.99998V2.99998Z" />
+
    <path d="M8 2.99998L10 2.99998L10 1.99998L8 1.99998V2.99998Z" />
+
    <path d="M7 5.99999H9V6.99999H7V5.99999Z" />
+
    <path d="M7 8.99999H9V9.99998H7V8.99999Z" />
+
    <path d="M10 6.99998V8.99998L9 8.99999L9 6.99999L10 6.99998Z" />
+
    <path d="M7 6.99999L7 8.99999L6 8.99998V6.99998L7 6.99999Z" />
+
  {:else if name === "lock"}
+
    <path d="M6 2H10V3H6V2Z" />
+
    <path d="M10 3L11 3V4H10V3Z" />
+
    <path d="M6 3H5V4H6L6 3Z" />
+
    <path d="M11 4L12 4V7H11V4Z" />
+
    <path d="M5 4H4V7H5V4Z" />
+
    <path d="M2 7H3V13H2V7Z" />
+
    <path d="M3 13H13V14H3L3 13Z" />
+
    <path d="M13 7H14V13H13V7Z" />
+
    <path d="M3 6H13V7H3L3 6Z" />
+
    <path d="M7 8H9V11H7V8Z" />
  {:else if name === "moon"}
    <path d="M4 3H6V4H4V3Z" />
    <path d="M3 4L4 4L4 6H3V4Z" />
@@ -195,6 +230,39 @@
    <path d="M4 11L5 11V12L4 12L4 11Z" />
    <path d="M3 12H4L4 13H3L3 12Z" />
    <path d="M2 13L3 13L3 14H2V13Z" />
+
  {:else if name === "patch"}
+
    <path d="M9 2H10V3H9V2Z" />
+
    <path d="M10 2L11 2V3L10 3V2Z" />
+
    <path d="M13 6H14V7H13V6Z" />
+
    <path d="M13 5H14L14 6H13L13 5Z" />
+
    <path d="M11 2L12 2V3L11 3V2Z" />
+
    <path d="M7 6H8V7H7V6Z" />
+
    <path d="M6 7H7V8H6V7Z" />
+
    <path d="M2 11H3V12H2V11Z" />
+
    <path d="M2 10H3L3 11H2L2 10Z" />
+
    <path d="M12 3H13V4H12V3Z" />
+
    <path d="M8 7L9 7V8H8V7Z" />
+
    <path d="M7 8L8 8V9H7V8Z" />
+
    <path d="M3 12L4 12V13H3L3 12Z" />
+
    <path d="M13 4H14V5H13L13 4Z" />
+
    <path d="M9 8H10V9H9L9 8Z" />
+
    <path d="M8 9L9 9L9 10H8V9Z" />
+
    <path d="M4 13H5V14H4L4 13Z" />
+
    <path d="M5 13L6 13V14L5 14V13Z" />
+
    <path d="M8 3H9V4H8V3Z" />
+
    <path d="M12 7L13 7L13 8H12V7Z" />
+
    <path d="M7 4H8V5H7V4Z" />
+
    <path d="M11 8H12V9H11V8Z" />
+
    <path d="M6 5H7V6H6V5Z" />
+
    <path d="M10 9L11 9V10H10V9Z" />
+
    <path d="M5 6H6V7H5V6Z" />
+
    <path d="M9 10H10V11H9V10Z" />
+
    <path d="M4 7H5L5 8H4V7Z" />
+
    <path d="M8 11H9V12H8V11Z" />
+
    <path d="M3 8H4V9H3V8Z" />
+
    <path d="M7 12L8 12L8 13H7V12Z" />
+
    <path d="M2 9L3 9L3 10H2L2 9Z" />
+
    <path d="M6 13H7V14H6V13Z" />
  {:else if name === "plus"}
    <path d="M7.00002 2H9.00002V14H7.00002V2Z" />
    <path d="M14 7V9L2.00002 9L2.00002 7L14 7Z" />
added src/components/RepoCard.svelte
@@ -0,0 +1,83 @@
+
<script lang="ts">
+
  import type { Repo } from "@app/lib/api/repo";
+

+
  import { formatRepositoryId, formatTimestamp } from "@app/lib/utils";
+

+
  import Border from "./Border.svelte";
+
  import Fill from "./Fill.svelte";
+
  import Icon from "./Icon.svelte";
+

+
  // TODO: Pass this via repo.
+
  export let updatedAt: number = 1725360130;
+

+
  export let repo: Repo;
+
  export let selfDid: string;
+

+
  $: project = repo.payloads["xyz.radicle.project"];
+
</script>
+

+
<style>
+
  .header {
+
    justify-content: space-between;
+
  }
+
  .footer {
+
    margin-top: 1rem;
+
    justify-content: space-between;
+
  }
+
  .title {
+
    display: flex;
+
    color: var(--color-fill-gray);
+
    margin-top: 4px;
+
  }
+
  .container {
+
    width: 100%;
+
  }
+
</style>
+

+
<Border variant="ghost" styleWidth="100%" stylePadding="8px 12px">
+
  <div class="container txt-small">
+
    <div class="global-flex header">
+
      <div class="global-flex">
+
        <Fill styleWidth="1.5rem" styleHeight="24px" variant="ghost">
+
          {project.data.name[0]}
+
        </Fill>{project.data.name}
+
      </div>
+
      <div class="global-flex">
+
        {#if repo.visibility.type === "private"}
+
          <Fill variant="private" styleWidth="24px" styleHeight="24px">
+
            <div style:color="var(--color-foreground-yellow)">
+
              <Icon name="lock" />
+
            </div>
+
          </Fill>
+
        {/if}
+
        {#if repo.delegates.find(x => x.id === selfDid)}
+
          <Fill variant="delegate" styleWidth="24px" styleHeight="24px">
+
            <div style:color="var(--color-fill-primary)">
+
              <Icon name="delegate" />
+
            </div>
+
          </Fill>
+
        {/if}
+
        <div class="global-flex">
+
          <Fill variant="ghost" styleHeight="24px" stylePadding="0 4px">
+
            <Icon name="seedling" />
+
            {repo.seeding}
+
          </Fill>
+
        </div>
+
      </div>
+
    </div>
+

+
    <div class="title">Radicle Heartwood Protocol & Stack</div>
+

+
    <div class="global-oid">{formatRepositoryId(repo.rid)}</div>
+

+
    <div class="global-flex footer">
+
      <div class="global-flex">
+
        <div class="global-flex" style:gap="4px"><Icon name="issue" /> 4</div>
+
        <div class="global-flex" style:gap="4px"><Icon name="patch" /> 6</div>
+
      </div>
+
      <span style:color="var(--color-fill-gray)">
+
        Updated {formatTimestamp(updatedAt)}
+
      </span>
+
    </div>
+
  </div>
+
</Border>
added src/lib/api/author.ts
@@ -0,0 +1,8 @@
+
import { object, string, z } from "zod";
+

+
export type Author = z.infer<typeof authorSchema>;
+

+
export const authorSchema = object({
+
  id: string(),
+
  alias: string().optional(),
+
});
added src/lib/api/fixtures/heartwood-repo.json
@@ -0,0 +1,36 @@
+
{
+
  "rid": "rad:z3trNYnLWS11cJWC6BbxDs5niGo82",
+
  "payloads": {
+
    "xyz.radicle.project": {
+
      "data": {
+
        "name": "rips",
+
        "description": "Radicle Improvement Proposals (RIPs)",
+
        "defaultBranch": "master"
+
      },
+
      "meta": {
+
        "head": "329dee9a4b65169ea3889a7da239892b705d0d68",
+
        "patches": {
+
          "open": 0,
+
          "draft": 2,
+
          "archived": 0,
+
          "merged": 0
+
        },
+
        "issues": {
+
          "open": 1,
+
          "closed": 0
+
        }
+
      }
+
    }
+
  },
+
  "delegates": [
+
    {
+
      "id": "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
+
      "alias": "cloudhead"
+
    }
+
  ],
+
  "threshold": 1,
+
  "visibility": {
+
    "type": "private"
+
  },
+
  "seeding": 29
+
}
added src/lib/api/repo.ts
@@ -0,0 +1,48 @@
+
import {
+
  array,
+
  literal,
+
  number,
+
  object,
+
  optional,
+
  string,
+
  union,
+
  z,
+
} from "zod";
+

+
import { authorSchema } from "./author";
+

+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
+
const repoSchema = object({
+
  rid: string(),
+
  payloads: object({
+
    "xyz.radicle.project": object({
+
      data: object({
+
        name: string(),
+
        description: string(),
+
        defaultBranch: string(),
+
      }),
+
      meta: object({
+
        head: string(),
+
        patches: object({
+
          open: number(),
+
          draft: number(),
+
          archived: number(),
+
          merged: number(),
+
        }),
+
        issues: object({
+
          open: number(),
+
          closed: number(),
+
        }),
+
      }),
+
    }),
+
  }),
+
  delegates: array(authorSchema),
+
  threshold: number(),
+
  visibility: union([
+
    object({ type: literal("public") }),
+
    object({ type: literal("private"), allow: optional(array(string())) }),
+
  ]),
+
  seeding: number(),
+
});
+

+
export type Repo = z.infer<typeof repoSchema>;
modified src/lib/utils.ts
@@ -1,3 +1,75 @@
+
import bs58 from "bs58";
+

export const unreachable = (value: never): never => {
  throw new Error(`Unreachable code: ${value}`);
};
+

+
export function formatRepositoryId(id: string): string {
+
  const parsedId = parseRepositoryId(id);
+

+
  if (parsedId) {
+
    return `${parsedId.prefix}${truncateId(parsedId.pubkey)}`;
+
  }
+

+
  return id;
+
}
+

+
export function parseRepositoryId(
+
  rid: string,
+
): { prefix: string; pubkey: string } | undefined {
+
  const match = /^(rad:)?(z[a-zA-Z0-9]+)$/.exec(rid);
+
  if (match) {
+
    const hex = bs58.decode(match[2].substring(1));
+
    if (hex.byteLength !== 20) {
+
      return undefined;
+
    }
+

+
    return { prefix: match[1] || "rad:", pubkey: match[2] };
+
  }
+

+
  return undefined;
+
}
+

+
export function truncateId(pubkey: string): string {
+
  return `${pubkey.substring(0, 6)}…${pubkey.slice(-6)}`;
+
}
+

+
export const formatTimestamp = (
+
  timestamp: number,
+
  current = new Date().getTime(),
+
): string => {
+
  const units: Record<string, number> = {
+
    year: 24 * 60 * 60 * 1000 * 365,
+
    month: (24 * 60 * 60 * 1000 * 365) / 12,
+
    day: 24 * 60 * 60 * 1000,
+
    hour: 60 * 60 * 1000,
+
    minute: 60 * 1000,
+
    second: 1000,
+
  };
+

+
  // Multiplying timestamp with 1000 to convert from seconds to milliseconds
+
  timestamp = timestamp * 1000;
+
  const rtf = new Intl.RelativeTimeFormat("en", {
+
    numeric: "auto",
+
    style: "long",
+
  });
+
  const elapsed = current - timestamp;
+

+
  if (elapsed > units["year"]) {
+
    return "more than a year ago";
+
  } else if (elapsed < 0) {
+
    return "now"; // If elapsed is a negative number we are dealing with an item from the future, and we return "now"
+
  }
+

+
  for (const u in units) {
+
    if (elapsed > units[u] || u === "second") {
+
      // We convert the division result to a negative number to get "XX [unit] ago"
+
      return rtf.format(
+
        Math.round(elapsed / units[u]) * -1,
+
        u as Intl.RelativeTimeFormatUnit,
+
      );
+
    }
+
  }
+

+
  return new Date(timestamp).toUTCString();
+
};
modified src/views/Home.svelte
@@ -1,8 +1,51 @@
<script lang="ts">
+
  import type { Repo } from "@app/lib/api/repo";
+

+
  import repoFixture from "@app/lib/api/fixtures/heartwood-repo.json";
+

  import Header from "@app/components/Header.svelte";
  import Link from "@app/components/Link.svelte";
+
  import RepoCard from "@app/components/RepoCard.svelte";
+

+
  const selfDid = "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT";
</script>

+
<style>
+
  .layout {
+
    padding: 1rem;
+
  }
+
  .repo-grid {
+
    display: grid;
+
    grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr));
+
    gap: 1rem;
+
  }
+
</style>
+

<Header currentPage="Repositories" />
+
<div class="layout">
+
  <div class="repo-grid">
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+

+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+

+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+

+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
    <RepoCard repo={repoFixture as Repo} {selfDid} />
+
  </div>

-
👉 <Link route={{ resource: "designSystem" }}>Design System</Link>
+
  <div style:margin-top="1rem">
+
    👉 <Link route={{ resource: "designSystem" }}>Design System</Link>
+
  </div>
+
</div>