messages implemented

This commit is contained in:
2026-02-26 21:12:32 +01:00
parent d0aefc5ddc
commit 15b7b77d20
168 changed files with 14728 additions and 6786 deletions

View File

@@ -0,0 +1,158 @@
import React, { useCallback, useRef, useState } from 'react'
import axios from 'axios'
import EmojiPickerButton from './EmojiPickerButton'
/**
* Comment form with emoji picker and Markdown-lite support.
*
* Props:
* artworkId number Target artwork
* onPosted (comment) => void Called when comment is successfully posted
* isLoggedIn boolean
* loginUrl string Where to redirect non-authenticated users
*/
export default function CommentForm({
artworkId,
onPosted,
isLoggedIn = false,
loginUrl = '/login',
}) {
const [content, setContent] = useState('')
const [submitting, setSubmitting] = useState(false)
const [errors, setErrors] = useState([])
const textareaRef = useRef(null)
// Insert text at current cursor position
const insertAtCursor = useCallback((text) => {
const el = textareaRef.current
if (!el) {
setContent((v) => v + text)
return
}
const start = el.selectionStart ?? content.length
const end = el.selectionEnd ?? content.length
const next = content.slice(0, start) + text + content.slice(end)
setContent(next)
// Restore cursor after the inserted text
requestAnimationFrame(() => {
el.selectionStart = start + text.length
el.selectionEnd = start + text.length
el.focus()
})
}, [content])
const handleEmojiSelect = useCallback((emoji) => {
insertAtCursor(emoji)
}, [insertAtCursor])
const handleSubmit = useCallback(
async (e) => {
e.preventDefault()
if (!isLoggedIn) {
window.location.href = loginUrl
return
}
const trimmed = content.trim()
if (!trimmed) return
setSubmitting(true)
setErrors([])
try {
const { data } = await axios.post(`/api/artworks/${artworkId}/comments`, {
content: trimmed,
})
setContent('')
onPosted?.(data.data)
} catch (err) {
if (err.response?.status === 422) {
const apiErrors = err.response.data?.errors?.content ?? ['Invalid content.']
setErrors(Array.isArray(apiErrors) ? apiErrors : [apiErrors])
} else {
setErrors(['Something went wrong. Please try again.'])
}
} finally {
setSubmitting(false)
}
},
[artworkId, content, isLoggedIn, loginUrl, onPosted],
)
if (!isLoggedIn) {
return (
<div className="rounded-xl border border-white/[0.08] bg-white/[0.02] px-5 py-4 text-sm text-white/50">
<a href={loginUrl} className="text-sky-400 hover:text-sky-300 font-medium transition-colors">
Sign in
</a>{' '}
to leave a comment.
</div>
)
}
return (
<form onSubmit={handleSubmit} className="space-y-2">
{/* Textarea */}
<div className="relative rounded-xl border border-white/[0.1] bg-white/[0.03] focus-within:border-white/[0.2] focus-within:bg-white/[0.05] transition-colors">
<textarea
ref={textareaRef}
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Write a comment… Markdown supported: **bold**, *italic*, `code`"
rows={3}
maxLength={10000}
disabled={submitting}
aria-label="Comment text"
className="w-full resize-none bg-transparent px-4 pt-3 pb-10 text-sm text-white placeholder-white/25 focus:outline-none disabled:opacity-50"
/>
{/* Toolbar at bottom-right of textarea */}
<div className="absolute bottom-2 right-3 flex items-center gap-2">
<span
className={[
'text-xs tabular-nums transition-colors',
content.length > 9000 ? 'text-amber-400' : 'text-white/20',
].join(' ')}
aria-live="polite"
>
{content.length}/10 000
</span>
<EmojiPickerButton onEmojiSelect={handleEmojiSelect} disabled={submitting} />
</div>
</div>
{/* Markdown hint */}
<p className="text-xs text-white/25 px-1">
**bold** · *italic* · `code` · https://links.auto-linked · @mentions
</p>
{/* Errors */}
{errors.length > 0 && (
<ul className="space-y-1" role="alert">
{errors.map((e, i) => (
<li key={i} className="text-xs text-red-400 px-1">
{e}
</li>
))}
</ul>
)}
{/* Submit */}
<div className="flex justify-end">
<button
type="submit"
disabled={submitting || !content.trim()}
className="px-5 py-2 rounded-lg text-sm font-medium bg-sky-600 hover:bg-sky-500 text-white transition-colors disabled:opacity-40 disabled:pointer-events-none focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-400"
>
{submitting ? 'Posting…' : 'Post comment'}
</button>
</div>
</form>
)
}