diff --git a/apps/web/package.json b/apps/web/package.json index b832b279c..9a04b9421 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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" diff --git a/apps/web/src/components/date-icon.tsx b/apps/web/src/components/date-icon.tsx index 8aabc69c7..bf5a5526e 100644 --- a/apps/web/src/components/date-icon.tsx +++ b/apps/web/src/components/date-icon.tsx @@ -10,22 +10,15 @@ export const DateIconInner = (props: { return (
-
+
{props.dow}
-
-
-
- {props.day} -
-
- {props.month} -
-
+
+ {props.day}
); diff --git a/apps/web/src/components/feedback.tsx b/apps/web/src/components/feedback.tsx index f0abfadad..94a99484b 100644 --- a/apps/web/src/components/feedback.tsx +++ b/apps/web/src/components/feedback.tsx @@ -19,7 +19,7 @@ import { Trans } from "next-i18next"; const FeedbackButton = () => { return ( - + diff --git a/apps/web/src/components/poll-context.tsx b/apps/web/src/components/poll-context.tsx index 17af7384c..3bfa16af9 100644 --- a/apps/web/src/components/poll-context.tsx +++ b/apps/web/src/components/poll-context.tsx @@ -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 ( diff --git a/apps/web/src/components/poll/desktop-poll.tsx b/apps/web/src/components/poll/desktop-poll.tsx index 497efa75e..e45f1407c 100644 --- a/apps/web/src/components/poll/desktop-poll.tsx +++ b/apps/web/src/components/poll/desktop-poll.tsx @@ -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,269 +21,228 @@ 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 = ( + ref: React.RefObject, +) => { + 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(); + 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( - 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(null); + + const isOverflowing = useIsOverflowing(scrollRef); + + const { x } = useScroll(scrollRef); + return ( - -
-
-
+
+
+
+ {mode !== "view" ? (
- {shouldShowNewParticipantForm || editingParticipantId ? ( -
- }} - /> -
- ) : ( -
- -
- {t("participants", { count: participants.length })} ( - {participants.length}) -
- {canAddNewParticipant ? ( -
- )} + }} + />
-
+ ) : ( +
+
- {t("optionCount", { count: poll.options.length })} + {t("participants", { count: participants.length })} ( + {participants.length})
- {maxScrollPosition > 0 ? ( -
- - -
- ) : null} -
-
- {poll.options[0].duration !== 0 ? ( -
- -
- ) : null} -
-
-
-
-
- -
-
-
-
- {shouldShowNewParticipantForm && !editingParticipantId ? ( - { - showNewParticipantModal({ - votes, - onSubmit: () => { - setShouldShowNewParticipantForm(false); - }, - }); + {canAddNewParticipant ? ( +
+ )} +
+
+
+ {t("optionCount", { count: poll.options.length })}
- - {shouldShowNewParticipantForm || editingParticipantId ? ( -
-
- - -
+ {isOverflowing ? ( +
+ +
) : null}
- + {poll.options[0].duration !== 0 ? ( +
+ +
+ ) : null} +
+ + {mode !== "view" ? ( +
+
+ + {mode === "new" ? ( + + ) : ( + + )} +
+
+ ) : null} +
); }; -export default React.memo(Poll); +const WrappedDesktopPoll = () => { + return ( + + + + ); +}; + +export default WrappedDesktopPoll; diff --git a/apps/web/src/components/poll/desktop-poll/controlled-scroll-area.tsx b/apps/web/src/components/poll/desktop-poll/controlled-scroll-area.tsx deleted file mode 100644 index c21dbeafe..000000000 --- a/apps/web/src/components/poll/desktop-poll/controlled-scroll-area.tsx +++ /dev/null @@ -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 ( -
- - - {children} - - -
- ); -}; - -export default ControlledScrollArea; diff --git a/apps/web/src/components/poll/desktop-poll/participant-row-form.tsx b/apps/web/src/components/poll/desktop-poll/participant-row-form.tsx index 63cfa25e1..fbf5ebbd2 100644 --- a/apps/web/src/components/poll/desktop-poll/participant-row-form.tsx +++ b/apps/web/src/components/poll/desktop-poll/participant-row-form.tsx @@ -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; - onSubmit: (data: ParticipantFormSubmitted) => Promise; 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>([]); - return ( -
{ - await onSubmit({ - votes: normalizeVotes(optionIds, votes), - }); + + +
+ {name ? ( + + ) : ( + + )} +
+ + {optionIds.map((optionId, i) => { + return ( + + ( + { + field.onChange({ optionId, type: vote }); + }} + /> + )} + /> + + ); })} - className={clsx("flex h-12 shrink-0", className)} - > -
- {name ? ( - - ) : ( - - )} -
- { - return ( - - {optionIds.map((optionId, index) => { - const value = field.value[index]; - - return ( -
- { - if ( - e.code === "Tab" && - index < optionIds.length - 1 && - !isColumnVisible(index + 1) - ) { - e.preventDefault(); - goToNextPage(); - setTimeout(() => { - checkboxRefs.current[index + 1]?.focus(); - }, 100); - } - }} - onChange={(vote) => { - const newValue = [...field.value]; - newValue[index] = { optionId, type: vote }; - field.onChange(newValue); - }} - ref={(el) => { - checkboxRefs.current[index] = el; - }} - /> -
- ); - })} -
- ); - }} - /> - + ); }; -export default React.forwardRef(ParticipantRowForm); +export default ParticipantRowForm; diff --git a/apps/web/src/components/poll/desktop-poll/participant-row.tsx b/apps/web/src/components/poll/desktop-poll/participant-row.tsx index c1b8e74e0..63b4e00b9 100644 --- a/apps/web/src/components/poll/desktop-poll/participant-row.tsx +++ b/apps/web/src/components/poll/desktop-poll/participant-row.tsx @@ -9,85 +9,63 @@ 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; } export const ParticipantRowView: React.FunctionComponent<{ name: string; action?: React.ReactNode; votes: Array; - 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 ( -
-
- - {action} -
- - {votes.map((vote, i) => { - return ( +
+ + {action} +
+ + {votes.map((vote, i) => { + return ( +
-
- -
+
- ); - })} -
-
+ + ); + })} + ); }; const ParticipantRow: React.FunctionComponent = ({ participant, editMode, - onSubmit, className, onChangeEditMode, }) => { - const { columnWidth, sidebarWidth } = usePollContext(); - const { user, ownsObject } = useUser(); const { getVote, optionIds } = usePoll(); @@ -100,47 +78,33 @@ const ParticipantRow: React.FunctionComponent = ({ return ( { - 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 ( - <> - { - return getVote(participant.id, optionId); - })} - participantId={participant.id} - action={ - canEdit ? ( - onChangeEditMode?.(true)} - > -