Browse Source

eslint + prettier (#2)

Co-authored-by: DanConwayDev <114834599+DanConwayDev@users.noreply.github.com>
master
jk 2 years ago committed by GitHub
parent
commit
8ae123fd0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 44
      .eslintrc
  2. 16
      .prettierrc
  3. 14
      .vscode/settings.json
  4. 7
      maintainers.yaml
  5. 10
      package.json
  6. 16
      src/lib/components/Container.svelte
  7. 28
      src/lib/components/Navbar.stories.svelte
  8. 24
      src/lib/components/Navbar.svelte
  9. 20
      src/lib/components/RepoSummaryCard.stories.svelte
  10. 38
      src/lib/components/RepoSummaryCard.svelte
  11. 72
      src/lib/components/ReposSummaryList.stories.svelte
  12. 11
      src/lib/components/ReposSummaryList.svelte
  13. 53
      src/lib/components/events/Compose.svelte
  14. 119
      src/lib/components/events/EventWrapper.svelte
  15. 2
      src/lib/components/events/ThreadWrapper.svelte
  16. 32
      src/lib/components/events/content/Kind19851985.svelte
  17. 95
      src/lib/components/events/content/Kind317.svelte
  18. 42
      src/lib/components/events/content/ParsedContent.svelte
  19. 147
      src/lib/components/events/content/utils.ts
  20. 12
      src/lib/components/events/type.ts
  21. 78
      src/lib/components/prs/PRDetails.svelte
  22. 20
      src/lib/components/prs/PRHeader.stories.svelte
  23. 136
      src/lib/components/prs/PRHeader.svelte
  24. 72
      src/lib/components/prs/PRsList.stories.svelte
  25. 51
      src/lib/components/prs/PRsList.svelte
  26. 22
      src/lib/components/prs/PRsListItem.stories.svelte
  27. 204
      src/lib/components/prs/PRsListItem.svelte
  28. 32
      src/lib/components/prs/Status.stories.svelte
  29. 128
      src/lib/components/prs/Status.svelte
  30. 220
      src/lib/components/prs/StatusSelector.svelte
  31. 15
      src/lib/components/prs/icons.ts
  32. 107
      src/lib/components/prs/type.ts
  33. 112
      src/lib/components/prs/vectors.ts
  34. 34
      src/lib/components/repo/RepoDetails.stories.svelte
  35. 167
      src/lib/components/repo/RepoDetails.svelte
  36. 20
      src/lib/components/repo/RepoHeader.stories.svelte
  37. 70
      src/lib/components/repo/RepoHeader.svelte
  38. 36
      src/lib/components/repo/type.ts
  39. 173
      src/lib/components/repo/vectors.ts
  40. 59
      src/lib/components/users/UserHeader.stories.svelte
  41. 53
      src/lib/components/users/UserHeader.svelte
  42. 39
      src/lib/components/users/type.ts
  43. 51
      src/lib/components/users/vectors.ts
  44. 10
      src/lib/kinds.ts
  45. 371
      src/lib/stores/PR.ts
  46. 335
      src/lib/stores/PRs.ts
  47. 41
      src/lib/stores/ndk.ts
  48. 184
      src/lib/stores/repo.ts
  49. 236
      src/lib/stores/users.ts
  50. 124
      src/lib/wrappers/Compose.svelte
  51. 57
      src/lib/wrappers/EventCard.svelte
  52. 34
      src/lib/wrappers/Navbar.svelte
  53. 14
      src/lib/wrappers/OpenPRs.svelte
  54. 8
      src/lib/wrappers/RepoDetails.svelte
  55. 96
      src/lib/wrappers/ReposRecent.svelte
  56. 40
      src/lib/wrappers/Thread.svelte
  57. 4
      src/routes/+layout.svelte
  58. 16
      src/routes/+page.svelte
  59. 159
      src/routes/ngit/+page.svelte
  60. 81
      src/routes/repo/[repo_id]/+page.svelte
  61. 6
      src/routes/repo/[repo_id]/+page.ts
  62. 200
      src/routes/repo/[repo_id]/pr/[pr_id]/+page.svelte
  63. 8
      src/routes/repo/[repo_id]/pr/[pr_id]/+page.ts
  64. 22
      tsconfig.json
  65. 272
      yarn.lock

44
.eslintrc

@ -0,0 +1,44 @@
// eslint-plugin-svelte
// https://sveltejs.github.io/eslint-plugin-svelte/user-guide/
{
"root": true,
"extends": [
// add more generic rule sets here, such as:
// 'eslint:recommended',
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:svelte/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
// ...
"project": "./tsconfig.json",
"extraFileExtensions": [".svelte"] // This is a required setting in `@typescript-eslint/parser` v4.24.0.
},
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
"no-console": 1,
"prettier/prettier": 2,
"comma-dangle": [2, "always-multiline"],
"no-alert": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
]
// 'svelte/rule-name': 'error'
},
"overrides": [
{
"files": ["*.svelte"],
"parser": "svelte-eslint-parser",
// Parse the `<script>` in `.svelte` as TypeScript by adding the following configuration.
"parserOptions": {
"parser": "@typescript-eslint/parser"
}
}
]
}

16
.prettierrc

@ -0,0 +1,16 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 80,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.eslintrc",
"options": {
"trailingComma": "none"
}
}
]
}

14
.vscode/settings.json vendored

@ -1,6 +1,10 @@
{ {
"diffEditor.ignoreTrimWhitespace": false, "eslint.validate": ["javascript", "javascriptreact", "svelte"],
"editor.formatOnPaste": true, "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "diffEditor.ignoreTrimWhitespace": false,
"svelte.enable-ts-plugin": true "editor.formatOnPaste": true,
} "editor.formatOnSave": true,
"files.associations": {
"*.css": "tailwindcss"
}
}

7
maintainers.yaml

@ -1,7 +0,0 @@
maintainers:
- npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr
relays:
- wss://relay.snort.social/
- wss://relayable.org/
- wss://relay.damus.io/
- wss://nos.lol/

10
package.json

@ -8,6 +8,8 @@
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint 'src/**/*.{ts,svelte}' --fix",
"format": "prettier 'src/**/*.{ts,svelte}' --write",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build", "build-storybook": "storybook build",
"test": "storybook build && (concurrently -k -s first \"http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && test-storybook --index-json --maxWorkers=2\") && rm storybook-static -r -f", "test": "storybook build && (concurrently -k -s first \"http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && test-storybook --index-json --maxWorkers=2\") && rm storybook-static -r -f",
@ -32,11 +34,19 @@
"@types/jest-image-snapshot": "^6.2.1", "@types/jest-image-snapshot": "^6.2.1",
"@types/node": "^20.8.2", "@types/node": "^20.8.2",
"@types/ramda": "^0.29.10", "@types/ramda": "^0.29.10",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-svelte": "^2.35.1",
"http-server": "^14.1.1", "http-server": "^14.1.1",
"jest-image-snapshot": "^6.2.0", "jest-image-snapshot": "^6.2.0",
"postcss": "^8.4.30", "postcss": "^8.4.30",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-tailwindcss": "^0.5.11",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"storybook": "^7.4.5", "storybook": "^7.4.5",

16
src/lib/components/Container.svelte

@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
export let no_wrap: boolean = false; export let no_wrap: boolean = false
</script> </script>
<div class="mx-auto lg:container"> <div class="mx-auto lg:container">
{#if no_wrap} {#if no_wrap}
<slot /> <slot />
{:else} {:else}
<div class="px-3"> <div class="px-3">
<slot /> <slot />
</div> </div>
{/if} {/if}
</div> </div>

28
src/lib/components/Navbar.stories.svelte

@ -1,18 +1,18 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { Meta } from "@storybook/svelte"; import type { Meta } from '@storybook/svelte'
import Navbar from "$lib/components/Navbar.svelte"; import Navbar from '$lib/components/Navbar.svelte'
import { Story, Template } from "@storybook/addon-svelte-csf"; import { Story, Template } from '@storybook/addon-svelte-csf'
import { UserVectors } from "$lib/components/users/vectors"; import { UserVectors } from '$lib/components/users/vectors'
export const meta: Meta<Navbar> = { export const meta: Meta<Navbar> = {
title: "Navbar", title: 'Navbar',
component: Navbar, component: Navbar,
tags: ["autodocs"], tags: ['autodocs'],
}; }
</script> </script>
<Template let:args> <Template let:args>
<Navbar {...args} /> <Navbar {...args} />
</Template> </Template>
<Story name="Default" /> <Story name="Default" />
@ -22,11 +22,11 @@
<Story name="NoNIP07" args={{ nip07_plugin: false }} /> <Story name="NoNIP07" args={{ nip07_plugin: false }} />
<Story <Story
name="NIP07Exists" name="NIP07Exists"
args={{ nip07_plugin: true, logged_in_user: undefined }} args={{ nip07_plugin: true, logged_in_user: undefined }}
/> />
<Story <Story
name="Logged in" name="Logged in"
args={{ nip07_plugin: true, logged_in_user: { ...UserVectors.default } }} args={{ nip07_plugin: true, logged_in_user: { ...UserVectors.default } }}
/> />

24
src/lib/components/Navbar.svelte

@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import Container from "./Container.svelte"; import Container from './Container.svelte'
import UserHeader from "./users/UserHeader.svelte"; import UserHeader from './users/UserHeader.svelte'
import type { User } from "./users/type"; import type { User } from './users/type'
export let logged_in_user: User | undefined = undefined; export let logged_in_user: User | undefined = undefined
export let nip07_plugin: boolean | undefined = undefined; export let nip07_plugin: boolean | undefined = undefined
export let login_function: Function = () => {}; export let login_function = () => {}
export let singup_function: Function = () => {}; export let singup_function = () => {}
</script> </script>
<div class="bg-base-400"> <div class="bg-base-400">
@ -24,20 +24,20 @@
{#if logged_in_user} {#if logged_in_user}
<UserHeader user={logged_in_user} /> <UserHeader user={logged_in_user} />
{:else if nip07_plugin === undefined} {:else if nip07_plugin === undefined}
<div class="h-8 skeleton w-20"></div> <div class="skeleton h-8 w-20"></div>
{:else if nip07_plugin} {:else if nip07_plugin}
<button <button
on:click={() => { on:click={() => {
login_function(); login_function()
}} }}
class="btn normal-case btn-sm btn-ghost">Login</button class="btn btn-ghost btn-sm normal-case">Login</button
> >
{:else} {:else}
<button <button
on:click={() => { on:click={() => {
singup_function(); singup_function()
}} }}
class="btn normal-case btn-sm btn-ghost">Sign up</button class="btn btn-ghost btn-sm normal-case">Sign up</button
> >
{/if} {/if}
</div> </div>

20
src/lib/components/RepoSummaryCard.stories.svelte

@ -1,18 +1,18 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { Meta } from "@storybook/svelte"; import type { Meta } from '@storybook/svelte'
import RepoSummaryCard from "./RepoSummaryCard.svelte"; import RepoSummaryCard from './RepoSummaryCard.svelte'
import { Story, Template } from "@storybook/addon-svelte-csf"; import { Story, Template } from '@storybook/addon-svelte-csf'
import { RepoSummaryCardArgsVectors as vectors } from "./repo/vectors"; import { RepoSummaryCardArgsVectors as vectors } from './repo/vectors'
export const meta: Meta<RepoSummaryCard> = { export const meta: Meta<RepoSummaryCard> = {
title: "Repo/Summary/Card", title: 'Repo/Summary/Card',
component: RepoSummaryCard, component: RepoSummaryCard,
tags: ["autodocs"], tags: ['autodocs'],
}; }
</script> </script>
<Template let:args> <Template let:args>
<RepoSummaryCard {...args} /> <RepoSummaryCard {...args} />
</Template> </Template>
<Story name="Short Details" args={vectors.Short} /> <Story name="Short Details" args={vectors.Short} />

38
src/lib/components/RepoSummaryCard.svelte

@ -1,38 +1,38 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
export interface Args { export interface Args {
name: string; name: string
description: string; description: string
repo_id: string; repo_id: string
loading?: boolean; loading?: boolean
created_at: number; created_at: number
} }
export const defaults: Args = { export const defaults: Args = {
name: "", name: '',
repo_id: "", repo_id: '',
description: "", description: '',
loading: false, loading: false,
created_at: 0, created_at: 0,
}; }
</script> </script>
<script lang="ts"> <script lang="ts">
export let { name, description, repo_id: repo_id, loading } = defaults; export let { name, description, repo_id: repo_id, loading } = defaults
let short_name: string; let short_name: string
$: { $: {
if (name.length > 45) short_name = name.slice(0, 45) + "..."; if (name.length > 45) short_name = name.slice(0, 45) + '...'
else if (name.length == 0) short_name = "Untitled"; else if (name.length == 0) short_name = 'Untitled'
else short_name = name; else short_name = name
} }
$: short_descrption = $: short_descrption =
description.length > 50 ? description.slice(0, 45) + "..." : description; description.length > 50 ? description.slice(0, 45) + '...' : description
</script> </script>
<div class="p-4 bg-base-200 my-2 rounded-lg"> <div class="my-2 rounded-lg bg-base-200 p-4">
{#if loading} {#if loading}
<div class="h-5 mb-2 skeleton w-40"></div> <div class="skeleton mb-2 h-5 w-40"></div>
<div class="h-4 skeleton w-100"></div> <div class="w-100 skeleton h-4"></div>
{:else} {:else}
<a class="link-primary break-words" href="/repo/{repo_id}">{short_name}</a> <a class="link-primary break-words" href="/repo/{repo_id}">{short_name}</a>
<p class="text-sm text-muted break-words">{short_descrption}</p> <p class="text-muted break-words text-sm">{short_descrption}</p>
{/if} {/if}
</div> </div>

72
src/lib/components/ReposSummaryList.stories.svelte

@ -1,56 +1,56 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { Meta } from "@storybook/svelte"; import type { Meta } from '@storybook/svelte'
import ReposSummaryList from "./ReposSummaryList.svelte"; import ReposSummaryList from './ReposSummaryList.svelte'
import { Story, Template } from "@storybook/addon-svelte-csf"; import { Story, Template } from '@storybook/addon-svelte-csf'
import { RepoSummaryCardArgsVectors as vectors } from "./repo/vectors"; import { RepoSummaryCardArgsVectors as vectors } from './repo/vectors'
export const meta: Meta<ReposSummaryList> = { export const meta: Meta<ReposSummaryList> = {
title: "Repo/Summary/List", title: 'Repo/Summary/List',
component: ReposSummaryList, component: ReposSummaryList,
tags: ["autodocs"], tags: ['autodocs'],
}; }
</script> </script>
<Template let:args> <Template let:args>
<ReposSummaryList {...args} /> <ReposSummaryList {...args} />
</Template> </Template>
<Story <Story
name="Default" name="Default"
args={{ args={{
title: "Featured Repositories", title: 'Featured Repositories',
repos: [vectors.Short, vectors.Long, vectors.LongNoSpaces], repos: [vectors.Short, vectors.Long, vectors.LongNoSpaces],
}} }}
/> />
<Story <Story
name="No Title" name="No Title"
args={{ args={{
repos: [vectors.Short, vectors.Long], repos: [vectors.Short, vectors.Long],
}} }}
/> />
<Story <Story
name="Empty" name="Empty"
args={{ args={{
title: "Latest", title: 'Latest',
repos: [], repos: [],
}} }}
/> />
<Story <Story
name="Loading" name="Loading"
args={{ args={{
title: "Latest", title: 'Latest',
repos: [], repos: [],
loading: true, loading: true,
}} }}
/> />
<Story <Story
name="Partially Loaded" name="Partially Loaded"
args={{ args={{
title: "Latest", title: 'Latest',
repos: [vectors.Short, vectors.Long], repos: [vectors.Short, vectors.Long],
loading: true, loading: true,
}} }}
/> />

11
src/lib/components/ReposSummaryList.svelte

@ -1,14 +1,11 @@
<script lang="ts"> <script lang="ts">
import { fade } from "svelte/transition";
import { onMount } from "svelte";
import RepoSummaryCard, { import RepoSummaryCard, {
type Args as RepoSummaryCardArgs, type Args as RepoSummaryCardArgs,
} from "$lib/components/RepoSummaryCard.svelte"; } from '$lib/components/RepoSummaryCard.svelte'
export let title: string = ""; export let title: string = ''
export let repos: RepoSummaryCardArgs[] = []; export let repos: RepoSummaryCardArgs[] = []
export let loading: boolean = false; export let loading: boolean = false
</script> </script>
<div class="min-width"> <div class="min-width">

53
src/lib/components/events/Compose.svelte

@ -1,32 +1,31 @@
<script lang="ts"> <script lang="ts">
import ParsedContent from "./content/ParsedContent.svelte"; export let sendReply: (content: string) => void = () => {}
export let sendReply: (content: string) => void = (content: string) => {}; export let submitting = false
export let submitting = false; let submit = () => {
let submit = () => { sendReply(content)
sendReply(content); }
}; let content = ''
let content = "";
</script> </script>
<div class=""> <div class="">
<textarea <textarea
disabled={submitting} disabled={submitting}
bind:value={content} bind:value={content}
class="textarea textarea-primary w-full" class="textarea textarea-primary w-full"
placeholder="reply..." placeholder="reply..."
></textarea> ></textarea>
<div class="flex"> <div class="flex">
<div class="flex-auto"></div> <div class="flex-auto"></div>
<button <button
on:click={submit} on:click={submit}
disabled={submitting} disabled={submitting}
class="btn btn-primary align-bottom mt-2 btn-sm align-right" class="align-right btn btn-primary btn-sm mt-2 align-bottom"
> >
{#if submitting} {#if submitting}
Sending Sending
{:else} {:else}
Send Send
{/if} {/if}
</button> </button>
</div> </div>
</div> </div>

119
src/lib/components/events/EventWrapper.svelte

@ -1,68 +1,67 @@
<script lang="ts"> <script lang="ts">
import dayjs from "dayjs"; import dayjs from 'dayjs'
import UserHeader from "../users/UserHeader.svelte"; import UserHeader from '../users/UserHeader.svelte'
import type { User } from "../users/type"; import type { User } from '../users/type'
import { defaults as user_defaults } from "../users/type"; import { defaults as user_defaults } from '../users/type'
import Compose from "$lib/wrappers/Compose.svelte"; import Compose from '$lib/wrappers/Compose.svelte'
import { logged_in_user } from "$lib/stores/users"; import { logged_in_user } from '$lib/stores/users'
export let author: User = { ...user_defaults }; export let author: User = { ...user_defaults }
export let created_at: number | undefined; export let created_at: number | undefined
export let event_id = ""; export let event_id = ''
export let logged_in = $logged_in_user; export let logged_in = $logged_in_user
let show_compose = false; let show_compose = false
let created_at_ago = ""; let created_at_ago = ''
$: created_at_ago = created_at ? dayjs(created_at * 1000).fromNow() : ""; $: created_at_ago = created_at ? dayjs(created_at * 1000).fromNow() : ''
</script> </script>
<div class="pl-3 p-3 border-b border-base-300"> <div class="border-b border-base-300 p-3 pl-3">
<div class="flex"> <div class="flex">
<div class="flex-auto"> <div class="flex-auto">
<UserHeader user={author} /> <UserHeader user={author} />
</div>
{#if !show_compose}
<div class="mt-1 mb-1 aling-middle">
<span class="text-xs mb-1">{created_at_ago}</span>
{#if logged_in}
<button
on:click={() => {
show_compose = true;
}}
class="btn btn-xs"
><svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
><path
fill="currentColor"
d="M6.78 1.97a.75.75 0 0 1 0 1.06L3.81 6h6.44A4.75 4.75 0 0 1 15 10.75v2.5a.75.75 0 0 1-1.5 0v-2.5a3.25 3.25 0 0 0-3.25-3.25H3.81l2.97 2.97a.749.749 0 0 1-.326 1.275a.749.749 0 0 1-.734-.215L1.47 7.28a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0"
/></svg
></button
>
{/if}
</div>
{/if}
</div> </div>
<div class="ml-11"> {#if !show_compose}
<slot /> <div class="aling-middle mb-1 mt-1">
{#if show_compose} <span class="mb-1 text-xs">{created_at_ago}</span>
<div class=""> {#if logged_in}
<div class="flex"> <button
<div class="flex-auto"></div> on:click={() => {
<button show_compose = true
on:click={() => { }}
show_compose = false; class="btn btn-xs"
}} ><svg
class="btn btn-sm btn-circle btn-ghost right-2 top-2" xmlns="http://www.w3.org/2000/svg"
>✕</button width="16"
> height="16"
</div> viewBox="0 0 16 16"
<div class=""> ><path
<Compose reply_to_event_id={event_id} /> fill="currentColor"
</div> d="M6.78 1.97a.75.75 0 0 1 0 1.06L3.81 6h6.44A4.75 4.75 0 0 1 15 10.75v2.5a.75.75 0 0 1-1.5 0v-2.5a3.25 3.25 0 0 0-3.25-3.25H3.81l2.97 2.97a.749.749 0 0 1-.326 1.275a.749.749 0 0 1-.734-.215L1.47 7.28a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0"
</div> /></svg
></button
>
{/if} {/if}
</div> </div>
{/if}
</div>
<div class="ml-11">
<slot />
{#if show_compose}
<div class="">
<div class="flex">
<div class="flex-auto"></div>
<button
on:click={() => {
show_compose = false
}}
class="btn btn-circle btn-ghost btn-sm right-2 top-2">✕</button
>
</div>
<div class="">
<Compose reply_to_event_id={event_id} />
</div>
</div>
{/if}
</div>
</div> </div>

2
src/lib/components/events/ThreadWrapper.svelte

@ -1,3 +1,3 @@
<div class="border-l border-blue-500 pl-1"> <div class="border-l border-blue-500 pl-1">
<slot /> <slot />
</div> </div>

32
src/lib/components/events/content/Kind19851985.svelte

@ -1,23 +1,23 @@
<script lang="ts"> <script lang="ts">
import type { NDKTag } from "@nostr-dev-kit/ndk"; import type { NDKTag } from '@nostr-dev-kit/ndk'
import Status from "$lib/components/prs/Status.svelte"; import Status from '$lib/components/prs/Status.svelte'
export let tags: NDKTag[] = []; export let tags: NDKTag[] = []
function extractTagContent(name: string): string | undefined { function extractTagContent(name: string): string | undefined {
let tag = tags.find((tag) => tag[0] === name); let tag = tags.find((tag) => tag[0] === name)
return tag ? tag[1] : undefined; return tag ? tag[1] : undefined
} }
let status: string | undefined; let status: string | undefined
$: { $: {
status = extractTagContent("l"); status = extractTagContent('l')
} }
</script> </script>
<div class=""> <div class="">
{#if status} {#if status}
set status to <Status {status} /> set status to <Status {status} />
{:else} {:else}
set status incorrectly set status incorrectly
{/if} {/if}
</div> </div>

95
src/lib/components/events/content/Kind317.svelte

@ -1,59 +1,54 @@
<script lang="ts"> <script lang="ts">
import type { NDKTag } from "@nostr-dev-kit/ndk"; import type { NDKTag } from '@nostr-dev-kit/ndk'
import parseDiff from "parse-diff"; import parseDiff from 'parse-diff'
import ParsedContent from "./ParsedContent.svelte"; import ParsedContent from './ParsedContent.svelte'
export let content: string = ""; export let content: string = ''
export let tags: NDKTag[] = []; export let tags: NDKTag[] = []
export let lite: boolean = true;
let commit_id = extractTagContent("commit") || "[unknown commit_id]"; let commit_id = extractTagContent('commit') || '[unknown commit_id]'
let commit_message = extractTagContent("description") || "[untitled]"; let commit_message = extractTagContent('description') || '[untitled]'
let files = parseDiff(content); let files = parseDiff(content)
function extractTagContent(name: string): string | undefined { function extractTagContent(name: string): string | undefined {
let tag = tags.find((tag) => tag[0] === name); let tag = tags.find((tag) => tag[0] === name)
return tag ? tag[1] : undefined; return tag ? tag[1] : undefined
} }
</script> </script>
<div class=""> <div class="">
<div class="bg-base-300 rounded-t p-1 flex"> <div class="flex rounded-t bg-base-300 p-1">
<article class="ml-2 prose font-mono flex-grow"> <article class="prose ml-2 flex-grow font-mono">
<ParsedContent content={commit_message} /> <ParsedContent content={commit_message} />
</article> </article>
<div class="text-xs text-neutral p-1 flex-none align-middle"> <div class="flex-none p-1 align-middle text-xs text-neutral">commit</div>
commit </div>
</div>
</div>
<div class="bg-base-200 p-1 rounded-b"> <div class="rounded-b bg-base-200 p-1">
<table class="table table-xs table-zebra"> <table class="table table-zebra table-xs">
<tr> <tr>
<td class="text-xs">Changes: </td> <td class="text-xs">Changes: </td>
<td class="text-right"> <td class="text-right">
<span class="text-xs font-mono" <span class="font-mono text-xs">{commit_id.substring(0, 8)}</span>
>{commit_id.substring(0, 8)}</span </td>
> </tr>
</td> {#each files as file}
</tr> <tr>
{#each files as file} <td>
<tr> <span
<td> class:text-success={file.new}
<span class:text-error={file.deleted}
class:text-success={file.new} class="text-success"
class:text-error={file.deleted} >
class="text-success" {file.to || file.from}
> </span>
{file.to || file.from} </td>
</span> <td class="text-right">
</td> <span class="text-success">+{file.additions}</span>
<td class="text-right"> <span class="text-error">- {file.deletions}</span>
<span class="text-success">+{file.additions}</span> </td>
<span class="text-error">- {file.deletions}</span> </tr>
</td> {/each}
</tr> </table>
{/each} </div>
</table>
</div>
</div> </div>

42
src/lib/components/events/content/ParsedContent.svelte

@ -1,28 +1,28 @@
<script lang="ts"> <script lang="ts">
import type { NDKTag } from "@nostr-dev-kit/ndk"; import type { NDKTag } from '@nostr-dev-kit/ndk'
import { import {
isParsedNewLine, isParsedNewLine,
isParsedText, isParsedText,
parseContent, parseContent,
type ParsedPart, type ParsedPart,
} from "./utils"; } from './utils'
export let content: string = ""; export let content: string = ''
export let tags: NDKTag[] = []; export let tags: NDKTag[] = []
let fullContent: ParsedPart[] = []; let fullContent: ParsedPart[] = []
$: fullContent = parseContent({ content, tags }); $: fullContent = parseContent({ content, tags })
</script> </script>
<div> <div>
{#each fullContent as part, i} {#each fullContent as part}
{#if isParsedNewLine(part)} {#if isParsedNewLine(part)}
{#if part.value.length > 1} {#if part.value.length > 1}
<br /> <br />
{/if} {/if}
<br /> <br />
{:else if isParsedText(part)} {:else if isParsedText(part)}
{part.value} {part.value}
{/if} {/if}
{/each} {/each}
</div> </div>

147
src/lib/components/events/content/utils.ts

@ -1,96 +1,95 @@
import type { NDKTag } from "@nostr-dev-kit/ndk"; import type { NDKTag } from '@nostr-dev-kit/ndk'
import { nip19 } from "nostr-tools"; import { last } from 'ramda'
import { identity, last, pluck } from "ramda";
export const TOPIC = 'topic'
export const TOPIC = "topic"; export const LINK = 'link'
export const LINK = "link"; export const LINKCOLLECTION = 'link[]'
export const LINKCOLLECTION = "link[]"; export const HTML = 'html'
export const HTML = "html"; export const INVOICE = 'invoice'
export const INVOICE = "invoice"; export const NOSTR_NOTE = 'nostr:note'
export const NOSTR_NOTE = "nostr:note"; export const NOSTR_NEVENT = 'nostr:nevent'
export const NOSTR_NEVENT = "nostr:nevent"; export const NOSTR_NPUB = 'nostr:npub'
export const NOSTR_NPUB = "nostr:npub"; export const NOSTR_NPROFILE = 'nostr:nprofile'
export const NOSTR_NPROFILE = "nostr:nprofile"; export const NOSTR_NADDR = 'nostr:naddr'
export const NOSTR_NADDR = "nostr:naddr";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const first = (list: any) => (list ? list[0] : undefined); const first = (list: any) => (list ? list[0] : undefined)
export const fromNostrURI = (s: string) => s.replace(/^[\w+]+:\/?\/?/, ""); export const fromNostrURI = (s: string) => s.replace(/^[\w+]+:\/?\/?/, '')
export const urlIsMedia = (url: string) => export const urlIsMedia = (url: string) =>
!url.match(/\.(apk|docx|xlsx|csv|dmg)/) && last(url.split("://"))?.includes("/"); !url.match(/\.(apk|docx|xlsx|csv|dmg)/) &&
last(url.split('://'))?.includes('/')
export type ContentArgs = { export type ContentArgs = {
content: string; content: string
tags?: Array<NDKTag>; tags?: Array<NDKTag>
}; }
export type ParsedPart = ParsedNewLine | ParsedText; export type ParsedPart = ParsedNewLine | ParsedText
export const NEWLINE = "newline"; export const NEWLINE = 'newline'
export type ParsedNewLine = { export type ParsedNewLine = {
type: "newline", type: 'newline'
value: string, value: string
}; }
export const isParsedNewLine = (part: ParsedPart): part is ParsedNewLine => { export const isParsedNewLine = (part: ParsedPart): part is ParsedNewLine => {
return part.type == "newline" return part.type == 'newline'
}; }
export const TEXT = "text"; export const TEXT = 'text'
export type ParsedText = { export type ParsedText = {
type: "text", type: 'text'
value: string, value: string
}; }
export const isParsedText = (part: ParsedPart): part is ParsedText => { export const isParsedText = (part: ParsedPart): part is ParsedText => {
return part.type == "text" return part.type == 'text'
}; }
export const parseContent = ({ content, tags = [] }: ContentArgs): ParsedPart[] => { export const parseContent = ({ content }: ContentArgs): ParsedPart[] => {
const result: ParsedPart[] = []; const result: ParsedPart[] = []
let text = content.trim(); let text = content.trim()
let buffer = ""; let buffer = ''
const parseNewline = () => { const parseNewline = () => {
const newline = first(text.match(/^\n+/)); const newline = first(text.match(/^\n+/))
if (newline) { if (newline) {
return [NEWLINE, newline, newline]; return [NEWLINE, newline, newline]
}
};
while (text) {
// The order that this runs matters
const part =
parseNewline();
if (part) {
if (buffer) {
result.push({ type: "text", value: buffer });
buffer = "";
}
const [type, raw, value] = part;
result.push({ type, value });
text = text.slice(raw.length);
} else {
// Instead of going character by character and re-running all the above regular expressions
// a million times, try to match the next word and add it to the buffer
const match = first(text.match(/^[\w\d]+ ?/i)) || text[0];
buffer += match;
text = text.slice(match.length);
}
} }
}
while (text) {
// The order that this runs matters
const part = parseNewline()
if (buffer) { if (part) {
result.push({ type: TEXT, value: buffer }); if (buffer) {
result.push({ type: 'text', value: buffer })
buffer = ''
}
const [type, raw, value] = part
result.push({ type, value })
text = text.slice(raw.length)
} else {
// Instead of going character by character and re-running all the above regular expressions
// a million times, try to match the next word and add it to the buffer
const match = first(text.match(/^[\w\d]+ ?/i)) || text[0]
buffer += match
text = text.slice(match.length)
} }
}
if (buffer) {
result.push({ type: TEXT, value: buffer })
}
return result; return result
}; }

12
src/lib/components/events/type.ts

@ -1,12 +1,6 @@
import { defaults as user_defaults } from "../users/type"; import type { User } from '../users/type'
import type { User } from "../users/type";
export interface Event { export interface Event {
author: User; author: User
content: any; content: unknown
} }
let defaults: Event = {
author: { ...user_defaults },
content: [],
}

78
src/lib/components/prs/PRDetails.svelte

@ -1,47 +1,47 @@
<script lang="ts"> <script lang="ts">
import { full_defaults } from "./type"; import { full_defaults } from './type'
import UserHeader from "../users/UserHeader.svelte"; import UserHeader from '../users/UserHeader.svelte'
import StatusSelector from "./StatusSelector.svelte"; import StatusSelector from './StatusSelector.svelte'
export let { summary, labels, loading } = { ...full_defaults }; export let { summary, labels, loading } = { ...full_defaults }
</script> </script>
<div class="max-w-md"> <div class="max-w-md">
<div> <div>
{#if loading} {#if loading}
<div class="h-5 my-3 skeleton w-20"></div> <div class="skeleton my-3 h-5 w-20"></div>
<div class="badge skeleton my-2 w-60 block"></div> <div class="badge skeleton my-2 block w-60"></div>
<div class="badge skeleton my-2 w-40 block"></div> <div class="badge skeleton my-2 block w-40"></div>
{:else} {:else}
<h4>Author</h4> <h4>Author</h4>
<UserHeader user={summary.author} /> <UserHeader user={summary.author} />
{/if} {/if}
</div> </div>
<div> <div>
{#if loading} {#if loading}
<div class="h-5 my-3 skeleton w-20"></div> <div class="skeleton my-3 h-5 w-20"></div>
<div class="badge skeleton my-2 w-60 block"></div> <div class="badge skeleton my-2 block w-60"></div>
<div class="badge skeleton my-2 w-40 block"></div> <div class="badge skeleton my-2 block w-40"></div>
{:else} {:else}
<h4>Status</h4> <h4>Status</h4>
<StatusSelector <StatusSelector
status={summary.status} status={summary.status}
repo_id={summary.repo_id} repo_id={summary.repo_id}
pr_id={summary.id} pr_id={summary.id}
/> />
{/if} {/if}
</div> </div>
<div> <div>
{#if loading} {#if loading}
<div class="badge skeleton w-20"></div> <div class="badge skeleton w-20"></div>
<div class="badge skeleton w-20"></div> <div class="badge skeleton w-20"></div>
{:else} {:else}
<h4>Labels</h4> <h4>Labels</h4>
{#each labels as label} {#each labels as label}
<div class="badge badge-secondary mr-2">{label}</div> <div class="badge badge-secondary mr-2">{label}</div>
{/each} {/each}
{/if} {/if}
</div> </div>
</div> </div>

20
src/lib/components/prs/PRHeader.stories.svelte

@ -1,18 +1,18 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { Meta } from "@storybook/svelte"; import type { Meta } from '@storybook/svelte'
import PRHeader from "./PRHeader.svelte"; import PRHeader from './PRHeader.svelte'
import { Story, Template } from "@storybook/addon-svelte-csf"; import { Story, Template } from '@storybook/addon-svelte-csf'
import { PRsListItemArgsVectors as vectors } from "./vectors"; import { PRsListItemArgsVectors as vectors } from './vectors'
export const meta: Meta<PRHeader> = { export const meta: Meta<PRHeader> = {
title: "PRs/Header", title: 'PRs/Header',
component: PRHeader, component: PRHeader,
tags: ["autodocs"], tags: ['autodocs'],
}; }
</script> </script>
<Template let:args> <Template let:args>
<PRHeader {...args} /> <PRHeader {...args} />
</Template> </Template>
<Story name="Short Details" args={vectors.Short} /> <Story name="Short Details" args={vectors.Short} />

136
src/lib/components/prs/PRHeader.svelte

@ -2,78 +2,76 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import dayjs from "dayjs"; import dayjs from 'dayjs'
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from 'dayjs/plugin/relativeTime'
import { summary_defaults } from "./type"; import { summary_defaults } from './type'
import { getName } from "../users/type"; import { getName } from '../users/type'
import Container from "../Container.svelte"; import Container from '../Container.svelte'
import Status from "./Status.svelte"; import Status from './Status.svelte'
dayjs.extend(relativeTime); dayjs.extend(relativeTime)
export let { export let {
title, title,
descritpion, descritpion,
id, id,
repo_id, repo_id,
comments, comments,
status, status,
status_date, status_date,
author, author,
created_at, created_at,
loading, loading,
} = summary_defaults; } = summary_defaults
let short_title: string; let short_title: string
let created_at_ago: string; let created_at_ago: string
let author_name = ""; let author_name = ''
$: { $: {
author_name = getName(author); author_name = getName(author)
} }
$: { $: {
if (title.length > 70) short_title = title.slice(0, 65) + "..."; if (title.length > 70) short_title = title.slice(0, 65) + '...'
else if (title.length == 0) short_title = "Untitled"; else if (title.length == 0) short_title = 'Untitled'
else short_title = title; else short_title = title
created_at_ago = created_at ? dayjs(created_at * 1000).fromNow() : ""; created_at_ago = created_at ? dayjs(created_at * 1000).fromNow() : ''
} }
</script> </script>
<div <div
class="overflow-hidden grow text-xs text-neutral-content bg-base-200 border-b border-accent-content pt-2 pb-4" class="grow overflow-hidden border-b border-accent-content bg-base-200 pb-4 pt-2 text-xs text-neutral-content"
> >
<Container> <Container>
{#if loading} {#if loading}
<div> <div>
<div class="h-7 w-60 pt-1 skeleton"></div> <div class="skeleton h-7 w-60 pt-1"></div>
<div class=""> <div class="">
<div <div class="skeleton mt-3 inline-block h-8 w-20 align-middle"></div>
class="h-8 w-20 mt-3 skeleton align-middle inline-block" <div
></div> class="skeleton ml-3 mt-5 inline-block h-3 w-28 align-middle"
<div ></div>
class="h-3 w-28 ml-3 mt-5 align-middle skeleton inline-block" <div
></div> class="skeleton ml-3 mt-5 inline-block h-3 w-28 align-middle"
<div ></div>
class="h-3 w-28 ml-3 mt-5 align-middle skeleton inline-block" </div>
></div> </div>
</div> {:else}
</div> <div class="mb-2 text-lg text-base-content">
{:else} {short_title}
<div class="text-lg text-base-content mb-2"> </div>
{short_title} <div class="pt-1">
</div> <div class="mr-3 inline align-middle">
<div class="pt-1"> <Status {status} />
<div class="inline mr-3 align-middle"> </div>
<Status {status} /> <div class="mr-3 inline align-middle">
</div> opened {created_at_ago}
<div class="inline mr-3 align-middle"> </div>
opened {created_at_ago} <div class="inline align-middle">
</div> {#if author.loading}
<div class="inline align-middle"> <div class="skeleton inline-block h-3 w-20 pb-2"></div>
{#if author.loading} {:else}
<div class="skeleton h-3 pb-2 w-20 inline-block"></div> {author_name}
{:else} {/if}
{author_name} </div>
{/if} </div>
</div> {/if}
</div> </Container>
{/if}
</Container>
</div> </div>

72
src/lib/components/prs/PRsList.stories.svelte

@ -1,56 +1,56 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { Meta } from "@storybook/svelte"; import type { Meta } from '@storybook/svelte'
import PRsList from "./PRsList.svelte"; import PRsList from './PRsList.svelte'
import { Story, Template } from "@storybook/addon-svelte-csf"; import { Story, Template } from '@storybook/addon-svelte-csf'
import { PRsListItemArgsVectors as vectors } from "./vectors"; import { PRsListItemArgsVectors as vectors } from './vectors'
export const meta: Meta<PRsList> = { export const meta: Meta<PRsList> = {
title: "PRs/List/List", title: 'PRs/List/List',
component: PRsList, component: PRsList,
tags: ["autodocs"], tags: ['autodocs'],
}; }
</script> </script>
<Template let:args> <Template let:args>
<PRsList {...args} /> <PRsList {...args} />
</Template> </Template>
<Story <Story
name="Default" name="Default"
args={{ args={{
title: "Open PRs", title: 'Open PRs',
prs: [vectors.Short, vectors.Long, vectors.LongNoSpaces], prs: [vectors.Short, vectors.Long, vectors.LongNoSpaces],
}} }}
/> />
<Story <Story
name="No Title" name="No Title"
args={{ args={{
prs: [vectors.Short, vectors.Long], prs: [vectors.Short, vectors.Long],
}} }}
/> />
<Story <Story
name="Empty" name="Empty"
args={{ args={{
title: "Open PRs", title: 'Open PRs',
prs: [], prs: [],
}} }}
/> />
<Story <Story
name="Loading" name="Loading"
args={{ args={{
title: "Open PRs", title: 'Open PRs',
prs: [], prs: [],
loading: true, loading: true,
}} }}
/> />
<Story <Story
name="Partially Loaded" name="Partially Loaded"
args={{ args={{
title: "Open PRs", title: 'Open PRs',
prs: [vectors.Short, vectors.Long], prs: [vectors.Short, vectors.Long],
loading: true, loading: true,
}} }}
/> />

51
src/lib/components/prs/PRsList.svelte

@ -1,34 +1,31 @@
<script lang="ts"> <script lang="ts">
import { fade } from "svelte/transition"; import PRsListItem from '$lib/components/prs/PRsListItem.svelte'
import { onMount } from "svelte"; import type { PRSummary } from './type'
import PRsListItem from "$lib/components/prs/PRsListItem.svelte"; export let title: string = ''
import type { PRSummary } from "./type"; export let prs: PRSummary[] = []
export let loading: boolean = false
export let title: string = "";
export let prs: PRSummary[] = [];
export let loading: boolean = false;
</script> </script>
<div class=""> <div class="">
{#if title.length > 0} {#if title.length > 0}
<div class="prose"> <div class="prose">
<h4>{title}</h4> <h4>{title}</h4>
</div> </div>
{/if} {/if}
{#if prs.length == 0 && !loading} {#if prs.length == 0 && !loading}
<p class="prose">None</p> <p class="prose">None</p>
{/if}
<ul class=" divide-y divide-neutral-600">
{#each prs as pr}
<PRsListItem {...pr} />
{/each}
{#if loading}
<PRsListItem loading={true} />
{#if prs.length == 0}
<PRsListItem loading={true} />
<PRsListItem loading={true} />
{/if}
{/if} {/if}
<ul class=" divide-y divide-neutral-600"> </ul>
{#each prs as pr}
<PRsListItem {...pr} />
{/each}
{#if loading}
<PRsListItem loading={true} />
{#if prs.length == 0}
<PRsListItem loading={true} />
<PRsListItem loading={true} />
{/if}
{/if}
</ul>
</div> </div>

22
src/lib/components/prs/PRsListItem.stories.svelte

@ -1,18 +1,18 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { Meta } from "@storybook/svelte"; import type { Meta } from '@storybook/svelte'
import PRsListItem from "./PRsListItem.svelte"; import PRsListItem from './PRsListItem.svelte'
import { Story, Template } from "@storybook/addon-svelte-csf"; import { Story, Template } from '@storybook/addon-svelte-csf'
import { PRsListItemArgsVectors as vectors } from "./vectors"; import { PRsListItemArgsVectors as vectors } from './vectors'
export const meta: Meta<PRsListItem> = { export const meta: Meta<PRsListItem> = {
title: "PRs/List/Item", title: 'PRs/List/Item',
component: PRsListItem, component: PRsListItem,
tags: ["autodocs"], tags: ['autodocs'],
}; }
</script> </script>
<Template let:args> <Template let:args>
<PRsListItem {...args} /> <PRsListItem {...args} />
</Template> </Template>
<Story name="Short Details" args={vectors.Short} /> <Story name="Short Details" args={vectors.Short} />

204
src/lib/components/prs/PRsListItem.svelte

@ -2,118 +2,116 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import dayjs from "dayjs"; import dayjs from 'dayjs'
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from 'dayjs/plugin/relativeTime'
import { summary_defaults } from "./type"; import { summary_defaults } from './type'
import { getName } from "../users/type"; import { getName } from '../users/type'
import { pr_icon_path } from "./icons"; import { pr_icon_path } from './icons'
dayjs.extend(relativeTime); dayjs.extend(relativeTime)
export let { export let {
title, title,
id, id,
repo_id, repo_id,
comments, comments,
status, status,
author, author,
created_at, created_at,
loading, loading,
} = summary_defaults; } = summary_defaults
let short_title: string; let short_title: string
let created_at_ago: string; let created_at_ago: string
let author_name = ""; let author_name = ''
$: { $: {
author_name = getName(author); author_name = getName(author)
} }
$: { $: {
if (title.length > 70) short_title = title.slice(0, 65) + "..."; if (title.length > 70) short_title = title.slice(0, 65) + '...'
else if (title.length == 0) short_title = "Untitled"; else if (title.length == 0) short_title = 'Untitled'
else short_title = title; else short_title = title
created_at_ago = created_at ? dayjs(created_at * 1000).fromNow() : ""; created_at_ago = created_at ? dayjs(created_at * 1000).fromNow() : ''
} }
</script> </script>
<li <li
class="flex p-2 pt-4 {!loading class="flex p-2 pt-4 {!loading ? 'cursor-pointer hover:bg-neutral-700' : ''}"
? 'hover:bg-neutral-700 cursor-pointer'
: ''}"
> >
<!-- <figure class="p-4 pl-0 text-color-primary"> --> <!-- <figure class="p-4 pl-0 text-color-primary"> -->
<!-- http://icon-sets.iconify.design/octicon/git-pull-request-16/ --> <!-- http://icon-sets.iconify.design/octicon/git-pull-request-16/ -->
{#if loading || !status} {#if loading || !status}
<div class="h-5 w-5 pt-1 flex-none skeleton"></div> <div class="skeleton h-5 w-5 flex-none pt-1"></div>
{:else if status === "Open"} {:else if status === 'Open'}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
class="h-5 w-5 pt-1 flex-none fill-success" class="h-5 w-5 flex-none fill-success pt-1"
><path d={pr_icon_path.open} /></svg ><path d={pr_icon_path.open} /></svg
> >
{:else if status === "Closed"} {:else if status === 'Closed'}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
class="h-5 w-5 pt-1 flex-none fill-neutral-content" class="h-5 w-5 flex-none fill-neutral-content pt-1"
><path d={pr_icon_path.close} /></svg ><path d={pr_icon_path.close} /></svg
> >
{:else if status === "Draft"} {:else if status === 'Draft'}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
class="h-5 w-5 pt-1 flex-none fill-neutral-content" class="h-5 w-5 flex-none fill-neutral-content pt-1"
><path d={pr_icon_path.draft} /></svg ><path d={pr_icon_path.draft} /></svg
>
{:else if status === "Merged"}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
class="h-5 w-5 pt-1 flex-none fill-primary"
><path d={pr_icon_path.merge} /></svg
>
{/if}
<a
href="/repo/{repo_id}/pr/{id}"
class="ml-3 overflow-hidden grow text-xs text-neutral-content"
> >
{#if loading} {:else if status === 'Merged'}
<div class="h-5 w-60 pt-1 flex-none skeleton"></div> <svg
<div class="h-3 w-40 mt-3 mb-1 flex-none skeleton"></div> xmlns="http://www.w3.org/2000/svg"
{:else} viewBox="0 0 16 16"
<div class="text-sm text-base-content"> class="h-5 w-5 flex-none fill-primary pt-1"
{short_title} ><path d={pr_icon_path.merge} /></svg
</div> >
<!-- <div class="text-xs text-neutral-content"> {/if}
<a
href="/repo/{repo_id}/pr/{id}"
class="ml-3 grow overflow-hidden text-xs text-neutral-content"
>
{#if loading}
<div class="skeleton h-5 w-60 flex-none pt-1"></div>
<div class="skeleton mb-1 mt-3 h-3 w-40 flex-none"></div>
{:else}
<div class="text-sm text-base-content">
{short_title}
</div>
<!-- <div class="text-xs text-neutral-content">
{description} {description}
</div> --> </div> -->
<ul class="pt-2"> <ul class="pt-2">
{#if comments > 0} {#if comments > 0}
<li class="align-middle inline mr-3"> <li class="mr-3 inline align-middle">
<!-- http://icon-sets.iconify.design/octicon/comment-16/ --> <!-- http://icon-sets.iconify.design/octicon/comment-16/ -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3 pt-0 flex-none fill-base-content inline-block" class="inline-block h-3 w-3 flex-none fill-base-content pt-0"
viewBox="0 0 16 16" viewBox="0 0 16 16"
><path ><path
d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
/></svg /></svg
> >
{comments} {comments}
</li> </li>
{/if}
<li class="inline mr-3">
opened {created_at_ago}
</li>
<li class="inline">
{#if author.loading}
<div class="skeleton h-3 pb-2 w-20 inline-block"></div>
{:else}
{author_name}
{/if}
</li>
</ul>
{/if} {/if}
</a> <li class="mr-3 inline">
<!-- <div class="flex-none text-xs pt-0 hidden md:block"> opened {created_at_ago}
</li>
<li class="inline">
{#if author.loading}
<div class="skeleton inline-block h-3 w-20 pb-2"></div>
{:else}
{author_name}
{/if}
</li>
</ul>
{/if}
</a>
<!-- <div class="flex-none text-xs pt-0 hidden md:block">
<div class="align-middle"> <div class="align-middle">
{#if loading} {#if loading}
<div class="skeleton w-10 h-10"></div> <div class="skeleton w-10 h-10"></div>

32
src/lib/components/prs/Status.stories.svelte

@ -1,27 +1,27 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { Meta } from "@storybook/svelte"; import type { Meta } from '@storybook/svelte'
import Status from "./Status.svelte"; import Status from './Status.svelte'
import { Story, Template } from "@storybook/addon-svelte-csf"; import { Story, Template } from '@storybook/addon-svelte-csf'
export const meta: Meta<Status> = { export const meta: Meta<Status> = {
title: "PRs/Status", title: 'PRs/Status',
component: Status, component: Status,
tags: ["autodocs"], tags: ['autodocs'],
}; }
</script> </script>
<Template let:args> <Template let:args>
<Status {...args} /> <Status {...args} />
</Template> </Template>
<Story name="Open" args={{ status: "Open"}} /> <Story name="Open" args={{ status: 'Open' }} />
<Story name="Closed" args={{ status: "Closed"}} /> <Story name="Closed" args={{ status: 'Closed' }} />
<Story name="Draft" args={{ status: "Draft"}} /> <Story name="Draft" args={{ status: 'Draft' }} />
<Story name="Merged" args={{ status: "Merged"}} /> <Story name="Merged" args={{ status: 'Merged' }} />
<Story name="Open Edit Mode" args={{ edit_mode: true, status: "Open"}} /> <Story name="Open Edit Mode" args={{ edit_mode: true, status: 'Open' }} />
<Story name="Loading" args={{ status: undefined}} /> <Story name="Loading" args={{ status: undefined }} />

128
src/lib/components/prs/Status.svelte

@ -1,71 +1,71 @@
<script lang="ts"> <script lang="ts">
import { pr_icon_path } from "./icons"; import { pr_icon_path } from './icons'
export let status: string | undefined = undefined; export let status: string | undefined = undefined
export let edit_mode = false; export let edit_mode = false
</script> </script>
{#if !status} {#if !status}
<div class="skeleton w-24 h-8 rounded-md inline-block align-middle"></div> <div class="skeleton inline-block h-8 w-24 rounded-md align-middle"></div>
{:else} {:else}
<div <div
tabIndex={0} tabIndex={0}
role="button" role="button"
class:btn-success={status && status === "Open"} class:btn-success={status && status === 'Open'}
class:btn-primary={status && status === "Merged"} class:btn-primary={status && status === 'Merged'}
class:btn-neutral={!status || status === "Draft" || status === "Closed"} class:btn-neutral={!status || status === 'Draft' || status === 'Closed'}
class:cursor-default={!edit_mode} class:cursor-default={!edit_mode}
class="btn btn-success btn-sm align-middle" class="btn btn-success btn-sm align-middle"
> >
{#if status === "Open"} {#if status === 'Open'}
<!-- http://icon-sets.iconify.design/octicon/git-pull-request-16/ --> <!-- http://icon-sets.iconify.design/octicon/git-pull-request-16/ -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 18 18" viewBox="0 0 18 18"
class="h-5 w-5 pt-1 flex-none fill-success-content" class="h-5 w-5 flex-none fill-success-content pt-1"
><path d={pr_icon_path.open} /> ><path d={pr_icon_path.open} />
</svg> </svg>
Open Open
{:else if status === "Merged"} {:else if status === 'Merged'}
<!-- https://icon-sets.iconify.design/octicon/git-merge-16/ --> <!-- https://icon-sets.iconify.design/octicon/git-merge-16/ -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
class="h-5 w-5 pt-1 flex-none fill-primary-content" class="h-5 w-5 flex-none fill-primary-content pt-1"
><path d={pr_icon_path.merge} /></svg ><path d={pr_icon_path.merge} /></svg
> >
Merged Merged
{:else if status === "Closed"} {:else if status === 'Closed'}
<!-- https://icon-sets.iconify.design/octicon/git-pull-request-closed-16/ --> <!-- https://icon-sets.iconify.design/octicon/git-pull-request-closed-16/ -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
class="h-5 w-5 pt-1 flex-none fill-neutral-content" class="h-5 w-5 flex-none fill-neutral-content pt-1"
><path d={pr_icon_path.close} /></svg ><path d={pr_icon_path.close} /></svg
> >
Closed Closed
{:else if status === "Draft"} {:else if status === 'Draft'}
<!-- https://icon-sets.iconify.design/octicon/git-pull-request-draft-16// --> <!-- https://icon-sets.iconify.design/octicon/git-pull-request-draft-16// -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
class="h-5 w-5 pt-1 flex-none fill-neutral-content" class="h-5 w-5 flex-none fill-neutral-content pt-1"
><path d={pr_icon_path.draft} /></svg ><path d={pr_icon_path.draft} /></svg
> >
Draft Draft
{:else} {:else}
{status} {status}
{/if} {/if}
{#if edit_mode} {#if edit_mode}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="h-5 w-5 flex-none fill-success-content" class="h-5 w-5 flex-none fill-success-content"
><path ><path
fill="currentColor" fill="currentColor"
d="M11.646 15.146L5.854 9.354a.5.5 0 0 1 .353-.854h11.586a.5.5 0 0 1 .353.854l-5.793 5.792a.5.5 0 0 1-.707 0" d="M11.646 15.146L5.854 9.354a.5.5 0 0 1 .353-.854h11.586a.5.5 0 0 1 .353.854l-5.793 5.792a.5.5 0 0 1-.707 0"
/></svg /></svg
> >
{/if} {/if}
</div> </div>
{/if} {/if}

220
src/lib/components/prs/StatusSelector.svelte

@ -1,126 +1,120 @@
<script lang="ts"> <script lang="ts">
import { ndk } from "$lib/stores/ndk"; import { ndk } from '$lib/stores/ndk'
import { NDKEvent, NDKRelaySet, type NDKTag } from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKRelaySet } from '@nostr-dev-kit/ndk'
import type { PRStatus } from "./type"; import type { PRStatus } from './type'
import { selected_pr_full, selected_pr_replies } from "$lib/stores/PR"; import { selected_pr_full } from '$lib/stores/PR'
import { pr_status_kind } from "$lib/kinds"; import { pr_status_kind } from '$lib/kinds'
import { getUserRelays, logged_in_user } from "$lib/stores/users"; import { getUserRelays, logged_in_user } from '$lib/stores/users'
import { selected_repo } from "$lib/stores/repo"; import { selected_repo } from '$lib/stores/repo'
import Status from "$lib/components/prs/Status.svelte"; import Status from '$lib/components/prs/Status.svelte'
export let status: PRStatus | undefined = undefined; export let status: PRStatus | undefined = undefined
export let repo_id: string = ""; export let repo_id: string = ''
export let pr_id: string = ""; export let pr_id: string = ''
let loading = false; let loading = false
let edit_mode = false; let edit_mode = false
$: { $: {
edit_mode = edit_mode =
$logged_in_user !== undefined && repo_id === $selected_repo.repo_id; $logged_in_user !== undefined && repo_id === $selected_repo.repo_id
} }
async function changeStatus(new_status: PRStatus) { async function changeStatus(new_status: PRStatus) {
if (!$logged_in_user) return; if (!$logged_in_user) return
let event = new NDKEvent(ndk); let event = new NDKEvent(ndk)
event.kind = pr_status_kind; event.kind = pr_status_kind
event.tags.push(["l", new_status]); event.tags.push(['l', new_status])
event.tags.push(["e", pr_id, "root"]); event.tags.push(['e', pr_id, 'root'])
event.tags.push(["r", `r-${repo_id}`]); event.tags.push(['r', `r-${repo_id}`])
loading = true; loading = true
let relays = [...$selected_repo.relays]; let relays = [...$selected_repo.relays]
try { try {
event.sign(); event.sign()
} catch { } catch {
alert("failed to sign event"); alert('failed to sign event')
}
try {
let user_relays = await getUserRelays($logged_in_user.hexpubkey);
relays = [
...relays,
...(user_relays.ndk_relays
? user_relays.ndk_relays.writeRelayUrls
: []),
// TODO: pr event pubkey relays
];
} catch {
alert("failed to get user relays");
}
try {
let res = await event.publish(
NDKRelaySet.fromRelayUrls(relays, ndk),
);
selected_pr_full.update((pr_full) => {
if (pr_full.summary.id !== pr_id) return pr_full;
return {
...pr_full,
summary: {
...pr_full.summary,
status: new_status,
status_date: event.created_at || 0,
},
};
});
loading = false;
} catch {}
} }
try {
let user_relays = await getUserRelays($logged_in_user.hexpubkey)
relays = [
...relays,
...(user_relays.ndk_relays
? user_relays.ndk_relays.writeRelayUrls
: []),
// TODO: pr event pubkey relays
]
} catch {
alert('failed to get user relays')
}
try {
let _ = await event.publish(NDKRelaySet.fromRelayUrls(relays, ndk))
selected_pr_full.update((pr_full) => {
if (pr_full.summary.id !== pr_id) return pr_full
return {
...pr_full,
summary: {
...pr_full.summary,
status: new_status,
status_date: event.created_at || 0,
},
}
})
loading = false
} catch {}
}
</script> </script>
{#if loading || !status} {#if loading || !status}
<Status /> <Status />
{:else} {:else}
<div class="dropdown"> <div class="dropdown">
<Status {edit_mode} {status} /> <Status {edit_mode} {status} />
{#if edit_mode} {#if edit_mode}
<ul <ul
tabIndex={0} tabIndex={0}
class="dropdown-content z-[1] menu p-2 ml-0 shadow bg-base-300 rounded-box w-52" class="menu dropdown-content z-[1] ml-0 w-52 rounded-box bg-base-300 p-2 shadow"
>
{#if status !== 'Draft'}
<li class="pl-0">
<button
on:click={() => {
changeStatus('Draft')
}}
class="btn btn-neutral btn-sm mx-2 align-middle">Draft</button
>
</li>
{/if}
{#if status !== 'Open'}
<li class="pl-0">
<button
on:click={() => {
changeStatus('Open')
}}
class="btn btn-success btn-sm mx-2 align-middle">Open</button
>
</li>
{/if}
{#if status !== 'Merged'}
<li class="pl-0">
<button
on:click={() => {
changeStatus('Merged')
}}
class="btn btn-primary btn-sm mx-2 align-middle">Merged</button
>
</li>
{/if}
{#if status !== 'Closed'}
<li class="pl-0">
<button
on:click={() => {
changeStatus('Closed')
}}
class="btn btn-neutral btn-sm mx-2 align-middle">Closed</button
> >
{#if status !== "Draft"} </li>
<li class="pl-0">
<button
on:click={() => {
changeStatus("Draft");
}}
class="btn btn-neutral btn-sm mx-2 align-middle"
>Draft</button
>
</li>
{/if}
{#if status !== "Open"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Open");
}}
class="btn btn-success btn-sm mx-2 align-middle"
>Open</button
>
</li>
{/if}
{#if status !== "Merged"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Merged");
}}
class="btn btn-primary btn-sm mx-2 align-middle"
>Merged</button
>
</li>
{/if}
{#if status !== "Closed"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Closed");
}}
class="btn btn-neutral btn-sm mx-2 align-middle"
>Closed</button
>
</li>
{/if}
</ul>
{/if} {/if}
</div> </ul>
{/if}
</div>
{/if} {/if}

15
src/lib/components/prs/icons.ts

@ -1,6 +1,9 @@
export let pr_icon_path = { export const pr_icon_path = {
open: "M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25m5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354M3.75 2.5a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m0 9.5a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m8.25.75a.75.75 0 1 0 1.5 0a.75.75 0 0 0-1.5 0", open: 'M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25m5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354M3.75 2.5a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m0 9.5a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m8.25.75a.75.75 0 1 0 1.5 0a.75.75 0 0 0-1.5 0',
close: "M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1m9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75m-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97l.97-.97a.748.748 0 0 1 1.265.332a.75.75 0 0 1-.205.729l-.97.97l.97.97a.751.751 0 0 1-.018 1.042a.751.751 0 0 1-1.042.018l-.97-.97l-.97.97a.749.749 0 0 1-1.275-.326a.749.749 0 0 1 .215-.734l.97-.97l-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0a.75.75 0 0 0-1.5 0M3.25 12a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m9.5 0a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5", close:
draft: "M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1m9.5 14a2.25 2.25 0 1 1 0-4.5a2.25 2.25 0 0 1 0 4.5M2.5 3.25a.75.75 0 1 0 1.5 0a.75.75 0 0 0-1.5 0M3.25 12a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m9.5 0a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5M14 7.5a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0m0-4.25a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0", 'M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1m9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75m-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97l.97-.97a.748.748 0 0 1 1.265.332a.75.75 0 0 1-.205.729l-.97.97l.97.97a.751.751 0 0 1-.018 1.042a.751.751 0 0 1-1.042.018l-.97-.97l-.97.97a.749.749 0 0 1-1.275-.326a.749.749 0 0 1 .215-.734l.97-.97l-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0a.75.75 0 0 0-1.5 0M3.25 12a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m9.5 0a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5',
merge: "M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218M4.25 13.5a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5m8.5-4.5a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5M5 3.25a.75.75 0 1 0 0 .005z", draft:
}; 'M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1m9.5 14a2.25 2.25 0 1 1 0-4.5a2.25 2.25 0 0 1 0 4.5M2.5 3.25a.75.75 0 1 0 1.5 0a.75.75 0 0 0-1.5 0M3.25 12a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m9.5 0a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5M14 7.5a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0m0-4.25a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0',
merge:
'M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218M4.25 13.5a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5m8.5-4.5a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5M5 3.25a.75.75 0 1 0 0 .005z',
}

107
src/lib/components/prs/type.ts

@ -1,70 +1,71 @@
import type { User } from "../users/type"; import type { User } from '../users/type'
import { defaults as user_defaults } from "../users/type"; import { defaults as user_defaults } from '../users/type'
import type { Event } from "../events/type"; import type { Event } from '../events/type'
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from '@nostr-dev-kit/ndk'
export interface PRSummary { export interface PRSummary {
title: string; title: string
descritpion: string; descritpion: string
repo_id: string; repo_id: string
id: string; id: string
comments: number; comments: number
status: undefined | PRStatus, status: undefined | PRStatus
status_date: number, status_date: number
author: User; author: User
created_at: number | undefined; created_at: number | undefined
loading: boolean; loading: boolean
} }
export const summary_defaults: PRSummary = { export const summary_defaults: PRSummary = {
title: "", title: '',
descritpion: "", descritpion: '',
repo_id: "", repo_id: '',
id: "", id: '',
comments: 0, comments: 0,
status: undefined, status: undefined,
status_date: 0, status_date: 0,
author: { ...user_defaults }, author: { ...user_defaults },
created_at: 0, created_at: 0,
loading: true, loading: true,
}; }
export interface PRSummaries { export interface PRSummaries {
id: string; id: string
summaries: PRSummary[]; summaries: PRSummary[]
loading: boolean; loading: boolean
} }
export const summaries_defaults: PRSummaries = { export const summaries_defaults: PRSummaries = {
id: "", id: '',
summaries: [], summaries: [],
loading: true, loading: true,
}; }
export type PRStatus = "Draft" | "Open" | "Merged" | "Closed"; export type PRStatus = 'Draft' | 'Open' | 'Merged' | 'Closed'
export function isPRStatus(potential_status: string | undefined): potential_status is PRStatus { export function isPRStatus(
return !!potential_status potential_status: string | undefined
&& ( ): potential_status is PRStatus {
potential_status == "Draft" return (
|| potential_status == "Open" !!potential_status &&
|| potential_status == "Merged" (potential_status == 'Draft' ||
|| potential_status == "Closed" potential_status == 'Open' ||
) potential_status == 'Merged' ||
potential_status == 'Closed')
)
} }
export interface PRFull { export interface PRFull {
summary: PRSummary; summary: PRSummary
pr_event: NDKEvent | undefined; pr_event: NDKEvent | undefined
labels: string[]; labels: string[]
events: Event[]; events: Event[]
loading: boolean; loading: boolean
} }
export const full_defaults: PRFull = { export const full_defaults: PRFull = {
summary: { ...summary_defaults }, summary: { ...summary_defaults },
pr_event: undefined, pr_event: undefined,
labels: [], labels: [],
events: [], events: [],
loading: true, loading: true,
}; }

112
src/lib/components/prs/vectors.ts

@ -1,59 +1,61 @@
import dayjs from "dayjs"; import dayjs from 'dayjs'
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from 'dayjs/plugin/relativeTime'
import type { PRSummary } from "./type"; import type { PRSummary } from './type'
import { UserVectors } from "../users/vectors"; import { UserVectors } from '../users/vectors'
dayjs.extend(relativeTime); dayjs.extend(relativeTime)
let Short = { const Short = {
title: "short title", title: 'short title',
author: { ...UserVectors.default },
created_at: dayjs().subtract(7, 'days').unix(),
comments: 2,
status: 'Open',
loading: false,
} as PRSummary
export const PRsListItemArgsVectors = {
Short,
Long: {
title:
'rather long title that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on',
author: { ...UserVectors.default }, author: { ...UserVectors.default },
created_at: dayjs().subtract(7, 'days').unix(), created_at: dayjs().subtract(1, 'minute').unix(),
comments: 2, comments: 0,
status: "Open", status: 'Open',
loading: false, loading: false,
} as PRSummary; } as PRSummary,
LongNoSpaces: {
export let PRsListItemArgsVectors = { title:
Short, 'LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongName',
Long: { author: { ...UserVectors.default },
title: "rather long title that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on", created_at: dayjs().subtract(3, 'month').subtract(3, 'days').unix(),
author: { ...UserVectors.default }, comments: 1,
created_at: dayjs().subtract(1, 'minute').unix(), status: 'Open',
comments: 0, loading: false,
status: "Open", } as PRSummary,
loading: false, AuthorLoading: {
} as PRSummary, title: 'short title',
LongNoSpaces: { author: { ...UserVectors.loading },
title: "LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongName", created_at: dayjs().subtract(3, 'month').subtract(3, 'days').unix(),
author: { ...UserVectors.default }, comments: 1,
created_at: dayjs().subtract(3, 'month').subtract(3, 'days').unix(), status: 'Open',
comments: 1, loading: false,
status: "Open", } as PRSummary,
loading: false, StatusLoading: {
} as PRSummary, ...Short,
AuthorLoading: { status: undefined,
title: "short title", } as PRSummary,
author: { ...UserVectors.loading }, StatusDraft: {
created_at: dayjs().subtract(3, 'month').subtract(3, 'days').unix(), ...Short,
comments: 1, status: 'Draft',
status: "Open", } as PRSummary,
loading: false, StatusClosed: {
} as PRSummary, ...Short,
StatusLoading: { status: 'Closed',
...Short, } as PRSummary,
status: undefined, StatusMerged: {
} as PRSummary, ...Short,
StatusDraft: { status: 'Merged',
...Short, } as PRSummary,
status: "Draft", }
} as PRSummary,
StatusClosed: {
...Short,
status: "Closed",
} as PRSummary,
StatusMerged: {
...Short,
status: "Merged",
} as PRSummary,
};

34
src/lib/components/repo/RepoDetails.stories.svelte

@ -1,18 +1,18 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { Meta } from "@storybook/svelte"; import type { Meta } from '@storybook/svelte'
import RepoDetails from "./RepoDetails.svelte"; import RepoDetails from './RepoDetails.svelte'
import { Story, Template } from "@storybook/addon-svelte-csf"; import { Story, Template } from '@storybook/addon-svelte-csf'
import { RepoDetailsArgsVectors as vectors } from "./vectors"; import { RepoDetailsArgsVectors as vectors } from './vectors'
export const meta: Meta<RepoDetails> = { export const meta: Meta<RepoDetails> = {
title: "Repo/Details", title: 'Repo/Details',
component: RepoDetails, component: RepoDetails,
tags: ["autodocs"], tags: ['autodocs'],
}; }
</script> </script>
<Template let:args> <Template let:args>
<RepoDetails {...args} /> <RepoDetails {...args} />
</Template> </Template>
<Story name="Short Details" args={vectors.NoMaintainers} /> <Story name="Short Details" args={vectors.NoMaintainers} />
@ -32,18 +32,18 @@
<Story name="No Maintainers" args={vectors.NoMaintainers} /> <Story name="No Maintainers" args={vectors.NoMaintainers} />
<Story <Story
name="One Maintainer's Profile Not Loaded" name="One Maintainer's Profile Not Loaded"
args={vectors.MaintainersOneProfileNotLoaded} args={vectors.MaintainersOneProfileNotLoaded}
/> />
<Story <Story
name="One Maintainer's Profile Only Has displayName But No Name" name="One Maintainer's Profile Only Has displayName But No Name"
args={vectors.MaintainersOneProfileDisplayNameWithoutName} args={vectors.MaintainersOneProfileDisplayNameWithoutName}
/> />
<Story <Story
name="One Maintainer's Profile Has No displayName or Name" name="One Maintainer's Profile Has No displayName or Name"
args={vectors.MaintainersOneProfileNoNameOrDisplayNameBeingPresent} args={vectors.MaintainersOneProfileNoNameOrDisplayNameBeingPresent}
/> />
<Story name="No Relays" args={vectors.NoRelays} /> <Story name="No Relays" args={vectors.NoRelays} />

167
src/lib/components/repo/RepoDetails.svelte

@ -1,98 +1,85 @@
<script lang="ts" context="module">
import type { NDKUserProfile } from "@nostr-dev-kit/ndk";
</script>
<script lang="ts"> <script lang="ts">
import type { User } from "$lib/components/users/type"; import UserHeader from '$lib/components/users/UserHeader.svelte'
import UserHeader from "$lib/components/users/UserHeader.svelte"; import { defaults } from './type'
import { defaults } from "./type";
export let { export let {
repo_id, repo_id,
name, name,
description, description,
git_server, git_server,
tags, tags,
maintainers, maintainers,
relays, relays,
loading, loading,
} = defaults; } = defaults
let short_name: string; $: short_descrption =
$: { description.length > 500 ? description.slice(0, 450) + '...' : description
if (name.length > 45) short_name = name.slice(0, 45) + "...";
else if (name.length == 0) short_name = "Untitled";
else short_name = name;
}
$: short_descrption =
description.length > 500
? description.slice(0, 450) + "..."
: description;
</script> </script>
<div class="max-w-md prose w-full"> <div class="prose w-full max-w-md">
{#if loading}
<div class="skeleton my-3 h-5 w-20"></div>
<div class="skeleton my-2 h-4"></div>
<div class="skeleton my-2 mb-3 h-4 w-2/3"></div>
{:else if description.length == 0}
<div />
{:else}
<h4>description</h4>
<p class="my-2 break-words text-sm">{short_descrption}</p>
{/if}
<div>
{#if loading}
<div class="badge skeleton w-20"></div>
<div class="badge skeleton w-20"></div>
{:else}
{#each tags as tag}
<div class="badge badge-secondary mr-2">{tag}</div>
{/each}
{/if}
</div>
<div>
{#if loading}
<div class="skeleton my-3 h-5 w-20"></div>
<div class="badge skeleton my-2 block w-60"></div>
{:else if git_server.length == 0}
<div />
{:else}
<h4>git server</h4>
<a
href={git_server}
target="_blank"
class="link link-primary my-2 break-words"
>
{git_server}
</a>
{/if}
</div>
<div>
{#if loading}
<div class="skeleton my-3 h-5 w-20"></div>
<div class="badge skeleton my-2 block w-60"></div>
<div class="badge skeleton my-2 block w-40"></div>
{:else if maintainers.length == 0}
<div />
{:else}
<h4>maintainers</h4>
{#each maintainers as maintainer}
<UserHeader user={maintainer} />
{/each}
{/if}
</div>
<div>
{#if loading} {#if loading}
<div class="h-5 my-3 skeleton w-20"></div> <div class="skeleton my-3 h-5 w-20"></div>
<div class="h-4 my-2 skeleton"></div> <div class="badge skeleton my-2 block w-60"></div>
<div class="h-4 my-2 mb-3 w-2/3 skeleton"></div> <div class="badge skeleton my-2 block w-40"></div>
{:else if description.length == 0} {:else if relays.length == 0}
<div /> <div />
{:else} {:else}
<h4>description</h4> <h4>relays</h4>
<p class="text-sm my-2 break-words">{short_descrption}</p> {#each relays as relay}
<div class="badge badge-secondary my-2 block">{relay}</div>
{/each}
{/if} {/if}
<div> </div>
{#if loading}
<div class="badge skeleton w-20"></div>
<div class="badge skeleton w-20"></div>
{:else}
{#each tags as tag}
<div class="badge badge-secondary mr-2">{tag}</div>
{/each}
{/if}
</div>
<div>
{#if loading}
<div class="h-5 my-3 skeleton w-20"></div>
<div class="badge skeleton my-2 w-60 block"></div>
{:else if git_server.length == 0}
<div />
{:else}
<h4>git server</h4>
<a
href={git_server}
target="_blank"
class="link link-primary break-words my-2"
>
{git_server}
</a>
{/if}
</div>
<div>
{#if loading}
<div class="h-5 my-3 skeleton w-20"></div>
<div class="badge skeleton my-2 w-60 block"></div>
<div class="badge skeleton my-2 w-40 block"></div>
{:else if maintainers.length == 0}
<div />
{:else}
<h4>maintainers</h4>
{#each maintainers as maintainer}
<UserHeader user={maintainer} />
{/each}
{/if}
</div>
<div>
{#if loading}
<div class="h-5 my-3 skeleton w-20"></div>
<div class="badge skeleton my-2 w-60 block"></div>
<div class="badge skeleton my-2 w-40 block"></div>
{:else if relays.length == 0}
<div />
{:else}
<h4>relays</h4>
{#each relays as relay}
<div class="badge badge-secondary block my-2">{relay}</div>
{/each}
{/if}
</div>
</div> </div>

20
src/lib/components/repo/RepoHeader.stories.svelte

@ -1,18 +1,18 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { Meta } from "@storybook/svelte"; import type { Meta } from '@storybook/svelte'
import RepoHeader from "./RepoHeader.svelte"; import RepoHeader from './RepoHeader.svelte'
import { Story, Template } from "@storybook/addon-svelte-csf"; import { Story, Template } from '@storybook/addon-svelte-csf'
import { RepoDetailsArgsVectors as vectors } from "./vectors"; import { RepoDetailsArgsVectors as vectors } from './vectors'
export const meta: Meta<RepoHeader> = { export const meta: Meta<RepoHeader> = {
title: "Repo/Header", title: 'Repo/Header',
component: RepoHeader, component: RepoHeader,
tags: ["autodocs"], tags: ['autodocs'],
}; }
</script> </script>
<Template let:args> <Template let:args>
<RepoHeader {...args} /> <RepoHeader {...args} />
</Template> </Template>
<Story name="Short Name" args={vectors.NoMaintainers} /> <Story name="Short Name" args={vectors.NoMaintainers} />

70
src/lib/components/repo/RepoHeader.svelte

@ -1,43 +1,37 @@
<script lang="ts" context="module">
import type { NDKUserProfile } from "@nostr-dev-kit/ndk";
</script>
<script lang="ts"> <script lang="ts">
import type { User } from "$lib/components/users/type"; import Container from '../Container.svelte'
import UserHeader from "$lib/components/users/UserHeader.svelte"; import { defaults } from './type'
import Container from "../Container.svelte";
import { defaults } from "./type";
export let { export let {
repo_id, repo_id,
name, name,
description, description,
git_server, git_server,
tags, tags,
maintainers, maintainers,
relays, relays,
loading, loading,
} = defaults; } = defaults
let short_name: string; let short_name: string
$: { $: {
if (name.length > 45) short_name = name.slice(0, 45) + "..."; if (name.length > 45) short_name = name.slice(0, 45) + '...'
else if (name.length == 0) short_name = "Untitled"; else if (name.length == 0) short_name = 'Untitled'
else short_name = name; else short_name = name
} }
</script> </script>
<div class="bg-base-300 border-b border-accent-content"> <div class="border-b border-accent-content bg-base-300">
<Container no_wrap={true}> <Container no_wrap={true}>
{#if loading} {#if loading}
<div class="p-3"> <div class="p-3">
<div class="h-6 skeleton w-28 bg-base-200"></div> <div class="skeleton h-6 w-28 bg-base-200"></div>
</div> </div>
{:else} {:else}
<a <a
href={`/repo/${repo_id}`} href={`/repo/${repo_id}`}
class="btn btn-ghost text-sm break-words strong mt-0 mb-0 px-3" class="strong btn btn-ghost mb-0 mt-0 break-words px-3 text-sm"
>{short_name}</a >{short_name}</a
> >
{/if} {/if}
</Container> </Container>
</div> </div>

36
src/lib/components/repo/type.ts

@ -1,22 +1,22 @@
import type { User } from "../users/type"; import type { User } from '../users/type'
export interface Repo { export interface Repo {
repo_id: string; repo_id: string
name: string; name: string
description: string; description: string
git_server: string; git_server: string
tags: string[]; tags: string[]
maintainers: User[]; maintainers: User[]
relays: string[]; relays: string[]
loading: boolean; loading: boolean
} }
export const defaults: Repo = { export const defaults: Repo = {
repo_id: "", repo_id: '',
name: "", name: '',
description: "", description: '',
git_server: "", git_server: '',
tags: [], tags: [],
maintainers: [], maintainers: [],
relays: [], relays: [],
loading: true, loading: true,
}; }

173
src/lib/components/repo/vectors.ts

@ -1,92 +1,89 @@
import type { Args as SummaryCardArgs } from "../RepoSummaryCard.svelte"; import type { Args as SummaryCardArgs } from '../RepoSummaryCard.svelte'
import type { NDKUserProfile } from "@nostr-dev-kit/ndk"; import { UserVectors, withName } from '../users/vectors'
import type { User } from "../users/type"; import type { Repo } from './type'
import { UserVectors, withName } from "../users/vectors";
import type { Repo } from "./type";
export let RepoSummaryCardArgsVectors = { export const RepoSummaryCardArgsVectors = {
Short: { Short: {
name: "Short Name", name: 'Short Name',
description: "short description", description: 'short description',
} as SummaryCardArgs, } as SummaryCardArgs,
Long: { Long: {
name: "Long Name that goes on and on and on and on and on and on and on and on and on", name: 'Long Name that goes on and on and on and on and on and on and on and on and on',
description: description:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis nisl eget turpis congue molestie. Nulla vitae purus nec augue accumsan facilisis sed sed ligula. Vestibulum sed risus lacinia risus lacinia molestie. Ut lorem quam, consequat eget tempus in, rhoncus vel nunc. Duis efficitur a leo vel sodales. Nam id fermentum lacus. Etiam nec placerat velit. Praesent ac consectetur est. Aenean iaculis commodo enim.", 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis nisl eget turpis congue molestie. Nulla vitae purus nec augue accumsan facilisis sed sed ligula. Vestibulum sed risus lacinia risus lacinia molestie. Ut lorem quam, consequat eget tempus in, rhoncus vel nunc. Duis efficitur a leo vel sodales. Nam id fermentum lacus. Etiam nec placerat velit. Praesent ac consectetur est. Aenean iaculis commodo enim.',
} as SummaryCardArgs, } as SummaryCardArgs,
LongNoSpaces: { LongNoSpaces: {
name: "LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongName", name: 'LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongName',
description: description:
"LoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsum>", 'LoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsum>',
} as SummaryCardArgs, } as SummaryCardArgs,
}; }
let base: Repo = { const base: Repo = {
repo_id: "9ee507fc4357d7ee16a5d8901bedcd103f23c17d", repo_id: '9ee507fc4357d7ee16a5d8901bedcd103f23c17d',
name: "Short Name", name: 'Short Name',
description: "short description", description: 'short description',
git_server: "github.com/example/example", git_server: 'github.com/example/example',
tags: ["svelte", "nostr", "code-collaboration", "git"], tags: ['svelte', 'nostr', 'code-collaboration', 'git'],
relays: [ relays: ['relay.damus.io', 'relay.snort.social', 'relayable.org'],
"relay.damus.io", maintainers: [
"relay.snort.social", withName(UserVectors.default, 'carole'),
"relayable.org", withName(UserVectors.default, 'bob'),
withName(UserVectors.default, 'steve'),
],
loading: false,
}
export const RepoDetailsArgsVectors = {
Short: { ...base } as Repo,
Long: {
...base,
name: 'Long Name that goes on and on and on and on and on and on and on and on and on',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis nisl eget turpis congue molestie. Nulla vitae purus nec augue accumsan facilisis sed sed ligula. Vestibulum sed risus lacinia risus lacinia molestie. Ut lorem quam, consequat eget tempus in, rhoncus vel nunc. Duis efficitur a leo vel sodales. Nam id fermentum lacus. Etiam nec placerat velit. Praesent ac consectetur est. Aenean iaculis commodo enim.\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis nisl eget turpis congue molestie.',
} as Repo,
LongNoSpaces: {
...base,
name: 'LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongName',
description:
'LoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsum',
} as Repo,
NoNameOrDescription: { ...base, name: '', description: '' } as Repo,
NoDescription: { ...base, description: '' } as Repo,
NoTags: { ...base, tags: [] } as Repo,
NoGitServer: { ...base, git_server: '' } as Repo,
MaintainersOneProfileNotLoaded: {
...base,
maintainers: [
{ ...base.maintainers[0] },
{ ...UserVectors.loading },
{ ...base.maintainers[2] },
], ],
} as Repo,
MaintainersOneProfileDisplayNameWithoutName: {
...base,
maintainers: [ maintainers: [
withName(UserVectors.default, "carole"), { ...base.maintainers[0] },
withName(UserVectors.default, "bob"), { ...UserVectors.display_name_only },
withName(UserVectors.default, "steve"), { ...base.maintainers[2] },
], ],
loading: false, } as Repo,
}; MaintainersOneProfileNameAndDisplayNamePresent: {
...base,
export let RepoDetailsArgsVectors = { maintainers: [
Short: { ...base, } as Repo, { ...base.maintainers[0] },
Long: { { ...UserVectors.display_name_and_name },
...base, { ...base.maintainers[2] },
name: "Long Name that goes on and on and on and on and on and on and on and on and on", ],
description: } as Repo,
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis nisl eget turpis congue molestie. Nulla vitae purus nec augue accumsan facilisis sed sed ligula. Vestibulum sed risus lacinia risus lacinia molestie. Ut lorem quam, consequat eget tempus in, rhoncus vel nunc. Duis efficitur a leo vel sodales. Nam id fermentum lacus. Etiam nec placerat velit. Praesent ac consectetur est. Aenean iaculis commodo enim.\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis nisl eget turpis congue molestie.", MaintainersOneProfileNoNameOrDisplayNameBeingPresent: {
} as Repo, ...base,
LongNoSpaces: { maintainers: [
...base, { ...base.maintainers[0] },
name: "LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongName", { ...UserVectors.no_profile },
description: { ...base.maintainers[2] },
"LoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsum", ],
} as Repo, } as Repo,
NoNameOrDescription: { ...base, name: "", description: "" } as Repo, NoMaintainers: { ...base, maintainers: [] } as Repo,
NoDescription: { ...base, description: "" } as Repo, NoRelays: { ...base, relays: [] } as Repo,
NoTags: { ...base, tags: [] } as Repo, NoMaintainersOrRelays: { ...base, maintainers: [], relays: [] } as Repo,
NoGitServer: { ...base, git_server: "" } as Repo, }
MaintainersOneProfileNotLoaded: {
...base, maintainers: [
{ ...base.maintainers[0] },
{ ...UserVectors.loading },
{ ...base.maintainers[2] },
]
} as Repo,
MaintainersOneProfileDisplayNameWithoutName: {
...base, maintainers: [
{ ...base.maintainers[0] },
{ ...UserVectors.display_name_only },
{ ...base.maintainers[2] },
]
} as Repo,
MaintainersOneProfileNameAndDisplayNamePresent: {
...base, maintainers: [
{ ...base.maintainers[0] },
{ ...UserVectors.display_name_and_name },
{ ...base.maintainers[2] },
]
} as Repo,
MaintainersOneProfileNoNameOrDisplayNameBeingPresent: {
...base, maintainers: [
{ ...base.maintainers[0] },
{ ...UserVectors.no_profile },
{ ...base.maintainers[2] },
]
} as Repo,
NoMaintainers: { ...base, maintainers: [] } as Repo,
NoRelays: { ...base, relays: [] } as Repo,
NoMaintainersOrRelays: { ...base, maintainers: [], relays: [] } as Repo,
};

59
src/lib/components/users/UserHeader.stories.svelte

@ -1,41 +1,36 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { Meta } from "@storybook/svelte"; import type { Meta } from '@storybook/svelte'
import UserHeader from "$lib/components/users/UserHeader.svelte"; import UserHeader from '$lib/components/users/UserHeader.svelte'
import { Story, Template } from "@storybook/addon-svelte-csf"; import { Story, Template } from '@storybook/addon-svelte-csf'
import { UserVectors as vectors } from "./vectors"; import { UserVectors as vectors } from './vectors'
export const meta: Meta<UserHeader> = { export const meta: Meta<UserHeader> = {
title: "Users/Header", title: 'Users/Header',
component: UserHeader, component: UserHeader,
tags: ["autodocs"], tags: ['autodocs'],
}; }
let base = {
hexpubkey:
"3eb45c6f15752d796fa5465d0530a5a5feb79fb6f08c0a4176be9d73cc28c40d",
npub: "npub18669cmc4w5khjma9gews2v995hlt08ak7zxq5stkh6wh8npgcsxslt2xjn",
};
</script> </script>
<Template let:args> <Template let:args>
<UserHeader {...args} /> <UserHeader {...args} />
</Template> </Template>
<Story <Story
name="default" name="default"
args={{ args={{
user: { user: {
...vectors.default, ...vectors.default,
}, },
}} }}
/> />
<Story <Story
name="no image" name="no image"
args={{ args={{
user: { user: {
...vectors.no_image, ...vectors.no_image,
}, },
}} }}
/> />
<Story name="loading" args={{ user: { ...vectors.loading } }} /> <Story name="loading" args={{ user: { ...vectors.loading } }} />
@ -43,11 +38,11 @@
<Story name="not found" args={{ user: { ...vectors.no_profile } }} /> <Story name="not found" args={{ user: { ...vectors.no_profile } }} />
<Story <Story
name="displayName without name" name="displayName without name"
args={{ user: { ...vectors.display_name_only } }} args={{ user: { ...vectors.display_name_only } }}
/> />
<Story <Story
name="name and displayName shows name" name="name and displayName shows name"
args={{ user: { ...vectors.display_name_and_name } }} args={{ user: { ...vectors.display_name_and_name } }}
/> />

53
src/lib/components/users/UserHeader.svelte

@ -1,32 +1,33 @@
<script lang="ts"> <script lang="ts">
import { getName, type User } from "./type"; import { getName, type User } from './type'
export let user: User = { export let user: User = {
hexpubkey: "", hexpubkey: '',
npub: "", npub: '',
loading: true, loading: true,
}; }
$: ({ profile, loading } = user);
$: display_name = getName(user); $: ({ profile, loading } = user)
$: display_name = getName(user)
</script> </script>
<div class="flex my-2"> <div class="my-2 flex">
<div class="avatar flex-none"> <div class="avatar flex-none">
<div <div
class="w-8 h-8 rounded" class="h-8 w-8 rounded"
class:skeleton={!profile && loading} class:skeleton={!profile && loading}
class:bg-neutral={!loading && (!profile || !profile.image)} class:bg-neutral={!loading && (!profile || !profile.image)}
> >
{#if !!profile?.image} {#if profile && profile?.image}
<img class="my-0" src={profile?.image} alt={display_name} /> <img class="my-0" src={profile?.image} alt={display_name} />
{/if} {/if}
</div>
</div>
<div class="flex-auto pl-3 m-auto">
{#if loading}
<div class="w-24 h-4 skeleton"></div>
{:else}
{display_name}
{/if}
</div> </div>
</div>
<div class="m-auto flex-auto pl-3">
{#if loading}
<div class="skeleton h-4 w-24"></div>
{:else}
{display_name}
{/if}
</div>
</div> </div>

39
src/lib/components/users/type.ts

@ -1,29 +1,28 @@
import type { NDKUserProfile } from "@nostr-dev-kit/ndk"; import type { NDKUserProfile } from '@nostr-dev-kit/ndk'
export interface User { export interface User {
loading: boolean; loading: boolean
hexpubkey: string; hexpubkey: string
npub: string; npub: string
profile?: NDKUserProfile; profile?: NDKUserProfile
} }
export let defaults: User = { export const defaults: User = {
loading: true, loading: true,
hexpubkey: "", hexpubkey: '',
npub: "", npub: '',
} }
export function getName(user: User, fallback_to_pubkey: boolean = false): string { export function getName(user: User): string {
return user.profile ? ( return user.profile
user.profile.name ? user.profile.name
? user.profile.name ? user.profile.name
: user.profile.displayName : user.profile.displayName
? user.profile.displayName ? user.profile.displayName
: truncateNpub(user.npub) : truncateNpub(user.npub)
) : truncateNpub(user.npub)
: truncateNpub(user.npub);
} }
function truncateNpub(npub: string): string { function truncateNpub(npub: string): string {
return `${npub.substring(0, 9)}...`; return `${npub.substring(0, 9)}...`
} }

51
src/lib/components/users/vectors.ts

@ -1,30 +1,35 @@
import type { User } from "./type"; import type { User } from './type'
// nsec1rg53qfv09az39dlw6j64ange3cx8sh5p8np29qcxtythplvplktsv93tnr // nsec1rg53qfv09az39dlw6j64ange3cx8sh5p8np29qcxtythplvplktsv93tnr
let base: User = { const base: User = {
hexpubkey: hexpubkey: '3eb45c6f15752d796fa5465d0530a5a5feb79fb6f08c0a4176be9d73cc28c40d',
"3eb45c6f15752d796fa5465d0530a5a5feb79fb6f08c0a4176be9d73cc28c40d", npub: 'npub18669cmc4w5khjma9gews2v995hlt08ak7zxq5stkh6wh8npgcsxslt2xjn',
npub: "npub18669cmc4w5khjma9gews2v995hlt08ak7zxq5stkh6wh8npgcsxslt2xjn", loading: false,
loading: false, }
};
let image = "../test-profile-image.jpg"; const image = '../test-profile-image.jpg'
export let UserVectors = { export const UserVectors = {
loading: { ...base, loading: true } as User, loading: { ...base, loading: true } as User,
default: { ...base, profile: { name: "DanConwayDev", image } } as User, default: { ...base, profile: { name: 'DanConwayDev', image } } as User,
display_name_only: { ...base, profile: { displayName: "DanConwayDev", image } } as User, display_name_only: {
display_name_and_name: { ...base, profile: { name: "Dan", displayName: "DanConwayDev", image } } as User, ...base,
no_image: { ...base, profile: { name: "DanConwayDev" } } as User, profile: { displayName: 'DanConwayDev', image },
no_profile: { ...base } as User, } as User,
}; display_name_and_name: {
...base,
profile: { name: 'Dan', displayName: 'DanConwayDev', image },
} as User,
no_image: { ...base, profile: { name: 'DanConwayDev' } } as User,
no_profile: { ...base } as User,
}
export function withName(base: User, name: string): User { export function withName(base: User, name: string): User {
return { return {
...base, ...base,
profile: { profile: {
...base.profile, ...base.profile,
name, name,
} },
} as User } as User
} }

10
src/lib/kinds.ts

@ -1,9 +1,9 @@
export let reply_kind = 1; export const reply_kind: number = 1
export let pr_status_kind = 19851985; export const pr_status_kind: number = 19851985
export let repo_kind = 30317; export const repo_kind: number = 30317
export let pr_kind = 318; export const pr_kind: number = 318
export let patch_kind = 317; export const patch_kind: number = 317

371
src/lib/stores/PR.ts

@ -1,184 +1,193 @@
import { NDKRelaySet, type NDKEvent, NDKSubscription } from "@nostr-dev-kit/ndk"; import { NDKRelaySet, type NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
import { writable, type Unsubscriber, type Writable } from "svelte/store" import { writable, type Unsubscriber, type Writable } from 'svelte/store'
import { ndk } from "./ndk"; import { ndk } from './ndk'
import type { User } from "$lib/components/users/type"; import type { User } from '$lib/components/users/type'
import { ensureUser } from "./users"; import { ensureUser } from './users'
import { type PRFull, full_defaults, isPRStatus, type PRStatus } from "$lib/components/prs/type"; import {
import { pr_kind, pr_status_kind } from "$lib/kinds"; type PRFull,
import { ensureSelectedRepo } from "./repo"; full_defaults,
isPRStatus,
export let selected_pr_full: Writable<PRFull> = writable({ ...full_defaults }); type PRStatus,
} from '$lib/components/prs/type'
let selected_pr_repo_id: string = ""; import { pr_kind, pr_status_kind } from '$lib/kinds'
let selected_pr_id: string = ""; import { ensureSelectedRepo } from './repo'
let pr_summary_author_unsubsriber: Unsubscriber | undefined;
export const selected_pr_full: Writable<PRFull> = writable({ ...full_defaults })
export let selected_pr_replies: Writable<NDKEvent[]> = writable([]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let selected_pr_status_date = 0; let selected_pr_repo_id: string = ''
let selected_pr_id: string = ''
let sub: NDKSubscription; let pr_summary_author_unsubsriber: Unsubscriber | undefined
let sub_replies: NDKSubscription; export const selected_pr_replies: Writable<NDKEvent[]> = writable([])
export let ensurePRFull = (repo_id: string, pr_id: string) => { let selected_pr_status_date = 0
if (selected_pr_id == pr_id) return;
if (pr_id == "") { let sub: NDKSubscription
selected_pr_full.set({ ...full_defaults });
selected_pr_replies.set([]); let sub_replies: NDKSubscription
return;
} export const ensurePRFull = (repo_id: string, pr_id: string) => {
if (selected_pr_id == pr_id) return
if (sub) sub.stop(); if (pr_id == '') {
if (sub_replies) sub_replies.stop(); selected_pr_full.set({ ...full_defaults })
selected_pr_replies.set([])
selected_pr_repo_id = repo_id; return
selected_pr_id = pr_id; }
selected_pr_status_date = 0;
selected_pr_replies.set([]); if (sub) sub.stop()
if (sub_replies) sub_replies.stop()
selected_pr_full.set({
...full_defaults, selected_pr_repo_id = repo_id
summary: { selected_pr_id = pr_id
...full_defaults.summary, selected_pr_status_date = 0
id: pr_id, selected_pr_replies.set([])
repo_id: repo_id,
loading: true, selected_pr_full.set({
}, ...full_defaults,
loading: true, summary: {
}); ...full_defaults.summary,
if (pr_summary_author_unsubsriber) pr_summary_author_unsubsriber(); id: pr_id,
pr_summary_author_unsubsriber = undefined; repo_id: repo_id,
loading: true,
new Promise(async (r) => { },
let repo = await ensureSelectedRepo(repo_id); loading: true,
})
sub = ndk.subscribe( if (pr_summary_author_unsubsriber) pr_summary_author_unsubsriber()
{ pr_summary_author_unsubsriber = undefined
ids: [pr_id],
kinds: [pr_kind], new Promise(async (r) => {
'#r': [`r-${repo_id}`], const repo = await ensureSelectedRepo(repo_id)
limit: 50,
}, sub = ndk.subscribe(
{ {
closeOnEose: false, ids: [pr_id],
}, kinds: [pr_kind],
NDKRelaySet.fromRelayUrls(repo.relays, ndk), '#r': [`r-${repo_id}`],
); limit: 50,
},
sub.on("event", (event: NDKEvent) => { {
try { closeOnEose: false,
if (event.kind == pr_kind },
&& event.getMatchingTags("r").find(t => t[1] === `r-${repo_id}`) NDKRelaySet.fromRelayUrls(repo.relays, ndk)
&& event.id == pr_id )
) {
selected_pr_full.update(full => { sub.on('event', (event: NDKEvent) => {
return { try {
...full, if (
pr_event: event, event.kind == pr_kind &&
summary: { event.getMatchingTags('r').find((t) => t[1] === `r-${repo_id}`) &&
...full.summary, event.id == pr_id
title: event.tagValue("name") || "", ) {
descritpion: event.tagValue("description") || "", selected_pr_full.update((full) => {
created_at: event.created_at, return {
comments: 0, ...full,
author: { pr_event: event,
hexpubkey: event.pubkey, summary: {
loading: true, ...full.summary,
npub: "", title: event.tagValue('name') || '',
}, descritpion: event.tagValue('description') || '',
loading: false, created_at: event.created_at,
} comments: 0,
}; author: {
}); hexpubkey: event.pubkey,
loading: true,
pr_summary_author_unsubsriber = ensureUser(event.pubkey).subscribe((u: User) => { npub: '',
selected_pr_full.update(full => { },
return { loading: false,
...full, },
summary: {
...full.summary,
author: event.pubkey == u.hexpubkey ? u : full.summary.author,
}
};
});
});
}
} catch { }
});
sub.on("eose", () => {
selected_pr_full.update(full => {
let updated = {
...full,
summary: {
...full.summary,
loading: false,
},
};
if (full.loading === false) {
r({ ...updated });
}
return updated;
});
});
sub_replies = ndk.subscribe(
{
"#e": [pr_id],
},
{
closeOnEose: false
},
NDKRelaySet.fromRelayUrls(repo.relays, ndk),
);
sub_replies.on("event", (event: NDKEvent) => {
if (event.kind == pr_status_kind
&& event.created_at && selected_pr_status_date < event.created_at
&& event.getMatchingTags("l").length === 1
&& event.getMatchingTags("l")[0].length > 1
) {
let potential_status = event.getMatchingTags("l")[0][1];
if (isPRStatus(potential_status)) {
selected_pr_status_date = event.created_at;
selected_pr_full.update(full => {
return {
...full,
summary: {
...full.summary,
status: potential_status as PRStatus,
// this wont be 0 as we are ensuring it is not undefined above
status_date: event.created_at || 0,
},
};
});
}
} }
selected_pr_replies.update(replies => { })
return [
...replies, pr_summary_author_unsubsriber = ensureUser(event.pubkey).subscribe(
event, (u: User) => {
]; selected_pr_full.update((full) => {
}); return {
}); ...full,
summary: {
sub_replies.on("eose", () => { ...full.summary,
selected_pr_full.update(full => { author:
let updated = { event.pubkey == u.hexpubkey ? u : full.summary.author,
...full, },
summary: {
...full.summary,
status: full.summary.status || "Open",
},
loading: false,
};
if (full.summary.loading === false) {
r({ ...updated });
} }
return updated; })
}); }
}); )
}); }
} catch {}
})
sub.on('eose', () => {
selected_pr_full.update((full) => {
const updated = {
...full,
summary: {
...full.summary,
loading: false,
},
}
if (full.loading === false) {
r({ ...updated })
}
return updated
})
})
sub_replies = ndk.subscribe(
{
'#e': [pr_id],
},
{
closeOnEose: false,
},
NDKRelaySet.fromRelayUrls(repo.relays, ndk)
)
sub_replies.on('event', (event: NDKEvent) => {
if (
event.kind == pr_status_kind &&
event.created_at &&
selected_pr_status_date < event.created_at &&
event.getMatchingTags('l').length === 1 &&
event.getMatchingTags('l')[0].length > 1
) {
const potential_status = event.getMatchingTags('l')[0][1]
if (isPRStatus(potential_status)) {
selected_pr_status_date = event.created_at
selected_pr_full.update((full) => {
return {
...full,
summary: {
...full.summary,
status: potential_status as PRStatus,
// this wont be 0 as we are ensuring it is not undefined above
status_date: event.created_at || 0,
},
}
})
}
}
selected_pr_replies.update((replies) => {
return [...replies, event]
})
})
sub_replies.on('eose', () => {
selected_pr_full.update((full) => {
const updated = {
...full,
summary: {
...full.summary,
status: full.summary.status || 'Open',
},
loading: false,
}
if (full.summary.loading === false) {
r({ ...updated })
}
return updated
})
})
})
} }

335
src/lib/stores/PRs.ts

@ -1,173 +1,174 @@
import { NDKRelaySet, type NDKEvent, NDKSubscription } from "@nostr-dev-kit/ndk"; import { NDKRelaySet, type NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
import { writable, type Unsubscriber, type Writable } from "svelte/store" import { writable, type Unsubscriber, type Writable } from 'svelte/store'
import { ndk } from "./ndk"; import { ndk } from './ndk'
import { isPRStatus, summary_defaults } from "$lib/components/prs/type"; import { isPRStatus, summary_defaults } from '$lib/components/prs/type'
import type { User } from "$lib/components/users/type"; import type { User } from '$lib/components/users/type'
import { ensureUser } from "./users"; import { ensureUser } from './users'
import type { PRStatus, PRSummaries } from "$lib/components/prs/type"; import type { PRStatus, PRSummaries } from '$lib/components/prs/type'
import { ensureSelectedRepo } from "./repo"; import { ensureSelectedRepo } from './repo'
import { pr_status_kind } from "$lib/kinds"; import { pr_status_kind } from '$lib/kinds'
import type { Repo } from "$lib/components/repo/type"; import type { Repo } from '$lib/components/repo/type'
export let pr_summaries: Writable<PRSummaries> = writable({ export const pr_summaries: Writable<PRSummaries> = writable({
id: "", id: '',
summaries: [],
loading: false,
})
const pr_kind: number = 318
let selected_repo_id: string = ''
let authors_unsubscribers: Unsubscriber[] = []
let sub: NDKSubscription
export const ensurePRSummaries = async (repo_id: string) => {
if (selected_repo_id == repo_id) return
pr_summaries.set({
id: repo_id,
summaries: [], summaries: [],
loading: false, loading: repo_id !== '',
}); })
let pr_kind: number = 318; if (sub) sub.stop()
if (sub_statuses) sub_statuses.stop()
let selected_repo_id: string = ""; authors_unsubscribers.forEach((u) => u())
authors_unsubscribers = []
let authors_unsubscribers: Unsubscriber[] = [];
selected_repo_id = repo_id
let sub: NDKSubscription;
const repo = await ensureSelectedRepo(repo_id)
export let ensurePRSummaries = async (repo_id: string) => {
if (selected_repo_id == repo_id) return; sub = ndk.subscribe(
pr_summaries.set({ {
id: repo_id, kinds: [pr_kind],
summaries: [], '#r': [`r-${repo_id}`],
loading: repo_id !== "", limit: 50,
}); },
{
if (sub) sub.stop(); closeOnEose: false,
if (sub_statuses) sub_statuses.stop(); },
authors_unsubscribers.forEach(u => u()); NDKRelaySet.fromRelayUrls(repo.relays, ndk)
authors_unsubscribers = []; )
selected_repo_id = repo_id; sub.on('event', (event: NDKEvent) => {
try {
let repo = await ensureSelectedRepo(repo_id); if (
event.kind == pr_kind &&
sub = ndk.subscribe( event.getMatchingTags('r').find((t) => t[1] === `r-${repo_id}`)
{ ) {
kinds: [pr_kind], pr_summaries.update((prs) => {
'#r': [`r-${repo_id}`], return {
limit: 50, ...prs,
}, summaries: [
{ ...prs.summaries,
closeOnEose: false, {
}, ...summary_defaults,
NDKRelaySet.fromRelayUrls(repo.relays, ndk), id: event.id,
); repo_id: repo_id,
title: event.tagValue('name') || '',
sub.on("event", (event: NDKEvent) => { descritpion: event.tagValue('description') || '',
try { created_at: event.created_at,
if (event.kind == pr_kind comments: 0,
&& event.getMatchingTags("r").find(t => t[1] === `r-${repo_id}`) author: {
) { hexpubkey: event.pubkey,
pr_summaries.update(prs => { loading: true,
return { npub: '',
...prs, },
summaries: [
...prs.summaries,
{
...summary_defaults,
id: event.id,
repo_id: repo_id,
title: event.tagValue("name") || "",
descritpion: event.tagValue("description") || "",
created_at: event.created_at,
comments: 0,
author: {
hexpubkey: event.pubkey,
loading: true,
npub: "",
},
loading: false,
}
],
}
});
authors_unsubscribers.push(
ensureUser(event.pubkey).subscribe((u: User) => {
pr_summaries.update(prs => {
return {
...prs,
summaries: prs.summaries.map(o => ({
...o,
author: u,
})),
}
});
})
);
}
} catch { }
});
sub.on("eose", () => {
pr_summaries.update(prs => {
getAndUpdatePRStatus(prs, repo);
return {
...prs,
loading: false, loading: false,
}; },
}); ],
}); }
})
authors_unsubscribers.push(
ensureUser(event.pubkey).subscribe((u: User) => {
pr_summaries.update((prs) => {
return {
...prs,
summaries: prs.summaries.map((o) => ({
...o,
author: u,
})),
}
})
})
)
}
} catch {}
})
sub.on('eose', () => {
pr_summaries.update((prs) => {
getAndUpdatePRStatus(prs, repo)
return {
...prs,
loading: false,
}
})
})
} }
let sub_statuses: NDKSubscription; let sub_statuses: NDKSubscription
function getAndUpdatePRStatus(prs: PRSummaries, repo: Repo): void { function getAndUpdatePRStatus(prs: PRSummaries, repo: Repo): void {
if (sub_statuses) sub_statuses.stop(); if (sub_statuses) sub_statuses.stop()
sub_statuses = ndk.subscribe( sub_statuses = ndk.subscribe(
{ {
kinds: [pr_status_kind], kinds: [pr_status_kind],
"#e": prs.summaries.map(pr => pr.id), '#e': prs.summaries.map((pr) => pr.id),
'#r': [`r-${prs.id}`], '#r': [`r-${prs.id}`],
}, },
{ {
closeOnEose: false, closeOnEose: false,
}, },
NDKRelaySet.fromRelayUrls(repo.relays, ndk), NDKRelaySet.fromRelayUrls(repo.relays, ndk)
); )
sub_statuses.on("event", (event: NDKEvent) => { sub_statuses.on('event', (event: NDKEvent) => {
let tagged_pr_event = event.tagValue('e'); const tagged_pr_event = event.tagValue('e')
if (event.kind == pr_status_kind if (
&& tagged_pr_event event.kind == pr_status_kind &&
&& event.created_at tagged_pr_event &&
&& event.getMatchingTags("l").length === 1 event.created_at &&
&& event.getMatchingTags("l")[0].length > 1 event.getMatchingTags('l').length === 1 &&
) { event.getMatchingTags('l')[0].length > 1
let potential_status = event.getMatchingTags("l")[0][1]; ) {
const potential_status = event.getMatchingTags('l')[0][1]
if (isPRStatus(potential_status)) {
pr_summaries.update(prs => { if (isPRStatus(potential_status)) {
return { pr_summaries.update((prs) => {
...prs, return {
summaries: prs.summaries.map(o => { ...prs,
if ( summaries: prs.summaries.map((o) => {
o.id === tagged_pr_event if (
&& event.created_at o.id === tagged_pr_event &&
&& o.status_date < event.created_at event.created_at &&
) { o.status_date < event.created_at
return { ) {
...o, return {
status: potential_status as PRStatus, ...o,
status_date: event.created_at, status: potential_status as PRStatus,
} status_date: event.created_at,
} }
}
return o;
}), return o
} }),
}); }
} })
} }
}); }
})
sub_statuses.on("eose", () => {
pr_summaries.update(prs => { sub_statuses.on('eose', () => {
return { pr_summaries.update((prs) => {
...prs, return {
summaries: prs.summaries.map(o => ({ ...prs,
...o, summaries: prs.summaries.map((o) => ({
status: o.status || "Open", ...o,
})), status: o.status || 'Open',
} })),
}); }
}); })
})
} }

41
src/lib/stores/ndk.ts

@ -1,24 +1,31 @@
import NDKSvelte from '@nostr-dev-kit/ndk-svelte'; import NDKSvelte from '@nostr-dev-kit/ndk-svelte'
export let base_relays = import.meta.env.DEV // export let base_relays = import.meta.env.DEV
? [ // ? ["ws://localhost:8080"]
"ws://localhost:8080", // : [
] // "wss://relayable.org",
: [ // "wss://relay.f7z.io",
"wss://relayable.org", // "wss://relay.damus.io",
"wss://relay.f7z.io", // "wss://nos.lol",
"wss://relay.damus.io", // "wss://nostr.wine/",
"wss://nos.lol" // "wss://eden.nostr.land/",
// "wss://nostr.wine/", // "wss://relay.nostr.band/",
// "wss://eden.nostr.land/", // ];
// "wss://relay.nostr.band/",
]; export const base_relays = [
'wss://relayable.org',
'wss://relay.f7z.io',
'wss://relay.damus.io',
'wss://nos.lol',
'wss://nostr.wine/',
'wss://eden.nostr.land/',
'wss://relay.nostr.band/',
]
// TODO: fallback_relays for if profile cannot be found // TODO: fallback_relays for if profile cannot be found
export const ndk = new NDKSvelte({ export const ndk = new NDKSvelte({
explicitRelayUrls: [...base_relays], explicitRelayUrls: [...base_relays],
}); })
ndk.connect(); ndk.connect()

184
src/lib/stores/repo.ts

@ -1,103 +1,99 @@
import { NDKRelaySet, NDKSubscription } from "@nostr-dev-kit/ndk"; import { NDKRelaySet, NDKSubscription } from '@nostr-dev-kit/ndk'
import { writable, type Unsubscriber, type Writable, get } from "svelte/store" import { writable, type Unsubscriber, type Writable } from 'svelte/store'
import { base_relays, ndk } from "./ndk"; import { base_relays, ndk } from './ndk'
import type { Repo } from "$lib/components/repo/type"; import type { Repo } from '$lib/components/repo/type'
import { defaults } from "$lib/components/repo/type"; import { defaults } from '$lib/components/repo/type'
import type { User } from "$lib/components/users/type"; import type { User } from '$lib/components/users/type'
import { ensureUser, users } from "./users"; import { ensureUser } from './users'
import { repo_kind } from "$lib/kinds"; import { repo_kind } from '$lib/kinds'
export let selected_repo: Writable<Repo> = writable({ ...defaults }); export const selected_repo: Writable<Repo> = writable({ ...defaults })
let selected_repo_id: string = ""; let selected_repo_id: string = ''
let maintainers_unsubscribers: Unsubscriber[] = []; let maintainers_unsubscribers: Unsubscriber[] = []
let sub: NDKSubscription;
export let ensureSelectedRepo = async (repo_id: string): Promise<Repo> => {
if (selected_repo_id == repo_id) {
return new Promise(r => {
let unsubscriber = selected_repo.subscribe(repo => {
if (repo.repo_id === repo_id && !repo.loading) {
setTimeout(() => {
unsubscriber();
}, 5);
r({ ...repo });
}
});
})
}
selected_repo_id = repo_id;
if (sub) sub.stop();
sub = ndk.subscribe(
{
kinds: [repo_kind],
'#d': [repo_id],
limit: 1,
},
{
closeOnEose: false,
},
NDKRelaySet.fromRelayUrls(base_relays, ndk),
);
let sub: NDKSubscription
export const ensureSelectedRepo = async (repo_id: string): Promise<Repo> => {
if (selected_repo_id == repo_id) {
return new Promise((r) => { return new Promise((r) => {
sub.on("event", (event) => { const unsubscriber = selected_repo.subscribe((repo) => {
try { if (repo.repo_id === repo_id && !repo.loading) {
if (event.kind == repo_kind && event.tagValue("d") == repo_id) { setTimeout(() => {
selected_repo.set({ unsubscriber()
loading: false, }, 5)
repo_id: event.replaceableDTag(), r({ ...repo })
name: event.tagValue("name") || "", }
description: event.tagValue("description") || "", })
git_server: event.tagValue("git-server") || "", })
tags: event.getMatchingTags("t") || [], }
maintainers: event.getMatchingTags("p").map( selected_repo_id = repo_id
(t: string[]) =>
({
hexpubkey: t[1],
loading: true,
npub: "",
}) as User,
),
relays: event
.getMatchingTags("relay")
.map((t: string[]) => t[1]),
});
let old_unsubscribers = maintainers_unsubscribers;
maintainers_unsubscribers = event
.getMatchingTags("p")
.map((t: string[]) => {
return ensureUser(t[1]).subscribe((u: User) => {
selected_repo.update((repo) => {
return {
...repo,
maintainers: repo.maintainers.map((m) => {
if (m.hexpubkey == u.hexpubkey) return { ...u };
else return { ...m };
}),
};
});
})
});
old_unsubscribers.forEach((unsubscriber) => unsubscriber());
}
} catch { }
});
sub.on("eose", () => { if (sub) sub.stop()
selected_repo.update((repo) => { sub = ndk.subscribe(
r({ {
...repo, kinds: [repo_kind],
loading: false, '#d': [repo_id],
}); limit: 1,
return { },
{
closeOnEose: false,
},
NDKRelaySet.fromRelayUrls(base_relays, ndk)
)
return new Promise((r) => {
sub.on('event', (event) => {
try {
if (event.kind == repo_kind && event.tagValue('d') == repo_id) {
selected_repo.set({
loading: false,
repo_id: event.replaceableDTag(),
name: event.tagValue('name') || '',
description: event.tagValue('description') || '',
git_server: event.tagValue('git-server') || '',
tags: event.getMatchingTags('t') || [],
maintainers: event.getMatchingTags('p').map(
(t: string[]) =>
({
hexpubkey: t[1],
loading: true,
npub: '',
}) as User
),
relays: event.getMatchingTags('relay').map((t: string[]) => t[1]),
})
const old_unsubscribers = maintainers_unsubscribers
maintainers_unsubscribers = event
.getMatchingTags('p')
.map((t: string[]) => {
return ensureUser(t[1]).subscribe((u: User) => {
selected_repo.update((repo) => {
return {
...repo, ...repo,
loading: false, maintainers: repo.maintainers.map((m) => {
} if (m.hexpubkey == u.hexpubkey) return { ...u }
else return { ...m }
}),
}
})
})
}) })
}); old_unsubscribers.forEach((unsubscriber) => unsubscriber())
}); }
} catch {}
})
sub.on('eose', () => {
selected_repo.update((repo) => {
r({
...repo,
loading: false,
})
return {
...repo,
loading: false,
}
})
})
})
} }

236
src/lib/stores/users.ts

@ -1,137 +1,137 @@
import { defaults as user_defaults, type User } from "$lib/components/users/type"; import {
import { NDKNip07Signer, NDKRelayList } from "@nostr-dev-kit/ndk"; defaults as user_defaults,
import { get, writable, type Unsubscriber, type Writable } from "svelte/store" type User,
import { ndk } from "./ndk"; } from '$lib/components/users/type'
import { NDKNip07Signer, NDKRelayList } from '@nostr-dev-kit/ndk'
import { get, writable, type Unsubscriber, type Writable } from 'svelte/store'
import { ndk } from './ndk'
export let users: { [hexpubkey: string]: Writable<User>; } = {}; export const users: { [hexpubkey: string]: Writable<User> } = {}
export let ensureUser = (hexpubkey: string): Writable<User> => { export const ensureUser = (hexpubkey: string): Writable<User> => {
if (!users[hexpubkey]) { if (!users[hexpubkey]) {
let u = ndk.getUser({ hexpubkey }); const u = ndk.getUser({ hexpubkey })
let base: User = { const base: User = {
loading: false, loading: false,
hexpubkey, hexpubkey,
npub: u.npub, npub: u.npub,
};
users[hexpubkey] = writable(base);
getUserRelays(hexpubkey);
u.fetchProfile().then(
(p) => {
users[hexpubkey].update((u) => ({
...u,
loading: false,
profile: p === null ? undefined : p,
}));
},
() => {
users[hexpubkey].update((u) => ({
...u,
loading: false,
}));
}
);
} }
return users[hexpubkey];
users[hexpubkey] = writable(base)
getUserRelays(hexpubkey)
u.fetchProfile().then(
(p) => {
users[hexpubkey].update((u) => ({
...u,
loading: false,
profile: p === null ? undefined : p,
}))
},
() => {
users[hexpubkey].update((u) => ({
...u,
loading: false,
}))
}
)
}
return users[hexpubkey]
} }
export let returnUser = async (hexpubkey: string): Promise<User> => { export const returnUser = async (hexpubkey: string): Promise<User> => {
return new Promise((r) => { return new Promise((r) => {
let unsubscriber = ensureUser(hexpubkey).subscribe((u) => { const unsubscriber = ensureUser(hexpubkey).subscribe((u) => {
if (!u.loading) { if (!u.loading) {
unsubscriber(); unsubscriber()
r(u); r(u)
} }
}); })
}); })
} }
// nip07_plugin is set in Navbar component // nip07_plugin is set in Navbar component
export let nip07_plugin: Writable<undefined | boolean> = writable(undefined); export const nip07_plugin: Writable<undefined | boolean> = writable(undefined)
export let checkForNip07Plugin = () => {
if (window.nostr) {
nip07_plugin.set(true);
} else {
let timerId: NodeJS.Timeout;
const intervalId = setInterval(() => {
if (window.nostr) {
clearTimeout(timerId);
clearInterval(intervalId);
nip07_plugin.set(true);
}
}, 100);
timerId = setTimeout(() => {
clearInterval(intervalId);
nip07_plugin.set(false);
}, 5000);
}
}; export const checkForNip07Plugin = () => {
if (window.nostr) {
nip07_plugin.set(true)
} else {
let timerId: NodeJS.Timeout | undefined = undefined
const intervalId = setInterval(() => {
if (window.nostr) {
clearTimeout(timerId)
clearInterval(intervalId)
nip07_plugin.set(true)
}
}, 100)
timerId = setTimeout(() => {
clearInterval(intervalId)
nip07_plugin.set(false)
}, 5000)
}
}
let signer = new NDKNip07Signer(2000); const signer = new NDKNip07Signer(2000)
export let logged_in_user: Writable<undefined | User> = writable(undefined); export const logged_in_user: Writable<undefined | User> = writable(undefined)
export let login = async (): Promise<void> => { export const login = async (): Promise<void> => {
return new Promise(async (res, rej) => { return new Promise(async (res, rej) => {
let user = get(logged_in_user); const user = get(logged_in_user)
if (user) return res(); if (user) return res()
if (get(nip07_plugin)) { if (get(nip07_plugin)) {
try { try {
let ndk_user = await signer.blockUntilReady(); const ndk_user = await signer.blockUntilReady()
logged_in_user.set({ logged_in_user.set({
...user_defaults, ...user_defaults,
hexpubkey: ndk_user.pubkey, hexpubkey: ndk_user.pubkey,
}); })
ndk.signer = signer; ndk.signer = signer
ensureUser(ndk_user.pubkey).subscribe(user => { ensureUser(ndk_user.pubkey).subscribe((user) => {
logged_in_user.set({ ...user }); logged_in_user.set({ ...user })
}); })
return res(); return res()
} } catch (e) {
catch (e) { alert(e)
alert(e); rej()
rej(); }
} } else {
} rej()
else { }
rej(); })
} }
});
};
interface UserRelays { interface UserRelays {
loading: boolean; loading: boolean
ndk_relays: NDKRelayList | undefined; ndk_relays: NDKRelayList | undefined
} }
export let user_relays: { [hexpubkey: string]: Writable<UserRelays>; } = {}; export const user_relays: { [hexpubkey: string]: Writable<UserRelays> } = {}
export let getUserRelays = async (hexpubkey: string): Promise<UserRelays> => { export const getUserRelays = async (hexpubkey: string): Promise<UserRelays> => {
return new Promise(async (res, rej) => { return new Promise(async (res, _) => {
if (user_relays[hexpubkey]) { if (user_relays[hexpubkey]) {
let unsubscriber: Unsubscriber; const unsubscriber: Unsubscriber = user_relays[hexpubkey].subscribe(
unsubscriber = user_relays[hexpubkey].subscribe(querying_user_relays => { (querying_user_relays) => {
if (querying_user_relays && !querying_user_relays.loading) { if (querying_user_relays && !querying_user_relays.loading) {
res(querying_user_relays); res(querying_user_relays)
if (unsubscriber) unsubscriber(); if (unsubscriber) unsubscriber()
} }
});
}
else {
user_relays[hexpubkey] = writable({
loading: true,
ndk_relays: undefined,
});
let relay_list = await ndk.getUser({ hexpubkey }).relayList();
let querying_user_relays = {
loading: false,
ndk_relays: relay_list,
};
user_relays[hexpubkey].set({ ...querying_user_relays });
res(querying_user_relays);
} }
}); )
}; } else {
user_relays[hexpubkey] = writable({
loading: true,
ndk_relays: undefined,
})
const relay_list = await ndk.getUser({ hexpubkey }).relayList()
const querying_user_relays = {
loading: false,
ndk_relays: relay_list,
}
user_relays[hexpubkey].set({ ...querying_user_relays })
res(querying_user_relays)
}
})
}

124
src/lib/wrappers/Compose.svelte

@ -1,76 +1,74 @@
<script lang="ts"> <script lang="ts">
import { ndk } from "$lib/stores/ndk"; import { ndk } from '$lib/stores/ndk'
import { NDKEvent, NDKRelaySet, type NDKTag } from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKRelaySet } from '@nostr-dev-kit/ndk'
import { reply_kind } from "$lib/kinds"; import { reply_kind } from '$lib/kinds'
import { getUserRelays, logged_in_user } from "$lib/stores/users"; import { getUserRelays, logged_in_user } from '$lib/stores/users'
import { selected_repo } from "$lib/stores/repo"; import { selected_repo } from '$lib/stores/repo'
import Compose from "$lib/components/events/Compose.svelte"; import Compose from '$lib/components/events/Compose.svelte'
import { selected_pr_full } from "$lib/stores/PR"; import { selected_pr_full } from '$lib/stores/PR'
export let reply_to_event_id = ""; export let reply_to_event_id = ''
let repo_id: string; let repo_id: string
let pr_id: string; let pr_id: string
let submitting = false; let submitting = false
let submitted = false; let submitted = false
let edit_mode = false; let edit_mode = false
$: { $: {
repo_id = $selected_repo.repo_id; repo_id = $selected_repo.repo_id
pr_id = $selected_pr_full.summary.id; pr_id = $selected_pr_full.summary.id
edit_mode = edit_mode =
$logged_in_user !== undefined && $logged_in_user !== undefined &&
repo_id.length > 0 && repo_id.length > 0 &&
pr_id.length > 0 && pr_id.length > 0 &&
!submitted; !submitted
} }
async function sendReply(content: string) { async function sendReply(content: string) {
if (!$logged_in_user) return; if (!$logged_in_user) return
let event = new NDKEvent(ndk); let event = new NDKEvent(ndk)
event.kind = reply_kind; event.kind = reply_kind
event.tags.push(["e", pr_id, "root"]); event.tags.push(['e', pr_id, 'root'])
if (reply_to_event_id.length > 0) { if (reply_to_event_id.length > 0) {
event.tags.push(["e", pr_id, "reply"]); event.tags.push(['e', pr_id, 'reply'])
} }
event.tags.push(["r", `r-${repo_id}`]); event.tags.push(['r', `r-${repo_id}`])
event.content = content; event.content = content
submitting = true; submitting = true
let relays = [...$selected_repo.relays]; let relays = [...$selected_repo.relays]
try { try {
event.sign(); event.sign()
} catch { } catch {
alert("failed to sign event"); alert('failed to sign event')
} }
try { try {
let user_relays = await getUserRelays($logged_in_user.hexpubkey); let user_relays = await getUserRelays($logged_in_user.hexpubkey)
relays = [ relays = [
...relays, ...relays,
...(user_relays.ndk_relays ...(user_relays.ndk_relays
? user_relays.ndk_relays.writeRelayUrls ? user_relays.ndk_relays.writeRelayUrls
: []), : []),
// TODO: pr event pubkey relays // TODO: pr event pubkey relays
]; ]
} catch { } catch {
alert("failed to get user relays"); alert('failed to get user relays')
}
try {
let res = await event.publish(
NDKRelaySet.fromRelayUrls(relays, ndk),
);
submitting = false;
submitted = true;
setTimeout(() => {
submitted = false;
}, 5000);
} catch {}
} }
try {
let _ = await event.publish(NDKRelaySet.fromRelayUrls(relays, ndk))
submitting = false
submitted = true
setTimeout(() => {
submitted = false
}, 5000)
} catch {}
}
</script> </script>
{#if edit_mode} {#if edit_mode}
<Compose {sendReply} {submitting} /> <Compose {sendReply} {submitting} />
{/if} {/if}
{#if submitted} {#if submitted}
<div>sent!</div> <div>sent!</div>
{/if} {/if}

57
src/lib/wrappers/EventCard.svelte

@ -1,37 +1,36 @@
<script lang="ts"> <script lang="ts">
import EventWrapper from "$lib/components/events/EventWrapper.svelte"; import EventWrapper from '$lib/components/events/EventWrapper.svelte'
import Kind19851985 from "$lib/components/events/content/Kind19851985.svelte"; import Kind19851985 from '$lib/components/events/content/Kind19851985.svelte'
import Kind317 from "$lib/components/events/content/Kind317.svelte"; import Kind317 from '$lib/components/events/content/Kind317.svelte'
import ParsedContent from "$lib/components/events/content/ParsedContent.svelte"; import ParsedContent from '$lib/components/events/content/ParsedContent.svelte'
import type { User } from "$lib/components/users/type"; import { defaults as user_defaults } from '$lib/components/users/type'
import { defaults as user_defaults } from "$lib/components/users/type"; import { patch_kind, pr_status_kind } from '$lib/kinds'
import { patch_kind, pr_status_kind } from "$lib/kinds"; import { ensureUser } from '$lib/stores/users'
import { ensureUser } from "$lib/stores/users"; import type { NDKEvent } from '@nostr-dev-kit/ndk'
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { onDestroy } from 'svelte'
import { onDestroy } from "svelte"; import { writable } from 'svelte/store'
import { writable } from "svelte/store";
export let event: NDKEvent; export let event: NDKEvent
let author = writable({ ...user_defaults }); let author = writable({ ...user_defaults })
let author_unsubsriber = ensureUser(event.pubkey).subscribe((u) => { let author_unsubsriber = ensureUser(event.pubkey).subscribe((u) => {
author.set({ ...u }); author.set({ ...u })
}); })
onDestroy(() => { onDestroy(() => {
author_unsubsriber(); author_unsubsriber()
}); })
</script> </script>
<EventWrapper <EventWrapper
author={$author} author={$author}
created_at={event.created_at} created_at={event.created_at}
event_id={event.id} event_id={event.id}
> >
{#if event.kind == patch_kind} {#if event.kind == patch_kind}
<Kind317 content={event.content} tags={event.tags} /> <Kind317 content={event.content} tags={event.tags} />
{:else if event.kind === pr_status_kind} {:else if event.kind === pr_status_kind}
<Kind19851985 tags={event.tags} /> <Kind19851985 tags={event.tags} />
{:else} {:else}
<ParsedContent content={event.content} tags={event.tags} /> <ParsedContent content={event.content} tags={event.tags} />
{/if} {/if}
</EventWrapper> </EventWrapper>

34
src/lib/wrappers/Navbar.svelte

@ -1,25 +1,23 @@
<script lang="ts"> <script lang="ts">
import { import {
checkForNip07Plugin, checkForNip07Plugin,
logged_in_user, logged_in_user,
login, login,
nip07_plugin, nip07_plugin,
} from "$lib/stores/users"; } from '$lib/stores/users'
import { onMount } from "svelte"; import { onMount } from 'svelte'
import Navbar from "$lib/components/Navbar.svelte"; import Navbar from '$lib/components/Navbar.svelte'
let singup_function: Function = () => { let singup_function = () => {
alert( alert('a NIP-07 browser extension is required. currently no signup page')
"a NIP-07 browser extension is required. currently no signup page", }
);
};
onMount(checkForNip07Plugin); onMount(checkForNip07Plugin)
</script> </script>
<Navbar <Navbar
logged_in_user={$logged_in_user} logged_in_user={$logged_in_user}
nip07_plugin={$nip07_plugin} nip07_plugin={$nip07_plugin}
login_function={login} login_function={login}
{singup_function} {singup_function}
/> />

14
src/lib/wrappers/OpenPRs.svelte

@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import PRsList from "$lib/components/prs/PRsList.svelte"; import PRsList from '$lib/components/prs/PRsList.svelte'
import { ensurePRSummaries, pr_summaries } from "$lib/stores/PRs"; import { ensurePRSummaries, pr_summaries } from '$lib/stores/PRs'
export let repo_id: string = ""; export let repo_id: string = ''
ensurePRSummaries(repo_id); ensurePRSummaries(repo_id)
</script> </script>
<PRsList <PRsList
title="Open PRs" title="Open PRs"
prs={$pr_summaries.summaries} prs={$pr_summaries.summaries}
loading={$pr_summaries.loading} loading={$pr_summaries.loading}
/> />

8
src/lib/wrappers/RepoDetails.svelte

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import RepoDetails from "$lib/components/repo/RepoDetails.svelte"; import RepoDetails from '$lib/components/repo/RepoDetails.svelte'
import { ensureSelectedRepo, selected_repo } from "$lib/stores/repo"; import { ensureSelectedRepo, selected_repo } from '$lib/stores/repo'
export let repo_id = ""; export let repo_id = ''
ensureSelectedRepo(repo_id); ensureSelectedRepo(repo_id)
</script> </script>
<RepoDetails {...$selected_repo} /> <RepoDetails {...$selected_repo} />

96
src/lib/wrappers/ReposRecent.svelte

@ -1,55 +1,55 @@
<script lang="ts"> <script lang="ts">
import type { Args } from "$lib/components/RepoSummaryCard.svelte"; import type { Args } from '$lib/components/RepoSummaryCard.svelte'
import ReposSummaryList from "$lib/components/ReposSummaryList.svelte"; import ReposSummaryList from '$lib/components/ReposSummaryList.svelte'
import { repo_kind } from "$lib/kinds"; import { repo_kind } from '$lib/kinds'
import { ndk } from "$lib/stores/ndk"; import { ndk } from '$lib/stores/ndk'
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from '@nostr-dev-kit/ndk'
import { onDestroy } from "svelte"; import { onDestroy } from 'svelte'
export let limit: number = 10; export let limit: number = 10
let repos: Args[] = []; let repos: Args[] = []
let loading: boolean = true; let loading: boolean = true
let sub = ndk.subscribe({ let sub = ndk.subscribe({
kinds: [repo_kind], kinds: [repo_kind],
limit, limit,
}); })
sub.on("event", (event: NDKEvent) => { sub.on('event', (event: NDKEvent) => {
if (repos.length < limit) { if (repos.length < limit) {
try { try {
if ( if (
event.kind == repo_kind && event.kind == repo_kind &&
!repos.some( !repos.some(
(r) => (r) =>
r.repo_id == event.replaceableDTag() && r.repo_id == event.replaceableDTag() &&
event.created_at && event.created_at &&
r.created_at > event.created_at, r.created_at > event.created_at
) )
) )
repos = [ repos = [
...repos.filter( ...repos.filter(
(r) => (r) =>
!event.created_at || !event.created_at ||
r.repo_id !== event.replaceableDTag() || r.repo_id !== event.replaceableDTag() ||
r.created_at > event.created_at, r.created_at > event.created_at
), ),
{ {
name: event.tagValue("name") || "", name: event.tagValue('name') || '',
description: event.tagValue("description") || "", description: event.tagValue('description') || '',
repo_id: event.replaceableDTag(), repo_id: event.replaceableDTag(),
created_at: event.created_at || 0, created_at: event.created_at || 0,
}, },
]; ]
} catch {} } catch {}
} else if (loading == true) loading = false; } else if (loading == true) loading = false
}); })
sub.on("eose", () => { sub.on('eose', () => {
if (loading == true) loading = false; if (loading == true) loading = false
}); })
onDestroy(() => { onDestroy(() => {
sub.stop(); sub.stop()
}); })
</script> </script>
<ReposSummaryList title="Latest Repositories" {repos} {loading} /> <ReposSummaryList title="Latest Repositories" {repos} {loading} />

40
src/lib/wrappers/Thread.svelte

@ -1,32 +1,28 @@
<script lang="ts"> <script lang="ts">
import type { User } from "$lib/components/users/type"; import { ndk } from '$lib/stores/ndk'
import { defaults as user_defaults } from "$lib/components/users/type"; import type { NDKEvent } from '@nostr-dev-kit/ndk'
import { ndk } from "$lib/stores/ndk"; import EventCard from './EventCard.svelte'
import { ensureUser } from "$lib/stores/users"; import ThreadWrapper from '$lib/components/events/ThreadWrapper.svelte'
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { writable } from 'svelte/store'
import { onDestroy } from "svelte";
import EventCard from "./EventCard.svelte";
import ThreadWrapper from "$lib/components/events/ThreadWrapper.svelte";
import { writable } from "svelte/store";
export let event: NDKEvent; export let event: NDKEvent
export let replies: NDKEvent[] | undefined = undefined; export let replies: NDKEvent[] | undefined = undefined
let replies_store = replies let replies_store = replies
? writable(replies) ? writable(replies)
: ndk.storeSubscribe({ : ndk.storeSubscribe({
"#e": [event.id], '#e': [event.id],
}); })
$: { $: {
if (replies) replies_store.set(replies); if (replies) replies_store.set(replies)
} }
</script> </script>
<EventCard {event} /> <EventCard {event} />
<ThreadWrapper> <ThreadWrapper>
{#each $replies_store as event} {#each $replies_store as event}
<EventCard {event} /> <EventCard {event} />
{/each} {/each}
</ThreadWrapper> </ThreadWrapper>

4
src/routes/+layout.svelte

@ -1,6 +1,6 @@
<script> <script>
import "../app.css"; import '../app.css'
import Navbar from "$lib/wrappers/Navbar.svelte"; import Navbar from '$lib/wrappers/Navbar.svelte'
</script> </script>
<Navbar /> <Navbar />

16
src/routes/+page.svelte

@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import ReposRecent from "$lib/wrappers/ReposRecent.svelte"; import ReposRecent from '$lib/wrappers/ReposRecent.svelte'
</script> </script>
<div role="alert" class="alert"> <div role="alert" class="alert">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6" class="h-6 w-6 shrink-0 stroke-current"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
><path ><path
@ -16,12 +16,12 @@
/></svg /></svg
> >
<div> <div>
<h3 class="text-xs prose">Warning Experimental: expect breaking changes</h3> <h3 class="prose text-xs">Warning Experimental: expect breaking changes</h3>
</div> </div>
</div> </div>
<div class="flex flex-row"> <div class="flex flex-row">
<div class="hero bg-pr h-[calc(100vh-4rem)] md:basis-1/2"> <div class="bg-pr hero h-[calc(100vh-4rem)] md:basis-1/2">
<div class="hero-content text-center"> <div class="hero-content text-center">
<div class="max-w-md"> <div class="max-w-md">
<div class="prose"> <div class="prose">
@ -30,12 +30,12 @@
>workshop</span >workshop</span
><span class="text-neutral">.dev</span> ><span class="text-neutral">.dev</span>
</h1> </h1>
<p class="mt-3 mb-8"> <p class="mb-8 mt-3">
permissionless alternative to GitHub over nostr permissionless alternative to GitHub over nostr
</p> </p>
</div> </div>
<div> <div>
<div class="bg-base-300 card mb-8"> <div class="card mb-8 bg-base-300">
<div class="card-body items-center text-center"> <div class="card-body items-center text-center">
<div class="card-title"> <div class="card-title">
<h3> <h3>
@ -46,7 +46,7 @@
a command-line tool to <span class="badge">create</span>, a command-line tool to <span class="badge">create</span>,
<span class="badge">list</span> and <span class="badge">list</span> and
<span class="badge">apply</span> PRs and patches <span class="badge">apply</span> PRs and patches
<div class="card-actions flex mt-3"> <div class="card-actions mt-3 flex">
<div class="flex-grow"> <div class="flex-grow">
<a href="/ngit" class="btn btn-neutral">install ngit</a> <a href="/ngit" class="btn btn-neutral">install ngit</a>
</div> </div>
@ -61,7 +61,7 @@
<button <button
on:click={() => on:click={() =>
alert( alert(
"install ngit and run `ngit claim` your local git repository", 'install ngit and run `ngit claim` your local git repository'
)} )}
class="btn btn-secondary">list your repository</button class="btn btn-secondary">list your repository</button
> >

159
src/routes/ngit/+page.svelte

@ -1,87 +1,84 @@
<script lang="ts"> <script lang="ts">
import Container from "$lib/components/Container.svelte"; import Container from '$lib/components/Container.svelte'
</script> </script>
<Container> <Container>
<div class="prose mt-8 m-auto"> <div class="prose m-auto mt-8">
<h2 class=""><span class="text-purple-600">n</span>git</h2> <h2 class=""><span class="text-purple-600">n</span>git</h2>
<p> <p>
a command-line tool to create, list and apply PRs and patches to git a command-line tool to create, list and apply PRs and patches to git
repositories via nostr events. repositories via nostr events.
</p> </p>
It is designed to work seemlessly with this site gitworkshop.dev. It is designed to work seemlessly with this site gitworkshop.dev.
<h3>Install</h3> <h3>Install</h3>
<p> <p>
If you have cargo installed run<code>cargo install ngit</code>. If you have cargo installed run<code>cargo install ngit</code>.
Alternatively download: Alternatively download:
</p> </p>
<a <a
href="https://github.com/DanConwayDev/ngit-cli/releases/download/v0.1.1/ngit-x86_64-unknown-linux-gnu.tar.gz" href="https://github.com/DanConwayDev/ngit-cli/releases/download/v0.1.1/ngit-x86_64-unknown-linux-gnu.tar.gz"
class="btn btn-neutral">Linux</a class="btn btn-neutral">Linux</a
> >
<a <a
href="https://github.com/DanConwayDev/ngit-cli/releases/download/v0.1.1/ngit-x86_64-apple-darwin.tar.gz" href="https://github.com/DanConwayDev/ngit-cli/releases/download/v0.1.1/ngit-x86_64-apple-darwin.tar.gz"
class="btn btn-neutral">Mac</a class="btn btn-neutral">Mac</a
> >
<a <a
href="https://github.com/DanConwayDev/ngit-cli/releases/download/v0.1.1/ngit-x86_64-pc-windows-msvc.zip" href="https://github.com/DanConwayDev/ngit-cli/releases/download/v0.1.1/ngit-x86_64-pc-windows-msvc.zip"
class="btn btn-neutral">Windows</a class="btn btn-neutral">Windows</a
> >
v.0.1.1 v.0.1.1
<p>add the binary to a directory from which it can be run globally.</p> <p>add the binary to a directory from which it can be run globally.</p>
<h3>Commands</h3> <h3>Commands</h3>
<p> <p>All commands should be run from the directory of the git repository</p>
All commands should be run from the directory of the git repository <p>
</p> <span class="rounded bg-neutral p-2 font-mono"
<p> ><span class="py-5">ngit prs create</span></span
<span class="rounded p-2 bg-neutral font-mono" >
><span class="py-5">ngit prs create</span></span use this when you are on a feature branch to propose it gets merged into the
> remote branch by repository maintainer.
use this when you are on a feature branch to propose it gets merged into </p>
the remote branch by repository maintainer. <p>
</p> It creates a PR event and patch events for each commit that isn't also in
<p> the main or master branch.
It creates a PR event and patch events for each commit that isn't </p>
also in the main or master branch. <p>
</p> <span class="rounded bg-neutral p-2 font-mono"
<p> ><span class="py-5">ngit prs list</span></span
<span class="rounded p-2 bg-neutral font-mono" >
><span class="py-5">ngit prs list</span></span fetches a selectable list of open PRs and, if one is selected and the repository
> is clean, will create a branch for it and apply the commits. If the branch
fetches a selectable list of open PRs and, if one is selected and the already exists it will pull any updates.
repository is clean, will create a branch for it and apply the commits. </p>
If the branch already exists it will pull any updates. <p>
</p> <span class="rounded bg-neutral p-2 font-mono"
<p> ><span class="py-5">ngit pull</span></span
<span class="rounded p-2 bg-neutral font-mono" >
><span class="py-5">ngit pull</span></span if the repository is currently in a branch related to an existing PR, it will
> pull any updates from nostr.
if the repository is currently in a branch related to an existing PR, </p>
it will pull any updates from nostr. <p>
</p> <span class="rounded bg-neutral p-2 font-mono"
<p> ><span class="py-5">ngit push</span></span
<span class="rounded p-2 bg-neutral font-mono" >
><span class="py-5">ngit push</span></span if the repository is currently in a branch related to an existing PR, it will
> create patch events for any new commits.
if the repository is currently in a branch related to an existing PR, </p>
it will create patch events for any new commits. <p>
</p> <span class="rounded bg-neutral p-2 font-mono"
<p> ><span class="py-5">ngit claim</span></span
<span class="rounded p-2 bg-neutral font-mono" >
><span class="py-5">ngit claim</span></span issue a repository nostr event which lists it on gitworkshop.dev and indicates
> you are accepting patches and PRs via nostr. This can be run many times to
issue a repository nostr event which lists it on gitworkshop.dev and update the details. The relays will reflect your user relays plus blaster.
indicates you are accepting patches and PRs via nostr. This can be run </p>
many times to update the details. The relays will reflect your user relays <p>
plus blaster. It also creates an optional <span class="bg-base-200 p-2 font-mono"
</p> >maintainers.yaml</span
<p> > file in the root of your repo that lists maintainers and relays. If commited
It also creates an optional <span class="font-mono p-2 bg-base-200" it will make it smoother for contributors to find your repository event and
>maintainers.yaml</span also makes any future transfer of repository ownership easier.
> file in the root of your repo that lists maintainers and relays. If </p>
commited it will make it smoother for contributors to find your repository </div>
event and also makes any future transfer of repository ownership easier.
</p>
</div>
</Container> </Container>

81
src/routes/repo/[repo_id]/+page.svelte

@ -1,51 +1,50 @@
<script lang="ts"> <script lang="ts">
import RepoDetails from "$lib/wrappers/RepoDetails.svelte"; import RepoDetails from '$lib/wrappers/RepoDetails.svelte'
import OpenPRs from "$lib/wrappers/OpenPRs.svelte"; import OpenPRs from '$lib/wrappers/OpenPRs.svelte'
import { ensureSelectedRepo, selected_repo } from "$lib/stores/repo"; import { ensureSelectedRepo, selected_repo } from '$lib/stores/repo'
import RepoHeader from "$lib/components/repo/RepoHeader.svelte"; import RepoHeader from '$lib/components/repo/RepoHeader.svelte'
import Container from "$lib/components/Container.svelte"; import Container from '$lib/components/Container.svelte'
export let data: { repo_id: string }; export let data: { repo_id: string }
let repo_id = data.repo_id; let repo_id = data.repo_id
ensureSelectedRepo(repo_id); ensureSelectedRepo(repo_id)
let repo_error = false; let repo_error = false
$: { $: {
repo_error = repo_error = !$selected_repo.loading && $selected_repo.name.length === 0
!$selected_repo.loading && $selected_repo.name.length === 0; }
}
</script> </script>
{#if repo_error} {#if repo_error}
<Container> <Container>
<div role="alert" class="alert alert-error mt-6 w-full max-w-xs m-auto"> <div role="alert" class="alert alert-error m-auto mt-6 w-full max-w-xs">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6" class="h-6 w-6 shrink-0 stroke-current"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
><path ><path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/></svg /></svg
> >
<span>Error! cannot find repository event</span> <span>Error! cannot find repository event</span>
</div> </div>
</Container> </Container>
{:else} {:else}
<RepoHeader {...$selected_repo} /> <RepoHeader {...$selected_repo} />
<Container> <Container>
<div class="md:flex mt-2"> <div class="mt-2 md:flex">
<div class="md:w-2/3 md:mr-2"> <div class="md:mr-2 md:w-2/3">
<OpenPRs {repo_id} /> <OpenPRs {repo_id} />
</div> </div>
<div class="w-1/3 ml-2 prose hidden md:flex"> <div class="prose ml-2 hidden w-1/3 md:flex">
<RepoDetails {repo_id} /> <RepoDetails {repo_id} />
</div> </div>
</div> </div>
</Container> </Container>
{/if} {/if}

6
src/routes/repo/[repo_id]/+page.ts

@ -1,5 +1,5 @@
export const load = ({ params }) => { export const load = ({ params }) => {
return { return {
repo_id: params.repo_id, repo_id: params.repo_id,
} }
} }

200
src/routes/repo/[repo_id]/pr/[pr_id]/+page.svelte

@ -1,121 +1,107 @@
<script lang="ts"> <script lang="ts">
import { ensureSelectedRepo, selected_repo } from "$lib/stores/repo"; import { ensureSelectedRepo, selected_repo } from '$lib/stores/repo'
import { import {
ensurePRFull, ensurePRFull,
selected_pr_full, selected_pr_full,
selected_pr_replies, selected_pr_replies,
} from "$lib/stores/PR"; } from '$lib/stores/PR'
import PrHeader from "$lib/components/prs/PRHeader.svelte"; import PrHeader from '$lib/components/prs/PRHeader.svelte'
import RepoHeader from "$lib/components/repo/RepoHeader.svelte"; import RepoHeader from '$lib/components/repo/RepoHeader.svelte'
import Thread from "$lib/wrappers/Thread.svelte"; import Thread from '$lib/wrappers/Thread.svelte'
import PrDetails from "$lib/components/prs/PRDetails.svelte"; import PrDetails from '$lib/components/prs/PRDetails.svelte'
import Container from "$lib/components/Container.svelte"; import Container from '$lib/components/Container.svelte'
import ParsedContent from "$lib/components/events/content/ParsedContent.svelte"; import ParsedContent from '$lib/components/events/content/ParsedContent.svelte'
import Compose from "$lib/wrappers/Compose.svelte"; import Compose from '$lib/wrappers/Compose.svelte'
import type { NDKEvent } from "@nostr-dev-kit/ndk";
export let data: { export let data: {
repo_id: string; repo_id: string
pr_id: string; pr_id: string
}; }
let repo_id = data.repo_id; let repo_id = data.repo_id
let pr_id = data.pr_id; let pr_id = data.pr_id
ensureSelectedRepo(repo_id); ensureSelectedRepo(repo_id)
ensurePRFull(repo_id, pr_id); ensurePRFull(repo_id, pr_id)
let replies: NDKEvent[] = []; let repo_error = false
let pr_error = false
$: { $: {
replies = $selected_pr_replies.sort((a, b) => repo_error = !$selected_repo.loading && $selected_repo.name.length === 0
a.created_at && b.created_at ? a.created_at - b.created_at : 1, pr_error =
); !$selected_pr_full.summary.loading &&
} $selected_pr_full.summary.created_at === 0
}
let repo_error = false;
let pr_error = false;
$: {
repo_error =
!$selected_repo.loading && $selected_repo.name.length === 0;
pr_error =
!$selected_pr_full.summary.loading &&
$selected_pr_full.summary.created_at === 0;
}
</script> </script>
{#if !repo_error} {#if !repo_error}
<RepoHeader {...$selected_repo} /> <RepoHeader {...$selected_repo} />
{/if} {/if}
{#if pr_error} {#if pr_error}
<Container> <Container>
<div role="alert" class="alert alert-error mt-6 w-full max-w-xs m-auto"> <div role="alert" class="alert alert-error m-auto mt-6 w-full max-w-xs">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6" class="h-6 w-6 shrink-0 stroke-current"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
><path ><path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/></svg /></svg
> >
<span <span>Error! cannot find PR {repo_error ? 'or repo ' : ''}event</span>
>Error! cannot find PR {repo_error ? "or repo " : ""}event</span </div>
> </Container>
</div>
</Container>
{:else} {:else}
<PrHeader {...$selected_pr_full.summary} /> <PrHeader {...$selected_pr_full.summary} />
<Container> <Container>
<div class="md:flex"> <div class="md:flex">
<div class="md:w-2/3 md:mr-2"> <div class="md:mr-2 md:w-2/3">
<div class="prose my-3"> <div class="prose my-3">
<ParsedContent <ParsedContent content={$selected_pr_full.summary.descritpion} />
content={$selected_pr_full.summary.descritpion} </div>
/> <div role="alert" class="alert">
</div> <svg
<div role="alert" class="alert"> xmlns="http://www.w3.org/2000/svg"
<svg fill="none"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" class="h-6 w-6 shrink-0 stroke-info"
viewBox="0 0 24 24" ><path
class="stroke-info shrink-0 w-6 h-6" stroke-linecap="round"
><path stroke-linejoin="round"
stroke-linecap="round" stroke-width="2"
stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-width="2" ></path></svg
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" >
></path></svg <div>
> <h3 class="prose text-xs">
<div> to view the latest patches from this PR <a href="/ngit"
<h3 class="text-xs prose"> >install ngit</a
to view the latest patches from this PR <a >, run
href="/ngit">install ngit</a <span class="rounded bg-neutral p-1 font-mono"
>, run ><span class="py-3">ngit prs list</span></span
<span class="rounded p-1 bg-neutral font-mono" > from the local repository and select this PR title
><span class="py-3">ngit prs list</span></span </h3>
> from the local repository and select this PR title </div>
</h3> </div>
</div> {#each $selected_pr_replies as event}
</div> <Thread {event} replies={[]} />
{#each $selected_pr_replies as event} {/each}
<Thread {event} replies={[]} /> <div class="my-3">
{/each} <Compose />
<div class="my-3">
<Compose />
</div>
</div>
<div class="w-1/3 ml-2 prose hidden md:flex">
<PrDetails
summary={$selected_pr_full.summary}
labels={$selected_pr_full.labels}
loading={$selected_pr_full.loading}
/>
</div>
</div> </div>
</Container> </div>
<div class="prose ml-2 hidden w-1/3 md:flex">
<PrDetails
summary={$selected_pr_full.summary}
labels={$selected_pr_full.labels}
loading={$selected_pr_full.loading}
/>
</div>
</div>
</Container>
{/if} {/if}

8
src/routes/repo/[repo_id]/pr/[pr_id]/+page.ts

@ -1,6 +1,6 @@
export const load = ({ params }) => { export const load = ({ params }) => {
return { return {
repo_id: params.repo_id, repo_id: params.repo_id,
pr_id: params.pr_id, pr_id: params.pr_id,
} }
} }

22
tsconfig.json

@ -1,13 +1,13 @@
{ {
"extends": "./.svelte-kit/tsconfig.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true "strict": true,
} },
} }

272
yarn.lock

@ -1277,6 +1277,18 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
dependencies:
eslint-visitor-keys "^3.3.0"
"@eslint-community/regexpp@^4.5.1":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63"
integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==
"@eslint/eslintrc@^1.0.5": "@eslint/eslintrc@^1.0.5":
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e"
@ -1737,6 +1749,11 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@pkgr/core@^0.1.0":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
"@polka/url@^1.0.0-next.20": "@polka/url@^1.0.0-next.20":
version "1.0.0-next.24" version "1.0.0-next.24"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.24.tgz#58601079e11784d20f82d0585865bb42305c4df3" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.24.tgz#58601079e11784d20f82d0585865bb42305c4df3"
@ -3255,6 +3272,11 @@
expect "^29.0.0" expect "^29.0.0"
pretty-format "^29.0.0" pretty-format "^29.0.0"
"@types/json-schema@^7.0.12":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/lodash@^4.14.167": "@types/lodash@^4.14.167":
version "4.14.202" version "4.14.202"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8"
@ -3372,7 +3394,7 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff"
integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==
"@types/semver@^7.3.4": "@types/semver@^7.3.4", "@types/semver@^7.5.0":
version "7.5.6" version "7.5.6"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339"
integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==
@ -3440,6 +3462,92 @@
dependencies: dependencies:
"@types/yargs-parser" "*" "@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^6.20.0":
version "6.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.20.0.tgz#9cf31546d2d5e884602626d89b0e0d2168ac25ed"
integrity sha512-fTwGQUnjhoYHeSF6m5pWNkzmDDdsKELYrOBxhjMrofPqCkoC2k3B2wvGHFxa1CTIqkEn88nlW1HVMztjo2K8Hg==
dependencies:
"@eslint-community/regexpp" "^4.5.1"
"@typescript-eslint/scope-manager" "6.20.0"
"@typescript-eslint/type-utils" "6.20.0"
"@typescript-eslint/utils" "6.20.0"
"@typescript-eslint/visitor-keys" "6.20.0"
debug "^4.3.4"
graphemer "^1.4.0"
ignore "^5.2.4"
natural-compare "^1.4.0"
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/parser@^6.20.0":
version "6.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.20.0.tgz#17e314177304bdf498527e3c4b112e41287b7416"
integrity sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==
dependencies:
"@typescript-eslint/scope-manager" "6.20.0"
"@typescript-eslint/types" "6.20.0"
"@typescript-eslint/typescript-estree" "6.20.0"
"@typescript-eslint/visitor-keys" "6.20.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@6.20.0":
version "6.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.20.0.tgz#8a926e60f6c47feb5bab878246dc2ae465730151"
integrity sha512-p4rvHQRDTI1tGGMDFQm+GtxP1ZHyAh64WANVoyEcNMpaTFn3ox/3CcgtIlELnRfKzSs/DwYlDccJEtr3O6qBvA==
dependencies:
"@typescript-eslint/types" "6.20.0"
"@typescript-eslint/visitor-keys" "6.20.0"
"@typescript-eslint/type-utils@6.20.0":
version "6.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.20.0.tgz#d395475cd0f3610dd80c7d8716fa0db767da3831"
integrity sha512-qnSobiJQb1F5JjN0YDRPHruQTrX7ICsmltXhkV536mp4idGAYrIyr47zF/JmkJtEcAVnIz4gUYJ7gOZa6SmN4g==
dependencies:
"@typescript-eslint/typescript-estree" "6.20.0"
"@typescript-eslint/utils" "6.20.0"
debug "^4.3.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/types@6.20.0":
version "6.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.20.0.tgz#5ccd74c29011ae7714ae6973e4ec0c634708b448"
integrity sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==
"@typescript-eslint/typescript-estree@6.20.0":
version "6.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.20.0.tgz#5b2d0975949e6bdd8d45ee1471461ef5fadc5542"
integrity sha512-RnRya9q5m6YYSpBN7IzKu9FmLcYtErkDkc8/dKv81I9QiLLtVBHrjz+Ev/crAqgMNW2FCsoZF4g2QUylMnJz+g==
dependencies:
"@typescript-eslint/types" "6.20.0"
"@typescript-eslint/visitor-keys" "6.20.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
minimatch "9.0.3"
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/utils@6.20.0":
version "6.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.20.0.tgz#0e52afcfaa51af5656490ba4b7437cc3aa28633d"
integrity sha512-/EKuw+kRu2vAqCoDwDCBtDRU6CTKbUmwwI7SH7AashZ+W+7o8eiyy6V2cdOqN49KsTcASWsC5QeghYuRDTyOOg==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@types/json-schema" "^7.0.12"
"@types/semver" "^7.5.0"
"@typescript-eslint/scope-manager" "6.20.0"
"@typescript-eslint/types" "6.20.0"
"@typescript-eslint/typescript-estree" "6.20.0"
semver "^7.5.4"
"@typescript-eslint/visitor-keys@6.20.0":
version "6.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz#f7ada27f2803de89df0edd9fd7be22c05ce6a498"
integrity sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==
dependencies:
"@typescript-eslint/types" "6.20.0"
eslint-visitor-keys "^3.4.1"
"@ungap/structured-clone@^1.0.0": "@ungap/structured-clone@^1.0.0":
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
@ -5017,7 +5125,43 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-scope@^7.1.0: eslint-compat-utils@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz#f45e3b5ced4c746c127cf724fb074cd4e730d653"
integrity sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==
eslint-config-prettier@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f"
integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==
eslint-plugin-prettier@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz#17cfade9e732cef32b5f5be53bd4e07afd8e67e1"
integrity sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==
dependencies:
prettier-linter-helpers "^1.0.0"
synckit "^0.8.6"
eslint-plugin-svelte@^2.35.1:
version "2.35.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-svelte/-/eslint-plugin-svelte-2.35.1.tgz#7b1e3c263b09dbc9293c25fe02d03d309725d2b9"
integrity sha512-IF8TpLnROSGy98Z3NrsKXWDSCbNY2ReHDcrYTuXZMbfX7VmESISR78TWgO9zdg4Dht1X8coub5jKwHzP0ExRug==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
debug "^4.3.1"
eslint-compat-utils "^0.1.2"
esutils "^2.0.3"
known-css-properties "^0.29.0"
postcss "^8.4.5"
postcss-load-config "^3.1.4"
postcss-safe-parser "^6.0.0"
postcss-selector-parser "^6.0.11"
semver "^7.5.3"
svelte-eslint-parser ">=0.33.0 <1.0.0"
eslint-scope@^7.0.0, eslint-scope@^7.1.0:
version "7.2.2" version "7.2.2"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
@ -5037,7 +5181,7 @@ eslint-visitor-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.4.1: eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
version "3.4.3" version "3.4.3"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
@ -5100,7 +5244,7 @@ espree@9.2.0:
acorn-jsx "^5.3.1" acorn-jsx "^5.3.1"
eslint-visitor-keys "^3.1.0" eslint-visitor-keys "^3.1.0"
espree@^9.2.0, espree@^9.4.0: espree@^9.0.0, espree@^9.2.0, espree@^9.4.0:
version "9.6.1" version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
@ -5145,7 +5289,7 @@ estree-walker@^3.0.0, estree-walker@^3.0.3:
dependencies: dependencies:
"@types/estree" "^1.0.0" "@types/estree" "^1.0.0"
esutils@^2.0.2: esutils@^2.0.2, esutils@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
@ -5272,6 +5416,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-diff@^1.1.2:
version "1.3.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.0: fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.0:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
@ -5760,7 +5909,7 @@ globalyzer@0.1.0:
resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465"
integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==
globby@^11.0.1, globby@^11.0.2: globby@^11.0.1, globby@^11.0.2, globby@^11.1.0:
version "11.1.0" version "11.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
@ -5794,6 +5943,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
graphemer@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
gunzip-maybe@^1.4.2: gunzip-maybe@^1.4.2:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac" resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac"
@ -6043,6 +6197,11 @@ ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
ignore@^5.2.4:
version "5.3.1"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
import-fresh@^3.0.0, import-fresh@^3.2.1: import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@ -7091,6 +7250,11 @@ kleur@^4.1.5:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
known-css-properties@^0.29.0:
version "0.29.0"
resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.29.0.tgz#e8ba024fb03886f23cb882e806929f32d814158f"
integrity sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==
lazy-universal-dotenv@^4.0.0: lazy-universal-dotenv@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/lazy-universal-dotenv/-/lazy-universal-dotenv-4.0.0.tgz#0b220c264e89a042a37181a4928cdd298af73422" resolved "https://registry.yarnpkg.com/lazy-universal-dotenv/-/lazy-universal-dotenv-4.0.0.tgz#0b220c264e89a042a37181a4928cdd298af73422"
@ -7120,7 +7284,7 @@ light-bolt11-decoder@^3.0.0:
dependencies: dependencies:
"@scure/base" "1.1.1" "@scure/base" "1.1.1"
lilconfig@^2.1.0: lilconfig@^2.0.5, lilconfig@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==
@ -7395,6 +7559,13 @@ min-indent@^1.0.0:
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
minimatch@9.0.3, minimatch@^9.0.1:
version "9.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
dependencies:
brace-expansion "^2.0.1"
minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@ -7409,13 +7580,6 @@ minimatch@^5.0.1:
dependencies: dependencies:
brace-expansion "^2.0.1" brace-expansion "^2.0.1"
minimatch@^9.0.1:
version "9.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8:
version "1.2.8" version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
@ -8045,6 +8209,14 @@ postcss-js@^4, postcss-js@^4.0.1:
dependencies: dependencies:
camelcase-css "^2.0.1" camelcase-css "^2.0.1"
postcss-load-config@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
dependencies:
lilconfig "^2.0.5"
yaml "^1.10.2"
postcss-load-config@^4.0.1: postcss-load-config@^4.0.1:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3"
@ -8060,6 +8232,16 @@ postcss-nested@^6.0.1:
dependencies: dependencies:
postcss-selector-parser "^6.0.11" postcss-selector-parser "^6.0.11"
postcss-safe-parser@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1"
integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==
postcss-scss@^4.0.8:
version "4.0.9"
resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.9.tgz#a03c773cd4c9623cb04ce142a52afcec74806685"
integrity sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==
postcss-selector-parser@6.0.10: postcss-selector-parser@6.0.10:
version "6.0.10" version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
@ -8090,16 +8272,47 @@ postcss@^8.3.11, postcss@^8.4.23, postcss@^8.4.27, postcss@^8.4.30:
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.2" source-map-js "^1.0.2"
postcss@^8.4.29, postcss@^8.4.5:
version "8.4.33"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742"
integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==
dependencies:
nanoid "^3.3.7"
picocolors "^1.0.0"
source-map-js "^1.0.2"
prelude-ls@^1.2.1: prelude-ls@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier-linter-helpers@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
dependencies:
fast-diff "^1.1.2"
prettier-plugin-svelte@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-3.1.2.tgz#2e050eb56dbb467a42c45ad6ce18bb277d28ffa0"
integrity sha512-7xfMZtwgAWHMT0iZc8jN4o65zgbAQ3+O32V6W7pXrqNvKnHnkoyQCGCbKeUyXKZLbYE0YhFRnamfxfkEGxm8qA==
prettier-plugin-tailwindcss@^0.5.11:
version "0.5.11"
resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.11.tgz#1aa9308c3285b3cb7942aaeaec8d0e0775ac54d0"
integrity sha512-AvI/DNyMctyyxGOjyePgi/gqj5hJYClZ1avtQvLlqMT3uDZkRbi4HhGUpok3DRzv9z7Lti85Kdj3s3/1CeNI0w==
prettier@^2.8.0: prettier@^2.8.0:
version "2.8.8" version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
prettier@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.4.tgz#4723cadeac2ce7c9227de758e5ff9b14e075f283"
integrity sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==
pretty-format@^27.0.2: pretty-format@^27.0.2:
version "27.5.1" version "27.5.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
@ -9079,6 +9292,17 @@ svelte-check@^3.0.1:
svelte-preprocess "^5.1.0" svelte-preprocess "^5.1.0"
typescript "^5.0.3" typescript "^5.0.3"
"svelte-eslint-parser@>=0.33.0 <1.0.0":
version "0.33.1"
resolved "https://registry.yarnpkg.com/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz#c64dbed2fad099577429b3c39377f6b8d36e5d97"
integrity sha512-vo7xPGTlKBGdLH8T5L64FipvTrqv3OQRx9d2z5X05KKZDlF4rQk8KViZO4flKERY+5BiVdOh7zZ7JGJWo5P0uA==
dependencies:
eslint-scope "^7.0.0"
eslint-visitor-keys "^3.0.0"
espree "^9.0.0"
postcss "^8.4.29"
postcss-scss "^4.0.8"
svelte-hmr@^0.15.3: svelte-hmr@^0.15.3:
version "0.15.3" version "0.15.3"
resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.15.3.tgz#df54ccde9be3f091bf5f18fc4ef7b8eb6405fbe6" resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.15.3.tgz#df54ccde9be3f091bf5f18fc4ef7b8eb6405fbe6"
@ -9140,6 +9364,14 @@ synchronous-promise@^2.0.15:
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.17.tgz#38901319632f946c982152586f2caf8ddc25c032" resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.17.tgz#38901319632f946c982152586f2caf8ddc25c032"
integrity sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g== integrity sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==
synckit@^0.8.6:
version "0.8.8"
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7"
integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==
dependencies:
"@pkgr/core" "^0.1.0"
tslib "^2.6.2"
tailwindcss@^3.3.3: tailwindcss@^3.3.3:
version "3.3.6" version "3.3.6"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.6.tgz#4dd7986bf4902ad385d90d45fd4b2fa5fab26d5f" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.6.tgz#4dd7986bf4902ad385d90d45fd4b2fa5fab26d5f"
@ -9327,6 +9559,11 @@ trough@^2.0.0:
resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876"
integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==
ts-api-utils@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331"
integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==
ts-dedent@^2.0.0, ts-dedent@^2.2.0: ts-dedent@^2.0.0, ts-dedent@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
@ -9347,7 +9584,7 @@ tslib@^1.13.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1: tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.6.2:
version "2.6.2" version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
@ -9991,6 +10228,11 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^2.3.4: yaml@^2.3.4:
version "2.3.4" version "2.3.4"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"

Loading…
Cancel
Save