diff --git a/components/freeagents/FAModal.tsx b/components/freeagents/FAModal.tsx index 586a560b1..8295cec9d 100644 --- a/components/freeagents/FAModal.tsx +++ b/components/freeagents/FAModal.tsx @@ -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; } -type FormData = z.infer; +type FreeAgentPostSchemaInput = z.infer; const FAModal = ({ onClose, post, refetchQuery }: Props) => { - const { handleSubmit, errors, register, watch, control } = useForm({ - resolver: zodResolver(freeAgentPostSchema), - defaultValues: post, - }); + const { handleSubmit, errors, register, watch, control } = + useForm({ + 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({ + 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({ + 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) => {
upsertPostMutation.mutate(data))} + onSubmit={handleSubmit((data) => + upsertFreeAgentPostMutation.mutate(data) + )} > {post && ( @@ -101,10 +95,10 @@ const FAModal = ({ onClose, post, refetchQuery }: Props) => { leftIcon={} 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 } > Save @@ -194,7 +189,8 @@ const FAModal = ({ onClose, post, refetchQuery }: Props) => { onClick={onClose} variant="outline" isDisabled={ - upsertPostMutation.isLoading || deletePostMutation.isLoading + upsertFreeAgentPostMutation.isMutating || + deleteFreeAgentPostMutation.isMutating } > Cancel diff --git a/components/freeagents/FreeAgentSection.tsx b/components/freeagents/FreeAgentSection.tsx index 4ba51aa19..626e1c63e 100644 --- a/components/freeagents/FreeAgentSection.tsx +++ b/components/freeagents/FreeAgentSection.tsx @@ -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; }) => { - 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 ? : } - disabled={addLikeMutation.isLoading || deleteLikeMutation.isLoading} + disabled={ + addLikeMutation.isMutating || deleteLikeMutation.isMutating + } onClick={handleClick} /> )} diff --git a/hooks/common.ts b/hooks/common.ts index d71b2b135..944dcc44b 100644 --- a/hooks/common.ts +++ b/hooks/common.ts @@ -51,7 +51,7 @@ export const useMutation = ({ url: string; method?: "POST" | "DELETE" | "PUT"; data?: T; - successToastMsg: string; + successToastMsg?: string; afterSuccess?: () => void; }) => { const toast = useToast(); @@ -85,7 +85,9 @@ export const useMutation = ({ description, }); } else { - toast(getToastOptions(successToastMsg, "success")); + if (successToastMsg) { + toast(getToastOptions(successToastMsg, "success")); + } afterSuccess?.(); } }; diff --git a/pages/api/free-agents/index.ts b/pages/api/free-agents/index.ts index 4b0540b3e..15552efbc 100644 --- a/pages/api/free-agents/index.ts +++ b/pages/api/free-agents/index.ts @@ -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; @@ -10,7 +11,32 @@ const GET = async (_req: NextApiRequest, res: NextApiResponse) => { 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; diff --git a/pages/api/free-agents/likes.ts b/pages/api/free-agents/likes.ts index aa65ca39b..868c36dd8 100644 --- a/pages/api/free-agents/likes.ts +++ b/pages/api/free-agents/likes.ts @@ -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; @@ -15,7 +16,43 @@ const GET = async (req: NextApiRequest, res: NextApiResponse) => { 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; diff --git a/pages/api/trpc/[trpc].ts b/pages/api/trpc/[trpc].ts index 2571f2ec8..54770a37f 100644 --- a/pages/api/trpc/[trpc].ts +++ b/pages/api/trpc/[trpc].ts @@ -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(); } // 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 diff --git a/pages/freeagents.tsx b/pages/freeagents.tsx index d2c4e5985..21c2861d1 100644 --- a/pages/freeagents.tsx +++ b/pages/freeagents.tsx @@ -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; + const FreeAgentsPage = ({ postsInitialData }: Props) => { const { postsData, @@ -46,13 +48,20 @@ const FreeAgentsPage = ({ postsInitialData }: Props) => { const [postIdToScrollTo, setPostIdToScrollTo] = useState( undefined ); - const [sending, setSending] = useState(false); const postRef = useRef(null); const [modalIsOpen, setModalIsOpen] = useState(false); + const refreshPostMutation = useMutation({ + 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) => { diff --git a/routers/freeagents.ts b/routers/freeagents.ts deleted file mode 100644 index 5f8484362..000000000 --- a/routers/freeagents.ts +++ /dev/null @@ -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;