simple chat creation

This commit is contained in:
Stepan Usatiuk
2023-12-22 14:01:03 +01:00
parent 9d38035205
commit 9910f38ec5
11 changed files with 186 additions and 12 deletions

View File

@@ -13,20 +13,26 @@ import { Home } from "./Home";
import {
homeAction,
loginAction,
newChatAction,
profileSelfAction,
signupAction,
userListAction,
} from "./actions";
import {
chatListLoader,
chatLoader,
feedLoader,
homeLoader,
newChatLoader,
profileLoader,
userListLoader,
} from "./loaders";
import { Feed } from "./Feed";
import { Messages } from "./Messages";
import { Profile } from "./Profile";
import { UserList } from "./UserList";
import { Chats } from "./Chats";
import { ChatCreate } from "./ChatCreate";
import { Chat } from "./Chat";
const router = createBrowserRouter([
{
@@ -46,7 +52,23 @@ const router = createBrowserRouter([
element: <Home />,
children: [
{ path: "feed", element: <Feed />, loader: feedLoader },
{ path: "messages", element: <Messages /> },
// { path: "messages", element: <Messages /> },
{
path: "messages/chats",
element: <Chats />,
loader: chatListLoader,
},
{
path: "messages/chats/new",
element: <ChatCreate />,
loader: newChatLoader,
action: newChatAction,
},
{
path: "messages/chat/:id",
element: <Chat />,
loader: chatLoader,
},
{
path: "users",
element: <UserList />,

12
client/src/Chat.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { useLoaderData } from "react-router-dom";
import { chatLoader, LoaderToType } from "./loaders";
import { isError } from "./api/dto";
export function Chat() {
const loaderData = useLoaderData() as LoaderToType<typeof chatLoader>;
if (!loaderData || isError(loaderData)) {
return <div>error</div>;
}
return <div className={"chat"}>{loaderData.name}</div>;
}

34
client/src/ChatCreate.tsx Normal file
View File

@@ -0,0 +1,34 @@
import { Form, useLoaderData, useNavigation } from "react-router-dom";
import { LoaderToType, newChatLoader } from "./loaders";
import { isError } from "./api/dto";
import { getTokenUserUuid } from "./api/utils";
export function ChatCreate() {
const loaderData = useLoaderData() as LoaderToType<typeof newChatLoader>;
if (!loaderData || isError(loaderData)) {
return <div>error</div>;
}
const navigation = useNavigation();
const busy = navigation.state === "submitting";
return (
<div className={"chatsNew"}>
<Form method="post">
<label htmlFor="fname">name:</label>
<input type="text" name="name" />
<select multiple name={"members"}>
{loaderData
.filter((p) => p.uuid != getTokenUserUuid())
.map((p) => (
<option value={p.uuid}>{p.username}</option>
))}
</select>
<button type="submit" disabled={busy}>
Create
</button>
</Form>
</div>
);
}

3
client/src/Chats.scss Normal file
View File

@@ -0,0 +1,3 @@
.chats {
}

27
client/src/Chats.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { Link, useLoaderData } from "react-router-dom";
import { chatListLoader, LoaderToType } from "./loaders";
import { isError } from "./api/dto";
import "./Chats.scss";
export function Chats() {
const loaderData = useLoaderData() as LoaderToType<typeof chatListLoader>;
if (!loaderData || isError(loaderData)) {
return <div>error</div>;
}
return (
<div className={"chats"}>
<div className={"chatsHeader"}>
<Link to={"../messages/chats/new"}>create</Link>
</div>
<div className={"chatsList"}>
{loaderData.map((c) => (
<Link to={"../messages/chat/" + c.id} key={c.id}>
{c.name}
</Link>
))}
</div>
</div>
);
}

View File

@@ -41,7 +41,10 @@ export function Home() {
<NavLink to={"feed"} className={activePendingClassName}>
Feed
</NavLink>{" "}
<NavLink to={"messages"} className={activePendingClassName}>
<NavLink
to={"messages/chats"}
className={activePendingClassName}
>
Messages
</NavLink>
<NavLink to={"users"} className={activePendingClassName}>

View File

@@ -2,8 +2,9 @@ import { addFollower, removeFollower, signup } from "./api/Person";
import { ActionFunctionArgs, redirect } from "react-router-dom";
import { login } from "./api/Token";
import { isError } from "./api/dto";
import { deleteToken, setToken } from "./api/utils";
import { deleteToken, getTokenUserUuid, setToken } from "./api/utils";
import { createPost, deletePost, updatePost } from "./api/Post";
import { createChat } from "./api/Chat";
export type ActionToType<T extends (...args: any) => any> =
| Exclude<Awaited<ReturnType<T>>, Response>
@@ -85,3 +86,11 @@ export async function userListAction({ request }: ActionFunctionArgs) {
return await removeFollower(formData.get("uuid")!.toString());
}
}
export async function newChatAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
return await createChat(formData.get("name")!.toString(), [
...formData.getAll("members")!.map((p) => p.toString()),
getTokenUserUuid()!,
]);
}

17
client/src/api/Chat.ts Normal file
View File

@@ -0,0 +1,17 @@
import { fetchJSONAuth } from "./utils";
import { ChatsToResp, ChatToResp, TChatsToResp, TChatToResp } from "./dto";
export async function getMyChats(): Promise<TChatsToResp> {
return fetchJSONAuth("/chat/my", "GET", ChatsToResp);
}
export async function getChat(id: number): Promise<TChatToResp> {
return fetchJSONAuth("/chat/by-id/" + id, "GET", ChatToResp);
}
export async function createChat(
name: string,
memberUuids: string[],
): Promise<TChatToResp> {
return fetchJSONAuth("/chat", "POST", ChatToResp, { name, memberUuids });
}

View File

@@ -1,4 +1,4 @@
import { z } from "zod";
import { number, z } from "zod";
export const ErrorTo = z.object({
errors: z.array(z.string()),
@@ -6,6 +6,10 @@ export const ErrorTo = z.object({
});
export type TErrorTo = z.infer<typeof ErrorTo>;
export function isError(value: unknown): value is TErrorTo {
return ErrorTo.safeParse(value).success;
}
function CreateAPIResponse<T extends z.ZodTypeAny>(obj: T) {
return z.union([ErrorTo, obj]);
}
@@ -71,6 +75,28 @@ export type TPostToResp = z.infer<typeof PostToResp>;
export const PostToArrResp = CreateAPIResponse(PostToArr);
export type TPostToArrResp = z.infer<typeof PostToArrResp>;
export function isError(value: unknown): value is TErrorTo {
return ErrorTo.safeParse(value).success;
}
export const MessageTo = z.object({
id: number(),
chatId: z.number(),
authorUuid: z.string(),
contents: z.string(),
});
export type TMessageTo = z.infer<typeof MessageTo>;
export const MessageToResp = CreateAPIResponse(MessageTo);
export type TMessageToResp = z.infer<typeof MessageToResp>;
export const ChatTo = z.object({
id: z.number(),
name: z.string(),
creatorUuid: z.string(),
members: z.array(PersonTo),
messages: z.array(MessageTo),
});
export type TChatTo = z.infer<typeof ChatTo>;
export const ChatToResp = CreateAPIResponse(ChatTo);
export type TChatToResp = z.infer<typeof ChatToResp>;
export const ChatsToResp = CreateAPIResponse(z.array(ChatTo));
export type TChatsToResp = z.infer<typeof ChatsToResp>;

View File

@@ -12,6 +12,7 @@ import {
getPostsByAuthorUuid,
getPostsByFollowees,
} from "./api/Post";
import { getChat, getMyChats } from "./api/Chat";
export type LoaderToType<T extends (...args: any) => any> =
| Exclude<Awaited<ReturnType<T>>, Response>
@@ -75,3 +76,15 @@ export async function profileLoader({
export async function feedLoader() {
return await getPostsByFollowees();
}
export async function chatListLoader() {
return await getMyChats();
}
export async function newChatLoader() {
return await getAllPerson();
}
export async function chatLoader({ params }: { params: { id?: number } }) {
return getChat(params.id!);
}