Skip to content

Commit

Permalink
If need be (lukevella#168)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukevella authored May 13, 2022
1 parent 6375e80 commit 17dc951
Show file tree
Hide file tree
Showing 48 changed files with 1,030 additions and 639 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ yarn-error.log*

# playwright
/playwright-report
/test-results
/test-results

# ts
tsconfig.tsbuildinfo
4 changes: 2 additions & 2 deletions api-client/add-participant.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Participant, Vote } from "@prisma/client";
import { Participant, Vote, VoteType } from "@prisma/client";
import axios from "axios";

export interface AddParticipantPayload {
pollId: string;
name: string;
votes: string[];
votes: Array<{ optionId: string; type: VoteType }>;
}

export type AddParticipantResponse = Participant & {
Expand Down
2 changes: 1 addition & 1 deletion api-client/get-poll.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Link, Option, Participant, Poll, User, Vote } from "@prisma/client";

export interface GetPollApiResponse extends Omit<Poll, "verificationCode"> {
options: Array<Option & { votes: Vote[] }>;
options: Option[];
participants: Array<Participant & { votes: Vote[] }>;
user: User;
role: "admin" | "participant";
Expand Down
11 changes: 8 additions & 3 deletions api-client/update-participant.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { Participant, Vote, VoteType } from "@prisma/client";
import axios from "axios";

export interface UpdateParticipantPayload {
pollId: string;
participantId: string;
name: string;
votes: string[];
votes: Array<{ optionId: string; type: VoteType }>;
}

export const updateParticipant = async (
payload: UpdateParticipantPayload,
): Promise<void> => {
): Promise<Participant & { votes: Vote[] }> => {
const { pollId, participantId, ...body } = payload;
await axios.patch(`/api/poll/${pollId}/participant/${participantId}`, body);
const res = await axios.patch<Participant & { votes: Vote[] }>(
`/api/poll/${pollId}/participant/${participantId}`,
body,
);
return res.data;
};
4 changes: 2 additions & 2 deletions components/date-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const DateCard: React.VoidFunctionComponent<DateCardProps> = ({
return (
<div
className={clsx(
"relative inline-block h-14 w-14 rounded-md border bg-white text-center shadow-md shadow-slate-100",
"relative mt-1 inline-block h-14 w-14 rounded-md border bg-white text-center shadow-md shadow-slate-100",
className,
)}
>
Expand All @@ -31,7 +31,7 @@ const DateCard: React.VoidFunctionComponent<DateCardProps> = ({
{dow}
</span>
</div>
<div className="-mb-1 text-center text-lg text-red-500">{day}</div>
<div className="-mb-1 text-center text-lg text-rose-500">{day}</div>
<div className="text-center text-xs font-semibold uppercase text-gray-800">
{month}
</div>
Expand Down
17 changes: 6 additions & 11 deletions components/home/poll-demo.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { format } from "date-fns";
import { useTranslation } from "next-i18next";
import * as React from "react";
import { useTimeoutFn } from "react-use";

import DateCard from "../date-card";
import Score from "../poll/desktop-poll/score";
import { ScoreSummary } from "../poll/score-summary";
import UserAvatar from "../poll/user-avatar";
import VoteIcon from "../poll/vote-icon";

Expand Down Expand Up @@ -36,10 +35,6 @@ const options = ["2022-12-14", "2022-12-15", "2022-12-16", "2022-12-17"];

const PollDemo: React.VoidFunctionComponent = () => {
const { t } = useTranslation("app");
const [bestOption, setBestOption] = React.useState<number>();
useTimeoutFn(() => {
setBestOption(2);
}, 1500);

return (
<div
Expand All @@ -48,7 +43,7 @@ const PollDemo: React.VoidFunctionComponent = () => {
>
<div className="flex border-b shadow-sm">
<div
className="flex shrink-0 items-center py-4 pl-4 pr-2 font-medium"
className="flex shrink-0 items-center py-2 pl-4 pr-2 font-medium"
style={{ width: sidebarWidth }}
>
<div className="flex h-full grow items-end">
Expand All @@ -66,17 +61,17 @@ const PollDemo: React.VoidFunctionComponent = () => {
return (
<div
key={i}
className="shrink-0 py-4 text-center transition-colors"
className="shrink-0 space-y-3 py-2 pt-3 text-center transition-colors"
style={{ width: 100 }}
>
<DateCard
day={format(d, "dd")}
dow={format(d, "E")}
month={format(d, "MMM")}
annotation={
<Score count={score} highlight={i === bestOption} />
}
/>
<div>
<ScoreSummary yesScore={score} compact={true} />
</div>
</div>
);
})}
Expand Down
3 changes: 3 additions & 0 deletions components/icons/fire.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions components/icons/if-need-be.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions components/icons/star.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions components/icons/user-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions components/icons/x-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 42 additions & 22 deletions components/poll-context.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Participant, Vote } from "@prisma/client";
import { Participant, Vote, VoteType } from "@prisma/client";
import { GetPollResponse } from "api-client/get-poll";
import { keyBy } from "lodash";
import React from "react";
Expand All @@ -13,8 +13,6 @@ import { usePreferences } from "./preferences/use-preferences";
import { useSession } from "./session";
import { useRequiredContext } from "./use-required-context";

type VoteType = "yes" | "no";

type PollContextValue = {
userAlreadyVoted: boolean;
poll: GetPollResponse;
Expand All @@ -23,10 +21,11 @@ type PollContextValue = {
pollType: "date" | "timeSlot";
highScore: number;
getParticipantsWhoVotedForOption: (optionId: string) => Participant[]; // maybe just attach votes to parsed options
getScore: (optionId: string) => { yes: number; ifNeedBe: number };
getParticipantById: (
participantId: string,
) => (Participant & { votes: Vote[] }) | undefined;
getVote: (participantId: string, optionId: string) => VoteType;
getVote: (participantId: string, optionId: string) => VoteType | undefined;
} & (
| { pollType: "date"; options: ParsedDateOption[] }
| { pollType: "timeSlot"; options: ParsedTimeSlotOption[] }
Expand Down Expand Up @@ -57,21 +56,46 @@ export const PollContextProvider: React.VoidFunctionComponent<{
const participantsByOptionId = React.useMemo(() => {
const res: Record<string, Participant[]> = {};
poll.options.forEach((option) => {
res[option.id] = option.votes.map(
({ participantId }) => participantById[participantId],
res[option.id] = poll.participants.filter((participant) =>
participant.votes.some(
({ type, optionId }) => optionId === option.id && type !== "no",
),
);
});
return res;
}, [participantById, poll.options]);
}, [poll.options, poll.participants]);

const { locale } = usePreferences();

const getScore = React.useCallback(
(optionId: string) => {
return poll.participants.reduce(
(acc, curr) => {
curr.votes.forEach((vote) => {
if (vote.optionId !== optionId) {
return;
}
if (vote.type === "yes") {
acc.yes += 1;
}
if (vote.type === "ifNeedBe") {
acc.ifNeedBe += 1;
}
});
return acc;
},
{ yes: 0, ifNeedBe: 0 },
);
},
[poll.participants],
);

const contextValue = React.useMemo<PollContextValue>(() => {
let highScore = 1;
poll.options.forEach((option) => {
if (option.votes.length > highScore) {
highScore = option.votes.length;
}
});
const highScore = poll.options.reduce((acc, curr) => {
const score = getScore(curr.id).yes;

return score > acc ? score : acc;
}, 1);

const parsedOptions = decodeOptions(
poll.options,
Expand Down Expand Up @@ -99,29 +123,25 @@ export const PollContextProvider: React.VoidFunctionComponent<{
return {
userAlreadyVoted,
poll,
getVotesForOption: (optionId: string) => {
// TODO (Luke Vella) [2022-04-16]: Build an index instead
const option = poll.options.find(({ id }) => id === optionId);
return option?.votes ?? [];
},
getParticipantById: (participantId) => {
return participantById[participantId];
},
highScore,
getParticipantsWhoVotedForOption: (optionId: string) =>
participantsByOptionId[optionId],
getVote: (participantId, optionId) => {
return getParticipantById(participantId)?.votes.some(
const vote = getParticipantById(participantId)?.votes.find(
(vote) => vote.optionId === optionId,
)
? "yes"
: "no";
);
return vote?.type;
},
getScore,
...parsedOptions,
targetTimeZone,
setTargetTimeZone,
};
}, [
getScore,
locale,
participantById,
participantsByOptionId,
Expand Down
42 changes: 23 additions & 19 deletions components/poll.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { GetPollResponse } from "api-client/get-poll";
import axios from "axios";
import { NextPage } from "next";
import Head from "next/head";
Expand All @@ -21,7 +20,8 @@ import NotificationsToggle from "./poll/notifications-toggle";
import PollSubheader from "./poll/poll-subheader";
import TruncatedLinkify from "./poll/truncated-linkify";
import { UserAvatarProvider } from "./poll/user-avatar";
import { PollContextProvider, usePoll } from "./poll-context";
import VoteIcon from "./poll/vote-icon";
import { usePoll } from "./poll-context";
import Popover from "./popover";
import { useSession } from "./session";
import Sharing from "./sharing";
Expand All @@ -32,7 +32,7 @@ const Discussion = React.lazy(() => import("@/components/discussion"));
const DesktopPoll = React.lazy(() => import("@/components/poll/desktop-poll"));
const MobilePoll = React.lazy(() => import("@/components/poll/mobile-poll"));

const PollInner: NextPage = () => {
const PollPage: NextPage = () => {
const { poll } = usePoll();

const router = useRouter();
Expand Down Expand Up @@ -121,13 +121,6 @@ const PollInner: NextPage = () => {

const PollComponent = isWideScreen ? DesktopPoll : MobilePoll;

let highScore = 1; // set to one because we don't want to highlight
poll.options.forEach((option) => {
if (option.votes.length > highScore) {
highScore = option.votes.length;
}
});

const names = React.useMemo(
() => poll.participants.map(({ name }) => name),
[poll.participants],
Expand Down Expand Up @@ -210,9 +203,28 @@ const PollInner: NextPage = () => {
This poll has been locked (voting is disabled)
</div>
) : null}

<div className="flex items-center space-x-3 px-4 py-2 sm:justify-end">
<span className="text-xs font-semibold text-slate-500">
Legend:
</span>
<span className="inline-flex items-center space-x-2">
<VoteIcon type="yes" />
<span className="text-xs text-slate-500">Yes</span>
</span>
<span className="inline-flex items-center space-x-2">
<VoteIcon type="ifNeedBe" />
<span className="text-xs text-slate-500">If need be</span>
</span>

<span className="inline-flex items-center space-x-2">
<VoteIcon type="no" />
<span className="text-xs text-slate-500">No</span>
</span>
</div>
<React.Suspense fallback={<div>Loading…</div>}>
<div className="mb-4 lg:mb-8">
<PollComponent pollId={poll.urlId} highScore={highScore} />
<PollComponent pollId={poll.urlId} />
</div>
<Discussion pollId={poll.urlId} />
</React.Suspense>
Expand All @@ -223,12 +235,4 @@ const PollInner: NextPage = () => {
);
};

const PollPage = ({ poll }: { poll: GetPollResponse }) => {
return (
<PollContextProvider value={poll}>
<PollInner />
</PollContextProvider>
);
};

export default PollPage;
Loading

0 comments on commit 17dc951

Please sign in to comment.