Improvements to table component (#809)

This commit is contained in:
Luke Vella 2023-08-14 12:00:35 +01:00 committed by GitHub
parent b727c3f5e5
commit 83ad12b884
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 711 additions and 618 deletions

View file

@ -64,8 +64,6 @@
"smoothscroll-polyfill": "^0.4.4",
"spacetime": "^7.1.4",
"superjson": "^1.12.2",
"tailwindcss": "^3.2.4",
"tailwindcss-animate": "^1.0.5",
"timezone-soft": "^1.4.1",
"typescript": "^4.9.4",
"zod": "^3.20.2"

View file

@ -10,23 +10,16 @@ export const DateIconInner = (props: {
return (
<div
className={clsx(
"w-14 overflow-hidden rounded-md border bg-white text-center text-slate-800",
"inline-flex h-12 w-12 flex-col overflow-hidden rounded-md border bg-gray-50 text-center text-slate-800",
props.className,
)}
>
<div className="h-4 border-b border-slate-200 bg-slate-50 text-xs leading-4">
<div className="text-muted-foreground border-b border-gray-200 text-xs font-normal leading-4">
{props.dow}
</div>
<div className="flex h-10 items-center justify-center">
<div>
<div className="my-px text-lg font-bold leading-none">
<div className="flex grow items-center justify-center bg-white text-lg font-semibold leading-none tracking-tight">
{props.day}
</div>
<div className="text-xs font-bold uppercase tracking-wider">
{props.month}
</div>
</div>
</div>
</div>
);
};

View file

@ -19,7 +19,7 @@ import { Trans } from "next-i18next";
const FeedbackButton = () => {
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger className="shadow-huge fixed bottom-8 right-6 hidden h-12 w-12 items-center justify-center rounded-full bg-gray-800 hover:bg-gray-700 active:shadow-none sm:inline-flex">
<DropdownMenuTrigger className="shadow-huge fixed bottom-8 right-6 z-20 hidden h-12 w-12 items-center justify-center rounded-full bg-gray-800 hover:bg-gray-700 active:shadow-none sm:inline-flex">
<MegaphoneIcon className="h-5 text-white" />
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={10} align="end">

View file

@ -4,7 +4,6 @@ import { keyBy } from "lodash";
import { useTranslation } from "next-i18next";
import React from "react";
import { usePermissions } from "@/contexts/permissions";
import {
decodeOptions,
ParsedDateOption,
@ -16,10 +15,8 @@ import { GetPollApiResponse } from "@/utils/trpc/types";
import ErrorPage from "./error-page";
import { useParticipants } from "./participants-provider";
import { useRequiredContext } from "./use-required-context";
import { useUser } from "./user-provider";
type PollContextValue = {
userAlreadyVoted: boolean;
poll: GetPollApiResponse;
urlId: string;
admin: boolean;
@ -51,9 +48,6 @@ export const PollContextProvider: React.FunctionComponent<{
}> = ({ poll, urlId, admin, children }) => {
const { t } = useTranslation();
const { participants } = useParticipants();
const { user } = useUser();
const { canEditParticipant } = usePermissions();
const getScore = React.useCallback(
(optionId: string) => {
@ -95,11 +89,6 @@ export const PollContextProvider: React.FunctionComponent<{
return participant;
};
const userAlreadyVoted =
user && participants
? participants.some((participant) => canEditParticipant(participant.id))
: false;
const optionIds = poll.options.map(({ id }) => id);
const participantById = keyBy(
@ -119,7 +108,6 @@ export const PollContextProvider: React.FunctionComponent<{
return {
optionIds,
userAlreadyVoted,
poll,
urlId,
admin,
@ -137,7 +125,7 @@ export const PollContextProvider: React.FunctionComponent<{
},
getScore,
};
}, [admin, canEditParticipant, getScore, participants, poll, urlId, user]);
}, [admin, getScore, participants, poll, urlId]);
if (poll.deleted) {
return (

View file

@ -4,17 +4,16 @@ import {
PlusIcon,
Users2Icon,
} from "@rallly/icons";
import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button";
import clsx from "clsx";
import { Trans, useTranslation } from "next-i18next";
import * as React from "react";
import { useMeasure, useUpdateEffect } from "react-use";
import { useScroll } from "react-use";
import { TimesShownIn } from "@/components/clock";
import { useVotingForm, VotingForm } from "@/components/poll/voting-form";
import { usePermissions } from "@/contexts/permissions";
import { useRole } from "@/contexts/role";
import { useNewParticipantModal } from "../new-participant-modal";
import {
useParticipants,
useVisibleParticipants,
@ -22,126 +21,84 @@ import {
import { usePoll } from "../poll-context";
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,
useUpdateParticipantMutation,
} from "./mutations";
const minSidebarWidth = 200;
const useIsOverflowing = <E extends Element | null>(
ref: React.RefObject<E>,
) => {
const [isOverflowing, setIsOverflowing] = React.useState(false);
const Poll: React.FunctionComponent = () => {
React.useEffect(() => {
const checkOverflow = () => {
if (ref.current) {
const element = ref.current;
const overflowX = element.scrollWidth > element.clientWidth;
const overflowY = element.scrollHeight > element.clientHeight;
setIsOverflowing(overflowX || overflowY);
}
};
if (ref.current) {
const resizeObserver = new ResizeObserver(checkOverflow);
resizeObserver.observe(ref.current);
// Initial check
checkOverflow();
return () => {
resizeObserver.disconnect();
};
}
}, [ref]);
return isOverflowing;
};
const DesktopPoll: React.FunctionComponent = () => {
const { t } = useTranslation();
const { poll, userAlreadyVoted } = usePoll();
const { poll } = usePoll();
const { participants } = useParticipants();
const [ref, { width }] = useMeasure<HTMLDivElement>();
const votingForm = useVotingForm();
const [editingParticipantId, setEditingParticipantId] = React.useState<
string | null
>(null);
const columnWidth = 80;
const numberOfVisibleColumns = Math.min(
poll.options.length,
Math.floor((width - minSidebarWidth) / columnWidth),
);
const sidebarWidth = Math.min(
width - numberOfVisibleColumns * columnWidth,
275,
);
const availableSpace = Math.min(
numberOfVisibleColumns * columnWidth,
poll.options.length * columnWidth,
);
const [activeOptionId, setActiveOptionId] = React.useState<string | null>(
null,
);
const [scrollPosition, setScrollPosition] = React.useState(0);
const maxScrollPosition =
columnWidth * poll.options.length - columnWidth * numberOfVisibleColumns;
const mode = votingForm.watch("mode");
const { canAddNewParticipant } = usePermissions();
const role = useRole();
const [shouldShowNewParticipantForm, setShouldShowNewParticipantForm] =
React.useState(
canAddNewParticipant && !userAlreadyVoted && role === "participant",
);
const pollWidth = sidebarWidth + poll.options.length * columnWidth;
const addParticipant = useAddParticipantMutation();
useUpdateEffect(() => {
if (!canAddNewParticipant) {
setShouldShowNewParticipantForm(false);
}
}, [canAddNewParticipant]);
const goToNextPage = () => {
setScrollPosition(
Math.min(
maxScrollPosition,
scrollPosition + numberOfVisibleColumns * columnWidth,
),
);
if (scrollRef.current) {
scrollRef.current.scrollLeft += 220;
}
};
const goToPreviousPage = () => {
setScrollPosition(
Math.max(0, scrollPosition - numberOfVisibleColumns * columnWidth),
);
if (scrollRef.current) {
scrollRef.current.scrollLeft -= 220;
}
};
const updateParticipant = useUpdateParticipantMutation();
const showNewParticipantModal = useNewParticipantModal();
const visibleParticipants = useVisibleParticipants();
const scrollRef = React.useRef<HTMLDivElement>(null);
const isOverflowing = useIsOverflowing(scrollRef);
const { x } = useScroll(scrollRef);
return (
<PollContext.Provider
value={{
activeOptionId,
setActiveOptionId,
scrollPosition,
setScrollPosition,
columnWidth,
sidebarWidth,
goToNextPage,
goToPreviousPage,
numberOfColumns: numberOfVisibleColumns,
availableSpace,
maxScrollPosition,
}}
>
<div
className={clsx(
"relative min-w-full max-w-full duration-300",
width === 0 ? "invisible" : "visible",
)} // Don't add styles like border, margin, padding that can mess up the sizing calculations
style={{ width: pollWidth }}
ref={ref}
>
<div className="flex flex-col overflow-hidden">
<div className="flex flex-col">
<div className="flex h-14 shrink-0 items-center justify-between rounded-t-md border-b bg-gradient-to-b from-gray-50 to-gray-100/50 py-3 px-4">
<div>
{shouldShowNewParticipantForm || editingParticipantId ? (
<div className="px-1">
{mode !== "view" ? (
<div>
<Trans
t={t}
i18nKey="saveInstruction"
values={{
action: shouldShowNewParticipantForm
? t("continue")
: t("save"),
action: mode === "new" ? t("continue") : t("save"),
}}
components={{ b: <strong /> }}
/>
@ -160,8 +117,7 @@ const Poll: React.FunctionComponent = () => {
data-testid="add-participant-button"
icon={PlusIcon}
onClick={() => {
setEditingParticipantId(null);
setShouldShowNewParticipantForm(true);
votingForm.newParticipant();
}}
/>
) : null}
@ -172,16 +128,17 @@ const Poll: React.FunctionComponent = () => {
<div className="font-semibold">
{t("optionCount", { count: poll.options.length })}
</div>
{maxScrollPosition > 0 ? (
{isOverflowing ? (
<div className="flex gap-2">
<Button
onClick={goToPreviousPage}
disabled={scrollPosition === 0}
>
<Button disabled={x === 0} onClick={goToPreviousPage}>
<ArrowLeftIcon className="h-4 w-4" />
</Button>
<Button
disabled={scrollPosition === maxScrollPosition}
disabled={Boolean(
scrollRef.current &&
x + scrollRef.current.offsetWidth >=
scrollRef.current.scrollWidth,
)}
onClick={() => {
goToNextPage();
}}
@ -197,94 +154,95 @@ const Poll: React.FunctionComponent = () => {
<TimesShownIn />
</div>
) : null}
<div>
<div className="flex py-3">
<div className="relative">
<div
className="flex shrink-0 items-end pl-4 pr-2 font-medium"
style={{ width: sidebarWidth }}
>
<div className="font-semibold text-gray-800"></div>
</div>
<PollHeader />
</div>
</div>
<div>
<div>
{shouldShowNewParticipantForm && !editingParticipantId ? (
<ParticipantRowForm
className="mb-2 shrink-0"
onSubmit={async ({ votes }) => {
showNewParticipantModal({
votes,
onSubmit: () => {
setShouldShowNewParticipantForm(false);
},
});
}}
aria-hidden="true"
className={cn(
"pointer-events-none absolute left-[240px] top-0 bottom-2 z-30 w-4 border-l bg-gradient-to-r from-gray-200/50 via-transparent to-transparent transition-opacity",
x > 0 ? "opacity-100" : "opacity-0",
)}
/>
<div
ref={scrollRef}
className="scrollbar-thin hover:scrollbar-thumb-gray-400 scrollbar-thumb-gray-300 scrollbar-track-gray-100 relative z-10 flex-grow overflow-auto scroll-smooth pr-3 pb-3"
>
<table className="w-full table-auto border-separate border-spacing-0 ">
<thead>
<PollHeader />
</thead>
<tbody>
{mode === "new" ? (
<>
<ParticipantRowForm />
<tr aria-hidden="true">
<td colSpan={poll.options.length + 1} className="py-2" />
</tr>
</>
) : null}
{visibleParticipants.length > 0 ? (
<div className="py-2">
{visibleParticipants.map((participant, i) => {
{visibleParticipants.length > 0
? visibleParticipants.map((participant, i) => {
return (
<ParticipantRow
key={i}
participant={participant}
editMode={editingParticipantId === participant.id}
editMode={
votingForm.watch("participantId") === participant.id
}
onChangeEditMode={(isEditing) => {
if (isEditing) {
setShouldShowNewParticipantForm(false);
setEditingParticipantId(participant.id);
votingForm.setEditingParticipantId(participant.id);
}
}}
onSubmit={async ({ votes }) => {
await updateParticipant.mutateAsync({
participantId: participant.id,
pollId: poll.id,
votes,
});
setEditingParticipantId(null);
}}
/>
);
})}
</div>
) : null}
})
: null}
</tbody>
</table>
</div>
</div>
{shouldShowNewParticipantForm || editingParticipantId ? (
{mode !== "view" ? (
<div className="flex shrink-0 items-center border-t bg-gray-50">
<div className="flex w-full items-center justify-between gap-3 p-3">
<Button
onClick={() => {
if (editingParticipantId) {
setEditingParticipantId(null);
} else {
setShouldShowNewParticipantForm(false);
}
votingForm.cancel();
}}
>
{t("cancel")}
</Button>
{mode === "new" ? (
<Button
key="submit"
form="participant-row-form"
form="voting-form"
type="submit"
variant="primary"
loading={
addParticipant.isLoading || updateParticipant.isLoading
}
loading={votingForm.formState.isSubmitting}
>
{shouldShowNewParticipantForm ? t("continue") : t("save")}
{t("continue")}
</Button>
) : (
<Button
form="voting-form"
type="submit"
variant="primary"
loading={votingForm.formState.isSubmitting}
>
{t("save")}
</Button>
)}
</div>
</div>
) : null}
</div>
</div>
</PollContext.Provider>
);
};
export default React.memo(Poll);
const WrappedDesktopPoll = () => {
return (
<VotingForm>
<DesktopPoll />
</VotingForm>
);
};
export default WrappedDesktopPoll;

View file

@ -1,35 +0,0 @@
import clsx from "clsx";
import { AnimatePresence, m } from "framer-motion";
import React from "react";
import { usePollContext } from "./poll-context";
const ControlledScrollArea: React.FunctionComponent<{
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}>
<m.div
className="flex h-full"
transition={{
type: "spring",
mass: 0.4,
}}
initial={{ x: 0 }}
animate={{ x: scrollPosition * -1 }}
>
{children}
</m.div>
</AnimatePresence>
</div>
);
};
export default ControlledScrollArea;

View file

@ -1,45 +1,31 @@
import clsx from "clsx";
import { cn } from "@rallly/ui";
import { useTranslation } from "next-i18next";
import * as React from "react";
import { Controller, useForm } from "react-hook-form";
import { Controller } from "react-hook-form";
import { useVotingForm } from "@/components/poll/voting-form";
import { usePoll } from "../../poll-context";
import { normalizeVotes } from "../mutations";
import { ParticipantForm, ParticipantFormSubmitted } from "../types";
import UserAvatar, { YouAvatar } from "../user-avatar";
import { VoteSelector } from "../vote-selector";
import ControlledScrollArea from "./controlled-scroll-area";
import { usePollContext } from "./poll-context";
export interface ParticipantRowFormProps {
name?: string;
defaultValues?: Partial<ParticipantForm>;
onSubmit: (data: ParticipantFormSubmitted) => Promise<void>;
className?: string;
isYou?: boolean;
onCancel?: () => void;
}
const ParticipantRowForm: React.ForwardRefRenderFunction<
HTMLFormElement,
ParticipantRowFormProps
> = ({ defaultValues, onSubmit, name, isYou, className, onCancel }, ref) => {
const ParticipantRowForm = ({
name,
isYou,
className,
onCancel,
}: ParticipantRowFormProps) => {
const { t } = useTranslation();
const {
columnWidth,
scrollPosition,
sidebarWidth,
numberOfColumns,
goToNextPage,
} = usePollContext();
const { optionIds } = usePoll();
const { handleSubmit, control } = useForm({
defaultValues: {
votes: [],
...defaultValues,
},
});
const form = useVotingForm();
React.useEffect(() => {
window.addEventListener("keydown", (e) => {
@ -49,79 +35,38 @@ const ParticipantRowForm: React.ForwardRefRenderFunction<
});
}, [onCancel]);
const isColumnVisible = (index: number) => {
return scrollPosition + numberOfColumns * columnWidth > columnWidth * index;
};
const checkboxRefs = React.useRef<Array<HTMLButtonElement | null>>([]);
return (
<form
id="participant-row-form"
ref={ref}
onSubmit={handleSubmit(async ({ votes }) => {
await onSubmit({
votes: normalizeVotes(optionIds, votes),
});
})}
className={clsx("flex h-12 shrink-0", className)}
>
<div className="flex items-center px-5" style={{ width: sidebarWidth }}>
<tr className={cn(className)}>
<td className="sticky left-0 z-10 bg-white pl-4 pr-4">
<div className="flex items-center">
{name ? (
<UserAvatar name={name ?? t("you")} isYou={isYou} showName={true} />
) : (
<YouAvatar />
)}
</div>
</td>
{optionIds.map((optionId, i) => {
return (
<td key={optionId} className="h-12 bg-white p-1">
<Controller
control={control}
name="votes"
render={({ field }) => {
return (
<ControlledScrollArea>
{optionIds.map((optionId, index) => {
const value = field.value[index];
return (
<div
key={optionId}
className="flex shrink-0 items-center justify-center p-1"
style={{ width: columnWidth }}
>
control={form.control}
name={`votes.${i}`}
render={({ field }) => (
<VoteSelector
className="h-full w-full"
value={value?.type}
onKeyDown={(e) => {
if (
e.code === "Tab" &&
index < optionIds.length - 1 &&
!isColumnVisible(index + 1)
) {
e.preventDefault();
goToNextPage();
setTimeout(() => {
checkboxRefs.current[index + 1]?.focus();
}, 100);
}
}}
value={field.value.type}
onChange={(vote) => {
const newValue = [...field.value];
newValue[index] = { optionId, type: vote };
field.onChange(newValue);
}}
ref={(el) => {
checkboxRefs.current[index] = el;
field.onChange({ optionId, type: vote });
}}
/>
</div>
)}
/>
</td>
);
})}
</ControlledScrollArea>
);
}}
/>
</form>
</tr>
);
};
export default React.forwardRef(ParticipantRowForm);
export default ParticipantRowForm;

View file

@ -9,61 +9,43 @@ import { usePoll } from "@/components/poll-context";
import { useUser } from "@/components/user-provider";
import { usePermissions } from "@/contexts/permissions";
import { ParticipantFormSubmitted } from "../types";
import UserAvatar from "../user-avatar";
import VoteIcon from "../vote-icon";
import ControlledScrollArea from "./controlled-scroll-area";
import ParticipantRowForm from "./participant-row-form";
import { usePollContext } from "./poll-context";
export interface ParticipantRowProps {
participant: Participant & { votes: Vote[] };
className?: string;
editMode?: boolean;
onChangeEditMode?: (editMode: boolean) => void;
onSubmit?: (data: ParticipantFormSubmitted) => Promise<void>;
}
export const ParticipantRowView: React.FunctionComponent<{
name: string;
action?: React.ReactNode;
votes: Array<VoteType | undefined>;
columnWidth: number;
className?: string;
sidebarWidth: number;
isYou?: boolean;
participantId: string;
}> = ({
name,
action,
votes,
className,
sidebarWidth,
columnWidth,
isYou,
participantId,
}) => {
}> = ({ name, action, votes, className, isYou, participantId }) => {
return (
<div
<tr
data-testid="participant-row"
data-participantid={participantId}
className={clsx("flex h-12 items-center", className)}
className={clsx(className)}
>
<div
className="flex h-full shrink-0 items-center justify-between gap-2 px-3 sm:pl-5"
style={{ width: sidebarWidth }}
<td
style={{ minWidth: 240, maxWidth: 240 }}
className="sticky left-0 z-10 bg-white px-4"
>
<div className="flex max-w-full items-center justify-between gap-x-4 ">
<UserAvatar name={name} showName={true} isYou={isYou} />
{action}
</div>
<ControlledScrollArea className="h-full">
</td>
{votes.map((vote, i) => {
return (
<div
key={i}
className={clsx("relative flex h-full shrink-0 p-1")}
style={{ width: columnWidth }}
>
<td key={i} className={clsx("h-12 p-1")}>
<div
className={clsx(
"flex h-full w-full items-center justify-center rounded border bg-gray-50",
@ -71,23 +53,19 @@ export const ParticipantRowView: React.FunctionComponent<{
>
<VoteIcon type={vote} />
</div>
</div>
</td>
);
})}
</ControlledScrollArea>
</div>
</tr>
);
};
const ParticipantRow: React.FunctionComponent<ParticipantRowProps> = ({
participant,
editMode,
onSubmit,
className,
onChangeEditMode,
}) => {
const { columnWidth, sidebarWidth } = usePollContext();
const { user, ownsObject } = useUser();
const { getVote, optionIds } = usePoll();
@ -100,27 +78,14 @@ const ParticipantRow: React.FunctionComponent<ParticipantRowProps> = ({
return (
<ParticipantRowForm
name={participant.name}
defaultValues={{
votes: optionIds.map((optionId) => {
const type = getVote(participant.id, optionId);
return type ? { optionId, type } : undefined;
}),
}}
isYou={isYou}
onSubmit={async ({ votes }) => {
await onSubmit?.({ votes });
onChangeEditMode?.(false);
}}
onCancel={() => onChangeEditMode?.(false)}
/>
);
}
return (
<>
<ParticipantRowView
sidebarWidth={sidebarWidth}
columnWidth={columnWidth}
className={className}
name={participant.name}
votes={optionIds.map((optionId) => {
@ -140,7 +105,6 @@ const ParticipantRow: React.FunctionComponent<ParticipantRowProps> = ({
}
isYou={isYou}
/>
</>
);
};

View file

@ -1,30 +0,0 @@
import noop from "lodash/noop";
import React from "react";
export const PollContext = React.createContext<{
activeOptionId: string | null;
setActiveOptionId: (optionId: string | null) => void;
scrollPosition: number;
maxScrollPosition: number;
setScrollPosition: (position: number) => void;
columnWidth: number;
sidebarWidth: number;
numberOfColumns: number;
availableSpace: number | string;
goToNextPage: () => void;
goToPreviousPage: () => void;
}>({
activeOptionId: null,
setActiveOptionId: noop,
scrollPosition: 0,
maxScrollPosition: 100,
setScrollPosition: noop,
columnWidth: 100,
sidebarWidth: 200,
numberOfColumns: 0,
availableSpace: "auto",
goToNextPage: noop,
goToPreviousPage: noop,
});
export const usePollContext = () => React.useContext(PollContext);

View file

@ -1,3 +1,4 @@
import { cn } from "@rallly/ui";
import clsx from "clsx";
import * as React from "react";
@ -5,8 +6,6 @@ import { DateIconInner } from "@/components/date-icon";
import { useOptions } from "@/components/poll-context";
import { ConnectedScoreSummary } from "../score-summary";
import ControlledScrollArea from "./controlled-scroll-area";
import { usePollContext } from "./poll-context";
const TimeRange: React.FunctionComponent<{
start: string;
@ -16,43 +15,145 @@ const TimeRange: React.FunctionComponent<{
return (
<div
className={clsx(
"relative -mr-2 inline-block pr-2 text-right text-xs font-semibold after:absolute after:right-0 after:top-2 after:h-4 after:w-1 after:border-b after:border-r after:border-t after:border-gray-300 after:content-['']",
"relative -mr-2 inline-block whitespace-nowrap pr-2 text-right text-xs font-normal after:absolute after:right-0 after:top-2 after:h-4 after:w-1 after:border-b after:border-r after:border-t after:border-gray-300 after:content-['']",
className,
)}
>
<div>{start}</div>
<div className="text-gray-500">{end}</div>
<div className="font-medium tabular-nums">{start}</div>
<div className="text-muted-foreground tabular-nums">{end}</div>
</div>
);
};
const TimelineRow = ({
children,
top,
}: React.PropsWithChildren<{ top?: number }>) => {
return (
<tr>
<th
style={{ minWidth: 240, top }}
className="sticky left-0 z-30 bg-white pl-4 pr-4"
></th>
{children}
<th className="w-full" />
</tr>
);
};
const monthRowHeight = 48;
const dayRowHeight = 64;
const scoreRowTop = monthRowHeight + dayRowHeight;
const Trail = ({ end }: { end?: boolean }) => {
return end ? (
<div aria-hidden="true" className="absolute top-6 left-0 z-10 h-full w-1/2">
<div className="h-px bg-gray-200" />
<div className="absolute right-0 top-0 h-5 w-px bg-gray-200" />
</div>
) : (
<div
aria-hidden="true"
className={cn("absolute top-6 left-0 z-10 h-full w-full")}
>
<div className="h-px bg-gray-200" />
<div className={cn("absolute right-1/2 top-0 h-2 w-px bg-gray-200")} />
</div>
);
};
const PollHeader: React.FunctionComponent = () => {
const { options } = useOptions();
const { setActiveOptionId, columnWidth } = usePollContext();
return (
<ControlledScrollArea>
{options.map((option) => {
<>
<TimelineRow top={0}>
{options.map((option, i) => {
const firstOfMonth =
i === 0 || options[i - 1]?.month !== option.month;
const lastOfMonth = options[i + 1]?.month !== option.month;
return (
<div
<th
key={option.optionId}
className="flex shrink-0 flex-col items-center gap-y-3"
style={{ width: columnWidth }}
onMouseOver={() => setActiveOptionId(option.optionId)}
onMouseOut={() => setActiveOptionId(null)}
style={{ height: monthRowHeight }}
className={cn(
"sticky top-0 space-y-3 bg-white",
firstOfMonth ? "left-[240px] z-20" : "z-10",
)}
>
<div className="flex items-center justify-center">
{firstOfMonth ? null : <Trail end={lastOfMonth} />}
<div
className={cn(
"h-5 px-2 py-0.5 text-sm font-semibold",
firstOfMonth ? "opacity-100" : "opacity-0",
)}
>
{option.month}
</div>
</div>
</th>
);
})}
</TimelineRow>
<TimelineRow top={monthRowHeight}>
{options.map((option, i) => {
const firstOfDay =
i === 0 ||
options[i - 1]?.day !== option.day ||
options[i - 1]?.month !== option.month;
const lastOfDay =
options[i + 1]?.day !== option.day ||
options[i + 1]?.month !== option.month;
return (
<th
key={option.optionId}
style={{
minWidth: 80,
width: 80,
maxWidth: 90,
height: dayRowHeight,
// could enable this to make the date column sticky
left: firstOfDay ? 240 : 0,
top: monthRowHeight,
}}
className={cn(
"sticky space-y-2 align-top",
firstOfDay
? "z-20 bg-gradient-to-r from-transparent to-white"
: "z-10 bg-white",
)}
>
{firstOfDay ? null : <Trail end={lastOfDay} />}
<DateIconInner
className={firstOfDay ? "opacity-100" : "opacity-0"}
day={option.day}
dow={option.dow}
month={option.month}
/>
</th>
);
})}
</TimelineRow>
<TimelineRow top={scoreRowTop}>
{options.map((option) => {
return (
<th
key={option.optionId}
style={{ minWidth: 80, maxWidth: 90, top: scoreRowTop }}
className="sticky z-20 space-y-2 bg-white py-2"
>
{option.type === "timeSlot" ? (
<TimeRange start={option.startTime} end={option.endTime} />
) : null}
<div>
<ConnectedScoreSummary optionId={option.optionId} />
</div>
</th>
);
})}
</ControlledScrollArea>
</TimelineRow>
</>
);
};

View file

@ -32,14 +32,7 @@ if (typeof window !== "undefined") {
const MobilePoll: React.FunctionComponent = () => {
const pollContext = usePoll();
const {
poll,
admin,
getParticipantById,
optionIds,
getVote,
userAlreadyVoted,
} = pollContext;
const { poll, admin, getParticipantById, optionIds, getVote } = pollContext;
const { options } = useOptions();
const { participants } = useParticipants();
@ -70,7 +63,7 @@ const MobilePoll: React.FunctionComponent = () => {
const { canEditParticipant, canAddNewParticipant } = usePermissions();
const [isEditing, setIsEditing] = React.useState(
canAddNewParticipant && !userAlreadyVoted,
canAddNewParticipant && !participants.some((p) => canEditParticipant(p.id)),
);
useUpdateEffect(() => {

View file

@ -190,7 +190,7 @@ const PollOption: React.FunctionComponent<PollOptionProps> = ({
return (
<div
className={clsx("space-y-4 overflow-hidden p-3", {
className={clsx("space-y-4 overflow-hidden px-4 py-3", {
"bg-gray-500/5": editable && active,
})}
onTouchStart={() => setActive(editable)}

View file

@ -48,7 +48,7 @@ export const ScoreSummary: React.FunctionComponent<PopularityScoreProps> =
<div
data-testid="popularity-score"
className={cn(
"relative flex select-none items-center gap-1 rounded-full border py-0.5 px-2 text-xs tabular-nums",
"relative inline-flex select-none items-center gap-1 rounded-full border py-0.5 px-2 text-xs font-normal tabular-nums",
highlight
? "border-green-500 text-green-500"
: "border-transparent text-gray-600",

View file

@ -4,7 +4,7 @@ export interface ParticipantForm {
votes: Array<
| {
optionId: string;
type: VoteType;
type?: VoteType;
}
| undefined
>;

View file

@ -0,0 +1,138 @@
import React from "react";
import { FormProvider, useForm, useFormContext } from "react-hook-form";
import { z } from "zod";
import { useNewParticipantModal } from "@/components/new-participant-modal";
import { useParticipants } from "@/components/participants-provider";
import {
normalizeVotes,
useUpdateParticipantMutation,
} from "@/components/poll/mutations";
import { usePermissions } from "@/contexts/permissions";
import { usePoll } from "@/contexts/poll";
import { useRole } from "@/contexts/role";
const formSchema = z.object({
mode: z.enum(["new", "edit", "view"]),
participantId: z.string().optional(),
votes: z.array(
z.object({
optionId: z.string(),
type: z.enum(["yes", "no", "ifNeedBe"]).optional(),
}),
),
});
type VotingFormValues = z.infer<typeof formSchema>;
export const useVotingForm = () => {
const { options } = usePoll();
const { participants } = useParticipants();
const form = useFormContext<VotingFormValues>();
return {
...form,
newParticipant: () => {
form.reset({
mode: "new",
participantId: undefined,
votes: options.map((option) => ({
optionId: option.id,
})),
});
},
setEditingParticipantId: (newParticipantId: string) => {
const participant = participants.find((p) => p.id === newParticipantId);
if (participant) {
form.reset({
mode: "edit",
participantId: newParticipantId,
votes: options.map((option) => ({
optionId: option.id,
type: participant.votes.find((vote) => vote.optionId === option.id)
?.type,
})),
});
} else {
console.error("Participant not found");
}
},
cancel: () =>
form.reset({
mode: "view",
participantId: undefined,
votes: options.map((option) => ({
optionId: option.id,
})),
}),
};
};
export const VotingForm = ({ children }: React.PropsWithChildren) => {
const { id: pollId, options } = usePoll();
const showNewParticipantModal = useNewParticipantModal();
const updateParticipant = useUpdateParticipantMutation();
const { participants } = useParticipants();
const { canAddNewParticipant, canEditParticipant } = usePermissions();
const userAlreadyVoted = participants.some((participant) =>
canEditParticipant(participant.id),
);
const role = useRole();
const form = useForm<VotingFormValues>({
defaultValues: {
mode:
canAddNewParticipant && !userAlreadyVoted && role === "participant"
? "new"
: "view",
votes: options.map((option) => ({
optionId: option.id,
})),
},
});
return (
<FormProvider {...form}>
<form
id="voting-form"
onSubmit={form.handleSubmit(async (data) => {
const optionIds = options.map((option) => option.id);
if (data.participantId) {
// update participant
await updateParticipant.mutateAsync({
participantId: data.participantId,
pollId,
votes: normalizeVotes(optionIds, data.votes),
});
form.reset({
participantId: undefined,
votes: options.map((option) => ({
optionId: option.id,
})),
});
} else {
// new participant
showNewParticipantModal({
votes: normalizeVotes(optionIds, data.votes),
onSubmit: async () => {
form.reset({
mode: "view",
participantId: undefined,
votes: options.map((option) => ({
optionId: option.id,
})),
});
},
});
}
})}
/>
{children}
</FormProvider>
);
};

View file

@ -20,7 +20,7 @@ async function main() {
const polls = await Promise.all(
Array.from({ length: 20 }).map(async (_, i) => {
// create some polls with no duration (all day) and some with a random duration.
const duration = i % 5 === 0 ? 15 * randInt(8) : 0;
const duration = i % 2 === 0 ? 15 * randInt(8) : 0;
const poll = await prisma.poll.create({
include: {
participants: true,
@ -43,7 +43,7 @@ async function main() {
.betweens(
Date.now(),
Date.now() + 1000 * 60 * 60 * 24 * 30,
randInt(16, 1),
randInt(30, 1),
)
.map((date) => {
// rounded to nearest 15 minutes

View file

@ -5,9 +5,10 @@
"main": "tailwind.config.js",
"types": "tailwind.config.d.ts",
"dependencies": {
"tailwindcss": "^3.2.7"
"tailwindcss": "^3.3.3"
},
"devDependencies": {
"tailwind-scrollbar": "^3.0.4"
"tailwind-scrollbar": "^3.0.4",
"tailwindcss-animate": "^1.0.5"
}
}

225
yarn.lock
View file

@ -2,6 +2,11 @@
# yarn lockfile v1
"@alloc/quick-lru@^5.2.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
"@ampproject/remapping@^2.2.0":
version "2.2.0"
resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz"
@ -4058,15 +4063,6 @@ acorn-jsx@^5.3.1:
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn-node@^1.8.2:
version "1.8.2"
resolved "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz"
integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
dependencies:
acorn "^7.0.0"
acorn-walk "^7.0.0"
xtend "^4.0.2"
acorn-private-class-elements@^0.2.7:
version "0.2.7"
resolved "https://registry.npmjs.org/acorn-private-class-elements/-/acorn-private-class-elements-0.2.7.tgz"
@ -4095,17 +4091,12 @@ acorn-static-class-features@^0.2.4:
dependencies:
acorn-private-class-elements "^0.2.7"
acorn-walk@^7.0.0:
version "7.2.0"
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
acorn-walk@^8.0.0, acorn-walk@^8.1.1:
version "8.2.0"
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^7.0.0, acorn@^7.4.0:
acorn@^7.4.0:
version "7.4.1"
resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
@ -4175,6 +4166,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
any-promise@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
any@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/any/-/any-1.0.0.tgz"
@ -4752,7 +4748,7 @@ color-name@1.1.3:
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
color-name@^1.1.4, color-name@~1.1.4:
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
@ -4777,6 +4773,11 @@ commander@^2.19.0:
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
commander@^5.0.0:
version "5.1.0"
resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz"
@ -5065,11 +5066,6 @@ define-properties@^1.1.3, define-properties@^1.1.4:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
defined@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz"
integrity sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==
deprecation@^2.0.0, deprecation@^2.3.1:
version "2.3.1"
resolved "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz"
@ -5092,15 +5088,6 @@ detect-package-manager@2.0.1:
dependencies:
execa "^5.1.1"
detective@^5.2.1:
version "5.2.1"
resolved "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz"
integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==
dependencies:
acorn-node "^1.8.2"
defined "^1.0.0"
minimist "^1.2.6"
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz"
@ -6070,6 +6057,18 @@ glob-stream@^6.1.0:
to-absolute-glob "^2.0.0"
unique-stream "^2.0.2"
glob@7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@7.1.7:
version "7.1.7"
resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz"
@ -6693,6 +6692,13 @@ is-core-module@^2.10.0, is-core-module@^2.11.0, is-core-module@^2.9.0:
dependencies:
has "^1.0.3"
is-core-module@^2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db"
integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==
dependencies:
has "^1.0.3"
is-date-object@^1.0.1, is-date-object@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz"
@ -6943,6 +6949,11 @@ isobject@^3.0.1:
resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz"
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
jiti@^1.18.2:
version "1.19.1"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.1.tgz#fa99e4b76a23053e0e7cde098efe1704a14c16f1"
integrity sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==
jju@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz"
@ -7153,11 +7164,16 @@ lie@3.1.1:
dependencies:
immediate "~3.0.5"
lilconfig@^2.0.5, lilconfig@^2.0.6:
lilconfig@^2.0.5:
version "2.0.6"
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz"
integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==
lilconfig@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==
lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
@ -7705,6 +7721,15 @@ ms@^2.1.1:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
dependencies:
any-promise "^1.0.0"
object-assign "^4.0.1"
thenify-all "^1.0.0"
nano-css@^5.3.1:
version "5.3.5"
resolved "https://registry.npmjs.org/nano-css/-/nano-css-5.3.5.tgz"
@ -7724,7 +7749,7 @@ nanoclone@^0.2.1:
resolved "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz"
integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
nanoid@^3.1.23:
nanoid@^3.1.23, nanoid@^3.3.6:
version "3.3.6"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
@ -7872,7 +7897,7 @@ nth-check@^2.0.1:
dependencies:
boolbase "^1.0.0"
object-assign@^4.1.1:
object-assign@^4.0.1, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
@ -8171,41 +8196,46 @@ pify@^4.0.1:
resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz"
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
pirates@^4.0.1:
version "4.0.6"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
playwright-core@1.35.1:
version "1.35.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.35.1.tgz#52c1e6ffaa6a8c29de1a5bdf8cce0ce290ffb81d"
integrity sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==
postcss-import@^14.1.0:
version "14.1.0"
resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz"
integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==
postcss-import@^15.1.0:
version "15.1.0"
resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70"
integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==
dependencies:
postcss-value-parser "^4.0.0"
read-cache "^1.0.0"
resolve "^1.1.7"
postcss-js@^4.0.0:
postcss-js@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz"
resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2"
integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
dependencies:
camelcase-css "^2.0.1"
postcss-load-config@^3.1.4:
version "3.1.4"
resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz"
integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
postcss-load-config@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz#152383f481c2758274404e4962743191d73875bd"
integrity sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==
dependencies:
lilconfig "^2.0.5"
yaml "^1.10.2"
yaml "^2.1.1"
postcss-nested@6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz"
integrity sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==
postcss-nested@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c"
integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==
dependencies:
postcss-selector-parser "^6.0.10"
postcss-selector-parser "^6.0.11"
postcss-selector-parser@6.0.10:
version "6.0.10"
@ -8215,7 +8245,7 @@ postcss-selector-parser@6.0.10:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11:
postcss-selector-parser@^6.0.11:
version "6.0.11"
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz"
integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==
@ -8237,7 +8267,7 @@ postcss@8.4.14:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.0.9, postcss@^8.4.21:
postcss@^8.4.21:
version "8.4.21"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz"
integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
@ -8246,6 +8276,15 @@ postcss@^8.0.9, postcss@^8.4.21:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.23:
version "8.4.27"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
posthog-js@^1.57.2:
version "1.57.2"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.57.2.tgz#131fb93e2ad099baff4317f3d91a4d6c96a08e7f"
@ -8373,11 +8412,6 @@ queue-microtask@^1.2.2:
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
quick-lru@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz"
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
react-big-calendar@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/react-big-calendar/-/react-big-calendar-1.8.1.tgz#07886a66086fcae16934572c5ace8c4c433dbbed"
@ -8794,6 +8828,15 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.22.
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
resolve@^1.22.2:
version "1.22.4"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34"
integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==
dependencies:
is-core-module "^2.13.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
resolve@^2.0.0-next.4:
version "2.0.0-next.4"
resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz"
@ -9288,6 +9331,19 @@ stylis@^4.0.6:
resolved "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz"
integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==
sucrase@^3.32.0:
version "3.34.0"
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f"
integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==
dependencies:
"@jridgewell/gen-mapping" "^0.3.2"
commander "^4.0.0"
glob "7.1.6"
lines-and-columns "^1.1.6"
mz "^2.7.0"
pirates "^4.0.1"
ts-interface-checker "^0.1.9"
superjson@^1.12.2:
version "1.12.2"
resolved "https://registry.npmjs.org/superjson/-/superjson-1.12.2.tgz"
@ -9371,34 +9427,33 @@ tailwindcss-animate@^1.0.5:
resolved "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.5.tgz"
integrity sha512-UU3qrOJ4lFQABY+MVADmBm+0KW3xZyhMdRvejwtXqYOL7YjHYxmuREFAZdmVG5LPe5E9CAst846SLC4j5I3dcw==
tailwindcss@^3.2.4, tailwindcss@^3.2.7:
version "3.2.7"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz"
integrity sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==
tailwindcss@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"
integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==
dependencies:
"@alloc/quick-lru" "^5.2.0"
arg "^5.0.2"
chokidar "^3.5.3"
color-name "^1.1.4"
detective "^5.2.1"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.2.12"
glob-parent "^6.0.2"
is-glob "^4.0.3"
lilconfig "^2.0.6"
jiti "^1.18.2"
lilconfig "^2.1.0"
micromatch "^4.0.5"
normalize-path "^3.0.0"
object-hash "^3.0.0"
picocolors "^1.0.0"
postcss "^8.0.9"
postcss-import "^14.1.0"
postcss-js "^4.0.0"
postcss-load-config "^3.1.4"
postcss-nested "6.0.0"
postcss "^8.4.23"
postcss-import "^15.1.0"
postcss-js "^4.0.1"
postcss-load-config "^4.0.1"
postcss-nested "^6.0.1"
postcss-selector-parser "^6.0.11"
postcss-value-parser "^4.2.0"
quick-lru "^5.1.1"
resolve "^1.22.1"
resolve "^1.22.2"
sucrase "^3.32.0"
tapable@^2.2.0:
version "2.2.1"
@ -9410,6 +9465,20 @@ text-table@^0.2.0:
resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
thenify-all@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==
dependencies:
thenify ">= 3.1.0 < 4"
"thenify@>= 3.1.0 < 4":
version "3.3.1"
resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
dependencies:
any-promise "^1.0.0"
throttle-debounce@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz"
@ -9532,6 +9601,11 @@ ts-easing@^0.2.0:
resolved "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz"
integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==
ts-interface-checker@^0.1.9:
version "0.1.13"
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
ts-node@^10.9.1:
version "10.9.1"
resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz"
@ -10137,7 +10211,7 @@ ws@^7.3.1:
resolved "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1:
xtend@~4.0.0, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
@ -10157,11 +10231,16 @@ yallist@^4.0.0:
resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.10.0, yaml@^1.10.2:
yaml@^1.10.0:
version "1.10.2"
resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^2.1.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
yn@3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz"