228 lines
7.8 KiB
TypeScript
228 lines
7.8 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 (submitError) {
|
|
setError(submitError instanceof Error ? submitError.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={(e) => {
|
|
void handleSubmit(e)
|
|
}} 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>
|
|
)
|
|
}
|