From 40fe3e93895e8ba8861c1f4c9d1bae688dd014aa Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Mon, 22 Dec 2025 09:48:57 +0100 Subject: [PATCH] Initial commit --- .eslintrc.json | 56 + .gitignore | 34 + README.md | 47 + STRICT_CONFIG_SUMMARY.md | 75 + components/AlbyInstaller.tsx | 101 + components/ArticleCard.tsx | 49 + components/ArticleEditor.tsx | 65 + components/ArticleEditorForm.tsx | 80 + components/ArticleField.tsx | 64 + components/ArticleFilters.tsx | 156 + components/ArticleFormButtons.tsx | 30 + components/ArticlePreview.tsx | 43 + components/ClearButton.tsx | 24 + components/ConnectButton.tsx | 38 + components/ConnectedUserMenu.tsx | 42 + components/DocsContent.tsx | 26 + components/DocsSidebar.tsx | 41 + components/NotificationActions.tsx | 33 + components/NotificationBadge.tsx | 42 + components/NotificationBadgeButton.tsx | 37 + components/NotificationCenter.tsx | 43 + components/NotificationContent.tsx | 32 + components/NotificationItem.tsx | 34 + components/NotificationPanel.tsx | 69 + components/NotificationPanelHeader.tsx | 44 + components/PaymentModal.tsx | 149 + components/SearchBar.tsx | 44 + components/SearchIcon.tsx | 19 + components/UserArticles.tsx | 67 + components/UserProfile.tsx | 35 + components/UserProfileHeader.tsx | 39 + docs/faq.md | 239 + docs/payment-guide.md | 248 + docs/publishing-guide.md | 208 + docs/rizful-api-setup.md | 133 + docs/user-guide.md | 279 + features/alby-integration.md | 145 + features/code-cleanup-summary.md | 122 + features/fallbacks-found.md | 112 + features/filtering-search-implementation.md | 138 + features/final-cleanup-summary.md | 127 + features/implementation-summary.md | 175 + features/nostr-paywall-implementation.md | 217 + features/notifications-implementation.md | 179 + features/priority1-implementation.md | 85 + features/remaining-tasks.md | 215 + features/rizful-integration.md | 215 + .../storage-improvement-implementation.md | 158 + features/todo-implementation-updated.md | 244 + features/todo-implementation.md | 276 + features/user-profile-implementation.md | 169 + hooks/useArticlePayment.ts | 95 + hooks/useArticlePublishing.ts | 51 + hooks/useArticles.ts | 102 + hooks/useDocs.ts | 49 + hooks/useNostrConnect.ts | 48 + hooks/useNotificationCenter.ts | 33 + hooks/useNotifications.ts | 87 + hooks/useUserArticles.ts | 118 + lib/alby.ts | 209 + lib/articleFiltering.ts | 91 + lib/articleInvoice.ts | 45 + lib/articlePublisher.ts | 182 + lib/articleStorage.ts | 107 + lib/formatTime.ts | 26 + lib/invoiceResolver.ts | 24 + lib/markdownRenderer.tsx | 175 + lib/nostr.ts | 285 + lib/nostrEventParsing.ts | 46 + lib/nostrPrivateMessages.ts | 66 + lib/nostrRemoteSigner.ts | 58 + lib/nostrSubscription.ts | 52 + lib/nostrZapVerification.ts | 71 + lib/nostrconnect.ts | 178 + lib/nostrconnectHandler.ts | 2 + lib/nostrconnectMessageHandler.ts | 119 + lib/notifications.ts | 160 + lib/payment.ts | 121 + lib/paymentPolling.ts | 95 + lib/retry.ts | 91 + lib/storage/indexedDB.ts | 213 + lib/zapVerification.ts | 110 + next.config.js | 10 + package-lock.json | 6260 +++++++++++++++++ package.json | 30 + pages/_app.tsx | 6 + pages/api/docs/[file].ts | 34 + pages/docs.tsx | 83 + pages/index.tsx | 129 + pages/profile.tsx | 159 + pages/publish.tsx | 54 + postcss.config.js | 6 + styles/globals.css | 19 + tailwind.config.js | 12 + tsconfig.json | 35 + types/alby.ts | 19 + types/nostr.ts | 35 + types/notifications.ts | 19 + 98 files changed, 15361 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 README.md create mode 100644 STRICT_CONFIG_SUMMARY.md create mode 100644 components/AlbyInstaller.tsx create mode 100644 components/ArticleCard.tsx create mode 100644 components/ArticleEditor.tsx create mode 100644 components/ArticleEditorForm.tsx create mode 100644 components/ArticleField.tsx create mode 100644 components/ArticleFilters.tsx create mode 100644 components/ArticleFormButtons.tsx create mode 100644 components/ArticlePreview.tsx create mode 100644 components/ClearButton.tsx create mode 100644 components/ConnectButton.tsx create mode 100644 components/ConnectedUserMenu.tsx create mode 100644 components/DocsContent.tsx create mode 100644 components/DocsSidebar.tsx create mode 100644 components/NotificationActions.tsx create mode 100644 components/NotificationBadge.tsx create mode 100644 components/NotificationBadgeButton.tsx create mode 100644 components/NotificationCenter.tsx create mode 100644 components/NotificationContent.tsx create mode 100644 components/NotificationItem.tsx create mode 100644 components/NotificationPanel.tsx create mode 100644 components/NotificationPanelHeader.tsx create mode 100644 components/PaymentModal.tsx create mode 100644 components/SearchBar.tsx create mode 100644 components/SearchIcon.tsx create mode 100644 components/UserArticles.tsx create mode 100644 components/UserProfile.tsx create mode 100644 components/UserProfileHeader.tsx create mode 100644 docs/faq.md create mode 100644 docs/payment-guide.md create mode 100644 docs/publishing-guide.md create mode 100644 docs/rizful-api-setup.md create mode 100644 docs/user-guide.md create mode 100644 features/alby-integration.md create mode 100644 features/code-cleanup-summary.md create mode 100644 features/fallbacks-found.md create mode 100644 features/filtering-search-implementation.md create mode 100644 features/final-cleanup-summary.md create mode 100644 features/implementation-summary.md create mode 100644 features/nostr-paywall-implementation.md create mode 100644 features/notifications-implementation.md create mode 100644 features/priority1-implementation.md create mode 100644 features/remaining-tasks.md create mode 100644 features/rizful-integration.md create mode 100644 features/storage-improvement-implementation.md create mode 100644 features/todo-implementation-updated.md create mode 100644 features/todo-implementation.md create mode 100644 features/user-profile-implementation.md create mode 100644 hooks/useArticlePayment.ts create mode 100644 hooks/useArticlePublishing.ts create mode 100644 hooks/useArticles.ts create mode 100644 hooks/useDocs.ts create mode 100644 hooks/useNostrConnect.ts create mode 100644 hooks/useNotificationCenter.ts create mode 100644 hooks/useNotifications.ts create mode 100644 hooks/useUserArticles.ts create mode 100644 lib/alby.ts create mode 100644 lib/articleFiltering.ts create mode 100644 lib/articleInvoice.ts create mode 100644 lib/articlePublisher.ts create mode 100644 lib/articleStorage.ts create mode 100644 lib/formatTime.ts create mode 100644 lib/invoiceResolver.ts create mode 100644 lib/markdownRenderer.tsx create mode 100644 lib/nostr.ts create mode 100644 lib/nostrEventParsing.ts create mode 100644 lib/nostrPrivateMessages.ts create mode 100644 lib/nostrRemoteSigner.ts create mode 100644 lib/nostrSubscription.ts create mode 100644 lib/nostrZapVerification.ts create mode 100644 lib/nostrconnect.ts create mode 100644 lib/nostrconnectHandler.ts create mode 100644 lib/nostrconnectMessageHandler.ts create mode 100644 lib/notifications.ts create mode 100644 lib/payment.ts create mode 100644 lib/paymentPolling.ts create mode 100644 lib/retry.ts create mode 100644 lib/storage/indexedDB.ts create mode 100644 lib/zapVerification.ts create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pages/_app.tsx create mode 100644 pages/api/docs/[file].ts create mode 100644 pages/docs.tsx create mode 100644 pages/index.tsx create mode 100644 pages/profile.tsx create mode 100644 pages/publish.tsx create mode 100644 postcss.config.js create mode 100644 styles/globals.css create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 types/alby.ts create mode 100644 types/nostr.ts create mode 100644 types/notifications.ts diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..728abe1 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,56 @@ +{ + "extends": [ + "next/core-web-vitals", + "next/typescript" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module", + "project": "./tsconfig.json" + }, + "rules": { + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/await-thenable": "error", + "@typescript-eslint/no-unnecessary-type-assertion": "error", + "@typescript-eslint/no-non-null-assertion": "error", + "@typescript-eslint/prefer-nullish-coalescing": "error", + "@typescript-eslint/prefer-optional-chain": "error", + "@typescript-eslint/no-non-null-asserted-optional-chain": "error", + "no-console": ["warn", { "allow": ["warn", "error"] }], + "no-debugger": "error", + "no-alert": "error", + "prefer-const": "error", + "no-var": "error", + "object-shorthand": "error", + "prefer-arrow-callback": "warn", + "prefer-template": "error", + "eqeqeq": ["error", "always"], + "curly": ["error", "all"], + "no-throw-literal": "error", + "no-return-await": "error", + "require-await": "warn", + "no-await-in-loop": "warn", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error", + "react/jsx-key": "error", + "react/jsx-no-duplicate-props": "error", + "react/jsx-no-undef": "error", + "react/no-unescaped-entities": "warn", + "react/no-unknown-property": "error", + "max-lines": ["error", { "max": 250, "skipBlankLines": false, "skipComments": false }], + "max-lines-per-function": ["error", { "max": 40, "skipBlankLines": false, "skipComments": false, "IIFEs": true }] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..338ba29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules/ +/.pnp +.pnp.js + +# Testing +/coverage + +# Next.js +/.next/ +/out/ + +# Production +/build + +# Misc +.DS_Store +*.pem + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local env files +.env*.local +.env + +# Vercel +.vercel + +# TypeScript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..97dd632 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Nostr Paywall + +A Nostr-based article platform with free previews and paid content. Users can read article previews for free and unlock full content by sending a 800 sats zap via Lightning Network. + +## Features + +- **NostrConnect Integration**: Authenticate using NostrConnect (default: use.nsec.app) +- **Free Previews**: Public notes showing article previews +- **Paid Content**: Private notes containing full content, unlocked after 800 sats zap +- **Lightning Payments**: Integrated Alby/WebLN for Lightning payments (works with Alby and other Lightning wallets) +- **Payment Modal**: User-friendly payment interface with invoice display +- **TypeScript**: Fully typed codebase + +## Getting Started + +1. Install dependencies: +```bash +npm install +``` + +2. Run the development server: +```bash +npm run dev +``` + +3. Open [http://localhost:3000](http://localhost:3000) in your browser + +## Environment Variables + +- `NEXT_PUBLIC_NOSTR_RELAY_URL`: Nostr relay URL (default: wss://relay.damus.io) +- `NEXT_PUBLIC_NOSTRCONNECT_BRIDGE`: NostrConnect bridge URL (default: https://use.nsec.app) + +## Lightning Wallet Setup + +This project uses the WebLN standard for Lightning payments, which works with: +- **Alby** (recommended): Install the [Alby browser extension](https://getalby.com/) +- Other WebLN-compatible Lightning wallets + +Users need to have a Lightning wallet extension installed to make payments. The payment flow will prompt them to connect their wallet when needed. + +## Project Structure + +- `/pages`: Next.js pages +- `/components`: React components +- `/lib`: Utilities and Nostr helpers +- `/types`: TypeScript type definitions +- `/hooks`: Custom React hooks diff --git a/STRICT_CONFIG_SUMMARY.md b/STRICT_CONFIG_SUMMARY.md new file mode 100644 index 0000000..45f3f36 --- /dev/null +++ b/STRICT_CONFIG_SUMMARY.md @@ -0,0 +1,75 @@ +# Configuration stricte TypeScript et ESLint + +## Règles TypeScript strictes (tsconfig.json) + +### Activées : +- ✅ `strict: true` - Mode strict complet +- ✅ `noUnusedLocals: true` - Variables locales non utilisées = erreur +- ✅ `noUnusedParameters: true` - Paramètres non utilisés = erreur +- ✅ `noImplicitReturns: true` - Return explicite requis +- ✅ `noFallthroughCasesInSwitch: true` - Pas de fallthrough dans switch +- ✅ `noUncheckedIndexedAccess: true` - Accès aux tableaux/objets vérifiés +- ✅ `noImplicitOverride: true` - Override explicite requis +- ✅ `exactOptionalPropertyTypes: true` - Types optionnels exacts + +### Règles ESLint strictes (.eslintrc.json) + +#### TypeScript avec informations de type : +- ✅ `@typescript-eslint/no-floating-promises: error` - Promesses non gérées = erreur +- ✅ `@typescript-eslint/no-misused-promises: error` - Promesses mal utilisées = erreur +- ✅ `@typescript-eslint/await-thenable: error` - Await sur non-promesse = erreur +- ✅ `@typescript-eslint/no-unnecessary-type-assertion: error` - Assertions inutiles = erreur +- ✅ `@typescript-eslint/no-non-null-assertion: error` - Non-null assertions interdites +- ✅ `@typescript-eslint/prefer-nullish-coalescing: error` - Force `??` au lieu de `||` +- ✅ `@typescript-eslint/prefer-optional-chain: error` - Force l'optional chaining +- ✅ `@typescript-eslint/no-non-null-asserted-optional-chain: error` - Chaînage + assertion interdite +- ✅ `@typescript-eslint/no-explicit-any: error` - `any` explicite interdit + +#### Variables et code mort : +- ✅ `@typescript-eslint/no-unused-vars: error` - Variables non utilisées = erreur (sauf `_*`) + +#### Bonnes pratiques JavaScript/TypeScript : +- ✅ `prefer-const: error` - Force `const` quand possible +- ✅ `no-var: error` - Interdit `var` +- ✅ `object-shorthand: error` - Force la syntaxe raccourcie +- ✅ `prefer-template: error` - Force les template literals +- ✅ `eqeqeq: error` - Force `===` et `!==` +- ✅ `curly: error` - Force les accolades dans if/for +- ✅ `no-throw-literal: error` - Interdit de throw des primitives +- ✅ `no-return-await: error` - Interdit `return await` + +#### React : +- ✅ `react-hooks/rules-of-hooks: error` - Règles des hooks strictes +- ✅ `react-hooks/exhaustive-deps: error` - Dépendances des hooks strictes + +#### Console/Debug : +- ✅ `no-console: warn` - Console interdit (sauf warn/error) +- ✅ `no-debugger: error` - Debugger interdit +- ✅ `no-alert: error` - Alert interdit + +#### Longueur de code : +- ✅ `max-lines: error` - Max 250 lignes par fichier +- ✅ `max-lines-per-function: error` - Max 40 lignes par fonction + +## Configuration ParserOptions + +Pour activer les règles TypeScript avec type information : +```json +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module", + "project": "./tsconfig.json" + } +} +``` + +## Objectif + +Le but n'est PAS d'éviter les erreurs, mais d'avoir une **très haute qualité de code** en : +- Détectant les bugs avant l'exécution +- Forçant les bonnes pratiques +- Éliminant le code mort +- Garantissant la sécurité des types +- Prévenant les erreurs courantes diff --git a/components/AlbyInstaller.tsx b/components/AlbyInstaller.tsx new file mode 100644 index 0000000..f909492 --- /dev/null +++ b/components/AlbyInstaller.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from 'react' +import { getAlbyService } from '@/lib/alby' + +interface AlbyInstallerProps { + onInstalled?: () => void +} + +export function AlbyInstaller({ onInstalled }: AlbyInstallerProps) { + const [isInstalled, setIsInstalled] = useState(false) + const [isChecking, setIsChecking] = useState(true) + + useEffect(() => { + const checkAlby = async () => { + try { + const alby = getAlbyService() + const installed = alby.isEnabled() + setIsInstalled(installed) + if (installed) { + onInstalled?.() + } + } catch (e) { + console.error('Error checking Alby:', e) + setIsInstalled(false) + } finally { + setIsChecking(false) + } + } + + checkAlby() + }, [onInstalled]) + + if (isChecking) { + return null + } + + if (isInstalled) { + return null + } + + return ( +
+
+
+ + + +
+
+

+ Alby Extension Required +

+
+

+ To make Lightning payments, please install the Alby browser extension. +

+
+
+
+ + Install Alby + + +
+
+
+

+ Alby is a Lightning wallet that enables instant Bitcoin payments in your browser. +

+
+
+
+
+ ) +} diff --git a/components/ArticleCard.tsx b/components/ArticleCard.tsx new file mode 100644 index 0000000..e398a38 --- /dev/null +++ b/components/ArticleCard.tsx @@ -0,0 +1,49 @@ +import type { Article } from '@/types/nostr' +import { useNostrConnect } from '@/hooks/useNostrConnect' +import { useArticlePayment } from '@/hooks/useArticlePayment' +import { ArticlePreview } from './ArticlePreview' +import { PaymentModal } from './PaymentModal' + +interface ArticleCardProps { + article: Article + onUnlock?: (article: Article) => void +} + +export function ArticleCard({ article, onUnlock }: ArticleCardProps) { + const { connected, pubkey } = useNostrConnect() + const { + loading, + error, + paymentInvoice, + handleUnlock, + handlePaymentComplete, + handleCloseModal, + } = useArticlePayment(article, pubkey ?? null, () => { + onUnlock?.(article) + }) + + return ( +
+

{article.title}

+
+ +
+ {error &&

{error}

} +
+ Published {new Date(article.createdAt * 1000).toLocaleDateString()} +
+ {paymentInvoice && ( + + )} +
+ ) +} diff --git a/components/ArticleEditor.tsx b/components/ArticleEditor.tsx new file mode 100644 index 0000000..64f7ec9 --- /dev/null +++ b/components/ArticleEditor.tsx @@ -0,0 +1,65 @@ +import { useState } from 'react' +import { useNostrConnect } from '@/hooks/useNostrConnect' +import { useArticlePublishing } from '@/hooks/useArticlePublishing' +import type { ArticleDraft } from '@/lib/articlePublisher' +import { ArticleEditorForm } from './ArticleEditorForm' + +interface ArticleEditorProps { + onPublishSuccess?: (articleId: string) => void + onCancel?: () => void +} + +function NotConnectedMessage() { + return ( +
+

Please connect with Nostr to publish articles

+
+ ) +} + +function SuccessMessage() { + return ( +
+

Article Published!

+

Your article has been successfully published.

+
+ ) +} + +export function ArticleEditor({ onPublishSuccess, onCancel }: ArticleEditorProps) { + const { connected, pubkey } = useNostrConnect() + const { loading, error, success, publishArticle } = useArticlePublishing(pubkey ?? null) + const [draft, setDraft] = useState({ + title: '', + preview: '', + content: '', + zapAmount: 800, + }) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + const articleId = await publishArticle(draft) + if (articleId) { + onPublishSuccess?.(articleId) + } + } + + if (!connected) { + return + } + + if (success) { + return + } + + return ( + + ) +} diff --git a/components/ArticleEditorForm.tsx b/components/ArticleEditorForm.tsx new file mode 100644 index 0000000..a8502f7 --- /dev/null +++ b/components/ArticleEditorForm.tsx @@ -0,0 +1,80 @@ +import React from 'react' +import type { ArticleDraft } from '@/lib/articlePublisher' +import { ArticleField } from './ArticleField' +import { ArticleFormButtons } from './ArticleFormButtons' + +interface ArticleEditorFormProps { + draft: ArticleDraft + onDraftChange: (draft: ArticleDraft) => void + onSubmit: (e: React.FormEvent) => void + loading: boolean + error: string | null + onCancel?: () => void +} + +export function ArticleEditorForm({ + draft, + onDraftChange, + onSubmit, + loading, + error, + onCancel, +}: ArticleEditorFormProps) { + return ( +
+

Publish New Article

+ + onDraftChange({ ...draft, title: value as string })} + required + placeholder="Enter article title" + /> + + onDraftChange({ ...draft, preview: value as string })} + required + type="textarea" + rows={4} + placeholder="This preview will be visible to everyone for free" + helpText="This content will be visible to everyone" + /> + + onDraftChange({ ...draft, content: value as string })} + required + type="textarea" + rows={8} + placeholder="This content will be encrypted and sent to users who pay" + helpText="This content will be encrypted and sent as a private message after payment" + /> + + onDraftChange({ ...draft, zapAmount: value as number })} + required + type="number" + min={1} + helpText="Amount in satoshis to unlock the full content" + /> + + {error && ( +
+

{error}

+
+ )} + + + + ) +} diff --git a/components/ArticleField.tsx b/components/ArticleField.tsx new file mode 100644 index 0000000..41e82b4 --- /dev/null +++ b/components/ArticleField.tsx @@ -0,0 +1,64 @@ +import React from 'react' + +interface ArticleFieldProps { + id: string + label: string + value: string | number + onChange: (value: string | number) => void + required?: boolean + type?: 'text' | 'textarea' | 'number' + rows?: number + placeholder?: string + helpText?: string + min?: number +} + +export function ArticleField({ + id, + label, + value, + onChange, + required = false, + type = 'text', + rows, + placeholder, + helpText, + min, +}: ArticleFieldProps) { + const inputClass = + 'w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500' + + return ( +
+ + {type === 'textarea' ? ( +