-
Créer un compte
+
{t('account.create.title')}
- Créez un nouveau compte Nostr ou importez une clé privée existante.
+ {t('account.create.description')}
{error &&
{error}
}
diff --git a/components/ImageUploadField.tsx b/components/ImageUploadField.tsx
index 1775bf6..3564622 100644
--- a/components/ImageUploadField.tsx
+++ b/components/ImageUploadField.tsx
@@ -112,7 +112,7 @@ function useImageUpload(onChange: (url: string) => void) {
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e))
// Check if unlock is required
- if (error.message === 'UNLOCK_REQUIRED' || (error as any).unlockRequired) {
+ if (error.message === 'UNLOCK_REQUIRED' || ('unlockRequired' in error && (error as { unlockRequired?: boolean }).unlockRequired)) {
setPendingFile(file)
setShowUnlockModal(true)
setError(null) // Don't show error, show unlock modal instead
diff --git a/components/KeyManagementManager.tsx b/components/KeyManagementManager.tsx
index cc9baa7..07e91e4 100644
--- a/components/KeyManagementManager.tsx
+++ b/components/KeyManagementManager.tsx
@@ -102,7 +102,7 @@ export function KeyManagementManager() {
if (typeof decoded.data !== 'string' && !(decoded.data instanceof Uint8Array)) {
throw new Error('Invalid nsec format')
}
- } catch (e) {
+ } catch {
// If decoding failed, assume it's hex, validate length (64 hex chars = 32 bytes)
if (!/^[0-9a-f]{64}$/i.test(extractedKey)) {
setError(t('settings.keyManagement.import.error.invalid'))
@@ -137,6 +137,12 @@ export function KeyManagementManager() {
setImportKey('')
setShowImportForm(false)
await loadKeys()
+
+ // Sync user content to IndexedDB cache
+ if (result.publicKey) {
+ const { syncUserContentToCache } = await import('@/lib/userContentSync')
+ void syncUserContentToCache(result.publicKey)
+ }
} catch (e) {
const errorMessage = e instanceof Error ? e.message : t('settings.keyManagement.import.error.failed')
setError(errorMessage)
diff --git a/components/MarkdownEditor.tsx b/components/MarkdownEditor.tsx
index cfe7bab..90025eb 100644
--- a/components/MarkdownEditor.tsx
+++ b/components/MarkdownEditor.tsx
@@ -65,10 +65,10 @@ function MarkdownToolbar({
return (
- {preview ? 'Éditer' : 'Preview'}
+ {preview ? t('upload.edit') : t('upload.preview')}
- Upload media (NIP-95)
+ {t('markdown.upload.media')}
- {uploading && Upload en cours... }
+ {uploading && {t('markdown.upload.uploading')} }
{error && {error} }
)
diff --git a/components/NotificationPanel.tsx b/components/NotificationPanel.tsx
index dd9c9f8..40b2956 100644
--- a/components/NotificationPanel.tsx
+++ b/components/NotificationPanel.tsx
@@ -1,6 +1,7 @@
import type { Notification } from '@/types/notifications'
import { NotificationItem } from './NotificationItem'
import { NotificationPanelHeader } from './NotificationPanelHeader'
+import { t } from '@/lib/i18n'
interface NotificationPanelProps {
notifications: Notification[]
@@ -19,7 +20,7 @@ function NotificationList({ notifications, onNotificationClick, onDelete }: {
if (notifications.length === 0) {
return (
-
No notifications yet
+
{t('notification.empty')}
)
}
diff --git a/components/NotificationPanelHeader.tsx b/components/NotificationPanelHeader.tsx
index 62d7199..c54a897 100644
--- a/components/NotificationPanelHeader.tsx
+++ b/components/NotificationPanelHeader.tsx
@@ -1,3 +1,4 @@
+import { t } from '@/lib/i18n'
interface NotificationPanelHeaderProps {
unreadCount: number
@@ -12,20 +13,20 @@ export function NotificationPanelHeader({
}: NotificationPanelHeaderProps) {
return (
-
Notifications
+
{t('notification.title')}
{unreadCount > 0 && (
- Mark all as read
+ {t('notification.markAllAsRead')}
)}
e.stopPropagation()}
>
diff --git a/components/PaymentModal.tsx b/components/PaymentModal.tsx
index c0430d7..e409877 100644
--- a/components/PaymentModal.tsx
+++ b/components/PaymentModal.tsx
@@ -3,6 +3,7 @@ import QRCode from 'react-qr-code'
import type { AlbyInvoice } from '@/types/alby'
import { getAlbyService, isWebLNAvailable } from '@/lib/alby'
import { AlbyInstaller } from './AlbyInstaller'
+import { t } from '@/lib/i18n'
interface PaymentModalProps {
invoice: AlbyInvoice
@@ -44,7 +45,7 @@ function PaymentHeader({
return null
}
if (timeRemaining <= 0) {
- return 'Expired'
+ return t('payment.expired')
}
const minutes = Math.floor(timeRemaining / 60)
const secs = timeRemaining % 60
@@ -54,10 +55,10 @@ function PaymentHeader({
return (
-
Zap de {amount} sats
+
{t('payment.modal.zapAmount', { amount })}
{timeLabel && (
- Time remaining: {timeLabel}
+ {t('payment.modal.timeRemaining', { time: timeLabel })}
)}
@@ -71,7 +72,7 @@ function PaymentHeader({
function InvoiceDisplay({ invoiceText, paymentUrl }: { invoiceText: string; paymentUrl: string }) {
return (
-
Lightning Invoice:
+
{t('payment.modal.lightningInvoice')}
{invoiceText}
@@ -83,7 +84,7 @@ function InvoiceDisplay({ invoiceText, paymentUrl }: { invoiceText: string; paym
/>
-
Scan with your Lightning wallet to pay
+
{t('payment.modal.scanQr')}
)
}
@@ -105,13 +106,13 @@ function PaymentActions({
}}
className="flex-1 px-4 py-2 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg font-medium transition-colors"
>
- {copied ? 'Copied!' : 'Copy Invoice'}
+ {copied ? t('payment.modal.copied') : t('payment.modal.copyInvoice')}
- Pay with Alby
+ {t('payment.modal.payWithAlby')}
)
@@ -123,8 +124,8 @@ function ExpiredNotice({ show }: { show: boolean }) {
}
return (
-
This invoice has expired
-
Please close this modal and try again to generate a new invoice.
+
{t('payment.modal.invoiceExpired')}
+
{t('payment.modal.invoiceExpiredHelp')}
)
}
@@ -142,7 +143,7 @@ function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => voi
setTimeout(() => setCopied(false), 2000)
} catch (e) {
console.error('Failed to copy:', e)
- setErrorMessage('Failed to copy the invoice')
+ setErrorMessage(t('payment.modal.copyFailed'))
}
}, [invoice.invoice])
@@ -150,7 +151,7 @@ function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => voi
try {
const alby = getAlbyService()
if (!isWebLNAvailable()) {
- throw new Error('WebLN is not available. Please install Alby or another Lightning wallet extension.')
+ throw new Error(t('payment.modal.weblnNotAvailable'))
}
await alby.enable()
await alby.sendPayment(invoice.invoice)
@@ -193,7 +194,7 @@ export function PaymentModal({ invoice, onClose, onPaymentComplete }: PaymentMod
)}
- Payment will be automatically verified once completed
+ {t('payment.modal.autoVerify')}
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..57d89d2
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,245 @@
+import js from '@eslint/js'
+import typescriptEslint from '@typescript-eslint/eslint-plugin'
+import typescriptParser from '@typescript-eslint/parser'
+import react from 'eslint-plugin-react'
+import reactHooks from 'eslint-plugin-react-hooks'
+import unusedImports from 'eslint-plugin-unused-imports'
+
+export default [
+ {
+ ignores: ['.next/**', 'node_modules/**', 'out/**', 'build/**'],
+ },
+ js.configs.recommended,
+ {
+ files: ['**/*.{js,jsx,ts,tsx}'],
+ languageOptions: {
+ parser: typescriptParser,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ project: './tsconfig.json',
+ },
+ globals: {
+ console: 'readonly',
+ window: 'readonly',
+ document: 'readonly',
+ navigator: 'readonly',
+ process: 'readonly',
+ Buffer: 'readonly',
+ setTimeout: 'readonly',
+ clearTimeout: 'readonly',
+ setInterval: 'readonly',
+ clearInterval: 'readonly',
+ confirm: 'readonly',
+ alert: 'readonly',
+ React: 'readonly',
+ __dirname: 'readonly',
+ __filename: 'readonly',
+ },
+ },
+ plugins: {
+ '@typescript-eslint': typescriptEslint,
+ react,
+ 'react-hooks': reactHooks,
+ 'unused-imports': unusedImports,
+ },
+ settings: {
+ react: {
+ version: 'detect',
+ },
+ },
+ rules: {
+ ...typescriptEslint.configs.recommended.rules,
+ ...react.configs.recommended.rules,
+ ...reactHooks.configs.recommended.rules,
+
+ // Longueurs de fichiers et fonctions
+ 'max-lines': ['error', { max: 250, skipBlankLines: true, skipComments: true }],
+ 'max-lines-per-function': ['error', { max: 40, skipBlankLines: true, skipComments: true }],
+ 'max-params': ['error', { max: 4 }], // Max 4 paramètres par fonction
+ 'max-depth': ['error', { max: 4 }], // Profondeur d'imbrication max 4
+ 'complexity': ['error', { max: 10 }], // Complexité cyclomatique max 10
+ 'max-nested-callbacks': ['error', { max: 3 }], // Callbacks imbriqués max 3
+
+ // Imports inutiles
+ 'unused-imports/no-unused-imports': 'error',
+ 'unused-imports/no-unused-vars': [
+ 'error',
+ {
+ vars: 'all',
+ varsIgnorePattern: '^_',
+ args: 'after-used',
+ argsIgnorePattern: '^_',
+ },
+ ],
+ '@typescript-eslint/no-unused-vars': 'off', // Désactivé car remplacé par unused-imports
+
+ // Retours de fonctions non décrits
+ '@typescript-eslint/explicit-function-return-type': [
+ 'error',
+ {
+ allowExpressions: true,
+ allowTypedFunctionExpressions: true,
+ allowHigherOrderFunctions: true,
+ allowDirectConstAssertionInArrowFunctions: true,
+ },
+ ],
+
+ // Erreurs sur les valeurs null
+ '@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',
+ '@typescript-eslint/no-unnecessary-type-assertion': 'error',
+
+ // Promesses et async
+ '@typescript-eslint/no-floating-promises': 'error',
+ '@typescript-eslint/no-misused-promises': 'error',
+ '@typescript-eslint/await-thenable': 'error',
+ 'no-return-await': 'error',
+
+ // Bonnes pratiques JavaScript/TypeScript
+ 'prefer-const': 'error',
+ 'no-var': 'error',
+ 'object-shorthand': 'error',
+ 'prefer-template': 'error',
+ 'eqeqeq': 'error',
+ 'curly': ['error', 'all'], // Accolades obligatoires même pour une ligne
+ 'no-throw-literal': 'error',
+ 'no-implicit-coercion': 'error', // Pas de coercition implicite
+ 'no-useless-concat': 'error', // Concaténation inutile
+ 'no-useless-return': 'error', // Return inutile
+ 'no-useless-constructor': 'error', // Constructeur inutile
+ 'no-else-return': 'error', // Pas de else après return
+ 'prefer-arrow-callback': 'error', // Préférer arrow functions
+ 'prefer-destructuring': ['error', { object: true, array: false }], // Préférer destructuring objets
+ 'prefer-spread': 'error', // Préférer spread à apply
+ 'prefer-rest-params': 'error', // Préférer rest params à arguments
+ 'no-param-reassign': ['error', { props: true }], // Pas de réassignation de paramètres
+ 'no-return-assign': 'error', // Pas d'assignation dans return
+ 'no-sequences': 'error', // Pas de séquences d'expressions
+ 'no-nested-ternary': 'error', // Pas de ternaires imbriqués
+ 'no-unneeded-ternary': 'error', // Pas de ternaire inutile
+ 'no-lonely-if': 'error', // Pas de if seul dans else
+ 'no-confusing-arrow': 'error', // Pas de flèches confuses
+ 'no-iterator': 'error', // Pas d'itérateurs __iterator__
+ 'no-proto': 'error', // Pas de __proto__
+ 'no-array-constructor': 'error', // Pas de Array() constructor
+ 'no-new-object': 'error', // Pas de new Object()
+ 'no-new-wrappers': 'error', // Pas de new String/Number/Boolean
+ 'no-bitwise': 'error', // Pas d'opérateurs bitwise
+ 'no-continue': 'error', // Pas de continue
+ 'no-labels': 'error', // Pas de labels
+ 'no-restricted-syntax': [
+ 'error',
+ {
+ selector: 'ForInStatement',
+ message: 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.',
+ },
+ {
+ selector: 'LabeledStatement',
+ message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.',
+ },
+ {
+ selector: 'WithStatement',
+ message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
+ },
+ ],
+
+ // Console/Debug
+ 'no-console': ['warn', { allow: ['warn', 'error'] }],
+ 'no-debugger': 'error',
+ 'no-alert': 'error',
+
+ // TypeScript - Types et sécurité
+ '@typescript-eslint/no-explicit-any': 'error',
+ '@typescript-eslint/no-require-imports': 'off',
+ '@typescript-eslint/explicit-module-boundary-types': 'error', // Types explicites pour exports
+ '@typescript-eslint/no-unsafe-assignment': 'error', // Assignations non sûres
+ '@typescript-eslint/no-unsafe-member-access': 'error', // Accès membres non sûrs
+ '@typescript-eslint/no-unsafe-call': 'error', // Appels non sûrs
+ '@typescript-eslint/no-unsafe-return': 'error', // Retours non sûrs
+ '@typescript-eslint/no-unsafe-argument': 'error', // Arguments non sûrs
+ '@typescript-eslint/restrict-template-expressions': 'error', // Expressions template restreintes
+ '@typescript-eslint/restrict-plus-operands': 'error', // Opérandes + restreints
+ '@typescript-eslint/no-redundant-type-constituents': 'error', // Types redondants
+ '@typescript-eslint/prefer-reduce-type-parameter': 'error', // Préférer type parameter pour reduce
+ '@typescript-eslint/prefer-includes': 'error', // Préférer includes à indexOf
+ '@typescript-eslint/prefer-string-starts-ends-with': 'error', // Préférer startsWith/endsWith
+
+ // React - Qualité et performance
+ 'react/react-in-jsx-scope': 'off',
+ 'react/prop-types': 'off',
+ 'react-hooks/refs': 'off',
+ 'react/jsx-key': 'error', // Clés obligatoires dans les listes
+ 'react/jsx-no-duplicate-props': 'error', // Pas de props dupliquées
+ 'react/jsx-no-undef': 'error', // Pas de variables non définies dans JSX
+ 'react/jsx-uses-react': 'off', // Désactivé avec React 17+
+ 'react/jsx-uses-vars': 'error', // Variables utilisées dans JSX
+ 'react/no-array-index-key': 'warn', // Éviter index comme key
+ 'react/no-children-prop': 'error', // Pas de children comme prop
+ 'react/no-danger-with-children': 'error', // Pas de dangerouslySetInnerHTML avec children
+ 'react/no-deprecated': 'error', // Pas d'API dépréciées
+ 'react/no-direct-mutation-state': 'error', // Pas de mutation directe du state
+ 'react/no-find-dom-node': 'error', // Pas de findDOMNode
+ 'react/no-is-mounted': 'error', // Pas de isMounted
+ 'react/no-render-return-value': 'error', // Pas de valeur de retour de render
+ 'react/no-string-refs': 'error', // Pas de string refs
+ 'react/no-unescaped-entities': 'error', // Pas d'entités non échappées
+ 'react/no-unknown-property': 'error', // Pas de propriétés inconnues
+ 'react/require-render-return': 'error', // Return obligatoire dans render
+ 'react/self-closing-comp': 'error', // Composants auto-fermants
+ 'react/jsx-boolean-value': ['error', 'never'], // Pas de boolean explicite dans JSX
+ 'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never' }], // Pas de {} inutiles
+ 'react/jsx-fragments': ['error', 'syntax'], // Préférer <> à
+ 'react/jsx-no-useless-fragment': 'error', // Pas de fragments inutiles
+ 'react/jsx-pascal-case': 'error', // PascalCase pour les composants
+ 'react/no-unstable-nested-components': 'error', // Pas de composants imbriqués instables
+ 'react-hooks/exhaustive-deps': 'error', // Dépendances exhaustives
+
+ // Sécurité et patterns dangereux
+ 'no-eval': 'error', // Pas d'eval
+ 'no-implied-eval': 'error', // Pas d'implied eval
+ 'no-new-func': 'error', // Pas de new Function()
+ 'no-script-url': 'error', // Pas de javascript: URLs
+ 'no-void': 'off', // Désactivé car utilisé pour ignorer les promesses (void promise)
+ 'no-with': 'error', // Pas de with statement
+ 'no-caller': 'error', // Pas de caller
+ 'no-extend-native': 'error', // Pas d'extension de natives
+ 'no-global-assign': 'error', // Pas d'assignation de globals
+ 'no-implicit-globals': 'error', // Pas de globals implicites
+ 'no-restricted-globals': ['error', 'event', 'fdescribe'], // Globals restreints
+ 'no-shadow-restricted-names': 'error', // Pas d'ombre sur noms restreints
+
+ // Qualité et maintenabilité
+ 'no-misleading-character-class': 'off',
+ 'no-multi-assign': 'error', // Pas d'assignations multiples
+ 'no-multi-str': 'error', // Pas de strings multi-lignes
+ 'no-new': 'error', // Pas de new sans assignation
+ 'no-octal-escape': 'error', // Pas d'échappement octal
+ 'no-redeclare': 'error', // Pas de redéclaration
+ 'no-self-assign': 'error', // Pas d'auto-assignation
+ 'no-self-compare': 'error', // Pas d'auto-comparaison
+ 'no-shadow': 'off', // Désactivé car @typescript-eslint/no-shadow est meilleur
+ '@typescript-eslint/no-shadow': 'error', // Pas d'ombre de variables
+ 'no-undef-init': 'error', // Pas d'init à undefined
+ 'no-undefined': 'off', // undefined est parfois nécessaire
+ 'no-use-before-define': 'off', // Désactivé car @typescript-eslint est meilleur
+ '@typescript-eslint/no-use-before-define': ['error', { functions: false, classes: true, variables: true }],
+ 'no-useless-call': 'error', // Pas d'appel inutile
+ 'no-useless-computed-key': 'error', // Pas de clé calculée inutile
+ 'no-useless-rename': 'error', // Pas de rename inutile
+ 'no-whitespace-before-property': 'error', // Pas d'espace avant propriété
+ 'spaced-comment': ['error', 'always'], // Commentaires espacés
+ 'yoda': 'error', // Pas de Yoda conditions
+
+ // Accessibilité (si plugin disponible)
+ // 'jsx-a11y/alt-text': 'error',
+ // 'jsx-a11y/anchor-has-content': 'error',
+ // 'jsx-a11y/anchor-is-valid': 'error',
+ },
+ },
+]
diff --git a/lib/accessControl.ts b/lib/accessControl.ts
index 210d4a2..4bc55fb 100644
--- a/lib/accessControl.ts
+++ b/lib/accessControl.ts
@@ -11,6 +11,7 @@
import type { Event } from 'nostr-tools'
import { extractTagsFromEvent } from './nostrTagSystem'
import { canModifyObject } from './versionManager'
+import { t } from './i18n'
/**
* Check if a user can modify an object
@@ -88,11 +89,11 @@ export function getAccessControl(
let reason: string | undefined
if (isPaid && !canReadFullContent) {
- reason = 'Payment required to access full content'
+ reason = t('access.paymentRequired')
} else if (!canModify && userPubkey) {
- reason = 'Only the author can modify this object'
+ reason = t('access.onlyAuthorModify')
} else if (!canDelete && userPubkey) {
- reason = 'Only the author can delete this object'
+ reason = t('access.onlyAuthorDelete')
}
return {
diff --git a/lib/nostrAuth.ts b/lib/nostrAuth.ts
index 46d2224..4e67c11 100644
--- a/lib/nostrAuth.ts
+++ b/lib/nostrAuth.ts
@@ -63,6 +63,12 @@ export class NostrAuthService {
void this.saveStateToStorage()
this.notifyListeners()
+ // Sync user content to IndexedDB cache (background operation)
+ if (result.publicKey) {
+ const { syncUserContentToCache } = await import('@/lib/userContentSync')
+ void syncUserContentToCache(result.publicKey)
+ }
+
return result
}
diff --git a/lib/userContentSync.ts b/lib/userContentSync.ts
new file mode 100644
index 0000000..96a7ed9
--- /dev/null
+++ b/lib/userContentSync.ts
@@ -0,0 +1,219 @@
+/**
+ * Synchronize user content (profile, series, publications) to IndexedDB cache
+ * Called after key import to ensure all user content is cached locally
+ */
+
+import type { Event } from 'nostr-tools'
+import { nostrService } from './nostr'
+import { fetchAuthorPresentationFromPool } from './articlePublisherHelpersPresentation'
+import { extractTagsFromEvent } from './nostrTagSystem'
+import { extractSeriesFromEvent, extractPublicationFromEvent } from './metadataExtractor'
+import { objectCache } from './objectCache'
+import { getLatestVersion } from './versionManager'
+import { buildTagFilter } from './nostrTagSystemFilter'
+import { getPrimaryRelaySync } from './config'
+import { PLATFORM_SERVICE, MIN_EVENT_DATE } from './platformConfig'
+import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
+
+/**
+ * Fetch all publications by an author and cache them
+ */
+async function fetchAndCachePublications(
+ pool: SimplePoolWithSub,
+ authorPubkey: string
+): Promise {
+ const filters = [
+ {
+ ...buildTagFilter({
+ type: 'publication',
+ authorPubkey,
+ service: PLATFORM_SERVICE,
+ }),
+ since: MIN_EVENT_DATE,
+ limit: 1000, // Get all publications
+ },
+ ]
+
+ const relayUrl = getPrimaryRelaySync()
+ const { createSubscription } = require('@/types/nostr-tools-extended')
+ const sub = createSubscription(pool, [relayUrl], filters)
+
+ const events: Event[] = []
+
+ return new Promise((resolve) => {
+ let finished = false
+
+ const done = async () => {
+ if (finished) {
+ return
+ }
+ finished = true
+ sub.unsub()
+
+ // Group events by hash ID and cache the latest version of each
+ const eventsByHashId = new Map()
+ for (const event of events) {
+ const tags = extractTagsFromEvent(event)
+ if (tags.id) {
+ const hashId = tags.id
+ if (!eventsByHashId.has(hashId)) {
+ eventsByHashId.set(hashId, [])
+ }
+ eventsByHashId.get(hashId)!.push(event)
+ }
+ }
+
+ // Cache each publication
+ for (const [hashId, hashEvents] of eventsByHashId.entries()) {
+ const latestEvent = getLatestVersion(hashEvents)
+ if (latestEvent) {
+ const extracted = await extractPublicationFromEvent(latestEvent)
+ if (extracted) {
+ const tags = extractTagsFromEvent(latestEvent)
+ await objectCache.set(
+ 'publication',
+ hashId,
+ latestEvent,
+ extracted,
+ tags.version ?? 0,
+ tags.hidden ?? false
+ )
+ }
+ }
+ }
+
+ resolve()
+ }
+
+ sub.on('event', (event: Event) => {
+ const tags = extractTagsFromEvent(event)
+ if (tags.type === 'publication' && !tags.hidden) {
+ events.push(event)
+ }
+ })
+
+ sub.on('eose', () => {
+ void done()
+ })
+
+ setTimeout(() => {
+ void done()
+ }, 10000).unref?.()
+ })
+}
+
+/**
+ * Fetch all series by an author and cache them
+ */
+async function fetchAndCacheSeries(
+ pool: SimplePoolWithSub,
+ authorPubkey: string
+): Promise {
+ // Fetch all events for series to cache them properly
+ const filters = [
+ {
+ ...buildTagFilter({
+ type: 'series',
+ authorPubkey,
+ service: PLATFORM_SERVICE,
+ }),
+ since: MIN_EVENT_DATE,
+ limit: 1000, // Get all series events
+ },
+ ]
+
+ const relayUrl = getPrimaryRelaySync()
+ const { createSubscription } = require('@/types/nostr-tools-extended')
+ const sub = createSubscription(pool, [relayUrl], filters)
+
+ const events: Event[] = []
+
+ return new Promise((resolve) => {
+ let finished = false
+
+ const done = async () => {
+ if (finished) {
+ return
+ }
+ finished = true
+ sub.unsub()
+
+ // Group events by hash ID and cache the latest version of each
+ const eventsByHashId = new Map()
+ for (const event of events) {
+ const tags = extractTagsFromEvent(event)
+ if (tags.id) {
+ const hashId = tags.id
+ if (!eventsByHashId.has(hashId)) {
+ eventsByHashId.set(hashId, [])
+ }
+ eventsByHashId.get(hashId)!.push(event)
+ }
+ }
+
+ // Cache each series
+ for (const [hashId, hashEvents] of eventsByHashId.entries()) {
+ const latestEvent = getLatestVersion(hashEvents)
+ if (latestEvent) {
+ const extracted = await extractSeriesFromEvent(latestEvent)
+ if (extracted) {
+ const tags = extractTagsFromEvent(latestEvent)
+ await objectCache.set(
+ 'series',
+ hashId,
+ latestEvent,
+ extracted,
+ tags.version ?? 0,
+ tags.hidden ?? false
+ )
+ }
+ }
+ }
+
+ resolve()
+ }
+
+ sub.on('event', (event: Event) => {
+ const tags = extractTagsFromEvent(event)
+ if (tags.type === 'series' && !tags.hidden) {
+ events.push(event)
+ }
+ })
+
+ sub.on('eose', () => {
+ void done()
+ })
+
+ setTimeout(() => {
+ void done()
+ }, 10000).unref?.()
+ })
+}
+
+/**
+ * Synchronize all user content to IndexedDB cache
+ * Fetches profile, series, and publications and caches them
+ */
+export async function syncUserContentToCache(userPubkey: string): Promise {
+ try {
+ const pool = nostrService.getPool()
+ if (!pool) {
+ console.warn('Pool not initialized, cannot sync user content')
+ return
+ }
+
+ const poolWithSub = pool as unknown as SimplePoolWithSub
+
+ // Fetch and cache author profile (already caches itself)
+ await fetchAuthorPresentationFromPool(poolWithSub, userPubkey)
+
+ // Fetch and cache all series
+ await fetchAndCacheSeries(poolWithSub, userPubkey)
+
+ // Fetch and cache all publications
+ await fetchAndCachePublications(poolWithSub, userPubkey)
+ } catch (error) {
+ console.error('Error syncing user content to cache:', error)
+ // Don't throw - this is a background operation
+ }
+}
diff --git a/locales/en.txt b/locales/en.txt
index c796e7e..dc5dd11 100644
--- a/locales/en.txt
+++ b/locales/en.txt
@@ -149,6 +149,31 @@ search.clear=Clear search
# Upload
upload.error.failed=Upload failed
+upload.edit=Edit
+upload.preview=Preview
+
+# Common author
+common.author=Author
+
+# Import
+import.loading=Importing...
+import.button=Import
+
+# Payment
+payment.expired=Expired
+
+# Article
+article.title=Title
+
+# Notification
+notification.title=Notifications
+notification.close=Close
+notification.markAllAsRead=Mark all as read
+
+# Account
+account.create.title=Create account
+account.create.description=Create a new Nostr account or import an existing private key.
+account.import.title=Import private key
# Notification
notification.delete=Delete notification
diff --git a/locales/fr.txt b/locales/fr.txt
index 58a176a..5f3deb2 100644
--- a/locales/fr.txt
+++ b/locales/fr.txt
@@ -220,3 +220,8 @@ settings.nip95.list.editUrl=Cliquer pour modifier l'URL
settings.nip95.note.title=Note :
settings.nip95.note.priority=Les endpoints sont essayés dans l'ordre de priorité (nombre plus bas = priorité plus haute). Seuls les endpoints activés seront utilisés pour les uploads.
settings.nip95.note.fallback=Si un endpoint échoue, le prochain endpoint activé sera essayé automatiquement.
+
+# Account
+account.create.title=Créer un compte
+account.create.description=Créez un nouveau compte Nostr ou importez une clé privée existante.
+account.import.title=Importer une clé privée
diff --git a/package-lock.json b/package-lock.json
index 3590d9e..03e2b5a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,13 +18,20 @@
"react-qr-code": "^2.0.18"
},
"devDependencies": {
+ "@eslint/eslintrc": "^3.3.3",
+ "@eslint/js": "^9.39.2",
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
+ "@typescript-eslint/eslint-plugin": "^8.52.0",
+ "@typescript-eslint/parser": "^8.52.0",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.1",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-unused-imports": "^4.3.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
@@ -74,7 +81,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -100,29 +106,6 @@
"url": "https://opencollective.com/babel"
}
},
- "node_modules/@babel/core/node_modules/json5": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "json5": "lib/cli.js"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/@babel/core/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@babel/generator": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
@@ -157,16 +140,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
@@ -318,9 +291,9 @@
}
},
"node_modules/@emnapi/core": {
- "version": "1.7.1",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
- "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
+ "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -330,9 +303,9 @@
}
},
"node_modules/@emnapi/runtime": {
- "version": "1.7.1",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
- "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
+ "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -351,9 +324,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.9.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
- "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -369,6 +342,19 @@
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
},
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@eslint-community/regexpp": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
@@ -1216,24 +1202,36 @@
}
},
"node_modules/@noble/curves": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
- "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
+ "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"license": "MIT",
"dependencies": {
- "@noble/hashes": "1.3.1"
+ "@noble/hashes": "1.3.2"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/curves/node_modules/@noble/hashes": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
+ "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
- "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
- "node": ">= 16"
+ "node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
@@ -1329,6 +1327,42 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/@scure/bip32/node_modules/@noble/curves": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
+ "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "1.3.1"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
+ "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/bip32/node_modules/@noble/hashes": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz",
+ "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/@scure/bip39": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
@@ -1342,6 +1376,18 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/@scure/bip39/node_modules/@noble/hashes": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz",
+ "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1678,7 +1724,6 @@
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1694,20 +1739,20 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.50.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz",
- "integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz",
+ "integrity": "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.50.1",
- "@typescript-eslint/type-utils": "8.50.1",
- "@typescript-eslint/utils": "8.50.1",
- "@typescript-eslint/visitor-keys": "8.50.1",
- "ignore": "^7.0.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.52.0",
+ "@typescript-eslint/type-utils": "8.52.0",
+ "@typescript-eslint/utils": "8.52.0",
+ "@typescript-eslint/visitor-keys": "8.52.0",
+ "ignore": "^7.0.5",
"natural-compare": "^1.4.0",
- "ts-api-utils": "^2.1.0"
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1717,7 +1762,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.50.1",
+ "@typescript-eslint/parser": "^8.52.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
@@ -1733,18 +1778,17 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.50.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz",
- "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz",
+ "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "8.50.1",
- "@typescript-eslint/types": "8.50.1",
- "@typescript-eslint/typescript-estree": "8.50.1",
- "@typescript-eslint/visitor-keys": "8.50.1",
- "debug": "^4.3.4"
+ "@typescript-eslint/scope-manager": "8.52.0",
+ "@typescript-eslint/types": "8.52.0",
+ "@typescript-eslint/typescript-estree": "8.52.0",
+ "@typescript-eslint/visitor-keys": "8.52.0",
+ "debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1759,15 +1803,15 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.50.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.1.tgz",
- "integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.52.0.tgz",
+ "integrity": "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.50.1",
- "@typescript-eslint/types": "^8.50.1",
- "debug": "^4.3.4"
+ "@typescript-eslint/tsconfig-utils": "^8.52.0",
+ "@typescript-eslint/types": "^8.52.0",
+ "debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1781,14 +1825,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.50.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz",
- "integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.52.0.tgz",
+ "integrity": "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.50.1",
- "@typescript-eslint/visitor-keys": "8.50.1"
+ "@typescript-eslint/types": "8.52.0",
+ "@typescript-eslint/visitor-keys": "8.52.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1799,9 +1843,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.50.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz",
- "integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.52.0.tgz",
+ "integrity": "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1816,17 +1860,17 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.50.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz",
- "integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.52.0.tgz",
+ "integrity": "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.50.1",
- "@typescript-eslint/typescript-estree": "8.50.1",
- "@typescript-eslint/utils": "8.50.1",
- "debug": "^4.3.4",
- "ts-api-utils": "^2.1.0"
+ "@typescript-eslint/types": "8.52.0",
+ "@typescript-eslint/typescript-estree": "8.52.0",
+ "@typescript-eslint/utils": "8.52.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1841,9 +1885,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.50.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz",
- "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.52.0.tgz",
+ "integrity": "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1855,21 +1899,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.50.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz",
- "integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.52.0.tgz",
+ "integrity": "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.50.1",
- "@typescript-eslint/tsconfig-utils": "8.50.1",
- "@typescript-eslint/types": "8.50.1",
- "@typescript-eslint/visitor-keys": "8.50.1",
- "debug": "^4.3.4",
- "minimatch": "^9.0.4",
- "semver": "^7.6.0",
+ "@typescript-eslint/project-service": "8.52.0",
+ "@typescript-eslint/tsconfig-utils": "8.52.0",
+ "@typescript-eslint/types": "8.52.0",
+ "@typescript-eslint/visitor-keys": "8.52.0",
+ "debug": "^4.4.3",
+ "minimatch": "^9.0.5",
+ "semver": "^7.7.3",
"tinyglobby": "^0.2.15",
- "ts-api-utils": "^2.1.0"
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1908,17 +1952,30 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@typescript-eslint/utils": {
- "version": "8.50.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.1.tgz",
- "integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.52.0.tgz",
+ "integrity": "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.7.0",
- "@typescript-eslint/scope-manager": "8.50.1",
- "@typescript-eslint/types": "8.50.1",
- "@typescript-eslint/typescript-estree": "8.50.1"
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.52.0",
+ "@typescript-eslint/types": "8.52.0",
+ "@typescript-eslint/typescript-estree": "8.52.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1933,13 +1990,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.50.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz",
- "integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.52.0.tgz",
+ "integrity": "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.50.1",
+ "@typescript-eslint/types": "8.52.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -1950,19 +2007,6 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
- "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
@@ -2238,7 +2282,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2628,7 +2671,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2703,9 +2745,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001761",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
- "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
+ "version": "1.0.30001762",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
+ "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
"funding": [
{
"type": "opencollective",
@@ -2964,6 +3006,19 @@
"wrappy": "1"
}
},
+ "node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -3208,7 +3263,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3394,7 +3448,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -3433,29 +3486,6 @@
"ms": "^2.1.1"
}
},
- "node_modules/eslint-plugin-import/node_modules/doctrine": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
- "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/eslint-plugin-import/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/eslint-plugin-jsx-a11y": {
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
@@ -3539,19 +3569,6 @@
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
}
},
- "node_modules/eslint-plugin-react/node_modules/doctrine": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
- "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/eslint-plugin-react/node_modules/resolve": {
"version": "2.0.0-next.5",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
@@ -3570,14 +3587,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint-plugin-react/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "node_modules/eslint-plugin-unused-imports": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.3.0.tgz",
+ "integrity": "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==",
"dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
+ "license": "MIT",
+ "peerDependencies": {
+ "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0",
+ "eslint": "^9.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@typescript-eslint/eslint-plugin": {
+ "optional": true
+ }
}
},
"node_modules/eslint-scope": {
@@ -3598,19 +3621,6 @@
}
},
"node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
@@ -3641,23 +3651,10 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/espree/node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
- "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
"node_modules/esquery": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
- "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -4319,6 +4316,19 @@
"semver": "^7.7.1"
}
},
+ "node_modules/is-bun-module/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
@@ -4736,16 +4746,16 @@
"license": "MIT"
},
"node_modules/json5": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
- "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "minimist": "^1.2.0"
- },
"bin": {
"json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
}
},
"node_modules/jsx-ast-utils": {
@@ -5360,22 +5370,10 @@
}
}
},
- "node_modules/nostr-tools/node_modules/@noble/curves": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
- "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
- "license": "MIT",
- "dependencies": {
- "@noble/hashes": "1.3.2"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
- "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
+ "node_modules/nostr-tools/node_modules/@noble/hashes": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
+ "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"license": "MIT",
"engines": {
"node": ">= 16"
@@ -5678,7 +5676,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -5758,7 +5755,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5768,7 +5764,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -5977,16 +5972,13 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
- "devOptional": true,
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
}
},
"node_modules/set-function-length": {
@@ -6083,6 +6075,19 @@
"@img/sharp-win32-x64": "0.34.5"
}
},
+ "node_modules/sharp/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -6459,7 +6464,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -6481,9 +6485,9 @@
}
},
"node_modules/ts-api-utils": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz",
- "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6506,6 +6510,19 @@
"strip-bom": "^3.0.0"
}
},
+ "node_modules/tsconfig-paths/node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -6609,7 +6626,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6619,16 +6635,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.50.1",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.1.tgz",
- "integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==",
+ "version": "8.52.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.52.0.tgz",
+ "integrity": "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.50.1",
- "@typescript-eslint/parser": "8.50.1",
- "@typescript-eslint/typescript-estree": "8.50.1",
- "@typescript-eslint/utils": "8.50.1"
+ "@typescript-eslint/eslint-plugin": "8.52.0",
+ "@typescript-eslint/parser": "8.52.0",
+ "@typescript-eslint/typescript-estree": "8.52.0",
+ "@typescript-eslint/utils": "8.52.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -6885,12 +6901,11 @@
}
},
"node_modules/zod": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
- "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
+ "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index fc9f5d9..b13830d 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "next lint",
+ "lint": "node scripts/lint.js",
"type-check": "tsc --noEmit"
},
"dependencies": {
@@ -20,13 +20,20 @@
"react-qr-code": "^2.0.18"
},
"devDependencies": {
+ "@eslint/eslintrc": "^3.3.3",
+ "@eslint/js": "^9.39.2",
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
+ "@typescript-eslint/eslint-plugin": "^8.52.0",
+ "@typescript-eslint/parser": "^8.52.0",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.1",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-unused-imports": "^4.3.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
diff --git a/public/locales/en.txt b/public/locales/en.txt
index d658a04..603b58b 100644
--- a/public/locales/en.txt
+++ b/public/locales/en.txt
@@ -113,6 +113,8 @@ presentation.field.mainnetAddress=Bitcoin mainnet address (for sponsoring)
presentation.field.mainnetAddress.placeholder=1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
presentation.field.mainnetAddress.help=Bitcoin mainnet address where you will receive sponsoring payments (0.046 BTC excluding fees per sponsoring)
presentation.validation.invalidAddress=Invalid Bitcoin address (must start with 1, 3 or bc1)
+presentation.validation.authorNameRequired=Author name is required
+account.create.error.failed=Failed to create account
presentation.fallback.user=User
presentation.update.button=Update author page
presentation.delete.button=Delete author page
@@ -150,6 +152,31 @@ search.clear=Clear search
# Upload
upload.error.failed=Upload failed
+upload.edit=Edit
+upload.preview=Preview
+
+# Common author
+common.author=Author
+
+# Import
+import.loading=Importing...
+import.button=Import
+
+# Payment
+payment.expired=Expired
+
+# Article
+article.title=Title
+
+# Notification
+notification.title=Notifications
+notification.close=Close
+notification.markAllAsRead=Mark all as read
+
+# Account
+account.create.title=Create account
+account.create.description=Create a new Nostr account or import an existing private key.
+account.import.title=Import private key
# Notification
notification.delete=Delete notification
@@ -234,3 +261,78 @@ settings.nip95.list.editUrl=Click to edit URL
settings.nip95.note.title=Note:
settings.nip95.note.priority=Endpoints are tried in priority order (lower number = higher priority). Only enabled endpoints will be used for uploads.
settings.nip95.note.fallback=If an endpoint fails, the next enabled endpoint will be tried automatically.
+
+# Common UI
+common.repositoryGit=Git Repository
+
+# Article Editor
+article.editor.title=Publish a new publication
+article.editor.category=Category
+article.editor.category.help=Select your article category
+article.editor.category.select=Select a category
+article.editor.category.scienceFiction=Science Fiction
+article.editor.category.scientificResearch=Scientific Research
+article.editor.title.placeholder=Enter article title
+article.editor.preview.label=Preview (Public)
+article.editor.preview.placeholder=This preview will be visible to everyone for free
+article.editor.preview.help=This content will be visible to everyone
+article.editor.series.label=Series
+article.editor.series.none=None (standalone article)
+article.editor.content.label=Full Content (Private) — Markdown + preview
+article.editor.content.help=Media is uploaded via NIP-95 (images ≤5MB, videos ≤45MB) and inserted as URLs. Content remains encrypted for buyers.
+article.editor.sponsoring.label=Sponsoring (sats)
+article.editor.sponsoring.help=Sponsoring amount in satoshis to unlock full content (zap only)
+
+# Payment Modal
+payment.modal.zapAmount=Zap of {{amount}} sats
+payment.modal.timeRemaining=Time remaining: {{time}}
+payment.modal.lightningInvoice=Lightning Invoice:
+payment.modal.scanQr=Scan with your Lightning wallet to pay
+payment.modal.copyInvoice=Copy Invoice
+payment.modal.copied=✓ Copied!
+payment.modal.payWithAlby=Pay with Alby
+payment.modal.invoiceExpired=This invoice has expired
+payment.modal.invoiceExpiredHelp=Please close this modal and try again to generate a new invoice.
+payment.modal.autoVerify=Payment will be automatically verified once completed
+payment.modal.copyFailed=Failed to copy the invoice
+payment.modal.weblnNotAvailable=WebLN is not available. Please install Alby or another Lightning wallet extension.
+
+# Access Control
+access.paymentRequired=Payment required to access full content
+access.onlyAuthorModify=Only the author can modify this object
+access.onlyAuthorDelete=Only the author can delete this object
+
+# Account Creation
+account.create.title=Create account
+account.create.description=Create a new Nostr account or import an existing private key.
+account.create.import.title=Import private key
+account.create.recovery.title=Save your 4 recovery words
+account.create.recovery.saved=I have saved my words
+account.create.noAccount=Create an account or import your secret key to get started
+account.create.generating=Creating account...
+account.create.generateButton=Generate new account
+account.create.importButton=Import existing key
+account.create.importing=Generating...
+account.create.importKey.label=Private Key (nsec or hex)
+account.create.importKey.placeholder=nsec1...
+account.create.importKey.help=After importing, you will receive 4 recovery words (BIP39 dictionary) to secure your account. These words encrypt a Key Encryption Key (KEK) stored in the Credentials API, which then encrypts your private key.
+account.create.publicKey=Your public key (npub)
+account.create.recovery.warning.title=⚠️ Important
+account.create.recovery.warning.part1=These 4 recovery words are your only way to recover your account. They will never be displayed again.
+account.create.recovery.warning.part2=These words (BIP39 dictionary) are used with PBKDF2 to encrypt a Key Encryption Key (KEK) stored in the browser's Credentials API. This KEK then encrypts your private key stored in IndexedDB (two-level system).
+account.create.recovery.warning.part3=Save them in a safe place. Without these words, you will permanently lose access to your account.
+account.create.recovery.copy=Copy recovery words
+account.create.recovery.copied=✓ Copied!
+account.create.back=Back
+account.create.cancel=Cancel
+
+# Markdown Editor
+markdown.upload.media=Upload media (NIP-95)
+markdown.upload.uploading=Uploading...
+
+# Notification
+notification.empty=No notifications yet
+
+# Profile
+profile.articles.title=My Articles
+profile.articles.search.placeholder=Search my articles...
diff --git a/public/locales/fr.txt b/public/locales/fr.txt
index 5f1f25e..32fe023 100644
--- a/public/locales/fr.txt
+++ b/public/locales/fr.txt
@@ -113,6 +113,8 @@ presentation.field.mainnetAddress=Adresse Bitcoin mainnet (pour le sponsoring)
presentation.field.mainnetAddress.placeholder=1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
presentation.field.mainnetAddress.help=Adresse Bitcoin mainnet où vous recevrez les paiements de sponsoring (0.046 BTC hors frais par sponsoring)
presentation.validation.invalidAddress=Adresse Bitcoin invalide (doit commencer par 1, 3 ou bc1)
+presentation.validation.authorNameRequired=Le nom d'auteur est requis
+account.create.error.failed=Échec de la création du compte
presentation.fallback.user=Utilisateur
presentation.update.button=Mettre à jour la page auteur
presentation.delete.button=Supprimer la page auteur
@@ -150,6 +152,31 @@ search.clear=Effacer la recherche
# Upload
upload.error.failed=Échec du téléchargement
+upload.edit=Éditer
+upload.preview=Aperçu
+
+# Common author
+common.author=Auteur
+
+# Import
+import.loading=Importation...
+import.button=Importer
+
+# Payment
+payment.expired=Expiré
+
+# Article
+article.title=Titre
+
+# Notification
+notification.title=Notifications
+notification.close=Fermer
+notification.markAllAsRead=Marquer tout comme lu
+
+# Account
+account.create.title=Créer un compte
+account.create.description=Créez un nouveau compte Nostr ou importez une clé privée existante.
+account.import.title=Importer une clé privée
# Notification
notification.delete=Supprimer la notification
@@ -234,3 +261,78 @@ settings.nip95.list.editUrl=Cliquer pour modifier l'URL
settings.nip95.note.title=Note :
settings.nip95.note.priority=Les endpoints sont essayés dans l'ordre de priorité (nombre plus bas = priorité plus haute). Seuls les endpoints activés seront utilisés pour les uploads.
settings.nip95.note.fallback=Si un endpoint échoue, le prochain endpoint activé sera essayé automatiquement.
+
+# Common UI
+common.repositoryGit=Repository Git
+
+# Article Editor
+article.editor.title=Publier une nouvelle publication
+article.editor.category=Catégorie
+article.editor.category.help=Sélectionnez la catégorie de votre article
+article.editor.category.select=Sélectionnez une catégorie
+article.editor.category.scienceFiction=Science-fiction
+article.editor.category.scientificResearch=Recherche scientifique
+article.editor.title.placeholder=Entrez le titre de l'article
+article.editor.preview.label=Aperçu (Public)
+article.editor.preview.placeholder=Cet aperçu sera visible par tous gratuitement
+article.editor.preview.help=Ce contenu sera visible par tous
+article.editor.series.label=Série
+article.editor.series.none=Aucune (article indépendant)
+article.editor.content.label=Contenu complet (Privé) — Markdown + preview
+article.editor.content.help=Les médias sont uploadés via NIP-95 (images ≤5Mo, vidéos ≤45Mo) et insérés comme URL. Le contenu reste chiffré pour les acheteurs.
+article.editor.sponsoring.label=Sponsoring (sats)
+article.editor.sponsoring.help=Montant de sponsoring en satoshis pour débloquer le contenu complet (zap uniquement)
+
+# Payment Modal
+payment.modal.zapAmount=Zap de {{amount}} sats
+payment.modal.timeRemaining=Temps restant : {{time}}
+payment.modal.lightningInvoice=Facture Lightning :
+payment.modal.scanQr=Scannez avec votre portefeuille Lightning pour payer
+payment.modal.copyInvoice=Copier la facture
+payment.modal.copied=✓ Copié !
+payment.modal.payWithAlby=Payer avec Alby
+payment.modal.invoiceExpired=Cette facture a expiré
+payment.modal.invoiceExpiredHelp=Veuillez fermer cette fenêtre et réessayer pour générer une nouvelle facture.
+payment.modal.autoVerify=Le paiement sera automatiquement vérifié une fois terminé
+payment.modal.copyFailed=Échec de la copie de la facture
+payment.modal.weblnNotAvailable=WebLN n'est pas disponible. Veuillez installer Alby ou une autre extension de portefeuille Lightning.
+
+# Access Control
+access.paymentRequired=Paiement requis pour accéder au contenu complet
+access.onlyAuthorModify=Seul l'auteur peut modifier cet objet
+access.onlyAuthorDelete=Seul l'auteur peut supprimer cet objet
+
+# Account Creation
+account.create.title=Créer un compte
+account.create.description=Créez un nouveau compte Nostr ou importez une clé privée existante.
+account.create.import.title=Importer une clé privée
+account.create.recovery.title=Sauvegardez vos 4 mots-clés de récupération
+account.create.recovery.saved=J'ai sauvegardé mes mots-clés
+account.create.noAccount=Créez un compte ou importez votre clé secrète pour commencer
+account.create.generating=Génération du compte...
+account.create.generateButton=Générer un nouveau compte
+account.create.importButton=Importer une clé existante
+account.create.importing=Génération...
+account.create.importKey.label=Clé privée (nsec ou hex)
+account.create.importKey.placeholder=nsec1...
+account.create.importKey.help=Après l'import, vous recevrez 4 mots-clés de récupération (dictionnaire BIP39) pour sécuriser votre compte. Ces mots-clés chiffrent une clé de chiffrement (KEK) stockée dans l'API Credentials, qui chiffre ensuite votre clé privée.
+account.create.publicKey=Votre clé publique (npub)
+account.create.recovery.warning.title=⚠️ Important
+account.create.recovery.warning.part1=Ces 4 mots-clés sont votre seule façon de récupérer votre compte. Ils ne seront jamais affichés à nouveau.
+account.create.recovery.warning.part2=Ces mots-clés (dictionnaire BIP39) sont utilisés avec PBKDF2 pour chiffrer une clé de chiffrement (KEK) stockée dans l'API Credentials du navigateur. Cette KEK chiffre ensuite votre clé privée stockée dans IndexedDB (système à deux niveaux).
+account.create.recovery.warning.part3=Notez-les dans un endroit sûr. Sans ces mots-clés, vous perdrez définitivement l'accès à votre compte.
+account.create.recovery.copy=Copier les mots-clés
+account.create.recovery.copied=✓ Copié!
+account.create.back=Retour
+account.create.cancel=Annuler
+
+# Markdown Editor
+markdown.upload.media=Upload média (NIP-95)
+markdown.upload.uploading=Upload en cours...
+
+# Notification
+notification.empty=Aucune notification pour le moment
+
+# Profile
+profile.articles.title=Mes articles
+profile.articles.search.placeholder=Rechercher mes articles...
diff --git a/scripts/lint.js b/scripts/lint.js
new file mode 100644
index 0000000..995b35d
--- /dev/null
+++ b/scripts/lint.js
@@ -0,0 +1,33 @@
+#!/usr/bin/env node
+
+/**
+ * Wrapper script to fix Next.js lint command bug
+ * that interprets "lint" as a directory instead of a command
+ */
+
+const { execSync } = require('child_process')
+const path = require('path')
+
+const projectRoot = process.cwd()
+
+try {
+ // Change to project root and run next lint
+ process.chdir(projectRoot)
+ execSync('npx next lint', {
+ stdio: 'inherit',
+ cwd: projectRoot,
+ env: { ...process.env, PWD: projectRoot },
+ })
+} catch (error) {
+ // If next lint fails, try eslint directly with flat config
+ console.log('Falling back to eslint directly...')
+ try {
+ execSync('npx eslint . --ext .ts,.tsx', {
+ stdio: 'inherit',
+ cwd: projectRoot,
+ })
+ } catch (eslintError) {
+ console.error('Both next lint and eslint failed')
+ process.exit(1)
+ }
+}