feat: forum rich-text editor, emoji picker, mentions, discover nav, feed, uploads, profile
Forum: - TipTap WYSIWYG editor with full toolbar - @emoji-mart/react emoji picker (consistent with tweets) - @mention autocomplete with user search API - Fix PHP 8.4 parse errors in Blade templates - Fix thread data display (paginator items) - Align forum page widths to max-w-5xl Discover: - Extract shared _nav.blade.php partial - Add missing nav links to for-you page - Add Following link for authenticated users Feed/Posts: - Post model, controllers, policies, migrations - Feed page components (PostComposer, FeedCard, etc) - Post reactions, comments, saves, reports, sharing - Scheduled publishing support - Link preview controller Profile: - Profile page components (ProfileHero, ProfileTabs) - Profile API controller Uploads: - Upload wizard enhancements - Scheduled publish picker - Studio status bar and readiness checklist
This commit is contained in:
82
resources/js/components/forum/ReplyForm.jsx
Normal file
82
resources/js/components/forum/ReplyForm.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useState, useRef, useCallback } from 'react'
|
||||
import Button from '../ui/Button'
|
||||
import RichTextEditor from './RichTextEditor'
|
||||
|
||||
export default function ReplyForm({ threadId, prefill = '', quotedAuthor = null, csrfToken }) {
|
||||
const [content, setContent] = useState(prefill)
|
||||
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 res = await fetch(`/forum/thread/${threadId}/reply`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ content: content.trim() }),
|
||||
})
|
||||
|
||||
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] ?? 'Validation error.')
|
||||
} else {
|
||||
setError('Failed to post reply. Please try again.')
|
||||
}
|
||||
} catch {
|
||||
setError('Network error. Please try again.')
|
||||
}
|
||||
|
||||
setSubmitting(false)
|
||||
}, [content, threadId, 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 */}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user