diff --git a/components/plus/SuggestionVouchModal.tsx b/components/plus/SuggestionVouchModal.tsx index 69f2e5843..da6de6d11 100644 --- a/components/plus/SuggestionVouchModal.tsx +++ b/components/plus/SuggestionVouchModal.tsx @@ -24,6 +24,7 @@ import { import * as z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import UserSelector from "components/common/UserSelector"; +import useMutation from "hooks/useMutation"; interface Props { canVouch: boolean; @@ -39,10 +40,15 @@ const SuggestionVouchModal: React.FC = ({ userPlusMembershipTier, }) => { const [isOpen, setIsOpen] = useState(false); - const sending = false; //usemutation hook const { handleSubmit, errors, register, watch, control } = useForm({ resolver: zodResolver(suggestionSchema), }); + const { onSubmit, sending } = useMutation({ + onClose: () => setIsOpen(false), + route: "plus/suggestions", + mutationKey: "", + successText: t`New suggestion submitted`, + }); const watchDescription = watch("description", ""); @@ -75,7 +81,7 @@ const SuggestionVouchModal: React.FC = ({ Adding a new suggestion or vouch -
+ Tier @@ -99,21 +105,28 @@ const SuggestionVouchModal: React.FC = ({ )} /> - - User - - ( - - )} - /> + + + + User + + ( + + )} + /> + + {errors.suggestedId?.message} + + + Region @@ -127,6 +140,7 @@ const SuggestionVouchModal: React.FC = ({ they play most commonly with. + Description diff --git a/hooks/useMutation.ts b/hooks/useMutation.ts new file mode 100644 index 000000000..ac6439312 --- /dev/null +++ b/hooks/useMutation.ts @@ -0,0 +1,51 @@ +import { useToast } from "@chakra-ui/react"; +import { getToastOptions } from "lib/getToastOptions"; +import { sendData } from "lib/postData"; +import { useState } from "react"; +import { mutate } from "swr"; +import { useUser } from "./common"; + +const useMutation = ({ + route, + mutationKey, + onClose, + successText, +}: { + route: string; + mutationKey: string; + onClose: () => 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(mutationKey); + + toast(getToastOptions(successText, "success")); + onClose(); + }; + + return { onSubmit, sending }; +}; + +export default useMutation; diff --git a/lib/validators/suggestion.ts b/lib/validators/suggestion.ts index b94f00f03..3ac05d612 100644 --- a/lib/validators/suggestion.ts +++ b/lib/validators/suggestion.ts @@ -3,7 +3,7 @@ import * as z from "zod"; export const SUGGESTION_DESCRIPTION_LIMIT = 500; export const suggestionSchema = z.object({ - description: z.string().max(SUGGESTION_DESCRIPTION_LIMIT), + description: z.string().max(SUGGESTION_DESCRIPTION_LIMIT).min(10), suggestedId: z.number().int(), tier: z.number().int().min(1).max(3), region: z.enum(["NA", "EU"]), diff --git a/services/plus.ts b/services/plus.ts index 67d5bd046..53ea020e5 100644 --- a/services/plus.ts +++ b/services/plus.ts @@ -108,9 +108,9 @@ const addSuggestion = async ({ userId: number; }) => { const parsedData = { ...suggestionSchema.parse(data), suggesterId: userId }; - const [suggestions, plusStatus] = await Promise.all([ + const [suggestions, plusStatuses] = await Promise.all([ prisma.plusSuggestion.findMany({}), - prisma.plusStatus.findUnique({ where: { userId } }), + prisma.plusStatus.findMany({}), ]); const existingSuggestion = suggestions.find( ({ tier, suggestedId }) => @@ -128,21 +128,47 @@ const addSuggestion = async ({ } } + const suggesterPlusStatus = plusStatuses.find( + (status) => status.userId === userId + ); + if ( - !plusStatus || - !plusStatus.membershipTier || - plusStatus.membershipTier > parsedData.tier + !suggesterPlusStatus || + !suggesterPlusStatus.membershipTier || + suggesterPlusStatus.membershipTier > parsedData.tier ) { throw new UserError( "not a member of high enough tier to suggest for this tier" ); } + if (suggestedUserAlreadyHasAccess()) { + throw new UserError("suggested user already has access"); + } + // TODO voting has started - return prisma.plusSuggestion.create({ - data: { ...parsedData, isResuggestion: !!existingSuggestion }, - }); + return prisma.$transaction([ + prisma.plusSuggestion.create({ + data: { ...parsedData, isResuggestion: !!existingSuggestion }, + }), + prisma.plusStatus.upsert({ + where: { userId: parsedData.suggestedId }, + create: { region: parsedData.region, userId: parsedData.suggestedId }, + update: {}, + }), + ]); + + function suggestedUserAlreadyHasAccess() { + const suggestedPlusStatus = plusStatuses.find( + (status) => status.userId === parsedData.suggestedId + ); + return Boolean( + suggestedPlusStatus && + ((suggestedPlusStatus.membershipTier ?? 999) <= parsedData.tier || + (suggestedPlusStatus.vouchTier ?? 999) <= parsedData.tier) + ); + } }; export default {