From 1ad1df2664b4a9592e49bf37c0afe05e6d4f921e Mon Sep 17 00:00:00 2001
From: spherical-spinach <48479421+spherical-spinach@users.noreply.github.com>
Date: Thu, 6 Aug 2026 16:14:48 +0300
Subject: [PATCH 1/3] feat: add single bounty page UI with mock data
---
src/lib/components/BountyCard.svelte | 56 ++--
src/lib/mock/bounties.ts | 149 ++++++++++
src/lib/types/bounty.ts | 23 ++
src/routes/+page.svelte | 28 +-
src/routes/bounties/[id]/+page.svelte | 389 ++++++++++++++++++++++++++
5 files changed, 597 insertions(+), 48 deletions(-)
create mode 100644 src/lib/mock/bounties.ts
create mode 100644 src/routes/bounties/[id]/+page.svelte
diff --git a/src/lib/components/BountyCard.svelte b/src/lib/components/BountyCard.svelte
index 2b8399d..8c0a5d5 100644
--- a/src/lib/components/BountyCard.svelte
+++ b/src/lib/components/BountyCard.svelte
@@ -1,5 +1,6 @@
-
-
-
- {statusBadge[bounty.status].label}
-
-
- {formatSats(bounty.amountSats)}
-
-
- {bounty.title}
- {#if bounty.tags.length > 0}
-
- {#each bounty.tags as tag (tag)}
- -
- #{tag}
-
- {/each}
-
- {/if}
-
+
+
+
+ {statusBadge[bounty.status].label}
+
+
+ {formatSats(bounty.amountSats)}
+
+
+ {bounty.title}
+ {#if bounty.tags.length > 0}
+
+ {#each bounty.tags as tag (tag)}
+ -
+ #{tag}
+
+ {/each}
+
+ {/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..6784034
--- /dev/null
+++ b/src/routes/bounties/[id]/+page.svelte
@@ -0,0 +1,389 @@
+
+
+
+ {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}
+

+ {/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}
+

+ {/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}
+

+ {/if}
+
{replyProfile?.displayName ??
+ shortPubkey(reply.pubkey)}
+
·
+
{timeAgo(reply.createdAt)}
+
+
{reply.content}
+
+ {/each}
+
+ {/each}
+
+ {/if}
+
+
+
+ {#if isLoggedIn}
+
+
+
+
+
+
+
+ {:else}
+
+ Login with Nostr
+ to join the discussion.
+
+ {/if}
+
+
+
+{/if}
+
+
From a6b22ee1cf157229315b8faa0d10a93e255f5936 Mon Sep 17 00:00:00 2001
From: spherical-spinach <48479421+spherical-spinach@users.noreply.github.com>
Date: Tue, 11 Aug 2026 20:10:44 +0300
Subject: [PATCH 2/3] fix: disable Nostr login links
---
src/routes/bounties/[id]/+page.svelte | 26 ++++++++++++++------------
1 file changed, 14 insertions(+), 12 deletions(-)
diff --git a/src/routes/bounties/[id]/+page.svelte b/src/routes/bounties/[id]/+page.svelte
index 6784034..059c071 100644
--- a/src/routes/bounties/[id]/+page.svelte
+++ b/src/routes/bounties/[id]/+page.svelte
@@ -218,10 +218,8 @@
{#if !isLoggedIn}
- Login with NostrLogin with Nostr
to accept this bounty.
@@ -334,14 +332,18 @@
{:else}
-
- Login with Nostr
- to join the discussion.
-
+ {#if bounty.status === 'claimed'}
+
+ Bounty is claimed and the discussion has been closed.
+
+ {:else}
+
+ Login with Nostr
+ to join the discussion.
+
+ {/if}
{/if}
From 29ddc8df31a75174679b0881c28ab0795428aec3 Mon Sep 17 00:00:00 2001
From: spherical-spinach <48479421+spherical-spinach@users.noreply.github.com>
Date: Tue, 11 Aug 2026 20:27:12 +0300
Subject: [PATCH 3/3] fix: remove unnecessary remove() in href
---
src/lib/components/BountyCard.svelte | 3 +--
src/routes/bounties/[id]/+page.svelte | 5 ++---
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/lib/components/BountyCard.svelte b/src/lib/components/BountyCard.svelte
index 8c0a5d5..f6c824d 100644
--- a/src/lib/components/BountyCard.svelte
+++ b/src/lib/components/BountyCard.svelte
@@ -1,6 +1,5 @@
diff --git a/src/routes/bounties/[id]/+page.svelte b/src/routes/bounties/[id]/+page.svelte
index 059c071..3ebfa03 100644
--- a/src/routes/bounties/[id]/+page.svelte
+++ b/src/routes/bounties/[id]/+page.svelte
@@ -1,6 +1,5 @@