story-research-zapwall/components/CreateSeriesModal.tsx
2026-01-06 14:17:55 +01:00

226 lines
7.7 KiB
TypeScript

import { useState } from 'react'
import { ImageUploadField } from './ImageUploadField'
import { publishSeries } from '@/lib/articleMutations'
import { useNostrAuth } from '@/hooks/useNostrAuth'
import { nostrService } from '@/lib/nostr'
import { t } from '@/lib/i18n'
import type { ArticleDraft } from '@/lib/articlePublisherTypes'
interface CreateSeriesModalProps {
isOpen: boolean
onClose: () => void
onSuccess: () => void
authorPubkey: string
}
interface SeriesDraft {
title: string
description: string
preview: string
coverUrl: string
category: ArticleDraft['category']
}
export function CreateSeriesModal({ isOpen, onClose, onSuccess, authorPubkey }: CreateSeriesModalProps): React.ReactElement | null {
const { pubkey, isUnlocked } = useNostrAuth()
const [draft, setDraft] = useState<SeriesDraft>({
title: '',
description: '',
preview: '',
coverUrl: '',
category: 'science-fiction',
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
if (!isOpen) {
return null
}
const privateKey = nostrService.getPrivateKey()
const canPublish = pubkey === authorPubkey && isUnlocked && privateKey !== null
const handleSubmit = async (e: React.FormEvent): Promise<void> => {
e.preventDefault()
if (!canPublish) {
setError(t('series.create.error.notAuthor'))
return
}
if (!draft.title.trim() || !draft.description.trim() || !draft.preview.trim()) {
setError(t('series.create.error.missingFields'))
return
}
setLoading(true)
setError(null)
try {
if (!privateKey) {
setError(t('series.create.error.notAuthor'))
return
}
await publishSeries({
title: draft.title,
description: draft.description,
preview: draft.preview,
...(draft.coverUrl ? { coverUrl: draft.coverUrl } : {}),
category: draft.category,
authorPubkey,
authorPrivateKey: privateKey,
})
// Reset form
setDraft({
title: '',
description: '',
preview: '',
coverUrl: '',
category: 'science-fiction',
})
onSuccess()
onClose()
} catch (e) {
setError(e instanceof Error ? e.message : t('series.create.error.publishFailed'))
} finally {
setLoading(false)
}
}
const handleClose = (): void => {
if (!loading) {
setDraft({
title: '',
description: '',
preview: '',
coverUrl: '',
category: 'science-fiction',
})
setError(null)
onClose()
}
}
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">
<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={handleClose}
disabled={loading}
className="text-cyber-accent hover:text-neon-cyan transition-colors disabled:opacity-50"
>
</button>
</div>
{!canPublish && (
<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>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="series-title" className="block text-sm font-medium text-neon-cyan mb-2">
{t('series.create.field.title')}
</label>
<input
id="series-title"
type="text"
value={draft.title}
onChange={(e) => setDraft({ ...draft, title: 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
disabled={loading || !canPublish}
/>
</div>
<div>
<label htmlFor="series-description" className="block text-sm font-medium text-neon-cyan mb-2">
{t('series.create.field.description')}
</label>
<textarea
id="series-description"
value={draft.description}
onChange={(e) => setDraft({ ...draft, description: e.target.value })}
rows={4}
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={loading || !canPublish}
/>
</div>
<div>
<label htmlFor="series-preview" className="block text-sm font-medium text-neon-cyan mb-2">
{t('series.create.field.preview')}
</label>
<textarea
id="series-preview"
value={draft.preview}
onChange={(e) => setDraft({ ...draft, preview: e.target.value })}
rows={3}
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={loading || !canPublish}
/>
<p className="text-xs text-cyber-accent/70 mt-1">{t('series.create.field.preview.help')}</p>
</div>
<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={draft.category}
onChange={(e) => setDraft({ ...draft, category: e.target.value as ArticleDraft['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={loading || !canPublish}
>
<option value="science-fiction">{t('category.science-fiction')}</option>
<option value="scientific-research">{t('category.scientific-research')}</option>
</select>
</div>
<div>
<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')}
/>
</div>
{error && (
<div className="p-4 bg-red-900/30 border border-red-500/50 rounded text-red-300">
<p>{error}</p>
</div>
)}
<div className="flex items-center justify-end gap-4 pt-4">
<button
type="button"
onClick={handleClose}
disabled={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={loading || !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"
>
{loading ? t('common.loading') : t('series.create.submit')}
</button>
</div>
</form>
</div>
</div>
)
}