From 7b67a96b305f944caeccc73bf2220dc24241ccbb Mon Sep 17 00:00:00 2001 From: "Kalle (Sendou)" <38327916+Sendouc@users.noreply.github.com> Date: Sat, 18 Sep 2021 18:05:33 +0300 Subject: [PATCH] Closes #625 --- components/plus/Suggestion.tsx | 37 ++-- components/plus/SuggestionModal.tsx | 38 ++-- components/plus/VouchModal.tsx | 28 ++- components/plus/VouchesList.tsx | 4 +- hooks/common.ts | 2 +- hooks/plus.ts | 140 +++++++------- package-lock.json | 273 +--------------------------- package.json | 5 - pages/_app.tsx | 25 +-- pages/api/plus/index.ts | 19 ++ pages/api/plus/suggestions.ts | 36 ++++ pages/api/plus/users-for-voting.ts | 24 +++ pages/api/plus/votes.ts | 61 +++++++ pages/api/plus/vouches.ts | 28 +++ pages/api/trpc/[trpc].ts | 40 ---- pages/plus/index.tsx | 41 +++-- pages/plus/voting.tsx | 4 +- routers/plus.ts | 64 ------- services/freeagents.ts | 3 +- services/plus.ts | 57 +++--- utils/api.ts | 9 +- utils/trpc.ts | 16 -- 22 files changed, 357 insertions(+), 597 deletions(-) create mode 100644 pages/api/plus/index.ts create mode 100644 pages/api/plus/suggestions.ts create mode 100644 pages/api/plus/users-for-voting.ts create mode 100644 pages/api/plus/votes.ts create mode 100644 pages/api/plus/vouches.ts delete mode 100644 pages/api/trpc/[trpc].ts delete mode 100644 routers/plus.ts delete mode 100644 utils/trpc.ts diff --git a/components/plus/Suggestion.tsx b/components/plus/Suggestion.tsx index 0603ec206..a025c2a83 100644 --- a/components/plus/Suggestion.tsx +++ b/components/plus/Suggestion.tsx @@ -7,20 +7,19 @@ import { FormLabel, Text, Textarea, - useToast, } from "@chakra-ui/react"; import { zodResolver } from "@hookform/resolvers/zod"; import { Trans } from "@lingui/macro"; import MyLink from "components/common/MyLink"; import SubText from "components/common/SubText"; import UserAvatar from "components/common/UserAvatar"; +import { useMutation } from "hooks/common"; +import type { SuggestionsGet } from "pages/api/plus/suggestions"; import { useState } from "react"; import { useForm } from "react-hook-form"; -import { Suggestions } from "services/plus"; -import { getToastOptions } from "utils/objects"; +import { mutate } from "swr"; import { getVotingRange } from "utils/plus"; import { getFullUsername } from "utils/strings"; -import { trpc } from "utils/trpc"; import { Unpacked } from "utils/types"; import { resuggestionSchema, @@ -28,32 +27,28 @@ import { } from "utils/validators/suggestion"; import * as z from "zod"; -type FormData = z.infer; +type SuggestionsData = z.infer; const Suggestion = ({ suggestion, canSuggest, }: { - suggestion: Unpacked; + suggestion: Unpacked; canSuggest: boolean; }) => { - const toast = useToast(); const [showTextarea, setShowTextarea] = useState(false); - const { handleSubmit, errors, register, watch } = useForm({ + const { handleSubmit, errors, register, watch } = useForm({ resolver: zodResolver(resuggestionSchema), }); - const utils = trpc.useQueryUtils(); - const { mutate, status } = trpc.useMutation("plus.suggestion", { - onSuccess() { - toast(getToastOptions("Comment added", "success")); - // TODO: - utils.invalidateQuery(["plus.suggestions"]); + const suggestionMutation = useMutation({ + url: "/api/plus/suggestions", + method: "POST", + successToastMsg: "Comment added", + afterSuccess: () => { + mutate("/api/plus/suggestions"); setShowTextarea(false); }, - onError(error) { - toast(getToastOptions(error.message, "error")); - }, }); const watchDescription = watch("description", ""); @@ -109,10 +104,10 @@ const Suggestion = ({ {showTextarea && (
- mutate({ + suggestionMutation.mutate({ ...values, - // region doesn't matter as it is not updated after the first suggestion - region: "NA", + // @ts-expect-error region doesn't matter as it is not updated after the first suggestion + region: "NA" as const, tier: suggestion.tier, suggestedId: suggestion.suggestedUser.id, }) @@ -136,7 +131,7 @@ const Suggestion = ({ size="sm" mr={3} type="submit" - isLoading={status === "loading"} + isLoading={suggestionMutation.isMutating} data-cy="submit-button" > Save diff --git a/components/plus/SuggestionModal.tsx b/components/plus/SuggestionModal.tsx index d9475a35d..a8750a040 100644 --- a/components/plus/SuggestionModal.tsx +++ b/components/plus/SuggestionModal.tsx @@ -13,14 +13,13 @@ import { ModalOverlay, Select, Textarea, - useToast, } from "@chakra-ui/react"; import { zodResolver } from "@hookform/resolvers/zod"; import UserSelector from "components/common/UserSelector"; +import { useMutation } from "hooks/common"; import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; -import { getToastOptions } from "utils/objects"; -import { trpc } from "utils/trpc"; +import { mutate } from "swr"; import { suggestionFullSchema, SUGGESTION_DESCRIPTION_LIMIT, @@ -31,24 +30,23 @@ interface Props { userPlusMembershipTier: number; } -type FormData = z.infer; +type SuggestionsData = z.infer; const SuggestionModal: React.FC = ({ userPlusMembershipTier }) => { - const toast = useToast(); const [isOpen, setIsOpen] = useState(false); - const { handleSubmit, errors, register, watch, control } = useForm({ - resolver: zodResolver(suggestionFullSchema), - }); - const utils = trpc.useQueryUtils(); - const { mutate, status } = trpc.useMutation("plus.suggestion", { - onSuccess() { - toast(getToastOptions("New suggestion submitted", "success")); - utils.invalidateQuery(["plus.suggestions"]); + const { handleSubmit, errors, register, watch, control } = + useForm({ + resolver: zodResolver(suggestionFullSchema), + }); + + const suggestionMutation = useMutation({ + url: "/api/plus/suggestions", + method: "POST", + successToastMsg: "New suggestion submitted", + afterSuccess: () => { + mutate("/api/plus/suggestions"); setIsOpen(false); }, - onError(error) { - toast(getToastOptions(error.message, "error")); - }, }); const watchDescription = watch("description", ""); @@ -74,7 +72,11 @@ const SuggestionModal: React.FC = ({ userPlusMembershipTier }) => { Adding a new suggestion - mutate(data))}> + + suggestionMutation.mutate(data) + )} + > Tier = ({ userPlusMembershipTier }) => { + )} diff --git a/routers/plus.ts b/routers/plus.ts deleted file mode 100644 index 4ea791ce5..000000000 --- a/routers/plus.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { createRouter } from "pages/api/trpc/[trpc]"; -import service from "services/plus"; -import { throwIfNotLoggedIn } from "utils/api"; -import { suggestionFullSchema } from "utils/validators/suggestion"; -import { voteSchema, votesSchema } from "utils/validators/votes"; -import { vouchSchema } from "utils/validators/vouch"; - -const plusApi = createRouter() - .query("suggestions", { - resolve() { - return service.getSuggestions(); - }, - }) - .query("statuses", { - resolve() { - return service.getPlusStatuses(); - }, - }) - .query("usersForVoting", { - resolve({ ctx }) { - const user = throwIfNotLoggedIn(ctx.user); - return service.getUsersForVoting(user.id); - }, - }) - .query("votedUserScores", { - resolve({ ctx }) { - const user = throwIfNotLoggedIn(ctx.user); - return service.votedUserScores(user.id); - }, - }) - .query("votingProgress", { - resolve() { - return service.votingProgress(); - }, - }) - .mutation("suggestion", { - input: suggestionFullSchema, - resolve({ input, ctx }) { - const user = throwIfNotLoggedIn(ctx.user); - return service.addSuggestion({ input, userId: user.id }); - }, - }) - .mutation("vouch", { - input: vouchSchema, - resolve({ input, ctx }) { - const user = throwIfNotLoggedIn(ctx.user); - return service.addVouch({ input, userId: user.id }); - }, - }) - .mutation("vote", { - input: votesSchema, - resolve({ input, ctx }) { - const user = throwIfNotLoggedIn(ctx.user); - return service.addVotes({ input, userId: user.id }); - }, - }) - .mutation("editVote", { - input: voteSchema, - resolve({ input, ctx }) { - const user = throwIfNotLoggedIn(ctx.user); - return service.editVote({ input, userId: user.id }); - }, - }); -export default plusApi; diff --git a/services/freeagents.ts b/services/freeagents.ts index 114e79899..a13f4124c 100644 --- a/services/freeagents.ts +++ b/services/freeagents.ts @@ -1,5 +1,4 @@ import { Prisma, User } from "@prisma/client"; -import { httpError } from "@trpc/server"; import prisma from "prisma/client"; import { freeAgentPostSchema } from "utils/validators/fapost"; import * as z from "zod"; @@ -93,7 +92,7 @@ const likes = async ({ user }: { user: User }) => { dateMonthAgo.setMonth(dateMonthAgo.getMonth() - 1); if (!post || post.updatedAt.getTime() < dateMonthAgo.getTime()) { - throw httpError.badRequest("no post"); + throw new Error("no post"); } const likerPostIds = new Set( diff --git a/services/plus.ts b/services/plus.ts index a2dc348fa..48748224f 100644 --- a/services/plus.ts +++ b/services/plus.ts @@ -1,5 +1,4 @@ import { PlusRegion, Prisma } from "@prisma/client"; -import { httpError } from "@trpc/server"; import prisma from "prisma/client"; import { shuffleArray } from "utils/arrays"; import { getPercentageFromCounts, getVotingRange } from "utils/plus"; @@ -160,11 +159,15 @@ const getDistinctSummaryMonths = () => { }); }; +export type GetUsersForVoting = Prisma.PromiseReturnType< + typeof getUsersForVoting +>; + const getUsersForVoting = async (userId: number) => { - if (!getVotingRange().isHappening) return null; + if (!getVotingRange().isHappening) return []; const plusStatus = await prisma.plusStatus.findUnique({ where: { userId } }); - if (!plusStatus?.membershipTier) return null; + if (!plusStatus?.membershipTier) return []; const [plusStatuses, suggestions] = await Promise.all([ prisma.plusStatus.findMany({ @@ -256,24 +259,24 @@ const getUsersForVoting = async (userId: number) => { return shuffleArray(result).sort((a, b) => a.region.localeCompare(b.region)); }; +export type VotedUserScores = Prisma.PromiseReturnType; + const votedUserScores = async (userId: number) => { const ballots = await prisma.plusBallot.findMany({ where: { isStale: false, voterId: userId }, }); - if (ballots.length === 0) { - return undefined; - } - - const result = new Map(); + const result: Record = {}; for (const ballot of ballots) { - result.set(ballot.votedId, ballot.score); + result[ballot.votedId] = ballot.score; } return result; }; +export type VotingProgress = Prisma.PromiseReturnType; + const votingProgress = async () => { const [ballots, statuses] = await Promise.all([ prisma.plusBallot.findMany({ @@ -333,7 +336,7 @@ const addSuggestion = async ({ isResuggestion === false && suggesterId === userId ); if (usersSuggestion) { - throw httpError.badRequest("already made a new suggestion"); + throw new Error("already made a new suggestion"); } } @@ -346,17 +349,17 @@ const addSuggestion = async ({ !suggesterPlusStatus.membershipTier || suggesterPlusStatus.membershipTier > input.tier ) { - throw httpError.badRequest( + throw new Error( "not a member of high enough tier to suggest for this tier" ); } if (suggestedUserAlreadyHasAccess()) { - throw httpError.badRequest("suggested user already has access"); + throw new Error("suggested user already has access"); } if (getVotingRange().isHappening) { - throw httpError.badRequest("voting has already started"); + throw new Error("voting has already started"); } return prisma.$transaction([ @@ -418,24 +421,22 @@ const addVouch = async ({ if (summary.userId === input.vouchedId && summary.tier === input.tier) { // can't vouch if they were just kicked - throw httpError.badRequest( + throw new Error( "can't vouch the user because they were kicked last month" ); } } if ((suggesterPlusStatus?.canVouchFor ?? Infinity) > input.tier) { - throw httpError.badRequest( - "not a member of high enough tier to vouch for this tier" - ); + throw new Error("not a member of high enough tier to vouch for this tier"); } if (vouchedUserAlreadyHasAccess()) { - throw httpError.badRequest("vouched user already has access"); + throw new Error("vouched user already has access"); } if (getVotingRange().isHappening) { - throw httpError.badRequest("voting has already started"); + throw new Error("voting has already started"); } return prisma.$transaction([ @@ -478,7 +479,7 @@ const addVotes = async ({ userId: number; }) => { if (!getVotingRange().isHappening) { - throw httpError.badRequest("voting is not happening right now"); + throw new Error("voting is not happening right now"); } const [plusStatuses, suggestions] = await Promise.all([ @@ -492,8 +493,7 @@ const addVotes = async ({ const usersMembership = usersPlusStatus?.membershipTier; - if (!usersPlusStatus || !usersMembership) - throw httpError.badRequest("not a member"); + if (!usersPlusStatus || !usersMembership) throw new Error("not a member"); const allowedUsers = new Map(); @@ -516,14 +516,13 @@ const addVotes = async ({ const status = plusStatuses.find( (status) => status.userId === suggestion.suggestedId ); - if (!status) - throw httpError.badRequest("unexpected no status for suggested user"); + if (!status) throw new Error("unexpected no status for suggested user"); allowedUsers.set(suggestion.suggestedId, status.region); } if (input.length !== allowedUsers.size) { - throw httpError.badRequest("didn't vote on every user exactly once"); + throw new Error("didn't vote on every user exactly once"); } if ( @@ -540,7 +539,7 @@ const addVotes = async ({ return false; }) ) { - throw httpError.badRequest("invalid vote provided"); + throw new Error("invalid vote provided"); } return prisma.plusBallot.createMany({ @@ -563,7 +562,7 @@ const editVote = async ({ userId: number; }) => { if (!getVotingRange().isHappening) { - throw httpError.badRequest("voting is not happening right now"); + throw new Error("voting is not happening right now"); } const statuses = await prisma.plusStatus.findMany({ @@ -579,14 +578,14 @@ const editVote = async ({ statuses[0].region !== statuses[1].region && ![-1, 1].includes(input.score) ) { - throw httpError.badRequest("invalid score"); + throw new Error("invalid score"); } if ( statuses[0].region === statuses[1].region && ![-2, -1, 1, 2].includes(input.score) ) { - throw httpError.badRequest("invalid score"); + throw new Error("invalid score"); } return prisma.plusBallot.update({ diff --git a/utils/api.ts b/utils/api.ts index f23941c90..936277f5f 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -1,5 +1,4 @@ import { User } from "@prisma/client"; -import { httpError } from "@trpc/server"; import { NextApiRequest, NextApiResponse } from "next"; import { getSession } from "next-auth/client"; @@ -15,18 +14,12 @@ export const getMySession = (req: NextApiRequest): Promise => { return getSession({ req }); }; -export const throwIfNotLoggedIn = (user: User | null) => { - if (!user) throw httpError.unauthorized(); - - return user; -}; - export const createHandler = ( req: NextApiRequest, res: NextApiResponse, handlers: Partial< Record< - "GET" | "POST" | "PUT" | "DELETE", + "GET" | "POST" | "PUT" | "PATCH" | "DELETE", (req: NextApiRequest, res: NextApiResponse) => Promise > > diff --git a/utils/trpc.ts b/utils/trpc.ts deleted file mode 100644 index 92a758ed8..000000000 --- a/utils/trpc.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createReactQueryHooks, createTRPCClient } from "@trpc/react"; -import superjson from "superjson"; -// Type-only import: -// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export -import type { AppRouter } from "../pages/api/trpc/[trpc]"; - -// create helper methods for queries, mutations, and subscriptionos -export const client = createTRPCClient({ - url: "/api/trpc", - transformer: superjson, -}); - -// create react query hooks for trpc -export const trpc = createReactQueryHooks({ - client, -});