fix key import
This commit is contained in:
parent
27cb1a7b5b
commit
572ee2dde5
@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
interface ArticleFieldProps {
|
interface ArticleFieldProps {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
interface ArticleFormButtonsProps {
|
interface ArticleFormButtonsProps {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import type { Article } from '@/types/nostr'
|
import type { Article } from '@/types/nostr'
|
||||||
|
|
||||||
interface ArticlePreviewProps {
|
interface ArticlePreviewProps {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { useRouter } from 'next/router'
|
|||||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||||
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
|
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
|
||||||
import { ArticleField } from './ArticleField'
|
import { ArticleField } from './ArticleField'
|
||||||
import { ArticleFormButtons } from './ArticleFormButtons'
|
|
||||||
import { CreateAccountModal } from './CreateAccountModal'
|
import { CreateAccountModal } from './CreateAccountModal'
|
||||||
import { RecoveryStep } from './CreateAccountModalSteps'
|
import { RecoveryStep } from './CreateAccountModalSteps'
|
||||||
import { UnlockAccountModal } from './UnlockAccountModal'
|
import { UnlockAccountModal } from './UnlockAccountModal'
|
||||||
@ -251,7 +250,7 @@ function useAuthorPresentationState(pubkey: string | null, existingAuthorName?:
|
|||||||
presentation,
|
presentation,
|
||||||
contentDescription,
|
contentDescription,
|
||||||
mainnetAddress: existingPresentation.mainnetAddress || '',
|
mainnetAddress: existingPresentation.mainnetAddress || '',
|
||||||
pictureUrl: existingPresentation.bannerUrl,
|
...(existingPresentation.bannerUrl ? { pictureUrl: existingPresentation.bannerUrl } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import type { ArticleCategory } from '@/types/nostr'
|
import type { ArticleCategory } from '@/types/nostr'
|
||||||
|
|
||||||
interface CategorySelectProps {
|
interface CategorySelectProps {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
type CategoryFilter = 'science-fiction' | 'scientific-research' | 'all' | null
|
type CategoryFilter = 'science-fiction' | 'scientific-research' | 'all' | null
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
interface ClearButtonProps {
|
interface ClearButtonProps {
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
@ -9,7 +9,7 @@ export function ClearButton({ onClick }: ClearButtonProps) {
|
|||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-neon-cyan/70 hover:text-neon-cyan transition-colors"
|
className="absolute inset-y-0 right-0 pr-3 flex items-center text-neon-cyan/70 hover:text-neon-cyan transition-colors"
|
||||||
aria-label="Clear search"
|
aria-label={t('search.clear')}
|
||||||
>
|
>
|
||||||
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { ConnectedUserMenu } from './ConnectedUserMenu'
|
|||||||
import { RecoveryStep } from './CreateAccountModalSteps'
|
import { RecoveryStep } from './CreateAccountModalSteps'
|
||||||
import { UnlockAccountModal } from './UnlockAccountModal'
|
import { UnlockAccountModal } from './UnlockAccountModal'
|
||||||
import type { NostrProfile } from '@/types/nostr'
|
import type { NostrProfile } from '@/types/nostr'
|
||||||
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
function ConnectForm({
|
function ConnectForm({
|
||||||
onCreateAccount,
|
onCreateAccount,
|
||||||
@ -23,14 +24,14 @@ function ConnectForm({
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
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 disabled:opacity-50"
|
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 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Créer un compte
|
{t('connect.createAccount')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onUnlock}
|
onClick={onUnlock}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="px-6 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-medium transition-colors disabled:opacity-50"
|
className="px-6 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-medium transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Se connecter
|
{t('connect.connect')}
|
||||||
</button>
|
</button>
|
||||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
@ -146,7 +147,7 @@ export function ConnectButton() {
|
|||||||
return <ConnectedState pubkey={pubkey} profile={profile} loading={loading} disconnect={disconnect} />
|
return <ConnectedState pubkey={pubkey} profile={profile} loading={loading} disconnect={disconnect} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accountExists === true && pubkey && !isUnlocked && !showUnlockModal && !showCreateModal) {
|
if (accountExists === true && pubkey && !isUnlocked && !showUnlockModal) {
|
||||||
return (
|
return (
|
||||||
<UnlockState
|
<UnlockState
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|||||||
225
components/CreateSeriesModal.tsx
Normal file
225
components/CreateSeriesModal.tsx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { ImageUploadField } from './ImageUploadField'
|
||||||
|
import { publishSeries } from '@/lib/articleMutations'
|
||||||
|
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||||
|
import { nostrService } from '@/lib/nostr'
|
||||||
|
import { t } from '@/lib/i18n'
|
||||||
|
import type { ArticleDraft } from '@/lib/articlePublisherTypes'
|
||||||
|
|
||||||
|
interface CreateSeriesModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSuccess: () => void
|
||||||
|
authorPubkey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SeriesDraft {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
preview: string
|
||||||
|
coverUrl: string
|
||||||
|
category: ArticleDraft['category']
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateSeriesModal({ isOpen, onClose, onSuccess, authorPubkey }: CreateSeriesModalProps) {
|
||||||
|
const { pubkey, isUnlocked } = useNostrAuth()
|
||||||
|
const [draft, setDraft] = useState<SeriesDraft>({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
preview: '',
|
||||||
|
coverUrl: '',
|
||||||
|
category: 'science-fiction',
|
||||||
|
})
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateKey = nostrService.getPrivateKey()
|
||||||
|
const canPublish = pubkey === authorPubkey && isUnlocked && privateKey !== null
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!canPublish) {
|
||||||
|
setError(t('series.create.error.notAuthor'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!draft.title.trim() || !draft.description.trim() || !draft.preview.trim()) {
|
||||||
|
setError(t('series.create.error.missingFields'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!privateKey) {
|
||||||
|
setError(t('series.create.error.notAuthor'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await publishSeries({
|
||||||
|
title: draft.title,
|
||||||
|
description: draft.description,
|
||||||
|
preview: draft.preview,
|
||||||
|
...(draft.coverUrl ? { coverUrl: draft.coverUrl } : {}),
|
||||||
|
category: draft.category,
|
||||||
|
authorPubkey,
|
||||||
|
authorPrivateKey: privateKey,
|
||||||
|
})
|
||||||
|
// Reset form
|
||||||
|
setDraft({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
preview: '',
|
||||||
|
coverUrl: '',
|
||||||
|
category: 'science-fiction',
|
||||||
|
})
|
||||||
|
onSuccess()
|
||||||
|
onClose()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : t('series.create.error.publishFailed'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!loading) {
|
||||||
|
setDraft({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
preview: '',
|
||||||
|
coverUrl: '',
|
||||||
|
category: 'science-fiction',
|
||||||
|
})
|
||||||
|
setError(null)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||||
|
<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">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-2xl font-semibold text-neon-cyan">{t('series.create.title')}</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={loading}
|
||||||
|
className="text-cyber-accent hover:text-neon-cyan transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!canPublish && (
|
||||||
|
<div className="mb-4 p-4 bg-yellow-900/30 border border-yellow-500/50 rounded text-yellow-300">
|
||||||
|
<p>{t('series.create.error.notAuthor')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="series-title" className="block text-sm font-medium text-neon-cyan mb-2">
|
||||||
|
{t('series.create.field.title')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="series-title"
|
||||||
|
type="text"
|
||||||
|
value={draft.title}
|
||||||
|
onChange={(e) => setDraft({ ...draft, title: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
|
||||||
|
required
|
||||||
|
disabled={loading || !canPublish}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="series-description" className="block text-sm font-medium text-neon-cyan mb-2">
|
||||||
|
{t('series.create.field.description')}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="series-description"
|
||||||
|
value={draft.description}
|
||||||
|
onChange={(e) => setDraft({ ...draft, description: e.target.value })}
|
||||||
|
rows={4}
|
||||||
|
className="w-full px-3 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
|
||||||
|
required
|
||||||
|
disabled={loading || !canPublish}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="series-preview" className="block text-sm font-medium text-neon-cyan mb-2">
|
||||||
|
{t('series.create.field.preview')}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="series-preview"
|
||||||
|
value={draft.preview}
|
||||||
|
onChange={(e) => setDraft({ ...draft, preview: e.target.value })}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-3 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
|
||||||
|
required
|
||||||
|
disabled={loading || !canPublish}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-cyber-accent/70 mt-1">{t('series.create.field.preview.help')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="series-category" className="block text-sm font-medium text-neon-cyan mb-2">
|
||||||
|
{t('series.create.field.category')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="series-category"
|
||||||
|
value={draft.category}
|
||||||
|
onChange={(e) => setDraft({ ...draft, category: e.target.value as ArticleDraft['category'] })}
|
||||||
|
className="w-full px-3 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
|
||||||
|
required
|
||||||
|
disabled={loading || !canPublish}
|
||||||
|
>
|
||||||
|
<option value="science-fiction">{t('category.science-fiction')}</option>
|
||||||
|
<option value="scientific-research">{t('category.scientific-research')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ImageUploadField
|
||||||
|
id="series-cover"
|
||||||
|
label={t('series.create.field.cover')}
|
||||||
|
value={draft.coverUrl}
|
||||||
|
onChange={(url) => setDraft({ ...draft, coverUrl: url })}
|
||||||
|
helpText={t('series.create.field.cover.help')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 bg-red-900/30 border border-red-500/50 rounded text-red-300">
|
||||||
|
<p>{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-4 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light hover:border-neon-cyan transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading || !canPublish}
|
||||||
|
className="px-4 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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? t('common.loading') : t('series.create.submit')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,12 +1,5 @@
|
|||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
import type { DocLink, DocSection } from '@/hooks/useDocs'
|
||||||
type DocSection = 'user-guide' | 'faq' | 'publishing' | 'payment'
|
|
||||||
|
|
||||||
interface DocLink {
|
|
||||||
id: DocSection
|
|
||||||
title: string
|
|
||||||
file: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DocsSidebarProps {
|
interface DocsSidebarProps {
|
||||||
docs: DocLink[]
|
docs: DocLink[]
|
||||||
|
|||||||
@ -43,6 +43,7 @@ function FundingStats({ stats }: { stats: ReturnType<typeof estimatePlatformFund
|
|||||||
|
|
||||||
export function FundingGauge() {
|
export function FundingGauge() {
|
||||||
const [stats, setStats] = useState(estimatePlatformFunds())
|
const [stats, setStats] = useState(estimatePlatformFunds())
|
||||||
|
const [certificationStats, setCertificationStats] = useState(estimatePlatformFunds())
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -52,6 +53,8 @@ export function FundingGauge() {
|
|||||||
try {
|
try {
|
||||||
const fundingStats = estimatePlatformFunds()
|
const fundingStats = estimatePlatformFunds()
|
||||||
setStats(fundingStats)
|
setStats(fundingStats)
|
||||||
|
// Certification uses the same funding pool
|
||||||
|
setCertificationStats(fundingStats)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error loading funding stats:', e)
|
console.error('Error loading funding stats:', e)
|
||||||
} finally {
|
} finally {
|
||||||
@ -70,9 +73,15 @@ export function FundingGauge() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-cyber-dark/50 border border-neon-cyan/20 rounded-lg p-6 mb-8">
|
<div className="space-y-6">
|
||||||
|
<div className="bg-cyber-dark/50 border border-neon-cyan/20 rounded-lg p-6">
|
||||||
<h2 className="text-xl font-semibold text-neon-cyan mb-4">{t('home.funding.title')}</h2>
|
<h2 className="text-xl font-semibold text-neon-cyan mb-4">{t('home.funding.title')}</h2>
|
||||||
<FundingStats stats={stats} />
|
<FundingStats stats={stats} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bg-cyber-dark/50 border border-neon-cyan/20 rounded-lg p-6">
|
||||||
|
<h2 className="text-xl font-semibold text-neon-cyan mb-4">{t('home.funding.certification.title')}</h2>
|
||||||
|
<FundingStats stats={certificationStats} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,20 +61,9 @@ function ArticlesHero({
|
|||||||
function HomeIntroSection() {
|
function HomeIntroSection() {
|
||||||
return (
|
return (
|
||||||
<div className="mt-12 mb-8">
|
<div className="mt-12 mb-8">
|
||||||
<div className="mb-6 text-cyber-accent leading-relaxed bg-cyber-dark/50 border border-neon-cyan/20 rounded-lg p-4 backdrop-blur-sm">
|
<p className="mb-6 text-sm text-cyber-accent/70">
|
||||||
<p className="mb-2">
|
{t('home.funding.description')}
|
||||||
Consultez les auteurs et aperçus, achetez les parutions au fil de l'eau par <strong className="text-neon-green">800 sats</strong> (moins 100 sats et frais de transaction).
|
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-2">
|
|
||||||
Sponsorisez l'auteur pour <strong className="text-neon-green">0.046 BTC</strong> (moins 0.004 BTC et frais de transaction).
|
|
||||||
</p>
|
|
||||||
<p className="mb-2">
|
|
||||||
Les avis sont remerciables pour <strong className="text-neon-green">70 sats</strong> (moins 21 sats et frais de transaction).
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-cyber-accent/70 italic">
|
|
||||||
Les fonds de la plateforme servent à son développement.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<FundingGauge />
|
<FundingGauge />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import type { MediaRef } from '@/types/nostr'
|
import type { MediaRef } from '@/types/nostr'
|
||||||
import { uploadNip95Media } from '@/lib/nip95'
|
import { uploadNip95Media } from '@/lib/nip95'
|
||||||
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
interface MarkdownEditorProps {
|
interface MarkdownEditorProps {
|
||||||
value: string
|
value: string
|
||||||
@ -108,7 +109,7 @@ async function handleUpload(
|
|||||||
handlers.onBannerChange?.(media.url)
|
handlers.onBannerChange?.(media.url)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handlers.setError(e instanceof Error ? e.message : 'Upload failed')
|
handlers.setError(e instanceof Error ? e.message : t('upload.error.failed'))
|
||||||
} finally {
|
} finally {
|
||||||
handlers.setUploading(false)
|
handlers.setUploading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -224,9 +224,6 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
|
|||||||
{api.url}
|
{api.url}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-sm text-cyber-accent mt-1">
|
|
||||||
{t('settings.nip95.list.priorityLabel', { priority: api.priority, id: api.id })}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
@ -249,7 +246,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-2">
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<span className="text-sm text-cyber-accent">{t('settings.nip95.list.priority')}:</span>
|
<span className="text-sm text-cyber-accent">{t('settings.nip95.list.priority')}:</span>
|
||||||
<input
|
<input
|
||||||
@ -264,6 +261,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
|
|||||||
}}
|
}}
|
||||||
className="w-20 px-2 py-1 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
|
className="w-20 px-2 py-1 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
|
||||||
/>
|
/>
|
||||||
|
<span className="text-sm text-cyber-accent">| ID: {api.id}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
|
||||||
import { formatTime } from '@/lib/formatTime'
|
import { formatTime } from '@/lib/formatTime'
|
||||||
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
interface NotificationActionsProps {
|
interface NotificationActionsProps {
|
||||||
timestamp: number
|
timestamp: number
|
||||||
@ -16,7 +16,7 @@ export function NotificationActions({ timestamp, onDelete }: NotificationActions
|
|||||||
onDelete()
|
onDelete()
|
||||||
}}
|
}}
|
||||||
className="text-gray-400 hover:text-red-600 transition-colors"
|
className="text-gray-400 hover:text-red-600 transition-colors"
|
||||||
aria-label="Delete notification"
|
aria-label={t('notification.delete')}
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@ -30,4 +30,3 @@ export function NotificationActions({ timestamp, onDelete }: NotificationActions
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
interface NotificationBadgeButtonProps {
|
interface NotificationBadgeButtonProps {
|
||||||
unreadCount: number
|
unreadCount: number
|
||||||
@ -34,4 +33,3 @@ export function NotificationBadgeButton({ unreadCount, onClick }: NotificationBa
|
|||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type { Notification } from '@/types/notifications'
|
import type { Notification } from '@/types/notifications'
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import type { Notification } from '@/types/notifications'
|
import type { Notification } from '@/types/notifications'
|
||||||
import { NotificationContent } from './NotificationContent'
|
import { NotificationContent } from './NotificationContent'
|
||||||
import { NotificationActions } from './NotificationActions'
|
import { NotificationActions } from './NotificationActions'
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import type { Notification } from '@/types/notifications'
|
import type { Notification } from '@/types/notifications'
|
||||||
import { NotificationItem } from './NotificationItem'
|
import { NotificationItem } from './NotificationItem'
|
||||||
import { NotificationPanelHeader } from './NotificationPanelHeader'
|
import { NotificationPanelHeader } from './NotificationPanelHeader'
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
interface NotificationPanelHeaderProps {
|
interface NotificationPanelHeaderProps {
|
||||||
unreadCount: number
|
unreadCount: number
|
||||||
@ -41,4 +40,3 @@ export function NotificationPanelHeader({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
export function SearchIcon() {
|
export function SearchIcon() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { ArticleCard } from './ArticleCard'
|
|||||||
import type { Article } from '@/types/nostr'
|
import type { Article } from '@/types/nostr'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
interface UserArticlesViewProps {
|
interface UserArticlesViewProps {
|
||||||
articles: Article[]
|
articles: Article[]
|
||||||
@ -21,7 +22,7 @@ interface UserArticlesViewProps {
|
|||||||
|
|
||||||
const ArticlesLoading = () => (
|
const ArticlesLoading = () => (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-gray-500">Loading articles...</p>
|
<p className="text-gray-500">{t('common.loading.articles')}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ const ArticlesError = ({ message }: { message: string }) => (
|
|||||||
const EmptyState = ({ show }: { show: boolean }) =>
|
const EmptyState = ({ show }: { show: boolean }) =>
|
||||||
show ? (
|
show ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-gray-500">No articles published yet.</p>
|
<p className="text-gray-500">{t('common.empty.articles')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
|
|
||||||
@ -60,13 +61,13 @@ function ArticleActions({
|
|||||||
className="px-3 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
|
className="px-3 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
|
||||||
disabled={editingArticleId !== null && editingArticleId !== article.id}
|
disabled={editingArticleId !== null && editingArticleId !== article.id}
|
||||||
>
|
>
|
||||||
Edit
|
{t('common.edit')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => (pendingDeleteId === article.id ? onDelete(article) : requestDelete(article.id))}
|
onClick={() => (pendingDeleteId === article.id ? onDelete(article) : requestDelete(article.id))}
|
||||||
className="px-3 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700"
|
className="px-3 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700"
|
||||||
>
|
>
|
||||||
{pendingDeleteId === article.id ? 'Confirm delete' : 'Delete'}
|
{pendingDeleteId === article.id ? t('common.confirmDelete') : t('common.delete')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
interface UserProfileHeaderProps {
|
interface UserProfileHeaderProps {
|
||||||
displayName: string
|
displayName: string
|
||||||
|
|||||||
16
docs/fees-and-contributions.md
Normal file
16
docs/fees-and-contributions.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Frais et contributions
|
||||||
|
|
||||||
|
## Tarification
|
||||||
|
|
||||||
|
### Achat de publications
|
||||||
|
Consultez les auteurs et aperçus, achetez les parutions au fil de l'eau par **800 sats** (moins 100 sats et frais de transaction).
|
||||||
|
|
||||||
|
### Sponsoring d'auteur
|
||||||
|
Sponsorisez l'auteur pour **0.046 BTC** (moins 0.004 BTC et frais de transaction).
|
||||||
|
|
||||||
|
### Remerciements d'avis
|
||||||
|
Les avis sont remerciables pour **70 sats** (moins 21 sats et frais de transaction).
|
||||||
|
|
||||||
|
## Utilisation des fonds
|
||||||
|
|
||||||
|
Les fonds collectés par la plateforme servent au développement de fonctions IA gratuites pour les auteurs (développement et matériel).
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
type DocSection = 'user-guide' | 'faq' | 'publishing' | 'payment'
|
export type DocSection = 'user-guide' | 'faq' | 'publishing' | 'payment' | 'fees-and-contributions'
|
||||||
|
|
||||||
interface DocLink {
|
export interface DocLink {
|
||||||
id: DocSection
|
id: DocSection
|
||||||
title: string
|
title: string
|
||||||
file: string
|
file: string
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export function canUserDelete(event: Event, userPubkey: string): boolean {
|
|||||||
* All users can read public content (previews, metadata)
|
* All users can read public content (previews, metadata)
|
||||||
* For paid content, users must have paid (via zap receipt) to access full content
|
* For paid content, users must have paid (via zap receipt) to access full content
|
||||||
*/
|
*/
|
||||||
export function canUserRead(event: Event, userPubkey: string | null, hasPaid: boolean = false): {
|
export function canUserRead(event: Event, _userPubkey: string | null, hasPaid: boolean = false): {
|
||||||
canReadPreview: boolean
|
canReadPreview: boolean
|
||||||
canReadFullContent: boolean
|
canReadFullContent: boolean
|
||||||
} {
|
} {
|
||||||
@ -101,6 +101,6 @@ export function getAccessControl(
|
|||||||
canReadPreview,
|
canReadPreview,
|
||||||
canReadFullContent,
|
canReadFullContent,
|
||||||
isPaid,
|
isPaid,
|
||||||
reason,
|
...(reason ? { reason } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -188,7 +188,7 @@ async function buildReviewEvent(
|
|||||||
articleId: params.articleId,
|
articleId: params.articleId,
|
||||||
reviewerPubkey: params.reviewerPubkey,
|
reviewerPubkey: params.reviewerPubkey,
|
||||||
content: params.content,
|
content: params.content,
|
||||||
title: params.title ?? undefined,
|
...(params.title ? { title: params.title } : {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Build JSON metadata
|
// Build JSON metadata
|
||||||
|
|||||||
@ -1,18 +1,6 @@
|
|||||||
import type { Article } from '@/types/nostr'
|
import type { Article } from '@/types/nostr'
|
||||||
import type { ArticleFilters } from '@/components/ArticleFilters'
|
import type { ArticleFilters } from '@/components/ArticleFilters'
|
||||||
|
|
||||||
/**
|
|
||||||
* Map category from ArticleFilters to tag category
|
|
||||||
*/
|
|
||||||
function mapCategoryToTag(category: ArticleFilters['category']): 'sciencefiction' | 'research' | null {
|
|
||||||
if (category === 'science-fiction') {
|
|
||||||
return 'sciencefiction'
|
|
||||||
}
|
|
||||||
if (category === 'scientific-research') {
|
|
||||||
return 'research'
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get authors (presentation articles) filtered by category
|
* Get authors (presentation articles) filtered by category
|
||||||
|
|||||||
@ -177,9 +177,15 @@ export async function extractAuthorFromEvent(event: Event): Promise<ExtractedAut
|
|||||||
return {
|
return {
|
||||||
type: 'author',
|
type: 'author',
|
||||||
id,
|
id,
|
||||||
...authorData,
|
pubkey: authorData.pubkey,
|
||||||
|
authorName: authorData.authorName,
|
||||||
|
presentation: authorData.presentation,
|
||||||
|
contentDescription: authorData.contentDescription,
|
||||||
|
category: authorData.category,
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
url: metadata.url as string | undefined,
|
...(authorData.mainnetAddress ? { mainnetAddress: authorData.mainnetAddress } : {}),
|
||||||
|
...(authorData.pictureUrl ? { pictureUrl: authorData.pictureUrl } : {}),
|
||||||
|
...(metadata.url ? { url: metadata.url as string } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,8 +226,13 @@ export async function extractSeriesFromEvent(event: Event): Promise<ExtractedSer
|
|||||||
return {
|
return {
|
||||||
type: 'series',
|
type: 'series',
|
||||||
id,
|
id,
|
||||||
...seriesData,
|
pubkey: seriesData.pubkey,
|
||||||
|
title: seriesData.title,
|
||||||
|
description: seriesData.description,
|
||||||
|
category: seriesData.category,
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
|
...(seriesData.coverUrl ? { coverUrl: seriesData.coverUrl } : {}),
|
||||||
|
...(seriesData.preview ? { preview: seriesData.preview } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,8 +252,13 @@ export async function extractSeriesFromEvent(event: Event): Promise<ExtractedSer
|
|||||||
return {
|
return {
|
||||||
type: 'series',
|
type: 'series',
|
||||||
id,
|
id,
|
||||||
...seriesData,
|
pubkey: seriesData.pubkey,
|
||||||
|
title: seriesData.title,
|
||||||
|
description: seriesData.description,
|
||||||
|
category: seriesData.category,
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
|
...(seriesData.coverUrl ? { coverUrl: seriesData.coverUrl } : {}),
|
||||||
|
...(seriesData.preview ? { preview: seriesData.preview } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,8 +298,14 @@ export async function extractPublicationFromEvent(event: Event): Promise<Extract
|
|||||||
return {
|
return {
|
||||||
type: 'publication',
|
type: 'publication',
|
||||||
id,
|
id,
|
||||||
...publicationData,
|
pubkey: publicationData.pubkey,
|
||||||
|
title: publicationData.title,
|
||||||
|
preview: publicationData.preview,
|
||||||
|
category: publicationData.category,
|
||||||
|
zapAmount: publicationData.zapAmount,
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
|
...(publicationData.seriesId ? { seriesId: publicationData.seriesId } : {}),
|
||||||
|
...(publicationData.bannerUrl ? { bannerUrl: publicationData.bannerUrl } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,8 +326,14 @@ export async function extractPublicationFromEvent(event: Event): Promise<Extract
|
|||||||
return {
|
return {
|
||||||
type: 'publication',
|
type: 'publication',
|
||||||
id,
|
id,
|
||||||
...publicationData,
|
pubkey: publicationData.pubkey,
|
||||||
|
title: publicationData.title,
|
||||||
|
preview: publicationData.preview,
|
||||||
|
category: publicationData.category,
|
||||||
|
zapAmount: publicationData.zapAmount,
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
|
...(publicationData.seriesId ? { seriesId: publicationData.seriesId } : {}),
|
||||||
|
...(publicationData.bannerUrl ? { bannerUrl: publicationData.bannerUrl } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,13 +390,23 @@ export async function extractReviewFromEvent(event: Event): Promise<ExtractedRev
|
|||||||
title: tags.title as string | undefined,
|
title: tags.title as string | undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = await generateReviewHashId(reviewData)
|
const id = await generateReviewHashId({
|
||||||
|
pubkey: reviewData.pubkey,
|
||||||
|
articleId: reviewData.articleId,
|
||||||
|
reviewerPubkey: reviewData.reviewerPubkey,
|
||||||
|
content: reviewData.content,
|
||||||
|
...(reviewData.title ? { title: reviewData.title } : {}),
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'review',
|
type: 'review',
|
||||||
id,
|
id,
|
||||||
...reviewData,
|
pubkey: reviewData.pubkey,
|
||||||
|
articleId: reviewData.articleId,
|
||||||
|
reviewerPubkey: reviewData.reviewerPubkey,
|
||||||
|
content: reviewData.content,
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
|
...(reviewData.title ? { title: reviewData.title } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -502,13 +540,25 @@ export async function extractSponsoringFromEvent(event: Event): Promise<Extracte
|
|||||||
paymentHash,
|
paymentHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = await generateSponsoringHashId(sponsoringData)
|
const id = await generateSponsoringHashId({
|
||||||
|
payerPubkey: sponsoringData.payerPubkey,
|
||||||
|
authorPubkey: sponsoringData.authorPubkey,
|
||||||
|
amount: sponsoringData.amount,
|
||||||
|
paymentHash: sponsoringData.paymentHash,
|
||||||
|
...(sponsoringData.seriesId ? { seriesId: sponsoringData.seriesId } : {}),
|
||||||
|
...(sponsoringData.articleId ? { articleId: sponsoringData.articleId } : {}),
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'sponsoring',
|
type: 'sponsoring',
|
||||||
id,
|
id,
|
||||||
...sponsoringData,
|
payerPubkey: sponsoringData.payerPubkey,
|
||||||
|
authorPubkey: sponsoringData.authorPubkey,
|
||||||
|
amount: sponsoringData.amount,
|
||||||
|
paymentHash: sponsoringData.paymentHash,
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
|
...(sponsoringData.seriesId ? { seriesId: sponsoringData.seriesId } : {}),
|
||||||
|
...(sponsoringData.articleId ? { articleId: sponsoringData.articleId } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Event } from 'nostr-tools'
|
import type { Event } from 'nostr-tools'
|
||||||
import { extractTagsFromEvent } from './nostrTagSystem'
|
|
||||||
import { canModifyObject, getNextVersion } from './versionManager'
|
import { canModifyObject, getNextVersion } from './versionManager'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,7 +25,7 @@ export function canUserModifyObject(event: Event, userPubkey: string): boolean {
|
|||||||
*/
|
*/
|
||||||
export async function buildUpdateEvent(
|
export async function buildUpdateEvent(
|
||||||
originalEvent: Event,
|
originalEvent: Event,
|
||||||
updatedData: Record<string, unknown>,
|
_updatedData: Record<string, unknown>,
|
||||||
userPubkey: string
|
userPubkey: string
|
||||||
): Promise<Event | null> {
|
): Promise<Event | null> {
|
||||||
// Check if user can modify
|
// Check if user can modify
|
||||||
@ -34,7 +33,6 @@ export async function buildUpdateEvent(
|
|||||||
throw new Error('Only the author can modify this object')
|
throw new Error('Only the author can modify this object')
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = extractTagsFromEvent(originalEvent)
|
|
||||||
const nextVersion = getNextVersion([originalEvent])
|
const nextVersion = getNextVersion([originalEvent])
|
||||||
|
|
||||||
// Build new event with incremented version
|
// Build new event with incremented version
|
||||||
@ -70,7 +68,6 @@ export async function buildDeleteEvent(
|
|||||||
throw new Error('Only the author can delete this object')
|
throw new Error('Only the author can delete this object')
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = extractTagsFromEvent(originalEvent)
|
|
||||||
const nextVersion = getNextVersion([originalEvent])
|
const nextVersion = getNextVersion([originalEvent])
|
||||||
|
|
||||||
// Build new event with hidden=true and incremented version
|
// Build new event with hidden=true and incremented version
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export function extractPresentationData(presentation: Article): {
|
|||||||
const newFormatMatch = content.match(/Présentation personnelle : (.+?)(?:\nDescription de votre contenu :|$)/s)
|
const newFormatMatch = content.match(/Présentation personnelle : (.+?)(?:\nDescription de votre contenu :|$)/s)
|
||||||
const descriptionMatch = content.match(/Description de votre contenu : (.+?)(?:\nAdresse Bitcoin mainnet|$)/s)
|
const descriptionMatch = content.match(/Description de votre contenu : (.+?)(?:\nAdresse Bitcoin mainnet|$)/s)
|
||||||
|
|
||||||
if (newFormatMatch && descriptionMatch) {
|
if (newFormatMatch && descriptionMatch && newFormatMatch[1] && descriptionMatch[1]) {
|
||||||
return {
|
return {
|
||||||
presentation: newFormatMatch[1].trim(),
|
presentation: newFormatMatch[1].trim(),
|
||||||
contentDescription: descriptionMatch[1].trim(),
|
contentDescription: descriptionMatch[1].trim(),
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export function parseObjectUrl(url: string): {
|
|||||||
version: number | null
|
version: number | null
|
||||||
} {
|
} {
|
||||||
const match = url.match(/https?:\/\/zapwall\.fr\/(author|series|publication|review)\/([a-f0-9]+)_(\d+)_(\d+)/i)
|
const match = url.match(/https?:\/\/zapwall\.fr\/(author|series|publication|review)\/([a-f0-9]+)_(\d+)_(\d+)/i)
|
||||||
if (!match) {
|
if (!match || !match[1] || !match[2] || !match[3] || !match[4]) {
|
||||||
return { objectType: null, idHash: null, index: null, version: null }
|
return { objectType: null, idHash: null, index: null, version: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -60,7 +60,10 @@ export function getLatestVersion(events: Event[]): Event | null {
|
|||||||
|
|
||||||
// Sort by version (descending) and take the first (latest)
|
// Sort by version (descending) and take the first (latest)
|
||||||
visible.sort((a, b) => b.version - a.version)
|
visible.sort((a, b) => b.version - a.version)
|
||||||
latestVersions.push(visible[0])
|
const latest = visible[0]
|
||||||
|
if (latest) {
|
||||||
|
latestVersions.push(latest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have multiple IDs, we need to return the one with the highest version
|
// If we have multiple IDs, we need to return the one with the highest version
|
||||||
@ -71,7 +74,8 @@ export function getLatestVersion(events: Event[]): Event | null {
|
|||||||
|
|
||||||
// Sort by version and return the latest
|
// Sort by version and return the latest
|
||||||
latestVersions.sort((a, b) => b.version - a.version)
|
latestVersions.sort((a, b) => b.version - a.version)
|
||||||
return latestVersions[0].event
|
const latestVersion = latestVersions[0]
|
||||||
|
return latestVersion ? latestVersion.event : null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -11,6 +11,7 @@ home.funding.target=Target: {{target}} BTC
|
|||||||
home.funding.current=Raised: {{current}} BTC
|
home.funding.current=Raised: {{current}} BTC
|
||||||
home.funding.progress={{percent}}% of funding reached
|
home.funding.progress={{percent}}% of funding reached
|
||||||
home.funding.description=Funds collected by the platform serve the development of free AI features for authors (development and hardware).
|
home.funding.description=Funds collected by the platform serve the development of free AI features for authors (development and hardware).
|
||||||
|
home.funding.certification.title=Certification on a Bitcoin-anchored signet of intellectual property
|
||||||
|
|
||||||
# Navigation
|
# Navigation
|
||||||
nav.documentation=Documentation
|
nav.documentation=Documentation
|
||||||
@ -18,12 +19,17 @@ nav.publish=Publish profile
|
|||||||
nav.createAuthorPage=Create author page
|
nav.createAuthorPage=Create author page
|
||||||
nav.loading=Loading...
|
nav.loading=Loading...
|
||||||
|
|
||||||
|
# Connect
|
||||||
|
connect.createAccount=Create account
|
||||||
|
connect.connect=Connect
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
docs.title=Documentation
|
docs.title=Documentation
|
||||||
docs.userGuide=User Guide
|
docs.userGuide=User Guide
|
||||||
docs.faq=FAQ
|
docs.faq=FAQ
|
||||||
docs.publishing=Publishing Guide
|
docs.publishing=Publishing Guide
|
||||||
docs.payment=Payment Guide
|
docs.payment=Payment Guide
|
||||||
|
docs.feesAndContributions=Fees and Contributions
|
||||||
docs.error=Error
|
docs.error=Error
|
||||||
docs.error.loadFailed=Unable to load documentation.
|
docs.error.loadFailed=Unable to load documentation.
|
||||||
docs.meta.description=Complete documentation for zapwall.fr
|
docs.meta.description=Complete documentation for zapwall.fr
|
||||||
@ -47,6 +53,19 @@ series.empty=No series published yet.
|
|||||||
series.view=View series
|
series.view=View series
|
||||||
series.publications=Series publications
|
series.publications=Series publications
|
||||||
series.publications.empty=No publications for this series.
|
series.publications.empty=No publications for this series.
|
||||||
|
series.create.button=Create a series
|
||||||
|
series.create.title=Create a new series
|
||||||
|
series.create.submit=Create series
|
||||||
|
series.create.field.title=Series title
|
||||||
|
series.create.field.description=Series description
|
||||||
|
series.create.field.preview=Preview of publication content
|
||||||
|
series.create.field.preview.help=This preview will be visible to everyone to give a taste of the series content
|
||||||
|
series.create.field.category=Publication type
|
||||||
|
series.create.field.cover=Cover image
|
||||||
|
series.create.field.cover.help=Cover image for the series (optional, max 5MB, formats: PNG, JPG, WebP)
|
||||||
|
series.create.error.notAuthor=You must be the author of this page and have unlocked your account to create a series
|
||||||
|
series.create.error.missingFields=Please fill in all required fields
|
||||||
|
series.create.error.publishFailed=Error publishing series
|
||||||
|
|
||||||
# Author page
|
# Author page
|
||||||
author.title=Author page
|
author.title=Author page
|
||||||
@ -56,6 +75,7 @@ author.sponsoring.total=Total received: {{amount}} BTC
|
|||||||
author.sponsoring.sats=In satoshis: {{amount}} sats
|
author.sponsoring.sats=In satoshis: {{amount}} sats
|
||||||
author.notFound=Author page not found.
|
author.notFound=Author page not found.
|
||||||
author.profileNote=This profile data is specific to zapwall.fr and may differ from your Nostr profile.
|
author.profileNote=This profile data is specific to zapwall.fr and may differ from your Nostr profile.
|
||||||
|
author.profilePicture=Profile picture
|
||||||
|
|
||||||
# Publish
|
# Publish
|
||||||
publish.title=Publish a new publication
|
publish.title=Publish a new publication
|
||||||
@ -119,6 +139,19 @@ footer.privacy=Privacy Policy
|
|||||||
common.loading=Loading...
|
common.loading=Loading...
|
||||||
common.loading.articles=Loading articles...
|
common.loading.articles=Loading articles...
|
||||||
common.loading.authors=Loading authors...
|
common.loading.authors=Loading authors...
|
||||||
|
common.edit=Edit
|
||||||
|
common.delete=Delete
|
||||||
|
common.confirmDelete=Confirm delete
|
||||||
|
common.cancel=Cancel
|
||||||
|
|
||||||
|
# Search
|
||||||
|
search.clear=Clear search
|
||||||
|
|
||||||
|
# Upload
|
||||||
|
upload.error.failed=Upload failed
|
||||||
|
|
||||||
|
# Notification
|
||||||
|
notification.delete=Delete notification
|
||||||
common.error=Error
|
common.error=Error
|
||||||
common.error.noContent=No content found
|
common.error.noContent=No content found
|
||||||
common.empty.articles=No articles found. Check back later!
|
common.empty.articles=No articles found. Check back later!
|
||||||
|
|||||||
@ -11,6 +11,7 @@ home.funding.target=Cible : {{target}} BTC
|
|||||||
home.funding.current=Collecté : {{current}} BTC
|
home.funding.current=Collecté : {{current}} BTC
|
||||||
home.funding.progress={{percent}}% du financement atteint
|
home.funding.progress={{percent}}% du financement atteint
|
||||||
home.funding.description=Les fonds collectés par la plateforme servent au développement de fonctions IA gratuites pour les auteurs (développement et matériel).
|
home.funding.description=Les fonds collectés par la plateforme servent au développement de fonctions IA gratuites pour les auteurs (développement et matériel).
|
||||||
|
home.funding.certification.title=Certification sur un signet ancré sur Bitcoin de la propriété intellectuelle
|
||||||
|
|
||||||
# Navigation
|
# Navigation
|
||||||
nav.documentation=Documentation
|
nav.documentation=Documentation
|
||||||
@ -18,12 +19,17 @@ nav.publish=Publier le profil
|
|||||||
nav.createAuthorPage=Créer page auteur
|
nav.createAuthorPage=Créer page auteur
|
||||||
nav.loading=Chargement...
|
nav.loading=Chargement...
|
||||||
|
|
||||||
|
# Connect
|
||||||
|
connect.createAccount=Créer un compte
|
||||||
|
connect.connect=Se connecter
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
docs.title=Documentation
|
docs.title=Documentation
|
||||||
docs.userGuide=Guide d'utilisation
|
docs.userGuide=Guide d'utilisation
|
||||||
docs.faq=FAQ
|
docs.faq=FAQ
|
||||||
docs.publishing=Guide de publication
|
docs.publishing=Guide de publication
|
||||||
docs.payment=Guide de paiement
|
docs.payment=Guide de paiement
|
||||||
|
docs.feesAndContributions=Frais et contributions
|
||||||
docs.error=Erreur
|
docs.error=Erreur
|
||||||
docs.error.loadFailed=Impossible de charger la documentation.
|
docs.error.loadFailed=Impossible de charger la documentation.
|
||||||
docs.meta.description=Documentation complète pour zapwall.fr
|
docs.meta.description=Documentation complète pour zapwall.fr
|
||||||
@ -47,6 +53,19 @@ series.empty=Aucune série publiée pour le moment.
|
|||||||
series.view=Voir la série
|
series.view=Voir la série
|
||||||
series.publications=Publications de la série
|
series.publications=Publications de la série
|
||||||
series.publications.empty=Aucune publication pour cette série.
|
series.publications.empty=Aucune publication pour cette série.
|
||||||
|
series.create.button=Créer une série
|
||||||
|
series.create.title=Créer une nouvelle série
|
||||||
|
series.create.submit=Créer la série
|
||||||
|
series.create.field.title=Titre de la série
|
||||||
|
series.create.field.description=Description de la série
|
||||||
|
series.create.field.preview=Aperçu du contenu d'une publication
|
||||||
|
series.create.field.preview.help=Cet aperçu sera visible par tous pour donner un avant-goût du contenu de la série
|
||||||
|
series.create.field.category=Type de publication
|
||||||
|
series.create.field.cover=Image de couverture
|
||||||
|
series.create.field.cover.help=Image de couverture pour la série (optionnel, max 5Mo, formats: PNG, JPG, WebP)
|
||||||
|
series.create.error.notAuthor=Vous devez être l'auteur de cette page et avoir déverrouillé votre compte pour créer une série
|
||||||
|
series.create.error.missingFields=Veuillez remplir tous les champs obligatoires
|
||||||
|
series.create.error.publishFailed=Erreur lors de la publication de la série
|
||||||
|
|
||||||
# Author page
|
# Author page
|
||||||
author.title=Page auteur
|
author.title=Page auteur
|
||||||
@ -56,6 +75,7 @@ author.sponsoring.total=Total reçu : {{amount}} BTC
|
|||||||
author.sponsoring.sats=En satoshis : {{amount}} sats
|
author.sponsoring.sats=En satoshis : {{amount}} sats
|
||||||
author.notFound=Page auteur introuvable.
|
author.notFound=Page auteur introuvable.
|
||||||
author.profileNote=Les données de ce profil sont spécifiques à zapwall.fr et peuvent différer de votre profil Nostr.
|
author.profileNote=Les données de ce profil sont spécifiques à zapwall.fr et peuvent différer de votre profil Nostr.
|
||||||
|
author.profilePicture=Photo de profil
|
||||||
|
|
||||||
# Publish
|
# Publish
|
||||||
publish.title=Publier une nouvelle publication
|
publish.title=Publier une nouvelle publication
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import { t } from '@/lib/i18n'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { SeriesCard } from '@/components/SeriesCard'
|
import { SeriesCard } from '@/components/SeriesCard'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import { CreateSeriesModal } from '@/components/CreateSeriesModal'
|
||||||
|
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||||
|
|
||||||
function AuthorPageHeader({ presentation }: { presentation: AuthorPresentationArticle | null }) {
|
function AuthorPageHeader({ presentation }: { presentation: AuthorPresentationArticle | null }) {
|
||||||
if (!presentation) {
|
if (!presentation) {
|
||||||
@ -25,7 +27,7 @@ function AuthorPageHeader({ presentation }: { presentation: AuthorPresentationAr
|
|||||||
<div className="relative w-32 h-32 rounded-lg overflow-hidden border border-neon-cyan/20 flex-shrink-0">
|
<div className="relative w-32 h-32 rounded-lg overflow-hidden border border-neon-cyan/20 flex-shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={presentation.bannerUrl}
|
src={presentation.bannerUrl}
|
||||||
alt="Profile picture"
|
alt={t('author.profilePicture')}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
@ -65,18 +67,30 @@ function SponsoringSummary({ totalSponsoring }: { totalSponsoring: number }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SeriesList({ series }: { series: Series[]; authorPubkey: string }) {
|
function SeriesList({ series, authorPubkey, onSeriesCreated }: { series: Series[]; authorPubkey: string; onSeriesCreated: () => void }) {
|
||||||
if (series.length === 0) {
|
const { pubkey, isUnlocked } = useNostrAuth()
|
||||||
return (
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
<div className="bg-cyber-dark/50 border border-neon-cyan/20 rounded-lg p-6">
|
const isAuthor = pubkey === authorPubkey && isUnlocked
|
||||||
<p className="text-cyber-accent">{t('series.empty')}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-2xl font-semibold text-neon-cyan">{t('series.title')}</h2>
|
<h2 className="text-2xl font-semibold text-neon-cyan">{t('series.title')}</h2>
|
||||||
|
{isAuthor && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className="px-4 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"
|
||||||
|
>
|
||||||
|
{t('series.create.button')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{series.length === 0 ? (
|
||||||
|
<div className="bg-cyber-dark/50 border border-neon-cyan/20 rounded-lg p-6">
|
||||||
|
<p className="text-cyber-accent">{t('series.empty')}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{series.map((s) => (
|
{series.map((s) => (
|
||||||
<Link key={s.id} href={`/series/${s.id}`}>
|
<Link key={s.id} href={`/series/${s.id}`}>
|
||||||
@ -84,6 +98,13 @@ function SeriesList({ series }: { series: Series[]; authorPubkey: string }) {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<CreateSeriesModal
|
||||||
|
isOpen={showCreateModal}
|
||||||
|
onClose={() => setShowCreateModal(false)}
|
||||||
|
onSuccess={onSeriesCreated}
|
||||||
|
authorPubkey={authorPubkey}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -110,12 +131,11 @@ function useAuthorData(authorPubkey: string) {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
const reload = async () => {
|
||||||
if (!authorPubkey) {
|
if (!authorPubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const load = async () => {
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
@ -131,10 +151,11 @@ function useAuthorData(authorPubkey: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void load()
|
useEffect(() => {
|
||||||
|
void reload()
|
||||||
}, [authorPubkey])
|
}, [authorPubkey])
|
||||||
|
|
||||||
return { presentation, series, totalSponsoring, loading, error }
|
return { presentation, series, totalSponsoring, loading, error, reload }
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuthorPageContent({
|
function AuthorPageContent({
|
||||||
@ -144,6 +165,7 @@ function AuthorPageContent({
|
|||||||
authorPubkey,
|
authorPubkey,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
onSeriesCreated,
|
||||||
}: {
|
}: {
|
||||||
presentation: AuthorPresentationArticle | null
|
presentation: AuthorPresentationArticle | null
|
||||||
series: Series[]
|
series: Series[]
|
||||||
@ -151,6 +173,7 @@ function AuthorPageContent({
|
|||||||
authorPubkey: string
|
authorPubkey: string
|
||||||
loading: boolean
|
loading: boolean
|
||||||
error: string | null
|
error: string | null
|
||||||
|
onSeriesCreated: () => void
|
||||||
}) {
|
}) {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <p className="text-cyber-accent">{t('common.loading')}</p>
|
return <p className="text-cyber-accent">{t('common.loading')}</p>
|
||||||
@ -165,7 +188,7 @@ function AuthorPageContent({
|
|||||||
<>
|
<>
|
||||||
<AuthorPageHeader presentation={presentation} />
|
<AuthorPageHeader presentation={presentation} />
|
||||||
<SponsoringSummary totalSponsoring={totalSponsoring} />
|
<SponsoringSummary totalSponsoring={totalSponsoring} />
|
||||||
<SeriesList series={series} authorPubkey={authorPubkey} />
|
<SeriesList series={series} authorPubkey={authorPubkey} onSeriesCreated={onSeriesCreated} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -181,7 +204,7 @@ export default function AuthorPage() {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { pubkey } = router.query
|
const { pubkey } = router.query
|
||||||
const authorPubkey = typeof pubkey === 'string' ? pubkey : ''
|
const authorPubkey = typeof pubkey === 'string' ? pubkey : ''
|
||||||
const { presentation, series, totalSponsoring, loading, error } = useAuthorData(authorPubkey)
|
const { presentation, series, totalSponsoring, loading, error, reload } = useAuthorData(authorPubkey)
|
||||||
|
|
||||||
if (!authorPubkey) {
|
if (!authorPubkey) {
|
||||||
return null
|
return null
|
||||||
@ -204,6 +227,7 @@ export default function AuthorPage() {
|
|||||||
authorPubkey={authorPubkey}
|
authorPubkey={authorPubkey}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
|
onSeriesCreated={reload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@ -3,17 +3,9 @@ import { DocsSidebar } from '@/components/DocsSidebar'
|
|||||||
import { DocsContent } from '@/components/DocsContent'
|
import { DocsContent } from '@/components/DocsContent'
|
||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { Footer } from '@/components/Footer'
|
import { Footer } from '@/components/Footer'
|
||||||
import { useDocs } from '@/hooks/useDocs'
|
import { useDocs, type DocLink, type DocSection } from '@/hooks/useDocs'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
|
||||||
type DocSection = 'user-guide' | 'faq' | 'publishing' | 'payment'
|
|
||||||
|
|
||||||
interface DocLink {
|
|
||||||
id: DocSection
|
|
||||||
title: string
|
|
||||||
file: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DocsPage() {
|
export default function DocsPage() {
|
||||||
const docs: DocLink[] = [
|
const docs: DocLink[] = [
|
||||||
{
|
{
|
||||||
@ -36,6 +28,11 @@ export default function DocsPage() {
|
|||||||
title: t('docs.payment'),
|
title: t('docs.payment'),
|
||||||
file: 'payment-guide.md',
|
file: 'payment-guide.md',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'fees-and-contributions',
|
||||||
|
title: t('docs.feesAndContributions'),
|
||||||
|
file: 'fees-and-contributions.md',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const { selectedDoc, docContent, loading, loadDoc } = useDocs(docs)
|
const { selectedDoc, docContent, loading, loadDoc } = useDocs(docs)
|
||||||
@ -55,7 +52,7 @@ export default function DocsPage() {
|
|||||||
<DocsSidebar
|
<DocsSidebar
|
||||||
docs={docs}
|
docs={docs}
|
||||||
selectedDoc={selectedDoc}
|
selectedDoc={selectedDoc}
|
||||||
onSelectDoc={(slug) => {
|
onSelectDoc={(slug: DocSection) => {
|
||||||
void loadDoc(slug)
|
void loadDoc(slug)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ home.funding.target=Target: {{target}} BTC
|
|||||||
home.funding.current=Raised: {{current}} BTC
|
home.funding.current=Raised: {{current}} BTC
|
||||||
home.funding.progress={{percent}}% of funding reached
|
home.funding.progress={{percent}}% of funding reached
|
||||||
home.funding.description=Funds collected by the platform serve the development of free AI features for authors (development and hardware).
|
home.funding.description=Funds collected by the platform serve the development of free AI features for authors (development and hardware).
|
||||||
|
home.funding.certification.title=Certification on a Bitcoin-anchored signet of intellectual property
|
||||||
|
|
||||||
# Navigation
|
# Navigation
|
||||||
nav.documentation=Documentation
|
nav.documentation=Documentation
|
||||||
@ -18,12 +19,17 @@ nav.publish=Publish profile
|
|||||||
nav.createAuthorPage=Create author page
|
nav.createAuthorPage=Create author page
|
||||||
nav.loading=Loading...
|
nav.loading=Loading...
|
||||||
|
|
||||||
|
# Connect
|
||||||
|
connect.createAccount=Create account
|
||||||
|
connect.connect=Connect
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
docs.title=Documentation
|
docs.title=Documentation
|
||||||
docs.userGuide=User Guide
|
docs.userGuide=User Guide
|
||||||
docs.faq=FAQ
|
docs.faq=FAQ
|
||||||
docs.publishing=Publishing Guide
|
docs.publishing=Publishing Guide
|
||||||
docs.payment=Payment Guide
|
docs.payment=Payment Guide
|
||||||
|
docs.feesAndContributions=Fees and Contributions
|
||||||
docs.error=Error
|
docs.error=Error
|
||||||
docs.error.loadFailed=Unable to load documentation.
|
docs.error.loadFailed=Unable to load documentation.
|
||||||
docs.meta.description=Complete documentation for zapwall.fr
|
docs.meta.description=Complete documentation for zapwall.fr
|
||||||
@ -48,6 +54,19 @@ series.empty=No series published yet.
|
|||||||
series.view=View series
|
series.view=View series
|
||||||
series.publications=Series publications
|
series.publications=Series publications
|
||||||
series.publications.empty=No publications for this series.
|
series.publications.empty=No publications for this series.
|
||||||
|
series.create.button=Create a series
|
||||||
|
series.create.title=Create a new series
|
||||||
|
series.create.submit=Create series
|
||||||
|
series.create.field.title=Series title
|
||||||
|
series.create.field.description=Series description
|
||||||
|
series.create.field.preview=Preview of publication content
|
||||||
|
series.create.field.preview.help=This preview will be visible to everyone to give a taste of the series content
|
||||||
|
series.create.field.category=Publication type
|
||||||
|
series.create.field.cover=Cover image
|
||||||
|
series.create.field.cover.help=Cover image for the series (optional, max 5MB, formats: PNG, JPG, WebP)
|
||||||
|
series.create.error.notAuthor=You must be the author of this page and have unlocked your account to create a series
|
||||||
|
series.create.error.missingFields=Please fill in all required fields
|
||||||
|
series.create.error.publishFailed=Error publishing series
|
||||||
|
|
||||||
# Author page
|
# Author page
|
||||||
author.title=Author page
|
author.title=Author page
|
||||||
@ -57,6 +76,7 @@ author.sponsoring.total=Total received: {{amount}} BTC
|
|||||||
author.sponsoring.sats=In satoshis: {{amount}} sats
|
author.sponsoring.sats=In satoshis: {{amount}} sats
|
||||||
author.notFound=Author page not found.
|
author.notFound=Author page not found.
|
||||||
author.profileNote=This profile data is specific to zapwall.fr and may differ from your Nostr profile.
|
author.profileNote=This profile data is specific to zapwall.fr and may differ from your Nostr profile.
|
||||||
|
author.profilePicture=Profile picture
|
||||||
|
|
||||||
# Publish
|
# Publish
|
||||||
publish.title=Publish a new publication
|
publish.title=Publish a new publication
|
||||||
@ -120,6 +140,19 @@ footer.privacy=Privacy Policy
|
|||||||
common.loading=Loading...
|
common.loading=Loading...
|
||||||
common.loading.articles=Loading articles...
|
common.loading.articles=Loading articles...
|
||||||
common.loading.authors=Loading authors...
|
common.loading.authors=Loading authors...
|
||||||
|
common.edit=Edit
|
||||||
|
common.delete=Delete
|
||||||
|
common.confirmDelete=Confirm delete
|
||||||
|
common.cancel=Cancel
|
||||||
|
|
||||||
|
# Search
|
||||||
|
search.clear=Clear search
|
||||||
|
|
||||||
|
# Upload
|
||||||
|
upload.error.failed=Upload failed
|
||||||
|
|
||||||
|
# Notification
|
||||||
|
notification.delete=Delete notification
|
||||||
common.error=Error
|
common.error=Error
|
||||||
common.error.noContent=No content found
|
common.error.noContent=No content found
|
||||||
common.empty.articles=No articles found. Check back later!
|
common.empty.articles=No articles found. Check back later!
|
||||||
|
|||||||
@ -11,6 +11,7 @@ home.funding.target=Cible : {{target}} BTC
|
|||||||
home.funding.current=Collecté : {{current}} BTC
|
home.funding.current=Collecté : {{current}} BTC
|
||||||
home.funding.progress={{percent}}% du financement atteint
|
home.funding.progress={{percent}}% du financement atteint
|
||||||
home.funding.description=Les fonds collectés par la plateforme servent au développement de fonctions IA gratuites pour les auteurs (développement et matériel).
|
home.funding.description=Les fonds collectés par la plateforme servent au développement de fonctions IA gratuites pour les auteurs (développement et matériel).
|
||||||
|
home.funding.certification.title=Certification sur un signet ancré sur Bitcoin de la propriété intellectuelle
|
||||||
|
|
||||||
# Navigation
|
# Navigation
|
||||||
nav.documentation=Documentation
|
nav.documentation=Documentation
|
||||||
@ -18,12 +19,17 @@ nav.publish=Publier le profil
|
|||||||
nav.createAuthorPage=Créer page auteur
|
nav.createAuthorPage=Créer page auteur
|
||||||
nav.loading=Chargement...
|
nav.loading=Chargement...
|
||||||
|
|
||||||
|
# Connect
|
||||||
|
connect.createAccount=Créer un compte
|
||||||
|
connect.connect=Se connecter
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
docs.title=Documentation
|
docs.title=Documentation
|
||||||
docs.userGuide=Guide d'utilisation
|
docs.userGuide=Guide d'utilisation
|
||||||
docs.faq=FAQ
|
docs.faq=FAQ
|
||||||
docs.publishing=Guide de publication
|
docs.publishing=Guide de publication
|
||||||
docs.payment=Guide de paiement
|
docs.payment=Guide de paiement
|
||||||
|
docs.feesAndContributions=Frais et contributions
|
||||||
docs.error=Erreur
|
docs.error=Erreur
|
||||||
docs.error.loadFailed=Impossible de charger la documentation.
|
docs.error.loadFailed=Impossible de charger la documentation.
|
||||||
docs.meta.description=Documentation complète pour zapwall.fr
|
docs.meta.description=Documentation complète pour zapwall.fr
|
||||||
@ -48,6 +54,19 @@ series.empty=Aucune série publiée pour le moment.
|
|||||||
series.view=Voir la série
|
series.view=Voir la série
|
||||||
series.publications=Publications de la série
|
series.publications=Publications de la série
|
||||||
series.publications.empty=Aucune publication pour cette série.
|
series.publications.empty=Aucune publication pour cette série.
|
||||||
|
series.create.button=Créer une série
|
||||||
|
series.create.title=Créer une nouvelle série
|
||||||
|
series.create.submit=Créer la série
|
||||||
|
series.create.field.title=Titre de la série
|
||||||
|
series.create.field.description=Description de la série
|
||||||
|
series.create.field.preview=Aperçu du contenu d'une publication
|
||||||
|
series.create.field.preview.help=Cet aperçu sera visible par tous pour donner un avant-goût du contenu de la série
|
||||||
|
series.create.field.category=Type de publication
|
||||||
|
series.create.field.cover=Image de couverture
|
||||||
|
series.create.field.cover.help=Image de couverture pour la série (optionnel, max 5Mo, formats: PNG, JPG, WebP)
|
||||||
|
series.create.error.notAuthor=Vous devez être l'auteur de cette page et avoir déverrouillé votre compte pour créer une série
|
||||||
|
series.create.error.missingFields=Veuillez remplir tous les champs obligatoires
|
||||||
|
series.create.error.publishFailed=Erreur lors de la publication de la série
|
||||||
|
|
||||||
# Author page
|
# Author page
|
||||||
author.title=Page auteur
|
author.title=Page auteur
|
||||||
@ -57,6 +76,7 @@ author.sponsoring.total=Total reçu : {{amount}} BTC
|
|||||||
author.sponsoring.sats=En satoshis : {{amount}} sats
|
author.sponsoring.sats=En satoshis : {{amount}} sats
|
||||||
author.notFound=Page auteur introuvable.
|
author.notFound=Page auteur introuvable.
|
||||||
author.profileNote=Les données de ce profil sont spécifiques à zapwall.fr et peuvent différer de votre profil Nostr.
|
author.profileNote=Les données de ce profil sont spécifiques à zapwall.fr et peuvent différer de votre profil Nostr.
|
||||||
|
author.profilePicture=Photo de profil
|
||||||
|
|
||||||
# Publish
|
# Publish
|
||||||
publish.title=Publier une nouvelle publication
|
publish.title=Publier une nouvelle publication
|
||||||
@ -120,6 +140,19 @@ footer.privacy=Politique de confidentialité
|
|||||||
common.loading=Chargement...
|
common.loading=Chargement...
|
||||||
common.loading.articles=Chargement des articles...
|
common.loading.articles=Chargement des articles...
|
||||||
common.loading.authors=Chargement des auteurs...
|
common.loading.authors=Chargement des auteurs...
|
||||||
|
common.edit=Modifier
|
||||||
|
common.delete=Supprimer
|
||||||
|
common.confirmDelete=Confirmer la suppression
|
||||||
|
common.cancel=Annuler
|
||||||
|
|
||||||
|
# Search
|
||||||
|
search.clear=Effacer la recherche
|
||||||
|
|
||||||
|
# Upload
|
||||||
|
upload.error.failed=Échec du téléchargement
|
||||||
|
|
||||||
|
# Notification
|
||||||
|
notification.delete=Supprimer la notification
|
||||||
common.error=Erreur
|
common.error=Erreur
|
||||||
common.error.noContent=Aucun contenu trouvé
|
common.error.noContent=Aucun contenu trouvé
|
||||||
common.empty.articles=Aucun article trouvé. Revenez plus tard !
|
common.empty.articles=Aucun article trouvé. Revenez plus tard !
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user