fix key import
This commit is contained in:
parent
27cb1a7b5b
commit
572ee2dde5
@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
|
||||
interface ArticleFieldProps {
|
||||
id: string
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface ArticleFormButtonsProps {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import type { Article } from '@/types/nostr'
|
||||
|
||||
interface ArticlePreviewProps {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ import { useRouter } from 'next/router'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
|
||||
import { ArticleField } from './ArticleField'
|
||||
import { ArticleFormButtons } from './ArticleFormButtons'
|
||||
import { CreateAccountModal } from './CreateAccountModal'
|
||||
import { RecoveryStep } from './CreateAccountModalSteps'
|
||||
import { UnlockAccountModal } from './UnlockAccountModal'
|
||||
@ -251,7 +250,7 @@ function useAuthorPresentationState(pubkey: string | null, existingAuthorName?:
|
||||
presentation,
|
||||
contentDescription,
|
||||
mainnetAddress: existingPresentation.mainnetAddress || '',
|
||||
pictureUrl: existingPresentation.bannerUrl,
|
||||
...(existingPresentation.bannerUrl ? { pictureUrl: existingPresentation.bannerUrl } : {}),
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import type { ArticleCategory } from '@/types/nostr'
|
||||
|
||||
interface CategorySelectProps {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
type CategoryFilter = 'science-fiction' | 'scientific-research' | 'all' | null
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface ClearButtonProps {
|
||||
onClick: () => void
|
||||
@ -9,7 +9,7 @@ export function ClearButton({ onClick }: ClearButtonProps) {
|
||||
<button
|
||||
onClick={onClick}
|
||||
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">
|
||||
<path
|
||||
|
||||
@ -4,6 +4,7 @@ import { ConnectedUserMenu } from './ConnectedUserMenu'
|
||||
import { RecoveryStep } from './CreateAccountModalSteps'
|
||||
import { UnlockAccountModal } from './UnlockAccountModal'
|
||||
import type { NostrProfile } from '@/types/nostr'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
function ConnectForm({
|
||||
onCreateAccount,
|
||||
@ -23,14 +24,14 @@ function ConnectForm({
|
||||
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"
|
||||
>
|
||||
Créer un compte
|
||||
{t('connect.createAccount')}
|
||||
</button>
|
||||
<button
|
||||
onClick={onUnlock}
|
||||
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"
|
||||
>
|
||||
Se connecter
|
||||
{t('connect.connect')}
|
||||
</button>
|
||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||
</div>
|
||||
@ -146,7 +147,7 @@ export function ConnectButton() {
|
||||
return <ConnectedState pubkey={pubkey} profile={profile} loading={loading} disconnect={disconnect} />
|
||||
}
|
||||
|
||||
if (accountExists === true && pubkey && !isUnlocked && !showUnlockModal && !showCreateModal) {
|
||||
if (accountExists === true && pubkey && !isUnlocked && !showUnlockModal) {
|
||||
return (
|
||||
<UnlockState
|
||||
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'
|
||||
|
||||
type DocSection = 'user-guide' | 'faq' | 'publishing' | 'payment'
|
||||
|
||||
interface DocLink {
|
||||
id: DocSection
|
||||
title: string
|
||||
file: string
|
||||
}
|
||||
import type { DocLink, DocSection } from '@/hooks/useDocs'
|
||||
|
||||
interface DocsSidebarProps {
|
||||
docs: DocLink[]
|
||||
|
||||
@ -43,6 +43,7 @@ function FundingStats({ stats }: { stats: ReturnType<typeof estimatePlatformFund
|
||||
|
||||
export function FundingGauge() {
|
||||
const [stats, setStats] = useState(estimatePlatformFunds())
|
||||
const [certificationStats, setCertificationStats] = useState(estimatePlatformFunds())
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
@ -52,6 +53,8 @@ export function FundingGauge() {
|
||||
try {
|
||||
const fundingStats = estimatePlatformFunds()
|
||||
setStats(fundingStats)
|
||||
// Certification uses the same funding pool
|
||||
setCertificationStats(fundingStats)
|
||||
} catch (e) {
|
||||
console.error('Error loading funding stats:', e)
|
||||
} finally {
|
||||
@ -70,9 +73,15 @@ export function FundingGauge() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-cyber-dark/50 border border-neon-cyan/20 rounded-lg p-6 mb-8">
|
||||
<h2 className="text-xl font-semibold text-neon-cyan mb-4">{t('home.funding.title')}</h2>
|
||||
<FundingStats stats={stats} />
|
||||
<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>
|
||||
<FundingStats stats={stats} />
|
||||
</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() {
|
||||
return (
|
||||
<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-2">
|
||||
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 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>
|
||||
<p className="mb-6 text-sm text-cyber-accent/70">
|
||||
{t('home.funding.description')}
|
||||
</p>
|
||||
<FundingGauge />
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import type { MediaRef } from '@/types/nostr'
|
||||
import { uploadNip95Media } from '@/lib/nip95'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface MarkdownEditorProps {
|
||||
value: string
|
||||
@ -108,7 +109,7 @@ async function handleUpload(
|
||||
handlers.onBannerChange?.(media.url)
|
||||
}
|
||||
} catch (e) {
|
||||
handlers.setError(e instanceof Error ? e.message : 'Upload failed')
|
||||
handlers.setError(e instanceof Error ? e.message : t('upload.error.failed'))
|
||||
} finally {
|
||||
handlers.setUploading(false)
|
||||
}
|
||||
|
||||
@ -224,9 +224,6 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
|
||||
{api.url}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-cyber-accent mt-1">
|
||||
{t('settings.nip95.list.priorityLabel', { priority: api.priority, id: api.id })}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
@ -249,7 +246,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div 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>
|
||||
<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"
|
||||
/>
|
||||
<span className="text-sm text-cyber-accent">| ID: {api.id}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { formatTime } from '@/lib/formatTime'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface NotificationActionsProps {
|
||||
timestamp: number
|
||||
@ -16,7 +16,7 @@ export function NotificationActions({ timestamp, onDelete }: NotificationActions
|
||||
onDelete()
|
||||
}}
|
||||
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">
|
||||
<path
|
||||
@ -30,4 +30,3 @@ export function NotificationActions({ timestamp, onDelete }: NotificationActions
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
|
||||
interface NotificationBadgeButtonProps {
|
||||
unreadCount: number
|
||||
@ -34,4 +33,3 @@ export function NotificationBadgeButton({ unreadCount, onClick }: NotificationBa
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import type { Notification } from '@/types/notifications'
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import type { Notification } from '@/types/notifications'
|
||||
import { NotificationContent } from './NotificationContent'
|
||||
import { NotificationActions } from './NotificationActions'
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import type { Notification } from '@/types/notifications'
|
||||
import { NotificationItem } from './NotificationItem'
|
||||
import { NotificationPanelHeader } from './NotificationPanelHeader'
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
|
||||
interface NotificationPanelHeaderProps {
|
||||
unreadCount: number
|
||||
@ -41,4 +40,3 @@ export function NotificationPanelHeader({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
|
||||
export function SearchIcon() {
|
||||
return (
|
||||
|
||||
@ -2,6 +2,7 @@ import { ArticleCard } from './ArticleCard'
|
||||
import type { Article } from '@/types/nostr'
|
||||
import { memo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface UserArticlesViewProps {
|
||||
articles: Article[]
|
||||
@ -21,7 +22,7 @@ interface UserArticlesViewProps {
|
||||
|
||||
const ArticlesLoading = () => (
|
||||
<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>
|
||||
)
|
||||
|
||||
@ -34,7 +35,7 @@ const ArticlesError = ({ message }: { message: string }) => (
|
||||
const EmptyState = ({ show }: { show: boolean }) =>
|
||||
show ? (
|
||||
<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>
|
||||
) : 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"
|
||||
disabled={editingArticleId !== null && editingArticleId !== article.id}
|
||||
>
|
||||
Edit
|
||||
{t('common.edit')}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{pendingDeleteId === article.id ? 'Confirm delete' : 'Delete'}
|
||||
{pendingDeleteId === article.id ? t('common.confirmDelete') : t('common.delete')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import Image from 'next/image'
|
||||
import React from 'react'
|
||||
|
||||
interface UserProfileHeaderProps {
|
||||
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'
|
||||
|
||||
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
|
||||
title: string
|
||||
file: string
|
||||
|
||||
@ -33,7 +33,7 @@ export function canUserDelete(event: Event, userPubkey: string): boolean {
|
||||
* All users can read public content (previews, metadata)
|
||||
* 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
|
||||
canReadFullContent: boolean
|
||||
} {
|
||||
@ -101,6 +101,6 @@ export function getAccessControl(
|
||||
canReadPreview,
|
||||
canReadFullContent,
|
||||
isPaid,
|
||||
reason,
|
||||
...(reason ? { reason } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,7 +188,7 @@ async function buildReviewEvent(
|
||||
articleId: params.articleId,
|
||||
reviewerPubkey: params.reviewerPubkey,
|
||||
content: params.content,
|
||||
title: params.title ?? undefined,
|
||||
...(params.title ? { title: params.title } : {}),
|
||||
})
|
||||
|
||||
// Build JSON metadata
|
||||
|
||||
@ -1,18 +1,6 @@
|
||||
import type { Article } from '@/types/nostr'
|
||||
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
|
||||
|
||||
@ -177,9 +177,15 @@ export async function extractAuthorFromEvent(event: Event): Promise<ExtractedAut
|
||||
return {
|
||||
type: 'author',
|
||||
id,
|
||||
...authorData,
|
||||
pubkey: authorData.pubkey,
|
||||
authorName: authorData.authorName,
|
||||
presentation: authorData.presentation,
|
||||
contentDescription: authorData.contentDescription,
|
||||
category: authorData.category,
|
||||
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 {
|
||||
type: 'series',
|
||||
id,
|
||||
...seriesData,
|
||||
pubkey: seriesData.pubkey,
|
||||
title: seriesData.title,
|
||||
description: seriesData.description,
|
||||
category: seriesData.category,
|
||||
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 {
|
||||
type: 'series',
|
||||
id,
|
||||
...seriesData,
|
||||
pubkey: seriesData.pubkey,
|
||||
title: seriesData.title,
|
||||
description: seriesData.description,
|
||||
category: seriesData.category,
|
||||
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 {
|
||||
type: 'publication',
|
||||
id,
|
||||
...publicationData,
|
||||
pubkey: publicationData.pubkey,
|
||||
title: publicationData.title,
|
||||
preview: publicationData.preview,
|
||||
category: publicationData.category,
|
||||
zapAmount: publicationData.zapAmount,
|
||||
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 {
|
||||
type: 'publication',
|
||||
id,
|
||||
...publicationData,
|
||||
pubkey: publicationData.pubkey,
|
||||
title: publicationData.title,
|
||||
preview: publicationData.preview,
|
||||
category: publicationData.category,
|
||||
zapAmount: publicationData.zapAmount,
|
||||
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,
|
||||
}
|
||||
|
||||
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 {
|
||||
type: 'review',
|
||||
id,
|
||||
...reviewData,
|
||||
pubkey: reviewData.pubkey,
|
||||
articleId: reviewData.articleId,
|
||||
reviewerPubkey: reviewData.reviewerPubkey,
|
||||
content: reviewData.content,
|
||||
eventId: event.id,
|
||||
...(reviewData.title ? { title: reviewData.title } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
@ -502,13 +540,25 @@ export async function extractSponsoringFromEvent(event: Event): Promise<Extracte
|
||||
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 {
|
||||
type: 'sponsoring',
|
||||
id,
|
||||
...sponsoringData,
|
||||
payerPubkey: sponsoringData.payerPubkey,
|
||||
authorPubkey: sponsoringData.authorPubkey,
|
||||
amount: sponsoringData.amount,
|
||||
paymentHash: sponsoringData.paymentHash,
|
||||
eventId: event.id,
|
||||
...(sponsoringData.seriesId ? { seriesId: sponsoringData.seriesId } : {}),
|
||||
...(sponsoringData.articleId ? { articleId: sponsoringData.articleId } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
*/
|
||||
|
||||
import type { Event } from 'nostr-tools'
|
||||
import { extractTagsFromEvent } from './nostrTagSystem'
|
||||
import { canModifyObject, getNextVersion } from './versionManager'
|
||||
|
||||
/**
|
||||
@ -26,7 +25,7 @@ export function canUserModifyObject(event: Event, userPubkey: string): boolean {
|
||||
*/
|
||||
export async function buildUpdateEvent(
|
||||
originalEvent: Event,
|
||||
updatedData: Record<string, unknown>,
|
||||
_updatedData: Record<string, unknown>,
|
||||
userPubkey: string
|
||||
): Promise<Event | null> {
|
||||
// Check if user can modify
|
||||
@ -34,7 +33,6 @@ export async function buildUpdateEvent(
|
||||
throw new Error('Only the author can modify this object')
|
||||
}
|
||||
|
||||
const tags = extractTagsFromEvent(originalEvent)
|
||||
const nextVersion = getNextVersion([originalEvent])
|
||||
|
||||
// Build new event with incremented version
|
||||
@ -70,7 +68,6 @@ export async function buildDeleteEvent(
|
||||
throw new Error('Only the author can delete this object')
|
||||
}
|
||||
|
||||
const tags = extractTagsFromEvent(originalEvent)
|
||||
const nextVersion = getNextVersion([originalEvent])
|
||||
|
||||
// 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 descriptionMatch = content.match(/Description de votre contenu : (.+?)(?:\nAdresse Bitcoin mainnet|$)/s)
|
||||
|
||||
if (newFormatMatch && descriptionMatch) {
|
||||
if (newFormatMatch && descriptionMatch && newFormatMatch[1] && descriptionMatch[1]) {
|
||||
return {
|
||||
presentation: newFormatMatch[1].trim(),
|
||||
contentDescription: descriptionMatch[1].trim(),
|
||||
|
||||
@ -32,7 +32,7 @@ export function parseObjectUrl(url: string): {
|
||||
version: number | null
|
||||
} {
|
||||
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 }
|
||||
}
|
||||
|
||||
|
||||
@ -60,7 +60,10 @@ export function getLatestVersion(events: Event[]): Event | null {
|
||||
|
||||
// Sort by version (descending) and take the first (latest)
|
||||
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
|
||||
@ -71,7 +74,8 @@ export function getLatestVersion(events: Event[]): Event | null {
|
||||
|
||||
// Sort by version and return the latest
|
||||
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.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.certification.title=Certification on a Bitcoin-anchored signet of intellectual property
|
||||
|
||||
# Navigation
|
||||
nav.documentation=Documentation
|
||||
@ -18,12 +19,17 @@ nav.publish=Publish profile
|
||||
nav.createAuthorPage=Create author page
|
||||
nav.loading=Loading...
|
||||
|
||||
# Connect
|
||||
connect.createAccount=Create account
|
||||
connect.connect=Connect
|
||||
|
||||
# Documentation
|
||||
docs.title=Documentation
|
||||
docs.userGuide=User Guide
|
||||
docs.faq=FAQ
|
||||
docs.publishing=Publishing Guide
|
||||
docs.payment=Payment Guide
|
||||
docs.feesAndContributions=Fees and Contributions
|
||||
docs.error=Error
|
||||
docs.error.loadFailed=Unable to load documentation.
|
||||
docs.meta.description=Complete documentation for zapwall.fr
|
||||
@ -47,6 +53,19 @@ series.empty=No series published yet.
|
||||
series.view=View series
|
||||
series.publications=Series publications
|
||||
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.title=Author page
|
||||
@ -56,6 +75,7 @@ author.sponsoring.total=Total received: {{amount}} BTC
|
||||
author.sponsoring.sats=In satoshis: {{amount}} sats
|
||||
author.notFound=Author page not found.
|
||||
author.profileNote=This profile data is specific to zapwall.fr and may differ from your Nostr profile.
|
||||
author.profilePicture=Profile picture
|
||||
|
||||
# Publish
|
||||
publish.title=Publish a new publication
|
||||
@ -119,6 +139,19 @@ footer.privacy=Privacy Policy
|
||||
common.loading=Loading...
|
||||
common.loading.articles=Loading articles...
|
||||
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.noContent=No content found
|
||||
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.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.certification.title=Certification sur un signet ancré sur Bitcoin de la propriété intellectuelle
|
||||
|
||||
# Navigation
|
||||
nav.documentation=Documentation
|
||||
@ -18,12 +19,17 @@ nav.publish=Publier le profil
|
||||
nav.createAuthorPage=Créer page auteur
|
||||
nav.loading=Chargement...
|
||||
|
||||
# Connect
|
||||
connect.createAccount=Créer un compte
|
||||
connect.connect=Se connecter
|
||||
|
||||
# Documentation
|
||||
docs.title=Documentation
|
||||
docs.userGuide=Guide d'utilisation
|
||||
docs.faq=FAQ
|
||||
docs.publishing=Guide de publication
|
||||
docs.payment=Guide de paiement
|
||||
docs.feesAndContributions=Frais et contributions
|
||||
docs.error=Erreur
|
||||
docs.error.loadFailed=Impossible de charger la documentation.
|
||||
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.publications=Publications de la 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.title=Page auteur
|
||||
@ -56,6 +75,7 @@ author.sponsoring.total=Total reçu : {{amount}} BTC
|
||||
author.sponsoring.sats=En satoshis : {{amount}} sats
|
||||
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.profilePicture=Photo de profil
|
||||
|
||||
# Publish
|
||||
publish.title=Publier une nouvelle publication
|
||||
|
||||
@ -12,6 +12,8 @@ import { t } from '@/lib/i18n'
|
||||
import Link from 'next/link'
|
||||
import { SeriesCard } from '@/components/SeriesCard'
|
||||
import Image from 'next/image'
|
||||
import { CreateSeriesModal } from '@/components/CreateSeriesModal'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
|
||||
function AuthorPageHeader({ presentation }: { presentation: AuthorPresentationArticle | null }) {
|
||||
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">
|
||||
<Image
|
||||
src={presentation.bannerUrl}
|
||||
alt="Profile picture"
|
||||
alt={t('author.profilePicture')}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
@ -65,25 +67,44 @@ function SponsoringSummary({ totalSponsoring }: { totalSponsoring: number }) {
|
||||
)
|
||||
}
|
||||
|
||||
function SeriesList({ series }: { series: Series[]; authorPubkey: string }) {
|
||||
if (series.length === 0) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
function SeriesList({ series, authorPubkey, onSeriesCreated }: { series: Series[]; authorPubkey: string; onSeriesCreated: () => void }) {
|
||||
const { pubkey, isUnlocked } = useNostrAuth()
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const isAuthor = pubkey === authorPubkey && isUnlocked
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-semibold text-neon-cyan">{t('series.title')}</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{series.map((s) => (
|
||||
<Link key={s.id} href={`/series/${s.id}`}>
|
||||
<SeriesCard series={s} onSelect={() => {}} />
|
||||
</Link>
|
||||
))}
|
||||
<div className="flex items-center justify-between">
|
||||
<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">
|
||||
{series.map((s) => (
|
||||
<Link key={s.id} href={`/series/${s.id}`}>
|
||||
<SeriesCard series={s} onSelect={() => {}} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<CreateSeriesModal
|
||||
isOpen={showCreateModal}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
onSuccess={onSeriesCreated}
|
||||
authorPubkey={authorPubkey}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -110,31 +131,31 @@ function useAuthorData(authorPubkey: string) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const reload = async () => {
|
||||
if (!authorPubkey) {
|
||||
return
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const { pres, seriesList, sponsoring } = await loadAuthorData(authorPubkey)
|
||||
setPresentation(pres)
|
||||
setSeries(seriesList)
|
||||
setTotalSponsoring(sponsoring)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Erreur lors du chargement')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
try {
|
||||
const { pres, seriesList, sponsoring } = await loadAuthorData(authorPubkey)
|
||||
setPresentation(pres)
|
||||
setSeries(seriesList)
|
||||
setTotalSponsoring(sponsoring)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Erreur lors du chargement')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
void load()
|
||||
useEffect(() => {
|
||||
void reload()
|
||||
}, [authorPubkey])
|
||||
|
||||
return { presentation, series, totalSponsoring, loading, error }
|
||||
return { presentation, series, totalSponsoring, loading, error, reload }
|
||||
}
|
||||
|
||||
function AuthorPageContent({
|
||||
@ -144,6 +165,7 @@ function AuthorPageContent({
|
||||
authorPubkey,
|
||||
loading,
|
||||
error,
|
||||
onSeriesCreated,
|
||||
}: {
|
||||
presentation: AuthorPresentationArticle | null
|
||||
series: Series[]
|
||||
@ -151,6 +173,7 @@ function AuthorPageContent({
|
||||
authorPubkey: string
|
||||
loading: boolean
|
||||
error: string | null
|
||||
onSeriesCreated: () => void
|
||||
}) {
|
||||
if (loading) {
|
||||
return <p className="text-cyber-accent">{t('common.loading')}</p>
|
||||
@ -165,7 +188,7 @@ function AuthorPageContent({
|
||||
<>
|
||||
<AuthorPageHeader presentation={presentation} />
|
||||
<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 { pubkey } = router.query
|
||||
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) {
|
||||
return null
|
||||
@ -204,6 +227,7 @@ export default function AuthorPage() {
|
||||
authorPubkey={authorPubkey}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSeriesCreated={reload}
|
||||
/>
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
@ -3,17 +3,9 @@ import { DocsSidebar } from '@/components/DocsSidebar'
|
||||
import { DocsContent } from '@/components/DocsContent'
|
||||
import { PageHeader } from '@/components/PageHeader'
|
||||
import { Footer } from '@/components/Footer'
|
||||
import { useDocs } from '@/hooks/useDocs'
|
||||
import { useDocs, type DocLink, type DocSection } from '@/hooks/useDocs'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
type DocSection = 'user-guide' | 'faq' | 'publishing' | 'payment'
|
||||
|
||||
interface DocLink {
|
||||
id: DocSection
|
||||
title: string
|
||||
file: string
|
||||
}
|
||||
|
||||
export default function DocsPage() {
|
||||
const docs: DocLink[] = [
|
||||
{
|
||||
@ -36,6 +28,11 @@ export default function DocsPage() {
|
||||
title: t('docs.payment'),
|
||||
file: 'payment-guide.md',
|
||||
},
|
||||
{
|
||||
id: 'fees-and-contributions',
|
||||
title: t('docs.feesAndContributions'),
|
||||
file: 'fees-and-contributions.md',
|
||||
},
|
||||
]
|
||||
|
||||
const { selectedDoc, docContent, loading, loadDoc } = useDocs(docs)
|
||||
@ -55,7 +52,7 @@ export default function DocsPage() {
|
||||
<DocsSidebar
|
||||
docs={docs}
|
||||
selectedDoc={selectedDoc}
|
||||
onSelectDoc={(slug) => {
|
||||
onSelectDoc={(slug: DocSection) => {
|
||||
void loadDoc(slug)
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -11,6 +11,7 @@ home.funding.target=Target: {{target}} BTC
|
||||
home.funding.current=Raised: {{current}} BTC
|
||||
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.certification.title=Certification on a Bitcoin-anchored signet of intellectual property
|
||||
|
||||
# Navigation
|
||||
nav.documentation=Documentation
|
||||
@ -18,12 +19,17 @@ nav.publish=Publish profile
|
||||
nav.createAuthorPage=Create author page
|
||||
nav.loading=Loading...
|
||||
|
||||
# Connect
|
||||
connect.createAccount=Create account
|
||||
connect.connect=Connect
|
||||
|
||||
# Documentation
|
||||
docs.title=Documentation
|
||||
docs.userGuide=User Guide
|
||||
docs.faq=FAQ
|
||||
docs.publishing=Publishing Guide
|
||||
docs.payment=Payment Guide
|
||||
docs.feesAndContributions=Fees and Contributions
|
||||
docs.error=Error
|
||||
docs.error.loadFailed=Unable to load documentation.
|
||||
docs.meta.description=Complete documentation for zapwall.fr
|
||||
@ -48,6 +54,19 @@ series.empty=No series published yet.
|
||||
series.view=View series
|
||||
series.publications=Series publications
|
||||
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.title=Author page
|
||||
@ -57,6 +76,7 @@ author.sponsoring.total=Total received: {{amount}} BTC
|
||||
author.sponsoring.sats=In satoshis: {{amount}} sats
|
||||
author.notFound=Author page not found.
|
||||
author.profileNote=This profile data is specific to zapwall.fr and may differ from your Nostr profile.
|
||||
author.profilePicture=Profile picture
|
||||
|
||||
# Publish
|
||||
publish.title=Publish a new publication
|
||||
@ -120,6 +140,19 @@ footer.privacy=Privacy Policy
|
||||
common.loading=Loading...
|
||||
common.loading.articles=Loading articles...
|
||||
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.noContent=No content found
|
||||
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.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.certification.title=Certification sur un signet ancré sur Bitcoin de la propriété intellectuelle
|
||||
|
||||
# Navigation
|
||||
nav.documentation=Documentation
|
||||
@ -18,12 +19,17 @@ nav.publish=Publier le profil
|
||||
nav.createAuthorPage=Créer page auteur
|
||||
nav.loading=Chargement...
|
||||
|
||||
# Connect
|
||||
connect.createAccount=Créer un compte
|
||||
connect.connect=Se connecter
|
||||
|
||||
# Documentation
|
||||
docs.title=Documentation
|
||||
docs.userGuide=Guide d'utilisation
|
||||
docs.faq=FAQ
|
||||
docs.publishing=Guide de publication
|
||||
docs.payment=Guide de paiement
|
||||
docs.feesAndContributions=Frais et contributions
|
||||
docs.error=Erreur
|
||||
docs.error.loadFailed=Impossible de charger la documentation.
|
||||
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.publications=Publications de la 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.title=Page auteur
|
||||
@ -57,6 +76,7 @@ author.sponsoring.total=Total reçu : {{amount}} BTC
|
||||
author.sponsoring.sats=En satoshis : {{amount}} sats
|
||||
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.profilePicture=Photo de profil
|
||||
|
||||
# Publish
|
||||
publish.title=Publier une nouvelle publication
|
||||
@ -120,6 +140,19 @@ footer.privacy=Politique de confidentialité
|
||||
common.loading=Chargement...
|
||||
common.loading.articles=Chargement des articles...
|
||||
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.noContent=Aucun contenu trouvé
|
||||
common.empty.articles=Aucun article trouvé. Revenez plus tard !
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user