From 3edfcf91921c178add31abe83f84fa4fdfbbcd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C5=ABdolfs=20O=C5=A1i=C5=86=C5=A1?= Date: Thu, 5 Sep 2024 11:15:31 +0200 Subject: [PATCH] Add RepoCard component --- package-lock.json | 28 +++++++- package.json | 4 +- public/index.css | 13 ++++ src/components/Border.svelte | 15 ++++- src/components/Fill.svelte | 13 +++- src/components/Header.svelte | 5 +- src/components/Icon.svelte | 68 +++++++++++++++++++ src/components/RepoCard.svelte | 83 ++++++++++++++++++++++++ src/lib/api/author.ts | 8 +++ src/lib/api/fixtures/heartwood-repo.json | 36 ++++++++++ src/lib/api/repo.ts | 48 ++++++++++++++ src/lib/utils.ts | 72 ++++++++++++++++++++ src/views/Home.svelte | 47 +++++++++++++- 13 files changed, 431 insertions(+), 9 deletions(-) create mode 100644 src/components/RepoCard.svelte create mode 100644 src/lib/api/author.ts create mode 100644 src/lib/api/fixtures/heartwood-repo.json create mode 100644 src/lib/api/repo.ts diff --git a/package-lock.json b/package-lock.json index 4b8b090ac..67286d53a 100644 --- a/package-lock.json +++ b/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" + } } } } diff --git a/package.json b/package.json index 6dbefcfc6..4ec291d5b 100644 --- a/package.json +++ b/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" } } diff --git a/public/index.css b/public/index.css index 72f71d322..da888352f 100644 --- a/public/index.css +++ b/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; +} diff --git a/src/components/Border.svelte b/src/components/Border.svelte index b89b736d5..23d586e5a 100644 --- a/src/components/Border.svelte +++ b/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});`; @@ -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); }
@@ -146,7 +159,7 @@
-
+
diff --git a/src/components/Fill.svelte b/src/components/Fill.svelte index 82707fc07..4b7ef9ae9 100644 --- a/src/components/Fill.svelte +++ b/src/components/Fill.svelte @@ -1,7 +1,14 @@ + + + + +
+
+
+ + {project.data.name[0]} + {project.data.name} +
+
+ {#if repo.visibility.type === "private"} + +
+ +
+
+ {/if} + {#if repo.delegates.find(x => x.id === selfDid)} + +
+ +
+
+ {/if} +
+ + + {repo.seeding} + +
+
+
+ +
Radicle Heartwood Protocol & Stack
+ +
{formatRepositoryId(repo.rid)}
+ + +
+
diff --git a/src/lib/api/author.ts b/src/lib/api/author.ts new file mode 100644 index 000000000..e5c06bc17 --- /dev/null +++ b/src/lib/api/author.ts @@ -0,0 +1,8 @@ +import { object, string, z } from "zod"; + +export type Author = z.infer; + +export const authorSchema = object({ + id: string(), + alias: string().optional(), +}); diff --git a/src/lib/api/fixtures/heartwood-repo.json b/src/lib/api/fixtures/heartwood-repo.json new file mode 100644 index 000000000..6a0e9561a --- /dev/null +++ b/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 +} diff --git a/src/lib/api/repo.ts b/src/lib/api/repo.ts new file mode 100644 index 000000000..5671305c5 --- /dev/null +++ b/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; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 311b0426d..e24d800a7 100644 --- a/src/lib/utils.ts +++ b/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 = { + 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(); +}; diff --git a/src/views/Home.svelte b/src/views/Home.svelte index 6815ad2f8..b87abe35b 100644 --- a/src/views/Home.svelte +++ b/src/views/Home.svelte @@ -1,8 +1,51 @@ -
+ -👉 Design System +
+
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ 👉 Design System +
+