new api for plus

This commit is contained in:
Kalle (Sendou) 2021-03-09 00:01:59 +02:00
parent 710520d0f6
commit 950f2be402
8 changed files with 68 additions and 149 deletions

View File

@ -1,6 +1,7 @@
import { createRouter } from "pages/api/trpc/[trpc]";
import { throwIfNotLoggedIn } from "utils/api";
import { suggestionFullSchema } from "utils/validators/suggestion";
import { vouchSchema } from "utils/validators/vouch";
import service from "./service";
const plusApi = createRouter()
@ -20,6 +21,12 @@ const plusApi = createRouter()
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 });
},
});
export default plusApi;

View File

@ -6,6 +6,7 @@ import {
FormHelperText,
FormLabel,
Textarea,
useToast,
} from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { Trans } from "@lingui/macro";
@ -14,10 +15,11 @@ import MyLink from "components/common/MyLink";
import SubText from "components/common/SubText";
import UserAvatar from "components/common/UserAvatar";
import { useMyTheme } from "hooks/common";
import useMutation from "hooks/useMutation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { getToastOptions } from "utils/getToastOptions";
import { getFullUsername } from "utils/strings";
import { trpc } from "utils/trpc";
import { Unpacked } from "utils/types";
import {
resuggestionSchema,
@ -34,16 +36,23 @@ const Suggestion = ({
suggestion: Unpacked<Suggestions>;
canSuggest: boolean;
}) => {
const toast = useToast();
const { gray } = useMyTheme();
const [showTextarea, setShowTextarea] = useState(false);
const { handleSubmit, errors, register, watch } = useForm<FormData>({
resolver: zodResolver(resuggestionSchema),
});
const { onSubmit, sending } = useMutation({
route: "plus/suggestions",
mutationKey: "plus/suggestions",
successText: "Comment added",
onSuccess: () => setShowTextarea(false),
const { mutate, status } = trpc.useMutation("plus.suggestion", {
onSuccess() {
toast(getToastOptions("Comment added", "success"));
// TODO:
trpc.queryClient.invalidateQueries(["plus.suggestions"]);
setShowTextarea(false);
},
onError(error) {
toast(getToastOptions(error.message, "error"));
},
});
const watchDescription = watch("description", "");
@ -100,7 +109,7 @@ const Suggestion = ({
{showTextarea && (
<form
onSubmit={handleSubmit((values) =>
onSubmit({
mutate({
...values,
// region doesn't matter as it is not updated after the first suggestion
region: "NA",
@ -127,7 +136,7 @@ const Suggestion = ({
size="sm"
mr={3}
type="submit"
isLoading={sending}
isLoading={status === "loading"}
data-cy="submit-button"
>
<Trans>Save</Trans>

View File

@ -13,12 +13,14 @@ 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/useMutation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { getToastOptions } from "utils/getToastOptions";
import { trpc } from "utils/trpc";
import {
suggestionFullSchema,
SUGGESTION_DESCRIPTION_LIMIT,
@ -32,15 +34,22 @@ interface Props {
type FormData = z.infer<typeof suggestionFullSchema>;
const SuggestionModal: React.FC<Props> = ({ userPlusMembershipTier }) => {
const toast = useToast();
const [isOpen, setIsOpen] = useState(false);
const { handleSubmit, errors, register, watch, control } = useForm<FormData>({
resolver: zodResolver(suggestionFullSchema),
});
const { onSubmit, sending } = useMutation({
onSuccess: () => setIsOpen(false),
route: "plus/suggestions",
mutationKey: "plus/suggestions",
successText: "New suggestion submitted",
const { mutate, status } = trpc.useMutation("plus.suggestion", {
onSuccess() {
toast(getToastOptions("New suggestion submitted", "success"));
// TODO:
trpc.queryClient.invalidateQueries(["plus.suggestions"]);
setIsOpen(false);
},
onError(error) {
toast(getToastOptions(error.message, "error"));
},
});
const watchDescription = watch("description", "");
@ -66,7 +75,7 @@ const SuggestionModal: React.FC<Props> = ({ userPlusMembershipTier }) => {
<ModalContent>
<ModalHeader>Adding a new suggestion</ModalHeader>
<ModalCloseButton borderRadius="50%" />
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit((data) => mutate(data))}>
<ModalBody pb={2}>
<FormLabel>Tier</FormLabel>
<Controller
@ -146,7 +155,7 @@ const SuggestionModal: React.FC<Props> = ({ userPlusMembershipTier }) => {
<Button
mr={3}
type="submit"
isLoading={sending}
isLoading={status === "loading"}
data-cy="submit-button"
>
Save

View File

@ -12,12 +12,14 @@ import {
ModalHeader,
ModalOverlay,
Select,
useToast,
} from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import UserSelector from "components/common/UserSelector";
import useMutation from "hooks/useMutation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { getToastOptions } from "utils/getToastOptions";
import { trpc } from "utils/trpc";
import { vouchSchema } from "utils/validators/vouch";
import * as z from "zod";
@ -28,15 +30,22 @@ interface Props {
type FormData = z.infer<typeof vouchSchema>;
const VouchModal: React.FC<Props> = ({ canVouchFor }) => {
const toast = useToast();
const [isOpen, setIsOpen] = useState(false);
const { handleSubmit, errors, register, control } = useForm<FormData>({
resolver: zodResolver(vouchSchema),
});
const { onSubmit, sending } = useMutation({
onSuccess: () => setIsOpen(false),
route: "plus/vouch",
mutationKey: "plus",
successText: "Successfully vouched",
const { mutate, status } = trpc.useMutation("plus.vouch", {
onSuccess() {
toast(getToastOptions("Successfully vouched", "success"));
// TODO:
trpc.queryClient.invalidateQueries(["plus.statuses"]);
setIsOpen(false);
},
onError(error) {
toast(getToastOptions(error.message, "error"));
},
});
return (
@ -61,7 +70,7 @@ const VouchModal: React.FC<Props> = ({ canVouchFor }) => {
<ModalContent>
<ModalHeader>Vouching</ModalHeader>
<ModalCloseButton borderRadius="50%" />
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit((data) => mutate(data))}>
<ModalBody pb={2}>
<FormLabel>Tier</FormLabel>
<Controller
@ -119,7 +128,7 @@ const VouchModal: React.FC<Props> = ({ canVouchFor }) => {
<Button
mr={3}
type="submit"
isLoading={sending}
isLoading={status === "loading"}
data-cy="submit-button"
>
Save

View File

@ -240,20 +240,19 @@ const addSuggestion = async ({
};
const addVouch = async ({
data,
input,
userId,
}: {
data: unknown;
input: z.infer<typeof vouchSchema>;
userId: number;
}) => {
const parsedData = vouchSchema.parse(data);
const plusStatuses = await prisma.plusStatus.findMany({});
const suggesterPlusStatus = plusStatuses.find(
(status) => status.userId === userId
);
if ((suggesterPlusStatus?.canVouchFor ?? Infinity) > parsedData.tier) {
if ((suggesterPlusStatus?.canVouchFor ?? Infinity) > input.tier) {
httpError.badRequest(
"not a member of high enough tier to vouch for this tier"
);
@ -269,9 +268,9 @@ const addVouch = async ({
return prisma.$transaction([
prisma.plusStatus.upsert({
where: { userId: parsedData.vouchedId },
create: { region: parsedData.region, userId: parsedData.vouchedId },
update: { voucherId: userId, vouchTier: parsedData.tier },
where: { userId: input.vouchedId },
create: { region: input.region, userId: input.vouchedId },
update: { voucherId: userId, vouchTier: input.tier },
}),
prisma.plusStatus.update({
where: { userId },
@ -281,12 +280,12 @@ const addVouch = async ({
function vouchedUserAlreadyHasAccess() {
const suggestedPlusStatus = plusStatuses.find(
(status) => status.userId === parsedData.vouchedId
(status) => status.userId === input.vouchedId
);
return Boolean(
suggestedPlusStatus &&
((suggestedPlusStatus.membershipTier ?? 999) <= parsedData.tier ||
(suggestedPlusStatus.vouchTier ?? 999) <= parsedData.tier)
((suggestedPlusStatus.membershipTier ?? 999) <= input.tier ||
(suggestedPlusStatus.vouchTier ?? 999) <= input.tier)
);
}
};

View File

@ -1,51 +0,0 @@
import { useToast } from "@chakra-ui/react";
import { useState } from "react";
import { mutate } from "swr";
import { getToastOptions } from "utils/getToastOptions";
import { sendData } from "utils/postData";
import { useUser } from "./common";
const useMutation = ({
route,
mutationKey,
onSuccess,
successText,
}: {
route: string;
mutationKey: string;
onSuccess?: () => void;
successText: string;
}) => {
const toast = useToast();
const [sending, setSending] = useState(false);
const [loggedInUser] = useUser();
const onSubmit = async (formData: object) => {
if (!loggedInUser) {
console.error("Unexpected no logged in user");
return;
}
setSending(true);
const mutationData = { ...formData };
for (const [key, value] of Object.entries(mutationData)) {
if (value === "" || value === undefined) {
// @ts-ignore
mutationData[key] = null;
}
}
const success = await sendData("POST", "/api/" + route, mutationData);
setSending(false);
if (!success) return;
mutate("/api/" + mutationKey);
toast(getToastOptions(successText, "success"));
onSuccess?.();
};
return { onSubmit, sending };
};
export default useMutation;

View File

@ -1,23 +0,0 @@
import plusService from "app/plus/service";
import { NextApiRequest, NextApiResponse } from "next";
const plusHandler = async (req: NextApiRequest, res: NextApiResponse) => {
switch (req.method) {
case "GET":
await getHandler(req, res);
break;
default:
res.status(405).end();
}
async function getHandler(_req: NextApiRequest, res: NextApiResponse) {
try {
res.status(200).json(await plusService.getPlusStatuses());
} catch (e) {
console.error(e.message);
res.status(500).end();
}
}
};
export default plusHandler;

View File

@ -1,40 +0,0 @@
import plusService from "app/plus/service";
import { NextApiRequest, NextApiResponse } from "next";
import { getMySession } from "utils/api";
import { UserError } from "utils/errors";
import { ZodError } from "zod";
const vouchHandler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getMySession(req);
switch (req.method) {
case "POST":
await postHandler(req, res);
break;
default:
res.status(405).end();
}
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
if (!user) return res.status(401).end();
try {
await plusService.addVouch({ data: req.body, userId: user.id });
} catch (e) {
if (e instanceof ZodError) {
res.status(400).json({ message: e.message });
} else if (e instanceof UserError) {
res.status(400).json({ message: e.message });
} else {
console.error(e.message);
res.status(500).end();
}
return;
}
res.status(200).end();
}
};
export default vouchHandler;