series wip & code quality checks

This commit is contained in:
Nicolas Cantu 2026-01-06 09:26:07 +01:00
parent 572ee2dde5
commit 5ac5aab089
26 changed files with 1128 additions and 378 deletions

View File

@ -1,56 +1,44 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"next/core-web-vitals",
"next/typescript"
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"react",
"react-hooks"
],
"parserOptions": {
"ecmaVersion": 2020,
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"project": "./tsconfig.json"
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
"varsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/prefer-optional-chain": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }],
"no-debugger": "error",
"no-alert": "error",
"prefer-const": "error",
"no-var": "error",
"object-shorthand": "error",
"prefer-arrow-callback": "warn",
"prefer-template": "error",
"eqeqeq": ["error", "always"],
"curly": ["error", "all"],
"no-throw-literal": "error",
"no-return-await": "error",
"require-await": "warn",
"no-await-in-loop": "warn",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"react/jsx-key": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-undef": "error",
"react/no-unescaped-entities": "warn",
"react/no-unknown-property": "error",
"max-lines": ["error", { "max": 250, "skipBlankLines": false, "skipComments": false }],
"max-lines-per-function": ["error", { "max": 40, "skipBlankLines": false, "skipComments": false, "IIFEs": true }]
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-require-imports": "off",
"no-misleading-character-class": "off"
}
}
}

View File

@ -6,6 +6,7 @@ import { ArticleFormButtons } from './ArticleFormButtons'
import { CategorySelect } from './CategorySelect'
import { MarkdownEditor } from './MarkdownEditor'
import type { MediaRef } from '@/types/nostr'
import { t } from '@/lib/i18n'
interface ArticleEditorFormProps {
draft: ArticleDraft
@ -28,11 +29,11 @@ function CategoryField({
return (
<CategorySelect
id="category"
label="Catégorie"
label={t('article.editor.category')}
{...(value ? { value } : {})}
onChange={onChange}
required
helpText="Sélectionnez la catégorie de votre article"
helpText={t('article.editor.category.help')}
/>
)
}
@ -98,11 +99,11 @@ function ArticleTitleField({ draft, onDraftChange }: { draft: ArticleDraft; onDr
return (
<ArticleField
id="title"
label="Titre"
label={t('article.title')}
value={draft.title}
onChange={(value) => onDraftChange({ ...draft, title: value as string })}
required
placeholder="Entrez le titre de l'article"
placeholder={t('article.editor.title.placeholder')}
/>
)
}
@ -117,14 +118,14 @@ function ArticlePreviewField({
return (
<ArticleField
id="preview"
label="Aperçu (Public)"
label={t('article.editor.preview.label')}
value={draft.preview}
onChange={(value) => 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"
placeholder={t('article.editor.preview.placeholder')}
helpText={t('article.editor.preview.help')}
/>
)
}
@ -145,7 +146,7 @@ function SeriesSelect({
return (
<div>
<label htmlFor="series" className="block text-sm font-medium text-gray-700">
Série
{t('article.editor.series.label')}
</label>
<select
id="series"
@ -153,7 +154,7 @@ function SeriesSelect({
value={draft.seriesId ?? ''}
onChange={handleChange}
>
<option value="">Aucune (article indépendant)</option>
<option value="">{t('article.editor.series.none')}</option>
{seriesOptions.map((s) => (
<option key={s.id} value={s.id}>
{s.title}
@ -191,7 +192,7 @@ const ArticleFieldsRight = ({
}) => (
<div className="space-y-4">
<div className="space-y-2">
<div className="text-sm font-semibold text-gray-800">Contenu complet (Privé) Markdown + preview</div>
<div className="text-sm font-semibold text-gray-800">{t('article.editor.content.label')}</div>
<MarkdownEditor
value={draft.content}
onChange={(value) => onDraftChange({ ...draft, content: value })}
@ -204,19 +205,18 @@ const ArticleFieldsRight = ({
}}
/>
<p className="text-xs text-gray-500">
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.
{t('article.editor.content.help')}
</p>
</div>
<ArticleField
id="zapAmount"
label="Sponsoring (sats)"
label={t('article.editor.sponsoring.label')}
value={draft.zapAmount}
onChange={(value) => onDraftChange({ ...draft, zapAmount: value as number })}
required
type="number"
min={1}
helpText="Montant de sponsoring en satoshis pour débloquer le contenu complet (zap uniquement)"
helpText={t('article.editor.sponsoring.help')}
/>
</div>
)
@ -233,7 +233,7 @@ export function ArticleEditorForm({
}: ArticleEditorFormProps) {
return (
<form onSubmit={onSubmit} className="border rounded-lg p-6 bg-white space-y-4">
<h2 className="text-2xl font-bold mb-4">Publier une nouvelle publication</h2>
<h2 className="text-2xl font-bold mb-4">{t('article.editor.title')}</h2>
<div className="space-y-4">
<ArticleFieldsLeft
draft={draft}

View File

@ -8,7 +8,7 @@ interface AuthorCardProps {
}
export function AuthorCard({ presentation }: AuthorCardProps) {
const authorName = presentation.title.replace(/^Présentation de /, '') || 'Auteur'
const authorName = presentation.title.replace(/^Présentation de /, '') || t('common.author')
const totalBTC = (presentation.totalSponsoring ?? 0) / 100_000_000
return (

View File

@ -279,7 +279,7 @@ function useAuthorPresentationState(pubkey: string | null, existingAuthorName?:
return
}
if (!draft.authorName.trim()) {
setValidationError('Author name is required')
setValidationError(t('presentation.validation.authorNameRequired'))
return
}
setValidationError(null)
@ -325,13 +325,13 @@ function NoAccountActionButtons({
onClick={onGenerate}
className="px-6 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan"
>
Générer un nouveau compte
{t('account.create.generateButton')}
</button>
<button
onClick={onImport}
className="px-6 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-medium transition-colors"
>
Importer une clé existante
{t('account.create.importButton')}
</button>
</div>
)
@ -356,7 +356,7 @@ function NoAccountView() {
setNpub(result.npub)
setShowRecoveryStep(true)
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to create account')
setError(e instanceof Error ? e.message : t('account.create.error.failed'))
} finally {
setGenerating(false)
}

View File

@ -1,4 +1,5 @@
import type { ArticleCategory } from '@/types/nostr'
import { t } from '@/lib/i18n'
interface CategorySelectProps {
id: string
@ -32,9 +33,9 @@ export function CategorySelect({
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required={required}
>
<option value="">Sélectionnez une catégorie</option>
<option value="science-fiction">Science-fiction</option>
<option value="scientific-research">Recherche scientifique</option>
<option value="">{t('article.editor.category.select')}</option>
<option value="science-fiction">{t('article.editor.category.scienceFiction')}</option>
<option value="scientific-research">{t('article.editor.category.scientificResearch')}</option>
</select>
{helpText && <p className="text-xs text-gray-500 mt-1">{helpText}</p>}
</div>

View File

@ -21,7 +21,7 @@ function AuthorProfileLink({ presentation, profile }: { presentation: Article; p
// Title format: "Présentation de <name>" or just use profile name
let authorName = presentation.title.replace(/^Présentation de /, '').trim()
if (!authorName || authorName === 'Présentation') {
authorName = profile?.name || 'Auteur'
authorName = profile?.name || t('common.author')
}
// Extract picture from presentation (bannerUrl or from JSON metadata) or profile

View File

@ -1,18 +1,12 @@
import { t } from '@/lib/i18n'
export function RecoveryWarning() {
return (
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4 mb-6">
<p className="text-yellow-400 font-semibold mb-2"> Important</p>
<p className="text-yellow-300/90 text-sm">
Ces <strong className="font-bold">4 mots-clés</strong> sont votre seule façon de récupérer votre compte.
<strong className="font-bold"> Ils ne seront jamais affichés à nouveau.</strong>
</p>
<p className="text-yellow-300/90 text-sm mt-2">
Ces mots-clés (dictionnaire BIP39) sont utilisés avec <strong>PBKDF2</strong> pour chiffrer une clé de chiffrement (KEK) stockée dans l&apos;API Credentials du navigateur. Cette KEK chiffre ensuite votre clé privée stockée dans IndexedDB (système à deux niveaux).
</p>
<p className="text-yellow-300/90 text-sm mt-2">
Notez-les dans un endroit sûr. Sans ces mots-clés, vous perdrez définitivement l&apos;accès à votre compte.
</p>
<p className="text-yellow-400 font-semibold mb-2">{t('account.create.recovery.warning.title')}</p>
<p className="text-yellow-300/90 text-sm" dangerouslySetInnerHTML={{ __html: t('account.create.recovery.warning.part1') }} />
<p className="text-yellow-300/90 text-sm mt-2" dangerouslySetInnerHTML={{ __html: t('account.create.recovery.warning.part2') }} />
<p className="text-yellow-300/90 text-sm mt-2">{t('account.create.recovery.warning.part3')}</p>
</div>
)
}
@ -45,7 +39,7 @@ export function RecoveryPhraseDisplay({
}}
className="w-full py-2 px-4 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 text-sm font-medium transition-colors"
>
{copied ? '✓ Copié!' : 'Copier les mots-clés'}
{copied ? t('account.create.recovery.copied') : t('account.create.recovery.copy')}
</button>
</div>
)
@ -54,7 +48,7 @@ export function RecoveryPhraseDisplay({
export function PublicKeyDisplay({ npub }: { npub: string }) {
return (
<div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4 mb-6">
<p className="text-neon-blue font-semibold mb-2">Votre clé publique (npub)</p>
<p className="text-neon-blue font-semibold mb-2">{t('account.create.publicKey')}</p>
<p className="text-neon-cyan text-sm font-mono break-all">{npub}</p>
</div>
)
@ -73,20 +67,17 @@ export function ImportKeyForm({
<>
<div className="mb-4">
<label htmlFor="importKey" className="block text-sm font-medium text-cyber-accent mb-2">
Clé privée (nsec ou hex)
{t('account.create.importKey.label')}
</label>
<textarea
id="importKey"
value={importKey}
onChange={(e) => setImportKey(e.target.value)}
placeholder="nsec1..."
placeholder={t('account.create.importKey.placeholder')}
className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/30 rounded-lg font-mono text-sm text-neon-cyan"
rows={4}
/>
<p className="text-sm text-cyber-accent/70 mt-2">
Après l&apos;import, vous recevrez <strong>4 mots-clés de récupération</strong> (dictionnaire BIP39) pour sécuriser votre compte.
Ces mots-clés chiffrent une clé de chiffrement (KEK) stockée dans l&apos;API Credentials, qui chiffre ensuite votre clé privée.
</p>
<p className="text-sm text-cyber-accent/70 mt-2" dangerouslySetInnerHTML={{ __html: t('account.create.importKey.help') }} />
</div>
{error && <p className="text-sm text-red-400 mb-4">{error}</p>}
</>
@ -100,7 +91,7 @@ export function ImportStepButtons({ loading, onImport, onBack }: { loading: bool
onClick={onBack}
className="flex-1 py-2 px-4 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"
>
Retour
{t('account.create.back')}
</button>
<button
onClick={() => {
@ -109,7 +100,7 @@ export function ImportStepButtons({ loading, onImport, onBack }: { loading: bool
disabled={loading}
className="flex-1 py-2 px-4 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan disabled:opacity-50"
>
{loading ? 'Importation...' : 'Importer'}
{loading ? t('import.loading') : t('import.button')}
</button>
</div>
)
@ -135,20 +126,20 @@ export function ChooseStepButtons({
disabled={loading}
className="w-full py-3 px-6 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan disabled:opacity-50"
>
{loading ? 'Génération...' : 'Générer un nouveau compte'}
{loading ? t('account.create.importing') : t('account.create.generateButton')}
</button>
<button
onClick={onImport}
disabled={loading}
className="w-full py-3 px-6 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 disabled:opacity-50"
>
Importer une clé existante
{t('account.create.importButton')}
</button>
<button
onClick={onClose}
className="w-full py-2 px-4 text-cyber-accent/70 hover:text-neon-cyan font-medium transition-colors"
>
Annuler
{t('account.create.cancel')}
</button>
</div>
)

View File

@ -1,5 +1,6 @@
import { useState } from 'react'
import { RecoveryWarning, RecoveryPhraseDisplay, PublicKeyDisplay, ImportKeyForm, ImportStepButtons, ChooseStepButtons } from './CreateAccountModalComponents'
import { t } from '@/lib/i18n'
export function RecoveryStep({
recoveryPhrase,
@ -23,7 +24,7 @@ export function RecoveryStep({
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto shadow-glow-cyan">
<h2 className="text-2xl font-bold mb-4 text-neon-cyan">Sauvegardez vos 4 mots-clés de récupération</h2>
<h2 className="text-2xl font-bold mb-4 text-neon-cyan">{t('account.create.recovery.title')}</h2>
<RecoveryWarning />
<RecoveryPhraseDisplay recoveryPhrase={recoveryPhrase} copied={copied} onCopy={handleCopy} />
<PublicKeyDisplay npub={npub} />
@ -32,7 +33,7 @@ export function RecoveryStep({
onClick={onContinue}
className="flex-1 py-3 px-6 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan"
>
J&apos;ai sauvegardé mes mots-clés
{t('account.create.recovery.saved')}
</button>
</div>
</div>
@ -58,7 +59,7 @@ export function ImportStep({
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-md w-full mx-4 shadow-glow-cyan">
<h2 className="text-2xl font-bold mb-4 text-neon-cyan">Importer une clé privée</h2>
<h2 className="text-2xl font-bold mb-4 text-neon-cyan">{t('account.import.title')}</h2>
<ImportKeyForm importKey={importKey} setImportKey={setImportKey} error={error} />
<ImportStepButtons loading={loading} onImport={onImport} onBack={onBack} />
</div>
@ -82,9 +83,9 @@ export function ChooseStep({
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-md w-full mx-4 shadow-glow-cyan">
<h2 className="text-2xl font-bold mb-4 text-neon-cyan">Créer un compte</h2>
<h2 className="text-2xl font-bold mb-4 text-neon-cyan">{t('account.create.title')}</h2>
<p className="text-cyber-accent/70 mb-6">
Créez un nouveau compte Nostr ou importez une clé privée existante.
{t('account.create.description')}
</p>
{error && <p className="text-sm text-red-400 mb-4">{error}</p>}
<ChooseStepButtons loading={loading} onGenerate={onGenerate} onImport={onImport} onClose={onClose} />

View File

@ -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

View File

@ -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)

View File

@ -65,10 +65,10 @@ function MarkdownToolbar({
return (
<div className="flex items-center gap-2">
<button type="button" className="px-3 py-1 text-sm rounded bg-gray-200" onClick={onTogglePreview}>
{preview ? 'Éditer' : 'Preview'}
{preview ? t('upload.edit') : t('upload.preview')}
</button>
<label className="px-3 py-1 text-sm rounded bg-blue-600 text-white cursor-pointer hover:bg-blue-700">
Upload media (NIP-95)
{t('markdown.upload.media')}
<input
type="file"
accept=".png,.jpg,.jpeg,.webp,.mp4,.webm,.mov,.qt"
@ -81,7 +81,7 @@ function MarkdownToolbar({
}}
/>
</label>
{uploading && <span className="text-sm text-gray-500">Upload en cours...</span>}
{uploading && <span className="text-sm text-gray-500">{t('markdown.upload.uploading')}</span>}
{error && <span className="text-sm text-red-600">{error}</span>}
</div>
)

View File

@ -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 (
<div className="p-8 text-center text-gray-500">
<p>No notifications yet</p>
<p>{t('notification.empty')}</p>
</div>
)
}

View File

@ -1,3 +1,4 @@
import { t } from '@/lib/i18n'
interface NotificationPanelHeaderProps {
unreadCount: number
@ -12,20 +13,20 @@ export function NotificationPanelHeader({
}: NotificationPanelHeaderProps) {
return (
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">Notifications</h3>
<h3 className="text-lg font-semibold text-gray-900">{t('notification.title')}</h3>
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<button
onClick={onMarkAllAsRead}
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
Mark all as read
{t('notification.markAllAsRead')}
</button>
)}
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
aria-label="Close"
aria-label={t('notification.close')}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path

View File

@ -30,7 +30,7 @@ export function PageHeader() {
target="_blank"
rel="noopener noreferrer"
className="text-cyber-accent hover:text-neon-cyan transition-colors"
title="Repository Git"
title={t('common.repositoryGit')}
onClick={(e) => e.stopPropagation()}
>
<GitIcon />

View File

@ -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 (
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-xl font-bold text-neon-cyan">Zap de {amount} sats</h2>
<h2 className="text-xl font-bold text-neon-cyan">{t('payment.modal.zapAmount', { amount })}</h2>
{timeLabel && (
<p className={`text-sm ${timeRemaining !== null && timeRemaining <= 60 ? 'text-red-400 font-semibold' : 'text-cyber-accent/70'}`}>
Time remaining: {timeLabel}
{t('payment.modal.timeRemaining', { time: timeLabel })}
</p>
)}
</div>
@ -71,7 +72,7 @@ function PaymentHeader({
function InvoiceDisplay({ invoiceText, paymentUrl }: { invoiceText: string; paymentUrl: string }) {
return (
<div className="mb-4">
<p className="text-sm text-cyber-accent mb-2">Lightning Invoice:</p>
<p className="text-sm text-cyber-accent mb-2">{t('payment.modal.lightningInvoice')}</p>
<div className="bg-cyber-darker border border-neon-cyan/20 p-3 rounded break-all text-sm font-mono mb-4 text-neon-cyan">{invoiceText}</div>
<div className="flex justify-center mb-4">
<div className="bg-cyber-dark p-4 rounded-lg border-2 border-neon-cyan/30">
@ -83,7 +84,7 @@ function InvoiceDisplay({ invoiceText, paymentUrl }: { invoiceText: string; paym
/>
</div>
</div>
<p className="text-xs text-cyber-accent/70 text-center mb-2">Scan with your Lightning wallet to pay</p>
<p className="text-xs text-cyber-accent/70 text-center mb-2">{t('payment.modal.scanQr')}</p>
</div>
)
}
@ -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')}
</button>
<button
onClick={onOpenWallet}
className="flex-1 px-4 py-2 bg-neon-cyan/20 border border-neon-cyan/50 hover:bg-neon-cyan/30 hover:border-neon-cyan text-neon-cyan hover:text-neon-green rounded-lg font-medium transition-colors"
>
Pay with Alby
{t('payment.modal.payWithAlby')}
</button>
</div>
)
@ -123,8 +124,8 @@ function ExpiredNotice({ show }: { show: boolean }) {
}
return (
<div className="mt-4 p-3 bg-red-900/20 border border-red-400/50 rounded-lg">
<p className="text-sm text-red-400 font-semibold mb-2">This invoice has expired</p>
<p className="text-xs text-red-400/80">Please close this modal and try again to generate a new invoice.</p>
<p className="text-sm text-red-400 font-semibold mb-2">{t('payment.modal.invoiceExpired')}</p>
<p className="text-xs text-red-400/80">{t('payment.modal.invoiceExpiredHelp')}</p>
</div>
)
}
@ -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
</p>
)}
<p className="text-xs text-cyber-accent/70 mt-4 text-center">
Payment will be automatically verified once completed
{t('payment.modal.autoVerify')}
</p>
</div>
</div>

245
eslint.config.mjs Normal file
View File

@ -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.Fragment>
'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',
},
},
]

View File

@ -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 {

View File

@ -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
}

219
lib/userContentSync.ts Normal file
View File

@ -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<void> {
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<string, Event[]>()
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<void> {
// 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<string, Event[]>()
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<void> {
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
}
}

View File

@ -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

View File

@ -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

533
package-lock.json generated
View File

@ -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"
}

View File

@ -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"

View File

@ -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 <strong>4 recovery words</strong> (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 <strong>4 recovery words</strong> are your only way to recover your account. <strong>They will never be displayed again.</strong>
account.create.recovery.warning.part2=These words (BIP39 dictionary) are used with <strong>PBKDF2</strong> 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...

View File

@ -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 <strong>4 mots-clés de récupération</strong> (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 <strong>4 mots-clés</strong> sont votre seule façon de récupérer votre compte. <strong>Ils ne seront jamais affichés à nouveau.</strong>
account.create.recovery.warning.part2=Ces mots-clés (dictionnaire BIP39) sont utilisés avec <strong>PBKDF2</strong> 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...

33
scripts/lint.js Normal file
View File

@ -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)
}
}