mirror of
https://github.com/usatiuk/y.git
synced 2025-10-28 10:37:47 +01:00
simple chat creation
This commit is contained in:
@@ -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
12
client/src/Chat.tsx
Normal 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
34
client/src/ChatCreate.tsx
Normal 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
3
client/src/Chats.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.chats {
|
||||
|
||||
}
|
||||
27
client/src/Chats.tsx
Normal file
27
client/src/Chats.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
17
client/src/api/Chat.ts
Normal 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 });
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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!);
|
||||
}
|
||||
|
||||
@@ -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<Object> 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<Object> 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<Object> 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<Object> 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<Object> handleGenericException(Exception ex, WebRequest request) {
|
||||
return handleExceptionInternal(ex,
|
||||
new ErrorTo(List.of("Error"), HttpStatus.INTERNAL_SERVER_ERROR.value()),
|
||||
|
||||
Reference in New Issue
Block a user