From 9910f38ec5559884ef2c6304cb821c63dc6cd862 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Fri, 22 Dec 2023 14:01:03 +0100 Subject: [PATCH] simple chat creation --- client/src/App.tsx | 26 ++++++++++++-- client/src/Chat.tsx | 12 +++++++ client/src/ChatCreate.tsx | 34 +++++++++++++++++++ client/src/Chats.scss | 3 ++ client/src/Chats.tsx | 27 +++++++++++++++ client/src/Home.tsx | 5 ++- client/src/actions.ts | 11 +++++- client/src/api/Chat.ts | 17 ++++++++++ client/src/api/dto.ts | 34 ++++++++++++++++--- client/src/loaders.ts | 13 +++++++ .../controller/ApiExceptionHandler.java | 16 ++++++--- 11 files changed, 186 insertions(+), 12 deletions(-) create mode 100644 client/src/Chat.tsx create mode 100644 client/src/ChatCreate.tsx create mode 100644 client/src/Chats.scss create mode 100644 client/src/Chats.tsx create mode 100644 client/src/api/Chat.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 24b20bd..2589743 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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: , children: [ { path: "feed", element: , loader: feedLoader }, - { path: "messages", element: }, + // { path: "messages", element: }, + { + path: "messages/chats", + element: , + loader: chatListLoader, + }, + { + path: "messages/chats/new", + element: , + loader: newChatLoader, + action: newChatAction, + }, + { + path: "messages/chat/:id", + element: , + loader: chatLoader, + }, { path: "users", element: , diff --git a/client/src/Chat.tsx b/client/src/Chat.tsx new file mode 100644 index 0000000..ace3b6f --- /dev/null +++ b/client/src/Chat.tsx @@ -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; + if (!loaderData || isError(loaderData)) { + return
error
; + } + + return
{loaderData.name}
; +} diff --git a/client/src/ChatCreate.tsx b/client/src/ChatCreate.tsx new file mode 100644 index 0000000..f569b69 --- /dev/null +++ b/client/src/ChatCreate.tsx @@ -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; + if (!loaderData || isError(loaderData)) { + return
error
; + } + + const navigation = useNavigation(); + const busy = navigation.state === "submitting"; + + return ( +
+
+ + + + + +
+
+ ); +} diff --git a/client/src/Chats.scss b/client/src/Chats.scss new file mode 100644 index 0000000..b90effc --- /dev/null +++ b/client/src/Chats.scss @@ -0,0 +1,3 @@ +.chats { + +} \ No newline at end of file diff --git a/client/src/Chats.tsx b/client/src/Chats.tsx new file mode 100644 index 0000000..3103c60 --- /dev/null +++ b/client/src/Chats.tsx @@ -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; + if (!loaderData || isError(loaderData)) { + return
error
; + } + + return ( +
+
+ create +
+
+ {loaderData.map((c) => ( + + {c.name} + + ))} +
+
+ ); +} diff --git a/client/src/Home.tsx b/client/src/Home.tsx index 61d33c4..127ca3c 100644 --- a/client/src/Home.tsx +++ b/client/src/Home.tsx @@ -41,7 +41,10 @@ export function Home() { Feed {" "} - + Messages diff --git a/client/src/actions.ts b/client/src/actions.ts index bbebb0f..8b27cd9 100644 --- a/client/src/actions.ts +++ b/client/src/actions.ts @@ -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 any> = | Exclude>, 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()!, + ]); +} diff --git a/client/src/api/Chat.ts b/client/src/api/Chat.ts new file mode 100644 index 0000000..a2028eb --- /dev/null +++ b/client/src/api/Chat.ts @@ -0,0 +1,17 @@ +import { fetchJSONAuth } from "./utils"; +import { ChatsToResp, ChatToResp, TChatsToResp, TChatToResp } from "./dto"; + +export async function getMyChats(): Promise { + return fetchJSONAuth("/chat/my", "GET", ChatsToResp); +} + +export async function getChat(id: number): Promise { + return fetchJSONAuth("/chat/by-id/" + id, "GET", ChatToResp); +} + +export async function createChat( + name: string, + memberUuids: string[], +): Promise { + return fetchJSONAuth("/chat", "POST", ChatToResp, { name, memberUuids }); +} diff --git a/client/src/api/dto.ts b/client/src/api/dto.ts index 5dbe03c..7963866 100644 --- a/client/src/api/dto.ts +++ b/client/src/api/dto.ts @@ -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; +export function isError(value: unknown): value is TErrorTo { + return ErrorTo.safeParse(value).success; +} + function CreateAPIResponse(obj: T) { return z.union([ErrorTo, obj]); } @@ -71,6 +75,28 @@ export type TPostToResp = z.infer; export const PostToArrResp = CreateAPIResponse(PostToArr); export type TPostToArrResp = z.infer; -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; + +export const MessageToResp = CreateAPIResponse(MessageTo); +export type TMessageToResp = z.infer; + +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; + +export const ChatToResp = CreateAPIResponse(ChatTo); +export type TChatToResp = z.infer; + +export const ChatsToResp = CreateAPIResponse(z.array(ChatTo)); +export type TChatsToResp = z.infer; diff --git a/client/src/loaders.ts b/client/src/loaders.ts index 453f97a..6cb306b 100644 --- a/client/src/loaders.ts +++ b/client/src/loaders.ts @@ -12,6 +12,7 @@ import { getPostsByAuthorUuid, getPostsByFollowees, } from "./api/Post"; +import { getChat, getMyChats } from "./api/Chat"; export type LoaderToType any> = | Exclude>, 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!); +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/ApiExceptionHandler.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/ApiExceptionHandler.java index 69b68f7..5fce002 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/controller/ApiExceptionHandler.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/ApiExceptionHandler.java @@ -7,6 +7,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; @@ -18,28 +19,35 @@ import java.util.Objects; @ControllerAdvice public class ApiExceptionHandler extends ResponseEntityExceptionHandler { - @ExceptionHandler(value = {ConstraintViolationException.class}) + @ExceptionHandler(ConstraintViolationException.class) protected ResponseEntity handleConstraintViolation(ConstraintViolationException ex, WebRequest request) { return handleExceptionInternal(ex, new ErrorTo(ex.getConstraintViolations().stream().map(ConstraintViolation::getMessage), HttpStatus.BAD_REQUEST.value()), new HttpHeaders(), HttpStatus.BAD_REQUEST, request); } - @ExceptionHandler(value = {AuthenticationException.class}) + @ExceptionHandler(AuthenticationException.class) protected ResponseEntity handleAuthenticationException(AuthenticationException ex, WebRequest request) { return handleExceptionInternal(ex, new ErrorTo(List.of(ex.getMessage()), HttpStatus.UNAUTHORIZED.value()), new HttpHeaders(), HttpStatus.UNAUTHORIZED, request); } - @ExceptionHandler(value = {ResponseStatusException.class}) + @ExceptionHandler(UsernameNotFoundException.class) + protected ResponseEntity handleUsernameNotFoundException(UsernameNotFoundException ex, WebRequest request) { + return handleExceptionInternal(ex, + new ErrorTo(List.of(ex.getMessage()), HttpStatus.UNAUTHORIZED.value()), + new HttpHeaders(), HttpStatus.UNAUTHORIZED, request); + } + + @ExceptionHandler(ResponseStatusException.class) protected ResponseEntity handleResponseStatusException(ResponseStatusException ex, WebRequest request) { return handleExceptionInternal(ex, new ErrorTo(List.of(Objects.requireNonNullElse(ex.getReason(), ex.getStatusCode().toString())), ex.getStatusCode().value()), new HttpHeaders(), ex.getStatusCode(), request); } - @ExceptionHandler(value = {Exception.class}) + @ExceptionHandler(Exception.class) protected ResponseEntity handleGenericException(Exception ex, WebRequest request) { return handleExceptionInternal(ex, new ErrorTo(List.of("Error"), HttpStatus.INTERNAL_SERVER_ERROR.value()),