Runabout / .goosehints
9 days ago
sector01tech
a749f174

.goosehints

1
# Project Overview
2

3
This project is a Nostr client application built with React 18.x, TailwindCSS 3.x, Vite, shadcn/ui, and Nostrify.
4

5
## Technology Stack
6

7
- **React 18.x**: Stable version of React with hooks, concurrent rendering, and improved performance
8
- **TailwindCSS 3.x**: Utility-first CSS framework for styling
9
- **Vite**: Fast build tool and development server
10
- **shadcn/ui**: Unstyled, accessible UI components built with Radix UI and Tailwind
11
- **Nostrify**: Nostr protocol framework for Deno and web
12
- **React Router**: For client-side routing with BrowserRouter and ScrollToTop functionality
13
- **TanStack Query**: For data fetching, caching, and state management
14
- **TypeScript**: For type-safe JavaScript development
15

16
## Project Structure
17

18
- `/src/components/`: UI components including NostrProvider for Nostr integration
19
  - `/src/components/ui/`: shadcn/ui components (48+ components available)
20
  - `/src/components/auth/`: Authentication-related components (LoginArea, LoginDialog, etc.)
21
- `/src/hooks/`: Custom hooks including:
22
  - `useNostr`: Core Nostr protocol integration
23
  - `useAuthor`: Fetch user profile data by pubkey
24
  - `useCurrentUser`: Get currently logged-in user
25
  - `useNostrPublish`: Publish events to Nostr
26
  - `useUploadFile`: Upload files via Blossom servers
27
  - `useAppContext`: Access global app configuration
28
  - `useTheme`: Theme management
29
  - `useToast`: Toast notifications
30
  - `useLocalStorage`: Persistent local storage
31
  - `useLoggedInAccounts`: Manage multiple accounts
32
  - `useLoginActions`: Authentication actions
33
  - `useIsMobile`: Responsive design helper
34
- `/src/pages/`: Page components used by React Router (Index, NotFound)
35
- `/src/lib/`: Utility functions and shared logic
36
- `/src/contexts/`: React context providers (AppContext)
37
- `/src/test/`: Testing utilities including TestApp component
38
- `/public/`: Static assets
39
- `App.tsx`: Main app component with provider setup
40
- `AppRouter.tsx`: React Router configuration
41

42
## UI Components
43

44
The project uses shadcn/ui components located in `@/components/ui`. These are unstyled, accessible components built with Radix UI and styled with Tailwind CSS. Available components include:
45

46
- **Accordion**: Vertically collapsing content panels
47
- **Alert**: Displays important messages to users
48
- **AlertDialog**: Modal dialog for critical actions requiring confirmation
49
- **AspectRatio**: Maintains consistent width-to-height ratio
50
- **Avatar**: User profile pictures with fallback support
51
- **Badge**: Small status descriptors for UI elements
52
- **Breadcrumb**: Navigation aid showing current location in hierarchy
53
- **Button**: Customizable button with multiple variants and sizes
54
- **Calendar**: Date picker component
55
- **Card**: Container with header, content, and footer sections
56
- **Carousel**: Slideshow for cycling through elements
57
- **Chart**: Data visualization component
58
- **Checkbox**: Selectable input element
59
- **Collapsible**: Toggle for showing/hiding content
60
- **Command**: Command palette for keyboard-first interfaces
61
- **ContextMenu**: Right-click menu component
62
- **Dialog**: Modal window overlay
63
- **Drawer**: Side-sliding panel (using vaul)
64
- **DropdownMenu**: Menu that appears from a trigger element
65
- **Form**: Form validation and submission handling
66
- **HoverCard**: Card that appears when hovering over an element
67
- **InputOTP**: One-time password input field
68
- **Input**: Text input field
69
- **Label**: Accessible form labels
70
- **Menubar**: Horizontal menu with dropdowns
71
- **NavigationMenu**: Accessible navigation component
72
- **Pagination**: Controls for navigating between pages
73
- **Popover**: Floating content triggered by a button
74
- **Progress**: Progress indicator
75
- **RadioGroup**: Group of radio inputs
76
- **Resizable**: Resizable panels and interfaces
77
- **ScrollArea**: Scrollable container with custom scrollbars
78
- **Select**: Dropdown selection component
79
- **Separator**: Visual divider between content
80
- **Sheet**: Side-anchored dialog component
81
- **Sidebar**: Navigation sidebar component
82
- **Skeleton**: Loading placeholder
83
- **Slider**: Input for selecting a value from a range
84
- **Sonner**: Toast notification manager
85
- **Switch**: Toggle switch control
86
- **Table**: Data table with headers and rows
87
- **Tabs**: Tabbed interface component
88
- **Textarea**: Multi-line text input
89
- **Toast**: Toast notification component
90
- **ToggleGroup**: Group of toggle buttons
91
- **Toggle**: Two-state button
92
- **Tooltip**: Informational text that appears on hover
93

94
These components follow a consistent pattern using React's `forwardRef` and use the `cn()` utility for class name merging. Many are built on Radix UI primitives for accessibility and customized with Tailwind CSS.
95

96
## System Prompt Management
97

98
The AI assistant's behavior and knowledge is defined by the CONTEXT.md file, which serves as the system prompt. To modify the assistant's instructions or add new project-specific guidelines:
99

100
1. Edit CONTEXT.md directly
101
2. The changes take effect in the next session
102

103
## "Vibed with MKStack"
104

105
When building the site for the first time, include "Vibed with MKStack" somewhere in the UI, linked to this URL: https://soapbox.pub/mkstack
106

107
## Nostr Protocol Integration
108

109
This project comes with custom hooks for querying and publishing events on the Nostr network.
110

111
### Nostr Implementation Guidelines
112

113
- Always use the `nostr__read_nips_index` tool before implementing any Nostr features to see what kinds are currently in use across all NIPs.
114
- If any existing kind or NIP might offer the required functionality, use the `nostr__read_nip` tool to investigate thoroughly. Several NIPs may need to be read before making a decision.
115
- Only generate new kind numbers with the `nostr__generate_kind` tool if no existing suitable kinds are found after comprehensive research.
116

117
Knowing when to create a new kind versus reusing an existing kind requires careful judgement. Introducing new kinds means the project won't be interoperable with existing clients. But deviating too far from the schema of a particular kind can cause different interoperability issues.
118

119
#### Choosing Between Existing NIPs and Custom Kinds
120

121
When implementing features that could use existing NIPs, follow this decision framework:
122

123
1. **Thorough NIP Review**: Before considering a new kind, always perform a comprehensive review of existing NIPs and their associated kinds. Use the `nostr__read_nips_index` tool to get an overview, and then `nostr__read_nip` and `nostr__read_kind` to investigate any potentially relevant NIPs or kinds in detail. The goal is to find the closest existing solution.
124

125
2. **Prioritize Existing NIPs**: Always prefer extending or using existing NIPs over creating custom kinds, even if they require minor compromises in functionality.
126

127
3. **Interoperability vs. Perfect Fit**: Consider the trade-off between:
128
   - **Interoperability**: Using existing kinds means compatibility with other Nostr clients
129
   - **Perfect Schema**: Custom kinds allow perfect data modeling but create ecosystem fragmentation
130

131
4. **Extension Strategy**: When existing NIPs are close but not perfect:
132
   - Use the existing kind as the base
133
   - Add domain-specific tags for additional metadata
134
   - Document the extensions in `NIP.md`
135

136
5. **When to Generate Custom Kinds**:
137
   - No existing NIP covers the core functionality
138
   - The data structure is fundamentally different from existing patterns
139
   - The use case requires different storage characteristics (regular vs replaceable vs addressable)
140

141
6. **Custom Kind Publishing**: When publishing events with custom kinds generated by `nostr__generate_kind`, always include a NIP-31 "alt" tag with a human-readable description of the event's purpose.
142

143
**Example Decision Process**:
144
```
145
Need: Equipment marketplace for farmers
146
Options:
147
1. NIP-15 (Marketplace) - Too structured for peer-to-peer sales
148
2. NIP-99 (Classified Listings) - Good fit, can extend with farming tags
149
3. Custom kind - Perfect fit but no interoperability
150

151
Decision: Use NIP-99 + farming-specific tags for best balance
152
```
153

154
#### Tag Design Principles
155

156
When designing tags for Nostr events, follow these principles:
157

158
1. **Kind vs Tags Separation**:
159
   - **Kind** = Schema/structure (how the data is organized)
160
   - **Tags** = Semantics/categories (what the data represents)
161
   - Don't create different kinds for the same data structure
162

163
2. **Use Single-Letter Tags for Categories**:
164
   - **Relays only index single-letter tags** for efficient querying
165
   - Use `t` tags for categorization, not custom multi-letter tags
166
   - Multiple `t` tags allow items to belong to multiple categories
167

168
3. **Relay-Level Filtering**:
169
   - Design tags to enable efficient relay-level filtering with `#t: ["category"]`
170
   - Avoid client-side filtering when relay-level filtering is possible
171
   - Consider query patterns when designing tag structure
172

173
4. **Tag Examples**:
174
   ```json
175
   // ❌ Wrong: Multi-letter tag, not queryable at relay level
176
   ["product_type", "electronics"]
177

178
   // ✅ Correct: Single-letter tag, relay-indexed and queryable
179
   ["t", "electronics"]
180
   ["t", "smartphone"]
181
   ["t", "android"]
182
   ```
183

184
5. **Querying Best Practices**:
185
   ```typescript
186
   // ❌ Inefficient: Get all events, filter in JavaScript
187
   const events = await nostr.query([{ kinds: [30402] }]);
188
   const filtered = events.filter(e => hasTag(e, 'product_type', 'electronics'));
189

190
   // ✅ Efficient: Filter at relay level
191
   const events = await nostr.query([{ kinds: [30402], '#t': ['electronics'] }]);
192
   ```
193

194
#### `t` Tag Filtering for Community-Specific Content
195

196
For applications focused on a specific community or niche, you can use `t` tags to filter events for the target audience.
197

198
**When to Use:**
199
- ✅ Community apps: "farmers" → `t: "farming"`, "Poland" → `t: "poland"`
200
- ❌ Generic platforms: Twitter clones, general Nostr clients
201

202
**Implementation:**
203
```typescript
204
// Publishing with community tag
205
createEvent({
206
  kind: 1,
207
  content: data.content,
208
  tags: [['t', 'farming']]
209
});
210

211
// Querying community content
212
const events = await nostr.query([{
213
  kinds: [1],
214
  '#t': ['farming'],
215
  limit: 20
216
}], { signal });
217
```
218

219
### Kind Ranges
220

221
An event's kind number determines the event's behavior and storage characteristics:
222

223
- **Regular Events** (1000 ≤ kind < 10000): Expected to be stored by relays permanently. Used for persistent content like notes, articles, etc.
224
- **Replaceable Events** (10000 ≤ kind < 20000): Only the latest event per pubkey+kind combination is stored. Used for profile metadata, contact lists, etc.
225
- **Addressable Events** (30000 ≤ kind < 40000): Identified by pubkey+kind+d-tag combination, only latest per combination is stored. Used for articles, long-form content, etc.
226

227
Kinds below 1000 are considered "legacy" kinds, and may have different storage characteristics based on their kind definition. For example, kind 1 is regular, while kind 3 is replaceable.
228

229
### Content Field Design Principles
230

231
When designing new event kinds, the `content` field should be used for semantically important data that doesn't need to be queried by relays. **Structured JSON data generally shouldn't go in the content field** (kind 0 being an early exception).
232

233
#### Guidelines
234

235
- **Use content for**: Large text, freeform human-readable content, or existing industry-standard JSON formats (Tiled maps, FHIR, GeoJSON)
236
- **Use tags for**: Queryable metadata, structured data, anything that needs relay-level filtering
237
- **Empty content is valid**: Many events need only tags with `content: ""`
238
- **Relays only index tags**: If you need to filter by a field, it must be a tag
239

240
#### Example
241

242
**✅ Good - queryable data in tags:**
243
```json
244
{
245
  "kind": 30402,
246
  "content": "",
247
  "tags": [["d", "product-123"], ["title", "Camera"], ["price", "250"], ["t", "photography"]]
248
}
249
```
250

251
**❌ Bad - structured data in content:**
252
```json
253
{
254
  "kind": 30402,
255
  "content": "{\"title\":\"Camera\",\"price\":250,\"category\":\"photo\"}",
256
  "tags": [["d", "product-123"]]
257
}
258
```
259

260
### NIP.md
261

262
The file `NIP.md` is used by this project to define a custom Nostr protocol document. If the file doesn't exist, it means this project doesn't have any custom kinds associated with it.
263

264
Whenever new kinds are generated, the `NIP.md` file in the project must be created or updated to document the custom event schema. Whenever the schema of one of these custom events changes, `NIP.md` must also be updated accordingly.
265

266
### The `useNostr` Hook
267

268
The `useNostr` hook returns an object containing a `nostr` property, with `.query()` and `.event()` methods for querying and publishing Nostr events respectively.
269

270
```typescript
271
import { useNostr } from '@nostrify/react';
272

273
function useCustomHook() {
274
  const { nostr } = useNostr();
275

276
  // ...
277
}
278
```
279

280
### Query Nostr Data with `useNostr` and Tanstack Query
281

282
When querying Nostr, the best practice is to create custom hooks that combine `useNostr` and `useQuery` to get the required data.
283

284
```typescript
285
import { useNostr } from '@nostrify/react';
286
import { useQuery } from '@tanstack/query';
287

288
function usePosts() {
289
  const { nostr } = useNostr();
290

291
  return useQuery({
292
    queryKey: ['posts'],
293
    queryFn: async (c) => {
294
      const signal = AbortSignal.any([c.signal, AbortSignal.timeout(1500)]);
295
      const events = await nostr.query([{ kinds: [1], limit: 20 }], { signal });
296
      return events; // these events could be transformed into another format
297
    },
298
  });
299
}
300
```
301

302
#### Efficient Query Design
303

304
**Critical**: Always minimize the number of separate queries to avoid rate limiting and improve performance. Combine related queries whenever possible.
305

306
**✅ Efficient - Single query with multiple kinds:**
307
```typescript
308
// Query multiple event types in one request
309
const events = await nostr.query([
310
  {
311
    kinds: [1, 6, 16], // All repost kinds in one query
312
    '#e': [eventId],
313
    limit: 150,
314
  }
315
], { signal });
316

317
// Separate by type in JavaScript
318
const notes = events.filter((e) => e.kind === 1);
319
const reposts = events.filter((e) => e.kind === 6);
320
const genericReposts = events.filter((e) => e.kind === 16);
321
```
322

323
**❌ Inefficient - Multiple separate queries:**
324
```typescript
325
// This creates unnecessary load and can trigger rate limiting
326
const [notes, reposts, genericReposts] = await Promise.all([
327
  nostr.query([{ kinds: [1], '#e': [eventId] }], { signal }),
328
  nostr.query([{ kinds: [6], '#e': [eventId] }], { signal }),
329
  nostr.query([{ kinds: [16], '#e': [eventId] }], { signal }),
330
]);
331
```
332

333
**Query Optimization Guidelines:**
334
1. **Combine kinds**: Use `kinds: [1, 6, 16]` instead of separate queries
335
2. **Use multiple filters**: When you need different tag filters, use multiple filter objects in a single query
336
3. **Adjust limits**: When combining queries, increase the limit appropriately
337
4. **Filter in JavaScript**: Separate event types after receiving results rather than making multiple requests
338
5. **Consider relay capacity**: Each query consumes relay resources and may count against rate limits
339

340
The data may be transformed into a more appropriate format if needed, and multiple calls to `nostr.query()` may be made in a single queryFn.
341

342
#### Event Validation
343

344
When querying events, if the event kind being returned has required tags or required JSON fields in the content, the events should be filtered through a validator function. This is not generally needed for kinds such as 1, where all tags are optional and the content is freeform text, but is especially useful for custom kinds as well as kinds with strict requirements.
345

346
```typescript
347
// Example validator function for NIP-52 calendar events
348
function validateCalendarEvent(event: NostrEvent): boolean {
349
  // Check if it's a calendar event kind
350
  if (![31922, 31923].includes(event.kind)) return false;
351

352
  // Check for required tags according to NIP-52
353
  const d = event.tags.find(([name]) => name === 'd')?.[1];
354
  const title = event.tags.find(([name]) => name === 'title')?.[1];
355
  const start = event.tags.find(([name]) => name === 'start')?.[1];
356

357
  // All calendar events require 'd', 'title', and 'start' tags
358
  if (!d || !title || !start) return false;
359

360
  // Additional validation for date-based events (kind 31922)
361
  if (event.kind === 31922) {
362
    // start tag should be in YYYY-MM-DD format for date-based events
363
    const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
364
    if (!dateRegex.test(start)) return false;
365
  }
366

367
  // Additional validation for time-based events (kind 31923)
368
  if (event.kind === 31923) {
369
    // start tag should be a unix timestamp for time-based events
370
    const timestamp = parseInt(start);
371
    if (isNaN(timestamp) || timestamp <= 0) return false;
372
  }
373

374
  return true;
375
}
376

377
function useCalendarEvents() {
378
  const { nostr } = useNostr();
379

380
  return useQuery({
381
    queryKey: ['calendar-events'],
382
    queryFn: async (c) => {
383
      const signal = AbortSignal.any([c.signal, AbortSignal.timeout(1500)]);
384
      const events = await nostr.query([{ kinds: [31922, 31923], limit: 20 }], { signal });
385

386
      // Filter events through validator to ensure they meet NIP-52 requirements
387
      return events.filter(validateCalendarEvent);
388
    },
389
  });
390
}
391
```
392

393
### The `useAuthor` Hook
394

395
To display profile data for a user by their Nostr pubkey (such as an event author), use the `useAuthor` hook.
396

397
```tsx
398
import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify';
399
import { useAuthor } from '@/hooks/useAuthor';
400
import { genUserName } from '@/lib/genUserName';
401

402
function Post({ event }: { event: NostrEvent }) {
403
  const author = useAuthor(event.pubkey);
404
  const metadata: NostrMetadata | undefined = author.data?.metadata;
405

406
  const displayName = metadata?.name ?? genUserName(event.pubkey);
407
  const profileImage = metadata?.picture;
408

409
  // ...render elements with this data
410
}
411
```
412

413
#### `NostrMetadata` type
414

415
```ts
416
/** Kind 0 metadata. */
417
interface NostrMetadata {
418
  /** A short description of the user. */
419
  about?: string;
420
  /** A URL to a wide (~1024x768) picture to be optionally displayed in the background of a profile screen. */
421
  banner?: string;
422
  /** A boolean to clarify that the content is entirely or partially the result of automation, such as with chatbots or newsfeeds. */
423
  bot?: boolean;
424
  /** An alternative, bigger name with richer characters than `name`. `name` should always be set regardless of the presence of `display_name` in the metadata. */
425
  display_name?: string;
426
  /** A bech32 lightning address according to NIP-57 and LNURL specifications. */
427
  lud06?: string;
428
  /** An email-like lightning address according to NIP-57 and LNURL specifications. */
429
  lud16?: string;
430
  /** A short name to be displayed for the user. */
431
  name?: string;
432
  /** An email-like Nostr address according to NIP-05. */
433
  nip05?: string;
434
  /** A URL to the user's avatar. */
435
  picture?: string;
436
  /** A web URL related in any way to the event author. */
437
  website?: string;
438
}
439
```
440

441
### The `useNostrPublish` Hook
442

443
To publish events, use the `useNostrPublish` hook in this project. This hook automatically adds a "client" tag to published events.
444

445
```tsx
446
import { useState } from 'react';
447

448
import { useCurrentUser } from "@/hooks/useCurrentUser";
449
import { useNostrPublish } from '@/hooks/useNostrPublish';
450

451
export function MyComponent() {
452
  const [ data, setData] = useState<Record<string, string>>({});
453

454
  const { user } = useCurrentUser();
455
  const { mutate: createEvent } = useNostrPublish();
456

457
  const handleSubmit = () => {
458
    createEvent({ kind: 1, content: data.content });
459
  };
460

461
  if (!user) {
462
    return <span>You must be logged in to use this form.</span>;
463
  }
464

465
  return (
466
    <form onSubmit={handleSubmit} disabled={!user}>
467
      {/* ...some input fields */}
468
    </form>
469
  );
470
}
471
```
472

473
The `useCurrentUser` hook should be used to ensure that the user is logged in before they are able to publish Nostr events.
474

475
### Nostr Login
476

477
To enable login with Nostr, simply use the `LoginArea` component already included in this project.
478

479
```tsx
480
import { LoginArea } from "@/components/auth/LoginArea";
481

482
function MyComponent() {
483
  return (
484
    <div>
485
      {/* other components ... */}
486

487
      <LoginArea className="max-w-60" />
488
    </div>
489
  );
490
}
491
```
492

493
The `LoginArea` component handles all the login-related UI and interactions, including displaying login dialogs, sign up functionality, and switching between accounts. It should not be wrapped in any conditional logic.
494

495
`LoginArea` displays both "Log in" and "Sign Up" buttons when the user is logged out, and changes to an account switcher once the user is logged in. It is an inline-flex element by default. To make it expand to the width of its container, you can pass a className like `flex` (to make it a block element) or `w-full`. If it is left as inline-flex, it's recommended to set a max width.
496

497
### `npub`, `naddr`, and other Nostr addresses
498

499
Nostr defines a set of bech32-encoded identifiers in NIP-19. Their prefixes and purposes:
500

501
- `npub1`: **public keys** - Just the 32-byte public key, no additional metadata
502
- `nsec1`: **private keys** - Secret keys (should never be displayed publicly)  
503
- `note1`: **event IDs** - Just the 32-byte event ID (hex), no additional metadata
504
- `nevent1`: **event pointers** - Event ID plus optional relay hints and author pubkey
505
- `nprofile1`: **profile pointers** - Public key plus optional relay hints and petname
506
- `naddr1`: **addressable event coordinates** - For parameterized replaceable events (kind 30000-39999)
507
- `nrelay1`: **relay references** - Relay URLs (deprecated)
508

509
#### Key Differences Between Similar Identifiers
510

511
**`note1` vs `nevent1`:**
512
- `note1`: Contains only the event ID (32 bytes) - specifically for kind:1 events (Short Text Notes) as defined in NIP-10
513
- `nevent1`: Contains event ID plus optional relay hints and author pubkey - for any event kind
514
- Use `note1` for simple references to text notes and threads
515
- Use `nevent1` when you need to include relay hints or author context for any event type
516

517
**`npub1` vs `nprofile1`:**
518
- `npub1`: Contains only the public key (32 bytes)
519
- `nprofile1`: Contains public key plus optional relay hints and petname
520
- Use `npub1` for simple user references
521
- Use `nprofile1` when you need to include relay hints or display name context
522

523
#### NIP-19 Routing Implementation
524

525
**Critical**: NIP-19 identifiers should be handled at the **root level** of URLs (e.g., `/note1...`, `/npub1...`, `/naddr1...`), NOT nested under paths like `/note/note1...` or `/profile/npub1...`.
526

527
This project includes a boilerplate `NIP19Page` component that provides the foundation for handling all NIP-19 identifier types at the root level. The component is configured in the routing system and ready for AI agents to populate with specific functionality.
528

529
**How it works:**
530

531
1. **Root-Level Route**: The route `/:nip19` in `AppRouter.tsx` catches all NIP-19 identifiers
532
2. **Automatic Decoding**: The `NIP19Page` component automatically decodes the identifier using `nip19.decode()`
533
3. **Type-Specific Sections**: Different sections are rendered based on the identifier type:
534
   - `npub1`/`nprofile1`: Profile section with placeholder for profile view
535
   - `note1`: Note section with placeholder for kind:1 text note view
536
   - `nevent1`: Event section with placeholder for any event type view
537
   - `naddr1`: Addressable event section with placeholder for articles, marketplace items, etc.
538
4. **Error Handling**: Invalid, vacant, or unsupported identifiers show 404 NotFound page
539
5. **Ready for Population**: Each section includes comments indicating where AI agents should implement specific functionality
540

541
**Example URLs that work automatically:**
542
- `/npub1abc123...` - User profile (needs implementation)
543
- `/note1def456...` - Kind:1 text note (needs implementation)
544
- `/nevent1ghi789...` - Any event with relay hints (needs implementation)
545
- `/naddr1jkl012...` - Addressable event (needs implementation)
546

547
**Features included:**
548
- Basic NIP-19 identifier decoding and routing
549
- Type-specific sections for different identifier types
550
- Error handling for invalid identifiers
551
- Responsive container structure
552
- Comments indicating where to implement specific views
553

554
**Error handling:**
555
- Invalid NIP-19 format → 404 NotFound
556
- Unsupported identifier types (like `nsec1`) → 404 NotFound  
557
- Empty or missing identifiers → 404 NotFound
558

559
To implement NIP-19 routing in your Nostr application:
560

561
1. **The NIP19Page boilerplate is already created** - populate sections with specific functionality
562
2. **The route is already configured** in `AppRouter.tsx`
563
3. **Error handling is built-in** - all edge cases show appropriate 404 responses
564
4. **Add specific components** for profile views, event displays, etc. as needed
565

566
#### Event Type Distinctions
567

568
**`note1` identifiers** are specifically for **kind:1 events** (Short Text Notes) as defined in NIP-10: "Text Notes and Threads". These are the basic social media posts in Nostr.
569

570
**`nevent1` identifiers** can reference any event kind and include additional metadata like relay hints and author pubkey. Use `nevent1` when:
571
- The event is not a kind:1 text note
572
- You need to include relay hints for better discoverability
573
- You want to include author context
574

575
#### Use in Filters
576

577
The base Nostr protocol uses hex string identifiers when filtering by event IDs and pubkeys. Nostr filters only accept hex strings.
578

579
```ts
580
// ❌ Wrong: naddr is not decoded
581
const events = await nostr.query(
582
  [{ ids: [naddr] }],
583
  { signal }
584
);
585
```
586

587
Corrected example:
588

589
```ts
590
// Import nip19 from nostr-tools
591
import { nip19 } from 'nostr-tools';
592

593
// Decode a NIP-19 identifier
594
const decoded = nip19.decode(value);
595

596
// Optional: guard certain types (depending on the use-case)
597
if (decoded.type !== 'naddr') {
598
  throw new Error('Unsupported Nostr identifier');
599
}
600

601
// Get the addr object
602
const naddr = decoded.data;
603

604
// ✅ Correct: naddr is expanded into the correct filter
605
const events = await nostr.query(
606
  [{
607
    kinds: [naddr.kind],
608
    authors: [naddr.pubkey],
609
    '#d': [naddr.identifier],
610
  }],
611
  { signal }
612
);
613
```
614

615
#### Implementation Guidelines
616

617
1. **Always decode NIP-19 identifiers** before using them in queries
618
2. **Use the appropriate identifier type** based on your needs:
619
   - Use `note1` for kind:1 text notes specifically
620
   - Use `nevent1` when including relay hints or for non-kind:1 events
621
   - Use `naddr1` for addressable events (always includes author pubkey for security)
622
3. **Handle different identifier types** appropriately:
623
   - `npub1`/`nprofile1`: Display user profiles
624
   - `note1`: Display kind:1 text notes specifically
625
   - `nevent1`: Display any event with optional relay context
626
   - `naddr1`: Display addressable events (articles, marketplace items, etc.)
627
4. **Security considerations**: Always use `naddr1` for addressable events instead of just the `d` tag value, as `naddr1` contains the author pubkey needed to create secure filters
628
5. **Error handling**: Gracefully handle invalid or unsupported NIP-19 identifiers with 404 responses
629

630
### Nostr Edit Profile
631

632
To include an Edit Profile form, place the `EditProfileForm` component in the project:
633

634
```tsx
635
import { EditProfileForm } from "@/components/EditProfileForm";
636

637
function EditProfilePage() {
638
  return (
639
    <div>
640
      {/* you may want to wrap this in a layout or include other components depending on the project ... */}
641

642
      <EditProfileForm />
643
    </div>
644
  );
645
}
646
```
647

648
The `EditProfileForm` component displays just the form. It requires no props, and will "just work" automatically.
649

650
### Uploading Files on Nostr
651

652
Use the `useUploadFile` hook to upload files. This hook uses Blossom servers for file storage and returns NIP-94 compatible tags.
653

654
```tsx
655
import { useUploadFile } from "@/hooks/useUploadFile";
656

657
function MyComponent() {
658
  const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
659

660
  const handleUpload = async (file: File) => {
661
    try {
662
      // Provides an array of NIP-94 compatible tags
663
      // The first tag in the array contains the URL
664
      const [[_, url]] = await uploadFile(file);
665
      // ...use the url
666
    } catch (error) {
667
      // ...handle errors
668
    }
669
  };
670

671
  // ...rest of component
672
}
673
```
674

675
To attach files to kind 1 events, each file's URL should be appended to the event's `content`, and an `imeta` tag should be added for each file. For kind 0 events, the URL by itself can be used in relevant fields of the JSON content.
676

677
### Nostr Encryption and Decryption
678

679
The logged-in user has a `signer` object (matching the NIP-07 signer interface) that can be used for encryption and decryption. The signer's nip44 methods handle all cryptographic operations internally, including key derivation and conversation key management, so you never need direct access to private keys. Always use the signer interface for encryption rather than requesting private keys from users, as this maintains security and follows best practices.
680

681
```ts
682
// Get the current user
683
const { user } = useCurrentUser();
684

685
// Optional guard to check that nip44 is available
686
if (!user.signer.nip44) {
687
  throw new Error("Please upgrade your signer extension to a version that supports NIP-44 encryption");
688
}
689

690
// Encrypt message to self
691
const encrypted = await user.signer.nip44.encrypt(user.pubkey, "hello world");
692
// Decrypt message to self
693
const decrypted = await user.signer.nip44.decrypt(user.pubkey, encrypted) // "hello world"
694
```
695

696
### Rendering Rich Text Content
697

698
Nostr text notes (kind 1, 11, and 1111) have a plaintext `content` field that may contain URLs, hashtags, and Nostr URIs. These events should render their content using the `NoteContent` component:
699

700
```tsx
701
import { NoteContent } from "@/components/NoteContent";
702

703
export function Post(/* ...props */) {
704
  // ...
705

706
  return (
707
    <CardContent className="pb-2">
708
      <div className="whitespace-pre-wrap break-words">
709
        <NoteContent event={post} className="text-sm" />
710
      </div>
711
    </CardContent>
712
  );
713
}
714
```
715

716
### Adding Comments Sections
717

718
The project includes a complete commenting system using NIP-22 (kind 1111) comments that can be added to any Nostr event or URL. The `CommentsSection` component provides a full-featured commenting interface with threaded replies, user authentication, and real-time updates.
719

720
#### Basic Usage
721

722
```tsx
723
import { CommentsSection } from "@/components/comments/CommentsSection";
724

725
function ArticlePage({ article }: { article: NostrEvent }) {
726
  return (
727
    <div className="space-y-6">
728
      {/* Your article content */}
729
      <div>{/* article content */}</div>
730

731
      {/* Comments section */}
732
      <CommentsSection root={article} />
733
    </div>
734
  );
735
}
736
```
737

738
#### Props and Customization
739

740
The `CommentsSection` component accepts the following props:
741

742
- **`root`** (required): The root event or URL to comment on. Can be a `NostrEvent` or `URL` object.
743
- **`title`**: Custom title for the comments section (default: "Comments")
744
- **`emptyStateMessage`**: Message shown when no comments exist (default: "No comments yet")
745
- **`emptyStateSubtitle`**: Subtitle for empty state (default: "Be the first to share your thoughts!")
746
- **`className`**: Additional CSS classes for styling
747
- **`limit`**: Maximum number of comments to load (default: 500)
748

749
```tsx
750
<CommentsSection
751
  root={event}
752
  title="Discussion"
753
  emptyStateMessage="Start the conversation"
754
  emptyStateSubtitle="Share your thoughts about this post"
755
  className="mt-8"
756
  limit={100}
757
/>
758
```
759

760
#### Commenting on URLs
761

762
The comments system also supports commenting on external URLs, making it useful for web pages, articles, or any online content:
763

764
```tsx
765
<CommentsSection
766
  root={new URL("https://example.com/article")}
767
  title="Comments on this article"
768
/>
769
```
770

771
## App Configuration
772

773
The project includes an `AppProvider` that manages global application state including theme and relay configuration. The default configuration includes:
774

775
```typescript
776
const defaultConfig: AppConfig = {
777
  theme: "light",
778
  relayUrl: "wss://relay.nostr.band",
779
};
780
```
781

782
Preset relays are available including Ditto, Nostr.Band, Damus, and Primal. The app uses local storage to persist user preferences.
783

784
## Routing
785

786
The project uses React Router with a centralized routing configuration in `AppRouter.tsx`. To add new routes:
787

788
1. Create your page component in `/src/pages/`
789
2. Import it in `AppRouter.tsx`
790
3. Add the route above the catch-all `*` route:
791

792
```tsx
793
<Route path="/your-path" element={<YourComponent />} />
794
```
795

796
The router includes automatic scroll-to-top functionality and a 404 NotFound page for unmatched routes.
797

798
## Development Practices
799

800
- Uses React Query for data fetching and caching
801
- Follows shadcn/ui component patterns
802
- Implements Path Aliases with `@/` prefix for cleaner imports
803
- Uses Vite for fast development and production builds
804
- Component-based architecture with React hooks
805
- Default connection to one Nostr relay for best performance
806
- Comprehensive provider setup with NostrLoginProvider, QueryClientProvider, and custom AppProvider
807
- **Never use the `any` type**: Always use proper TypeScript types for type safety
808

809
## Loading States
810

811
**Use skeleton loading** for structured content (feeds, profiles, forms). **Use spinners** only for buttons or short operations.
812

813
```tsx
814
// Skeleton example matching component structure
815
<Card>
816
  <CardHeader>
817
    <div className="flex items-center space-x-3">
818
      <Skeleton className="h-10 w-10 rounded-full" />
819
      <div className="space-y-1">
820
        <Skeleton className="h-4 w-24" />
821
        <Skeleton className="h-3 w-16" />
822
      </div>
823
    </div>
824
  </CardHeader>
825
  <CardContent>
826
    <div className="space-y-2">
827
      <Skeleton className="h-4 w-full" />
828
      <Skeleton className="h-4 w-4/5" />
829
    </div>
830
  </CardContent>
831
</Card>
832
```
833

834
### Empty States and No Content Found
835

836
When no content is found (empty search results, no data available, etc.), display a minimalist empty state with the `RelaySelector` component. This allows users to easily switch relays to discover content from different sources.
837

838
```tsx
839
import { RelaySelector } from '@/components/RelaySelector';
840
import { Card, CardContent } from '@/components/ui/card';
841

842
// Empty state example
843
<div className="col-span-full">
844
  <Card className="border-dashed">
845
    <CardContent className="py-12 px-8 text-center">
846
      <div className="max-w-sm mx-auto space-y-6">
847
        <p className="text-muted-foreground">
848
          No results found. Try another relay?
849
        </p>
850
        <RelaySelector className="w-full" />
851
      </div>
852
    </CardContent>
853
  </Card>
854
</div>
855
```
856

857
## Design Customization
858

859
**Tailor the site's look and feel based on the user's specific request.** This includes:
860

861
- **Color schemes**: Incorporate the user's color preferences when specified, and choose an appropriate scheme that matches the application's purpose and aesthetic
862
- **Typography**: Choose fonts that match the requested aesthetic (modern, elegant, playful, etc.)
863
- **Layout**: Follow the requested structure (3-column, sidebar, grid, etc.)
864
- **Component styling**: Use appropriate border radius, shadows, and spacing for the desired feel
865
- **Interactive elements**: Style buttons, forms, and hover states to match the theme
866

867
### Adding Fonts
868

869
To add custom fonts, follow these steps:
870

871
1. **Install a font package** using the `js-dev__npm_add_package` tool:
872

873
   **Any Google Font can be installed** using the @fontsource packages. Examples:
874
   - For Inter Variable: `js-dev__npm_add_package({ name: "@fontsource-variable/inter" })`
875
   - For Roboto: `js-dev__npm_add_package({ name: "@fontsource/roboto" })`
876
   - For Outfit Variable: `js-dev__npm_add_package({ name: "@fontsource-variable/outfit" })`
877
   - For Poppins: `js-dev__npm_add_package({ name: "@fontsource/poppins" })`
878
   - For Open Sans: `js-dev__npm_add_package({ name: "@fontsource/open-sans" })`
879

880
   **Format**: `@fontsource/[font-name]` or `@fontsource-variable/[font-name]` (for variable fonts)
881

882
2. **Import the font** in `src/main.tsx`:
883
   ```typescript
884
   import '@fontsource-variable/<font-name>';
885
   ```
886

887
3. **Update Tailwind configuration** in `tailwind.config.ts`:
888
   ```typescript
889
   export default {
890
     theme: {
891
       extend: {
892
         fontFamily: {
893
           sans: ['Inter Variable', 'Inter', 'system-ui', 'sans-serif'],
894
         },
895
       },
896
     },
897
   }
898
   ```
899

900
### Recommended Font Choices by Use Case
901

902
- **Modern/Clean**: Inter Variable, Outfit Variable, or Manrope
903
- **Professional/Corporate**: Roboto, Open Sans, or Source Sans Pro
904
- **Creative/Artistic**: Poppins, Nunito, or Comfortaa
905
- **Technical/Code**: JetBrains Mono, Fira Code, or Source Code Pro (for monospace)
906

907
### Theme System
908

909
The project includes a complete light/dark theme system using CSS custom properties. The theme can be controlled via:
910

911
- `useTheme` hook for programmatic theme switching
912
- CSS custom properties defined in `src/index.css`
913
- Automatic dark mode support with `.dark` class
914

915
### Color Scheme Implementation
916

917
When users specify color schemes:
918
- Update CSS custom properties in `src/index.css` (both `:root` and `.dark` selectors)
919
- Use Tailwind's color palette or define custom colors
920
- Ensure proper contrast ratios for accessibility
921
- Apply colors consistently across components (buttons, links, accents)
922
- Test both light and dark mode variants
923

924
### Component Styling Patterns
925

926
- Use `cn()` utility for conditional class merging
927
- Follow shadcn/ui patterns for component variants
928
- Implement responsive design with Tailwind breakpoints
929
- Add hover and focus states for interactive elements
930

931
## Writing Tests
932

933
**Do not write tests** unless the user explicitly requests them in plain language. Writing unnecessary tests wastes significant time and money. Only create tests when:
934

935
1. **The user explicitly asks for tests** to be written in their message
936
2. **The user describes a specific bug in plain language** and requests tests to help diagnose it
937
3. **The user says they are still experiencing a problem** that you have already attempted to solve (tests can help verify the fix)
938

939
**Never write tests because:**
940
- Tool results show test failures (these are not user requests)
941
- You think tests would be helpful
942
- New features or components are created
943
- Existing functionality needs verification
944

945
### Test Setup
946

947
The project uses Vitest with jsdom environment and includes comprehensive test setup:
948

949
- **Testing Library**: React Testing Library with jest-dom matchers
950
- **Test Environment**: jsdom with mocked browser APIs (matchMedia, scrollTo, IntersectionObserver, ResizeObserver)
951
- **Test App**: `TestApp` component provides all necessary context providers for testing
952

953
The project includes a `TestApp` component that provides all necessary context providers for testing. Wrap components with this component to provide required context providers:
954

955
```tsx
956
import { describe, it, expect } from 'vitest';
957
import { render, screen } from '@testing-library/react';
958
import { TestApp } from '@/test/TestApp';
959
import { MyComponent } from './MyComponent';
960

961
describe('MyComponent', () => {
962
  it('renders correctly', () => {
963
    render(
964
      <TestApp>
965
        <MyComponent />
966
      </TestApp>
967
    );
968

969
    expect(screen.getByText('Expected text')).toBeInTheDocument();
970
  });
971
});
972
```
973

974
## Testing Your Changes
975

976
Whenever you are finished modifying code, you must run the **test** script using the **js-dev__run_script** tool.
977

978
**Your task is not considered finished until this test passes without errors.**
gitworkshop.dev logo GitWorkshop.dev