Add RepoCard component

This commit is contained in:
Rūdolfs Ošiņš
2024-09-05 11:17:58 +02:00
parent 7f67759d4b
commit 3edfcf9192
13 changed files with 431 additions and 9 deletions
+27 -1
View File
@@ -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"
}
}
}
}
+3 -1
View File
@@ -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"
}
}
+13
View File
@@ -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;
}
+14 -1
View File
@@ -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>
+11 -2
View File
@@ -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>
+3 -2
View File
@@ -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
+68
View File
@@ -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" />
+83
View File
@@ -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>
+8
View File
@@ -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(),
});
+36
View File
@@ -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
}
+48
View File
@@ -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>;
+72
View File
@@ -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();
};
+45 -2
View File
@@ -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>
<Header currentPage="Repositories" />
<style>
.layout {
padding: 1rem;
}
.repo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr));
gap: 1rem;
}
</style>
👉 <Link route={{ resource: "designSystem" }}>Design System</Link>
<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>
<div style:margin-top="1rem">
👉 <Link route={{ resource: "designSystem" }}>Design System</Link>
</div>
</div>