mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-02 18:51:52 +02:00
Replace headless ui transition with framer motion
Increase animation speed Cookies Remove unused import
This commit is contained in:
parent
eee23c1bb5
commit
50f5710bd3
7 changed files with 237 additions and 226 deletions
|
@ -1,4 +1,4 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import Cookies from "js-cookie";
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
|
@ -11,16 +11,24 @@ const CookieConsentPopover: React.VoidFunctionComponent = () => {
|
|||
const [visible, setVisible] = React.useState(true);
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<Transition
|
||||
show={visible}
|
||||
appear={true}
|
||||
as="div"
|
||||
enter="transition transform delay-1000 duration-1000"
|
||||
enterFrom="opacity-0 translate-y-8"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-8"
|
||||
<AnimatePresence>
|
||||
{visible ? (
|
||||
<motion.div
|
||||
variants={{
|
||||
enter: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { type: "spring", delay: 2, duration: 1 },
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
y: 10,
|
||||
transition: { duration: 0.1 },
|
||||
},
|
||||
}}
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
animate="enter"
|
||||
exit="exit"
|
||||
className="fixed bottom-8 right-8 z-50 w-60 rounded-lg bg-white p-4 pt-8 text-sm shadow-lg"
|
||||
>
|
||||
<CookiesIllustration className="absolute -top-6" />
|
||||
|
@ -30,7 +38,9 @@ const CookieConsentPopover: React.VoidFunctionComponent = () => {
|
|||
</div>
|
||||
<div className="flex items-center space-x-6">
|
||||
<Link href="/privacy-policy">
|
||||
<a className="text-slate-400 hover:text-indigo-500">Privacy Policy</a>
|
||||
<a className="text-slate-400 hover:text-indigo-500">
|
||||
Privacy Policy
|
||||
</a>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
@ -42,7 +52,9 @@ const CookieConsentPopover: React.VoidFunctionComponent = () => {
|
|||
OK
|
||||
</button>
|
||||
</div>
|
||||
</Transition>,
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>,
|
||||
getPortal(),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import { Comment } from "@prisma/client";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { usePlausible } from "next-plausible";
|
||||
import * as React from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { useList } from "react-use";
|
||||
|
||||
import {
|
||||
createComment,
|
||||
|
@ -33,75 +33,12 @@ interface CommentForm {
|
|||
content: string;
|
||||
}
|
||||
|
||||
const Comments: React.VoidFunctionComponent<{
|
||||
comments: {
|
||||
id: string;
|
||||
authorName: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
}[];
|
||||
deletedComments: string[];
|
||||
onDelete: (commentId: string) => void;
|
||||
canDelete?: boolean;
|
||||
}> = ({ comments, deletedComments, onDelete, canDelete }) => {
|
||||
return (
|
||||
<div className="space-y-3 border-b bg-slate-50 p-4">
|
||||
{comments.map((comment, i) => {
|
||||
return (
|
||||
<div className="flex" key={i}>
|
||||
<Transition
|
||||
show={!deletedComments.includes(comment.id)}
|
||||
as="div"
|
||||
enter="transition transform duration-300"
|
||||
enterFrom="opacity-0 translate-y-4"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition transform duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
className="w-fit rounded-xl border bg-white px-3 py-2 shadow-sm"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<UserAvater name={comment.authorName} />
|
||||
<div className="mb-1">
|
||||
<span className="mr-1">{comment.authorName}</span>
|
||||
<span className="mr-1 text-slate-400">•</span>
|
||||
<span className="text-sm text-slate-500">
|
||||
{formatRelative(new Date(comment.createdAt), Date.now())}
|
||||
</span>
|
||||
</div>
|
||||
{canDelete ? (
|
||||
<Dropdown
|
||||
placement="bottom-start"
|
||||
trigger={<CompactButton icon={DotsHorizontal} />}
|
||||
>
|
||||
<DropdownItem
|
||||
icon={Trash}
|
||||
label="Delete comment"
|
||||
onClick={() => {
|
||||
onDelete(comment.id);
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="w-fit whitespace-pre-wrap">
|
||||
<TruncatedLinkify>{comment.content}</TruncatedLinkify>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
|
||||
pollId,
|
||||
canDelete,
|
||||
}) => {
|
||||
const getCommentsQueryKey = ["poll", pollId, "comments"];
|
||||
const [userName, setUserName] = useUserName();
|
||||
const [deletedComments, { push }] = useList<string>([]);
|
||||
const queryClient = useQueryClient();
|
||||
const { data: comments } = useQuery(
|
||||
getCommentsQueryKey,
|
||||
|
@ -141,8 +78,7 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
|
|||
await axios.delete(`/api/poll/${pollId}/comments/${payload.commentId}`);
|
||||
},
|
||||
{
|
||||
onMutate: ({ commentId }) => {
|
||||
push(commentId);
|
||||
onMutate: () => {
|
||||
plausible("Deleted comment");
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
@ -179,14 +115,64 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
|
|||
<div className="border-b bg-white px-4 py-2">
|
||||
<div className="font-medium">Comments</div>
|
||||
</div>
|
||||
{comments.length ? (
|
||||
<Comments
|
||||
comments={comments}
|
||||
canDelete={canDelete}
|
||||
onDelete={handleDelete}
|
||||
deletedComments={deletedComments}
|
||||
<div
|
||||
className={clsx({
|
||||
"space-y-3 border-b bg-slate-50 p-4": comments.length > 0,
|
||||
})}
|
||||
>
|
||||
<AnimatePresence initial={false}>
|
||||
{comments.map((comment) => {
|
||||
return (
|
||||
<motion.div
|
||||
transition={{ duration: 0.2 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="flex"
|
||||
key={comment.id}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, y: 10 }}
|
||||
animate={{ scale: 1, y: 0 }}
|
||||
exit={{ scale: 0.8 }}
|
||||
className="w-fit rounded-xl border bg-white px-3 py-2 shadow-sm"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<UserAvater name={comment.authorName} />
|
||||
<div className="mb-1">
|
||||
<span className="mr-1">{comment.authorName}</span>
|
||||
<span className="mr-1 text-slate-400">•</span>
|
||||
<span className="text-sm text-slate-500">
|
||||
{formatRelative(
|
||||
new Date(comment.createdAt),
|
||||
Date.now(),
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{canDelete ? (
|
||||
<Dropdown
|
||||
placement="bottom-start"
|
||||
trigger={<CompactButton icon={DotsHorizontal} />}
|
||||
>
|
||||
<DropdownItem
|
||||
icon={Trash}
|
||||
label="Delete comment"
|
||||
onClick={() => {
|
||||
handleDelete(comment.id);
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="w-fit whitespace-pre-wrap">
|
||||
<TruncatedLinkify>{comment.content}</TruncatedLinkify>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<form
|
||||
className="bg-white p-4"
|
||||
onSubmit={handleSubmit((data) => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Dialog } from "@headlessui/react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import * as React from "react";
|
||||
|
||||
import Button, { ButtonProps } from "../button";
|
||||
|
@ -32,7 +33,8 @@ const Modal: React.VoidFunctionComponent<ModalProps> = ({
|
|||
}) => {
|
||||
const initialFocusRef = React.useRef<HTMLButtonElement>(null);
|
||||
return (
|
||||
<Transition appear={true} as={React.Fragment} show={visible}>
|
||||
<AnimatePresence>
|
||||
{visible ? (
|
||||
<Dialog
|
||||
open={visible}
|
||||
className="fixed inset-0 z-40 overflow-y-auto"
|
||||
|
@ -41,28 +43,24 @@ const Modal: React.VoidFunctionComponent<ModalProps> = ({
|
|||
if (overlayClosable) onCancel?.();
|
||||
}}
|
||||
>
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-200"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
<motion.div
|
||||
transition={{ duration: 0.5 }}
|
||||
className="flex min-h-screen items-center justify-center"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-slate-900 bg-opacity-10" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-100"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-100"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
className="my-8 mx-4 inline-block w-fit transform overflow-hidden rounded-xl bg-white text-left align-middle shadow-xl transition-all"
|
||||
<Dialog.Overlay
|
||||
as={motion.div}
|
||||
transition={{ duration: 0.5 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-0 bg-slate-900 bg-opacity-10"
|
||||
/>
|
||||
<motion.div
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
className="z-50 my-8 mx-4 inline-block w-fit transform overflow-hidden rounded-xl bg-white text-left align-middle shadow-xl transition-all"
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
|
@ -100,11 +98,11 @@ const Modal: React.VoidFunctionComponent<ModalProps> = ({
|
|||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Listbox } from "@headlessui/react";
|
||||
import { Participant, Vote } from "@prisma/client";
|
||||
import clsx from "clsx";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
@ -19,7 +20,6 @@ import Trash from "../../icons/trash.svg";
|
|||
import { styleMenuItem } from "../../menu-styles";
|
||||
import NameInput from "../../name-input";
|
||||
import TimeZonePicker from "../../time-zone-picker";
|
||||
import { TransitionPopInOut } from "../../transitions";
|
||||
import { useUserName } from "../../user-name-context";
|
||||
import {
|
||||
useAddParticipantMutation,
|
||||
|
@ -145,8 +145,15 @@ const MobilePoll: React.VoidFunctionComponent<PollProps> = ({
|
|||
</div>
|
||||
<ChevronDown className="h-5" />
|
||||
</Listbox.Button>
|
||||
<TransitionPopInOut>
|
||||
<Listbox.Options className="menu-items max-h-72 w-full overflow-auto">
|
||||
<Listbox.Options
|
||||
as={motion.div}
|
||||
transition={{
|
||||
duration: 0.1,
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="menu-items max-h-72 w-full overflow-auto"
|
||||
>
|
||||
<Listbox.Option value={undefined} className={styleMenuItem}>
|
||||
Show all
|
||||
</Listbox.Option>
|
||||
|
@ -163,7 +170,6 @@ const MobilePoll: React.VoidFunctionComponent<PollProps> = ({
|
|||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</TransitionPopInOut>
|
||||
</div>
|
||||
</Listbox>
|
||||
{!poll.closed ? (
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import clsx from "clsx";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
|
@ -12,7 +13,6 @@ import ArrowLeft from "../icons/arrow-left.svg";
|
|||
import ArrowRight from "../icons/arrow-right.svg";
|
||||
import PlusCircle from "../icons/plus-circle.svg";
|
||||
import TimeZonePicker from "../time-zone-picker";
|
||||
import { TransitionPopInOut } from "../transitions";
|
||||
import { usePoll } from "../use-poll";
|
||||
import { useAddParticipantMutation } from "./mutations";
|
||||
import ParticipantRow from "./participant-row";
|
||||
|
@ -26,6 +26,8 @@ if (typeof window !== "undefined") {
|
|||
smoothscroll.polyfill();
|
||||
}
|
||||
|
||||
const MotionButton = motion(Button);
|
||||
|
||||
// There's a bug in Safari 15.4 that causes `scroll` to no work as intended
|
||||
const isSafariV154 =
|
||||
typeof window !== "undefined"
|
||||
|
@ -214,11 +216,20 @@ const Poll: React.VoidFunctionComponent<PollProps> = ({
|
|||
<div className="flex h-full grow items-end">
|
||||
{t("participantCount", { count: participants.length })}
|
||||
</div>
|
||||
<TransitionPopInOut show={scrollPosition > 0}>
|
||||
<Button rounded={true} onClick={goToPreviousPage}>
|
||||
<AnimatePresence initial={false}>
|
||||
{scrollPosition > 0 ? (
|
||||
<MotionButton
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
rounded={true}
|
||||
onClick={goToPreviousPage}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</TransitionPopInOut>
|
||||
</MotionButton>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<ControlledScrollDiv>
|
||||
{options.map((option) => {
|
||||
|
@ -271,8 +282,14 @@ const Poll: React.VoidFunctionComponent<PollProps> = ({
|
|||
className="flex items-center py-3 px-2"
|
||||
style={{ width: actionColumnWidth }}
|
||||
>
|
||||
<TransitionPopInOut show={scrollPosition < maxScrollPosition}>
|
||||
<Button
|
||||
{maxScrollPosition > 0 ? (
|
||||
<AnimatePresence initial={false}>
|
||||
{scrollPosition < maxScrollPosition ? (
|
||||
<MotionButton
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
className="text-xs"
|
||||
rounded={true}
|
||||
onClick={() => {
|
||||
|
@ -285,8 +302,10 @@ const Poll: React.VoidFunctionComponent<PollProps> = ({
|
|||
) : (
|
||||
`+${numberOfInvisibleColumns} more…`
|
||||
)}
|
||||
</Button>
|
||||
</TransitionPopInOut>
|
||||
</MotionButton>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{shouldShowNewParticipantForm ? (
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import clsx from "clsx";
|
||||
import { motion } from "framer-motion";
|
||||
import * as React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { usePopper } from "react-popper";
|
||||
|
@ -16,7 +16,7 @@ export interface TooltipProps {
|
|||
}
|
||||
|
||||
const Tooltip: React.VoidFunctionComponent<TooltipProps> = ({
|
||||
placement,
|
||||
placement = "bottom",
|
||||
className,
|
||||
children,
|
||||
disabled,
|
||||
|
@ -29,7 +29,10 @@ const Tooltip: React.VoidFunctionComponent<TooltipProps> = ({
|
|||
const [arrowElement, setArrowElement] =
|
||||
React.useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
const { styles, attributes, update } = usePopper(
|
||||
referenceElement,
|
||||
popperElement,
|
||||
{
|
||||
placement,
|
||||
modifiers: [
|
||||
{
|
||||
|
@ -40,14 +43,16 @@ const Tooltip: React.VoidFunctionComponent<TooltipProps> = ({
|
|||
},
|
||||
{ name: "arrow", options: { element: arrowElement, padding: 5 } },
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const [isVisible, setIsVisible] = React.useState(false);
|
||||
|
||||
const [debouncedValue, setDebouncedValue] = React.useState(false);
|
||||
|
||||
const [, cancel] = useDebounce(
|
||||
() => {
|
||||
async () => {
|
||||
await update?.();
|
||||
setDebouncedValue(isVisible);
|
||||
},
|
||||
300,
|
||||
|
@ -98,13 +103,20 @@ const Tooltip: React.VoidFunctionComponent<TooltipProps> = ({
|
|||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<Transition
|
||||
<motion.div
|
||||
className="pointer-events-none rounded-md bg-slate-700 px-3 py-2 text-slate-200 shadow-md"
|
||||
as={"div"}
|
||||
show={debouncedValue}
|
||||
enter="transition transform duration-100"
|
||||
enterFrom="opacity-0 -translate-y-2"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
initial="hidden"
|
||||
transition={{
|
||||
duration: 0.1,
|
||||
}}
|
||||
variants={{
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
translateY: placement === "bottom" ? -4 : 4,
|
||||
},
|
||||
show: { opacity: 1, translateY: 0 },
|
||||
}}
|
||||
animate={debouncedValue ? "show" : "hidden"}
|
||||
>
|
||||
<div
|
||||
ref={setArrowElement}
|
||||
|
@ -113,7 +125,7 @@ const Tooltip: React.VoidFunctionComponent<TooltipProps> = ({
|
|||
data-popper-arrow
|
||||
></div>
|
||||
{typeof content === "string" ? preventWidows(content) : content}
|
||||
</Transition>
|
||||
</motion.div>
|
||||
</div>,
|
||||
portal,
|
||||
)
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import * as React from "react";
|
||||
|
||||
export const TransitionPopInOut: React.VoidFunctionComponent<{
|
||||
children: React.ReactNode;
|
||||
show?: boolean;
|
||||
}> = ({ children, show }) => {
|
||||
return (
|
||||
<Transition
|
||||
show={show}
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
{children}
|
||||
</Transition>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue