-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path.cursorrules
More file actions
991 lines (760 loc) · 39.2 KB
/
.cursorrules
File metadata and controls
991 lines (760 loc) · 39.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
# Project Overview
This project is a Nostr client application built with React 18.x, TailwindCSS 3.x, Vite, shadcn/ui, and Nostrify.
## Technology Stack
- **React 18.x**: Stable version of React with hooks, concurrent rendering, and improved performance
- **TailwindCSS 3.x**: Utility-first CSS framework for styling
- **Vite**: Fast build tool and development server
- **shadcn/ui**: Unstyled, accessible UI components built with Radix UI and Tailwind
- **Nostrify**: Nostr protocol framework for Deno and web
- **React Router**: For client-side routing with BrowserRouter and ScrollToTop functionality
- **TanStack Query**: For data fetching, caching, and state management
- **TypeScript**: For type-safe JavaScript development
## Project Structure
- `/src/components/`: UI components including NostrProvider for Nostr integration
- `/src/components/ui/`: shadcn/ui components (48+ components available)
- `/src/components/auth/`: Authentication-related components (LoginArea, LoginDialog, etc.)
- `/src/hooks/`: Custom hooks including:
- `useNostr`: Core Nostr protocol integration
- `useAuthor`: Fetch user profile data by pubkey
- `useCurrentUser`: Get currently logged-in user
- `useNostrPublish`: Publish events to Nostr
- `useUploadFile`: Upload files via Blossom servers
- `useAppContext`: Access global app configuration
- `useTheme`: Theme management
- `useToast`: Toast notifications
- `useLocalStorage`: Persistent local storage
- `useLoggedInAccounts`: Manage multiple accounts
- `useLoginActions`: Authentication actions
- `useIsMobile`: Responsive design helper
- `/src/pages/`: Page components used by React Router (Index, NotFound)
- `/src/lib/`: Utility functions and shared logic
- `/src/contexts/`: React context providers (AppContext)
- `/src/test/`: Testing utilities including TestApp component
- `/public/`: Static assets
- `App.tsx`: Main app component with provider setup
- `AppRouter.tsx`: React Router configuration
## UI Components
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:
- **Accordion**: Vertically collapsing content panels
- **Alert**: Displays important messages to users
- **AlertDialog**: Modal dialog for critical actions requiring confirmation
- **AspectRatio**: Maintains consistent width-to-height ratio
- **Avatar**: User profile pictures with fallback support
- **Badge**: Small status descriptors for UI elements
- **Breadcrumb**: Navigation aid showing current location in hierarchy
- **Button**: Customizable button with multiple variants and sizes
- **Calendar**: Date picker component
- **Card**: Container with header, content, and footer sections
- **Carousel**: Slideshow for cycling through elements
- **Chart**: Data visualization component
- **Checkbox**: Selectable input element
- **Collapsible**: Toggle for showing/hiding content
- **Command**: Command palette for keyboard-first interfaces
- **ContextMenu**: Right-click menu component
- **Dialog**: Modal window overlay
- **Drawer**: Side-sliding panel (using vaul)
- **DropdownMenu**: Menu that appears from a trigger element
- **Form**: Form validation and submission handling
- **HoverCard**: Card that appears when hovering over an element
- **InputOTP**: One-time password input field
- **Input**: Text input field
- **Label**: Accessible form labels
- **Menubar**: Horizontal menu with dropdowns
- **NavigationMenu**: Accessible navigation component
- **Pagination**: Controls for navigating between pages
- **Popover**: Floating content triggered by a button
- **Progress**: Progress indicator
- **RadioGroup**: Group of radio inputs
- **Resizable**: Resizable panels and interfaces
- **ScrollArea**: Scrollable container with custom scrollbars
- **Select**: Dropdown selection component
- **Separator**: Visual divider between content
- **Sheet**: Side-anchored dialog component
- **Sidebar**: Navigation sidebar component
- **Skeleton**: Loading placeholder
- **Slider**: Input for selecting a value from a range
- **Sonner**: Toast notification manager
- **Switch**: Toggle switch control
- **Table**: Data table with headers and rows
- **Tabs**: Tabbed interface component
- **Textarea**: Multi-line text input
- **Toast**: Toast notification component
- **ToggleGroup**: Group of toggle buttons
- **Toggle**: Two-state button
- **Tooltip**: Informational text that appears on hover
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.
## System Prompt Management
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:
1. Edit CONTEXT.md directly
2. The changes take effect in the next session
## "Vibed with MKStack"
When building the site for the first time, include "Vibed with MKStack" somewhere in the UI, linked to this URL: https://soapbox.pub/mkstack
## Nostr Protocol Integration
This project comes with custom hooks for querying and publishing events on the Nostr network.
### Nostr Implementation Guidelines
- Always use the `nostr__read_nips_index` tool before implementing any Nostr features to see what kinds are currently in use across all NIPs.
- 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.
- Only generate new kind numbers with the `nostr__generate_kind` tool if no existing suitable kinds are found after comprehensive research.
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.
#### Choosing Between Existing NIPs and Custom Kinds
When implementing features that could use existing NIPs, follow this decision framework:
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.
2. **Prioritize Existing NIPs**: Always prefer extending or using existing NIPs over creating custom kinds, even if they require minor compromises in functionality.
3. **Interoperability vs. Perfect Fit**: Consider the trade-off between:
- **Interoperability**: Using existing kinds means compatibility with other Nostr clients
- **Perfect Schema**: Custom kinds allow perfect data modeling but create ecosystem fragmentation
4. **Extension Strategy**: When existing NIPs are close but not perfect:
- Use the existing kind as the base
- Add domain-specific tags for additional metadata
- Document the extensions in `NIP.md`
5. **When to Generate Custom Kinds**:
- No existing NIP covers the core functionality
- The data structure is fundamentally different from existing patterns
- The use case requires different storage characteristics (regular vs replaceable vs addressable)
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.
**Example Decision Process**:
```
Need: Equipment marketplace for farmers
Options:
1. NIP-15 (Marketplace) - Too structured for peer-to-peer sales
2. NIP-99 (Classified Listings) - Good fit, can extend with farming tags
3. Custom kind - Perfect fit but no interoperability
Decision: Use NIP-99 + farming-specific tags for best balance
```
#### Tag Design Principles
When designing tags for Nostr events, follow these principles:
1. **Kind vs Tags Separation**:
- **Kind** = Schema/structure (how the data is organized)
- **Tags** = Semantics/categories (what the data represents)
- Don't create different kinds for the same data structure
2. **Use Single-Letter Tags for Categories**:
- **Relays only index single-letter tags** for efficient querying
- Use `t` tags for categorization, not custom multi-letter tags
- Multiple `t` tags allow items to belong to multiple categories
3. **Relay-Level Filtering**:
- Design tags to enable efficient relay-level filtering with `#t: ["category"]`
- Avoid client-side filtering when relay-level filtering is possible
- Consider query patterns when designing tag structure
4. **Tag Examples**:
```json
// ❌ Wrong: Multi-letter tag, not queryable at relay level
["product_type", "electronics"]
// ✅ Correct: Single-letter tag, relay-indexed and queryable
["t", "electronics"]
["t", "smartphone"]
["t", "android"]
```
5. **Querying Best Practices**:
```typescript
// ❌ Inefficient: Get all events, filter in JavaScript
const events = await nostr.query([{ kinds: [30402] }]);
const filtered = events.filter(e => hasTag(e, 'product_type', 'electronics'));
// ✅ Efficient: Filter at relay level
const events = await nostr.query([{ kinds: [30402], '#t': ['electronics'] }]);
```
#### `t` Tag Filtering for Community-Specific Content
For applications focused on a specific community or niche, you can use `t` tags to filter events for the target audience.
**When to Use:**
- ✅ Community apps: "farmers" → `t: "farming"`, "Poland" → `t: "poland"`
- ❌ Generic platforms: Twitter clones, general Nostr clients
**Implementation:**
```typescript
// Publishing with community tag
createEvent({
kind: 1,
content: data.content,
tags: [['t', 'farming']]
});
// Querying community content
const events = await nostr.query([{
kinds: [1],
'#t': ['farming'],
limit: 20
}], { signal });
```
### Kind Ranges
An event's kind number determines the event's behavior and storage characteristics:
- **Regular Events** (1000 ≤ kind < 10000): Expected to be stored by relays permanently. Used for persistent content like notes, articles, etc.
- **Replaceable Events** (10000 ≤ kind < 20000): Only the latest event per pubkey+kind combination is stored. Used for profile metadata, contact lists, etc.
- **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.
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.
### Content Field Design Principles
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).
#### Guidelines
- **Use content for**: Large text, freeform human-readable content, or existing industry-standard JSON formats (Tiled maps, FHIR, GeoJSON)
- **Use tags for**: Queryable metadata, structured data, anything that needs relay-level filtering
- **Empty content is valid**: Many events need only tags with `content: ""`
- **Relays only index tags**: If you need to filter by a field, it must be a tag
#### Example
**✅ Good - queryable data in tags:**
```json
{
"kind": 30402,
"content": "",
"tags": [["d", "product-123"], ["title", "Camera"], ["price", "250"], ["t", "photography"]]
}
```
**❌ Bad - structured data in content:**
```json
{
"kind": 30402,
"content": "{\"title\":\"Camera\",\"price\":250,\"category\":\"photo\"}",
"tags": [["d", "product-123"]]
}
```
### NIP.md
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.
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.
### The `useNostr` Hook
The `useNostr` hook returns an object containing a `nostr` property, with `.query()` and `.event()` methods for querying and publishing Nostr events respectively.
```typescript
import { useNostr } from '@nostrify/react';
function useCustomHook() {
const { nostr } = useNostr();
// ...
}
```
### Query Nostr Data with `useNostr` and Tanstack Query
When querying Nostr, the best practice is to create custom hooks that combine `useNostr` and `useQuery` to get the required data.
```typescript
import { useNostr } from '@nostrify/react';
import { useQuery } from '@tanstack/query';
function usePosts() {
const { nostr } = useNostr();
return useQuery({
queryKey: ['posts'],
queryFn: async (c) => {
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(1500)]);
const events = await nostr.query([{ kinds: [1], limit: 20 }], { signal });
return events; // these events could be transformed into another format
},
});
}
```
#### Efficient Query Design
**Critical**: Always minimize the number of separate queries to avoid rate limiting and improve performance. Combine related queries whenever possible.
**✅ Efficient - Single query with multiple kinds:**
```typescript
// Query multiple event types in one request
const events = await nostr.query([
{
kinds: [1, 6, 16], // All repost kinds in one query
'#e': [eventId],
limit: 150,
}
], { signal });
// Separate by type in JavaScript
const notes = events.filter((e) => e.kind === 1);
const reposts = events.filter((e) => e.kind === 6);
const genericReposts = events.filter((e) => e.kind === 16);
```
**❌ Inefficient - Multiple separate queries:**
```typescript
// This creates unnecessary load and can trigger rate limiting
const [notes, reposts, genericReposts] = await Promise.all([
nostr.query([{ kinds: [1], '#e': [eventId] }], { signal }),
nostr.query([{ kinds: [6], '#e': [eventId] }], { signal }),
nostr.query([{ kinds: [16], '#e': [eventId] }], { signal }),
]);
```
**Query Optimization Guidelines:**
1. **Combine kinds**: Use `kinds: [1, 6, 16]` instead of separate queries
2. **Use multiple filters**: When you need different tag filters, use multiple filter objects in a single query
3. **Adjust limits**: When combining queries, increase the limit appropriately
4. **Filter in JavaScript**: Separate event types after receiving results rather than making multiple requests
5. **Consider relay capacity**: Each query consumes relay resources and may count against rate limits
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.
#### Event Validation
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.
```typescript
// Example validator function for NIP-52 calendar events
function validateCalendarEvent(event: NostrEvent): boolean {
// Check if it's a calendar event kind
if (![31922, 31923].includes(event.kind)) return false;
// Check for required tags according to NIP-52
const d = event.tags.find(([name]) => name === 'd')?.[1];
const title = event.tags.find(([name]) => name === 'title')?.[1];
const start = event.tags.find(([name]) => name === 'start')?.[1];
// All calendar events require 'd', 'title', and 'start' tags
if (!d || !title || !start) return false;
// Additional validation for date-based events (kind 31922)
if (event.kind === 31922) {
// start tag should be in YYYY-MM-DD format for date-based events
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(start)) return false;
}
// Additional validation for time-based events (kind 31923)
if (event.kind === 31923) {
// start tag should be a unix timestamp for time-based events
const timestamp = parseInt(start);
if (isNaN(timestamp) || timestamp <= 0) return false;
}
return true;
}
function useCalendarEvents() {
const { nostr } = useNostr();
return useQuery({
queryKey: ['calendar-events'],
queryFn: async (c) => {
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(1500)]);
const events = await nostr.query([{ kinds: [31922, 31923], limit: 20 }], { signal });
// Filter events through validator to ensure they meet NIP-52 requirements
return events.filter(validateCalendarEvent);
},
});
}
```
### The `useAuthor` Hook
To display profile data for a user by their Nostr pubkey (such as an event author), use the `useAuthor` hook.
```tsx
import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
function Post({ event }: { event: NostrEvent }) {
const author = useAuthor(event.pubkey);
const metadata: NostrMetadata | undefined = author.data?.metadata;
const displayName = metadata?.name ?? genUserName(event.pubkey);
const profileImage = metadata?.picture;
// ...render elements with this data
}
```
#### `NostrMetadata` type
```ts
/** Kind 0 metadata. */
interface NostrMetadata {
/** A short description of the user. */
about?: string;
/** A URL to a wide (~1024x768) picture to be optionally displayed in the background of a profile screen. */
banner?: string;
/** A boolean to clarify that the content is entirely or partially the result of automation, such as with chatbots or newsfeeds. */
bot?: boolean;
/** An alternative, bigger name with richer characters than `name`. `name` should always be set regardless of the presence of `display_name` in the metadata. */
display_name?: string;
/** A bech32 lightning address according to NIP-57 and LNURL specifications. */
lud06?: string;
/** An email-like lightning address according to NIP-57 and LNURL specifications. */
lud16?: string;
/** A short name to be displayed for the user. */
name?: string;
/** An email-like Nostr address according to NIP-05. */
nip05?: string;
/** A URL to the user's avatar. */
picture?: string;
/** A web URL related in any way to the event author. */
website?: string;
}
```
### The `useNostrPublish` Hook
To publish events, use the `useNostrPublish` hook in this project. This hook automatically adds a "client" tag to published events.
```tsx
import { useState } from 'react';
import { useCurrentUser } from "@/hooks/useCurrentUser";
import { useNostrPublish } from '@/hooks/useNostrPublish';
export function MyComponent() {
const [ data, setData] = useState<Record<string, string>>({});
const { user } = useCurrentUser();
const { mutate: createEvent } = useNostrPublish();
const handleSubmit = () => {
createEvent({ kind: 1, content: data.content });
};
if (!user) {
return <span>You must be logged in to use this form.</span>;
}
return (
<form onSubmit={handleSubmit} disabled={!user}>
{/* ...some input fields */}
</form>
);
}
```
The `useCurrentUser` hook should be used to ensure that the user is logged in before they are able to publish Nostr events.
### Nostr Login
To enable login with Nostr, simply use the `LoginArea` component already included in this project.
```tsx
import { LoginArea } from "@/components/auth/LoginArea";
function MyComponent() {
return (
<div>
{/* other components ... */}
<LoginArea className="max-w-60" />
</div>
);
}
```
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.
`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.
### `npub`, `naddr`, and other Nostr addresses
Nostr defines a set of bech32-encoded identifiers in NIP-19. Their prefixes and purposes:
- `npub1`: **public keys** - Just the 32-byte public key, no additional metadata
- `nsec1`: **private keys** - Secret keys (should never be displayed publicly)
- `note1`: **event IDs** - Just the 32-byte event ID (hex), no additional metadata
- `nevent1`: **event pointers** - Event ID plus optional relay hints and author pubkey
- `nprofile1`: **profile pointers** - Public key plus optional relay hints and petname
- `naddr1`: **addressable event coordinates** - For parameterized replaceable events (kind 30000-39999)
- `nrelay1`: **relay references** - Relay URLs (deprecated)
#### Key Differences Between Similar Identifiers
**`note1` vs `nevent1`:**
- `note1`: Contains only the event ID (32 bytes) - specifically for kind:1 events (Short Text Notes) as defined in NIP-10
- `nevent1`: Contains event ID plus optional relay hints and author pubkey - for any event kind
- Use `note1` for simple references to text notes and threads
- Use `nevent1` when you need to include relay hints or author context for any event type
**`npub1` vs `nprofile1`:**
- `npub1`: Contains only the public key (32 bytes)
- `nprofile1`: Contains public key plus optional relay hints and petname
- Use `npub1` for simple user references
- Use `nprofile1` when you need to include relay hints or display name context
#### NIP-19 Routing Implementation
**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...`.
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.
**How it works:**
1. **Root-Level Route**: The route `/:nip19` in `AppRouter.tsx` catches all NIP-19 identifiers
2. **Automatic Decoding**: The `NIP19Page` component automatically decodes the identifier using `nip19.decode()`
3. **Type-Specific Sections**: Different sections are rendered based on the identifier type:
- `npub1`/`nprofile1`: Profile section with placeholder for profile view
- `note1`: Note section with placeholder for kind:1 text note view
- `nevent1`: Event section with placeholder for any event type view
- `naddr1`: Addressable event section with placeholder for articles, marketplace items, etc.
4. **Error Handling**: Invalid, vacant, or unsupported identifiers show 404 NotFound page
5. **Ready for Population**: Each section includes comments indicating where AI agents should implement specific functionality
**Example URLs that work automatically:**
- `/npub1abc123...` - User profile (needs implementation)
- `/note1def456...` - Kind:1 text note (needs implementation)
- `/nevent1ghi789...` - Any event with relay hints (needs implementation)
- `/naddr1jkl012...` - Addressable event (needs implementation)
**Features included:**
- Basic NIP-19 identifier decoding and routing
- Type-specific sections for different identifier types
- Error handling for invalid identifiers
- Responsive container structure
- Comments indicating where to implement specific views
**Error handling:**
- Invalid NIP-19 format → 404 NotFound
- Unsupported identifier types (like `nsec1`) → 404 NotFound
- Empty or missing identifiers → 404 NotFound
To implement NIP-19 routing in your Nostr application:
1. **The NIP19Page boilerplate is already created** - populate sections with specific functionality
2. **The route is already configured** in `AppRouter.tsx`
3. **Error handling is built-in** - all edge cases show appropriate 404 responses
4. **Add specific components** for profile views, event displays, etc. as needed
#### Event Type Distinctions
**`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.
**`nevent1` identifiers** can reference any event kind and include additional metadata like relay hints and author pubkey. Use `nevent1` when:
- The event is not a kind:1 text note
- You need to include relay hints for better discoverability
- You want to include author context
#### Use in Filters
The base Nostr protocol uses hex string identifiers when filtering by event IDs and pubkeys. Nostr filters only accept hex strings.
```ts
// ❌ Wrong: naddr is not decoded
const events = await nostr.query(
[{ ids: [naddr] }],
{ signal }
);
```
Corrected example:
```ts
// Import nip19 from nostr-tools
import { nip19 } from 'nostr-tools';
// Decode a NIP-19 identifier
const decoded = nip19.decode(value);
// Optional: guard certain types (depending on the use-case)
if (decoded.type !== 'naddr') {
throw new Error('Unsupported Nostr identifier');
}
// Get the addr object
const naddr = decoded.data;
// ✅ Correct: naddr is expanded into the correct filter
const events = await nostr.query(
[{
kinds: [naddr.kind],
authors: [naddr.pubkey],
'#d': [naddr.identifier],
}],
{ signal }
);
```
#### Implementation Guidelines
1. **Always decode NIP-19 identifiers** before using them in queries
2. **Use the appropriate identifier type** based on your needs:
- Use `note1` for kind:1 text notes specifically
- Use `nevent1` when including relay hints or for non-kind:1 events
- Use `naddr1` for addressable events (always includes author pubkey for security)
3. **Handle different identifier types** appropriately:
- `npub1`/`nprofile1`: Display user profiles
- `note1`: Display kind:1 text notes specifically
- `nevent1`: Display any event with optional relay context
- `naddr1`: Display addressable events (articles, marketplace items, etc.)
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
5. **Error handling**: Gracefully handle invalid or unsupported NIP-19 identifiers with 404 responses
### Nostr Edit Profile
To include an Edit Profile form, place the `EditProfileForm` component in the project:
```tsx
import { EditProfileForm } from "@/components/EditProfileForm";
function EditProfilePage() {
return (
<div>
{/* you may want to wrap this in a layout or include other components depending on the project ... */}
<EditProfileForm />
</div>
);
}
```
The `EditProfileForm` component displays just the form. It requires no props, and will "just work" automatically.
### Uploading Files on Nostr
Use the `useUploadFile` hook to upload files. This hook uses Blossom servers for file storage and returns NIP-94 compatible tags.
```tsx
import { useUploadFile } from "@/hooks/useUploadFile";
function MyComponent() {
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const handleUpload = async (file: File) => {
try {
// Provides an array of NIP-94 compatible tags
// The first tag in the array contains the URL
const [[_, url]] = await uploadFile(file);
// ...use the url
} catch (error) {
// ...handle errors
}
};
// ...rest of component
}
```
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.
### Nostr Encryption and Decryption
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.
```ts
// Get the current user
const { user } = useCurrentUser();
// Optional guard to check that nip44 is available
if (!user.signer.nip44) {
throw new Error("Please upgrade your signer extension to a version that supports NIP-44 encryption");
}
// Encrypt message to self
const encrypted = await user.signer.nip44.encrypt(user.pubkey, "hello world");
// Decrypt message to self
const decrypted = await user.signer.nip44.decrypt(user.pubkey, encrypted) // "hello world"
```
### Rendering Rich Text Content
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:
```tsx
import { NoteContent } from "@/components/NoteContent";
export function Post(/* ...props */) {
// ...
return (
<CardContent className="pb-2">
<div className="whitespace-pre-wrap break-words">
<NoteContent event={post} className="text-sm" />
</div>
</CardContent>
);
}
```
### Adding Comments Sections
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.
#### Basic Usage
```tsx
import { CommentsSection } from "@/components/comments/CommentsSection";
function ArticlePage({ article }: { article: NostrEvent }) {
return (
<div className="space-y-6">
{/* Your article content */}
<div>{/* article content */}</div>
{/* Comments section */}
<CommentsSection root={article} />
</div>
);
}
```
#### Props and Customization
The `CommentsSection` component accepts the following props:
- **`root`** (required): The root event or URL to comment on. Can be a `NostrEvent` or `URL` object.
- **`title`**: Custom title for the comments section (default: "Comments")
- **`emptyStateMessage`**: Message shown when no comments exist (default: "No comments yet")
- **`emptyStateSubtitle`**: Subtitle for empty state (default: "Be the first to share your thoughts!")
- **`className`**: Additional CSS classes for styling
- **`limit`**: Maximum number of comments to load (default: 500)
```tsx
<CommentsSection
root={event}
title="Discussion"
emptyStateMessage="Start the conversation"
emptyStateSubtitle="Share your thoughts about this post"
className="mt-8"
limit={100}
/>
```
#### Commenting on URLs
The comments system also supports commenting on external URLs, making it useful for web pages, articles, or any online content:
```tsx
<CommentsSection
root={new URL("https://example.com/article")}
title="Comments on this article"
/>
```
## App Configuration
The project includes an `AppProvider` that manages global application state including theme and relay configuration. The default configuration includes:
```typescript
const defaultConfig: AppConfig = {
theme: "light",
relayUrl: "wss://relay.nostr.band",
};
```
Preset relays are available including Ditto, Nostr.Band, Damus, and Primal. The app uses local storage to persist user preferences.
## Routing
The project uses React Router with a centralized routing configuration in `AppRouter.tsx`. To add new routes:
1. Create your page component in `/src/pages/`
2. Import it in `AppRouter.tsx`
3. Add the route above the catch-all `*` route:
```tsx
<Route path="/your-path" element={<YourComponent />} />
```
The router includes automatic scroll-to-top functionality and a 404 NotFound page for unmatched routes.
## Development Practices
- Uses React Query for data fetching and caching
- Follows shadcn/ui component patterns
- Implements Path Aliases with `@/` prefix for cleaner imports
- Uses Vite for fast development and production builds
- Component-based architecture with React hooks
- Default connection to one Nostr relay for best performance
- Comprehensive provider setup with NostrLoginProvider, QueryClientProvider, and custom AppProvider
- **Never use the `any` type**: Always use proper TypeScript types for type safety
## Loading States
**Use skeleton loading** for structured content (feeds, profiles, forms). **Use spinners** only for buttons or short operations.
```tsx
// Skeleton example matching component structure
<Card>
<CardHeader>
<div className="flex items-center space-x-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-1">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-16" />
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-4/5" />
</div>
</CardContent>
</Card>
```
### Empty States and No Content Found
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.
```tsx
import { RelaySelector } from '@/components/RelaySelector';
import { Card, CardContent } from '@/components/ui/card';
// Empty state example
<div className="col-span-full">
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center">
<div className="max-w-sm mx-auto space-y-6">
<p className="text-muted-foreground">
No results found. Try another relay?
</p>
<RelaySelector className="w-full" />
</div>
</CardContent>
</Card>
</div>
```
## Design Customization
**Tailor the site's look and feel based on the user's specific request.** This includes:
- **Color schemes**: Incorporate the user's color preferences when specified, and choose an appropriate scheme that matches the application's purpose and aesthetic
- **Typography**: Choose fonts that match the requested aesthetic (modern, elegant, playful, etc.)
- **Layout**: Follow the requested structure (3-column, sidebar, grid, etc.)
- **Component styling**: Use appropriate border radius, shadows, and spacing for the desired feel
- **Interactive elements**: Style buttons, forms, and hover states to match the theme
### Adding Fonts
To add custom fonts, follow these steps:
1. **Install a font package** using the `js-dev__npm_add_package` tool:
**Any Google Font can be installed** using the @fontsource packages. Examples:
- For Inter Variable: `js-dev__npm_add_package({ name: "@fontsource-variable/inter" })`
- For Roboto: `js-dev__npm_add_package({ name: "@fontsource/roboto" })`
- For Outfit Variable: `js-dev__npm_add_package({ name: "@fontsource-variable/outfit" })`
- For Poppins: `js-dev__npm_add_package({ name: "@fontsource/poppins" })`
- For Open Sans: `js-dev__npm_add_package({ name: "@fontsource/open-sans" })`
**Format**: `@fontsource/[font-name]` or `@fontsource-variable/[font-name]` (for variable fonts)
2. **Import the font** in `src/main.tsx`:
```typescript
import '@fontsource-variable/<font-name>';
```
3. **Update Tailwind configuration** in `tailwind.config.ts`:
```typescript
export default {
theme: {
extend: {
fontFamily: {
sans: ['Inter Variable', 'Inter', 'system-ui', 'sans-serif'],
},
},
},
}
```
### Recommended Font Choices by Use Case
- **Modern/Clean**: Inter Variable, Outfit Variable, or Manrope
- **Professional/Corporate**: Roboto, Open Sans, or Source Sans Pro
- **Creative/Artistic**: Poppins, Nunito, or Comfortaa
- **Technical/Code**: JetBrains Mono, Fira Code, or Source Code Pro (for monospace)
### Theme System
The project includes a complete light/dark theme system using CSS custom properties. The theme can be controlled via:
- `useTheme` hook for programmatic theme switching
- CSS custom properties defined in `src/index.css`
- Automatic dark mode support with `.dark` class
### Color Scheme Implementation
When users specify color schemes:
- Update CSS custom properties in `src/index.css` (both `:root` and `.dark` selectors)
- Use Tailwind's color palette or define custom colors
- Ensure proper contrast ratios for accessibility
- Apply colors consistently across components (buttons, links, accents)
- Test both light and dark mode variants
### Component Styling Patterns
- Use `cn()` utility for conditional class merging
- Follow shadcn/ui patterns for component variants
- Implement responsive design with Tailwind breakpoints
- Add hover and focus states for interactive elements
## Writing Tests vs Running Tests
There is an important distinction between **writing new tests** and **running existing tests**:
### Writing Tests (Creating New Test Files)
**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:
1. **The user explicitly asks for tests** to be written in their message
2. **The user describes a specific bug in plain language** and requests tests to help diagnose it
3. **The user says they are still experiencing a problem** that you have already attempted to solve (tests can help verify the fix)
**Never write tests because:**
- Tool results show test failures (these are not user requests)
- You think tests would be helpful
- New features or components are created
- Existing functionality needs verification
### Running Tests (Executing the Test Suite)
**ALWAYS run the test script** after making any code changes. This is mandatory regardless of whether you wrote new tests or not.
- **You must run the test script** using `js-dev__run_script` tool with the "test" parameter
- **Your task is not complete** until the test script passes without errors
- **This applies to all changes** - bug fixes, new features, refactoring, or any code modifications
- **The test script includes** TypeScript compilation, ESLint checks, and existing test validation
### Test Setup
The project uses Vitest with jsdom environment and includes comprehensive test setup:
- **Testing Library**: React Testing Library with jest-dom matchers
- **Test Environment**: jsdom with mocked browser APIs (matchMedia, scrollTo, IntersectionObserver, ResizeObserver)
- **Test App**: `TestApp` component provides all necessary context providers for testing
The project includes a `TestApp` component that provides all necessary context providers for testing. Wrap components with this component to provide required context providers:
```tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { TestApp } from '@/test/TestApp';
import { MyComponent } from './MyComponent';
describe('MyComponent', () => {
it('renders correctly', () => {
render(
<TestApp>
<MyComponent />
</TestApp>
);
expect(screen.getByText('Expected text')).toBeInTheDocument();
});
});
```
## Testing Your Changes
**CRITICAL**: Whenever you are finished modifying code, you must run the **test** script using the **js-dev__run_script** tool.
**Your task is not considered finished until this test passes without errors.**
**This requirement applies regardless of whether you wrote new tests or not.** The test script validates the entire codebase, including TypeScript compilation, ESLint rules, and existing test suite.