migrate to blitzjs
This commit is contained in:
82
app/messages/components/conversation.tsx
Normal file
82
app/messages/components/conversation.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { Suspense, useEffect, useRef } from "react"
|
||||
import { useRouter } from "blitz"
|
||||
import clsx from "clsx"
|
||||
|
||||
import { Direction } from "../../../db"
|
||||
import useConversation from "../hooks/use-conversation"
|
||||
import NewMessageArea from "./new-message-area"
|
||||
|
||||
export default function Conversation() {
|
||||
const router = useRouter()
|
||||
const conversation = useConversation(router.params.recipient)[0]
|
||||
const messagesListRef = useRef<HTMLUListElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
messagesListRef.current?.querySelector("li:last-child")?.scrollIntoView()
|
||||
}, [conversation, messagesListRef])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col space-y-6 p-6 pt-12 pb-16">
|
||||
<ul ref={messagesListRef}>
|
||||
{conversation.map((message, index) => {
|
||||
const isOutbound = message.direction === Direction.Outbound
|
||||
const nextMessage = conversation![index + 1]
|
||||
const previousMessage = conversation![index - 1]
|
||||
const isSameNext = message.from === nextMessage?.from
|
||||
const isSamePrevious = message.from === previousMessage?.from
|
||||
const differenceInMinutes = previousMessage
|
||||
? (new Date(message.sentAt).getTime() -
|
||||
new Date(previousMessage.sentAt).getTime()) /
|
||||
1000 /
|
||||
60
|
||||
: 0
|
||||
const isTooLate = differenceInMinutes > 15
|
||||
return (
|
||||
<li key={message.id}>
|
||||
{(!isSamePrevious || isTooLate) && (
|
||||
<div className="flex py-2 space-x-1 text-xs justify-center">
|
||||
<strong>
|
||||
{new Date(message.sentAt).toLocaleDateString("fr-FR", {
|
||||
weekday: "long",
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
})}
|
||||
</strong>
|
||||
<span>
|
||||
{new Date(message.sentAt).toLocaleTimeString("fr-FR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
isSameNext ? "pb-1" : "pb-2",
|
||||
isOutbound ? "text-right" : "text-left"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
"inline-block text-left w-[fit-content] p-2 rounded-lg text-white",
|
||||
isOutbound
|
||||
? "bg-[#3194ff] rounded-br-none"
|
||||
: "bg-black rounded-bl-none"
|
||||
)}
|
||||
>
|
||||
{message.content}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<Suspense fallback={null}>
|
||||
<NewMessageArea />
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
34
app/messages/components/conversations-list.tsx
Normal file
34
app/messages/components/conversations-list.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Link, useQuery } from "blitz"
|
||||
|
||||
import getConversationsQuery from "../queries/get-conversations"
|
||||
|
||||
export default function ConversationsList() {
|
||||
const conversations = useQuery(getConversationsQuery, {})[0]
|
||||
|
||||
if (Object.keys(conversations).length === 0) {
|
||||
return <div>empty state</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="divide-y">
|
||||
{Object.entries(conversations).map(([recipient, messages]) => {
|
||||
const lastMessage = messages[messages.length - 1]!
|
||||
return (
|
||||
<li key={recipient} className="py-2">
|
||||
<Link href={`/messages/${encodeURIComponent(recipient)}`}>
|
||||
<a className="flex flex-col">
|
||||
<div className="flex flex-row justify-between">
|
||||
<strong>{recipient}</strong>
|
||||
<div>
|
||||
{new Date(lastMessage.sentAt).toLocaleString("fr-FR")}
|
||||
</div>
|
||||
</div>
|
||||
<div>{lastMessage.content}</div>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
94
app/messages/components/new-message-area.tsx
Normal file
94
app/messages/components/new-message-area.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faPaperPlane } from "@fortawesome/pro-regular-svg-icons"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useMutation, useQuery, useRouter } from "blitz"
|
||||
|
||||
import sendMessage from "../mutations/send-message"
|
||||
import { Direction, Message, MessageStatus } from "../../../db"
|
||||
import getConversationsQuery from "../queries/get-conversations"
|
||||
import useCurrentCustomer from "../../core/hooks/use-current-customer"
|
||||
import useCustomerPhoneNumber from "../../core/hooks/use-customer-phone-number"
|
||||
|
||||
type Form = {
|
||||
content: string
|
||||
}
|
||||
|
||||
export default function NewMessageArea() {
|
||||
const router = useRouter()
|
||||
const recipient = router.params.recipient
|
||||
const { customer } = useCurrentCustomer()
|
||||
const phoneNumber = useCustomerPhoneNumber()
|
||||
const sendMessageMutation = useMutation(sendMessage)[0]
|
||||
const { setQueryData: setConversationsQueryData, refetch: refetchConversations } = useQuery(
|
||||
getConversationsQuery,
|
||||
{}
|
||||
)[1]
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<Form>()
|
||||
const onSubmit = handleSubmit(async ({ content }) => {
|
||||
if (isSubmitting) {
|
||||
return
|
||||
}
|
||||
|
||||
const id = uuidv4()
|
||||
const message: Message = {
|
||||
id,
|
||||
customerId: customer!.id,
|
||||
twilioSid: id,
|
||||
from: phoneNumber!.phoneNumber,
|
||||
to: recipient,
|
||||
content: content,
|
||||
direction: Direction.Outbound,
|
||||
status: MessageStatus.Queued,
|
||||
sentAt: new Date(),
|
||||
}
|
||||
|
||||
await setConversationsQueryData(
|
||||
(conversations) => {
|
||||
const nextConversations = { ...conversations }
|
||||
if (!nextConversations[recipient]) {
|
||||
nextConversations[recipient] = []
|
||||
}
|
||||
|
||||
nextConversations[recipient] = [...nextConversations[recipient]!, message]
|
||||
return nextConversations
|
||||
},
|
||||
{ refetch: false }
|
||||
)
|
||||
setValue("content", "")
|
||||
await sendMessageMutation({ to: recipient, content })
|
||||
await refetchConversations({ cancelRefetch: true })
|
||||
})
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="absolute bottom-0 w-screen backdrop-filter backdrop-blur-xl bg-white bg-opacity-75 border-t flex flex-row h-16 p-2 pr-0"
|
||||
>
|
||||
<textarea
|
||||
className="resize-none flex-1"
|
||||
autoCapitalize="sentences"
|
||||
autoCorrect="on"
|
||||
placeholder="Text message"
|
||||
rows={1}
|
||||
spellCheck
|
||||
{...register("content", { required: true })}
|
||||
/>
|
||||
<button type="submit">
|
||||
<FontAwesomeIcon size="2x" className="h-8 w-8 pl-1 pr-2" icon={faPaperPlane} />
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function uuidv4() {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
const r = (Math.random() * 16) | 0,
|
||||
v = c == "x" ? r : (r & 0x3) | 0x8
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user