mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-29 18:26:34 +02:00
⚡️ Optimize participant query
This commit is contained in:
parent
bc521f7ecb
commit
d5fc45c506
8 changed files with 33 additions and 271 deletions
|
@ -1,9 +1,10 @@
|
||||||
import { trpc } from "@rallly/backend";
|
import { trpc } from "@rallly/backend";
|
||||||
import { Participant, Vote, VoteType } from "@rallly/database";
|
import { Participant, VoteType } from "@rallly/database";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { useVisibility } from "@/components/visibility";
|
import { useVisibility } from "@/components/visibility";
|
||||||
import { usePermissions } from "@/contexts/permissions";
|
import { usePermissions } from "@/contexts/permissions";
|
||||||
|
import { Vote } from "@/utils/trpc/types";
|
||||||
|
|
||||||
import { useRequiredContext } from "./use-required-context";
|
import { useRequiredContext } from "./use-required-context";
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Participant, Vote, VoteType } from "@rallly/database";
|
import { Participant, VoteType } from "@rallly/database";
|
||||||
import { TrashIcon } from "@rallly/icons";
|
import { TrashIcon } from "@rallly/icons";
|
||||||
import { keyBy } from "lodash";
|
import { keyBy } from "lodash";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
@ -10,7 +10,7 @@ import {
|
||||||
ParsedTimeSlotOption,
|
ParsedTimeSlotOption,
|
||||||
} from "@/utils/date-time-utils";
|
} from "@/utils/date-time-utils";
|
||||||
import { useDayjs } from "@/utils/dayjs";
|
import { useDayjs } from "@/utils/dayjs";
|
||||||
import { GetPollApiResponse } from "@/utils/trpc/types";
|
import { GetPollApiResponse, Vote } from "@/utils/trpc/types";
|
||||||
|
|
||||||
import ErrorPage from "./error-page";
|
import ErrorPage from "./error-page";
|
||||||
import { useParticipants } from "./participants-provider";
|
import { useParticipants } from "./participants-provider";
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Participant, Vote, VoteType } from "@rallly/database";
|
import { Participant, VoteType } from "@rallly/database";
|
||||||
import { MoreHorizontalIcon } from "@rallly/icons";
|
import { MoreHorizontalIcon } from "@rallly/icons";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
@ -8,6 +8,7 @@ import { ParticipantDropdown } from "@/components/participant-dropdown";
|
||||||
import { usePoll } from "@/components/poll-context";
|
import { usePoll } from "@/components/poll-context";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
import { usePermissions } from "@/contexts/permissions";
|
import { usePermissions } from "@/contexts/permissions";
|
||||||
|
import { Vote } from "@/utils/trpc/types";
|
||||||
|
|
||||||
import UserAvatar from "../user-avatar";
|
import UserAvatar from "../user-avatar";
|
||||||
import VoteIcon from "../vote-icon";
|
import VoteIcon from "../vote-icon";
|
||||||
|
|
|
@ -38,31 +38,22 @@ const useScoreByOptionId = () => {
|
||||||
const { options } = usePoll();
|
const { options } = usePoll();
|
||||||
|
|
||||||
return React.useMemo(() => {
|
return React.useMemo(() => {
|
||||||
const res = options.reduce<Record<string, OptionScore>>((acc, option) => {
|
const scoreByOptionId: Record<string, OptionScore> = {};
|
||||||
acc[option.id] = { yes: [], ifNeedBe: [], no: [] };
|
options.forEach((option) => {
|
||||||
return acc;
|
scoreByOptionId[option.id] = {
|
||||||
}, {});
|
yes: [],
|
||||||
|
ifNeedBe: [],
|
||||||
|
no: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const votes = responses.flatMap((response) => response.votes);
|
responses?.forEach((response) => {
|
||||||
|
response.votes.forEach((vote) => {
|
||||||
|
scoreByOptionId[vote.optionId][vote.type].push(response.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
for (const vote of votes) {
|
return scoreByOptionId;
|
||||||
if (!res[vote.optionId]) {
|
|
||||||
res[vote.optionId] = { yes: [], ifNeedBe: [], no: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (vote.type) {
|
|
||||||
case "yes":
|
|
||||||
res[vote.optionId].yes.push(vote.participantId);
|
|
||||||
break;
|
|
||||||
case "ifNeedBe":
|
|
||||||
res[vote.optionId].ifNeedBe.push(vote.participantId);
|
|
||||||
break;
|
|
||||||
case "no":
|
|
||||||
res[vote.optionId].no.push(vote.participantId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}, [responses, options]);
|
}, [responses, options]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -195,7 +186,7 @@ export const FinalizePollForm = ({
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{max < options.length ? (
|
{max < options.length ? (
|
||||||
<div className="absolute bottom-0 mt-2 w-full bg-gradient-to-t from-white via-white to-white/10 py-8 px-3">
|
<div className="absolute bottom-0 mt-2 w-full bg-gradient-to-t from-white via-white to-white/10 px-3 py-8">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|
|
@ -1,209 +0,0 @@
|
||||||
import { cn } from "@rallly/ui";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
} from "@rallly/ui/form";
|
|
||||||
import { RadioGroup, RadioGroupItem } from "@rallly/ui/radio-group";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { Trans } from "next-i18next";
|
|
||||||
import React from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { DateIcon } from "@/components/date-icon";
|
|
||||||
import { useParticipants } from "@/components/participants-provider";
|
|
||||||
import { ConnectedScoreSummary } from "@/components/poll/score-summary";
|
|
||||||
import { VoteSummaryProgressBar } from "@/components/vote-summary-progress-bar";
|
|
||||||
import { useDateFormatter, usePoll } from "@/contexts/poll";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
|
||||||
selectedOptionId: z.string(),
|
|
||||||
notify: z.enum(["everyone", "available", "noone"]),
|
|
||||||
});
|
|
||||||
|
|
||||||
type FinalizeFormData = z.infer<typeof formSchema>;
|
|
||||||
|
|
||||||
type OptionScore = {
|
|
||||||
yes: string[];
|
|
||||||
ifNeedBe: string[];
|
|
||||||
no: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const useScoreByOptionId = () => {
|
|
||||||
const { participants: responses } = useParticipants();
|
|
||||||
const { options } = usePoll();
|
|
||||||
|
|
||||||
return React.useMemo(() => {
|
|
||||||
const res = options.reduce<Record<string, OptionScore>>((acc, option) => {
|
|
||||||
acc[option.id] = { yes: [], ifNeedBe: [], no: [] };
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const votes = responses.flatMap((response) => response.votes);
|
|
||||||
|
|
||||||
for (const vote of votes) {
|
|
||||||
if (!res[vote.optionId]) {
|
|
||||||
res[vote.optionId] = { yes: [], ifNeedBe: [], no: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (vote.type) {
|
|
||||||
case "yes":
|
|
||||||
res[vote.optionId].yes.push(vote.participantId);
|
|
||||||
break;
|
|
||||||
case "ifNeedBe":
|
|
||||||
res[vote.optionId].ifNeedBe.push(vote.participantId);
|
|
||||||
break;
|
|
||||||
case "no":
|
|
||||||
res[vote.optionId].no.push(vote.participantId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}, [responses, options]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FinalizePollForm = ({
|
|
||||||
name,
|
|
||||||
onSubmit,
|
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
onSubmit: (data: FinalizeFormData) => void;
|
|
||||||
}) => {
|
|
||||||
const poll = usePoll();
|
|
||||||
const [max, setMax] = React.useState(5);
|
|
||||||
|
|
||||||
const scoreByOptionId = useScoreByOptionId();
|
|
||||||
const { participants } = useParticipants();
|
|
||||||
|
|
||||||
const options = [...poll.options]
|
|
||||||
.sort((a, b) => {
|
|
||||||
const aYes = scoreByOptionId[a.id].yes.length;
|
|
||||||
const bYes = scoreByOptionId[b.id].yes.length;
|
|
||||||
|
|
||||||
if (aYes !== bYes) {
|
|
||||||
return bYes - aYes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aIfNeedBe = scoreByOptionId[a.id].ifNeedBe.length;
|
|
||||||
const bIfNeedBe = scoreByOptionId[b.id].ifNeedBe.length;
|
|
||||||
|
|
||||||
return bIfNeedBe - aIfNeedBe;
|
|
||||||
})
|
|
||||||
.map((option) => {
|
|
||||||
return { ...option, votes: scoreByOptionId[option.id] };
|
|
||||||
});
|
|
||||||
|
|
||||||
const dateFormatter = useDateFormatter();
|
|
||||||
const form = useForm<FinalizeFormData>({
|
|
||||||
defaultValues: {
|
|
||||||
selectedOptionId: options[0].id,
|
|
||||||
notify: "everyone",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id={name}
|
|
||||||
className="space-y-4"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="selectedOptionId"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem className="relative">
|
|
||||||
<FormLabel htmlFor={field.name}>
|
|
||||||
<Trans i18nKey="dates" />
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
className="grid gap-2"
|
|
||||||
>
|
|
||||||
{options.slice(0, max).map((option) => {
|
|
||||||
const start = dateFormatter(option.start);
|
|
||||||
const end = dateFormatter(
|
|
||||||
dayjs(option.start).add(option.duration, "minute"),
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
key={option.id}
|
|
||||||
htmlFor={option.id}
|
|
||||||
className={cn(
|
|
||||||
"group flex select-none items-center gap-4 rounded-md border bg-white p-3 text-base",
|
|
||||||
field.value === option.id
|
|
||||||
? "bg-primary-50 border-primary ring-primary ring-1"
|
|
||||||
: "hover:bg-gray-50",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="hidden">
|
|
||||||
<RadioGroupItem id={option.id} value={option.id} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<DateIcon date={start} />
|
|
||||||
</div>
|
|
||||||
<div className="grow">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="grow whitespace-nowrap">
|
|
||||||
<div className="text-sm font-semibold">
|
|
||||||
{option.duration > 0
|
|
||||||
? start.format("LL")
|
|
||||||
: start.format("LL")}
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground text-sm">
|
|
||||||
{option.duration > 0 ? (
|
|
||||||
`${start.format("LT")} - ${end.format(
|
|
||||||
"LT",
|
|
||||||
)}`
|
|
||||||
) : (
|
|
||||||
<Trans
|
|
||||||
i18nKey="allDay"
|
|
||||||
defaults="All day"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ConnectedScoreSummary optionId={option.id} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2">
|
|
||||||
<VoteSummaryProgressBar
|
|
||||||
{...scoreByOptionId[option.id]}
|
|
||||||
total={participants.length}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</RadioGroup>
|
|
||||||
</FormControl>
|
|
||||||
{max < options.length ? (
|
|
||||||
<div className="absolute bottom-0 mt-2 w-full bg-gradient-to-t from-white via-white to-white/10 py-8 px-3">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
setMax((oldMax) => oldMax + 5);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trans i18nKey="showMore" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -25,38 +25,6 @@ export type OptionScore = {
|
||||||
no: string[];
|
no: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useScoreByOptionId = () => {
|
|
||||||
const { data: responses = [] } = useCurrentPollResponses();
|
|
||||||
const { data: options = [] } = useCurrentPollOptions();
|
|
||||||
return React.useMemo(() => {
|
|
||||||
const res = options.reduce<Record<string, OptionScore>>((acc, option) => {
|
|
||||||
acc[option.id] = { yes: [], ifNeedBe: [], no: [] };
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const votes = responses.flatMap((response) => response.votes);
|
|
||||||
|
|
||||||
for (const vote of votes) {
|
|
||||||
if (!res[vote.optionId]) {
|
|
||||||
res[vote.optionId] = { yes: [], ifNeedBe: [], no: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (vote.type) {
|
|
||||||
case "yes":
|
|
||||||
res[vote.optionId].yes.push(vote.participantId);
|
|
||||||
break;
|
|
||||||
case "ifNeedBe":
|
|
||||||
res[vote.optionId].ifNeedBe.push(vote.participantId);
|
|
||||||
break;
|
|
||||||
case "no":
|
|
||||||
res[vote.optionId].no.push(vote.participantId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}, [responses, options]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCurrentPollOptions = () => {
|
export const useCurrentPollOptions = () => {
|
||||||
const pollId = useCurrentEventId();
|
const pollId = useCurrentEventId();
|
||||||
return trpc.polls.options.list.useQuery({ pollId });
|
return trpc.polls.options.list.useQuery({ pollId });
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { User } from "@rallly/database";
|
import { User, VoteType } from "@rallly/database";
|
||||||
|
|
||||||
export type GetPollApiResponse = {
|
export type GetPollApiResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -16,3 +16,8 @@ export type GetPollApiResponse = {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
deleted: boolean;
|
deleted: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Vote = {
|
||||||
|
optionId: string;
|
||||||
|
type: VoteType;
|
||||||
|
};
|
||||||
|
|
|
@ -21,7 +21,12 @@ export const participants = router({
|
||||||
pollId,
|
pollId,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
votes: true,
|
votes: {
|
||||||
|
select: {
|
||||||
|
optionId: true,
|
||||||
|
type: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Reference in a new issue