create for series

This commit is contained in:
Nicolas Cantu 2026-01-14 00:50:05 +01:00
parent 6fcfae4cc0
commit 055465ac7b
5 changed files with 78 additions and 101 deletions

View File

@ -1,4 +1,5 @@
import type { Page } from '@/types/nostr'
import { Card } from './ui'
import { t } from '@/lib/i18n'
import { useNostrAuth } from '@/hooks/useNostrAuth'
import { useEffect, useState } from 'react'
@ -28,10 +29,10 @@ function LockedPagesView({ pagesCount }: { pagesCount: number }): React.ReactEle
return (
<div className="space-y-6 mt-6">
<h3 className="text-xl font-semibold text-neon-cyan">{t('article.pages.title')}</h3>
<div className="border border-neon-cyan/30 rounded-lg p-6 bg-cyber-dark text-center">
<Card variant="default" className="text-center">
<p className="text-cyber-accent mb-2">{t('article.pages.locked.title')}</p>
<p className="text-sm text-cyber-accent/70">{t('article.pages.locked.message', { count: pagesCount })}</p>
</div>
</Card>
</div>
)
}
@ -98,7 +99,7 @@ function isUserPurchase({
function PageDisplay({ page }: { page: Page }): React.ReactElement {
return (
<div className="border border-neon-cyan/30 rounded-lg p-4 bg-cyber-dark">
<Card variant="default" className="mb-0">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-neon-cyan">
{t('page.number', { number: page.number })}
@ -124,6 +125,6 @@ function PageDisplay({ page }: { page: Page }): React.ReactElement {
)}
</div>
)}
</div>
</Card>
)
}

View File

@ -2,6 +2,7 @@ import { useState } from 'react'
import { useRouter } from 'next/router'
import { nostrAuthService } from '@/lib/nostrAuth'
import { objectCache } from '@/lib/objectCache'
import { Button, ErrorState } from './ui'
async function updateCache(): Promise<void> {
const state = nostrAuthService.getState()
@ -28,14 +29,6 @@ async function updateCache(): Promise<void> {
}
}
function ErrorMessage({ error }: { error: string }): React.ReactElement {
return (
<div className="bg-red-900/20 border border-red-400/50 rounded-lg p-4 mb-4">
<p className="text-red-400">{error}</p>
</div>
)
}
function SuccessMessage(): React.ReactElement {
return (
<div className="bg-green-900/20 border border-green-400/50 rounded-lg p-4 mb-4">
@ -78,12 +71,6 @@ function createUpdateHandler(
}
}
function Spinner(): React.ReactElement {
return (
<div className="inline-block animate-spin rounded-full h-4 w-4 border-2 border-neon-cyan border-t-transparent mr-2" />
)
}
export function CacheUpdateManager(): React.ReactElement {
const router = useRouter()
const [updating, setUpdating] = useState(false)
@ -102,20 +89,21 @@ export function CacheUpdateManager(): React.ReactElement {
Cela permet de récupérer les dernières versions de vos publications, séries et profil.
</p>
{error && <ErrorMessage error={error} />}
{error && <ErrorState message={error} />}
{success && <SuccessMessage />}
{!isConnected && <NotConnectedMessage />}
<button
<Button
variant="primary"
onClick={() => {
void handleUpdateCache()
}}
disabled={updating || !isConnected}
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 disabled:cursor-not-allowed flex items-center justify-center"
loading={updating}
className="w-full"
>
{updating && <Spinner />}
{updating ? 'Mise à jour en cours...' : 'Mettre à jour le cache'}
</button>
</Button>
</div>
)
}

View File

@ -2,6 +2,7 @@ import { useState } from 'react'
import { CreateAccountModal } from '../CreateAccountModal'
import { RecoveryStep } from '../CreateAccountModalSteps'
import { UnlockAccountModal } from '../UnlockAccountModal'
import { Button, Card } from '../ui'
import { t } from '@/lib/i18n'
export function NoAccountView(): React.ReactElement {
@ -50,20 +51,12 @@ export function NoAccountView(): React.ReactElement {
function NoAccountActionButtons(params: { onGenerate: () => void; onImport: () => void }): React.ReactElement {
return (
<div className="flex flex-col gap-3 w-full max-w-xs">
<button
type="button"
onClick={params.onGenerate}
className="px-6 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan"
>
<Button variant="primary" size="large" onClick={params.onGenerate}>
{t('account.create.generateButton')}
</button>
<button
type="button"
onClick={params.onImport}
className="px-6 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-medium transition-colors"
>
</Button>
<Button variant="secondary" size="large" onClick={params.onImport}>
{t('account.create.importButton')}
</button>
</Button>
</div>
)
}
@ -76,7 +69,7 @@ function NoAccountCard(params: {
modals: React.ReactElement
}): React.ReactElement {
return (
<div className="border border-neon-cyan/20 rounded-lg p-6 bg-cyber-dark/50">
<Card variant="default" className="bg-cyber-dark/50">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-cyber-accent mb-2">Créez un compte ou importez votre clé secrète pour commencer</p>
{params.error ? <p className="text-sm text-red-400">{params.error}</p> : null}
@ -84,7 +77,7 @@ function NoAccountCard(params: {
{params.generating ? <p className="text-cyber-accent text-sm">Génération du compte...</p> : null}
{params.modals}
</div>
</div>
</Card>
)
}

View File

@ -1,19 +1,21 @@
import React from 'react'
import { Button, ErrorState } from '@/components/ui'
import { Button, ErrorState, Input, Textarea, Card } from '@/components/ui'
import { t } from '@/lib/i18n'
import type { ReviewFormController } from './useReviewFormController'
export function ReviewFormView(params: { ctrl: ReviewFormController; onCancel?: () => void }): React.ReactElement {
return (
<Card variant="default" className="space-y-4">
<form
onSubmit={(e) => void params.ctrl.handleSubmit(e)}
className="border border-neon-cyan/30 rounded-lg p-4 bg-cyber-dark space-y-4"
className="space-y-4"
>
<ReviewFormHeader />
<ReviewFormFields ctrl={params.ctrl} />
{params.ctrl.error ? <ErrorBox message={params.ctrl.error} /> : null}
<ReviewFormActions loading={params.ctrl.loading} onCancel={params.onCancel} />
</form>
</Card>
)
}
@ -94,20 +96,16 @@ function TextInput(params: {
placeholder: string
optionalLabel?: string
}): React.ReactElement {
const labelText = params.optionalLabel ? `${params.label} ${params.optionalLabel}` : params.label
return (
<div>
<label htmlFor={params.id} className="block text-sm font-medium text-cyber-accent mb-1">
{params.label} {params.optionalLabel ? <span className="text-cyber-accent/50">{params.optionalLabel}</span> : null}
</label>
<input
<Input
id={params.id}
type="text"
label={labelText}
value={params.value}
onChange={(e) => params.onChange(e.target.value)}
placeholder={params.placeholder}
className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/30 rounded text-cyber-accent focus:border-neon-cyan focus:outline-none"
/>
</div>
)
}
@ -123,23 +121,23 @@ function TextAreaInput(params: {
optionalLabel?: string
helpText?: string
}): React.ReactElement {
let labelText = params.label
if (params.requiredMark) {
labelText = `${labelText} *`
}
if (params.optionalLabel) {
labelText = `${labelText} ${params.optionalLabel}`
}
return (
<div>
<label htmlFor={params.id} className="block text-sm font-medium text-cyber-accent mb-1">
{params.label}{' '}
{params.requiredMark ? <span className="text-red-400">*</span> : null}{' '}
{params.optionalLabel ? <span className="text-cyber-accent/50">{params.optionalLabel}</span> : null}
</label>
<textarea
<Textarea
id={params.id}
label={labelText}
value={params.value}
onChange={(e) => params.onChange(e.target.value)}
placeholder={params.placeholder}
rows={params.rows}
required={params.required}
className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/30 rounded text-cyber-accent focus:border-neon-cyan focus:outline-none"
{...(params.helpText ? { helperText: params.helpText } : {})}
/>
{params.helpText ? <p className="text-xs text-cyber-accent/70 mt-1">{params.helpText}</p> : null}
</div>
)
}

View File

@ -1,19 +1,21 @@
import React from 'react'
import { Button, ErrorState } from '@/components/ui'
import { Button, ErrorState, Textarea, Card } from '@/components/ui'
import { t } from '@/lib/i18n'
import type { ReviewTipFormController } from './useReviewTipFormController'
export function ReviewTipFormView(params: { ctrl: ReviewTipFormController; onCancel?: () => void }): React.ReactElement {
return (
<Card variant="default" className="space-y-4">
<form
onSubmit={(e) => void params.ctrl.handleSubmit(e)}
className="border border-neon-cyan/30 rounded-lg p-4 bg-cyber-dark space-y-4"
className="space-y-4"
>
<ReviewTipFormHeader split={params.ctrl.split} />
<ReviewTipTextField value={params.ctrl.text} onChange={params.ctrl.setText} />
{params.ctrl.error ? <ErrorBox message={params.ctrl.error} /> : null}
<ReviewTipFormActions amount={params.ctrl.split.total} loading={params.ctrl.loading} onCancel={params.onCancel} />
</form>
</Card>
)
}
@ -34,20 +36,15 @@ function ReviewTipFormHeader(params: { split: { total: number; reviewer: number;
function ReviewTipTextField(params: { value: string; onChange: (value: string) => void }): React.ReactElement {
return (
<div>
<label htmlFor="review-tip-text" className="block text-sm font-medium text-cyber-accent mb-1">
{t('reviewTip.form.text.label')} <span className="text-cyber-accent/50">({t('common.optional')})</span>
</label>
<textarea
<Textarea
id="review-tip-text"
label={`${t('reviewTip.form.text.label')} (${t('common.optional')})`}
value={params.value}
onChange={(e) => params.onChange(e.target.value)}
placeholder={t('reviewTip.form.text.placeholder')}
rows={3}
className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/30 rounded text-cyber-accent focus:border-neon-cyan focus:outline-none"
helperText={t('reviewTip.form.text.help')}
/>
<p className="text-xs text-cyber-accent/70 mt-1">{t('reviewTip.form.text.help')}</p>
</div>
)
}