Desktop poll code clean up and refinements (#120)

This commit is contained in:
Luke Vella 2022-04-21 13:01:29 +01:00 committed by GitHub
parent 8cd1db41db
commit 00b72d01bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 160 additions and 146 deletions

View file

@ -4,7 +4,7 @@ import * as React from "react";
import { useTimeoutFn } from "react-use";
import DateCard from "../date-card";
import Score from "../poll/score";
import Score from "../poll/desktop-poll/score";
import UserAvater from "../poll/user-avatar";
import VoteIcon from "../poll/vote-icon";

View file

@ -18,6 +18,7 @@ type PollContextValue = {
targetTimeZone: string;
setTargetTimeZone: (timeZone: string) => void;
pollType: "date" | "timeSlot";
highScore: number;
getParticipantsWhoVotedForOption: (optionId: string) => Participant[]; // maybe just attach votes to parsed options
getParticipantById: (
participantId: string,
@ -60,6 +61,13 @@ export const PollContextProvider: React.VoidFunctionComponent<{
}, [participantById, poll.options]);
const contextValue = React.useMemo<PollContextValue>(() => {
let highScore = 1;
poll.options.forEach((option) => {
if (option.votes.length > highScore) {
highScore = option.votes.length;
}
});
const parsedOptions = decodeOptions(
poll.options,
poll.timeZone,
@ -84,6 +92,7 @@ export const PollContextProvider: React.VoidFunctionComponent<{
getParticipantById: (participantId) => {
return participantById[participantId];
},
highScore,
getParticipantsWhoVotedForOption: (optionId: string) =>
participantsByOptionId[optionId],
getVote: (participantId, optionId) => {

View file

@ -1,24 +1,20 @@
import clsx from "clsx";
import { AnimatePresence, motion } from "framer-motion";
import { useTranslation } from "next-i18next";
import * as React from "react";
import { useMeasure } from "react-use";
import smoothscroll from "smoothscroll-polyfill";
import { decodeDateOption } from "../../utils/date-time-utils";
import Button from "../button";
import DateCard from "../date-card";
import ArrowLeft from "../icons/arrow-left.svg";
import ArrowRight from "../icons/arrow-right.svg";
import PlusCircle from "../icons/plus-circle.svg";
import { usePoll } from "../poll-context";
import TimeZonePicker from "../time-zone-picker";
import ParticipantRow from "./desktop-poll/participant-row";
import ParticipantRowForm from "./desktop-poll/participant-row-form";
import { PollContext } from "./desktop-poll/poll-context";
import PollHeader from "./desktop-poll/poll-header";
import { useAddParticipantMutation } from "./mutations";
import ParticipantRow from "./participant-row";
import ParticipantRowForm from "./participant-row-form";
import { PollContext, usePollContext } from "./poll-context";
import Score from "./score";
import TimeRange from "./time-range";
import { PollProps } from "./types";
if (typeof window !== "undefined") {
@ -27,48 +23,14 @@ if (typeof window !== "undefined") {
const MotionButton = motion(Button);
export const ControlledScrollDiv: React.VoidFunctionComponent<{
children?: React.ReactNode;
className?: string;
}> = ({ className, children }) => {
const { availableSpace, scrollPosition } = usePollContext();
const ref = React.useRef<HTMLDivElement>(null);
return (
<div
ref={ref}
className={clsx(" min-w-0 overflow-hidden", className)}
style={{ width: availableSpace, maxWidth: availableSpace }}
>
<AnimatePresence initial={false}>
<motion.div
className="flex h-full"
transition={{
type: "spring",
mass: 0.4,
}}
initial={{ x: 0 }}
animate={{ x: scrollPosition * -1 }}
>
{children}
</motion.div>
</AnimatePresence>
</div>
);
};
const minSidebarWidth = 180;
const Poll: React.VoidFunctionComponent<PollProps> = ({
pollId,
highScore,
}) => {
const Poll: React.VoidFunctionComponent<PollProps> = ({ pollId }) => {
const { t } = useTranslation("app");
const { poll, targetTimeZone, setTargetTimeZone } = usePoll();
const { poll, targetTimeZone, setTargetTimeZone, options } = usePoll();
const { timeZone, options, participants, role } = poll;
const { timeZone, participants, role } = poll;
const [ref, { width }] = useMeasure<HTMLDivElement>();
const [editingParticipantId, setEditingParticipantId] =
@ -204,53 +166,7 @@ const Poll: React.VoidFunctionComponent<PollProps> = ({
) : null}
</AnimatePresence>
</div>
<ControlledScrollDiv>
{options.map((option) => {
const parsedOption = decodeDateOption(
option,
timeZone,
targetTimeZone,
);
const numVotes = option.votes.length;
return (
<div
key={option.id}
className={clsx(
"shrink-0 py-4 text-center transition-colors",
{
"bg-slate-50": activeOptionId === option.id,
},
)}
style={{ width: columnWidth }}
onMouseOver={() => setActiveOptionId(option.id)}
onMouseOut={() => setActiveOptionId(null)}
>
<div>
<DateCard
day={parsedOption.day}
dow={parsedOption.dow}
month={parsedOption.month}
annotation={
<Score
count={numVotes}
highlight={numVotes === highScore}
/>
}
/>
</div>
<div>
{parsedOption.type === "timeSlot" ? (
<TimeRange
className="mt-2 -mr-2"
startTime={parsedOption.startTime}
endTime={parsedOption.endTime}
/>
) : null}
</div>
</div>
);
})}
</ControlledScrollDiv>
<PollHeader />
<div
className="flex items-center py-3 px-2"
style={{ width: actionColumnWidth }}
@ -295,7 +211,7 @@ const Poll: React.VoidFunctionComponent<PollProps> = ({
});
});
}}
options={options}
options={poll.options}
onCancel={() => {
setShouldShowNewParticipantForm(false);
}}
@ -309,7 +225,7 @@ const Poll: React.VoidFunctionComponent<PollProps> = ({
urlId={pollId}
key={i}
participant={participant}
options={options}
options={poll.options}
canDelete={role === "admin"}
editMode={editingParticipantId === participant.id}
onChangeEditMode={(isEditing) => {

View file

@ -0,0 +1,35 @@
import clsx from "clsx";
import { AnimatePresence, motion } from "framer-motion";
import React from "react";
import { usePollContext } from "./poll-context";
const ControlledScrollArea: React.VoidFunctionComponent<{
children?: React.ReactNode;
className?: string;
}> = ({ className, children }) => {
const { availableSpace, scrollPosition } = usePollContext();
return (
<div
className={clsx("min-w-0 overflow-hidden", className)}
style={{ width: availableSpace, maxWidth: availableSpace }}
>
<AnimatePresence initial={false}>
<motion.div
className="flex h-full"
transition={{
type: "spring",
mass: 0.4,
}}
initial={{ x: 0 }}
animate={{ x: scrollPosition * -1 }}
>
{children}
</motion.div>
</AnimatePresence>
</div>
);
};
export default ControlledScrollArea;

View file

@ -3,14 +3,15 @@ import clsx from "clsx";
import * as React from "react";
import { Controller, useForm } from "react-hook-form";
import { requiredString } from "../../utils/form-validation";
import Button from "../button";
import CheckCircle from "../icons/check-circle.svg";
import NameInput from "../name-input";
import Tooltip from "../tooltip";
import { ControlledScrollDiv } from "./poll";
import CheckCircle from "@/components/icons/check-circle.svg";
import { requiredString } from "../../../utils/form-validation";
import Button from "../../button";
import NameInput from "../../name-input";
import Tooltip from "../../tooltip";
import { ParticipantForm } from "../types";
import ControlledScrollArea from "./controlled-scroll-area";
import { usePollContext } from "./poll-context";
import { ParticipantForm } from "./types";
export interface ParticipantRowFormProps {
defaultValues?: Partial<ParticipantForm>;
@ -99,7 +100,7 @@ const ParticipantRowForm: React.VoidFunctionComponent<ParticipantRowFormProps> =
setScrollPosition(0);
setTimeout(() => {
checkboxRefs.current[0].focus();
}, 800);
}, 100);
}
}}
/>
@ -108,7 +109,7 @@ const ParticipantRowForm: React.VoidFunctionComponent<ParticipantRowFormProps> =
control={control}
/>
</div>
<ControlledScrollDiv>
<ControlledScrollArea>
{options.map((option, index) => {
return (
<div
@ -142,7 +143,7 @@ const ParticipantRowForm: React.VoidFunctionComponent<ParticipantRowFormProps> =
setTimeout(() => {
checkboxRefs.current[index + 1].focus();
isAnimatingRef.current = false;
}, 500);
}, 100);
}
}}
{...checkboxProps}
@ -159,7 +160,7 @@ const ParticipantRowForm: React.VoidFunctionComponent<ParticipantRowFormProps> =
</div>
);
})}
</ControlledScrollDiv>
</ControlledScrollArea>
<div className="flex items-center space-x-2 px-2 transition-all">
<Tooltip content="Save" placement="top">
<Button

View file

@ -2,17 +2,18 @@ import { Option, Participant, Vote } from "@prisma/client";
import clsx from "clsx";
import * as React from "react";
import Button from "../button";
import Pencil from "../icons/pencil.svg";
import Trash from "../icons/trash.svg";
import { usePoll } from "../poll-context";
import { useUpdateParticipantMutation } from "./mutations";
import Button from "@/components/button";
import Pencil from "@/components/icons/pencil.svg";
import Trash from "@/components/icons/trash.svg";
import { usePoll } from "@/components/poll-context";
import { useUpdateParticipantMutation } from "../mutations";
import { useDeleteParticipantModal } from "../use-delete-participant-modal";
import UserAvater from "../user-avatar";
import VoteIcon from "../vote-icon";
import ControlledScrollArea from "./controlled-scroll-area";
import ParticipantRowForm from "./participant-row-form";
import { ControlledScrollDiv } from "./poll";
import { usePollContext } from "./poll-context";
import { useDeleteParticipantModal } from "./use-delete-participant-modal";
import UserAvater from "./user-avatar";
import VoteIcon from "./vote-icon";
export interface ParticipantRowProps {
urlId: string;
@ -91,7 +92,7 @@ const ParticipantRow: React.VoidFunctionComponent<ParticipantRowProps> = ({
{participant.name}
</span>
</div>
<ControlledScrollDiv>
<ControlledScrollArea>
{options.map((option) => {
return (
<div
@ -116,7 +117,7 @@ const ParticipantRow: React.VoidFunctionComponent<ParticipantRowProps> = ({
</div>
);
})}
</ControlledScrollDiv>
</ControlledScrollArea>
{!poll.closed ? (
<div
style={{ width: actionColumnWidth }}

View file

@ -0,0 +1,80 @@
import clsx from "clsx";
import * as React from "react";
import DateCard from "@/components/date-card";
import { usePoll } from "@/components/poll-context";
import ControlledScrollArea from "./controlled-scroll-area";
import { usePollContext } from "./poll-context";
import Score from "./score";
const TimeRange: React.VoidFunctionComponent<{
startTime: string;
endTime: string;
className?: string;
}> = ({ startTime, endTime, className }) => {
return (
<div
className={clsx(
"relative inline-block pr-2 text-right text-xs font-semibold after:absolute after:top-2 after:right-0 after:h-4 after:w-1 after:border-t after:border-r after:border-b after:border-slate-300 after:content-['']",
className,
)}
>
<div>{startTime}</div>
<div className="text-slate-400">{endTime}</div>
</div>
);
};
const PollHeader: React.VoidFunctionComponent = () => {
const { options, getParticipantsWhoVotedForOption, highScore } = usePoll();
const { activeOptionId, setActiveOptionId, columnWidth } = usePollContext();
return (
<ControlledScrollArea>
{options.map((option) => {
const { optionId } = option;
const numVotes = getParticipantsWhoVotedForOption(optionId).length;
return (
<div
key={optionId}
className={clsx(
"shrink-0 pt-4 pb-3 text-center transition-colors",
{
"bg-slate-50": activeOptionId === optionId,
},
)}
style={{ width: columnWidth }}
onMouseOver={() => setActiveOptionId(optionId)}
onMouseOut={() => setActiveOptionId(null)}
>
<div>
<DateCard
day={option.day}
dow={option.dow}
month={option.month}
annotation={
numVotes > 0 ? (
<Score
count={numVotes}
highlight={numVotes > 1 && highScore === numVotes}
/>
) : null
}
/>
</div>
{option.type === "timeSlot" ? (
<TimeRange
className="mt-3"
startTime={option.startTime}
endTime={option.endTime}
/>
) : null}
</div>
);
})}
</ControlledScrollArea>
);
};
export default PollHeader;

View file

@ -1 +1 @@
export { default } from "./poll";
export { default } from "./desktop-poll";

View file

@ -1,28 +0,0 @@
import clsx from "clsx";
import * as React from "react";
export interface TimeRangeProps {
startTime: string;
endTime: string;
className?: string;
}
const TimeRange: React.VoidFunctionComponent<TimeRangeProps> = ({
startTime,
endTime,
className,
}) => {
return (
<div
className={clsx(
"relative inline-block pr-2 text-right font-mono text-xs after:absolute after:top-2 after:right-0 after:h-4 after:w-1 after:border-t after:border-r after:border-b after:border-slate-300 after:content-['']",
className,
)}
>
<div>{startTime}</div>
<div>{endTime}</div>
</div>
);
};
export default TimeRange;