221 lines
7.4 KiB
TypeScript
221 lines
7.4 KiB
TypeScript
import React from 'react'
|
|
import { ImageUploadField } from '../ImageUploadField'
|
|
import { t } from '@/lib/i18n'
|
|
import type { SeriesDraft } from './createSeriesModalTypes'
|
|
import type { CreateSeriesModalController } from './useCreateSeriesModalController'
|
|
|
|
export function CreateSeriesModalView({ ctrl }: { ctrl: CreateSeriesModalController }): React.ReactElement {
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
|
<CreateSeriesModalHeader loading={ctrl.loading} onClose={ctrl.handleClose} />
|
|
{!ctrl.canPublish ? <NotAuthorWarning /> : null}
|
|
<CreateSeriesForm ctrl={ctrl} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CreateSeriesModalHeader({ loading, onClose }: { loading: boolean; onClose: () => void }): React.ReactElement {
|
|
return (
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-semibold text-neon-cyan">{t('series.create.title')}</h2>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={loading}
|
|
className="text-cyber-accent hover:text-neon-cyan transition-colors disabled:opacity-50"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function NotAuthorWarning(): React.ReactElement {
|
|
return (
|
|
<div className="mb-4 p-4 bg-yellow-900/30 border border-yellow-500/50 rounded text-yellow-300">
|
|
<p>{t('series.create.error.notAuthor')}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CreateSeriesForm({ ctrl }: { ctrl: CreateSeriesModalController }): React.ReactElement {
|
|
return (
|
|
<form onSubmit={(e) => void ctrl.handleSubmit(e)} className="space-y-4">
|
|
<SeriesTextFields draft={ctrl.draft} setDraft={ctrl.setDraft} loading={ctrl.loading} canPublish={ctrl.canPublish} />
|
|
<SeriesCategoryField draft={ctrl.draft} setDraft={ctrl.setDraft} loading={ctrl.loading} canPublish={ctrl.canPublish} />
|
|
<SeriesCoverField draft={ctrl.draft} setDraft={ctrl.setDraft} />
|
|
<SeriesError error={ctrl.error} />
|
|
<SeriesActions loading={ctrl.loading} canPublish={ctrl.canPublish} onClose={ctrl.handleClose} />
|
|
</form>
|
|
)
|
|
}
|
|
|
|
function SeriesTextFields(params: {
|
|
draft: SeriesDraft
|
|
setDraft: (draft: SeriesDraft) => void
|
|
loading: boolean
|
|
canPublish: boolean
|
|
}): React.ReactElement {
|
|
const disabled = params.loading || !params.canPublish
|
|
return (
|
|
<>
|
|
<TextField
|
|
id="series-title"
|
|
label={t('series.create.field.title')}
|
|
value={params.draft.title}
|
|
disabled={disabled}
|
|
required
|
|
onChange={(value) => params.setDraft({ ...params.draft, title: value })}
|
|
/>
|
|
<TextAreaField
|
|
id="series-description"
|
|
label={t('series.create.field.description')}
|
|
value={params.draft.description}
|
|
disabled={disabled}
|
|
required
|
|
rows={4}
|
|
onChange={(value) => params.setDraft({ ...params.draft, description: value })}
|
|
/>
|
|
<TextAreaField
|
|
id="series-preview"
|
|
label={t('series.create.field.preview')}
|
|
value={params.draft.preview}
|
|
disabled={disabled}
|
|
required
|
|
rows={3}
|
|
helpText={t('series.create.field.preview.help')}
|
|
onChange={(value) => params.setDraft({ ...params.draft, preview: value })}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
function SeriesCategoryField(params: {
|
|
draft: SeriesDraft
|
|
setDraft: (draft: SeriesDraft) => void
|
|
loading: boolean
|
|
canPublish: boolean
|
|
}): React.ReactElement {
|
|
const disabled = params.loading || !params.canPublish
|
|
return (
|
|
<div>
|
|
<label htmlFor="series-category" className="block text-sm font-medium text-neon-cyan mb-2">
|
|
{t('series.create.field.category')}
|
|
</label>
|
|
<select
|
|
id="series-category"
|
|
value={params.draft.category}
|
|
onChange={(e) => params.setDraft({ ...params.draft, category: e.target.value as SeriesDraft['category'] })}
|
|
className="w-full px-3 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
|
|
required
|
|
disabled={disabled}
|
|
>
|
|
<option value="science-fiction">{t('category.science-fiction')}</option>
|
|
<option value="scientific-research">{t('category.scientific-research')}</option>
|
|
</select>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SeriesCoverField({ draft, setDraft }: { draft: SeriesDraft; setDraft: (draft: SeriesDraft) => void }): React.ReactElement {
|
|
return (
|
|
<ImageUploadField
|
|
id="series-cover"
|
|
label={t('series.create.field.cover')}
|
|
value={draft.coverUrl}
|
|
onChange={(url) => setDraft({ ...draft, coverUrl: url })}
|
|
helpText={t('series.create.field.cover.help')}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function SeriesError({ error }: { error: string | null }): React.ReactElement | null {
|
|
if (!error) {
|
|
return null
|
|
}
|
|
return (
|
|
<div className="p-4 bg-red-900/30 border border-red-500/50 rounded text-red-300">
|
|
<p>{error}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SeriesActions(params: { loading: boolean; canPublish: boolean; onClose: () => void }): React.ReactElement {
|
|
return (
|
|
<div className="flex items-center justify-end gap-4 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={params.onClose}
|
|
disabled={params.loading}
|
|
className="px-4 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light hover:border-neon-cyan transition-colors disabled:opacity-50"
|
|
>
|
|
{t('common.cancel')}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={params.loading || !params.canPublish}
|
|
className="px-4 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 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{params.loading ? t('common.loading') : t('series.create.submit')}
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function TextField(params: {
|
|
id: string
|
|
label: string
|
|
value: string
|
|
disabled: boolean
|
|
required: boolean
|
|
onChange: (value: string) => void
|
|
}): React.ReactElement {
|
|
return (
|
|
<div>
|
|
<label htmlFor={params.id} className="block text-sm font-medium text-neon-cyan mb-2">
|
|
{params.label}
|
|
</label>
|
|
<input
|
|
id={params.id}
|
|
type="text"
|
|
value={params.value}
|
|
onChange={(e) => params.onChange(e.target.value)}
|
|
className="w-full px-3 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
|
|
required={params.required}
|
|
disabled={params.disabled}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function TextAreaField(params: {
|
|
id: string
|
|
label: string
|
|
value: string
|
|
disabled: boolean
|
|
required: boolean
|
|
rows: number
|
|
helpText?: string
|
|
onChange: (value: string) => void
|
|
}): React.ReactElement {
|
|
return (
|
|
<div>
|
|
<label htmlFor={params.id} className="block text-sm font-medium text-neon-cyan mb-2">
|
|
{params.label}
|
|
</label>
|
|
<textarea
|
|
id={params.id}
|
|
value={params.value}
|
|
onChange={(e) => params.onChange(e.target.value)}
|
|
rows={params.rows}
|
|
className="w-full px-3 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
|
|
required={params.required}
|
|
disabled={params.disabled}
|
|
/>
|
|
{params.helpText ? <p className="text-xs text-cyber-accent/70 mt-1">{params.helpText}</p> : null}
|
|
</div>
|
|
)
|
|
}
|