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:
- **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.
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 by MKStack"
When building the site for the first time, include "Vibed by MKStack" somewhere in the UI, linked to this URL: https://soapbox.pub/tools/mkstack/
- Before implementing features on Nostr, use the `read_nips_index` tool to see what kinds are currently in use across all NIPs.
- If any existing kind or NIP might offer the required functionality, use the `read_nip` tool to investigate. Several NIPs may need to be read before making a decision.
- If there are no existing suitable kinds, new kind numbers can be generated with the `generate_kind` tool.
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.**Prioritize Existing NIPs**: Always prefer extending or using existing NIPs over creating custom kinds, even if they require minor compromises in functionality.
2.**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
3.**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`
4.**When to Create 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)
**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
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.
### 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 returns an object containing a `nostr` property, with `.query()` and `.event()` methods for querying and publishing Nostr events respectively.
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)]);
/** 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. */
The `LoginArea` component handles all the login-related UI and interactions, including displaying login dialogs and switching between accounts. It should not be wrapped in any conditional logic.
`LoginArea` displays a "Log in" button 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.
For URL routing, use NIP-19 identifiers as path parameters (e.g., `/:nip19`) to create secure, universal links to Nostr events. Decode the identifier and render the appropriate component based on the type:
- Regular events: Use `/nevent1...` paths
- Replaceable/addressable events: Use `/naddr1...` paths
Always use `naddr` identifiers for addressable events instead of just the `d` tag value, as `naddr` contains the author pubkey needed to create secure filters. This prevents security issues where malicious actors could publish events with the same `d` tag to override content.
```ts
// Secure routing with naddr
const decoded = nip19.decode(params.nip19);
if (decoded.type === 'naddr' && decoded.data.kind === 30024) {
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.
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.
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:
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:
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';
- **Color schemes**: Incorporate the user's color preferences when specified, and choose an appropriate scheme that matches the application's purpose and aesthetic
**Important for AI Assistants**: Only create tests when the user is experiencing a specific problem or explicitly requests tests. Do not proactively write tests for new features or components unless the user is having issues that require testing to diagnose or resolve.
The project includes a `TestApp` component that provides all necessary context providers for testing. Wrap components with this component to provide required context providers: