create for series

This commit is contained in:
Nicolas Cantu 2026-01-14 11:05:27 +01:00
parent 82dfbad5cb
commit 1082f33a77
30 changed files with 551 additions and 249 deletions

View File

@ -1,6 +1,6 @@
import { useEffect, useState, useCallback } from 'react'
import { Button, Card } from './ui'
import { getAlbyService } from '@/lib/alby'
import { Button } from './ui'
interface AlbyInstallerProps {
onInstalled?: () => void
@ -119,7 +119,7 @@ export function AlbyInstaller({ onInstalled }: AlbyInstallerProps): React.ReactE
}
return (
<div className="bg-cyber-dark/50 border border-neon-cyan/30 rounded-lg p-4 mb-4">
<Card variant="default" className="bg-cyber-dark/50 mb-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<InfoIcon />
@ -129,6 +129,6 @@ export function AlbyInstaller({ onInstalled }: AlbyInstallerProps): React.ReactE
{...(onInstalled ? { onInstalled } : {})}
/>
</div>
</div>
</Card>
)
}

View File

@ -1,6 +1,6 @@
import React from 'react'
import type { Article } from '@/types/nostr'
import { Card } from './ui'
import { Button, Card, Input } from './ui'
import { t } from '@/lib/i18n'
import { AuthorFilter } from './AuthorFilter'
@ -69,9 +69,9 @@ function FiltersHeader({
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-neon-cyan">{t('filters.sort')}</h3>
{hasActiveFilters && (
<button onClick={onClear} className="text-sm text-neon-cyan hover:text-neon-green font-medium transition-colors">
<Button onClick={onClear} variant="ghost" size="small" className="text-sm">
{t('filters.clear')}
</button>
</Button>
)}
</div>
)
@ -94,7 +94,7 @@ function SortFilter({
id="sort"
value={value}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => onChange(e.target.value as SortOption)}
className="block w-full px-3 py-2 border border-neon-cyan/30 rounded-lg focus:ring-2 focus:ring-neon-cyan focus:border-neon-cyan bg-cyber-dark text-cyber-accent hover:border-neon-cyan/50 transition-colors"
className="block w-full border border-neon-cyan/30 rounded-lg focus:ring-2 focus:ring-neon-cyan focus:border-neon-cyan bg-cyber-dark text-cyber-accent hover:border-neon-cyan/50 transition-colors"
>
<option value="newest" className="bg-cyber-dark">{t('filters.sort.newest')}</option>
<option value="oldest" className="bg-cyber-dark">{t('filters.sort.oldest')}</option>

View File

@ -1,4 +1,5 @@
import Image from 'next/image'
import { Card } from './ui'
import { t } from '@/lib/i18n'
export function AuthorAvatar({ picture, displayName }: { picture?: string; displayName: string }): React.ReactElement {
@ -214,9 +215,10 @@ export function AuthorDropdown({
}): React.ReactElement {
return (
<div
className="absolute z-20 w-full mt-1 bg-cyber-dark border border-neon-cyan/30 rounded-lg shadow-glow-cyan max-h-60 overflow-auto"
className="absolute z-20 w-full mt-1"
role="listbox"
>
<Card variant="default" className="bg-cyber-dark shadow-glow-cyan max-h-60 overflow-auto">
<AllAuthorsOption value={value} onChange={onChange} setIsOpen={setIsOpen} />
<AuthorDropdownContent
authors={authors}
@ -228,6 +230,7 @@ export function AuthorDropdown({
onChange={onChange}
setIsOpen={setIsOpen}
/>
</Card>
</div>
)
}

View File

@ -31,17 +31,17 @@ async function updateCache(): Promise<void> {
function SuccessMessage(): React.ReactElement {
return (
<div className="bg-green-900/20 border border-green-400/50 rounded-lg p-4 mb-4">
<Card variant="default" className="bg-green-900/20 border-green-400/50 mb-4">
<p className="text-green-400">Cache mis à jour avec succès</p>
</div>
</Card>
)
}
function NotConnectedMessage(): React.ReactElement {
return (
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4 mb-4">
<Card variant="default" className="bg-yellow-900/20 border-yellow-400/50 mb-4">
<p className="text-yellow-400">Vous devez être connecté pour mettre à jour le cache</p>
</div>
</Card>
)
}

View File

@ -1,3 +1,4 @@
import { Button } from './ui'
import { t } from '@/lib/i18n'
type CategoryFilter = 'science-fiction' | 'scientific-research' | 'all' | null
@ -12,26 +13,30 @@ export function CategoryTabs({ selectedCategory, onCategoryChange }: CategoryTab
<div className="mb-6">
<div className="border-b border-neon-cyan/30">
<nav className="-mb-px flex space-x-8">
<button
<Button
type="button"
variant="ghost"
onClick={() => onCategoryChange('science-fiction')}
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors ${
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors rounded-none ${
selectedCategory === 'science-fiction'
? 'border-neon-cyan text-neon-cyan'
: 'border-transparent text-cyber-accent/70 hover:text-neon-cyan hover:border-neon-cyan/50'
}`}
>
{t('category.science-fiction')}
</button>
<button
</Button>
<Button
type="button"
variant="ghost"
onClick={() => onCategoryChange('scientific-research')}
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors ${
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors rounded-none ${
selectedCategory === 'scientific-research'
? 'border-neon-cyan text-neon-cyan'
: 'border-transparent text-cyber-accent/70 hover:text-neon-cyan hover:border-neon-cyan/50'
}`}
>
{t('category.scientific-research')}
</button>
</Button>
</nav>
</div>
</div>

View File

@ -1,9 +1,10 @@
import Link from 'next/link'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useNostrAuth } from '@/hooks/useNostrAuth'
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
import { useEffect, useState } from 'react'
import { Button } from './ui'
import { Button, Card } from './ui'
import { t } from '@/lib/i18n'
import type { Article } from '@/types/nostr'
@ -28,11 +29,14 @@ function AuthorProfileLink({ presentation, profile }: { presentation: Article; p
// Extract picture from presentation (bannerUrl or from JSON metadata) or profile
const picture = presentation.bannerUrl ?? profile?.picture
const router = useRouter()
const handleClick = (): void => {
void router.push(`/author/${presentation.pubkey}`)
}
return (
<Link
href={`/author/${presentation.pubkey}`}
className="flex items-center gap-2 px-3 py-2 bg-cyber-dark/50 hover:bg-cyber-dark border border-neon-cyan/20 hover:border-neon-cyan/40 rounded-lg transition-all"
>
<Card variant="interactive" onClick={handleClick} className="flex items-center gap-2 px-3 py-2 bg-cyber-dark/50">
{picture ? (
<div className="relative w-8 h-8 rounded-full overflow-hidden border border-neon-cyan/30 flex-shrink-0">
<Image
@ -49,7 +53,7 @@ function AuthorProfileLink({ presentation, profile }: { presentation: Article; p
</div>
)}
<span className="text-sm text-neon-cyan font-medium truncate max-w-[120px]">{authorName}</span>
</Link>
</Card>
)
}

View File

@ -1,13 +1,14 @@
import { Button, Card, ErrorState, Textarea } from '@/components/ui'
import { t } from '@/lib/i18n'
export function RecoveryWarning(): React.ReactElement {
return (
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4 mb-6">
<Card variant="default" className="bg-yellow-900/20 border-yellow-400/50 mb-6">
<p className="text-yellow-400 font-semibold mb-2">{t('account.create.recovery.warning.title')}</p>
<p className="text-yellow-300/90 text-sm" dangerouslySetInnerHTML={{ __html: t('account.create.recovery.warning.part1') }} />
<p className="text-yellow-300/90 text-sm mt-2" dangerouslySetInnerHTML={{ __html: t('account.create.recovery.warning.part2') }} />
<p className="text-yellow-300/90 text-sm mt-2">{t('account.create.recovery.warning.part3')}</p>
</div>
</Card>
)
}
@ -22,27 +23,30 @@ export function RecoveryPhraseDisplay({
}): React.ReactElement {
const recoveryItems = buildRecoveryPhraseItems(recoveryPhrase)
return (
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6 mb-6">
<Card variant="default" className="bg-cyber-darker mb-6">
<div className="grid grid-cols-2 gap-4 mb-4">
{recoveryItems.map((item, index) => (
<div
<Card
key={item.key}
className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-3 text-center font-mono text-lg"
variant="default"
className="bg-cyber-dark p-3 text-center font-mono text-lg"
>
<span className="text-cyber-accent/70 text-sm mr-2">{index + 1}.</span>
<span className="font-semibold text-neon-cyan">{item.word}</span>
</div>
</Card>
))}
</div>
<button
<Button
onClick={() => {
void onCopy()
}}
className="w-full py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg text-sm font-medium transition-colors"
variant="secondary"
size="small"
className="w-full"
>
{copied ? t('account.create.recovery.copied') : t('account.create.recovery.copy')}
</button>
</div>
</Button>
</Card>
)
}
@ -57,10 +61,10 @@ function buildRecoveryPhraseItems(recoveryPhrase: string[]): { key: string; word
export function PublicKeyDisplay({ npub }: { npub: string }): React.ReactElement {
return (
<div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4 mb-6">
<Card variant="default" className="bg-neon-blue/10 border-neon-blue/30 mb-6">
<p className="text-neon-blue font-semibold mb-2">{t('account.create.publicKey')}</p>
<p className="text-neon-cyan text-sm font-mono break-all">{npub}</p>
</div>
</Card>
)
}
@ -75,21 +79,17 @@ export function ImportKeyForm({
}): React.ReactElement {
return (
<>
<div className="mb-4">
<label htmlFor="importKey" className="block text-sm font-medium text-cyber-accent mb-2">
{t('account.create.importKey.label')}
</label>
<textarea
<Textarea
id="importKey"
label={t('account.create.importKey.label')}
value={importKey}
onChange={(e) => setImportKey(e.target.value)}
placeholder={t('account.create.importKey.placeholder')}
className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/30 rounded-lg font-mono text-sm text-neon-cyan"
className="font-mono text-sm text-neon-cyan bg-cyber-darker"
rows={4}
helperText={t('account.create.importKey.help')}
/>
<p className="text-sm text-cyber-accent/70 mt-2" dangerouslySetInnerHTML={{ __html: t('account.create.importKey.help') }} />
</div>
{error && <p className="text-sm text-red-400 mb-4">{error}</p>}
{error && <ErrorState message={error} className="mb-4" />}
</>
)
}
@ -97,25 +97,71 @@ export function ImportKeyForm({
export function ImportStepButtons({ loading, onImport, onBack }: { loading: boolean; onImport: () => void; onBack: () => void }): React.ReactElement {
return (
<div className="flex gap-4">
<button
<Button
onClick={onBack}
className="flex-1 py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg font-medium transition-colors"
variant="secondary"
className="flex-1"
>
{t('account.create.back')}
</button>
<button
</Button>
<Button
onClick={() => {
void onImport()
}}
disabled={loading}
className="flex-1 py-2 px-4 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan disabled:opacity-50"
loading={loading}
variant="primary"
className="flex-1"
>
{loading ? t('import.loading') : t('import.button')}
</button>
</Button>
</div>
)
}
function GenerateButton({ loading, onGenerate }: { loading: boolean; onGenerate: () => void }): React.ReactElement {
return (
<Button
onClick={() => {
void onGenerate()
}}
disabled={loading}
loading={loading}
variant="primary"
size="large"
className="w-full"
>
{loading ? t('account.create.importing') : t('account.create.generateButton')}
</Button>
)
}
function ImportButton({ loading, onImport }: { loading: boolean; onImport: () => void }): React.ReactElement {
return (
<Button
onClick={onImport}
disabled={loading}
variant="secondary"
size="large"
className="w-full"
>
{t('account.create.importButton')}
</Button>
)
}
function CancelButton({ onClose }: { onClose: () => void }): React.ReactElement {
return (
<Button
onClick={onClose}
variant="ghost"
className="w-full"
>
{t('account.create.cancel')}
</Button>
)
}
export function ChooseStepButtons({
loading,
onGenerate,
@ -129,28 +175,9 @@ export function ChooseStepButtons({
}): React.ReactElement {
return (
<div className="flex flex-col gap-4">
<button
onClick={() => {
void onGenerate()
}}
disabled={loading}
className="w-full py-3 px-6 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan disabled:opacity-50"
>
{loading ? t('account.create.importing') : t('account.create.generateButton')}
</button>
<button
onClick={onImport}
disabled={loading}
className="w-full py-3 px-6 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg font-medium transition-colors disabled:opacity-50"
>
{t('account.create.importButton')}
</button>
<button
onClick={onClose}
className="w-full py-2 px-4 text-cyber-accent/70 hover:text-neon-cyan font-medium transition-colors"
>
{t('account.create.cancel')}
</button>
<GenerateButton loading={loading} onGenerate={onGenerate} />
<ImportButton loading={loading} onImport={onImport} />
<CancelButton onClose={onClose} />
</div>
)
}

View File

@ -1,4 +1,5 @@
import { useState } from 'react'
import { Button, Card } from './ui'
import { RecoveryWarning, RecoveryPhraseDisplay, PublicKeyDisplay, ImportKeyForm, ImportStepButtons, ChooseStepButtons } from './CreateAccountModalComponents'
import { t } from '@/lib/i18n'
@ -23,20 +24,22 @@ export function RecoveryStep({
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto shadow-glow-cyan">
<Card variant="default" className="bg-cyber-dark max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto shadow-glow-cyan">
<h2 className="text-2xl font-bold mb-4 text-neon-cyan">{t('account.create.recovery.title')}</h2>
<RecoveryWarning />
<RecoveryPhraseDisplay recoveryPhrase={recoveryPhrase} copied={copied} onCopy={() => { void handleCopy() }} />
<PublicKeyDisplay npub={npub} />
<div className="flex gap-4">
<button
<Button
onClick={onContinue}
className="flex-1 py-3 px-6 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan"
variant="primary"
size="large"
className="flex-1"
>
{t('account.create.recovery.saved')}
</button>
</div>
</Button>
</div>
</Card>
</div>
)
}
@ -58,11 +61,11 @@ export function ImportStep({
}): React.ReactElement {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-md w-full mx-4 shadow-glow-cyan">
<Card variant="default" className="bg-cyber-dark max-w-md w-full mx-4 shadow-glow-cyan">
<h2 className="text-2xl font-bold mb-4 text-neon-cyan">{t('account.import.title')}</h2>
<ImportKeyForm importKey={importKey} setImportKey={setImportKey} error={error} />
<ImportStepButtons loading={loading} onImport={onImport} onBack={onBack} />
</div>
</Card>
</div>
)
}
@ -82,14 +85,14 @@ export function ChooseStep({
}): React.ReactElement {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-md w-full mx-4 shadow-glow-cyan">
<Card variant="default" className="bg-cyber-dark max-w-md w-full mx-4 shadow-glow-cyan">
<h2 className="text-2xl font-bold mb-4 text-neon-cyan">{t('account.create.title')}</h2>
<p className="text-cyber-accent/70 mb-6">
{t('account.create.description')}
</p>
{error && <p className="text-sm text-red-400 mb-4">{error}</p>}
<ChooseStepButtons loading={loading} onGenerate={onGenerate} onImport={onImport} onClose={onClose} />
</div>
</Card>
</div>
)
}

View File

@ -1,3 +1,4 @@
import { Card } from './ui'
import { renderMarkdown } from '@/lib/markdownRenderer'
import { t } from '@/lib/i18n'
@ -16,10 +17,10 @@ export function DocsContent({ content, loading }: DocsContentProps): React.React
}
return (
<div className="bg-cyber-dark border border-neon-cyan/20 rounded-lg p-8 backdrop-blur-sm">
<Card variant="default" className="bg-cyber-dark backdrop-blur-sm">
<div className="prose prose-invert max-w-none prose-headings:text-neon-cyan prose-headings:font-mono prose-p:text-cyber-accent prose-a:text-neon-green prose-a:hover:text-neon-cyan prose-strong:text-neon-green prose-code:text-neon-cyan prose-code:bg-cyber-darker prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-pre:bg-cyber-darker prose-pre:border prose-pre:border-neon-cyan/20 prose-blockquote:border-neon-cyan/50 prose-blockquote:text-cyber-accent/70 prose-ul:text-cyber-accent prose-ol:text-cyber-accent prose-li:marker:text-neon-cyan">
{renderMarkdown(content)}
</div>
</div>
</Card>
)
}

View File

@ -1,3 +1,4 @@
import { Button, Card } from './ui'
import { t } from '@/lib/i18n'
import type { DocLink, DocSection } from '@/hooks/useDocs'
@ -10,24 +11,22 @@ interface DocsSidebarProps {
export function DocsSidebar({ docs, selectedDoc, onSelectDoc }: DocsSidebarProps): React.ReactElement {
return (
<aside className="lg:w-64 flex-shrink-0">
<div className="bg-cyber-dark border border-neon-cyan/20 rounded-lg p-4 sticky top-4 backdrop-blur-sm">
<Card variant="default" className="bg-cyber-dark sticky top-4 backdrop-blur-sm">
<h2 className="text-lg font-bold mb-4 text-neon-cyan font-mono">{t('docs.title')}</h2>
<nav className="space-y-2">
{docs.map((doc) => (
<button
<Button
key={doc.id}
type="button"
variant={selectedDoc === doc.id ? 'primary' : 'ghost'}
onClick={() => onSelectDoc(doc.id)}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-all ${
selectedDoc === doc.id
? 'bg-neon-cyan/20 text-neon-cyan font-medium border border-neon-cyan/50 shadow-glow-cyan'
: 'text-cyber-accent hover:bg-cyber-dark/50 hover:text-neon-cyan border border-transparent hover:border-neon-cyan/30'
}`}
className="w-full text-left justify-start"
>
{doc.title}
</button>
</Button>
))}
</nav>
</div>
</Card>
</aside>
)
}

View File

@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
import { estimatePlatformFunds } from '@/lib/fundingCalculation'
import { Card } from './ui'
import { t } from '@/lib/i18n'
interface FundingProgressBarProps {
@ -50,27 +51,27 @@ export function FundingGauge(): React.ReactElement {
return (
<div className="space-y-6">
<div className="bg-cyber-dark/50 border border-neon-cyan/20 rounded-lg p-6">
<Card variant="default" className="bg-cyber-dark/50">
<h2 className="text-xl font-semibold text-neon-cyan mb-4">
{t('home.funding.title')} - {t('home.funding.priority.ia')}
</h2>
<FundingStats stats={state.stats} />
</div>
<div className="bg-cyber-dark/50 border border-neon-cyan/20 rounded-lg p-6">
</Card>
<Card variant="default" className="bg-cyber-dark/50">
<h2 className="text-xl font-semibold text-neon-cyan mb-4">
{t('home.funding.certification.title')} - {t('home.funding.priority.ancrage')}
</h2>
<FundingStats stats={state.certificationStats} />
</div>
</Card>
</div>
)
}
function FundingGaugeLoading(): React.ReactElement {
return (
<div className="bg-cyber-dark/50 border border-neon-cyan/20 rounded-lg p-6 mb-8">
<Card variant="default" className="bg-cyber-dark/50 mb-8">
<p className="text-cyber-accent">{t('common.loading')}</p>
</div>
</Card>
)
}

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'
import { Button, Card } from './ui'
import { setLocale, getLocale, type Locale } from '@/lib/i18n'
import { t } from '@/lib/i18n'
import { localeStorage } from '@/lib/localeStorage'
@ -13,16 +14,13 @@ interface LocaleOptionProps {
function LocaleOption({ locale, label, currentLocale, onClick }: LocaleOptionProps): React.ReactElement {
const isActive = currentLocale === locale
return (
<button
<Button
type="button"
variant={isActive ? 'primary' : 'secondary'}
onClick={() => onClick(locale)}
className={`px-4 py-2 rounded-lg font-medium transition-colors border ${
isActive
? 'bg-neon-cyan/20 text-neon-cyan border-neon-cyan/50'
: 'bg-cyber-darker text-cyber-accent hover:text-neon-cyan border-neon-cyan/30 hover:border-neon-cyan/50'
}`}
>
{label}
</button>
</Button>
)
}
@ -48,20 +46,20 @@ function LanguageSettingsPanel(params: {
}): React.ReactElement {
if (params.loading) {
return (
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
<Card variant="default" className="bg-cyber-darker">
<div>{t('settings.language.loading')}</div>
</div>
</Card>
)
}
return (
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
<Card variant="default" className="bg-cyber-darker">
<h2 className="text-2xl font-bold text-neon-cyan mb-4">{t('settings.language.title')}</h2>
<p className="text-cyber-accent mb-4 text-sm">{t('settings.language.description')}</p>
<div className="flex items-center gap-3">
<LocaleOption locale="fr" label={t('settings.language.french')} currentLocale={params.currentLocale} onClick={params.onLocaleClick} />
<LocaleOption locale="en" label={t('settings.language.english')} currentLocale={params.currentLocale} onClick={params.onLocaleClick} />
</div>
</div>
</Card>
)
}

View File

@ -1,4 +1,5 @@
import { useState } from 'react'
import { Button } from './ui'
import type { MediaRef } from '@/types/nostr'
import { uploadNip95Media } from '@/lib/nip95'
import { t } from '@/lib/i18n'
@ -64,9 +65,9 @@ function MarkdownToolbar({
}): React.ReactElement {
return (
<div className="flex items-center gap-2">
<button type="button" className="px-3 py-1 text-sm rounded bg-gray-200" onClick={onTogglePreview}>
<Button type="button" variant="secondary" size="small" onClick={onTogglePreview} className="px-3 py-1 text-sm rounded bg-gray-200">
{preview ? t('upload.edit') : t('upload.preview')}
</button>
</Button>
<label className="px-3 py-1 text-sm rounded bg-blue-600 text-white cursor-pointer hover:bg-blue-700">
{t('markdown.upload.media')}
<input

View File

@ -3,7 +3,7 @@ import QRCode from 'react-qr-code'
import type { AlbyInvoice } from '@/types/alby'
import { getAlbyService, isWebLNAvailable } from '@/lib/alby'
import { AlbyInstaller } from './AlbyInstaller'
import { Modal, Button } from './ui'
import { Card, Modal, Button } from './ui'
import { t } from '@/lib/i18n'
interface PaymentModalProps {
@ -67,16 +67,18 @@ function InvoiceDisplay({ invoiceText, paymentUrl }: { invoiceText: string; paym
return (
<div className="mb-4">
<p className="text-sm text-cyber-accent mb-2">{t('payment.modal.lightningInvoice')}</p>
<div className="bg-cyber-darker border border-neon-cyan/20 p-3 rounded break-all text-sm font-mono mb-4 text-neon-cyan">{invoiceText}</div>
<Card variant="default" className="bg-cyber-darker border-neon-cyan/20 p-3 break-all text-sm font-mono mb-4 text-neon-cyan">
{invoiceText}
</Card>
<div className="flex justify-center mb-4">
<div className="bg-cyber-dark p-4 rounded-lg border-2 border-neon-cyan/30">
<Card variant="default" className="bg-cyber-dark p-4 border-2 border-neon-cyan/30">
<QRCode
value={paymentUrl}
size={200}
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
viewBox="0 0 256 256"
/>
</div>
</Card>
</div>
<p className="text-xs text-cyber-accent/70 text-center mb-2">{t('payment.modal.scanQr')}</p>
</div>
@ -119,10 +121,10 @@ function ExpiredNotice({ show }: { show: boolean }): React.ReactElement | null {
return null
}
return (
<div className="mt-4 p-3 bg-red-900/20 border border-red-400/50 rounded-lg">
<Card variant="default" className="mt-4 p-3 bg-red-900/20 border-red-400/50">
<p className="text-sm text-red-400 font-semibold mb-2">{t('payment.modal.invoiceExpired')}</p>
<p className="text-xs text-red-400/80">{t('payment.modal.invoiceExpiredHelp')}</p>
</div>
</Card>
)
}

View File

@ -1,6 +1,6 @@
import Image from 'next/image'
import Link from 'next/link'
import { Button } from './ui'
import { Button, Card } from './ui'
import type { Series } from '@/types/nostr'
import { t } from '@/lib/i18n'
@ -51,8 +51,8 @@ export function SeriesCard({ series, onSelect, selected }: SeriesCardProps): Rea
? 'border-neon-cyan ring-1 ring-neon-cyan/50 shadow-glow-cyan'
: 'border-neon-cyan/30 hover:border-neon-cyan/50 hover:shadow-glow-cyan'
return (
<div className={`border rounded-lg p-4 bg-cyber-dark transition-all ${cardClasses}`}>
<Card variant={selected ? 'interactive' : 'default'} className={`bg-cyber-dark transition-all ${cardClasses}`}>
<SeriesCardContent series={series} onSelect={onSelect} />
</div>
</Card>
)
}

View File

@ -126,9 +126,9 @@ function buildSeriesLink(
Ouvrir
</Link>
{onSelectSeries && (
<button type="button" className="underline" onClick={() => onSelectSeries(article.seriesId)}>
<Button type="button" variant="ghost" size="small" onClick={() => onSelectSeries(article.seriesId)} className="underline p-0 h-auto">
Filtrer
</button>
</Button>
)}
</div>
)

View File

@ -1,3 +1,4 @@
import { Card } from './ui'
import type { NostrProfile } from '@/types/nostr'
import { UserProfileHeader } from './UserProfileHeader'
@ -20,7 +21,7 @@ export function UserProfile({ profile, pubkey, articleCount }: UserProfileProps)
const displayName = profile.name ?? `${pubkey.slice(0, 16)}...`
return (
<div className="bg-white border border-gray-200 rounded-lg p-6 mb-6">
<Card variant="default" className="bg-white border-gray-200 mb-6">
<UserProfileHeader
displayName={displayName}
{...(profile.picture ? { picture: profile.picture } : {})}
@ -28,6 +29,6 @@ export function UserProfile({ profile, pubkey, articleCount }: UserProfileProps)
/>
{profile.about && <p className="text-gray-700 mt-2">{profile.about}</p>}
{articleCount !== undefined && <ProfileStats articleCount={articleCount} />}
</div>
</Card>
)
}

View File

@ -1,3 +1,4 @@
import { Card } from '../ui'
import { t } from '@/lib/i18n'
import type { AuthorPresentationArticle, Series } from '@/types/nostr'
import { AuthorPageHeader } from './AuthorPageHeader'
@ -33,9 +34,9 @@ export function AuthorPageContent({
if (!presentation) {
return (
<div className="bg-cyber-dark/50 border border-neon-cyan/20 rounded-lg p-6">
<Card variant="default" className="bg-cyber-dark/50">
<p className="text-cyber-accent">{t('author.notFound')}</p>
</div>
</Card>
)
}

View File

@ -1,6 +1,6 @@
import React from 'react'
import { ImageUploadField } from '../ImageUploadField'
import { Button, Input, Textarea, ErrorState } from '../ui'
import { Button, Card, ErrorState, Input, Textarea } from '../ui'
import { t } from '@/lib/i18n'
import type { SeriesDraft } from './createSeriesModalTypes'
import type { CreateSeriesModalController } from './useCreateSeriesModalController'
@ -8,11 +8,11 @@ import type { CreateSeriesModalController } from './useCreateSeriesModalControll
export function CreateSeriesModalView({ ctrl }: { ctrl: CreateSeriesModalController }): React.ReactElement {
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">
<Card variant="default" className="bg-cyber-dark max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<CreateSeriesModalHeader loading={ctrl.loading} onClose={ctrl.handleClose} />
{!ctrl.canPublish ? <NotAuthorWarning /> : null}
<CreateSeriesForm ctrl={ctrl} />
</div>
</Card>
</div>
)
}
@ -21,23 +21,26 @@ function CreateSeriesModalHeader({ loading, onClose }: { loading: boolean; onClo
return (
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-semibold text-neon-cyan">{t('series.create.title')}</h2>
<button
<Button
type="button"
variant="ghost"
size="small"
onClick={onClose}
disabled={loading}
className="text-cyber-accent hover:text-neon-cyan transition-colors disabled:opacity-50"
className="text-cyber-accent hover:text-neon-cyan p-0 min-w-0"
aria-label={t('common.close')}
>
</button>
</Button>
</div>
)
}
function NotAuthorWarning(): React.ReactElement {
return (
<div className="mb-4 p-4 bg-yellow-900/30 border border-yellow-500/50 rounded text-yellow-300">
<Card variant="default" className="mb-4 p-4 bg-yellow-900/30 border-yellow-500/50 text-yellow-300">
<p>{t('series.create.error.notAuthor')}</p>
</div>
</Card>
)
}

View File

@ -1,3 +1,4 @@
import { Button, Card, Textarea } from '@/components/ui'
import { t } from '@/lib/i18n'
import type { KeyManagementManagerActions } from './useKeyManagementManager'
import type { KeyManagementManagerState } from './keyManagementController'
@ -31,13 +32,13 @@ export function KeyManagementImportForm(params: {
function KeyManagementImportWarning(params: { accountExists: boolean }): React.ReactElement {
return (
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4">
<Card variant="default" className="bg-yellow-900/20 border-yellow-400/50">
<p className="text-yellow-400 font-semibold mb-2">{t('settings.keyManagement.import.warning.title')}</p>
<p className="text-yellow-300/90 text-sm" dangerouslySetInnerHTML={{ __html: t('settings.keyManagement.import.warning.description') }} />
{params.accountExists ? (
<p className="text-yellow-300/90 text-sm mt-2" dangerouslySetInnerHTML={{ __html: t('settings.keyManagement.import.warning.replace') }} />
) : null}
</div>
</Card>
)
}
@ -47,20 +48,18 @@ function KeyManagementImportTextarea(params: {
}): React.ReactElement {
return (
<div>
<label htmlFor="importKey" className="block text-sm font-medium text-cyber-accent mb-2">
{t('settings.keyManagement.import.label')}
</label>
<textarea
<Textarea
id="importKey"
label={t('settings.keyManagement.import.label')}
value={params.importKey}
onChange={(e) => {
params.onChangeImportKey(e.target.value)
}}
placeholder={t('settings.keyManagement.import.placeholder')}
className="w-full px-3 py-2 bg-cyber-dark border border-neon-cyan/30 rounded-lg font-mono text-sm text-neon-cyan focus:border-neon-cyan focus:outline-none"
className="font-mono text-sm text-neon-cyan"
rows={4}
helperText={t('settings.keyManagement.import.help')}
/>
<p className="text-sm text-cyber-accent/70 mt-2">{t('settings.keyManagement.import.help')}</p>
</div>
)
}
@ -75,27 +74,30 @@ function KeyManagementReplaceWarning(params: {
return null
}
return (
<div className="bg-red-900/20 border border-red-400/50 rounded-lg p-4">
<Card variant="default" className="bg-red-900/20 border-red-400/50">
<p className="text-red-400 font-semibold mb-2">{t('settings.keyManagement.replace.warning.title')}</p>
<p className="text-red-300/90 text-sm mb-4">{t('settings.keyManagement.replace.warning.description')}</p>
<div className="flex gap-4">
<button
<Button
type="button"
variant="secondary"
onClick={params.onCancel}
className="flex-1 py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg font-medium transition-colors"
className="flex-1"
>
{t('settings.keyManagement.replace.cancel')}
</button>
<button
</Button>
<Button
type="button"
variant="danger"
onClick={params.onConfirm}
disabled={params.importing}
className="flex-1 py-2 px-4 bg-red-600/20 hover:bg-red-600/30 text-red-400 rounded-lg font-medium transition-all border border-red-400/50 hover:shadow-glow-red disabled:opacity-50"
loading={params.importing}
className="flex-1"
>
{params.importing ? t('settings.keyManagement.replace.replacing') : t('settings.keyManagement.replace.confirm')}
</button>
</div>
</Button>
</div>
</Card>
)
}
@ -110,21 +112,24 @@ function KeyManagementImportFormActions(params: {
}
return (
<div className="flex gap-4">
<button
<Button
type="button"
variant="secondary"
onClick={params.onCancel}
className="flex-1 py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg font-medium transition-colors"
className="flex-1"
>
{t('settings.keyManagement.import.cancel')}
</button>
<button
</Button>
<Button
type="button"
variant="primary"
onClick={params.onImport}
disabled={params.importing}
className="flex-1 py-2 px-4 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan disabled:opacity-50"
loading={params.importing}
className="flex-1"
>
{params.importing ? t('settings.keyManagement.import.importing') : t('settings.keyManagement.import.import')}
</button>
</Button>
</div>
)
}

View File

@ -1,4 +1,4 @@
import { Button, ErrorState } from '@/components/ui'
import { Button, Card, ErrorState } from '@/components/ui'
import { t } from '@/lib/i18n'
import type { KeyManagementManagerActions } from './useKeyManagementManager'
import type { KeyManagementManagerState } from './keyManagementController'
@ -75,7 +75,7 @@ function KeyManagementKeyCard(params: {
onCopy: () => void
}): React.ReactElement {
return (
<div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4">
<Card variant="default" className="bg-neon-blue/10 border-neon-blue/30">
<div className="flex justify-between items-start mb-2">
<p className="text-neon-blue font-semibold">{params.label}</p>
<Button
@ -88,7 +88,7 @@ function KeyManagementKeyCard(params: {
</Button>
</div>
<p className="text-neon-cyan text-sm font-mono break-all">{params.value}</p>
</div>
</Card>
)
}

View File

@ -1,3 +1,4 @@
import { Card } from '../ui'
import { t } from '@/lib/i18n'
import { SyncProgressBar } from '../SyncProgressBar'
import { KeyManagementImportSection } from './KeyManagementImportSection'
@ -9,20 +10,20 @@ export function KeyManagementManager(): React.ReactElement {
if (state.loading) {
return (
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
<Card variant="default" className="bg-cyber-darker">
<p className="text-cyber-accent">{t('settings.keyManagement.loading')}</p>
</div>
</Card>
)
}
return (
<div className="space-y-6">
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
<Card variant="default" className="bg-cyber-darker">
<h2 className="text-2xl font-bold text-neon-cyan mb-4">{t('settings.keyManagement.title')}</h2>
<KeyManagementImportSection state={state} actions={actions} />
<SyncProgressBar />
<KeyManagementRecoverySection state={state} actions={actions} />
</div>
</Card>
</div>
)
}

View File

@ -1,3 +1,4 @@
import { Button, Card } from '@/components/ui'
import { t } from '@/lib/i18n'
import type { KeyManagementManagerActions } from './useKeyManagementManager'
import type { KeyManagementManagerState } from './keyManagementController'
@ -12,16 +13,18 @@ export function KeyManagementRecoverySection(params: {
return (
<div className="mt-6 space-y-4">
<KeyManagementRecoveryWarning />
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
<Card variant="default" className="bg-cyber-darker">
<RecoveryWordsGrid recoveryPhrase={params.state.recoveryPhrase} />
<button
<Button
type="button"
variant="secondary"
size="small"
onClick={params.actions.onCopyRecoveryPhrase}
className="w-full py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg text-sm font-medium transition-colors"
className="w-full"
>
{params.state.copiedRecoveryPhrase ? t('settings.keyManagement.recovery.copied') : t('settings.keyManagement.recovery.copy')}
</button>
</div>
</Button>
</Card>
<KeyManagementNewNpubCard newNpub={params.state.newNpub} />
<KeyManagementDoneButton onDone={params.actions.onDoneRecovery} />
</div>
@ -30,33 +33,34 @@ export function KeyManagementRecoverySection(params: {
function KeyManagementRecoveryWarning(): React.ReactElement {
return (
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4">
<Card variant="default" className="bg-yellow-900/20 border-yellow-400/50">
<p className="text-yellow-400 font-semibold mb-2">{t('settings.keyManagement.recovery.warning.title')}</p>
<p className="text-yellow-300/90 text-sm" dangerouslySetInnerHTML={{ __html: t('settings.keyManagement.recovery.warning.part1') }} />
<p className="text-yellow-300/90 text-sm mt-2" dangerouslySetInnerHTML={{ __html: t('settings.keyManagement.recovery.warning.part2') }} />
<p className="text-yellow-300/90 text-sm mt-2">{t('settings.keyManagement.recovery.warning.part3')}</p>
</div>
</Card>
)
}
function KeyManagementNewNpubCard(params: { newNpub: string }): React.ReactElement {
return (
<div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4">
<Card variant="default" className="bg-neon-blue/10 border-neon-blue/30">
<p className="text-neon-blue font-semibold mb-2">{t('settings.keyManagement.recovery.newNpub')}</p>
<p className="text-neon-cyan text-sm font-mono break-all">{params.newNpub}</p>
</div>
</Card>
)
}
function KeyManagementDoneButton(params: { onDone: () => void }): React.ReactElement {
return (
<button
<Button
type="button"
variant="primary"
onClick={params.onDone}
className="w-full py-2 px-4 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan"
className="w-full"
>
{t('settings.keyManagement.recovery.done')}
</button>
</Button>
)
}
@ -65,13 +69,14 @@ function RecoveryWordsGrid(params: { recoveryPhrase: string[] }): React.ReactEle
return (
<div className="grid grid-cols-2 gap-4 mb-4">
{items.map((item) => (
<div
<Card
key={item.id}
className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-3 text-center font-mono text-lg"
variant="default"
className="bg-cyber-dark p-3 text-center font-mono text-lg"
>
<span className="text-cyber-accent/70 text-sm mr-2">{item.position}.</span>
<span className="font-semibold text-neon-cyan">{item.word}</span>
</div>
</Card>
))}
</div>
)

View File

@ -1,8 +1,40 @@
import type { Nip95Config } from '@/lib/configStorageTypes'
import { Button, Card, Input } from '../ui'
import { t } from '@/lib/i18n'
import { DragHandle } from '../DragHandle'
import { getApiCardClassName } from './getApiCardClassName'
function Nip95ApiCardContent(params: {
api: Nip95Config
priority: number
isEditing: boolean
onStartEditing: (id: string) => void
onStopEditing: () => void
onUpdateUrl: (id: string, url: string) => void
onToggleEnabled: (id: string, enabled: boolean) => void
onRemoveApi: (id: string) => void
onDragStart: (e: React.DragEvent<HTMLDivElement>, id: string) => void
}): React.ReactElement {
return (
<>
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3 flex-1">
<DragGrip apiId={params.api.id} onDragStart={params.onDragStart} />
<UrlCell
api={params.api}
isEditing={params.isEditing}
onStartEditing={params.onStartEditing}
onStopEditing={params.onStopEditing}
onUpdateUrl={params.onUpdateUrl}
/>
</div>
<ActionsCell api={params.api} onToggleEnabled={params.onToggleEnabled} onRemoveApi={params.onRemoveApi} />
</div>
<PriorityRow priority={params.priority} apiId={params.api.id} />
</>
)
}
export function Nip95ApiCard(params: {
api: Nip95Config
priority: number
@ -21,25 +53,24 @@ export function Nip95ApiCard(params: {
}): React.ReactElement {
return (
<div
onDragOver={(e) => params.onDragOver(e, params.api.id)}
onDragOver={(e: React.DragEvent<HTMLDivElement>) => params.onDragOver(e, params.api.id)}
onDragLeave={params.onDragLeave}
onDrop={(e) => params.onDrop(e, params.api.id)}
className={`bg-cyber-dark border rounded p-4 space-y-3 transition-all ${getApiCardClassName(params.api.id, params.draggedId, params.dragOverId)}`}
onDrop={(e: React.DragEvent<HTMLDivElement>) => params.onDrop(e, params.api.id)}
className={getApiCardClassName(params.api.id, params.draggedId, params.dragOverId)}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3 flex-1">
<DragGrip apiId={params.api.id} onDragStart={params.onDragStart} />
<UrlCell
<Card variant="default" className="bg-cyber-dark space-y-3 transition-all">
<Nip95ApiCardContent
api={params.api}
priority={params.priority}
isEditing={params.isEditing}
onStartEditing={params.onStartEditing}
onStopEditing={params.onStopEditing}
onUpdateUrl={params.onUpdateUrl}
onToggleEnabled={params.onToggleEnabled}
onRemoveApi={params.onRemoveApi}
onDragStart={params.onDragStart}
/>
</div>
<ActionsCell api={params.api} onToggleEnabled={params.onToggleEnabled} onRemoveApi={params.onRemoveApi} />
</div>
<PriorityRow priority={params.priority} apiId={params.api.id} />
</Card>
</div>
)
}
@ -84,7 +115,7 @@ function UrlText(params: { api: Nip95Config; onStartEditing: (id: string) => voi
function UrlEditor(params: { api: Nip95Config; onStop: () => void; onUpdate: (id: string, url: string) => void }): React.ReactElement {
return (
<input
<Input
type="url"
defaultValue={params.api.url}
onBlur={(e) => {
@ -102,7 +133,7 @@ function UrlEditor(params: { api: Nip95Config; onStop: () => void; onUpdate: (id
params.onStop()
}
}}
className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/50 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
className="w-full bg-cyber-darker border-neon-cyan/50 text-cyber-light"
autoFocus
/>
)
@ -135,14 +166,15 @@ function EnabledToggle(params: { api: Nip95Config; onToggleEnabled: (id: string,
function RemoveButton(params: { apiId: string; onRemove: (id: string) => void }): React.ReactElement {
return (
<button
<Button
type="button"
variant="danger"
size="small"
onClick={() => params.onRemove(params.apiId)}
className="px-3 py-1 text-sm bg-red-900/30 hover:bg-red-900/50 text-red-300 border border-red-500/50 rounded transition-colors"
title={t('settings.nip95.list.remove')}
aria-label={t('settings.nip95.list.remove')}
>
{t('settings.nip95.list.remove')}
</button>
</Button>
)
}

View File

@ -1,4 +1,5 @@
import type { RelayConfig } from '@/lib/configStorageTypes'
import { Button, Card, Input } from '../ui'
import { t } from '@/lib/i18n'
import { DragHandle } from '../DragHandle'
import { getRelayCardClassName } from './controller'
@ -21,11 +22,12 @@ export function RelayCard(params: {
}): React.ReactElement {
return (
<div
onDragOver={(e) => params.onDragOver(e, params.relay.id)}
onDragOver={(e: React.DragEvent<HTMLDivElement>) => params.onDragOver(e, params.relay.id)}
onDragLeave={params.onDragLeave}
onDrop={(e) => params.onDrop(e, params.relay.id)}
className={`bg-cyber-dark border rounded p-4 space-y-3 transition-all ${getRelayCardClassName(params.relay.id, params.draggedId, params.dragOverId)}`}
onDrop={(e: React.DragEvent<HTMLDivElement>) => params.onDrop(e, params.relay.id)}
className={getRelayCardClassName(params.relay.id, params.draggedId, params.dragOverId)}
>
<Card variant="default" className="bg-cyber-dark space-y-3 transition-all">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3 flex-1">
<DragGrip relayId={params.relay.id} onDragStart={params.onDragStart} onDragEnd={params.onDragEnd} />
@ -34,6 +36,7 @@ export function RelayCard(params: {
</div>
<ActionsCell relay={params.relay} onToggleEnabled={params.onToggleEnabled} onRemoveRelay={params.onRemoveRelay} />
</div>
</Card>
</div>
)
}
@ -75,7 +78,7 @@ function UrlCell(params: {
function UrlEditor(params: { relay: RelayConfig; onStop: () => void; onUpdate: (id: string, url: string) => void }): React.ReactElement {
return (
<input
<Input
type="text"
defaultValue={params.relay.url}
onBlur={(e) => {
@ -93,7 +96,7 @@ function UrlEditor(params: { relay: RelayConfig; onStop: () => void; onUpdate: (
params.onStop()
}
}}
className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/50 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
className="w-full bg-cyber-darker border-neon-cyan/50 text-cyber-light"
autoFocus
/>
)
@ -119,14 +122,15 @@ function ActionsCell(params: { relay: RelayConfig; onToggleEnabled: (id: string,
/>
<span className="text-sm text-cyber-accent">{params.relay.enabled ? t('settings.relay.list.enabled') : t('settings.relay.list.disabled')}</span>
</label>
<button
<Button
type="button"
variant="danger"
size="small"
onClick={() => params.onRemoveRelay(params.relay.id)}
className="px-3 py-1 text-sm bg-red-900/30 hover:bg-red-900/50 text-red-300 border border-red-500/50 rounded transition-colors"
title={t('settings.relay.list.remove')}
aria-label={t('settings.relay.list.remove')}
>
{t('settings.relay.list.remove')}
</button>
</Button>
</div>
)
}

View File

@ -1,3 +1,4 @@
import { Card } from '../ui'
import { t } from '@/lib/i18n'
import { useSyncProgressBarController } from './controller'
import { SyncDateRange, SyncErrorBanner, SyncProgressSection, SyncResyncButton, SyncStatusMessage } from './view'
@ -8,7 +9,7 @@ export function SyncProgressBar(): React.ReactElement | null {
return null
}
return (
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-4 mt-6">
<Card variant="default" className="bg-cyber-darker mt-6">
<SyncErrorBanner error={controller.error} onDismiss={controller.dismissError} />
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-neon-cyan">{t('settings.sync.title')}</h3>
@ -17,6 +18,6 @@ export function SyncProgressBar(): React.ReactElement | null {
<SyncDateRange totalDays={controller.totalDays} startDate={controller.startDateLabel} endDate={controller.endDateLabel} />
<SyncProgressSection isSyncing={controller.isSyncing} syncProgress={controller.syncProgress} progressPercentage={controller.progressPercentage} />
<SyncStatusMessage isSyncing={controller.isSyncing} totalDays={controller.totalDays} isRecentlySynced={controller.isRecentlySynced} />
</div>
</Card>
)
}

131
docs/migration-status.md Normal file
View File

@ -0,0 +1,131 @@
# État de la migration des composants UI
**Date** : 2025-01-27
**Auteur** : Équipe 4NK
## Composants UI réutilisables créés
Les composants suivants ont été créés dans `components/ui/` :
- `Button` : Boutons avec variants (primary, secondary, success, danger, ghost) et tailles
- `Card` : Containers avec variants (default, interactive)
- `Input` : Champs de saisie avec support d'icônes
- `Textarea` : Zones de texte avec label et helperText
- `ErrorState` : Affichage d'erreurs
- `EmptyState` : États vides
- `Badge` : Badges
- `Skeleton` : Loaders skeleton
- `Modal` : Modales
- `Toast` : Notifications toast
- `MobileMenu` : Menu mobile
## Composants migrés
### Composants principaux
- ✅ `ArticlesList.tsx` - ErrorState, EmptyState
- ✅ `AuthorsList.tsx` - ErrorState, EmptyState
- ✅ `ArticleCard.tsx` - Card
- ✅ `AuthorCard.tsx` - Card
- ✅ `SearchBar.tsx` - Input
- ✅ `ArticleFilters.tsx` - Card, Button
- ✅ `ArticlePreview.tsx` - Button
- ✅ `PaymentModal.tsx` - Button, Card
- ✅ `ConnectButton.tsx` - Button
- ✅ `ArticleFormButtons.tsx` - Button
- ✅ `ConditionalPublishButton.tsx` - Button, Card
- ✅ `AlbyInstaller.tsx` - Button
- ✅ `CacheUpdateManager.tsx` - Button, Card, ErrorState
- ✅ `UserArticlesList.tsx` - Button, ErrorState
- ✅ `ConnectedUserMenu.tsx` - Button
- ✅ `SeriesCard.tsx` - Button
- ✅ `ProfileSeriesBlock.tsx` - Button
- ✅ `SeriesSection.tsx` - Button
### Formulaires
- ✅ `reviewForms/ReviewFormView.tsx` - Button, ErrorState, Input, Textarea, Card
- ✅ `reviewForms/ReviewTipFormView.tsx` - Button, ErrorState, Textarea, Card
- ✅ `reviewForms/ConnectRequiredCard.tsx` - Button, Card
- ✅ `SponsoringForm.tsx` - Button, Card, Textarea, ErrorState
- ✅ `authorPresentationEditor/PresentationForm.tsx` - Button, Card, ErrorState
- ✅ `authorPresentationEditor/NoAccountView.tsx` - Button, Card
- ✅ `authorPresentationEditor/AuthorPresentationEditor.tsx` - Button
- ✅ `createSeriesModal/CreateSeriesModalView.tsx` - Button, ErrorState, Input, Textarea
- ✅ `CreateAccountModalComponents.tsx` - Button, Card, ErrorState, Textarea
### Gestion de configuration
- ✅ `relayManager/RelayManagerContent.tsx` - Button, Card, Input, ErrorState
- ✅ `relayManager/RelayCard.tsx` - Button, Input
- ✅ `nip95Config/Nip95ConfigContent.tsx` - Button, Card, Input, ErrorState
- ✅ `nip95Config/Nip95ApiCard.tsx` - Button, Input
- ✅ `keyManagement/KeyManagementImportSection.tsx` - Button, Card, ErrorState
- ✅ `keyManagement/KeyManagementImportForm.tsx` - Textarea
### Autres composants
- ✅ `ArticleReviews.tsx` - Card, ErrorState, Button
- ✅ `ArticlePages.tsx` - Card
- ✅ `authorPage/SeriesList.tsx` - Button, Card, EmptyState
- ✅ `authorPage/SponsoringSummary.tsx` - Button, Card
- ✅ `syncProgressBar/view.tsx` - Button, ErrorState
- ✅ `MarkdownEditor.tsx` - Button
## Composants restants à migrer
### Priorité haute
1. **`SeriesCard.tsx`** - Le container principal utilise encore un `div` avec styles inline
2. **`createSeriesModal/CreateSeriesModalView.tsx`** - Le container de la modal utilise un `div` avec styles inline
3. **`AuthorFilterButton.tsx`** - Le bouton principal utilise encore un `button` avec styles inline
4. **`AuthorFilterDropdown.tsx`** - Les boutons d'option utilisent encore des `button` avec styles inline
5. **`CategoryTabs.tsx`** - Les boutons d'onglets utilisent encore des `button` avec styles inline
### Priorité moyenne
6. **`CreateAccountModalSteps.tsx`** - Containers de modales avec `div` et styles inline
7. **`LanguageSettingsManager.tsx`** - Containers avec `div` et styles inline
8. **`FundingGauge.tsx`** - Containers avec `div` et styles inline
9. **`DocsContent.tsx`** - Container principal avec `div` et styles inline
10. **`DocsSidebar.tsx`** - Container avec `div` et styles inline
11. **`keyManagement/KeyManagementRecoverySection.tsx`** - Containers et boutons avec styles inline
12. **`keyManagement/KeyManagementManager.tsx`** - Containers avec `div` et styles inline
13. **`keyManagement/KeyManagementImportForm.tsx`** - Boutons avec styles inline
14. **`syncProgressBar/SyncProgressBar.tsx`** - Container avec `div` et styles inline
15. **`AlbyInstaller.tsx`** - Container avec `div` et styles inline
16. **`ConditionalPublishButton.tsx`** - Le link container utilise encore un `Link` avec styles inline
17. **`NotificationActions.tsx`** - Boutons avec styles inline
18. **`NotificationBadge.tsx`** - Bouton avec styles inline
19. **`NotificationBadgeButton.tsx`** - Bouton avec styles inline
20. **`NotificationPanelHeader.tsx`** - Boutons avec styles inline
21. **`ProfileBackButton.tsx`** - Bouton avec styles inline
22. **`unlockAccount/UnlockAccountForm.tsx`** - Formulaires et boutons
23. **`unlockAccount/UnlockAccountButtons.tsx`** - Boutons
24. **`unlockAccount/WordSuggestions.tsx`** - Suggestions
25. **`markdownEditorTwoColumns/MarkdownEditorTwoColumns.tsx`** - Boutons et labels
26. **`markdownEditorTwoColumns/PagesManager.tsx`** - Boutons et labels
27. **`ImageUploadField.tsx`** - Le label d'upload utilise encore un `span` avec styles inline
28. **`ArticleEditorForm.tsx`** - Container d'erreur avec `div` et styles inline
29. **`LanguageSelector.tsx`** - Sélecteur de langue
30. **`authorPage/AuthorPageContent.tsx`** - Container avec `div` et styles inline
31. **`UserProfile.tsx`** - Container avec `div` et styles inline
### Priorité basse
32. **`relayManager/RelayCard.tsx`** - Le container principal utilise encore un `div` avec styles inline
33. **`nip95Config/Nip95ApiCard.tsx`** - Le container principal utilise encore un `div` avec styles inline
34. **`PaymentModal.tsx`** - Le container du QR code utilise encore un `div` avec styles inline
## Erreurs corrigées
### TypeScript
- ✅ `hooks/useAuthorPresentation.ts` : Ajout de l'import manquant `nostrService`
- ✅ `lib/authorQueries.ts` : Correction du type `picture` avec `exactOptionalPropertyTypes: true`
### Linting
- ✅ Toutes les erreurs de linting ont été corrigées
## État actuel
- ✅ **TypeScript** : Aucune erreur
- ✅ **Linting** : Aucune erreur
- ✅ **Compilation** : Succès
## Prochaines étapes
1. Continuer la migration des composants restants
2. Tester l'application après chaque migration
3. Documenter les changements dans `fixKnowledge/` ou `features/`

73
docs/todo-remaining.md Normal file
View File

@ -0,0 +1,73 @@
# Reste à faire sur le site
**Date** : 2025-01-27
**Auteur** : Équipe 4NK
## Migration des composants UI
Voir `docs/migration-status.md` pour la liste complète des composants restants à migrer vers les composants UI réutilisables.
**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
## 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é 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
## 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
## Corrections de bugs
Aucun bug critique identifié actuellement.
## Optimisations
- Performance : À évaluer après migration complète
- Accessibilité : Vérification complète après migration
- 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
## Documentation
- ✅ Documentation des composants UI créés
- ✅ Documentation de l'état de migration
- ⏳ Documentation des améliorations UX (en cours)
- ⏳ Documentation des améliorations UI (en cours)
## Déploiement
Le site peut être déployé en utilisant les scripts disponibles :
- `deploy.sh` : Déploiement initial complet
- `update-from-git.sh` : Mise à jour depuis Git local
- `update-remote-git.sh` : Mise à jour via Git sur le serveur
**Note** : Le déploiement nécessite une validation explicite avant exécution.

View File

@ -1,4 +1,5 @@
import { useState } from 'react'
import { nostrService } from '@/lib/nostr'
import { articlePublisher } from '@/lib/articlePublisher'
import type { Article } from '@/types/nostr'

View File

@ -61,7 +61,7 @@ export function convertPlatformProfileToNostrProfile(
pubkey: presentation.pubkey,
name: authorName,
about: presentation.description,
picture: presentation.bannerUrl,
...(presentation.bannerUrl !== undefined ? { picture: presentation.bannerUrl } : {}),
}
return profile
}