diff --git a/src/lib/components/BountyCard.svelte b/src/lib/components/BountyCard.svelte index 2b8399d..f6c824d 100644 --- a/src/lib/components/BountyCard.svelte +++ b/src/lib/components/BountyCard.svelte @@ -33,31 +33,34 @@ }; -
-
- - {statusBadge[bounty.status].label} - - - {formatSats(bounty.amountSats)} - -
-

{bounty.title}

- {#if bounty.tags.length > 0} - - {/if} -
+
+
+ + {statusBadge[bounty.status].label} + + + {formatSats(bounty.amountSats)} + +
+

{bounty.title}

+ {#if bounty.tags.length > 0} + + {/if} +
+ diff --git a/src/lib/mock/bounties.ts b/src/lib/mock/bounties.ts new file mode 100644 index 0000000..e797a48 --- /dev/null +++ b/src/lib/mock/bounties.ts @@ -0,0 +1,149 @@ +import type { Bounty, BountyComment, NostrProfile } from '$lib/types/bounty'; + +/** + * Mock bounties — single source of truth for development. + * + * When NDK integration is ready, replace this module with real fetches: + * export async function getBounties(): Promise { ... } + * export async function getBounty(id: string): Promise { ... } + */ + +export const mockBounties: Bounty[] = [ + { + id: '1', + title: 'Add NIP-57 zap receipts to the bounty feed', + description: `## Overview +Implement NIP-57 zap receipt parsing so that the bounty feed shows how much has been zapped to each bounty. + +## Requirements +- [ ] Parse kind 9735 events from relay +- [ ] Display zap total on BountyCard +- [ ] Show individual zap receipts on bounty detail page +- [x] Read the NIP-57 spec + +## Notes +- Use the existing NDK instance from \`$lib/ndk.ts\` +- Bolt11 decoding can use the lightningpay library`, + amountSats: 250000, + status: 'open', + tags: ['typescript', 'nostr'], + makerPubkey: 'npub1alice00000000000000000000000000000000000000000000000000', + createdAt: Math.floor(Date.now() / 1000) - 86400 * 2, // 2 days ago + resolutionMode: 'A', + checkInIntervalDays: 7 + }, + { + id: '2', + title: 'Escrow state machine for Arkade multiparty flows', + description: `## Overview +Design and implement a state machine that manages escrow transitions for multi-party Bitcoin transactions using Arkade. + +## Requirements +- [ ] Define state transitions (funded → locked → released / disputed) +- [ ] Integrate with Arkade SDK for VTXO management +- [ ] Emit Nostr events for each state change +- [ ] Handle timeout / expiry gracefully + +## Acceptance Criteria +- All state transitions are tested with unit tests +- Integration test with regtest Ark server passes`, + amountSats: 1200000, + status: 'in-dispute', + tags: ['bitcoin', 'escrow', 'arkade'], + makerPubkey: 'npub1bob0000000000000000000000000000000000000000000000000000', + createdAt: Math.floor(Date.now() / 1000) - 86400 * 5, // 5 days ago + resolutionMode: 'B', + checkInIntervalDays: 3 + }, + { + id: '3', + title: 'Dark-mode landing page skeleton', + description: `## Overview +Create a responsive dark-mode landing page skeleton using Tailwind CSS. + +## Requirements +- [x] Hero section with tagline +- [x] Feature grid (3 columns on desktop) +- [x] Footer with links +- [x] Mobile responsive + +Completed and claimed!`, + amountSats: 50000, + status: 'claimed', + tags: ['design', 'tailwind'], + makerPubkey: 'npub1alice00000000000000000000000000000000000000000000000000', + createdAt: Math.floor(Date.now() / 1000) - 86400 * 14, // 14 days ago + resolutionMode: 'A', + checkInIntervalDays: 0 + } +]; + +export const mockComments: BountyComment[] = [ + { + id: 'comment-1', + bountyId: '1', + pubkey: 'npub1bob0000000000000000000000000000000000000000000000000000', + content: + 'Is there a preference on which bolt11 decoding library to use? I was thinking of using light-bolt11-decoder.', + createdAt: Math.floor(Date.now() / 1000) - 3600 * 12, + replyToId: null + }, + { + id: 'comment-2', + bountyId: '1', + pubkey: 'npub1alice00000000000000000000000000000000000000000000000000', + content: 'Yes, light-bolt11-decoder works great. Go for it!', + createdAt: Math.floor(Date.now() / 1000) - 3600 * 6, + replyToId: 'comment-1' + }, + { + id: 'comment-3', + bountyId: '2', + pubkey: 'npub1charlie000000000000000000000000000000000000000000000000', + content: + 'I have experience with Ark. Happy to take this on. Will submit a proposal by end of week.', + createdAt: Math.floor(Date.now() / 1000) - 86400, + replyToId: null + } +]; + +export const mockProfiles: NostrProfile[] = [ + { + pubkey: 'npub1alice00000000000000000000000000000000000000000000000000', + displayName: 'Alice', + picture: undefined + }, + { + pubkey: 'npub1bob0000000000000000000000000000000000000000000000000000', + displayName: 'Bob', + picture: undefined + }, + { + pubkey: 'npub1charlie000000000000000000000000000000000000000000000000', + displayName: 'Charlie', + picture: undefined + } +]; + +// --- Accessor functions (easy to swap with async NDK versions later) --- + +export function getBounties(): Bounty[] { + return mockBounties; +} + +export function getBounty(id: string): Bounty | null { + return mockBounties.find((b) => b.id === id) ?? null; +} + +export function getComments(bountyId: string): BountyComment[] { + return mockComments.filter((c) => c.bountyId === bountyId); +} + +export function getProfile(pubkey: string): NostrProfile | null { + return mockProfiles.find((p) => p.pubkey === pubkey) ?? null; +} + +export function shortPubkey(pubkey: string): string { + if (pubkey.length <= 16) return pubkey; + return `${pubkey.slice(0, 8)}…${pubkey.slice(-4)}`; +} diff --git a/src/lib/types/bounty.ts b/src/lib/types/bounty.ts index dbacfa5..628f41a 100644 --- a/src/lib/types/bounty.ts +++ b/src/lib/types/bounty.ts @@ -1,9 +1,32 @@ export type BountyStatus = 'open' | 'in-progress' | 'in-dispute' | 'claimed' | 'cancelled'; +export type ResolutionMode = 'A' | 'B'; + export interface Bounty { + id: string; title: string; + description: string; amountSats: number; status: BountyStatus; tags: string[]; + makerPubkey: string; + createdAt: number; // unix timestamp + resolutionMode: ResolutionMode; + checkInIntervalDays: number; +} + +export interface BountyComment { + id: string; + bountyId: string; + pubkey: string; + content: string; + createdAt: number; // unix timestamp + replyToId: string | null; +} + +export interface NostrProfile { + pubkey: string; + displayName: string; + picture?: string; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3f51e88..07f9c9e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,27 +1,11 @@ @@ -34,7 +18,7 @@ Software bounties organized on Nostr, paid in Bitcoin.

- {#each bounties as bounty (bounty.title)} + {#each bounties as bounty (bounty.id)} {/each}
diff --git a/src/routes/bounties/[id]/+page.svelte b/src/routes/bounties/[id]/+page.svelte new file mode 100644 index 0000000..3ebfa03 --- /dev/null +++ b/src/routes/bounties/[id]/+page.svelte @@ -0,0 +1,390 @@ + + + + {bounty?.title ?? 'Bounty'} — Satcode + + +{#if !bounty} +
+

Bounty not found

+ ← Back to bounties +
+{:else} + {@const makerProfile = getProfile(bounty.makerPubkey)} +
+ + + + + + All bounties + + + +
+
+
+ {statusLabel[bounty.status]} + {#if bounty.resolutionMode === 'B'} + + Mode B · Oracle + + {:else} + + Mode A · Juror + + {/if} +
+ {formatSats(bounty.amountSats)} +
+ +

{bounty.title}

+ + +
+
+ {#if makerProfile?.picture} + {makerProfile.displayName + {/if} + Posted by {makerProfile?.displayName ?? + shortPubkey(bounty.makerPubkey)} +
+ · + {timeAgo(bounty.createdAt)} + {#if bounty.checkInIntervalDays > 0} + · + Check-in every {bounty.checkInIntervalDays}d + {/if} +
+ + + {#if bounty.tags.length > 0} +
+ {#each bounty.tags as tag (tag)} + + #{tag} + + {/each} +
+ {/if} + + +
+ {#each bounty.description.split('\n') as line, index (index)} + {#if line.startsWith('## ')} +

{line.slice(3)}

+ {:else if line.startsWith('# ')} +

{line.slice(2)}

+ {:else if line.startsWith('- [ ] ')} +
☐ {line.slice(6)}
+ {:else if line.startsWith('- [x] ')} +
☑ {line.slice(6)}
+ {:else if line.startsWith('- ')} +
{line.slice(2)}
+ {:else if line.trim() === ''} +
+ {:else} +

{line}

+ {/if} + {/each} +
+ + + {#if bounty.status === 'in-dispute'} +
+
+ + + +
+

+ This bounty is in dispute +

+

+ A 5-juror panel has been selected randomly. Evidence has been + submitted and votes are being tallied. +

+
+
+
+ {/if} + + + {#if bounty.status === 'open'} +
+ + {#if !isLoggedIn} +

+ Login with Nostr + to accept this bounty. +

+ {/if} +
+ {/if} +
+ + +
+

+ Discussion + {#if comments.length > 0} + ({comments.length}) + {/if} +

+ + {#if comments.length === 0} + {#if bounty.status === 'claimed'} +

+ There is no comments in this discussion. +

+ {:else} +

+ No comments yet. Be the first to ask a question. +

+ {/if} + {:else} +
+ {#each comments.filter((c) => !c.replyToId) as comment (comment.id)} + {@const commentProfile = getProfile(comment.pubkey)} +
+
+ {#if commentProfile?.picture} + {commentProfile.displayName + {/if} + {commentProfile?.displayName ?? + shortPubkey(comment.pubkey)} + · + {timeAgo(comment.createdAt)} +
+

{comment.content}

+ + + {#each comments.filter((r) => r.replyToId === comment.id) as reply (reply.id)} + {@const replyProfile = getProfile(reply.pubkey)} +
+
+ {#if replyProfile?.picture} + {replyProfile.displayName + {/if} + {replyProfile?.displayName ?? + shortPubkey(reply.pubkey)} + · + {timeAgo(reply.createdAt)} +
+

{reply.content}

+
+ {/each} +
+ {/each} +
+ {/if} + + +
+ {#if isLoggedIn} +
+ + +
+ +
+
+ {:else} + {#if bounty.status === 'claimed'} +

+ Bounty is claimed and the discussion has been closed. +

+ {:else} +

+ Login with Nostr + to join the discussion. +

+ {/if} + {/if} +
+
+
+{/if} + +