create for series
This commit is contained in:
parent
82dfbad5cb
commit
1082f33a77
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
131
docs/migration-status.md
Normal 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
73
docs/todo-remaining.md
Normal 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.
|
||||||
@ -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'
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user