174 lines
5.7 KiB
TypeScript
174 lines
5.7 KiB
TypeScript
import { useState } from 'react'
|
|
import { publishReview } from '@/lib/articleMutations'
|
|
import { nostrService } from '@/lib/nostr'
|
|
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
|
import { t } from '@/lib/i18n'
|
|
import type { Article } from '@/types/nostr'
|
|
|
|
interface ReviewFormProps {
|
|
article: Article
|
|
onSuccess?: () => void
|
|
onCancel?: () => void
|
|
}
|
|
|
|
export function ReviewForm({ article, onSuccess, onCancel }: ReviewFormProps): React.ReactElement {
|
|
const { pubkey, connect } = useNostrAuth()
|
|
const [content, setContent] = useState('')
|
|
const [title, setTitle] = useState('')
|
|
const [text, setText] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const handleSubmit = async (e: React.FormEvent): Promise<void> => {
|
|
e.preventDefault()
|
|
if (!pubkey) {
|
|
await connect()
|
|
return
|
|
}
|
|
|
|
if (!content.trim()) {
|
|
setError(t('review.form.error.contentRequired'))
|
|
return
|
|
}
|
|
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const privateKey = nostrService.getPrivateKey()
|
|
if (!privateKey) {
|
|
setError(t('review.form.error.noPrivateKey'))
|
|
return
|
|
}
|
|
|
|
const category = (() => {
|
|
if (article.category === 'author-presentation') {
|
|
return 'science-fiction'
|
|
}
|
|
return article.category ?? 'science-fiction'
|
|
})()
|
|
const seriesId = article.seriesId ?? ''
|
|
|
|
await publishReview({
|
|
articleId: article.id,
|
|
seriesId,
|
|
category: category === 'science-fiction' || category === 'scientific-research' ? category : 'science-fiction',
|
|
authorPubkey: article.pubkey,
|
|
reviewerPubkey: pubkey,
|
|
content: content.trim(),
|
|
...(title.trim() ? { title: title.trim() } : {}),
|
|
...(text.trim() ? { text: text.trim() } : {}),
|
|
authorPrivateKey: privateKey,
|
|
})
|
|
|
|
setContent('')
|
|
setTitle('')
|
|
setText('')
|
|
onSuccess?.()
|
|
} catch (submitError) {
|
|
setError(submitError instanceof Error ? submitError.message : t('review.form.error.publishFailed'))
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
if (!pubkey) {
|
|
return (
|
|
<div className="border border-neon-cyan/30 rounded-lg p-4 bg-cyber-dark">
|
|
<p className="text-cyber-accent mb-4">{t('review.form.connectRequired')}</p>
|
|
<button
|
|
onClick={() => {
|
|
void connect()
|
|
}}
|
|
className="px-4 py-2 bg-neon-green/20 hover:bg-neon-green/30 text-neon-green rounded-lg font-medium transition-all border border-neon-green/50"
|
|
>
|
|
{t('connect.connect')}
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={(e) => {
|
|
void handleSubmit(e)
|
|
}} className="border border-neon-cyan/30 rounded-lg p-4 bg-cyber-dark space-y-4">
|
|
<h3 className="text-lg font-semibold text-neon-cyan">{t('review.form.title')}</h3>
|
|
|
|
<div>
|
|
<label htmlFor="review-title" className="block text-sm font-medium text-cyber-accent mb-1">
|
|
{t('review.form.title.label')} <span className="text-cyber-accent/50">({t('common.optional')})</span>
|
|
</label>
|
|
<input
|
|
id="review-title"
|
|
type="text"
|
|
value={title}
|
|
onChange={(e) => {
|
|
setTitle(e.target.value)
|
|
}}
|
|
placeholder={t('review.form.title.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>
|
|
|
|
<div>
|
|
<label htmlFor="review-content" className="block text-sm font-medium text-cyber-accent mb-1">
|
|
{t('review.form.content.label')} <span className="text-red-400">*</span>
|
|
</label>
|
|
<textarea
|
|
id="review-content"
|
|
value={content}
|
|
onChange={(e) => {
|
|
setContent(e.target.value)
|
|
}}
|
|
placeholder={t('review.form.content.placeholder')}
|
|
rows={6}
|
|
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"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="review-text" className="block text-sm font-medium text-cyber-accent mb-1">
|
|
{t('review.form.text.label')} <span className="text-cyber-accent/50">({t('common.optional')})</span>
|
|
</label>
|
|
<textarea
|
|
id="review-text"
|
|
value={text}
|
|
onChange={(e) => {
|
|
setText(e.target.value)
|
|
}}
|
|
placeholder={t('review.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"
|
|
/>
|
|
<p className="text-xs text-cyber-accent/70 mt-1">{t('review.form.text.help')}</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="p-3 bg-red-900/20 border border-red-500/50 rounded text-red-400 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-2">
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="px-4 py-2 bg-neon-green/20 hover:bg-neon-green/30 text-neon-green rounded-lg font-medium transition-all border border-neon-green/50 hover:shadow-glow-green disabled:opacity-50"
|
|
>
|
|
{loading ? t('common.loading') : t('review.form.submit')}
|
|
</button>
|
|
{onCancel && (
|
|
<button
|
|
type="button"
|
|
onClick={onCancel}
|
|
className="px-4 py-2 bg-cyber-darker hover:bg-cyber-dark text-cyber-accent rounded-lg font-medium transition-all border border-neon-cyan/30"
|
|
>
|
|
{t('common.cancel')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|