create for series
This commit is contained in:
parent
8ac7de090c
commit
30d37ec19c
@ -4,6 +4,7 @@ import { useArticlePayment } from '@/hooks/useArticlePayment'
|
||||
import { ArticlePreview } from './ArticlePreview'
|
||||
import { PaymentModal } from './PaymentModal'
|
||||
import { Card } from './ui'
|
||||
import { useToast } from './ui/ToastContainer'
|
||||
import { t } from '@/lib/i18n'
|
||||
import Link from 'next/link'
|
||||
|
||||
@ -56,40 +57,90 @@ function ArticleMeta({
|
||||
)
|
||||
}
|
||||
|
||||
export function ArticleCard({ article, onUnlock }: ArticleCardProps): React.ReactElement {
|
||||
const { pubkey, connect } = useNostrAuth()
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
paymentInvoice,
|
||||
handleUnlock,
|
||||
handlePaymentComplete,
|
||||
handleCloseModal,
|
||||
} = useArticlePayment(article, pubkey ?? null, () => {
|
||||
onUnlock?.(article)
|
||||
}, connect)
|
||||
interface UseArticleCardStateParams {
|
||||
article: Article
|
||||
pubkey: string | null
|
||||
connect: (() => Promise<void>) | undefined
|
||||
onUnlock: ((article: Article) => void) | undefined
|
||||
showToast: (message: string, variant?: 'success' | 'info' | 'warning' | 'error', duration?: number) => void
|
||||
}
|
||||
|
||||
function useArticleCardState(params: UseArticleCardStateParams): {
|
||||
loading: boolean
|
||||
error: string | null
|
||||
paymentInvoice: ReturnType<typeof useArticlePayment>['paymentInvoice']
|
||||
handleUnlock: () => Promise<void>
|
||||
handlePaymentComplete: () => Promise<void>
|
||||
handleCloseModal: () => void
|
||||
} {
|
||||
return useArticlePayment({
|
||||
article: params.article,
|
||||
pubkey: params.pubkey,
|
||||
onUnlockSuccess: () => {
|
||||
params.showToast(t('article.unlock.success'), 'success')
|
||||
params.onUnlock?.(params.article)
|
||||
},
|
||||
connect: params.connect,
|
||||
showToast: params.showToast,
|
||||
})
|
||||
}
|
||||
|
||||
function ArticleCardContent(params: {
|
||||
article: Article
|
||||
loading: boolean
|
||||
error: string | null
|
||||
paymentInvoice: ReturnType<typeof useArticlePayment>['paymentInvoice']
|
||||
handleUnlock: () => Promise<void>
|
||||
handlePaymentComplete: () => Promise<void>
|
||||
handleCloseModal: () => void
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<Card variant="interactive" className="mb-0">
|
||||
<ArticleHeader article={article} />
|
||||
<>
|
||||
<ArticleHeader article={params.article} />
|
||||
<div className="text-cyber-accent mb-4">
|
||||
<ArticlePreview
|
||||
article={article}
|
||||
loading={loading}
|
||||
article={params.article}
|
||||
loading={params.loading}
|
||||
onUnlock={() => {
|
||||
void handleUnlock()
|
||||
void params.handleUnlock()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ArticleMeta
|
||||
article={article}
|
||||
error={error}
|
||||
paymentInvoice={paymentInvoice}
|
||||
onClose={handleCloseModal}
|
||||
article={params.article}
|
||||
error={params.error}
|
||||
paymentInvoice={params.paymentInvoice}
|
||||
onClose={params.handleCloseModal}
|
||||
onPaymentComplete={() => {
|
||||
void handlePaymentComplete()
|
||||
void params.handlePaymentComplete()
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function ArticleCard({ article, onUnlock }: ArticleCardProps): React.ReactElement {
|
||||
const { pubkey, connect } = useNostrAuth()
|
||||
const { showToast } = useToast()
|
||||
const state = useArticleCardState({
|
||||
article,
|
||||
pubkey: pubkey ?? null,
|
||||
connect,
|
||||
onUnlock,
|
||||
showToast,
|
||||
})
|
||||
|
||||
return (
|
||||
<Card variant="interactive" className="mb-0">
|
||||
<ArticleCardContent
|
||||
article={article}
|
||||
loading={state.loading}
|
||||
error={state.error}
|
||||
paymentInvoice={state.paymentInvoice}
|
||||
handleUnlock={state.handleUnlock}
|
||||
handlePaymentComplete={state.handlePaymentComplete}
|
||||
handleCloseModal={state.handleCloseModal}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Card } from './ui'
|
||||
import { useToast } from './ui/ToastContainer'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { useArticlePublishing } from '@/hooks/useArticlePublishing'
|
||||
import type { ArticleDraft } from '@/lib/articlePublisher'
|
||||
import { ArticleEditorForm } from './ArticleEditorForm'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface ArticleEditorProps {
|
||||
onPublishSuccess?: (articleId: string) => void
|
||||
@ -22,8 +24,18 @@ function SuccessMessage(): React.ReactElement {
|
||||
)
|
||||
}
|
||||
|
||||
function usePublishSuccessToast(success: boolean, showToast: (message: string, variant?: 'success' | 'info' | 'warning' | 'error', duration?: number) => void, onPublishSuccess: ((articleId: string) => void) | undefined, draftTitle: string): void {
|
||||
useEffect(() => {
|
||||
if (success) {
|
||||
showToast(t('article.publish.success'), 'success')
|
||||
onPublishSuccess?.(draftTitle)
|
||||
}
|
||||
}, [success, showToast, onPublishSuccess, draftTitle])
|
||||
}
|
||||
|
||||
export function ArticleEditor({ onPublishSuccess, onCancel, seriesOptions, onSelectSeries, defaultSeriesId }: ArticleEditorProps): React.ReactElement {
|
||||
const { connected, pubkey, connect } = useNostrAuth()
|
||||
const { showToast } = useToast()
|
||||
const { loading, error, success, relayStatuses, publishArticle } = useArticlePublishing(pubkey ?? null)
|
||||
const [draft, setDraft] = useState<ArticleDraft>({
|
||||
title: '',
|
||||
@ -34,6 +46,8 @@ export function ArticleEditor({ onPublishSuccess, onCancel, seriesOptions, onSel
|
||||
...(defaultSeriesId ? { seriesId: defaultSeriesId } : {}),
|
||||
})
|
||||
|
||||
usePublishSuccessToast(success, showToast, onPublishSuccess, draft.title)
|
||||
|
||||
const submit = buildSubmitHandler({
|
||||
publishArticle,
|
||||
draft,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { Article } from '@/types/nostr'
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
import { ErrorState, EmptyState } from './ui'
|
||||
import { ErrorState, EmptyState, Skeleton } from './ui'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface ArticlesListProps {
|
||||
@ -12,11 +12,28 @@ interface ArticlesListProps {
|
||||
unlockedArticles: Set<string>
|
||||
}
|
||||
|
||||
function LoadingState(): React.ReactElement {
|
||||
// Use generic loading message at startup, then specific message once we know what we're loading
|
||||
function ArticleCardSkeleton(): React.ReactElement {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-cyber-accent/70">{t('common.loading.articles')}</p>
|
||||
<div className="border border-neon-cyan/30 rounded-lg p-6 bg-cyber-dark space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Skeleton variant="rectangular" height={24} className="w-3/4" />
|
||||
<Skeleton variant="rectangular" height={16} className="w-1/2" />
|
||||
</div>
|
||||
<Skeleton variant="rectangular" height={100} className="w-full" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton variant="circular" width={32} height={32} />
|
||||
<Skeleton variant="rectangular" height={36} className="w-32" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LoadingState(): React.ReactElement {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<ArticleCardSkeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { Article } from '@/types/nostr'
|
||||
import { AuthorCard } from './AuthorCard'
|
||||
import { ErrorState, EmptyState } from './ui'
|
||||
import { ErrorState, EmptyState, Skeleton } from './ui'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface AuthorsListProps {
|
||||
@ -10,10 +10,27 @@ interface AuthorsListProps {
|
||||
error: string | null
|
||||
}
|
||||
|
||||
function AuthorCardSkeleton(): React.ReactElement {
|
||||
return (
|
||||
<div className="border border-neon-cyan/30 rounded-lg p-6 bg-cyber-dark space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Skeleton variant="circular" width={64} height={64} />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton variant="rectangular" height={20} className="w-2/3" />
|
||||
<Skeleton variant="rectangular" height={16} className="w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton variant="rectangular" height={60} className="w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LoadingState(): React.ReactElement {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-cyber-accent/70">{t('common.loading.authors')}</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<AuthorCardSkeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Card } from './ui'
|
||||
import { Card, Skeleton } from './ui'
|
||||
import { renderMarkdown } from '@/lib/markdownRenderer'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface DocsContentProps {
|
||||
content: string
|
||||
@ -10,9 +9,15 @@ interface DocsContentProps {
|
||||
export function DocsContent({ content, loading }: DocsContentProps): React.ReactElement {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-cyber-accent">{t('docs.loading')}</p>
|
||||
</div>
|
||||
<Card variant="default" className="bg-cyber-dark backdrop-blur-sm space-y-4">
|
||||
<Skeleton variant="rectangular" height={32} className="w-1/2" />
|
||||
<Skeleton variant="rectangular" height={16} className="w-full" />
|
||||
<Skeleton variant="rectangular" height={16} className="w-full" />
|
||||
<Skeleton variant="rectangular" height={16} className="w-3/4" />
|
||||
<Skeleton variant="rectangular" height={32} className="w-1/3" />
|
||||
<Skeleton variant="rectangular" height={16} className="w-full" />
|
||||
<Skeleton variant="rectangular" height={16} className="w-5/6" />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { estimatePlatformFunds } from '@/lib/fundingCalculation'
|
||||
import { Card } from './ui'
|
||||
import { Card, Skeleton } from './ui'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface FundingProgressBarProps {
|
||||
@ -69,8 +69,14 @@ export function FundingGauge(): React.ReactElement {
|
||||
|
||||
function FundingGaugeLoading(): React.ReactElement {
|
||||
return (
|
||||
<Card variant="default" className="bg-cyber-dark/50 mb-8">
|
||||
<p className="text-cyber-accent">{t('common.loading')}</p>
|
||||
<Card variant="default" className="bg-cyber-dark/50 mb-8 space-y-4">
|
||||
<Skeleton variant="rectangular" height={24} className="w-1/3" />
|
||||
<Skeleton variant="rectangular" height={100} className="w-full" />
|
||||
<div className="flex gap-4">
|
||||
<Skeleton variant="rectangular" height={60} className="w-1/3" />
|
||||
<Skeleton variant="rectangular" height={60} className="w-1/3" />
|
||||
<Skeleton variant="rectangular" height={60} className="w-1/3" />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import type { AlbyInvoice } from '@/types/alby'
|
||||
import { getAlbyService, isWebLNAvailable } from '@/lib/alby'
|
||||
import { AlbyInstaller } from './AlbyInstaller'
|
||||
import { Card, Modal, Button } from './ui'
|
||||
import { useToast } from './ui/ToastContainer'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
interface PaymentModalProps {
|
||||
@ -137,20 +138,20 @@ type PaymentModalState = {
|
||||
handleOpenWallet: () => Promise<void>
|
||||
}
|
||||
|
||||
function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => void): PaymentModalState {
|
||||
function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => void, showToast: ((message: string, variant?: 'success' | 'info' | 'warning' | 'error', duration?: number) => void) | undefined): PaymentModalState {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const paymentUrl = `lightning:${invoice.invoice}`
|
||||
const timeRemaining = useInvoiceTimer(invoice.expiresAt)
|
||||
|
||||
const handleCopy = useCallback(
|
||||
(): Promise<void> => copyInvoiceToClipboard({ invoice: invoice.invoice, setCopied, setErrorMessage }),
|
||||
[invoice.invoice]
|
||||
(): Promise<void> => copyInvoiceToClipboard({ invoice: invoice.invoice, setCopied, setErrorMessage, showToast: showToast ?? undefined }),
|
||||
[invoice.invoice, showToast]
|
||||
)
|
||||
|
||||
const handleOpenWallet = useCallback(
|
||||
(): Promise<void> => openWalletForInvoice({ invoice: invoice.invoice, onPaymentComplete, setErrorMessage }),
|
||||
[invoice.invoice, onPaymentComplete]
|
||||
(): Promise<void> => openWalletForInvoice({ invoice: invoice.invoice, onPaymentComplete, setErrorMessage, showToast: showToast ?? undefined }),
|
||||
[invoice.invoice, onPaymentComplete, showToast]
|
||||
)
|
||||
|
||||
return { copied, errorMessage, paymentUrl, timeRemaining, handleCopy, handleOpenWallet }
|
||||
@ -160,10 +161,14 @@ async function copyInvoiceToClipboard(params: {
|
||||
invoice: string
|
||||
setCopied: (value: boolean) => void
|
||||
setErrorMessage: (value: string | null) => void
|
||||
showToast: ((message: string, variant?: 'success' | 'info' | 'warning' | 'error', duration?: number) => void) | undefined
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(params.invoice)
|
||||
params.setCopied(true)
|
||||
if (params.showToast !== undefined) {
|
||||
params.showToast(t('payment.modal.copySuccess'), 'success', 2000)
|
||||
}
|
||||
scheduleCopiedReset(params.setCopied)
|
||||
} catch (e) {
|
||||
console.error('Failed to copy:', e)
|
||||
@ -175,9 +180,13 @@ async function openWalletForInvoice(params: {
|
||||
invoice: string
|
||||
onPaymentComplete: () => void
|
||||
setErrorMessage: (value: string | null) => void
|
||||
showToast: ((message: string, variant?: 'success' | 'info' | 'warning' | 'error', duration?: number) => void) | undefined
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await payWithWebLN(params.invoice)
|
||||
if (params.showToast !== undefined) {
|
||||
params.showToast(t('payment.modal.paymentInitiated'), 'success')
|
||||
}
|
||||
params.onPaymentComplete()
|
||||
} catch (e) {
|
||||
const error = normalizePaymentError(e)
|
||||
@ -211,8 +220,9 @@ async function payWithWebLN(invoice: string): Promise<void> {
|
||||
}
|
||||
|
||||
export function PaymentModal({ invoice, onClose, onPaymentComplete }: PaymentModalProps): React.ReactElement {
|
||||
const { showToast } = useToast()
|
||||
const { copied, errorMessage, paymentUrl, timeRemaining, handleCopy, handleOpenWallet } =
|
||||
usePaymentModalState(invoice, onPaymentComplete)
|
||||
usePaymentModalState(invoice, onPaymentComplete, showToast)
|
||||
const handleOpenWalletSync = (): void => {
|
||||
void handleOpenWallet()
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
import { Button, ErrorState } from './ui'
|
||||
import { Button, ErrorState, Skeleton } from './ui'
|
||||
import type { Article } from '@/types/nostr'
|
||||
import { memo } from 'react'
|
||||
import Link from 'next/link'
|
||||
@ -21,9 +21,27 @@ interface UserArticlesViewProps {
|
||||
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
|
||||
}
|
||||
|
||||
function ArticleCardSkeleton(): React.ReactElement {
|
||||
return (
|
||||
<div className="border border-neon-cyan/30 rounded-lg p-6 bg-cyber-dark space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Skeleton variant="rectangular" height={24} className="w-3/4" />
|
||||
<Skeleton variant="rectangular" height={16} className="w-1/2" />
|
||||
</div>
|
||||
<Skeleton variant="rectangular" height={100} className="w-full" />
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton variant="circular" width={32} height={32} />
|
||||
<Skeleton variant="rectangular" height={36} className="w-32" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ArticlesLoading = (): React.ReactElement => (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500">{t('common.loading.articles')}</p>
|
||||
<div className="space-y-6">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<ArticleCardSkeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Card } from '../ui'
|
||||
import { Card, Skeleton } from '../ui'
|
||||
import { t } from '@/lib/i18n'
|
||||
import type { AuthorPresentationArticle, Series } from '@/types/nostr'
|
||||
import { AuthorPageHeader } from './AuthorPageHeader'
|
||||
@ -15,6 +15,41 @@ type AuthorPageContentProps = {
|
||||
onSeriesCreated: () => void
|
||||
}
|
||||
|
||||
function AuthorPageLoadingSkeleton(): React.ReactElement {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<Skeleton variant="rectangular" height={32} className="w-1/3" />
|
||||
<Skeleton variant="rectangular" height={200} className="w-full" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Skeleton variant="rectangular" height={24} className="w-1/4" />
|
||||
<Skeleton variant="rectangular" height={100} className="w-full" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Skeleton variant="rectangular" height={24} className="w-1/4" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{Array.from({ length: 2 }).map((_, index) => (
|
||||
<Skeleton key={index} variant="rectangular" height={150} className="w-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AuthorPageError({ error }: { error: string }): React.ReactElement {
|
||||
return <p className="text-red-400">{error}</p>
|
||||
}
|
||||
|
||||
function AuthorPageNotFound(): React.ReactElement {
|
||||
return (
|
||||
<Card variant="default" className="bg-cyber-dark/50">
|
||||
<p className="text-cyber-accent">{t('author.notFound')}</p>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function AuthorPageContent({
|
||||
presentation,
|
||||
series,
|
||||
@ -25,21 +60,14 @@ export function AuthorPageContent({
|
||||
onSeriesCreated,
|
||||
}: AuthorPageContentProps): React.ReactElement {
|
||||
if (loading) {
|
||||
return <p className="text-cyber-accent">{t('common.loading')}</p>
|
||||
return <AuthorPageLoadingSkeleton />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <p className="text-red-400">{error}</p>
|
||||
return <AuthorPageError error={error} />
|
||||
}
|
||||
|
||||
if (!presentation) {
|
||||
return (
|
||||
<Card variant="default" className="bg-cyber-dark/50">
|
||||
<p className="text-cyber-accent">{t('author.notFound')}</p>
|
||||
</Card>
|
||||
)
|
||||
return <AuthorPageNotFound />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthorPageHeader presentation={presentation} />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
|
||||
import { Button, Card } from '../ui'
|
||||
import { Button, Card, Skeleton } from '../ui'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { NoAccountView } from './NoAccountView'
|
||||
import { PresentationForm } from './PresentationForm'
|
||||
@ -28,8 +28,19 @@ function SuccessNotice(params: { pubkey: string | null }): React.ReactElement {
|
||||
|
||||
function LoadingNotice(): React.ReactElement {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-cyber-accent/70">{t('common.loading')}</p>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<Skeleton variant="rectangular" height={24} className="w-1/3" />
|
||||
<Skeleton variant="rectangular" height={100} className="w-full" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Skeleton variant="rectangular" height={24} className="w-1/4" />
|
||||
<Skeleton variant="rectangular" height={200} className="w-full" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Skeleton variant="rectangular" height={24} className="w-1/4" />
|
||||
<Skeleton variant="rectangular" height={150} className="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
64
components/ui/ToastContainer.tsx
Normal file
64
components/ui/ToastContainer.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import { Toast, type ToastVariant } from './Toast'
|
||||
|
||||
interface ToastMessage {
|
||||
id: string
|
||||
message: string
|
||||
variant: ToastVariant
|
||||
duration?: number
|
||||
}
|
||||
|
||||
interface ToastContextValue {
|
||||
showToast: (message: string, variant?: ToastVariant, duration?: number) => void
|
||||
}
|
||||
|
||||
const ToastContext = React.createContext<ToastContextValue | undefined>(undefined)
|
||||
|
||||
function generateToastId(): string {
|
||||
return `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
export function useToast(): ToastContextValue {
|
||||
const context = React.useContext(ToastContext)
|
||||
if (!context) {
|
||||
throw new Error('useToast must be used within ToastProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function ToastProvider({ children }: { children: React.ReactNode }): React.ReactElement {
|
||||
const [toasts, setToasts] = useState<ToastMessage[]>([])
|
||||
|
||||
const showToast = useCallback((message: string, variant: ToastVariant = 'info', duration = 5000): void => {
|
||||
const id = generateToastId()
|
||||
setToasts((prev) => [...prev, { id, message, variant, duration }])
|
||||
}, [])
|
||||
|
||||
const removeToast = useCallback((id: string): void => {
|
||||
setToasts((prev) => prev.filter((toast) => toast.id !== id))
|
||||
}, [])
|
||||
|
||||
const contextValue: ToastContextValue = {
|
||||
showToast,
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={contextValue}>
|
||||
{children}
|
||||
<div className="fixed top-4 right-4 z-50 space-y-2 pointer-events-none">
|
||||
{toasts.map((toast) => (
|
||||
<div key={toast.id} className="pointer-events-auto">
|
||||
<Toast
|
||||
variant={toast.variant}
|
||||
{...(toast.duration !== undefined ? { duration: toast.duration } : {})}
|
||||
onClose={() => removeToast(toast.id)}
|
||||
aria-label={toast.message}
|
||||
>
|
||||
{toast.message}
|
||||
</Toast>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
)
|
||||
}
|
||||
@ -6,6 +6,7 @@ export { Badge, type BadgeVariant } from './Badge'
|
||||
export { Skeleton } from './Skeleton'
|
||||
export { Modal } from './Modal'
|
||||
export { Toast, type ToastVariant } from './Toast'
|
||||
export { ToastProvider } from './ToastContainer'
|
||||
export { MobileMenu } from './MobileMenu'
|
||||
export { EmptyState } from './EmptyState'
|
||||
export { ErrorState } from './ErrorState'
|
||||
|
||||
@ -105,6 +105,26 @@ Aucun composant prioritaire restant. Tous les composants principaux ont été mi
|
||||
- ✅ `components/markdownEditorTwoColumns/MarkdownEditorTwoColumns.tsx` - Migration de MarkdownPreview vers Card et textarea vers Textarea
|
||||
- ✅ `components/markdownEditorTwoColumns/PagesManager.tsx` - Nettoyage des classes CSS redondantes dans Textarea (w-full, border, rounded, p-* gérés par le composant)
|
||||
|
||||
## Améliorations UX implémentées
|
||||
|
||||
### Skeleton loaders (Priorité haute #1)
|
||||
- ✅ `components/ArticlesList.tsx` - Remplacement du message "Loading..." par des skeleton loaders pour les cartes d'articles
|
||||
- ✅ `components/AuthorsList.tsx` - Remplacement du message "Loading..." par des skeleton loaders pour les cartes d'auteurs
|
||||
- ✅ `components/UserArticlesList.tsx` - Remplacement du message "Loading..." par des skeleton loaders pour les articles utilisateur
|
||||
- ✅ `components/authorPage/AuthorPageContent.tsx` - Remplacement du message "Loading..." par des skeleton loaders pour la page auteur
|
||||
- ✅ `components/authorPresentationEditor/AuthorPresentationEditor.tsx` - Remplacement du message "Loading..." par des skeleton loaders pour le formulaire de présentation
|
||||
- ✅ `components/DocsContent.tsx` - Remplacement du message "Loading..." par des skeleton loaders pour le contenu markdown
|
||||
- ✅ `components/FundingGauge.tsx` - Remplacement du message "Loading..." par des skeleton loaders pour le gauge de financement
|
||||
|
||||
### Toast notifications (Priorité haute #2)
|
||||
- ✅ `components/ui/ToastContainer.tsx` - Création du système de gestion des toasts avec ToastProvider et useToast hook
|
||||
- ✅ `pages/_app.tsx` - Intégration du ToastProvider dans l'application
|
||||
- ✅ `components/ArticleCard.tsx` - Ajout d'un toast de succès après déblocage réussi d'un article
|
||||
- ✅ `components/ArticleEditor.tsx` - Ajout d'un toast de succès après publication réussie
|
||||
- ✅ `components/PaymentModal.tsx` - Ajout d'un toast de succès après copie d'invoice et après initiation du paiement
|
||||
- ✅ `hooks/useArticlePayment.ts` - Ajout du support showToast pour afficher les toasts après paiement
|
||||
- ✅ `locales/fr.txt` et `locales/en.txt` - Ajout des clés de traduction pour les toasts (`article.unlock.success`, `article.publish.success`, `payment.modal.copySuccess`, `payment.modal.paymentInitiated`)
|
||||
|
||||
## Erreurs corrigées
|
||||
|
||||
### TypeScript
|
||||
|
||||
@ -2,59 +2,147 @@
|
||||
|
||||
**Date** : 2025-01-27
|
||||
**Auteur** : Équipe 4NK
|
||||
**Dernière mise à jour** : 2025-01-27
|
||||
|
||||
## Migration des composants UI
|
||||
|
||||
Voir `docs/migration-status.md` pour la liste complète des composants restants à migrer vers les composants UI réutilisables.
|
||||
Voir `docs/migration-status.md` pour la liste complète des composants migrés et restants.
|
||||
|
||||
**Priorité haute** :
|
||||
- `SeriesCard.tsx` - Container principal
|
||||
- `createSeriesModal/CreateSeriesModalView.tsx` - Container de modal
|
||||
- `AuthorFilterButton.tsx` - Bouton principal
|
||||
- `AuthorFilterDropdown.tsx` - Boutons d'option
|
||||
- `CategoryTabs.tsx` - Boutons d'onglets
|
||||
**État actuel** : ✅ La plupart des composants principaux ont été migrés vers les composants UI réutilisables.
|
||||
|
||||
### Composants restants (priorité moyenne)
|
||||
|
||||
- **Uploads de fichiers** - Les labels d'upload utilisent encore des `span` avec styles inline car ils nécessitent une structure HTML spécifique (`<label>` + `<input type="file">` caché) :
|
||||
- `ImageUploadField.tsx` - Déjà partiellement migré avec Button pour RemoveButton
|
||||
- `MarkdownEditor.tsx` - Label d'upload de média
|
||||
- `markdownEditorTwoColumns/MarkdownEditorTwoColumns.tsx` - ToolbarUploadButton
|
||||
- `markdownEditorTwoColumns/PagesManager.tsx` - PageImageUploadButton
|
||||
|
||||
**Note** : Ces éléments nécessitent une structure HTML spécifique et ne peuvent pas être directement migrés vers `Button`. Une solution serait de créer un composant `FileUploadButton` spécialisé si nécessaire.
|
||||
|
||||
## Améliorations UX documentées
|
||||
|
||||
Voir `features/ux-improvements.md` pour la liste complète des améliorations UX proposées.
|
||||
|
||||
**Priorité haute** :
|
||||
1. Skeleton loaders
|
||||
2. Toast notifications
|
||||
3. Indicateur visuel pour contenu débloqué
|
||||
4. Raccourcis clavier de base (`/`, `Esc`)
|
||||
5. Amélioration de la modal de paiement
|
||||
### Priorité haute
|
||||
|
||||
**Priorité moyenne** :
|
||||
6. Recherche améliorée avec suggestions
|
||||
7. Filtres persistants
|
||||
8. Navigation clavier complète
|
||||
9. ARIA amélioré
|
||||
10. Messages d'erreur actionnables
|
||||
1. ✅ **Skeleton loaders** - Implémenté
|
||||
- Remplacé les messages "Loading..." par des skeleton loaders dans :
|
||||
- Liste d'articles (`ArticlesList.tsx`)
|
||||
- Cartes d'auteurs (`AuthorsList.tsx`)
|
||||
- Articles utilisateur (`UserArticlesList.tsx`)
|
||||
- Page auteur (`AuthorPageContent.tsx`)
|
||||
- Formulaire de présentation (`AuthorPresentationEditor.tsx`)
|
||||
- Contenu markdown (`DocsContent.tsx`)
|
||||
- Gauge de financement (`FundingGauge.tsx`)
|
||||
- Animation subtile (pulse) pour indiquer le chargement
|
||||
|
||||
2. ✅ **Toast notifications** - Implémenté
|
||||
- Système de gestion des toasts créé (`ToastContainer.tsx` avec `ToastProvider` et `useToast` hook)
|
||||
- Toasts de succès intégrés :
|
||||
- Après déblocage réussi d'un article (`ArticleCard.tsx`)
|
||||
- Après publication réussie (`ArticleEditor.tsx`)
|
||||
- Après copie d'invoice (`PaymentModal.tsx`)
|
||||
- Après initiation du paiement avec Alby (`PaymentModal.tsx`)
|
||||
- Toasts avec variants (success, info, warning, error), durée configurable, fermeture automatique et manuelle
|
||||
- Accessibilité : `role="alert"` et `aria-live="polite"` pour les screen readers
|
||||
|
||||
3. **Indicateur visuel pour contenu débloqué**
|
||||
- Badge "Débloqué" sur les articles
|
||||
- Icône de cadenas ouvert
|
||||
- Couleur différente ou bordure distinctive
|
||||
|
||||
4. **Raccourcis clavier de base**
|
||||
- `/` : Focus automatique sur la barre de recherche
|
||||
- `Esc` : Fermer les modals et overlays
|
||||
- `Ctrl/Cmd + K` : Ouvrir une recherche rapide (command palette)
|
||||
|
||||
5. **Amélioration de la modal de paiement**
|
||||
- Bouton "Payer avec Alby" en priorité (plus grand et plus visible)
|
||||
- Auto-détection d'Alby
|
||||
- Instructions étape par étape
|
||||
- QR code amélioré (plus grand, meilleur contraste)
|
||||
- Compte à rebours plus visible
|
||||
|
||||
### Priorité moyenne
|
||||
|
||||
6. **Recherche améliorée avec suggestions**
|
||||
- Autocomplétion basée sur les titres d'articles
|
||||
- Suggestions d'auteurs
|
||||
- Historique de recherche récente
|
||||
|
||||
7. **Filtres persistants**
|
||||
- Sauvegarder les préférences de filtres dans IndexedDB
|
||||
- Restaurer les filtres au retour sur la page
|
||||
|
||||
8. **Navigation clavier complète**
|
||||
- Tab order logique
|
||||
- Navigation par flèches dans les listes
|
||||
- Skip links pour navigation rapide
|
||||
|
||||
9. **ARIA amélioré**
|
||||
- Labels ARIA pour tous les boutons iconiques
|
||||
- Régions ARIA (`role="navigation"`, `role="main"`, `role="search"`)
|
||||
- Annonces screen reader (`aria-live`)
|
||||
|
||||
10. **Messages d'erreur actionnables**
|
||||
- Messages clairs avec explication
|
||||
- Actions de récupération (bouton "Réessayer")
|
||||
- Suggestions de solutions
|
||||
|
||||
### Priorité basse
|
||||
|
||||
11. Mode lecture
|
||||
12. Partage et engagement
|
||||
13. Guide de première utilisation
|
||||
14. Prévisualisation avant publication
|
||||
15. Chargement progressif avancé
|
||||
|
||||
## Améliorations UI documentées
|
||||
|
||||
Voir `features/ui-improvements.md` pour la liste complète des améliorations UI proposées.
|
||||
|
||||
**Toutes les améliorations UI ont été implémentées** :
|
||||
- ✅ ui-1 : Création des composants UI réutilisables
|
||||
- ✅ ui-2 à ui-12 : Migration des composants existants
|
||||
**État actuel** : ✅ Les composants UI réutilisables ont été créés et la plupart des composants ont été migrés.
|
||||
|
||||
## Corrections de bugs
|
||||
### Corrections de cohérence restantes
|
||||
|
||||
Aucun bug critique identifié actuellement.
|
||||
- **Audit des couleurs** : Identifier tous les usages de couleurs non-thème et remplacer par les couleurs du design system (cyber/neon)
|
||||
- **Cohérence des états** : États hover, focus, disabled, actifs/selected uniformes
|
||||
- **Typographie** : Système de tailles et poids cohérents
|
||||
- **Espacement** : Utiliser l'échelle Tailwind de manière cohérente
|
||||
|
||||
### Améliorations visuelles
|
||||
|
||||
- **Background grid** : Utiliser `bg-cyber-grid` sur certains containers (optionnel)
|
||||
- **Gradients** : Gradients subtils pour certains backgrounds
|
||||
- **Shadows et glows** : Utiliser `shadow-glow-cyan` et `shadow-glow-green` de manière cohérente
|
||||
|
||||
## Infrastructure nécessaire
|
||||
|
||||
Voir `docs/remaining-tasks.md` pour les détails.
|
||||
|
||||
### Transferts Lightning automatiques
|
||||
|
||||
**État actuel** : Les transferts sont loggés dans `lib/automaticTransfer.ts` mais nécessitent un nœud Lightning pour être exécutés automatiquement.
|
||||
|
||||
**À implémenter** :
|
||||
- Configuration nœud Lightning de la plateforme
|
||||
- API pour créer et payer des invoices Lightning
|
||||
- Queue de transferts pour gestion asynchrone
|
||||
- Retry en cas d'échec
|
||||
- Monitoring et alertes
|
||||
|
||||
## Optimisations
|
||||
|
||||
- Performance : À évaluer après migration complète
|
||||
- Accessibilité : Vérification complète après migration
|
||||
- SEO : À évaluer si nécessaire
|
||||
- **Performance** : À évaluer après migration complète
|
||||
- **Accessibilité** : Vérification complète après migration (contraste, focus, ARIA)
|
||||
- **SEO** : À évaluer si nécessaire
|
||||
|
||||
## Tests
|
||||
|
||||
- Tests unitaires : À définir selon la stratégie du projet
|
||||
- Tests d'intégration : À définir selon la stratégie du projet
|
||||
- Tests manuels : À effectuer après chaque migration
|
||||
- **Tests unitaires** : À définir selon la stratégie du projet
|
||||
- **Tests d'intégration** : À définir selon la stratégie du projet
|
||||
- **Tests manuels** : À effectuer après chaque migration
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
141
features/skeleton-loaders-toast-notifications.md
Normal file
141
features/skeleton-loaders-toast-notifications.md
Normal file
@ -0,0 +1,141 @@
|
||||
# Skeleton Loaders et Toast Notifications
|
||||
|
||||
**Auteur** : Équipe 4NK
|
||||
**Date** : 2025-01-27
|
||||
|
||||
## Objectif
|
||||
|
||||
Implémenter les deux premières améliorations UX de priorité haute :
|
||||
1. Skeleton loaders - Remplacer les messages "Loading..." par des skeleton loaders
|
||||
2. Toast notifications - Intégrer le système de toast pour les confirmations visuelles
|
||||
|
||||
## Impacts
|
||||
|
||||
### Skeleton loaders
|
||||
|
||||
- **Expérience utilisateur** : Amélioration de la perception de performance en affichant la structure du contenu pendant le chargement
|
||||
- **Cohérence visuelle** : Remplacement des messages textuels par des éléments visuels cohérents avec le design system
|
||||
- **Feedback visuel** : Animation subtile pour indiquer le chargement actif
|
||||
|
||||
### Toast notifications
|
||||
|
||||
- **Feedback utilisateur** : Confirmations visuelles immédiates après les actions importantes
|
||||
- **Accessibilité** : Toasts avec `role="alert"` et `aria-live="polite"` pour les screen readers
|
||||
- **Expérience utilisateur** : Retour visuel clair et non intrusif pour les actions réussies
|
||||
|
||||
## Modifications
|
||||
|
||||
### Skeleton loaders
|
||||
|
||||
1. **`components/ArticlesList.tsx`** :
|
||||
- Ajout de `ArticleCardSkeleton` qui reproduit la structure d'une carte d'article
|
||||
- Remplacement de `LoadingState` par un affichage de 3 skeletons
|
||||
|
||||
2. **`components/AuthorsList.tsx`** :
|
||||
- Ajout de `AuthorCardSkeleton` qui reproduit la structure d'une carte d'auteur
|
||||
- Remplacement de `LoadingState` par un affichage de 4 skeletons en grille
|
||||
|
||||
3. **`components/UserArticlesList.tsx`** :
|
||||
- Ajout de `ArticleCardSkeleton` (identique à ArticlesList)
|
||||
- Remplacement de `ArticlesLoading` par un affichage de 3 skeletons
|
||||
- Ajout de l'import `Skeleton`
|
||||
|
||||
4. **`components/authorPage/AuthorPageContent.tsx`** :
|
||||
- Extraction de `AuthorPageLoadingSkeleton`, `AuthorPageError`, `AuthorPageNotFound` pour respecter `max-lines-per-function`
|
||||
- Remplacement du message de chargement par des skeletons représentant la structure de la page auteur
|
||||
|
||||
5. **`components/authorPresentationEditor/AuthorPresentationEditor.tsx`** :
|
||||
- Remplacement de `LoadingNotice` par des skeletons représentant la structure du formulaire
|
||||
- Ajout de l'import `Skeleton`
|
||||
|
||||
6. **`components/DocsContent.tsx`** :
|
||||
- Remplacement du message de chargement par des skeletons représentant la structure du contenu markdown
|
||||
- Suppression de l'import `t` non utilisé
|
||||
|
||||
7. **`components/FundingGauge.tsx`** :
|
||||
- Remplacement de `FundingGaugeLoading` par des skeletons représentant la structure du gauge
|
||||
- Ajout de l'import `Skeleton`
|
||||
|
||||
### Toast notifications
|
||||
|
||||
1. **`components/ui/ToastContainer.tsx`** (nouveau) :
|
||||
- Création du système de gestion des toasts avec `ToastProvider` et `useToast` hook
|
||||
- Gestion d'une pile de toasts avec suppression automatique après durée configurable
|
||||
- Affichage des toasts en position fixe (top-right) avec z-index élevé
|
||||
|
||||
2. **`pages/_app.tsx`** :
|
||||
- Intégration du `ToastProvider` dans l'application pour rendre les toasts disponibles globalement
|
||||
|
||||
3. **`components/ArticleCard.tsx`** :
|
||||
- Ajout d'un toast de succès après déblocage réussi d'un article
|
||||
- Extraction de `useArticleCardState` pour respecter `max-lines-per-function`
|
||||
- Extraction de `ArticleCardContent` pour réduire le nombre de lignes de `ArticleCard`
|
||||
|
||||
4. **`components/ArticleEditor.tsx`** :
|
||||
- Ajout d'un toast de succès après publication réussie via `useEffect`
|
||||
- Extraction de `usePublishSuccessToast` pour respecter `max-lines-per-function`
|
||||
|
||||
5. **`components/PaymentModal.tsx`** :
|
||||
- Ajout d'un toast de succès après copie d'invoice dans `copyInvoiceToClipboard`
|
||||
- Ajout d'un toast de succès après initiation du paiement dans `openWalletForInvoice`
|
||||
- Mise à jour de `usePaymentModalState` pour accepter `showToast` optionnel
|
||||
|
||||
6. **`hooks/useArticlePayment.ts`** :
|
||||
- Refactoring pour accepter un objet de paramètres au lieu de paramètres individuels (respect de `max-params`)
|
||||
- Ajout du support `showToast` optionnel pour afficher des toasts après paiement réussi
|
||||
- Propagation de `showToast` à travers `checkPaymentAndUnlock` et `startArticlePaymentFlow`
|
||||
|
||||
7. **`locales/fr.txt` et `locales/en.txt`** :
|
||||
- Ajout des clés de traduction pour les toasts :
|
||||
- `article.unlock.success` : "Article débloqué avec succès!" / "Article unlocked successfully!"
|
||||
- `article.publish.success` : "Article publié avec succès!" / "Article published successfully!"
|
||||
- `payment.modal.copySuccess` : "Facture copiée dans le presse-papiers" / "Invoice copied to clipboard"
|
||||
- `payment.modal.paymentInitiated` : "Paiement initié avec succès" / "Payment initiated successfully"
|
||||
- Ajout des clés de traduction manquantes pour `payment.modal.*` déjà utilisées dans le code
|
||||
|
||||
## Modalités de déploiement
|
||||
|
||||
1. **Vérification** :
|
||||
- `npm run type-check` : Aucune erreur TypeScript
|
||||
- `npm run lint` : Aucune erreur de linting (4 warnings acceptables sur l'utilisation de l'index dans les keys pour les skeletons)
|
||||
|
||||
2. **Déploiement** :
|
||||
- Utiliser les scripts de déploiement existants (`deploy.sh`, `update-from-git.sh`, `update-remote-git.sh`)
|
||||
- Validation explicite requise avant déploiement
|
||||
|
||||
## Modalités d'analyse
|
||||
|
||||
1. **Skeleton loaders** :
|
||||
- Vérifier visuellement que les skeletons apparaissent pendant le chargement des listes
|
||||
- Vérifier que les animations de pulse sont fluides
|
||||
- Vérifier que les skeletons disparaissent correctement une fois les données chargées
|
||||
|
||||
2. **Toast notifications** :
|
||||
- Vérifier que les toasts apparaissent après :
|
||||
- Déblocage réussi d'un article
|
||||
- Publication réussie d'un article
|
||||
- Copie d'invoice dans le presse-papiers
|
||||
- Initiation du paiement avec Alby
|
||||
- Vérifier que les toasts disparaissent automatiquement après 5 secondes (2 secondes pour la copie)
|
||||
- Vérifier que les toasts peuvent être fermés manuellement via le bouton ×
|
||||
- Vérifier l'accessibilité avec un screen reader (toasts annoncés via `aria-live="polite"`)
|
||||
|
||||
## Pages affectées
|
||||
|
||||
- `components/ArticlesList.tsx`
|
||||
- `components/AuthorsList.tsx`
|
||||
- `components/UserArticlesList.tsx`
|
||||
- `components/authorPage/AuthorPageContent.tsx`
|
||||
- `components/authorPresentationEditor/AuthorPresentationEditor.tsx`
|
||||
- `components/DocsContent.tsx`
|
||||
- `components/FundingGauge.tsx`
|
||||
- `components/ui/ToastContainer.tsx` (nouveau)
|
||||
- `components/ArticleCard.tsx`
|
||||
- `components/ArticleEditor.tsx`
|
||||
- `components/PaymentModal.tsx`
|
||||
- `hooks/useArticlePayment.ts`
|
||||
- `pages/_app.tsx`
|
||||
- `locales/fr.txt`
|
||||
- `locales/en.txt`
|
||||
- `components/ui/index.ts`
|
||||
- `docs/migration-status.md`
|
||||
@ -13,20 +13,23 @@ type UseArticlePaymentResult = {
|
||||
handleCloseModal: () => void
|
||||
}
|
||||
|
||||
export function useArticlePayment(
|
||||
article: Article,
|
||||
pubkey: string | null,
|
||||
onUnlockSuccess?: () => void,
|
||||
connect?: () => Promise<void>
|
||||
): UseArticlePaymentResult {
|
||||
interface UseArticlePaymentParams {
|
||||
article: Article
|
||||
pubkey: string | null
|
||||
onUnlockSuccess?: (() => void) | undefined
|
||||
connect?: (() => Promise<void>) | undefined
|
||||
showToast?: ((message: string, variant?: 'success' | 'info' | 'warning' | 'error', duration?: number) => void) | undefined
|
||||
}
|
||||
|
||||
export function useArticlePayment(params: UseArticlePaymentParams): UseArticlePaymentResult {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [paymentInvoice, setPaymentInvoice] = useState<AlbyInvoice | null>(null)
|
||||
const [paymentHash, setPaymentHash] = useState<string | null>(null)
|
||||
|
||||
const handleUnlock = (): Promise<void> => unlockArticlePayment({ article, pubkey, connect, onUnlockSuccess, setLoading, setError, setPaymentInvoice, setPaymentHash })
|
||||
const handleUnlock = (): Promise<void> => unlockArticlePayment({ article: params.article, pubkey: params.pubkey, connect: params.connect, onUnlockSuccess: params.onUnlockSuccess, setLoading, setError, setPaymentInvoice, setPaymentHash, showToast: params.showToast })
|
||||
|
||||
const handlePaymentComplete = (): Promise<void> => checkPaymentAndUnlock({ article, pubkey, paymentHash, onUnlockSuccess, setError, setPaymentInvoice, setPaymentHash })
|
||||
const handlePaymentComplete = (): Promise<void> => checkPaymentAndUnlock({ article: params.article, pubkey: params.pubkey, paymentHash, onUnlockSuccess: params.onUnlockSuccess, setError, setPaymentInvoice, setPaymentHash, showToast: params.showToast })
|
||||
|
||||
const handleCloseModal = (): void => resetPaymentModalState({ setPaymentInvoice, setPaymentHash })
|
||||
|
||||
@ -42,6 +45,7 @@ async function unlockArticlePayment(params: {
|
||||
setError: (value: string | null) => void
|
||||
setPaymentInvoice: (value: AlbyInvoice | null) => void
|
||||
setPaymentHash: (value: string | null) => void
|
||||
showToast: ((message: string, variant?: 'success' | 'info' | 'warning' | 'error', duration?: number) => void) | undefined
|
||||
}): Promise<void> {
|
||||
const {pubkey} = params
|
||||
if (!pubkey) {
|
||||
@ -61,6 +65,7 @@ async function unlockArticlePayment(params: {
|
||||
setError: params.setError,
|
||||
setPaymentInvoice: params.setPaymentInvoice,
|
||||
setPaymentHash: params.setPaymentHash,
|
||||
showToast: params.showToast,
|
||||
})
|
||||
}
|
||||
|
||||
@ -79,6 +84,7 @@ async function startArticlePaymentFlow(params: {
|
||||
setError: (value: string | null) => void
|
||||
setPaymentInvoice: (value: AlbyInvoice | null) => void
|
||||
setPaymentHash: (value: string | null) => void
|
||||
showToast: ((message: string, variant?: 'success' | 'info' | 'warning' | 'error', duration?: number) => void) | undefined
|
||||
}): Promise<void> {
|
||||
params.setLoading(true)
|
||||
params.setError(null)
|
||||
@ -91,7 +97,7 @@ async function startArticlePaymentFlow(params: {
|
||||
}
|
||||
params.setPaymentInvoice(ok.invoice)
|
||||
params.setPaymentHash(ok.paymentHash)
|
||||
void checkPaymentAndUnlock({ article: params.article, pubkey: params.pubkey, paymentHash: ok.paymentHash, onUnlockSuccess: params.onUnlockSuccess, setError: params.setError, setPaymentInvoice: params.setPaymentInvoice, setPaymentHash: params.setPaymentHash })
|
||||
void checkPaymentAndUnlock({ article: params.article, pubkey: params.pubkey, paymentHash: ok.paymentHash, onUnlockSuccess: params.onUnlockSuccess, setError: params.setError, setPaymentInvoice: params.setPaymentInvoice, setPaymentHash: params.setPaymentHash, showToast: params.showToast })
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Failed to process payment'
|
||||
console.error('Payment processing error:', e)
|
||||
@ -126,6 +132,7 @@ async function checkPaymentAndUnlock(params: {
|
||||
setError: (value: string | null) => void
|
||||
setPaymentInvoice: (value: AlbyInvoice | null) => void
|
||||
setPaymentHash: (value: string | null) => void
|
||||
showToast: ((message: string, variant?: 'success' | 'info' | 'warning' | 'error', duration?: number) => void) | undefined
|
||||
}): Promise<void> {
|
||||
if (!params.paymentHash || !params.pubkey) {
|
||||
return
|
||||
@ -148,6 +155,9 @@ async function checkPaymentAndUnlock(params: {
|
||||
return
|
||||
}
|
||||
resetPaymentModalState({ setPaymentInvoice: params.setPaymentInvoice, setPaymentHash: params.setPaymentHash })
|
||||
if (params.showToast !== undefined) {
|
||||
params.showToast('Article débloqué avec succès!', 'success')
|
||||
}
|
||||
params.onUnlockSuccess?.()
|
||||
} catch (e) {
|
||||
console.error('Payment check error:', e)
|
||||
|
||||
@ -166,6 +166,24 @@ import.button=Import
|
||||
|
||||
# Payment
|
||||
payment.expired=Expired
|
||||
payment.modal.zapAmount=Pay {{amount}} sats
|
||||
payment.modal.timeRemaining=Time remaining: {{time}}
|
||||
payment.modal.lightningInvoice=Lightning Invoice
|
||||
payment.modal.scanQr=Scan the QR code with your Lightning wallet
|
||||
payment.modal.copyInvoice=Copy invoice
|
||||
payment.modal.copied=Copied!
|
||||
payment.modal.copySuccess=Invoice copied to clipboard
|
||||
payment.modal.copyFailed=Failed to copy invoice
|
||||
payment.modal.payWithAlby=Pay with Alby
|
||||
payment.modal.paymentInitiated=Payment initiated successfully
|
||||
payment.modal.invoiceExpired=Invoice expired
|
||||
payment.modal.invoiceExpiredHelp=The Lightning invoice has expired. Please unlock the article again to get a new invoice.
|
||||
payment.modal.weblnNotAvailable=WebLN is not available. Please install Alby or use another Lightning extension.
|
||||
payment.modal.autoVerify=Payment verification will happen automatically.
|
||||
|
||||
# Article
|
||||
article.unlock.success=Article unlocked successfully!
|
||||
article.publish.success=Article published successfully!
|
||||
|
||||
# Article
|
||||
article.title=Title
|
||||
|
||||
@ -235,3 +235,24 @@ settings.language.english=Anglais
|
||||
account.create.title=Créer un compte
|
||||
account.create.description=Créez un nouveau compte Nostr ou importez une clé privée existante.
|
||||
account.import.title=Importer une clé privée
|
||||
|
||||
# Payment
|
||||
payment.expired=Expiré
|
||||
payment.modal.zapAmount=Payer {{amount}} sats
|
||||
payment.modal.timeRemaining=Temps restant : {{time}}
|
||||
payment.modal.lightningInvoice=Facture Lightning
|
||||
payment.modal.scanQr=Scannez le QR code avec votre portefeuille Lightning
|
||||
payment.modal.copyInvoice=Copier la facture
|
||||
payment.modal.copied=Copié !
|
||||
payment.modal.copySuccess=Facture copiée dans le presse-papiers
|
||||
payment.modal.copyFailed=Impossible de copier la facture
|
||||
payment.modal.payWithAlby=Payer avec Alby
|
||||
payment.modal.paymentInitiated=Paiement initié avec succès
|
||||
payment.modal.invoiceExpired=Facture expirée
|
||||
payment.modal.invoiceExpiredHelp=La facture Lightning a expiré. Veuillez débloquer l'article à nouveau pour obtenir une nouvelle facture.
|
||||
payment.modal.weblnNotAvailable=WebLN n'est pas disponible. Veuillez installer Alby ou utiliser une autre extension Lightning.
|
||||
payment.modal.autoVerify=La vérification du paiement se fera automatiquement.
|
||||
|
||||
# Article
|
||||
article.unlock.success=Article débloqué avec succès !
|
||||
article.publish.success=Article publié avec succès !
|
||||
|
||||
@ -8,6 +8,7 @@ import { relaySessionManager } from '@/lib/relaySessionManager'
|
||||
import { publishWorker } from '@/lib/publishWorker'
|
||||
import { swSyncHandler } from '@/lib/swSyncHandler'
|
||||
import { swClient } from '@/lib/swClient'
|
||||
import { ToastProvider } from '@/components/ui/ToastContainer'
|
||||
|
||||
function I18nProvider({ children }: { children: React.ReactNode }): React.ReactElement {
|
||||
const [initialLocale, setInitialLocale] = React.useState<'fr' | 'en'>('fr')
|
||||
@ -60,7 +61,9 @@ export default function App({ Component, pageProps }: AppProps): React.ReactElem
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<Component {...pageProps} />
|
||||
<ToastProvider>
|
||||
<Component {...pageProps} />
|
||||
</ToastProvider>
|
||||
</I18nProvider>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user