105 lines
3.2 KiB
JavaScript
105 lines
3.2 KiB
JavaScript
import React, { useState, useRef, useCallback } from 'react'
|
|
import Button from '../ui/Button'
|
|
import RichTextEditor from './RichTextEditor'
|
|
import TurnstileField from '../security/TurnstileField'
|
|
import { buildBotFingerprint } from '../../lib/security/botFingerprint'
|
|
|
|
export default function ReplyForm({ topicKey, prefill = '', quotedAuthor = null, csrfToken, captcha = {} }) {
|
|
const [content, setContent] = useState(prefill)
|
|
const [captchaToken, setCaptchaToken] = useState('')
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [error, setError] = useState(null)
|
|
const formRef = useRef(null)
|
|
|
|
const handleSubmit = useCallback(async (e) => {
|
|
e.preventDefault()
|
|
if (submitting || content.trim().length < 2) return
|
|
|
|
setSubmitting(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const fingerprint = await buildBotFingerprint()
|
|
const res = await fetch(`/forum/topic/${topicKey}/reply`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken,
|
|
'X-Bot-Fingerprint': fingerprint,
|
|
'X-Captcha-Token': captchaToken,
|
|
'Accept': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({
|
|
content: content.trim(),
|
|
homepage_url: '',
|
|
_bot_fingerprint: fingerprint,
|
|
[captcha.inputName || 'cf-turnstile-response']: captchaToken,
|
|
}),
|
|
})
|
|
|
|
if (res.ok) {
|
|
// Reload page to show new reply
|
|
window.location.reload()
|
|
} else if (res.status === 422) {
|
|
const json = await res.json()
|
|
setError(json.errors?.content?.[0] ?? json.errors?.bot?.[0] ?? json.message ?? 'Validation error.')
|
|
} else {
|
|
const json = await res.json().catch(() => ({}))
|
|
setError(json?.errors?.bot?.[0] ?? json?.message ?? 'Failed to post reply. Please try again.')
|
|
}
|
|
} catch {
|
|
setError('Network error. Please try again.')
|
|
}
|
|
|
|
setSubmitting(false)
|
|
}, [content, topicKey, csrfToken, submitting])
|
|
|
|
return (
|
|
<form
|
|
ref={formRef}
|
|
onSubmit={handleSubmit}
|
|
className="space-y-4 rounded-2xl border border-white/[0.06] bg-nova-800/50 p-5 backdrop-blur"
|
|
>
|
|
{quotedAuthor && (
|
|
<p className="text-xs text-cyan-400/70">
|
|
Replying with quote from <strong className="text-cyan-300">{quotedAuthor}</strong>
|
|
</p>
|
|
)}
|
|
|
|
{/* Rich text editor */}
|
|
<RichTextEditor
|
|
content={content}
|
|
onChange={setContent}
|
|
placeholder="Write your reply…"
|
|
error={error}
|
|
minHeight={10}
|
|
/>
|
|
|
|
{/* Submit */}
|
|
{captcha.siteKey ? (
|
|
<TurnstileField
|
|
provider={captcha.provider}
|
|
siteKey={captcha.siteKey}
|
|
scriptUrl={captcha.scriptUrl}
|
|
onToken={setCaptchaToken}
|
|
className="rounded-lg border border-white/10 bg-black/20 p-3"
|
|
/>
|
|
) : null}
|
|
|
|
<div className="flex items-center justify-end">
|
|
<Button
|
|
type="submit"
|
|
variant="primary"
|
|
size="md"
|
|
loading={submitting}
|
|
disabled={content.trim().length < 2}
|
|
>
|
|
Post reply
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|