commit 40fe3e93895e8ba8861c1f4c9d1bae688dd014aa Author: Nicolas Cantu Date: Mon Dec 22 09:48:57 2025 +0100 Initial commit 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' ? ( +