Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 29 additions & 26 deletions src/lib/components/BountyCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,34 @@
};
</script>

<article
<a
href={`/bounties/${bounty.id}`}
class="rounded-xl border border-surface-600 bg-surface-800 p-5 transition-colors hover:border-bitcoin-500/60"
>
<div class="flex items-center justify-between gap-2">
<span
class="rounded-full border px-2 py-0.5 text-xs font-medium {statusBadge[
bounty.status
].classes}"
>
{statusBadge[bounty.status].label}
</span>
<span class="text-lg font-bold text-bitcoin-400">
{formatSats(bounty.amountSats)}
</span>
</div>
<h2 class="mt-3 text-base font-semibold text-gray-100">{bounty.title}</h2>
{#if bounty.tags.length > 0}
<ul class="mt-3 flex flex-wrap gap-1.5">
{#each bounty.tags as tag (tag)}
<li
class="rounded-md border border-surface-600 bg-surface-700 px-2 py-0.5 text-xs text-gray-400"
>
#{tag}
</li>
{/each}
</ul>
{/if}
</article>
<article>
<div class="flex items-center justify-between gap-2">
<span
class="rounded-full border px-2 py-0.5 text-xs font-medium {statusBadge[
bounty.status
].classes}"
>
{statusBadge[bounty.status].label}
</span>
<span class="text-lg font-bold text-bitcoin-400">
{formatSats(bounty.amountSats)}
</span>
</div>
<h2 class="mt-3 text-base font-semibold text-gray-100">{bounty.title}</h2>
{#if bounty.tags.length > 0}
<ul class="mt-3 flex flex-wrap gap-1.5">
{#each bounty.tags as tag (tag)}
<li
class="rounded-md border border-surface-600 bg-surface-700 px-2 py-0.5 text-xs text-gray-400"
>
#{tag}
</li>
{/each}
</ul>
{/if}
</article>
</a>
149 changes: 149 additions & 0 deletions src/lib/mock/bounties.ts
Original file line number Diff line number Diff line change
@@ -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<Bounty[]> { ... }
* export async function getBounty(id: string): Promise<Bounty | null> { ... }
*/

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)}`;
}
23 changes: 23 additions & 0 deletions src/lib/types/bounty.ts
Original file line number Diff line number Diff line change
@@ -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;
}
28 changes: 6 additions & 22 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,27 +1,11 @@
<script lang="ts">
import BountyCard from '$lib/components/BountyCard.svelte';
import type { Bounty } from '$lib/types/bounty';
import { getBounties } from '$lib/mock/bounties';

const bounties: Bounty[] = [
{
title: 'Add NIP-57 zap receipts to the bounty feed',
amountSats: 250000,
status: 'open',
tags: ['typescript', 'nostr']
},
{
title: 'Escrow state machine for Arkade multiparty flows',
amountSats: 1200000,
status: 'open',
tags: ['bitcoin', 'escrow']
},
{
title: 'Dark-mode landing page skeleton',
amountSats: 50000,
status: 'claimed',
tags: []
}
];
// TODO: Replace with NDK-based fetching:
// import { getBounties } from '$lib/services/bounties';

const bounties = getBounties();
</script>

<svelte:head>
Expand All @@ -34,7 +18,7 @@
Software bounties organized on Nostr, paid in Bitcoin.
</p>
<div class="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each bounties as bounty (bounty.title)}
{#each bounties as bounty (bounty.id)}
<BountyCard {bounty} />
{/each}
</div>
Expand Down
Loading