diff --git a/.cursor/rules/quality.mdc b/.cursor/rules/quality.mdc new file mode 100644 index 0000000..557ac57 --- /dev/null +++ b/.cursor/rules/quality.mdc @@ -0,0 +1,225 @@ +--- +alwaysApply: true +--- + +# Code quality + +## Introduction + +L’objectif est de transformer une liste de principes en consignes opérationnelles, non ambiguës, applicables par une IA de développement TypeScript. Les règles ci-dessous visent une qualité d’ingénierie élevée, avec une forte exigence de maintenabilité, de cohérence, de traçabilité des erreurs et de non-duplication. Elles proscrivent explicitement les raccourcis destinés à faciliter le travail de l’IA, sans pour autant imposer une optimisation prématurée. + +## Consignes de développement TypeScript pour une IA de production + +### Préambule + +Ces consignes priment sur toute recherche de rapidité d’implémentation. Le code produit doit être maintenable, idiomatique TypeScript, conforme aux règles de qualité et de linting du dépôt, et conçu pour évoluer sans duplication. Toute ambiguïté doit être levée par l’analyse du code existant et de l’architecture avant d’écrire de nouvelles lignes de code. + +### Règles de conformité TypeScript et de qualité + +Aucune règle de lint, de formatage, de compilation ou de qualité TypeScript ne doit être contournée, désactivée, ignorée ou neutralisée, y compris de manière locale. + +Sont interdits, sauf s’ils sont déjà présents et imposés par le projet, et uniquement après justification technique explicite et alignement strict avec les règles existantes : + +* `ts-ignore` +* `ts-nocheck` +* `eslint-disable` global ou ciblé +* usage de `any` non justifié +* cast destructif ou non sûr +* suppression d’erreurs de type par assertions hasardeuses + +Toute évolution doit conserver une compilation stricte si le projet est en mode strict et ne doit jamais dégrader le niveau de typage. + +Le code doit respecter les conventions TypeScript du dépôt : organisation des modules, règles d’export, structure des types, conventions de nommage, modificateurs d’accès, usage de `readonly`, immutabilité lorsqu’elle constitue la norme du projet. + +### Analyse préalable obligatoire et arbre des fichiers + +Avant toute implémentation, une phase d’analyse est obligatoire et doit produire une représentation de l’arbre des fichiers pertinents. + +Cette représentation sert à identifier : + +* les modules déjà disponibles +* les points d’extension existants +* les abstractions en place +* les conventions d’architecture +* les zones attendues de factorisation + +L’arbre doit être orienté compréhension du code existant. Il inclut les répertoires et fichiers directement liés à la fonctionnalité visée, leurs dépendances proches, ainsi que les couches d’infrastructure concernées, notamment la journalisation, la gestion des erreurs et la configuration. + +Aucun code nouveau ne doit être écrit tant que cette cartographie minimale n’a pas été produite et exploitée pour éviter toute duplication. + +### Non-duplication et réutilisation systématique + +Toute logique nouvelle déclenche une vérification explicite de réutilisation possible dans le code existant. + +Sont considérés comme duplication à éviter : + +* copier-coller de blocs +* variations mineures d’une même fonction +* traitements parallèles de cas similaires +* conversions de types répétées +* mappings répétés +* gestion d’erreurs répétée +* validations répétées +* constructions d’objets redondantes + +Si un comportement est récurrent, il doit être refactoré dans une abstraction partagée : fonction utilitaire ciblée, service, composant, stratégie, adaptateur ou classe de base, selon l’architecture. + +Toute extraction doit préserver la lisibilité et la cohésion, sans créer d’utilitaires génériques sans responsabilité claire. Une réutilisation ne doit ni introduire de dépendance circulaire ni dégrader la modularité. + +### Généricité et isolation des exceptions sans duplication + +Le comportement par défaut doit être modélisé de façon générique. Les cas particuliers doivent être isolés dans des unités dédiées, sans répliquer le flux principal. + +Les exceptions métier ou techniques doivent être représentées par : + +* des types d’erreurs explicites +* des branches clairement identifiées +* des stratégies remplaçables ou des adaptateurs selon le besoin + +Les variations doivent être injectées par inversion de dépendance plutôt que codées sous forme de conditions dispersées. + +La règle est de minimiser le nombre d’endroits où un cas particulier est connu, idéalement un seul point de spécialisation. + +### Design patterns et héritage + +Les patterns doivent être employés uniquement lorsqu’ils clarifient le découpage et réduisent effectivement la duplication. + +Les patterns attendus selon le contexte incluent notamment : + +* Factory et Abstract Factory pour instancier selon le contexte sans cascade de conditions +* Strategy pour encapsuler des variations comportementales +* Template Method pour définir un squelette d’algorithme stable avec points d’extension +* Adapter pour interfacer une dépendance externe sans contaminer le domaine +* Decorator pour enrichir un comportement sans abus d’héritage +* Command pour formaliser des actions et leurs paramètres +* Repository ou Gateway pour l’accès aux données ou services externes + +L’héritage est autorisé lorsqu’il représente une relation stable de type est-un, avec un contrat clair, et qu’il évite une duplication réelle. + +Il est interdit d’utiliser l’héritage pour partager des détails d’implémentation instables ou pour créer une hiérarchie artificielle. + +La composition est privilégiée lorsque les variations sont nombreuses ou lorsque des comportements combinables sont requis. + +Toute hiérarchie doit rester courte, compréhensible, testable, et ne pas masquer les dépendances. + +### Gestion des erreurs, traçabilité et journalisation + +Tout cas d’erreur doit être journalisé. + +Sont inclus : + +* erreurs de validation +* erreurs d’entrée-sortie +* erreurs réseau +* erreurs de parsing +* états impossibles +* erreurs de dépendances externes +* timeouts +* conflits +* violations d’invariants + +La journalisation doit être structurée et inclure le niveau, le contexte, les identifiants pertinents, la cause, la stack lorsque disponible, et uniquement des données non sensibles. + +Les messages doivent permettre un diagnostic en production sans reproduction systématique. + +Aucune erreur ne doit être ignorée silencieusement. + +Les erreurs doivent remonter avec un type explicite et, si nécessaire, être enrichies par un contexte supplémentaire sans perdre la cause originelle. + +Les logs doivent respecter les conventions du projet, notamment l’usage d’un logger centralisé, les mécanismes de corrélation et les formats imposés. L’usage direct de `console.log` est interdit si un système de journalisation est déjà en place. + +### Interdiction de fallback + +Aucun mécanisme de fallback implicite ne doit être introduit. + +Sont considérés comme fallback interdits : + +* l’utilisation de valeurs par défaut pour masquer une erreur +* l’attrapage d’une erreur suivi d’une poursuite silencieuse +* le déclenchement d’une voie alternative non explicitement spécifiée +* toute dégradation silencieuse de la qualité, de la sécurité ou de la cohérence + +Si un comportement alternatif est requis fonctionnellement, il doit être explicitement défini comme un chemin nominal, avec des conditions d’activation claires, un typage explicite et une observabilité complète. + +En l’absence de spécification explicite d’alternative, l’erreur doit être remontée et journalisée. + +### Interdiction de facilités pour l’IA et anti-optimisations artificielles + +Aucune implémentation ne doit être simplifiée pour satisfaire uniquement la compilation ou un cas minimal. + +Sont interdits : + +* stubs durables +* hacks temporaires laissés en place +* branches mortes +* TODO structurants non résolus +* implémentations partielles non signalées +* comportements best effort non spécifiés + +L’optimisation pour l’IA elle-même est interdite. Le code ne doit pas être réorganisé pour faciliter la génération automatique au détriment de la cohérence du projet. + +L’optimisation de performance n’est pas un objectif par défaut. Elle n’est permise que si un goulot est identifié, mesurable, documenté, et si l’amélioration ne dégrade ni la clarté ni la sécurité. + +### Tests + +Aucun test ad hoc ne doit être ajouté. + +Sont considérés comme ad hoc : + +* tests écrits pour forcer un comportement non contractuel +* tests dupliquant la logique de production +* tests utilisant des mocks excessifs pour faire passer un scénario +* tests ne respectant pas la stratégie existante du projet + +Si le projet dispose d’une stratégie de tests, elle doit être strictement respectée. + +Si une couverture supplémentaire est nécessaire, elle doit s’inscrire dans les patterns de tests existants, avec les fixtures, helpers et conventions en place, sans introduire une nouvelle infrastructure. + +En l’absence de tests pour la zone concernée, aucune approche improvisée ne doit être introduite. Les points de vérification attendus peuvent être documentés, mais l’implémentation doit s’arrêter au code de production, sauf instruction contraire. + +### Processus de modification du code + +Toute modification doit être minimale en surface et maximale en cohérence. + +Les changements doivent être localisés aux endroits architecturalement pertinents, sans dispersion. + +L’architecture en couches du projet doit être respectée, qu’il s’agisse du domaine, de l’application, de l’infrastructure ou de la présentation. + +Toute nouvelle dépendance doit être justifiée et alignée avec le socle existant. + +Aucune bibliothèque ne doit être introduite si une capacité équivalente existe déjà dans le projet, sauf contrainte documentée. + +Les noms, interfaces, types et contrats doivent être stables, explicites et orientés intention. + +### Documentation et contrats + +Les fonctions publiques, classes et modules introduits doivent être auto-descriptifs par leur nommage et leur typage. + +Les commentaires ne doivent pas répéter le code. Ils doivent expliciter : + +* les invariants +* les hypothèses +* les contrats +* les cas limites +* les raisons d’un choix de conception + +Les invariants critiques doivent être vérifiés explicitement. En cas de violation, une erreur typée et journalisée doit être levée. + +Les interfaces doivent éviter les paramètres optionnels fourre-tout. Les objets de configuration typés sont préférés, avec des champs requis ou optionnels clairement justifiés. + +### Critères d’acceptation implicites + +Le code final doit : + +* compiler sans contournement +* respecter le linting sans suppression +* réduire, et non augmenter, la duplication +* introduire des abstractions justifiées et localisées +* isoler les cas particuliers sans répliquer le flux principal +* être observable en erreur via des logs structurés +* ne contenir aucun fallback implicite +* ne pas introduire de tests ad hoc + +## Conclusion + +Ces consignes constituent un cadre de production strict. Elles imposent une analyse préalable via un arbre des fichiers, un typage TypeScript sans contournement, une non-duplication systématique, une architecture fondée sur des abstractions pertinentes, l’usage raisonné de patterns, une journalisation exhaustive des erreurs et un refus explicite des fallbacks implicites. Appliquées rigoureusement, elles conduisent à un code TypeScript robuste, évolutif et cohérent avec un référentiel de qualité de haut niveau. diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..815dd24 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,5 @@ +.eslintrc.json +.eslintignore +.eslintrc.json +.eslintignore +.env.local \ No newline at end of file diff --git a/README.md b/README.md index 97dd632..28147cd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Nostr Paywall +# zapwall4Science -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. +Plateforme de publication d'articles scientifiques et de science-fiction avec système de sponsoring, commissions et rémunération des avis. Les lecteurs peuvent lire les aperçus gratuitement et débloquer le contenu complet en payant avec Lightning Network. ## Features diff --git a/components/AlbyInstaller.tsx b/components/AlbyInstaller.tsx index f909492..96f624d 100644 --- a/components/AlbyInstaller.tsx +++ b/components/AlbyInstaller.tsx @@ -1,16 +1,81 @@ -import { useEffect, useState } from 'react' +import { useEffect, useState, useCallback } from 'react' import { getAlbyService } from '@/lib/alby' interface AlbyInstallerProps { onInstalled?: () => void } -export function AlbyInstaller({ onInstalled }: AlbyInstallerProps) { +function InfoIcon() { + return ( + + + + ) +} + +function InstallerActions({ onInstalled, markInstalled }: { onInstalled?: () => void; markInstalled: () => void }) { + const connect = useCallback(() => { + const alby = getAlbyService() + void alby.enable().then(() => { + markInstalled() + onInstalled?.() + }) + }, [markInstalled, onInstalled]) + + return ( +
+
+ + Install Alby + + +
+
+ ) +} + +function InstallerBody({ onInstalled, markInstalled }: { onInstalled?: () => void; markInstalled: () => void }) { + return ( +
+

Alby Extension Required

+
+

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

+
+ +
+

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

+
+
+ ) +} + +function useAlbyStatus(onInstalled?: () => void) { const [isInstalled, setIsInstalled] = useState(false) const [isChecking, setIsChecking] = useState(true) useEffect(() => { - const checkAlby = async () => { + const checkAlby = () => { try { const alby = getAlbyService() const installed = alby.isEnabled() @@ -25,15 +90,20 @@ export function AlbyInstaller({ onInstalled }: AlbyInstallerProps) { setIsChecking(false) } } - checkAlby() }, [onInstalled]) - if (isChecking) { - return null + const markInstalled = () => { + setIsInstalled(true) } - if (isInstalled) { + return { isInstalled, isChecking, markInstalled } +} + +export function AlbyInstaller({ onInstalled }: AlbyInstallerProps) { + const { isInstalled, isChecking, markInstalled } = useAlbyStatus(onInstalled) + + if (isChecking || isInstalled) { return null } @@ -41,60 +111,9 @@ export function AlbyInstaller({ onInstalled }: AlbyInstallerProps) {
- - - -
-
-

- 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 index e398a38..82531a6 100644 --- a/components/ArticleCard.tsx +++ b/components/ArticleCard.tsx @@ -9,6 +9,36 @@ interface ArticleCardProps { onUnlock?: (article: Article) => void } +function ArticleMeta({ + article, + error, + paymentInvoice, + onClose, + onPaymentComplete, +}: { + article: Article + error: string | null + paymentInvoice: ReturnType['paymentInvoice'] + onClose: () => void + onPaymentComplete: () => void +}) { + return ( + <> + {error &&

{error}

} +
+ Published {new Date(article.createdAt * 1000).toLocaleDateString()} +
+ {paymentInvoice && ( + + )} + + ) +} + export function ArticleCard({ article, onUnlock }: ArticleCardProps) { const { connected, pubkey } = useNostrConnect() const { @@ -30,20 +60,20 @@ export function ArticleCard({ article, onUnlock }: ArticleCardProps) { article={article} connected={connected} loading={loading} - onUnlock={handleUnlockClick} + onUnlock={() => { + void handleUnlock() + }} /> - {error &&

{error}

} -
- Published {new Date(article.createdAt * 1000).toLocaleDateString()} -
- {paymentInvoice && ( - - )} + { + void handlePaymentComplete() + }} + /> ) } diff --git a/components/ArticleEditor.tsx b/components/ArticleEditor.tsx index 64f7ec9..f383f5a 100644 --- a/components/ArticleEditor.tsx +++ b/components/ArticleEditor.tsx @@ -34,6 +34,7 @@ export function ArticleEditor({ onPublishSuccess, onCancel }: ArticleEditorProps preview: '', content: '', zapAmount: 800, + category: undefined, }) const handleSubmit = async (e: React.FormEvent) => { @@ -56,7 +57,9 @@ export function ArticleEditor({ onPublishSuccess, onCancel }: ArticleEditorProps { + void handleSubmit(e) + }} loading={loading} error={error} onCancel={onCancel} diff --git a/components/ArticleEditorForm.tsx b/components/ArticleEditorForm.tsx index a8502f7..22204e6 100644 --- a/components/ArticleEditorForm.tsx +++ b/components/ArticleEditorForm.tsx @@ -2,6 +2,7 @@ import React from 'react' import type { ArticleDraft } from '@/lib/articlePublisher' import { ArticleField } from './ArticleField' import { ArticleFormButtons } from './ArticleFormButtons' +import { CategorySelect } from './CategorySelect' interface ArticleEditorFormProps { draft: ArticleDraft @@ -12,6 +13,99 @@ interface ArticleEditorFormProps { onCancel?: () => void } +function CategoryField({ + value, + onChange, +}: { + value: ArticleDraft['category'] + onChange: (value: ArticleDraft['category']) => void +}) { + return ( + + ) +} + +function ErrorAlert({ error }: { error: string | null }) { + if (!error) { + return null + } + return ( +
+

{error}

+
+ ) +} + +const ArticleFieldsLeft = ({ + draft, + onDraftChange, +}: { + draft: ArticleDraft + onDraftChange: (draft: ArticleDraft) => void +}) => ( +
+ onDraftChange({ ...draft, category: value })} /> + onDraftChange({ ...draft, title: value as string })} + required + placeholder="Entrez le titre de l'article" + /> + onDraftChange({ ...draft, preview: value as string })} + required + type="textarea" + rows={4} + placeholder="Cet aperçu sera visible par tous gratuitement" + helpText="Ce contenu sera visible par tous" + /> +
+) + +const ArticleFieldsRight = ({ + draft, + onDraftChange, +}: { + draft: ArticleDraft + onDraftChange: (draft: ArticleDraft) => void +}) => ( +
+ onDraftChange({ ...draft, content: value as string })} + required + type="textarea" + rows={8} + placeholder="Ce contenu sera chiffré et envoyé aux lecteurs qui paient" + helpText="Ce contenu sera chiffré et envoyé comme message privé après paiement" + /> + onDraftChange({ ...draft, zapAmount: value as number })} + required + type="number" + min={1} + helpText="Montant en satoshis pour débloquer le contenu complet" + /> +
+) + export function ArticleEditorForm({ draft, onDraftChange, @@ -22,58 +116,12 @@ export function ArticleEditorForm({ }: 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}

-
- )} - +

Publier un nouvel article

+
+ + +
+ ) diff --git a/components/ArticleField.tsx b/components/ArticleField.tsx index 41e82b4..fb18082 100644 --- a/components/ArticleField.tsx +++ b/components/ArticleField.tsx @@ -13,52 +13,106 @@ interface ArticleFieldProps { min?: number } -export function ArticleField({ +function NumberOrTextInput({ id, - label, + type, value, - onChange, - required = false, - type = 'text', - rows, placeholder, - helpText, + required, min, -}: ArticleFieldProps) { + onChange, + className, +}: { + id: string + type: 'text' | 'number' + value: string | number + placeholder?: string + required: boolean + min?: number + className: string + onChange: (value: string | number) => void +}) { + return ( + onChange(type === 'number' ? Number(e.target.value) || 0 : e.target.value)} + className={className} + placeholder={placeholder} + min={min} + required={required} + /> + ) +} + +function TextAreaInput({ + id, + value, + placeholder, + required, + rows, + className, + onChange, +}: { + id: string + value: string | number + placeholder?: string + required: boolean + rows?: number + className: string + onChange: (value: string | number) => void +}) { + return ( +