FA API via REST closes #586 closes #626

This commit is contained in:
Kalle (Sendou)
2021-09-18 13:48:54 +03:00
parent 2f9f060848
commit 0f3da0e3f8
8 changed files with 134 additions and 110 deletions

View File

@@ -17,16 +17,14 @@ import {
Radio,
RadioGroup,
Stack,
useToast,
} from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { t, Trans } from "@lingui/macro";
import MarkdownTextarea from "components/common/MarkdownTextarea";
import { useMutation } from "hooks/common";
import { FreeAgentsGet } from "pages/api/free-agents";
import { Controller, useForm } from "react-hook-form";
import { FiTrash } from "react-icons/fi";
import { getToastOptions } from "utils/objects";
import { trpc } from "utils/trpc";
import { Unpacked } from "utils/types";
import {
FA_POST_CONTENT_LIMIT,
@@ -40,43 +38,37 @@ interface Props {
post?: Unpacked<FreeAgentsGet>;
}
type FormData = z.infer<typeof freeAgentPostSchema>;
type FreeAgentPostSchemaInput = z.infer<typeof freeAgentPostSchema>;
const FAModal = ({ onClose, post, refetchQuery }: Props) => {
const { handleSubmit, errors, register, watch, control } = useForm<FormData>({
resolver: zodResolver(freeAgentPostSchema),
defaultValues: post,
});
const { handleSubmit, errors, register, watch, control } =
useForm<FreeAgentPostSchemaInput>({
resolver: zodResolver(freeAgentPostSchema),
defaultValues: post,
});
const upsertPostMutation = trpc.useMutation("freeAgents.upsertPost", {
onSuccess() {
toast(
getToastOptions(
post ? t`Free agent post updated` : t`Free agent post submitted`,
"success"
)
);
const upsertFreeAgentPostMutation = useMutation<FreeAgentPostSchemaInput>({
url: "/api/free-agents",
method: "POST",
successToastMsg: post
? t`Free agent post updated`
: t`Free agent post submitted`,
afterSuccess: () => {
refetchQuery();
onClose();
},
onError(error) {
toast(getToastOptions(error.message, "error"));
},
});
const deletePostMutation = trpc.useMutation("freeAgents.deletePost", {
onSuccess() {
toast(getToastOptions(t`Free agent post deleted`, "success"));
const deleteFreeAgentPostMutation = useMutation<FreeAgentPostSchemaInput>({
url: "/api/free-agents",
method: "DELETE",
successToastMsg: t`Free agent post deleted`,
afterSuccess: () => {
refetchQuery();
onClose();
},
onError(error) {
toast(getToastOptions(error.message, "error"));
},
});
const toast = useToast();
const watchContent = watch("content", ""); // TODO: get initial fa content from props
return (
@@ -92,7 +84,9 @@ const FAModal = ({ onClose, post, refetchQuery }: Props) => {
</ModalHeader>
<ModalCloseButton borderRadius="50%" />
<form
onSubmit={handleSubmit((data) => upsertPostMutation.mutate(data))}
onSubmit={handleSubmit((data) =>
upsertFreeAgentPostMutation.mutate(data)
)}
>
<ModalBody pb={6}>
{post && (
@@ -101,10 +95,10 @@ const FAModal = ({ onClose, post, refetchQuery }: Props) => {
leftIcon={<FiTrash />}
variant="outline"
color="red.500"
isLoading={deletePostMutation.isLoading}
isLoading={deleteFreeAgentPostMutation.isMutating}
onClick={async () => {
if (window.confirm(t`Delete the free agent post?`)) {
deletePostMutation.mutate(null);
deleteFreeAgentPostMutation.mutate();
}
}}
>
@@ -185,7 +179,8 @@ const FAModal = ({ onClose, post, refetchQuery }: Props) => {
mr={3}
type="submit"
isLoading={
upsertPostMutation.isLoading || deletePostMutation.isLoading
upsertFreeAgentPostMutation.isMutating ||
deleteFreeAgentPostMutation.isMutating
}
>
<Trans>Save</Trans>
@@ -194,7 +189,8 @@ const FAModal = ({ onClose, post, refetchQuery }: Props) => {
onClick={onClose}
variant="outline"
isDisabled={
upsertPostMutation.isLoading || deletePostMutation.isLoading
upsertFreeAgentPostMutation.isMutating ||
deleteFreeAgentPostMutation.isMutating
}
>
<Trans>Cancel</Trans>

View File

@@ -1,4 +1,4 @@
import { Box, Divider, Flex, IconButton, useToast } from "@chakra-ui/react";
import { Box, Divider, Flex, IconButton } from "@chakra-ui/react";
import { t, Trans } from "@lingui/macro";
import Flag from "components/common/Flag";
import Markdown from "components/common/Markdown";
@@ -18,11 +18,10 @@ import {
RiPaintLine,
RiSwordLine,
} from "react-icons/ri";
import { getToastOptions } from "utils/objects";
import { trpc } from "utils/trpc";
import { Unpacked } from "utils/types";
import { FreeAgentsGet } from "pages/api/free-agents";
import { mutate } from "swr";
import { useMutation } from "hooks/common";
const playstyleToEmoji = {
FRONTLINE: RiSwordLine,
@@ -45,22 +44,19 @@ const FreeAgentSection = ({
showPlusServerMembership: boolean;
postRef?: RefObject<HTMLDivElement>;
}) => {
const toast = useToast();
const addLikeMutation = trpc.useMutation("freeAgents.addLike", {
onSuccess() {
const addLikeMutation = useMutation<{ postId: number }>({
url: "/api/free-agents/likes",
method: "POST",
afterSuccess: () => {
mutate("/api/free-agents/likes");
},
onError(error) {
toast(getToastOptions(error.message, "error"));
},
});
const deleteLikeMutation = trpc.useMutation("freeAgents.deleteLike", {
onSuccess() {
const deleteLikeMutation = useMutation<{ postId: number }>({
url: "/api/free-agents/likes",
method: "DELETE",
afterSuccess: () => {
mutate("/api/free-agents/likes");
},
onError(error) {
toast(getToastOptions(error.message, "error"));
},
});
const handleClick = () =>
@@ -179,7 +175,9 @@ const FreeAgentSection = ({
mt={4}
variant="ghost"
icon={isLiked ? <FaHeart /> : <FaRegHeart />}
disabled={addLikeMutation.isLoading || deleteLikeMutation.isLoading}
disabled={
addLikeMutation.isMutating || deleteLikeMutation.isMutating
}
onClick={handleClick}
/>
)}

View File

@@ -51,7 +51,7 @@ export const useMutation = <T>({
url: string;
method?: "POST" | "DELETE" | "PUT";
data?: T;
successToastMsg: string;
successToastMsg?: string;
afterSuccess?: () => void;
}) => {
const toast = useToast();
@@ -85,7 +85,9 @@ export const useMutation = <T>({
description,
});
} else {
toast(getToastOptions(successToastMsg, "success"));
if (successToastMsg) {
toast(getToastOptions(successToastMsg, "success"));
}
afterSuccess?.();
}
};

View File

@@ -1,7 +1,8 @@
import { NextApiRequest, NextApiResponse } from "next";
import { createHandler } from "utils/api";
import { createHandler, getMySession } from "utils/api";
import freeAgentsService, { PostsData } from "services/freeagents";
import { Serialized } from "utils/types";
import { freeAgentPostSchema } from "utils/validators/fapost";
export type FreeAgentsGet = Serialized<PostsData>;
@@ -10,7 +11,32 @@ const GET = async (_req: NextApiRequest, res: NextApiResponse<PostsData>) => {
res.status(200).json(events);
};
const POST = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getMySession(req);
if (!user) {
return res.status(401).end();
}
const parsed = freeAgentPostSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json(parsed.error);
}
await freeAgentsService.upsertPost({ input: parsed.data, userId: user.id });
res.status(200).end();
};
const DELETE = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getMySession(req);
if (!user) {
return res.status(401).end();
}
await freeAgentsService.deletePost(user.id);
res.status(200).end();
};
const freeAgentsHandler = (req: NextApiRequest, res: NextApiResponse) =>
createHandler(req, res, { GET });
createHandler(req, res, { GET, POST, DELETE });
export default freeAgentsHandler;

View File

@@ -2,6 +2,7 @@ import { NextApiRequest, NextApiResponse } from "next";
import { createHandler, getMySession } from "utils/api";
import freeAgentsService, { LikesData } from "services/freeagents";
import { Serialized } from "utils/types";
import * as z from "zod";
export type LikesGet = Serialized<LikesData>;
@@ -15,7 +16,43 @@ const GET = async (req: NextApiRequest, res: NextApiResponse<LikesData>) => {
res.status(200).json(likes);
};
const POST = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getMySession(req);
if (!user) {
return res.status(401).end();
}
const parsed = z.object({ postId: z.number() }).safeParse(req.body);
if (!parsed.success) {
return res.status(401).end();
}
await freeAgentsService.addLike({
userId: user.id,
postId: parsed.data.postId,
});
res.status(200).end();
};
const DELETE = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getMySession(req);
if (!user) {
return res.status(401).end();
}
const parsed = z.object({ postId: z.number() }).safeParse(req.body);
if (!parsed.success) {
return res.status(401).end();
}
await freeAgentsService.deleteLike({
userId: user.id,
postId: parsed.data.postId,
});
res.status(200).end();
};
const likesHandler = (req: NextApiRequest, res: NextApiResponse) =>
createHandler(req, res, { GET });
createHandler(req, res, { GET, POST, DELETE });
export default likesHandler;

View File

@@ -1,7 +1,6 @@
import * as trpc from "@trpc/server";
import { inferAsyncReturnType, inferProcedureOutput } from "@trpc/server";
import * as trpcNext from "@trpc/server/adapters/next";
import freeAgentsApi from "routers/freeagents";
import plusApi from "routers/plus";
import superjson from "superjson";
import { getMySession } from "utils/api";
@@ -18,9 +17,7 @@ export function createRouter() {
return trpc.router<Context>();
}
// Important: only use this export with SSR/SSG
export const appRouter = createRouter()
.merge("plus.", plusApi)
.merge("freeAgents.", freeAgentsApi);
export const appRouter = createRouter().merge("plus.", plusApi);
// Exporting type _type_ AppRouter only exposes types that can be used for inference
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export

View File

@@ -12,22 +12,24 @@ import FAFilters from "components/freeagents/FAFilters";
import FAModal from "components/freeagents/FAModal";
import FreeAgentSection from "components/freeagents/FreeAgentSection";
import MatchesInfo from "components/freeagents/MatchesInfo";
import { useUser } from "hooks/common";
import { useMutation, useUser } from "hooks/common";
import { useFreeAgents } from "hooks/freeagents";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import { mutate } from "swr";
import { CSSVariables } from "utils/CSSVariables";
import { sendData } from "utils/postData";
import freeAgentsService from "services/freeagents";
import { GetStaticProps } from "next";
import { serializeDataForGetStaticProps } from "utils/objects";
import { FreeAgentsGet } from "./api/free-agents";
import * as z from "zod";
import { freeAgentPostSchema } from "utils/validators/fapost";
interface Props {
postsInitialData: FreeAgentsGet;
}
type FreeAgentPostSchemaInput = z.infer<typeof freeAgentPostSchema>;
const FreeAgentsPage = ({ postsInitialData }: Props) => {
const {
postsData,
@@ -46,13 +48,20 @@ const FreeAgentsPage = ({ postsInitialData }: Props) => {
const [postIdToScrollTo, setPostIdToScrollTo] = useState<undefined | number>(
undefined
);
const [sending, setSending] = useState(false);
const postRef = useRef<HTMLDivElement>(null);
const [modalIsOpen, setModalIsOpen] = useState(false);
const refreshPostMutation = useMutation<FreeAgentPostSchemaInput>({
url: "/api/free-agents",
method: "POST",
successToastMsg: "Free agent post refreshed",
afterSuccess: () => {
refetchPosts();
},
});
useEffect(() => {
if (!postRef.current) {
console.error("no postRef");
return;
}
@@ -62,18 +71,11 @@ const FreeAgentsPage = ({ postsInitialData }: Props) => {
const dateThreeWeeksAgo = new Date();
dateThreeWeeksAgo.setDate(dateThreeWeeksAgo.getDate() - 7 * 3);
const onPostRefresh = async () => {
setSending(true);
const usersPostForRefresh = () => {
if (!usersPost) return;
const { id, user, updatedAt, ...result } = usersPost;
const success = await sendData("PUT", "/api/freeagents", {
canVC: usersPost!.canVC,
playstyles: usersPost!.playstyles,
content: usersPost!.content,
});
setSending(false);
if (!success) return;
mutate("/api/freeagents");
return result;
};
return (
@@ -122,8 +124,10 @@ const FreeAgentsPage = ({ postsInitialData }: Props) => {
<Button
mt={4}
variant="outline"
onClick={onPostRefresh}
isLoading={sending}
onClick={() =>
refreshPostMutation.mutate(usersPostForRefresh())
}
isLoading={refreshPostMutation.isMutating}
>
<Trans>I&apos;m still a free agent</Trans>
</Button>

View File

@@ -1,36 +0,0 @@
import { createRouter } from "pages/api/trpc/[trpc]";
import service from "services/freeagents";
import { throwIfNotLoggedIn } from "utils/api";
import { freeAgentPostSchema } from "utils/validators/fapost";
import * as z from "zod";
const freeAgentsApi = createRouter()
.mutation("upsertPost", {
input: freeAgentPostSchema,
resolve({ ctx, input }) {
const user = throwIfNotLoggedIn(ctx.user);
return service.upsertPost({ input, userId: user.id });
},
})
.mutation("deletePost", {
resolve({ ctx }) {
const user = throwIfNotLoggedIn(ctx.user);
return service.deletePost(user.id);
},
})
.mutation("addLike", {
input: z.object({ postId: z.number() }),
resolve({ ctx, input }) {
const user = throwIfNotLoggedIn(ctx.user);
return service.addLike({ userId: user.id, postId: input.postId });
},
})
.mutation("deleteLike", {
input: z.object({ postId: z.number() }),
resolve({ ctx, input }) {
const user = throwIfNotLoggedIn(ctx.user);
return service.deleteLike({ userId: user.id, postId: input.postId });
},
});
export default freeAgentsApi;