Files
SkinbaseNova/resources/js/components/messaging/NewConversationModal.jsx

183 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useCallback } from 'react'
export default function NewConversationModal({ currentUserId, apiFetch, onCreated, onClose }) {
const [type, setType] = useState('direct')
const [recipientInput, setRecipient] = useState('')
const [groupTitle, setGroupTitle] = useState('')
const [participantInputs, setParticipantInputs] = useState(['', ''])
const [body, setBody] = useState('')
const [sending, setSending] = useState(false)
const [error, setError] = useState(null)
const addParticipant = () => setParticipantInputs((prev) => [...prev, ''])
const updateParticipant = (index, value) => setParticipantInputs((prev) => prev.map((entry, currentIndex) => (currentIndex === index ? value : entry)))
const removeParticipant = (index) => setParticipantInputs((prev) => prev.filter((_, currentIndex) => currentIndex !== index))
const handleSubmit = useCallback(async (e) => {
e.preventDefault()
setError(null)
setSending(true)
try {
const payload = { type, body }
if (type === 'direct') {
const user = await resolveUsername(recipientInput.trim(), apiFetch)
payload.recipient_id = user.id
} else {
const resolved = await Promise.all(
participantInputs
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => resolveUsername(entry, apiFetch)),
)
payload.participant_ids = resolved.map((user) => user.id)
payload.title = groupTitle.trim()
}
const conv = await apiFetch('/api/messages/conversation', {
method: 'POST',
body: JSON.stringify(payload),
})
onCreated(conv)
} catch (e) {
setError(e.message)
} finally {
setSending(false)
}
}, [apiFetch, body, groupTitle, onCreated, participantInputs, recipientInput, type])
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[#020611cc] p-4 backdrop-blur-md">
<div className="w-full max-w-xl overflow-hidden rounded-[32px] border border-white/[0.08] bg-[linear-gradient(180deg,rgba(12,18,29,0.98),rgba(7,11,18,0.96))] shadow-[0_30px_80px_rgba(0,0,0,0.55)]">
<div className="border-b border-white/[0.06] bg-white/[0.02] px-6 py-5">
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-white/35">Compose</p>
<h2 className="mt-2 text-xl font-semibold text-white">Start a new conversation</h2>
<p className="mt-2 text-sm leading-6 text-white/50">Send a direct message or set up a group thread with collaborators.</p>
</div>
<button onClick={onClose} className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.04] text-white/55 transition hover:bg-white/[0.08] hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-5 px-6 py-6">
<div className="grid gap-3 sm:grid-cols-2">
{['direct', 'group'].map((entryType) => (
<button
key={entryType}
type="button"
onClick={() => setType(entryType)}
className={`rounded-[24px] border px-4 py-4 text-left transition ${type === entryType ? 'border-sky-400/28 bg-sky-500/[0.12] text-sky-100' : 'border-white/[0.08] bg-white/[0.03] text-white/65 hover:bg-white/[0.05] hover:text-white'}`}
>
<div className="flex items-center gap-3">
<span className={`inline-flex h-11 w-11 items-center justify-center rounded-2xl ${entryType === 'direct' ? 'bg-sky-500/14 text-sky-200' : 'bg-fuchsia-500/14 text-fuchsia-200'}`}>
<i className={`fa-solid ${entryType === 'direct' ? 'fa-user' : 'fa-user-group'}`} />
</span>
<div>
<p className="text-sm font-semibold">{entryType === 'direct' ? 'Direct message' : 'Group conversation'}</p>
<p className="mt-1 text-xs text-white/40">{entryType === 'direct' ? 'One creator, one thread.' : 'Coordinate with multiple people.'}</p>
</div>
</div>
</button>
))}
</div>
{type === 'direct' ? (
<div>
<label className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.18em] text-white/35">Recipient username</label>
<input
type="text"
value={recipientInput}
onChange={(e) => setRecipient(e.target.value)}
placeholder="username"
required
className="w-full rounded-2xl border border-white/[0.08] bg-black/15 px-4 py-3 text-sm text-white outline-none transition placeholder:text-white/25 focus:border-sky-400/30 focus:bg-black/25"
/>
</div>
) : (
<>
<div>
<label className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.18em] text-white/35">Group name</label>
<input
type="text"
value={groupTitle}
onChange={(e) => setGroupTitle(e.target.value)}
placeholder="Group name"
required
maxLength={120}
className="w-full rounded-2xl border border-white/[0.08] bg-black/15 px-4 py-3 text-sm text-white outline-none transition placeholder:text-white/25 focus:border-sky-400/30 focus:bg-black/25"
/>
</div>
<div>
<div className="mb-2 flex items-center justify-between gap-3">
<label className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-white/35">Participants</label>
<button type="button" onClick={addParticipant} className="text-xs font-medium text-sky-300 transition hover:text-sky-200">
+ Add participant
</button>
</div>
{participantInputs.map((value, index) => (
<div key={index} className="mb-2 flex gap-2">
<input
type="text"
value={value}
onChange={(e) => updateParticipant(index, e.target.value)}
placeholder={`Username ${index + 1}`}
required
className="flex-1 rounded-2xl border border-white/[0.08] bg-black/15 px-4 py-3 text-sm text-white outline-none transition placeholder:text-white/25 focus:border-sky-400/30 focus:bg-black/25"
/>
{participantInputs.length > 2 ? (
<button type="button" onClick={() => removeParticipant(index)} className="inline-flex h-12 w-12 items-center justify-center rounded-2xl border border-white/[0.08] bg-white/[0.04] text-white/45 transition hover:border-rose-400/18 hover:bg-rose-500/10 hover:text-rose-200">×</button>
) : null}
</div>
))}
</div>
</>
)}
<div>
<label className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.18em] text-white/35">Opening message</label>
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="Write your message…"
required
rows={3}
maxLength={5000}
className="w-full resize-none rounded-2xl border border-white/[0.08] bg-black/15 px-4 py-3 text-sm text-white outline-none transition placeholder:text-white/25 focus:border-sky-400/30 focus:bg-black/25"
/>
<p className="mt-2 text-[11px] text-white/30">Keep it clear. You can continue the thread after the conversation is created.</p>
</div>
{error ? (
<p className="rounded-2xl border border-rose-500/18 bg-rose-500/10 px-4 py-3 text-sm text-rose-200">{error}</p>
) : null}
<div className="flex justify-end gap-2 border-t border-white/[0.06] pt-5">
<button type="button" onClick={onClose} className="rounded-full border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/65 transition hover:bg-white/[0.08] hover:text-white">Cancel</button>
<button
type="submit"
disabled={sending}
className="rounded-full bg-sky-500 px-5 py-2 text-sm font-semibold text-slate-950 transition hover:bg-sky-400 disabled:opacity-50"
>
{sending ? 'Creating…' : 'Create conversation'}
</button>
</div>
</form>
</div>
</div>
)
}
async function resolveUsername(username, apiFetch) {
const data = await apiFetch(`/api/search/users?q=${encodeURIComponent(username)}&limit=1`)
const user = data?.data?.[0] ?? data?.[0]
if (!user) throw new Error(`User \"${username}\" not found.`)
return user
}