This commit is contained in:
Kalle (Sendou) 2021-09-18 18:05:33 +03:00
parent 0f3da0e3f8
commit 7b67a96b30
22 changed files with 357 additions and 597 deletions

View File

@ -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<typeof resuggestionSchema>;
type SuggestionsData = z.infer<typeof resuggestionSchema>;
const Suggestion = ({
suggestion,
canSuggest,
}: {
suggestion: Unpacked<Suggestions>;
suggestion: Unpacked<SuggestionsGet>;
canSuggest: boolean;
}) => {
const toast = useToast();
const [showTextarea, setShowTextarea] = useState(false);
const { handleSubmit, errors, register, watch } = useForm<FormData>({
const { handleSubmit, errors, register, watch } = useForm<SuggestionsData>({
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<SuggestionsData>({
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 && (
<form
onSubmit={handleSubmit((values) =>
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"
>
<Trans>Save</Trans>

View File

@ -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<typeof suggestionFullSchema>;
type SuggestionsData = 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 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<SuggestionsData>({
resolver: zodResolver(suggestionFullSchema),
});
const suggestionMutation = useMutation<SuggestionsData>({
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<Props> = ({ userPlusMembershipTier }) => {
<ModalContent>
<ModalHeader>Adding a new suggestion</ModalHeader>
<ModalCloseButton borderRadius="50%" />
<form onSubmit={handleSubmit((data) => mutate(data))}>
<form
onSubmit={handleSubmit((data) =>
suggestionMutation.mutate(data)
)}
>
<ModalBody pb={2}>
<FormLabel>Tier</FormLabel>
<Controller
@ -154,7 +156,7 @@ const SuggestionModal: React.FC<Props> = ({ userPlusMembershipTier }) => {
<Button
mr={3}
type="submit"
isLoading={status === "loading"}
isLoading={suggestionMutation.isMutating}
data-cy="submit-button"
>
Save

View File

@ -12,14 +12,12 @@ 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/common";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { getToastOptions } from "utils/objects";
import { trpc } from "utils/trpc";
import { vouchSchema } from "utils/validators/vouch";
import * as z from "zod";
@ -27,24 +25,20 @@ interface Props {
canVouchFor: number;
}
type FormData = z.infer<typeof vouchSchema>;
type VouchData = 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>({
const { handleSubmit, errors, register, control } = useForm<VouchData>({
resolver: zodResolver(vouchSchema),
});
const utils = trpc.useQueryUtils();
const { mutate, status } = trpc.useMutation("plus.vouch", {
onSuccess() {
toast(getToastOptions("Successfully vouched", "success"));
utils.invalidateQuery(["plus.statuses"]);
const vouchMutation = useMutation<VouchData>({
url: "/api/plus/vouches",
method: "POST",
successToastMsg: "Successfully vouched",
afterSuccess: () => {
setIsOpen(false);
},
onError(error) {
toast(getToastOptions(error.message, "error"));
},
});
return (
@ -69,7 +63,9 @@ const VouchModal: React.FC<Props> = ({ canVouchFor }) => {
<ModalContent>
<ModalHeader>Vouching</ModalHeader>
<ModalCloseButton borderRadius="50%" />
<form onSubmit={handleSubmit((data) => mutate(data))}>
<form
onSubmit={handleSubmit((data) => vouchMutation.mutate(data))}
>
<ModalBody pb={2}>
<FormLabel>Tier</FormLabel>
<Controller
@ -127,7 +123,7 @@ const VouchModal: React.FC<Props> = ({ canVouchFor }) => {
<Button
mr={3}
type="submit"
isLoading={status === "loading"}
isLoading={vouchMutation.isMutating}
data-cy="submit-button"
>
Save

View File

@ -1,11 +1,11 @@
import { Button } from "@chakra-ui/button";
import { Box, Grid } from "@chakra-ui/layout";
import MyLink from "components/common/MyLink";
import { PlusStatusesGet } from "pages/api/plus";
import { Fragment, useState } from "react";
import { PlusStatuses } from "services/plus";
import { getFullUsername } from "utils/strings";
const VouchesList = ({ vouches }: { vouches: PlusStatuses }) => {
const VouchesList = ({ vouches }: { vouches: PlusStatusesGet }) => {
const [show, setShow] = useState(false);
return (

View File

@ -49,7 +49,7 @@ export const useMutation = <T>({
afterSuccess,
}: {
url: string;
method?: "POST" | "DELETE" | "PUT";
method?: "POST" | "DELETE" | "PATCH" | "PUT";
data?: T;
successToastMsg?: string;
afterSuccess?: () => void;

View File

@ -1,43 +1,39 @@
import { useToast } from "@chakra-ui/toast";
import { useUser } from "hooks/common";
import { useMutation, useUser } from "hooks/common";
import { PlusStatusesGet } from "pages/api/plus";
import type { SuggestionsGet } from "pages/api/plus/suggestions";
import { UsersForVotingGet } from "pages/api/plus/users-for-voting";
import { VotesGet } from "pages/api/plus/votes";
import { useState } from "react";
import { getToastOptions } from "utils/objects";
import { getVotingRange } from "utils/plus";
import { trpc } from "utils/trpc";
import { Unpacked } from "utils/types";
import { votesSchema } from "utils/validators/votes";
import { PlusStatuses } from "services/plus";
import useSWR from "swr";
import { Serialized, Unpacked } from "utils/types";
import { voteSchema, votesSchema } from "utils/validators/votes";
import * as z from "zod";
export function usePlusHomePage() {
const [user] = useUser();
export function usePlusHomePage(statuses: Serialized<PlusStatuses>) {
const [user, userIsLoading] = useUser();
const [suggestionsFilter, setSuggestionsFilter] = useState<
number | undefined
>(undefined);
const { data: suggestionsData } = trpc.useQuery(["plus.suggestions"], {
enabled: !getVotingRange().isHappening,
});
const { data: plusStatusData } = trpc.useQuery(["plus.statuses"]);
const { data: votingProgress } = trpc.useQuery(["plus.votingProgress"], {
enabled: getVotingRange().isHappening,
});
const suggestionsQuery = useSWR<SuggestionsGet>("/api/plus/suggestions");
const suggestions = suggestionsData ?? [];
const suggestions = suggestionsQuery.data ?? [];
return {
plusStatusData: plusStatusData?.find(
(status) => status.user.id === user?.id
),
vouchStatuses: plusStatusData
plusStatusData: statuses?.find((status) => status.user.id === user?.id),
plusStatusDataLoading: userIsLoading,
vouchStatuses: statuses
?.filter((status) => status.voucher)
.sort((a, b) => a.vouchTier! - b.vouchTier!),
vouchedPlusStatusData: plusStatusData?.find(
vouchedPlusStatusData: statuses?.find(
(status) => status.voucher?.id === user?.id
),
suggestionsData: suggestions.filter(
(suggestion) =>
!suggestionsFilter || suggestion.tier === suggestionsFilter
),
suggestionsLoading: !suggestionsQuery.data,
suggestionCounts: suggestions.reduce(
(counts, suggestion) => {
const tierString = [null, "ONE", "TWO", "THREE"][
@ -53,74 +49,76 @@ export function usePlusHomePage() {
(suggestion) => suggestion.suggesterUser.id === user?.id
),
setSuggestionsFilter,
votingProgress,
};
}
type VoteInput = z.infer<typeof votesSchema>;
type EditVoteInput = z.infer<typeof voteSchema>;
export default function usePlusVoting() {
const [currentIndex, setCurrentIndex] = useState(0);
const [votes, setVotes] = useState<z.infer<typeof votesSchema>>([]);
const toast = useToast();
const [user] = useUser();
const { data: votedUserScores, isLoading: hasVotedIsLoading } = trpc.useQuery(
["plus.votedUserScores"]
const votedUserScores = useSWR<VotesGet>("/api/plus/votes");
const usersForVoting = useSWR<UsersForVotingGet>(
"/api/plus/users-for-voting"
);
const { data: usersForVoting, isLoading: isLoadingBallots } = trpc.useQuery([
"plus.usersForVoting",
]);
const { data: statuses, isLoading: isLoadingStatuses } = trpc.useQuery([
"plus.statuses",
]);
const utils = trpc.useQueryUtils();
const { mutate: mutateVote, status: voteStatus } = trpc.useMutation(
"plus.vote",
{
onSuccess() {
toast(getToastOptions("Successfully voted", "success"));
utils.invalidateQuery(["plus.votedUserScores"]);
utils.invalidateQuery(["plus.votingProgress"]);
},
onError(error) {
toast(getToastOptions(error.message, "error"));
},
}
);
const { mutate: editVoteMutate, isLoading: isLoadingEditVote } =
trpc.useMutation("plus.editVote", {
onSuccess() {
toast(getToastOptions("Successfully edited vote", "success"));
utils.invalidateQuery(["plus.votedUserScores"]);
},
onError(error) {
toast(getToastOptions(error.message, "error"));
},
});
const statuses = useSWR<PlusStatusesGet>("/api/plus");
const voteMutation = useMutation<VoteInput>({
url: "/api/plus/votes",
method: "POST",
successToastMsg: "Successfully voted",
afterSuccess: () => {
votedUserScores.mutate();
},
});
const ownPlusStatus = statuses?.find((status) => status.user.id === user?.id);
const editVoteMutation = useMutation<EditVoteInput>({
url: "/api/plus/votes",
method: "PATCH",
successToastMsg: "Successfully edited vote",
afterSuccess: () => {
votedUserScores.mutate();
},
});
const ownPlusStatus = statuses.data?.find(
(status) => status.user.id === user?.id
);
const getVotedUsers = () => {
if (!votedUserScores || !usersForVoting) return undefined;
if (
!usersForVoting.data ||
Object.keys(votedUserScores.data ?? {}).length === 0 ||
usersForVoting.data.length === 0
)
return undefined;
return usersForVoting
return usersForVoting.data
.map((u) => {
return { ...u, score: votedUserScores.get(u.userId)! };
return { ...u, score: votedUserScores.data?.[u.userId]! };
})
.sort((a, b) => a.username.localeCompare(b.username));
};
return {
isLoading: isLoadingBallots || isLoadingStatuses || hasVotedIsLoading,
shouldRedirect: !isLoadingBallots && !usersForVoting,
isLoading: !usersForVoting.data || !statuses.data || !votedUserScores.data,
shouldRedirect: !usersForVoting.data && !usersForVoting,
plusStatus: ownPlusStatus,
currentUser: usersForVoting?.[currentIndex],
currentUser: usersForVoting.data?.[currentIndex],
previousUser:
currentIndex > 0 && usersForVoting
? { ...usersForVoting[currentIndex - 1], ...votes[votes.length - 1] }
currentIndex > 0 &&
usersForVoting &&
(usersForVoting.data ?? []).length > 0
? {
...usersForVoting.data![currentIndex - 1],
...votes[votes.length - 1],
}
: undefined,
progress: usersForVoting
? (currentIndex / usersForVoting.length) * 100
? (currentIndex / (usersForVoting.data?.length ?? 0)) * 100
: undefined,
handleVote: (vote: Unpacked<z.infer<typeof votesSchema>>) => {
const nextIndex = currentIndex + 1;
@ -129,7 +127,7 @@ export default function usePlusVoting() {
(<HTMLElement>document.activeElement).blur();
// preload next avatar
const next = usersForVoting?.[nextIndex + 1];
const next = usersForVoting.data?.[nextIndex + 1];
if (next) {
new Image().src = `https://cdn.discordapp.com/avatars/${next.discordId}/${next.discordAvatar}.jpg`;
}
@ -138,10 +136,10 @@ export default function usePlusVoting() {
setVotes(votes.slice(0, votes.length - 1));
setCurrentIndex(currentIndex - 1);
},
submit: () => mutateVote(votes),
voteStatus,
submit: () => voteMutation.mutate(votes),
voteMutating: voteMutation.isMutating,
votedUsers: getVotedUsers(),
editVote: editVoteMutate,
isLoadingEditVote,
editVote: editVoteMutation.mutate,
isLoadingEditVote: editVoteMutation.isMutating,
};
}

273
package-lock.json generated
View File

@ -19,9 +19,6 @@
"@next/bundle-analyzer": "^11.1.0",
"@prisma/client": "^2.25.0",
"@sendou/react-sketch": "^0.5.2",
"@trpc/client": "5.0.0",
"@trpc/react": "5.0.0",
"@trpc/server": "5.0.0",
"countries-list": "^2.6.1",
"framer-motion": "^4.1.17",
"next": "^11.1.0",
@ -40,12 +37,10 @@
"react-icons": "^4.2.0",
"react-infinite-scroller": "^1.2.4",
"react-markdown": "^5.0.3",
"react-query": "^3.17.2",
"react-select": "^4.3.1",
"react-string-replace": "^0.4.4",
"recharts": "^2.1.2",
"remark-gfm": "^1.0.0",
"superjson": "^1.7.4",
"swr": "^0.5.6",
"ts-trueskill": "^3.2.2",
"uuid": "^8.3.2",
@ -2096,43 +2091,6 @@
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.3.tgz",
"integrity": "sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg=="
},
"node_modules/@trpc/client": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-5.0.0.tgz",
"integrity": "sha512-w7rDaDeTB2vUsZqctjnB0A7f5m1oLzpCH+LMZreoHJevrN4idT7ooicTaFs7YYobm2Ww9iGVWKTAy2FiO4ihlg==",
"dependencies": {
"@babel/runtime": "^7.9.0",
"@trpc/server": "^5.0.0"
}
},
"node_modules/@trpc/react": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@trpc/react/-/react-5.0.0.tgz",
"integrity": "sha512-TLcbkiXoZFSUdmJNfrvXKOsLRT3exwQM9sf2St271fceouRZWRNpRaUyw8exAdRwqr12FW5MjlbQ7WidvaRKbw==",
"dependencies": {
"@babel/runtime": "^7.9.0",
"@trpc/client": "^5.0.0",
"@trpc/server": "^5.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
"react-query": "^3.0.0"
}
},
"node_modules/@trpc/server": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-5.0.0.tgz",
"integrity": "sha512-XI9qbrbZplA3Lrt7od3+zg3BUcP5zVJpZuSQ6wBQObBcqj5SU49aDnGxEoYYgBXAvUzuzToGvHFL+jAr8K72fw==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@trpc/server/node_modules/tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
},
"node_modules/@tsconfig/node10": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.7.tgz",
@ -2885,14 +2843,6 @@
"is-decimal": "^1.0.0"
}
},
"node_modules/big-integer": {
"version": "1.6.48",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@ -2945,20 +2895,6 @@
"node": ">=8"
}
},
"node_modules/broadcast-channel": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.5.3.tgz",
"integrity": "sha512-OLOXfwReZa2AAAh9yOUyiALB3YxBe0QpThwwuyRHLgpl8bSznSDmV6Mz7LeBJg1VZsMcDcNMy7B53w12qHrIhQ==",
"dependencies": {
"@babel/runtime": "^7.7.2",
"detect-node": "^2.0.4",
"js-sha3": "0.8.0",
"microseconds": "0.2.0",
"nano-time": "1.0.0",
"rimraf": "3.0.2",
"unload": "2.2.0"
}
},
"node_modules/brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
@ -3752,11 +3688,6 @@
"minimalistic-assert": "^1.0.0"
}
},
"node_modules/detect-node": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.5.tgz",
"integrity": "sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw=="
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
@ -5883,11 +5814,6 @@
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-sha3": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -6149,7 +6075,8 @@
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
@ -6302,15 +6229,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/match-sorter": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.0.tgz",
"integrity": "sha512-efYOf/wUpNb8FgNY+cOD2EIJI1S5I7YPKsw0LBp7wqPh5pmMS6i/wr3ZWwfwrAw1NvqTA2KUReVRWDX84lUcOQ==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"remove-accents": "0.4.2"
}
},
"node_modules/material-colors": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
@ -6632,11 +6550,6 @@
"node": ">=8"
}
},
"node_modules/microseconds": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
"integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA=="
},
"node_modules/miller-rabin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
@ -6732,14 +6645,6 @@
"thenify-all": "^1.0.0"
}
},
"node_modules/nano-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
"integrity": "sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=",
"dependencies": {
"big-integer": "^1.6.16"
}
},
"node_modules/nanoid": {
"version": "3.1.25",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz",
@ -8140,31 +8045,6 @@
"react": "^16.8.0 || ^17"
}
},
"node_modules/react-query": {
"version": "3.17.2",
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.17.2.tgz",
"integrity": "sha512-icMtYL/+jUv0Q3n8GRz6CTyMYKh+l5EBKwhkr8qhbtYRORr43YGiN6EJSyw9WFaSQ9bwD1G9ZhaFxow9NMV0Sw==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"broadcast-channel": "^3.4.1",
"match-sorter": "^6.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
@ -8575,11 +8455,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/remove-accents": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
"integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U="
},
"node_modules/repeat-string": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
@ -8682,6 +8557,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
},
@ -9186,18 +9062,6 @@
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.7.tgz",
"integrity": "sha512-OFFeUXFgwnGOKvEXaSv0D0KQ5ADP0n6g3SVONx6I/85JzNZ3u50FRwB3lVIk1QO2HNdI75tbVzc4Z66Gdp9voA=="
},
"node_modules/superjson": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-1.7.4.tgz",
"integrity": "sha512-A6DYTe04+x4L9NPywHeGZNy6/gLe8qqKCwhEfTH9M4eXpTjiTsF83JZ3j4hwXx1ogRb4779nWxsDlJGIECOJkw==",
"dependencies": {
"debug": "^4.3.1",
"lodash.clonedeep": "^4.5.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -9782,15 +9646,6 @@
"node": ">= 10.0.0"
}
},
"node_modules/unload": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
"integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
"dependencies": {
"@babel/runtime": "^7.6.2",
"detect-node": "^2.0.4"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -11818,40 +11673,6 @@
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.3.tgz",
"integrity": "sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg=="
},
"@trpc/client": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-5.0.0.tgz",
"integrity": "sha512-w7rDaDeTB2vUsZqctjnB0A7f5m1oLzpCH+LMZreoHJevrN4idT7ooicTaFs7YYobm2Ww9iGVWKTAy2FiO4ihlg==",
"requires": {
"@babel/runtime": "^7.9.0",
"@trpc/server": "^5.0.0"
}
},
"@trpc/react": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@trpc/react/-/react-5.0.0.tgz",
"integrity": "sha512-TLcbkiXoZFSUdmJNfrvXKOsLRT3exwQM9sf2St271fceouRZWRNpRaUyw8exAdRwqr12FW5MjlbQ7WidvaRKbw==",
"requires": {
"@babel/runtime": "^7.9.0",
"@trpc/client": "^5.0.0",
"@trpc/server": "^5.0.0"
}
},
"@trpc/server": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-5.0.0.tgz",
"integrity": "sha512-XI9qbrbZplA3Lrt7od3+zg3BUcP5zVJpZuSQ6wBQObBcqj5SU49aDnGxEoYYgBXAvUzuzToGvHFL+jAr8K72fw==",
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
}
}
},
"@tsconfig/node10": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.7.tgz",
@ -12475,11 +12296,6 @@
"is-decimal": "^1.0.0"
}
},
"big-integer": {
"version": "1.6.48",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
},
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@ -12523,20 +12339,6 @@
"fill-range": "^7.0.1"
}
},
"broadcast-channel": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.5.3.tgz",
"integrity": "sha512-OLOXfwReZa2AAAh9yOUyiALB3YxBe0QpThwwuyRHLgpl8bSznSDmV6Mz7LeBJg1VZsMcDcNMy7B53w12qHrIhQ==",
"requires": {
"@babel/runtime": "^7.7.2",
"detect-node": "^2.0.4",
"js-sha3": "0.8.0",
"microseconds": "0.2.0",
"nano-time": "1.0.0",
"rimraf": "3.0.2",
"unload": "2.2.0"
}
},
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
@ -13202,11 +13004,6 @@
"minimalistic-assert": "^1.0.0"
}
},
"detect-node": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.5.tgz",
"integrity": "sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw=="
},
"detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
@ -14890,11 +14687,6 @@
"@panva/asn1.js": "^1.0.0"
}
},
"js-sha3": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -15116,7 +14908,8 @@
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
"lodash.debounce": {
"version": "4.0.8",
@ -15249,15 +15042,6 @@
"repeat-string": "^1.0.0"
}
},
"match-sorter": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.0.tgz",
"integrity": "sha512-efYOf/wUpNb8FgNY+cOD2EIJI1S5I7YPKsw0LBp7wqPh5pmMS6i/wr3ZWwfwrAw1NvqTA2KUReVRWDX84lUcOQ==",
"requires": {
"@babel/runtime": "^7.12.5",
"remove-accents": "0.4.2"
}
},
"material-colors": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
@ -15489,11 +15273,6 @@
"picomatch": "^2.0.5"
}
},
"microseconds": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
"integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA=="
},
"miller-rabin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
@ -15570,14 +15349,6 @@
"thenify-all": "^1.0.0"
}
},
"nano-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
"integrity": "sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=",
"requires": {
"big-integer": "^1.6.16"
}
},
"nanoid": {
"version": "3.1.25",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz",
@ -16703,16 +16474,6 @@
"warning": "^4.0.2"
}
},
"react-query": {
"version": "3.17.2",
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.17.2.tgz",
"integrity": "sha512-icMtYL/+jUv0Q3n8GRz6CTyMYKh+l5EBKwhkr8qhbtYRORr43YGiN6EJSyw9WFaSQ9bwD1G9ZhaFxow9NMV0Sw==",
"requires": {
"@babel/runtime": "^7.5.5",
"broadcast-channel": "^3.4.1",
"match-sorter": "^6.0.2"
}
},
"react-refresh": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
@ -17028,11 +16789,6 @@
"mdast-util-from-markdown": "^0.8.0"
}
},
"remove-accents": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
"integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U="
},
"repeat-string": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
@ -17109,6 +16865,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
@ -17539,15 +17296,6 @@
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.7.tgz",
"integrity": "sha512-OFFeUXFgwnGOKvEXaSv0D0KQ5ADP0n6g3SVONx6I/85JzNZ3u50FRwB3lVIk1QO2HNdI75tbVzc4Z66Gdp9voA=="
},
"superjson": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-1.7.4.tgz",
"integrity": "sha512-A6DYTe04+x4L9NPywHeGZNy6/gLe8qqKCwhEfTH9M4eXpTjiTsF83JZ3j4hwXx1ogRb4779nWxsDlJGIECOJkw==",
"requires": {
"debug": "^4.3.1",
"lodash.clonedeep": "^4.5.0"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -17931,15 +17679,6 @@
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
},
"unload": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
"integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
"requires": {
"@babel/runtime": "^7.6.2",
"detect-node": "^2.0.4"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

View File

@ -35,9 +35,6 @@
"@next/bundle-analyzer": "^11.1.0",
"@prisma/client": "^2.25.0",
"@sendou/react-sketch": "^0.5.2",
"@trpc/client": "5.0.0",
"@trpc/react": "5.0.0",
"@trpc/server": "5.0.0",
"countries-list": "^2.6.1",
"framer-motion": "^4.1.17",
"next": "^11.1.0",
@ -56,12 +53,10 @@
"react-icons": "^4.2.0",
"react-infinite-scroller": "^1.2.4",
"react-markdown": "^5.0.3",
"react-query": "^3.17.2",
"react-select": "^4.3.1",
"react-string-replace": "^0.4.4",
"recharts": "^2.1.2",
"remark-gfm": "^1.0.0",
"superjson": "^1.7.4",
"swr": "^0.5.6",
"ts-trueskill": "^3.2.2",
"uuid": "^8.3.2",

View File

@ -7,13 +7,10 @@ import { DefaultSeo } from "next-seo";
import type { AppProps } from "next/app";
import { Router } from "next/router";
import NProgress from "nprogress";
import { useEffect, useState } from "react";
import { QueryClient, QueryClientProvider } from "react-query";
import { Hydrate } from "react-query/hydration";
import { useEffect } from "react";
import { CSSVariables } from "utils/CSSVariables";
import { activateLocale } from "utils/i18n";
import { locales } from "utils/lists/locales";
import { trpc } from "utils/trpc";
import "./styles.css";
NProgress.configure({ showSpinner: false });
@ -149,18 +146,6 @@ const setDisplayedLanguage = () => {
const MyApp = ({ Component, pageProps }: AppProps) => {
useEffect(setDisplayedLanguage, []);
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// queries never go stale to save some work
// on our poor database
staleTime: Infinity,
},
},
})
);
return (
<>
@ -191,13 +176,7 @@ const MyApp = ({ Component, pageProps }: AppProps) => {
<ChakraProvider theme={extendedTheme} cssVarsRoot="body">
<I18nProvider i18n={i18n}>
<Layout>
<QueryClientProvider client={queryClient}>
<Hydrate
state={trpc.useDehydratedState(pageProps.dehydratedState)}
>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
<Component {...pageProps} />
</Layout>
</I18nProvider>
</ChakraProvider>

19
pages/api/plus/index.ts Normal file
View File

@ -0,0 +1,19 @@
import { NextApiRequest, NextApiResponse } from "next";
import { createHandler } from "utils/api";
import plusService, { PlusStatuses } from "services/plus";
import { Serialized } from "utils/types";
export type PlusStatusesGet = Serialized<PlusStatuses>;
const GET = async (
_req: NextApiRequest,
res: NextApiResponse<PlusStatuses>
) => {
const statuses = await plusService.getPlusStatuses();
res.status(200).json(statuses);
};
const plushandler = (req: NextApiRequest, res: NextApiResponse) =>
createHandler(req, res, { GET });
export default plushandler;

View File

@ -0,0 +1,36 @@
import { NextApiRequest, NextApiResponse } from "next";
import { createHandler, getMySession } from "utils/api";
import plusService, { Suggestions } from "services/plus";
import { Serialized } from "utils/types";
import { suggestionFullSchema } from "utils/validators/suggestion";
export type SuggestionsGet = Serialized<Suggestions>;
const GET = async (_req: NextApiRequest, res: NextApiResponse<Suggestions>) => {
const suggestions = await plusService.getSuggestions();
res.status(200).json(suggestions);
};
const POST = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getMySession(req);
if (!user) {
return res.status(401).end();
}
const parsed = suggestionFullSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json(parsed.error);
}
await plusService.addSuggestion({
input: parsed.data,
userId: user.id,
});
res.status(200).end();
};
const suggestionsHandler = (req: NextApiRequest, res: NextApiResponse) =>
createHandler(req, res, { GET, POST });
export default suggestionsHandler;

View File

@ -0,0 +1,24 @@
import { NextApiRequest, NextApiResponse } from "next";
import { createHandler, getMySession } from "utils/api";
import plusService, { GetUsersForVoting } from "services/plus";
import { Serialized } from "utils/types";
export type UsersForVotingGet = Serialized<GetUsersForVoting>;
const GET = async (
req: NextApiRequest,
res: NextApiResponse<GetUsersForVoting>
) => {
const user = await getMySession(req);
if (!user) {
return res.status(401).end();
}
const usersForVoting = await plusService.getUsersForVoting(user.id);
res.status(200).json(usersForVoting);
};
const usersForVotingHandler = (req: NextApiRequest, res: NextApiResponse) =>
createHandler(req, res, { GET });
export default usersForVotingHandler;

61
pages/api/plus/votes.ts Normal file
View File

@ -0,0 +1,61 @@
import { NextApiRequest, NextApiResponse } from "next";
import { createHandler, getMySession } from "utils/api";
import plusService, { VotedUserScores } from "services/plus";
import { Serialized } from "utils/types";
import { voteSchema, votesSchema } from "utils/validators/votes";
export type VotesGet = Serialized<VotedUserScores>;
const GET = async (
req: NextApiRequest,
res: NextApiResponse<VotedUserScores>
) => {
const user = await getMySession(req);
if (!user) {
return res.status(401).end();
}
const votedUserScores = await plusService.votedUserScores(user.id);
res.status(200).json(votedUserScores);
};
const POST = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getMySession(req);
if (!user) {
return res.status(401).end();
}
const parsed = votesSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json(parsed.error);
}
const votedUserScores = await plusService.addVotes({
input: parsed.data,
userId: user.id,
});
res.status(200).json(votedUserScores);
};
const PATCH = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getMySession(req);
if (!user) {
return res.status(401).end();
}
const parsed = voteSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json(parsed.error);
}
await plusService.editVote({ input: parsed.data, userId: user.id });
res.status(200).end();
};
const votesHandler = (req: NextApiRequest, res: NextApiResponse) =>
createHandler(req, res, { GET, POST, PATCH });
export default votesHandler;

28
pages/api/plus/vouches.ts Normal file
View File

@ -0,0 +1,28 @@
import { NextApiRequest, NextApiResponse } from "next";
import plusService from "services/plus";
import { createHandler, getMySession } from "utils/api";
import { vouchSchema } from "utils/validators/vouch";
const POST = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getMySession(req);
if (!user) {
return res.status(401).end();
}
const parsed = vouchSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json(parsed.error);
}
await plusService.addVouch({
input: parsed.data,
userId: user.id,
});
res.status(200).end();
};
const vouchesHandler = (req: NextApiRequest, res: NextApiResponse) =>
createHandler(req, res, { POST });
export default vouchesHandler;

View File

@ -1,40 +0,0 @@
import * as trpc from "@trpc/server";
import { inferAsyncReturnType, inferProcedureOutput } from "@trpc/server";
import * as trpcNext from "@trpc/server/adapters/next";
import plusApi from "routers/plus";
import superjson from "superjson";
import { getMySession } from "utils/api";
import { trpc as trcpReactQuery } from "utils/trpc";
const createContext = async ({ req }: trpcNext.CreateNextContextOptions) => {
const user = await getMySession(req);
return { user };
};
type Context = inferAsyncReturnType<typeof createContext>;
export function createRouter() {
return trpc.router<Context>();
}
// Important: only use this export with SSR/SSG
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
export type AppRouter = typeof appRouter;
/**
* This is a helper method to infer the output of a query resolver
* @example type HelloOutput = inferQueryOutput<'hello'>
*/
export type inferQueryOutput<
TRouteKey extends keyof AppRouter["_def"]["queries"]
> = inferProcedureOutput<AppRouter["_def"]["queries"][TRouteKey]>;
export const ssr = trcpReactQuery.ssr(appRouter, { user: null });
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
transformer: superjson,
});

View File

@ -17,24 +17,34 @@ import VouchesList from "components/plus/VouchesList";
import VouchModal from "components/plus/VouchModal";
import { useUser } from "hooks/common";
import { usePlusHomePage } from "hooks/plus";
import { ssr } from "pages/api/trpc/[trpc]";
import { Fragment } from "react";
import { getVotingRange } from "utils/plus";
import { getFullUsername } from "utils/strings";
import plusService, { PlusStatuses, VotingProgress } from "services/plus";
import { Serialized } from "utils/types";
import { serializeDataForGetStaticProps } from "utils/objects";
const PlusHomePage = () => {
const PlusHomePage = ({
statuses,
votingProgress,
}: {
statuses: Serialized<PlusStatuses>;
votingProgress: VotingProgress;
}) => {
const [user] = useUser();
const {
plusStatusData,
plusStatusDataLoading,
vouchStatuses,
suggestionsData,
suggestionsLoading,
ownSuggestion,
suggestionCounts,
setSuggestionsFilter,
vouchedPlusStatusData,
votingProgress,
} = usePlusHomePage();
} = usePlusHomePage(statuses);
if (plusStatusDataLoading) return null;
if (!plusStatusData?.membershipTier) {
return (
<Box>
@ -44,7 +54,11 @@ const PlusHomePage = () => {
<Heading size="md">Suggested players this month:</Heading>
<Flex flexWrap="wrap" data-cy="alt-suggestions-container">
{suggestionsData
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
.sort(
(a, b) =>
new Date(a.createdAt).getTime() -
new Date(b.createdAt).getTime()
)
.map((suggestion) => (
<Box
key={suggestion.tier + "+" + suggestion.suggestedUser.id}
@ -142,7 +156,10 @@ const PlusHomePage = () => {
{plusStatusData?.canVouchAgainAfter && (
<Box>
Can vouch again after:{" "}
{plusStatusData.canVouchAgainAfter.toLocaleDateString()}{" "}
{new Date(
// @ts-expect-error TODO: make Serialized<T> handle null union
plusStatusData.canVouchAgainAfter
).toLocaleDateString()}{" "}
(resets after voting)
</Box>
)}
@ -205,7 +222,7 @@ const PlusHomePage = () => {
</Box>
)}
{suggestionCounts.ONE + suggestionCounts.TWO + suggestionCounts.THREE ===
0 ? (
0 && !suggestionsLoading ? (
<Box mt={4}>No suggestions yet for this month</Box>
) : (
<>
@ -239,14 +256,14 @@ const PlusHomePage = () => {
};
export const getStaticProps = async () => {
await Promise.all([
ssr.prefetchQuery("plus.suggestions"),
ssr.prefetchQuery("plus.statuses"),
const [statuses, votingProgress] = await Promise.all([
plusService.getPlusStatuses(),
getVotingRange().isHappening ? plusService.votingProgress() : null,
]);
return {
props: {
dehydratedState: ssr.dehydrate(),
statuses: serializeDataForGetStaticProps(statuses),
votingProgress: serializeDataForGetStaticProps(votingProgress),
},
revalidate: 60,
};

View File

@ -27,7 +27,7 @@ export default function PlusVotingPage() {
previousUser,
goBack,
submit,
voteStatus,
voteMutating,
votedUsers,
editVote,
isLoadingEditVote,
@ -172,7 +172,7 @@ export default function PlusVotingPage() {
{previousUser && !currentUser && (
<Box onClick={submit} mt={6} textAlign="center">
{" "}
<Button isLoading={voteStatus === "loading"}>Submit</Button>
<Button isLoading={voteMutating}>Submit</Button>
</Box>
)}
</>

View File

@ -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;

View File

@ -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(

View File

@ -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<typeof votedUserScores>;
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<number, number>();
const result: Record<number, number> = {};
for (const ballot of ballots) {
result.set(ballot.votedId, ballot.score);
result[ballot.votedId] = ballot.score;
}
return result;
};
export type VotingProgress = Prisma.PromiseReturnType<typeof votingProgress>;
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<number, "EU" | "NA">();
@ -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({

View File

@ -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<User | null> => {
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<any>
>
>

View File

@ -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<AppRouter>({
url: "/api/trpc",
transformer: superjson,
});
// create react query hooks for trpc
export const trpc = createReactQueryHooks({
client,
});