diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 120000 index 0000000..c67157d --- /dev/null +++ b/.gemini/settings.json @@ -0,0 +1 @@ +../.mcp.json \ No newline at end of file diff --git a/.gitignore b/.gitignore index b485e7a..f74940e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ dist-ssr # Vercel .vercel + +# Secrets +.env +.env.* +!.env.example diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6afba16..14b7efa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,16 +5,13 @@ default: timeout: 1 minute stages: - - build + - test - deploy -build: - stage: build +test: + stage: test script: - - npm run build - only: - variables: - - $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME + - npm run test pages: stage: deploy diff --git a/CONTEXT.md b/CONTEXT.md index 06be660..413d7fb 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -102,7 +102,7 @@ The AI assistant's behavior and knowledge is defined by the CONTEXT.md file, whi ## "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/tools/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 @@ -138,6 +138,8 @@ When implementing features that could use existing NIPs, follow this decision fr - 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 @@ -649,6 +651,61 @@ export function Post(/* ...props */) { } ``` +### 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 ( +
+ {/* Your article content */} +
{/* article content */}
+ + {/* Comments section */} + +
+ ); +} +``` + +#### 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 + +``` + +#### Commenting on URLs + +The comments system also supports commenting on external URLs, making it useful for web pages, articles, or any online content: + +```tsx + +``` + ## App Configuration The project includes an `AppProvider` that manages global application state including theme and relay configuration. The default configuration includes: @@ -811,7 +868,17 @@ When users specify color schemes: ## Writing Tests -**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. +**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 ### Test Setup diff --git a/GEMINI.md b/GEMINI.md new file mode 120000 index 0000000..fa62d27 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +CONTEXT.md \ No newline at end of file diff --git a/agent.json b/agent.json index 211f73f..de65194 100644 --- a/agent.json +++ b/agent.json @@ -1,5 +1,6 @@ { "model": "claude-sonnet-4", + "temperature": 0.2, "mcpServers": { "js-dev": { "type": "stdio", diff --git a/package-lock.json b/package-lock.json index dbfcfe7..b00e92a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.0", "dependencies": { "@hookform/resolvers": "^3.9.0", - "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.1", - "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.5", + "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.3", + "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.7", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.0", @@ -61,7 +61,7 @@ "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.3", - "zod": "^3.23.8" + "zod": "^3.25.71" }, "devDependencies": { "@eslint/js": "^9.9.0", @@ -1205,15 +1205,15 @@ } }, "node_modules/@jsr/nostrify__nostrify": { - "version": "0.46.1", - "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.46.1.tgz", - "integrity": "sha512-7XSP4+kjcPgw937jQPUxf+qo1mWx7rbKUuA5ma0CofRQZa2v01IA4f+OfVGJbABxbJ8SC+8VdPkLqqUOJxeVPg==", + "version": "0.46.3", + "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.46.3.tgz", + "integrity": "sha512-zJpOrD8bbrJroLRJjESAJZX/ZKFCaGfoz5fSfLP+gIcTiPo8JpzlrFBF6mvaDI/Mdd+1WTBwlCcW9On8rUVH7w==", "dependencies": { "@jsr/nostrify__types": "^0.36.0", "@jsr/scure__base": "^1.2.4", "@jsr/std__encoding": "^0.224.1", "lru-cache": "^10.2.0", - "nostr-tools": "^2.10.4", + "nostr-tools": "^2.13.0", "websocket-ts": "^2.2.1", "zod": "^3.23.8" } @@ -1315,27 +1315,27 @@ }, "node_modules/@nostrify/nostrify": { "name": "@jsr/nostrify__nostrify", - "version": "0.46.1", - "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.46.1.tgz", - "integrity": "sha512-7XSP4+kjcPgw937jQPUxf+qo1mWx7rbKUuA5ma0CofRQZa2v01IA4f+OfVGJbABxbJ8SC+8VdPkLqqUOJxeVPg==", + "version": "0.46.3", + "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__nostrify/0.46.3.tgz", + "integrity": "sha512-zJpOrD8bbrJroLRJjESAJZX/ZKFCaGfoz5fSfLP+gIcTiPo8JpzlrFBF6mvaDI/Mdd+1WTBwlCcW9On8rUVH7w==", "dependencies": { "@jsr/nostrify__types": "^0.36.0", "@jsr/scure__base": "^1.2.4", "@jsr/std__encoding": "^0.224.1", "lru-cache": "^10.2.0", - "nostr-tools": "^2.10.4", + "nostr-tools": "^2.13.0", "websocket-ts": "^2.2.1", "zod": "^3.23.8" } }, "node_modules/@nostrify/react": { "name": "@jsr/nostrify__react", - "version": "0.2.5", - "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__react/0.2.5.tgz", - "integrity": "sha512-Hyi1N4hXa89gYsuk7fqGrWEzIzTp7md8hKog+D2DgpSqVwFQM1vw267Uv3rOieE/hPereD3gen9X88XkeAXP+Q==", + "version": "0.2.7", + "resolved": "https://npm.jsr.io/~/11/@jsr/nostrify__react/0.2.7.tgz", + "integrity": "sha512-36fAOOymf34KR2OfE4jXBojbZnPsrIzQDAXzE7dko8/Qj+0s8iKnZoZKg9DVIT2F6Lxhr6SUiTze8xBxuJbf1A==", "dependencies": { - "@jsr/nostrify__nostrify": "^0.46.1", - "nostr-tools": "^2.10.4", + "@jsr/nostrify__nostrify": "^0.46.3", + "nostr-tools": "^2.13.0", "react": "^18.0.0" } }, @@ -8244,9 +8244,9 @@ } }, "node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "version": "3.25.71", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.71.tgz", + "integrity": "sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index eefa69c..f5170fe 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ }, "dependencies": { "@hookform/resolvers": "^3.9.0", - "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.1", - "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.5", + "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.3", + "@nostrify/react": "npm:@jsr/nostrify__react@^0.2.7", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.0", @@ -63,7 +63,7 @@ "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.3", - "zod": "^3.23.8" + "zod": "^3.25.71" }, "devDependencies": { "@eslint/js": "^9.9.0", diff --git a/recipe.yaml b/recipe.yaml deleted file mode 100644 index 9529871..0000000 --- a/recipe.yaml +++ /dev/null @@ -1,32 +0,0 @@ -version: 1.0.0 -title: Nostr Client Development -description: A recipe for building Nostr client applications -instructions: Create a Nostr client application according to the user's request. -extensions: - - type: builtin - name: developer - display_name: Developer Tools - timeout: 300 - bundled: true - - type: stdio - name: nostr - cmd: npx - args: - - -y - - xjsr - - "@nostrbook/mcp" - envs: {} - timeout: 20 - description: null - bundled: null - - type: stdio - name: fetch - cmd: uvx - args: - - mcp-server-fetch - envs: {} - timeout: 20 - description: null - bundled: null -author: - contact: Alex Gleason diff --git a/src/App.tsx b/src/App.tsx index 2300f58..7cab833 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,7 +32,7 @@ const queryClient = new QueryClient({ const defaultConfig: AppConfig = { theme: "light", - relayUrl: "wss://relay.nostr.band", + relayUrl: "wss://relay.primal.net", }; const presetRelays = [ diff --git a/src/components/AppProvider.tsx b/src/components/AppProvider.tsx index d571e24..79c9e69 100644 --- a/src/components/AppProvider.tsx +++ b/src/components/AppProvider.tsx @@ -1,4 +1,5 @@ import { ReactNode, useEffect } from 'react'; +import { z } from 'zod'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { AppContext, type AppConfig, type AppContextType, type Theme } from '@/contexts/AppContext'; @@ -12,6 +13,12 @@ interface AppProviderProps { presetRelays?: { name: string; url: string }[]; } +// Zod schema for AppConfig validation +const AppConfigSchema: z.ZodType = z.object({ + theme: z.enum(['dark', 'light', 'system']), + relayUrl: z.string().url(), +}); + export function AppProvider(props: AppProviderProps) { const { children, @@ -21,7 +28,17 @@ export function AppProvider(props: AppProviderProps) { } = props; // App configuration state with localStorage persistence - const [config, setConfig] = useLocalStorage(storageKey, defaultConfig); + const [config, setConfig] = useLocalStorage( + storageKey, + defaultConfig, + { + serialize: JSON.stringify, + deserialize: (value: string) => { + const parsed = JSON.parse(value); + return AppConfigSchema.parse(parsed); + } + } + ); // Generic config updater with callback pattern const updateConfig = (updater: (currentConfig: AppConfig) => AppConfig) => { diff --git a/src/components/auth/AccountSwitcher.tsx b/src/components/auth/AccountSwitcher.tsx index d3ebf5e..4f4518b 100644 --- a/src/components/auth/AccountSwitcher.tsx +++ b/src/components/auth/AccountSwitcher.tsx @@ -28,7 +28,7 @@ export function AccountSwitcher({ onAddAccountClick }: AccountSwitcherProps) { } return ( - + + + {hasReplies && ( + + + + + + )} + + + {/* Comment menu */} + + + + + + + + + + + {/* Reply Form */} + {showReplyForm && ( +
+ setShowReplyForm(false)} + placeholder="Write a reply..." + compact + /> +
+ )} + + {/* Replies */} + {hasReplies && ( + + + {replies.map((reply) => ( + + ))} + + + )} + + ); +} \ No newline at end of file diff --git a/src/components/comments/CommentForm.tsx b/src/components/comments/CommentForm.tsx new file mode 100644 index 0000000..7c973f2 --- /dev/null +++ b/src/components/comments/CommentForm.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent } from '@/components/ui/card'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { usePostComment } from '@/hooks/usePostComment'; +import { LoginArea } from '@/components/auth/LoginArea'; +import { NostrEvent } from '@nostrify/nostrify'; +import { MessageSquare, Send } from 'lucide-react'; + +interface CommentFormProps { + root: NostrEvent | URL; + reply?: NostrEvent | URL; + onSuccess?: () => void; + placeholder?: string; + compact?: boolean; +} + +export function CommentForm({ + root, + reply, + onSuccess, + placeholder = "Write a comment...", + compact = false +}: CommentFormProps) { + const [content, setContent] = useState(''); + const { user } = useCurrentUser(); + const { mutate: postComment, isPending } = usePostComment(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!content.trim() || !user) return; + + postComment( + { content: content.trim(), root, reply }, + { + onSuccess: () => { + setContent(''); + onSuccess?.(); + }, + } + ); + }; + + if (!user) { + return ( + + +
+
+ + Sign in to {reply ? 'reply' : 'comment'} +
+ +
+
+
+ ); + } + + return ( + + +
+