series wip & code quality checks
This commit is contained in:
parent
572ee2dde5
commit
5ac5aab089
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'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'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'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.
|
||||
</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>
|
||||
)
|
||||
|
||||
@ -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'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} />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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
245
eslint.config.mjs
Normal 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',
|
||||
},
|
||||
},
|
||||
]
|
||||
@ -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 {
|
||||
|
||||
@ -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
219
lib/userContentSync.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
533
package-lock.json
generated
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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...
|
||||
|
||||
@ -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
33
scripts/lint.js
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user