remixed v0

This commit is contained in:
m5r
2022-05-14 12:22:06 +02:00
parent 9275d4499b
commit 98b89ae0f7
338 changed files with 22549 additions and 44628 deletions

View File

@ -0,0 +1,81 @@
import { Suspense, useEffect, useMemo, useRef } from "react";
import { useLoaderData, useParams } from "@remix-run/react";
import clsx from "clsx";
import { Direction } from "@prisma/client";
import NewMessageArea from "./new-message-area";
import { formatDate, formatTime } from "~/features/core/helpers/date-formatter";
import { type ConversationLoaderData } from "~/routes/__app/messages.$recipient";
export default function Conversation() {
const params = useParams<{ recipient: string }>();
const recipient = decodeURIComponent(params.recipient ?? "");
const { conversation } = useLoaderData<ConversationLoaderData>();
const messages = useMemo(() => conversation?.messages ?? [], [conversation?.messages]);
const messagesListRef = useRef<HTMLUListElement>(null);
useEffect(() => {
messagesListRef.current?.querySelector("li:last-child")?.scrollIntoView();
}, [messages, messagesListRef]);
return (
<>
<div className="flex flex-col space-y-6 p-6 pt-12 pb-16">
<ul ref={messagesListRef}>
{messages.length === 0 ? "empty state" : null}
{messages.map((message, index) => {
const isOutbound = message.direction === Direction.Outbound;
const nextMessage = messages![index + 1];
const previousMessage = messages![index - 1];
const isNextMessageFromSameSender = message.from === nextMessage?.from;
const isPreviousMessageFromSameSender = message.from === previousMessage?.from;
const messageSentAt = new Date(message.sentAt);
const previousMessageSentAt = previousMessage ? new Date(previousMessage.sentAt) : null;
const quarter = Math.floor(messageSentAt.getMinutes() / 15);
const sameQuarter =
previousMessage &&
messageSentAt.getTime() - previousMessageSentAt!.getTime() < 15 * 60 * 1000 &&
quarter === Math.floor(previousMessageSentAt!.getMinutes() / 15);
const shouldGroupMessages = previousMessageSentAt && sameQuarter;
return (
<li key={message.id}>
{(!isPreviousMessageFromSameSender || !shouldGroupMessages) && (
<div className="flex py-2 space-x-1 text-xs justify-center">
<strong>
{formatDate(new Date(message.sentAt), {
weekday: "long",
day: "2-digit",
month: "short",
})}
</strong>
<span>{formatTime(new Date(message.sentAt))}</span>
</div>
)}
<div
className={clsx(
isNextMessageFromSameSender ? "pb-1" : "pb-2",
isOutbound ? "text-right" : "text-left",
)}
>
<span
className={clsx(
"inline-block whitespace-pre-line 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 recipient={recipient} />
</Suspense>
</>
);
}

View File

@ -0,0 +1,41 @@
import { Link, useLoaderData } from "@remix-run/react";
import { IoChevronForward } from "react-icons/io5";
import { formatRelativeDate } from "~/features/core/helpers/date-formatter";
import PhoneInitLoader from "~/features/core/components/phone-init-loader";
import EmptyMessages from "./empty-messages";
import type { MessagesLoaderData } from "~/routes/__app/messages";
export default function ConversationsList() {
const { conversations } = useLoaderData<MessagesLoaderData>();
if (!conversations) {
// we're still importing messages from twilio
return <PhoneInitLoader />;
}
if (Object.keys(conversations).length === 0) {
return <EmptyMessages />;
}
return (
<ul className="divide-y">
{Object.values(conversations).map(({ recipient, formattedPhoneNumber, lastMessage }) => {
return (
<li key={`sms-conversation-${recipient}`} className="py-2 px-4">
<Link to={`/messages/${recipient}`} className="flex flex-col">
<div className="flex flex-row justify-between">
<span className="font-medium">{formattedPhoneNumber ?? recipient}</span>
<div className="text-gray-700 flex flex-row gap-x-1">
{formatRelativeDate(lastMessage.sentAt)}
<IoChevronForward className="w-4 h-4 my-auto" />
</div>
</div>
<div className="line-clamp-2 text-gray-700">{lastMessage.content}</div>
</Link>
</li>
);
})}
</ul>
);
}

View File

@ -0,0 +1,32 @@
import { IoCreateOutline, IoMailOutline } from "react-icons/io5";
// import { useAtom } from "jotai";
// import { bottomSheetOpenAtom } from "../pages/messages";
export default function EmptyMessages() {
// const setIsBottomSheetOpen = useAtom(bottomSheetOpenAtom)[1];
// const openNewMessageArea = () => setIsBottomSheetOpen(true);
const openNewMessageArea = () => void 0;
return (
<div className="text-center my-auto">
<IoMailOutline className="mx-auto h-12 w-12 text-gray-400" aria-hidden="true" />
<h3 className="mt-2 text-sm font-medium text-gray-900">You don&#39;t have any messages yet</h3>
<p className="mt-1 text-sm text-gray-500">
Get started by sending a message
<br />
to someone you know.
</p>
<div className="mt-6">
<button
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-500 focus:outline-none focus:ring-2 focus:ring-offset-2"
onClick={openNewMessageArea}
>
<IoCreateOutline className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
Type a new message
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,88 @@
import type { FunctionComponent } from "react";
import { IoSend } from "react-icons/io5";
import { type Message, Direction, MessageStatus } from "@prisma/client";
import useSession from "~/features/core/hooks/use-session";
type Props = {
recipient: string;
onSend?: () => void;
};
const NewMessageArea: FunctionComponent<Props> = ({ recipient, onSend }) => {
const { currentOrganization, /*hasOngoingSubscription*/ } = useSession();
// const phoneNumber = useCurrentPhoneNumber();
// const sendMessageMutation = useMutation(sendMessage)[0];
const onSubmit = async () => {
/*const id = uuidv4();
const message: Message = {
id,
organizationId: organization!.id,
phoneNumberId: phoneNumber!.id,
from: phoneNumber!.number,
to: recipient,
content: hasOngoingSubscription
? content
: content + "\n\nSent from Shellphone (https://www.shellphone.app)",
direction: Direction.Outbound,
status: MessageStatus.Queued,
sentAt: new Date(),
};*/
/*await setConversationsQueryData(
(conversations) => {
const nextConversations = { ...conversations };
if (!nextConversations[recipient]) {
nextConversations[recipient] = {
recipient,
formattedPhoneNumber: recipient,
messages: [],
};
}
nextConversations[recipient]!.messages = [...nextConversations[recipient]!.messages, message];
return Object.fromEntries(
Object.entries(nextConversations).sort(
([, a], [, b]) =>
b.messages[b.messages.length - 1]!.sentAt.getTime() -
a.messages[a.messages.length - 1]!.sentAt.getTime(),
),
);
},
{ refetch: false },
);*/
// setValue("content", "");
// onSend?.();
};
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
name="content"
className="resize-none flex-1"
autoCapitalize="sentences"
autoCorrect="on"
placeholder="Text message"
rows={1}
spellCheck
required
/>
<button type="submit">
<IoSend className="h-8 w-8 pl-1 pr-2" />
</button>
</form>
);
};
export default NewMessageArea;
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);
});
}

View File

@ -0,0 +1,56 @@
import type { LoaderFunction } from "@remix-run/node";
import { type Message, Prisma, Direction, SubscriptionStatus } from "@prisma/client";
import db from "~/utils/db.server";
import { requireLoggedIn } from "~/utils/auth.server";
export type MessagesLoaderData = {
user: {
hasFilledTwilioCredentials: boolean;
hasPhoneNumber: boolean;
};
conversations: Record<string, Conversation> | undefined;
};
type Conversation = {
recipient: string;
formattedPhoneNumber: string;
messages: Message[];
};
const loader: LoaderFunction = async ({ request }) => {
const { id, organizations } = await requireLoggedIn(request);
const user = await db.user.findFirst({
where: { id },
select: {
id: true,
fullName: true,
email: true,
role: true,
memberships: {
include: {
organization: {
include: {
subscriptions: {
where: {
OR: [
{ status: { not: SubscriptionStatus.deleted } },
{
status: SubscriptionStatus.deleted,
cancellationEffectiveDate: { gt: new Date() },
},
],
},
orderBy: { lastEventTime: Prisma.SortOrder.desc },
},
},
},
},
},
},
});
const organization = user!.memberships[0]!.organization;
// const hasFilledTwilioCredentials = Boolean(organization?.twilioAccountSid && organization?.twilioAuthToken);
};
export default loader;