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

View File

@ -2,6 +2,7 @@ import { useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { nostrAuthService } from '@/lib/nostrAuth' import { nostrAuthService } from '@/lib/nostrAuth'
import { objectCache } from '@/lib/objectCache' import { objectCache } from '@/lib/objectCache'
import { Button, ErrorState } from './ui'
async function updateCache(): Promise<void> { async function updateCache(): Promise<void> {
const state = nostrAuthService.getState() 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 { function SuccessMessage(): React.ReactElement {
return ( return (
<div className="bg-green-900/20 border border-green-400/50 rounded-lg p-4 mb-4"> <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 { export function CacheUpdateManager(): React.ReactElement {
const router = useRouter() const router = useRouter()
const [updating, setUpdating] = useState(false) 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. Cela permet de récupérer les dernières versions de vos publications, séries et profil.
</p> </p>
{error && <ErrorMessage error={error} />} {error && <ErrorState message={error} />}
{success && <SuccessMessage />} {success && <SuccessMessage />}
{!isConnected && <NotConnectedMessage />} {!isConnected && <NotConnectedMessage />}
<button <Button
variant="primary"
onClick={() => { onClick={() => {
void handleUpdateCache() void handleUpdateCache()
}} }}
disabled={updating || !isConnected} 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'} {updating ? 'Mise à jour en cours...' : 'Mettre à jour le cache'}
</button> </Button>
</div> </div>
) )
} }

View File

@ -2,6 +2,7 @@ import { useState } from 'react'
import { CreateAccountModal } from '../CreateAccountModal' import { CreateAccountModal } from '../CreateAccountModal'
import { RecoveryStep } from '../CreateAccountModalSteps' import { RecoveryStep } from '../CreateAccountModalSteps'
import { UnlockAccountModal } from '../UnlockAccountModal' import { UnlockAccountModal } from '../UnlockAccountModal'
import { Button, Card } from '../ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
export function NoAccountView(): React.ReactElement { export function NoAccountView(): React.ReactElement {
@ -50,20 +51,12 @@ export function NoAccountView(): React.ReactElement {
function NoAccountActionButtons(params: { onGenerate: () => void; onImport: () => void }): React.ReactElement { function NoAccountActionButtons(params: { onGenerate: () => void; onImport: () => void }): React.ReactElement {
return ( return (
<div className="flex flex-col gap-3 w-full max-w-xs"> <div className="flex flex-col gap-3 w-full max-w-xs">
<button <Button variant="primary" size="large" onClick={params.onGenerate}>
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"
>
{t('account.create.generateButton')} {t('account.create.generateButton')}
</button> </Button>
<button <Button variant="secondary" size="large" onClick={params.onImport}>
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"
>
{t('account.create.importButton')} {t('account.create.importButton')}
</button> </Button>
</div> </div>
) )
} }
@ -76,7 +69,7 @@ function NoAccountCard(params: {
modals: React.ReactElement modals: React.ReactElement
}): React.ReactElement { }): React.ReactElement {
return ( 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"> <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> <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} {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.generating ? <p className="text-cyber-accent text-sm">Génération du compte...</p> : null}
{params.modals} {params.modals}
</div> </div>
</div> </Card>
) )
} }

View File

@ -1,19 +1,21 @@
import React from 'react' 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 { t } from '@/lib/i18n'
import type { ReviewFormController } from './useReviewFormController' import type { ReviewFormController } from './useReviewFormController'
export function ReviewFormView(params: { ctrl: ReviewFormController; onCancel?: () => void }): React.ReactElement { export function ReviewFormView(params: { ctrl: ReviewFormController; onCancel?: () => void }): React.ReactElement {
return ( return (
<Card variant="default" className="space-y-4">
<form <form
onSubmit={(e) => void params.ctrl.handleSubmit(e)} 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 /> <ReviewFormHeader />
<ReviewFormFields ctrl={params.ctrl} /> <ReviewFormFields ctrl={params.ctrl} />
{params.ctrl.error ? <ErrorBox message={params.ctrl.error} /> : null} {params.ctrl.error ? <ErrorBox message={params.ctrl.error} /> : null}
<ReviewFormActions loading={params.ctrl.loading} onCancel={params.onCancel} /> <ReviewFormActions loading={params.ctrl.loading} onCancel={params.onCancel} />
</form> </form>
</Card>
) )
} }
@ -94,20 +96,16 @@ function TextInput(params: {
placeholder: string placeholder: string
optionalLabel?: string optionalLabel?: string
}): React.ReactElement { }): React.ReactElement {
const labelText = params.optionalLabel ? `${params.label} ${params.optionalLabel}` : params.label
return ( return (
<div> <Input
<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
id={params.id} id={params.id}
type="text" type="text"
label={labelText}
value={params.value} value={params.value}
onChange={(e) => params.onChange(e.target.value)} onChange={(e) => params.onChange(e.target.value)}
placeholder={params.placeholder} 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 optionalLabel?: string
helpText?: string helpText?: string
}): React.ReactElement { }): React.ReactElement {
let labelText = params.label
if (params.requiredMark) {
labelText = `${labelText} *`
}
if (params.optionalLabel) {
labelText = `${labelText} ${params.optionalLabel}`
}
return ( return (
<div> <Textarea
<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
id={params.id} id={params.id}
label={labelText}
value={params.value} value={params.value}
onChange={(e) => params.onChange(e.target.value)} onChange={(e) => params.onChange(e.target.value)}
placeholder={params.placeholder} placeholder={params.placeholder}
rows={params.rows} rows={params.rows}
required={params.required} 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 React from 'react'
import { Button, ErrorState } from '@/components/ui' import { Button, ErrorState, Textarea, Card } from '@/components/ui'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import type { ReviewTipFormController } from './useReviewTipFormController' import type { ReviewTipFormController } from './useReviewTipFormController'
export function ReviewTipFormView(params: { ctrl: ReviewTipFormController; onCancel?: () => void }): React.ReactElement { export function ReviewTipFormView(params: { ctrl: ReviewTipFormController; onCancel?: () => void }): React.ReactElement {
return ( return (
<Card variant="default" className="space-y-4">
<form <form
onSubmit={(e) => void params.ctrl.handleSubmit(e)} 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} /> <ReviewTipFormHeader split={params.ctrl.split} />
<ReviewTipTextField value={params.ctrl.text} onChange={params.ctrl.setText} /> <ReviewTipTextField value={params.ctrl.text} onChange={params.ctrl.setText} />
{params.ctrl.error ? <ErrorBox message={params.ctrl.error} /> : null} {params.ctrl.error ? <ErrorBox message={params.ctrl.error} /> : null}
<ReviewTipFormActions amount={params.ctrl.split.total} loading={params.ctrl.loading} onCancel={params.onCancel} /> <ReviewTipFormActions amount={params.ctrl.split.total} loading={params.ctrl.loading} onCancel={params.onCancel} />
</form> </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 { function ReviewTipTextField(params: { value: string; onChange: (value: string) => void }): React.ReactElement {
return ( return (
<div> <Textarea
<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
id="review-tip-text" id="review-tip-text"
label={`${t('reviewTip.form.text.label')} (${t('common.optional')})`}
value={params.value} value={params.value}
onChange={(e) => params.onChange(e.target.value)} onChange={(e) => params.onChange(e.target.value)}
placeholder={t('reviewTip.form.text.placeholder')} placeholder={t('reviewTip.form.text.placeholder')}
rows={3} 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>
) )
} }