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 { useEffect, useState, useCallback } from 'react'
import { Button, Card } from './ui'
import { getAlbyService } from '@/lib/alby' import { getAlbyService } from '@/lib/alby'
import { Button } from './ui'
interface AlbyInstallerProps { interface AlbyInstallerProps {
onInstalled?: () => void onInstalled?: () => void
@ -119,7 +119,7 @@ export function AlbyInstaller({ onInstalled }: AlbyInstallerProps): React.ReactE
} }
return ( 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 items-start">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<InfoIcon /> <InfoIcon />
@ -129,6 +129,6 @@ export function AlbyInstaller({ onInstalled }: AlbyInstallerProps): React.ReactE
{...(onInstalled ? { onInstalled } : {})} {...(onInstalled ? { onInstalled } : {})}
/> />
</div> </div>
</div> </Card>
) )
} }

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import type { Article } from '@/types/nostr' import type { Article } from '@/types/nostr'
import { Card } from './ui' import { Button, Card, Input } from './ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { AuthorFilter } from './AuthorFilter' import { AuthorFilter } from './AuthorFilter'
@ -69,9 +69,9 @@ function FiltersHeader({
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-neon-cyan">{t('filters.sort')}</h3> <h3 className="text-lg font-semibold text-neon-cyan">{t('filters.sort')}</h3>
{hasActiveFilters && ( {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')} {t('filters.clear')}
</button> </Button>
)} )}
</div> </div>
) )
@ -94,7 +94,7 @@ function SortFilter({
id="sort" id="sort"
value={value} value={value}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => onChange(e.target.value as SortOption)} 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="newest" className="bg-cyber-dark">{t('filters.sort.newest')}</option>
<option value="oldest" className="bg-cyber-dark">{t('filters.sort.oldest')}</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 Image from 'next/image'
import { Card } from './ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
export function AuthorAvatar({ picture, displayName }: { picture?: string; displayName: string }): React.ReactElement { export function AuthorAvatar({ picture, displayName }: { picture?: string; displayName: string }): React.ReactElement {
@ -214,20 +215,22 @@ export function AuthorDropdown({
}): React.ReactElement { }): React.ReactElement {
return ( return (
<div <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" role="listbox"
> >
<AllAuthorsOption value={value} onChange={onChange} setIsOpen={setIsOpen} /> <Card variant="default" className="bg-cyber-dark shadow-glow-cyan max-h-60 overflow-auto">
<AuthorDropdownContent <AllAuthorsOption value={value} onChange={onChange} setIsOpen={setIsOpen} />
authors={authors} <AuthorDropdownContent
value={value} authors={authors}
loading={loading} value={value}
getDisplayName={getDisplayName} loading={loading}
getPicture={getPicture} getDisplayName={getDisplayName}
getMnemonicIcons={getMnemonicIcons} getPicture={getPicture}
onChange={onChange} getMnemonicIcons={getMnemonicIcons}
setIsOpen={setIsOpen} onChange={onChange}
/> setIsOpen={setIsOpen}
/>
</Card>
</div> </div>
) )
} }

View File

@ -31,17 +31,17 @@ async function updateCache(): Promise<void> {
function SuccessMessage(): React.ReactElement { function SuccessMessage(): React.ReactElement {
return ( 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> <p className="text-green-400">Cache mis à jour avec succès</p>
</div> </Card>
) )
} }
function NotConnectedMessage(): React.ReactElement { function NotConnectedMessage(): React.ReactElement {
return ( 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> <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' import { t } from '@/lib/i18n'
type CategoryFilter = 'science-fiction' | 'scientific-research' | 'all' | null type CategoryFilter = 'science-fiction' | 'scientific-research' | 'all' | null
@ -12,26 +13,30 @@ export function CategoryTabs({ selectedCategory, onCategoryChange }: CategoryTab
<div className="mb-6"> <div className="mb-6">
<div className="border-b border-neon-cyan/30"> <div className="border-b border-neon-cyan/30">
<nav className="-mb-px flex space-x-8"> <nav className="-mb-px flex space-x-8">
<button <Button
type="button"
variant="ghost"
onClick={() => onCategoryChange('science-fiction')} 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' selectedCategory === 'science-fiction'
? 'border-neon-cyan text-neon-cyan' ? 'border-neon-cyan text-neon-cyan'
: 'border-transparent text-cyber-accent/70 hover:text-neon-cyan hover:border-neon-cyan/50' : 'border-transparent text-cyber-accent/70 hover:text-neon-cyan hover:border-neon-cyan/50'
}`} }`}
> >
{t('category.science-fiction')} {t('category.science-fiction')}
</button> </Button>
<button <Button
type="button"
variant="ghost"
onClick={() => onCategoryChange('scientific-research')} 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' selectedCategory === 'scientific-research'
? 'border-neon-cyan text-neon-cyan' ? 'border-neon-cyan text-neon-cyan'
: 'border-transparent text-cyber-accent/70 hover:text-neon-cyan hover:border-neon-cyan/50' : 'border-transparent text-cyber-accent/70 hover:text-neon-cyan hover:border-neon-cyan/50'
}`} }`}
> >
{t('category.scientific-research')} {t('category.scientific-research')}
</button> </Button>
</nav> </nav>
</div> </div>
</div> </div>

View File

@ -1,9 +1,10 @@
import Link from 'next/link' import Link from 'next/link'
import Image from 'next/image' import Image from 'next/image'
import { useRouter } from 'next/router'
import { useNostrAuth } from '@/hooks/useNostrAuth' import { useNostrAuth } from '@/hooks/useNostrAuth'
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation' import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Button } from './ui' import { Button, Card } from './ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import type { Article } from '@/types/nostr' 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 // Extract picture from presentation (bannerUrl or from JSON metadata) or profile
const picture = presentation.bannerUrl ?? profile?.picture const picture = presentation.bannerUrl ?? profile?.picture
const router = useRouter()
const handleClick = (): void => {
void router.push(`/author/${presentation.pubkey}`)
}
return ( return (
<Link <Card variant="interactive" onClick={handleClick} className="flex items-center gap-2 px-3 py-2 bg-cyber-dark/50">
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"
>
{picture ? ( {picture ? (
<div className="relative w-8 h-8 rounded-full overflow-hidden border border-neon-cyan/30 flex-shrink-0"> <div className="relative w-8 h-8 rounded-full overflow-hidden border border-neon-cyan/30 flex-shrink-0">
<Image <Image
@ -49,7 +53,7 @@ function AuthorProfileLink({ presentation, profile }: { presentation: Article; p
</div> </div>
)} )}
<span className="text-sm text-neon-cyan font-medium truncate max-w-[120px]">{authorName}</span> <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' import { t } from '@/lib/i18n'
export function RecoveryWarning(): React.ReactElement { export function RecoveryWarning(): React.ReactElement {
return ( 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-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" 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" 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> <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 { }): React.ReactElement {
const recoveryItems = buildRecoveryPhraseItems(recoveryPhrase) const recoveryItems = buildRecoveryPhraseItems(recoveryPhrase)
return ( 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"> <div className="grid grid-cols-2 gap-4 mb-4">
{recoveryItems.map((item, index) => ( {recoveryItems.map((item, index) => (
<div <Card
key={item.key} 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="text-cyber-accent/70 text-sm mr-2">{index + 1}.</span>
<span className="font-semibold text-neon-cyan">{item.word}</span> <span className="font-semibold text-neon-cyan">{item.word}</span>
</div> </Card>
))} ))}
</div> </div>
<button <Button
onClick={() => { onClick={() => {
void onCopy() 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')} {copied ? t('account.create.recovery.copied') : t('account.create.recovery.copy')}
</button> </Button>
</div> </Card>
) )
} }
@ -57,10 +61,10 @@ function buildRecoveryPhraseItems(recoveryPhrase: string[]): { key: string; word
export function PublicKeyDisplay({ npub }: { npub: string }): React.ReactElement { export function PublicKeyDisplay({ npub }: { npub: string }): React.ReactElement {
return ( 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-blue font-semibold mb-2">{t('account.create.publicKey')}</p>
<p className="text-neon-cyan text-sm font-mono break-all">{npub}</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 { }): React.ReactElement {
return ( return (
<> <>
<div className="mb-4"> <Textarea
<label htmlFor="importKey" className="block text-sm font-medium text-cyber-accent mb-2"> id="importKey"
{t('account.create.importKey.label')} label={t('account.create.importKey.label')}
</label> value={importKey}
<textarea onChange={(e) => setImportKey(e.target.value)}
id="importKey" placeholder={t('account.create.importKey.placeholder')}
value={importKey} className="font-mono text-sm text-neon-cyan bg-cyber-darker"
onChange={(e) => setImportKey(e.target.value)} rows={4}
placeholder={t('account.create.importKey.placeholder')} helperText={t('account.create.importKey.help')}
className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/30 rounded-lg font-mono text-sm text-neon-cyan" />
rows={4} {error && <ErrorState message={error} className="mb-4" />}
/>
<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>}
</> </>
) )
} }
@ -97,25 +97,71 @@ export function ImportKeyForm({
export function ImportStepButtons({ loading, onImport, onBack }: { loading: boolean; onImport: () => void; onBack: () => void }): React.ReactElement { export function ImportStepButtons({ loading, onImport, onBack }: { loading: boolean; onImport: () => void; onBack: () => void }): React.ReactElement {
return ( return (
<div className="flex gap-4"> <div className="flex gap-4">
<button <Button
onClick={onBack} 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')} {t('account.create.back')}
</button> </Button>
<button <Button
onClick={() => { onClick={() => {
void onImport() void onImport()
}} }}
disabled={loading} 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')} {loading ? t('import.loading') : t('import.button')}
</button> </Button>
</div> </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({ export function ChooseStepButtons({
loading, loading,
onGenerate, onGenerate,
@ -129,28 +175,9 @@ export function ChooseStepButtons({
}): React.ReactElement { }): React.ReactElement {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<button <GenerateButton loading={loading} onGenerate={onGenerate} />
onClick={() => { <ImportButton loading={loading} onImport={onImport} />
void onGenerate() <CancelButton onClose={onClose} />
}}
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>
</div> </div>
) )
} }

View File

@ -1,4 +1,5 @@
import { useState } from 'react' import { useState } from 'react'
import { Button, Card } from './ui'
import { RecoveryWarning, RecoveryPhraseDisplay, PublicKeyDisplay, ImportKeyForm, ImportStepButtons, ChooseStepButtons } from './CreateAccountModalComponents' import { RecoveryWarning, RecoveryPhraseDisplay, PublicKeyDisplay, ImportKeyForm, ImportStepButtons, ChooseStepButtons } from './CreateAccountModalComponents'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
@ -23,20 +24,22 @@ export function RecoveryStep({
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <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> <h2 className="text-2xl font-bold mb-4 text-neon-cyan">{t('account.create.recovery.title')}</h2>
<RecoveryWarning /> <RecoveryWarning />
<RecoveryPhraseDisplay recoveryPhrase={recoveryPhrase} copied={copied} onCopy={() => { void handleCopy() }} /> <RecoveryPhraseDisplay recoveryPhrase={recoveryPhrase} copied={copied} onCopy={() => { void handleCopy() }} />
<PublicKeyDisplay npub={npub} /> <PublicKeyDisplay npub={npub} />
<div className="flex gap-4"> <div className="flex gap-4">
<button <Button
onClick={onContinue} 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')} {t('account.create.recovery.saved')}
</button> </Button>
</div> </div>
</div> </Card>
</div> </div>
) )
} }
@ -58,11 +61,11 @@ export function ImportStep({
}): React.ReactElement { }): React.ReactElement {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <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> <h2 className="text-2xl font-bold mb-4 text-neon-cyan">{t('account.import.title')}</h2>
<ImportKeyForm importKey={importKey} setImportKey={setImportKey} error={error} /> <ImportKeyForm importKey={importKey} setImportKey={setImportKey} error={error} />
<ImportStepButtons loading={loading} onImport={onImport} onBack={onBack} /> <ImportStepButtons loading={loading} onImport={onImport} onBack={onBack} />
</div> </Card>
</div> </div>
) )
} }
@ -82,14 +85,14 @@ export function ChooseStep({
}): React.ReactElement { }): React.ReactElement {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <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> <h2 className="text-2xl font-bold mb-4 text-neon-cyan">{t('account.create.title')}</h2>
<p className="text-cyber-accent/70 mb-6"> <p className="text-cyber-accent/70 mb-6">
{t('account.create.description')} {t('account.create.description')}
</p> </p>
{error && <p className="text-sm text-red-400 mb-4">{error}</p>} {error && <p className="text-sm text-red-400 mb-4">{error}</p>}
<ChooseStepButtons loading={loading} onGenerate={onGenerate} onImport={onImport} onClose={onClose} /> <ChooseStepButtons loading={loading} onGenerate={onGenerate} onImport={onImport} onClose={onClose} />
</div> </Card>
</div> </div>
) )
} }

View File

@ -1,3 +1,4 @@
import { Card } from './ui'
import { renderMarkdown } from '@/lib/markdownRenderer' import { renderMarkdown } from '@/lib/markdownRenderer'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
@ -16,10 +17,10 @@ export function DocsContent({ content, loading }: DocsContentProps): React.React
} }
return ( 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"> <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)} {renderMarkdown(content)}
</div> </div>
</div> </Card>
) )
} }

View File

@ -1,3 +1,4 @@
import { Button, Card } from './ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import type { DocLink, DocSection } from '@/hooks/useDocs' import type { DocLink, DocSection } from '@/hooks/useDocs'
@ -10,24 +11,22 @@ interface DocsSidebarProps {
export function DocsSidebar({ docs, selectedDoc, onSelectDoc }: DocsSidebarProps): React.ReactElement { export function DocsSidebar({ docs, selectedDoc, onSelectDoc }: DocsSidebarProps): React.ReactElement {
return ( return (
<aside className="lg:w-64 flex-shrink-0"> <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> <h2 className="text-lg font-bold mb-4 text-neon-cyan font-mono">{t('docs.title')}</h2>
<nav className="space-y-2"> <nav className="space-y-2">
{docs.map((doc) => ( {docs.map((doc) => (
<button <Button
key={doc.id} key={doc.id}
type="button"
variant={selectedDoc === doc.id ? 'primary' : 'ghost'}
onClick={() => onSelectDoc(doc.id)} onClick={() => onSelectDoc(doc.id)}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-all ${ className="w-full text-left justify-start"
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'
}`}
> >
{doc.title} {doc.title}
</button> </Button>
))} ))}
</nav> </nav>
</div> </Card>
</aside> </aside>
) )
} }

View File

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

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Button, Card } from './ui'
import { setLocale, getLocale, type Locale } from '@/lib/i18n' import { setLocale, getLocale, type Locale } from '@/lib/i18n'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { localeStorage } from '@/lib/localeStorage' import { localeStorage } from '@/lib/localeStorage'
@ -13,16 +14,13 @@ interface LocaleOptionProps {
function LocaleOption({ locale, label, currentLocale, onClick }: LocaleOptionProps): React.ReactElement { function LocaleOption({ locale, label, currentLocale, onClick }: LocaleOptionProps): React.ReactElement {
const isActive = currentLocale === locale const isActive = currentLocale === locale
return ( return (
<button <Button
type="button"
variant={isActive ? 'primary' : 'secondary'}
onClick={() => onClick(locale)} 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} {label}
</button> </Button>
) )
} }
@ -48,20 +46,20 @@ function LanguageSettingsPanel(params: {
}): React.ReactElement { }): React.ReactElement {
if (params.loading) { if (params.loading) {
return ( 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>{t('settings.language.loading')}</div>
</div> </Card>
) )
} }
return ( 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> <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> <p className="text-cyber-accent mb-4 text-sm">{t('settings.language.description')}</p>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<LocaleOption locale="fr" label={t('settings.language.french')} currentLocale={params.currentLocale} onClick={params.onLocaleClick} /> <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} /> <LocaleOption locale="en" label={t('settings.language.english')} currentLocale={params.currentLocale} onClick={params.onLocaleClick} />
</div> </div>
</div> </Card>
) )
} }

View File

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

View File

@ -3,7 +3,7 @@ import QRCode from 'react-qr-code'
import type { AlbyInvoice } from '@/types/alby' import type { AlbyInvoice } from '@/types/alby'
import { getAlbyService, isWebLNAvailable } from '@/lib/alby' import { getAlbyService, isWebLNAvailable } from '@/lib/alby'
import { AlbyInstaller } from './AlbyInstaller' import { AlbyInstaller } from './AlbyInstaller'
import { Modal, Button } from './ui' import { Card, Modal, Button } from './ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
interface PaymentModalProps { interface PaymentModalProps {
@ -67,16 +67,18 @@ function InvoiceDisplay({ invoiceText, paymentUrl }: { invoiceText: string; paym
return ( return (
<div className="mb-4"> <div className="mb-4">
<p className="text-sm text-cyber-accent mb-2">{t('payment.modal.lightningInvoice')}</p> <p className="text-sm text-cyber-accent mb-2">{t('payment.modal.lightningInvoice')}</p>
<div className="bg-cyber-darker border border-neon-cyan/20 p-3 rounded break-all text-sm font-mono mb-4 text-neon-cyan">{invoiceText}</div> <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="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 <QRCode
value={paymentUrl} value={paymentUrl}
size={200} size={200}
style={{ height: 'auto', maxWidth: '100%', width: '100%' }} style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
viewBox="0 0 256 256" viewBox="0 0 256 256"
/> />
</div> </Card>
</div> </div>
<p className="text-xs text-cyber-accent/70 text-center mb-2">{t('payment.modal.scanQr')}</p> <p className="text-xs text-cyber-accent/70 text-center mb-2">{t('payment.modal.scanQr')}</p>
</div> </div>
@ -119,10 +121,10 @@ function ExpiredNotice({ show }: { show: boolean }): React.ReactElement | null {
return null return null
} }
return ( 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-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> <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 Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { Button } from './ui' import { Button, Card } from './ui'
import type { Series } from '@/types/nostr' import type { Series } from '@/types/nostr'
import { t } from '@/lib/i18n' 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 ring-1 ring-neon-cyan/50 shadow-glow-cyan'
: 'border-neon-cyan/30 hover:border-neon-cyan/50 hover:shadow-glow-cyan' : 'border-neon-cyan/30 hover:border-neon-cyan/50 hover:shadow-glow-cyan'
return ( 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} /> <SeriesCardContent series={series} onSelect={onSelect} />
</div> </Card>
) )
} }

View File

@ -126,9 +126,9 @@ function buildSeriesLink(
Ouvrir Ouvrir
</Link> </Link>
{onSelectSeries && ( {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 Filtrer
</button> </Button>
)} )}
</div> </div>
) )

View File

@ -1,3 +1,4 @@
import { Card } from './ui'
import type { NostrProfile } from '@/types/nostr' import type { NostrProfile } from '@/types/nostr'
import { UserProfileHeader } from './UserProfileHeader' import { UserProfileHeader } from './UserProfileHeader'
@ -20,7 +21,7 @@ export function UserProfile({ profile, pubkey, articleCount }: UserProfileProps)
const displayName = profile.name ?? `${pubkey.slice(0, 16)}...` const displayName = profile.name ?? `${pubkey.slice(0, 16)}...`
return ( 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 <UserProfileHeader
displayName={displayName} displayName={displayName}
{...(profile.picture ? { picture: profile.picture } : {})} {...(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>} {profile.about && <p className="text-gray-700 mt-2">{profile.about}</p>}
{articleCount !== undefined && <ProfileStats articleCount={articleCount} />} {articleCount !== undefined && <ProfileStats articleCount={articleCount} />}
</div> </Card>
) )
} }

View File

@ -1,3 +1,4 @@
import { Card } from '../ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import type { AuthorPresentationArticle, Series } from '@/types/nostr' import type { AuthorPresentationArticle, Series } from '@/types/nostr'
import { AuthorPageHeader } from './AuthorPageHeader' import { AuthorPageHeader } from './AuthorPageHeader'
@ -33,9 +34,9 @@ export function AuthorPageContent({
if (!presentation) { if (!presentation) {
return ( 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> <p className="text-cyber-accent">{t('author.notFound')}</p>
</div> </Card>
) )
} }

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import { ImageUploadField } from '../ImageUploadField' 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 { t } from '@/lib/i18n'
import type { SeriesDraft } from './createSeriesModalTypes' import type { SeriesDraft } from './createSeriesModalTypes'
import type { CreateSeriesModalController } from './useCreateSeriesModalController' import type { CreateSeriesModalController } from './useCreateSeriesModalController'
@ -8,11 +8,11 @@ import type { CreateSeriesModalController } from './useCreateSeriesModalControll
export function CreateSeriesModalView({ ctrl }: { ctrl: CreateSeriesModalController }): React.ReactElement { export function CreateSeriesModalView({ ctrl }: { ctrl: CreateSeriesModalController }): React.ReactElement {
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"> <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} /> <CreateSeriesModalHeader loading={ctrl.loading} onClose={ctrl.handleClose} />
{!ctrl.canPublish ? <NotAuthorWarning /> : null} {!ctrl.canPublish ? <NotAuthorWarning /> : null}
<CreateSeriesForm ctrl={ctrl} /> <CreateSeriesForm ctrl={ctrl} />
</div> </Card>
</div> </div>
) )
} }
@ -21,23 +21,26 @@ function CreateSeriesModalHeader({ loading, onClose }: { loading: boolean; onClo
return ( return (
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-semibold text-neon-cyan">{t('series.create.title')}</h2> <h2 className="text-2xl font-semibold text-neon-cyan">{t('series.create.title')}</h2>
<button <Button
type="button" type="button"
variant="ghost"
size="small"
onClick={onClose} onClick={onClose}
disabled={loading} 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> </div>
) )
} }
function NotAuthorWarning(): React.ReactElement { function NotAuthorWarning(): React.ReactElement {
return ( 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> <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 { t } from '@/lib/i18n'
import type { KeyManagementManagerActions } from './useKeyManagementManager' import type { KeyManagementManagerActions } from './useKeyManagementManager'
import type { KeyManagementManagerState } from './keyManagementController' import type { KeyManagementManagerState } from './keyManagementController'
@ -31,13 +32,13 @@ export function KeyManagementImportForm(params: {
function KeyManagementImportWarning(params: { accountExists: boolean }): React.ReactElement { function KeyManagementImportWarning(params: { accountExists: boolean }): React.ReactElement {
return ( 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-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') }} /> <p className="text-yellow-300/90 text-sm" dangerouslySetInnerHTML={{ __html: t('settings.keyManagement.import.warning.description') }} />
{params.accountExists ? ( {params.accountExists ? (
<p className="text-yellow-300/90 text-sm mt-2" dangerouslySetInnerHTML={{ __html: t('settings.keyManagement.import.warning.replace') }} /> <p className="text-yellow-300/90 text-sm mt-2" dangerouslySetInnerHTML={{ __html: t('settings.keyManagement.import.warning.replace') }} />
) : null} ) : null}
</div> </Card>
) )
} }
@ -47,20 +48,18 @@ function KeyManagementImportTextarea(params: {
}): React.ReactElement { }): React.ReactElement {
return ( return (
<div> <div>
<label htmlFor="importKey" className="block text-sm font-medium text-cyber-accent mb-2"> <Textarea
{t('settings.keyManagement.import.label')}
</label>
<textarea
id="importKey" id="importKey"
label={t('settings.keyManagement.import.label')}
value={params.importKey} value={params.importKey}
onChange={(e) => { onChange={(e) => {
params.onChangeImportKey(e.target.value) params.onChangeImportKey(e.target.value)
}} }}
placeholder={t('settings.keyManagement.import.placeholder')} 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} 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> </div>
) )
} }
@ -75,27 +74,30 @@ function KeyManagementReplaceWarning(params: {
return null return null
} }
return ( 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-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> <p className="text-red-300/90 text-sm mb-4">{t('settings.keyManagement.replace.warning.description')}</p>
<div className="flex gap-4"> <div className="flex gap-4">
<button <Button
type="button" type="button"
variant="secondary"
onClick={params.onCancel} 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')} {t('settings.keyManagement.replace.cancel')}
</button> </Button>
<button <Button
type="button" type="button"
variant="danger"
onClick={params.onConfirm} onClick={params.onConfirm}
disabled={params.importing} 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')} {params.importing ? t('settings.keyManagement.replace.replacing') : t('settings.keyManagement.replace.confirm')}
</button> </Button>
</div> </div>
</div> </Card>
) )
} }
@ -109,22 +111,25 @@ function KeyManagementImportFormActions(params: {
return null return null
} }
return ( return (
<div className="flex gap-4"> <div className="flex gap-4">
<button <Button
type="button" type="button"
onClick={params.onCancel} variant="secondary"
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" onClick={params.onCancel}
> className="flex-1"
{t('settings.keyManagement.import.cancel')} >
</button> {t('settings.keyManagement.import.cancel')}
<button </Button>
type="button" <Button
onClick={params.onImport} type="button"
disabled={params.importing} variant="primary"
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" onClick={params.onImport}
> disabled={params.importing}
{params.importing ? t('settings.keyManagement.import.importing') : t('settings.keyManagement.import.import')} loading={params.importing}
</button> className="flex-1"
</div> >
{params.importing ? t('settings.keyManagement.import.importing') : t('settings.keyManagement.import.import')}
</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 { t } from '@/lib/i18n'
import type { KeyManagementManagerActions } from './useKeyManagementManager' import type { KeyManagementManagerActions } from './useKeyManagementManager'
import type { KeyManagementManagerState } from './keyManagementController' import type { KeyManagementManagerState } from './keyManagementController'
@ -75,7 +75,7 @@ function KeyManagementKeyCard(params: {
onCopy: () => void onCopy: () => void
}): React.ReactElement { }): React.ReactElement {
return ( 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"> <div className="flex justify-between items-start mb-2">
<p className="text-neon-blue font-semibold">{params.label}</p> <p className="text-neon-blue font-semibold">{params.label}</p>
<Button <Button
@ -88,7 +88,7 @@ function KeyManagementKeyCard(params: {
</Button> </Button>
</div> </div>
<p className="text-neon-cyan text-sm font-mono break-all">{params.value}</p> <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 { t } from '@/lib/i18n'
import { SyncProgressBar } from '../SyncProgressBar' import { SyncProgressBar } from '../SyncProgressBar'
import { KeyManagementImportSection } from './KeyManagementImportSection' import { KeyManagementImportSection } from './KeyManagementImportSection'
@ -9,20 +10,20 @@ export function KeyManagementManager(): React.ReactElement {
if (state.loading) { if (state.loading) {
return ( 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> <p className="text-cyber-accent">{t('settings.keyManagement.loading')}</p>
</div> </Card>
) )
} }
return ( return (
<div className="space-y-6"> <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> <h2 className="text-2xl font-bold text-neon-cyan mb-4">{t('settings.keyManagement.title')}</h2>
<KeyManagementImportSection state={state} actions={actions} /> <KeyManagementImportSection state={state} actions={actions} />
<SyncProgressBar /> <SyncProgressBar />
<KeyManagementRecoverySection state={state} actions={actions} /> <KeyManagementRecoverySection state={state} actions={actions} />
</div> </Card>
</div> </div>
) )
} }

View File

@ -1,3 +1,4 @@
import { Button, Card } from '@/components/ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import type { KeyManagementManagerActions } from './useKeyManagementManager' import type { KeyManagementManagerActions } from './useKeyManagementManager'
import type { KeyManagementManagerState } from './keyManagementController' import type { KeyManagementManagerState } from './keyManagementController'
@ -12,16 +13,18 @@ export function KeyManagementRecoverySection(params: {
return ( return (
<div className="mt-6 space-y-4"> <div className="mt-6 space-y-4">
<KeyManagementRecoveryWarning /> <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} /> <RecoveryWordsGrid recoveryPhrase={params.state.recoveryPhrase} />
<button <Button
type="button" type="button"
variant="secondary"
size="small"
onClick={params.actions.onCopyRecoveryPhrase} 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')} {params.state.copiedRecoveryPhrase ? t('settings.keyManagement.recovery.copied') : t('settings.keyManagement.recovery.copy')}
</button> </Button>
</div> </Card>
<KeyManagementNewNpubCard newNpub={params.state.newNpub} /> <KeyManagementNewNpubCard newNpub={params.state.newNpub} />
<KeyManagementDoneButton onDone={params.actions.onDoneRecovery} /> <KeyManagementDoneButton onDone={params.actions.onDoneRecovery} />
</div> </div>
@ -30,33 +33,34 @@ export function KeyManagementRecoverySection(params: {
function KeyManagementRecoveryWarning(): React.ReactElement { function KeyManagementRecoveryWarning(): React.ReactElement {
return ( 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-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" 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" 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> <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 { function KeyManagementNewNpubCard(params: { newNpub: string }): React.ReactElement {
return ( 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-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> <p className="text-neon-cyan text-sm font-mono break-all">{params.newNpub}</p>
</div> </Card>
) )
} }
function KeyManagementDoneButton(params: { onDone: () => void }): React.ReactElement { function KeyManagementDoneButton(params: { onDone: () => void }): React.ReactElement {
return ( return (
<button <Button
type="button" type="button"
variant="primary"
onClick={params.onDone} 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')} {t('settings.keyManagement.recovery.done')}
</button> </Button>
) )
} }
@ -65,13 +69,14 @@ function RecoveryWordsGrid(params: { recoveryPhrase: string[] }): React.ReactEle
return ( return (
<div className="grid grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-2 gap-4 mb-4">
{items.map((item) => ( {items.map((item) => (
<div <Card
key={item.id} 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="text-cyber-accent/70 text-sm mr-2">{item.position}.</span>
<span className="font-semibold text-neon-cyan">{item.word}</span> <span className="font-semibold text-neon-cyan">{item.word}</span>
</div> </Card>
))} ))}
</div> </div>
) )

View File

@ -1,8 +1,40 @@
import type { Nip95Config } from '@/lib/configStorageTypes' import type { Nip95Config } from '@/lib/configStorageTypes'
import { Button, Card, Input } from '../ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { DragHandle } from '../DragHandle' import { DragHandle } from '../DragHandle'
import { getApiCardClassName } from './getApiCardClassName' 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: { export function Nip95ApiCard(params: {
api: Nip95Config api: Nip95Config
priority: number priority: number
@ -21,25 +53,24 @@ export function Nip95ApiCard(params: {
}): React.ReactElement { }): React.ReactElement {
return ( return (
<div <div
onDragOver={(e) => params.onDragOver(e, params.api.id)} onDragOver={(e: React.DragEvent<HTMLDivElement>) => params.onDragOver(e, params.api.id)}
onDragLeave={params.onDragLeave} onDragLeave={params.onDragLeave}
onDrop={(e) => params.onDrop(e, params.api.id)} onDrop={(e: React.DragEvent<HTMLDivElement>) => 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)}`} className={getApiCardClassName(params.api.id, params.draggedId, params.dragOverId)}
> >
<div className="flex items-start justify-between gap-4"> <Card variant="default" className="bg-cyber-dark space-y-3 transition-all">
<div className="flex items-center gap-3 flex-1"> <Nip95ApiCardContent
<DragGrip apiId={params.api.id} onDragStart={params.onDragStart} /> api={params.api}
<UrlCell priority={params.priority}
api={params.api} isEditing={params.isEditing}
isEditing={params.isEditing} onStartEditing={params.onStartEditing}
onStartEditing={params.onStartEditing} onStopEditing={params.onStopEditing}
onStopEditing={params.onStopEditing} onUpdateUrl={params.onUpdateUrl}
onUpdateUrl={params.onUpdateUrl} onToggleEnabled={params.onToggleEnabled}
/> onRemoveApi={params.onRemoveApi}
</div> onDragStart={params.onDragStart}
<ActionsCell api={params.api} onToggleEnabled={params.onToggleEnabled} onRemoveApi={params.onRemoveApi} /> />
</div> </Card>
<PriorityRow priority={params.priority} apiId={params.api.id} />
</div> </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 { function UrlEditor(params: { api: Nip95Config; onStop: () => void; onUpdate: (id: string, url: string) => void }): React.ReactElement {
return ( return (
<input <Input
type="url" type="url"
defaultValue={params.api.url} defaultValue={params.api.url}
onBlur={(e) => { onBlur={(e) => {
@ -102,7 +133,7 @@ function UrlEditor(params: { api: Nip95Config; onStop: () => void; onUpdate: (id
params.onStop() 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 autoFocus
/> />
) )
@ -135,14 +166,15 @@ function EnabledToggle(params: { api: Nip95Config; onToggleEnabled: (id: string,
function RemoveButton(params: { apiId: string; onRemove: (id: string) => void }): React.ReactElement { function RemoveButton(params: { apiId: string; onRemove: (id: string) => void }): React.ReactElement {
return ( return (
<button <Button
type="button" type="button"
variant="danger"
size="small"
onClick={() => params.onRemove(params.apiId)} 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" aria-label={t('settings.nip95.list.remove')}
title={t('settings.nip95.list.remove')}
> >
{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 type { RelayConfig } from '@/lib/configStorageTypes'
import { Button, Card, Input } from '../ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { DragHandle } from '../DragHandle' import { DragHandle } from '../DragHandle'
import { getRelayCardClassName } from './controller' import { getRelayCardClassName } from './controller'
@ -21,19 +22,21 @@ export function RelayCard(params: {
}): React.ReactElement { }): React.ReactElement {
return ( return (
<div <div
onDragOver={(e) => params.onDragOver(e, params.relay.id)} onDragOver={(e: React.DragEvent<HTMLDivElement>) => params.onDragOver(e, params.relay.id)}
onDragLeave={params.onDragLeave} onDragLeave={params.onDragLeave}
onDrop={(e) => params.onDrop(e, params.relay.id)} onDrop={(e: React.DragEvent<HTMLDivElement>) => 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)}`} className={getRelayCardClassName(params.relay.id, params.draggedId, params.dragOverId)}
> >
<div className="flex items-start justify-between gap-4"> <Card variant="default" className="bg-cyber-dark space-y-3 transition-all">
<div className="flex items-center gap-3 flex-1"> <div className="flex items-start justify-between gap-4">
<DragGrip relayId={params.relay.id} onDragStart={params.onDragStart} onDragEnd={params.onDragEnd} /> <div className="flex items-center gap-3 flex-1">
<UrlCell relay={params.relay} isEditing={params.isEditing} onStartEditing={params.onStartEditing} onStopEditing={params.onStopEditing} onUpdateUrl={params.onUpdateUrl} /> <DragGrip relayId={params.relay.id} onDragStart={params.onDragStart} onDragEnd={params.onDragEnd} />
{params.relay.lastSyncDate ? <LastSync lastSyncDate={params.relay.lastSyncDate} /> : null} <UrlCell relay={params.relay} isEditing={params.isEditing} onStartEditing={params.onStartEditing} onStopEditing={params.onStopEditing} onUpdateUrl={params.onUpdateUrl} />
{params.relay.lastSyncDate ? <LastSync lastSyncDate={params.relay.lastSyncDate} /> : null}
</div>
<ActionsCell relay={params.relay} onToggleEnabled={params.onToggleEnabled} onRemoveRelay={params.onRemoveRelay} />
</div> </div>
<ActionsCell relay={params.relay} onToggleEnabled={params.onToggleEnabled} onRemoveRelay={params.onRemoveRelay} /> </Card>
</div>
</div> </div>
) )
} }
@ -75,7 +78,7 @@ function UrlCell(params: {
function UrlEditor(params: { relay: RelayConfig; onStop: () => void; onUpdate: (id: string, url: string) => void }): React.ReactElement { function UrlEditor(params: { relay: RelayConfig; onStop: () => void; onUpdate: (id: string, url: string) => void }): React.ReactElement {
return ( return (
<input <Input
type="text" type="text"
defaultValue={params.relay.url} defaultValue={params.relay.url}
onBlur={(e) => { onBlur={(e) => {
@ -93,7 +96,7 @@ function UrlEditor(params: { relay: RelayConfig; onStop: () => void; onUpdate: (
params.onStop() 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 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> <span className="text-sm text-cyber-accent">{params.relay.enabled ? t('settings.relay.list.enabled') : t('settings.relay.list.disabled')}</span>
</label> </label>
<button <Button
type="button" type="button"
variant="danger"
size="small"
onClick={() => params.onRemoveRelay(params.relay.id)} 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" aria-label={t('settings.relay.list.remove')}
title={t('settings.relay.list.remove')}
> >
{t('settings.relay.list.remove')} {t('settings.relay.list.remove')}
</button> </Button>
</div> </div>
) )
} }

View File

@ -1,3 +1,4 @@
import { Card } from '../ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { useSyncProgressBarController } from './controller' import { useSyncProgressBarController } from './controller'
import { SyncDateRange, SyncErrorBanner, SyncProgressSection, SyncResyncButton, SyncStatusMessage } from './view' import { SyncDateRange, SyncErrorBanner, SyncProgressSection, SyncResyncButton, SyncStatusMessage } from './view'
@ -8,7 +9,7 @@ export function SyncProgressBar(): React.ReactElement | null {
return null return null
} }
return ( 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} /> <SyncErrorBanner error={controller.error} onDismiss={controller.dismissError} />
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-neon-cyan">{t('settings.sync.title')}</h3> <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} /> <SyncDateRange totalDays={controller.totalDays} startDate={controller.startDateLabel} endDate={controller.endDateLabel} />
<SyncProgressSection isSyncing={controller.isSyncing} syncProgress={controller.syncProgress} progressPercentage={controller.progressPercentage} /> <SyncProgressSection isSyncing={controller.isSyncing} syncProgress={controller.syncProgress} progressPercentage={controller.progressPercentage} />
<SyncStatusMessage isSyncing={controller.isSyncing} totalDays={controller.totalDays} isRecentlySynced={controller.isRecentlySynced} /> <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 { useState } from 'react'
import { nostrService } from '@/lib/nostr'
import { articlePublisher } from '@/lib/articlePublisher' import { articlePublisher } from '@/lib/articlePublisher'
import type { Article } from '@/types/nostr' import type { Article } from '@/types/nostr'

View File

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