diff --git a/package.json b/package.json index baadf0087..9dc263258 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@next/bundle-analyzer": "^12.3.4", "@next/font": "^13.1.3", "@prisma/client": "^4.9.0", + "@radix-ui/react-popover": "^1.0.3", "@sentry/nextjs": "^7.33.0", "@svgr/webpack": "^6.2.1", "@tailwindcss/forms": "^0.4.0", @@ -60,6 +61,7 @@ "spacetime": "^7.1.4", "superjson": "^1.9.1", "tailwindcss": "^3.2.4", + "tailwindcss-animate": "^1.0.5", "timezone-soft": "^1.3.1", "typescript": "^4.9.4", "zod": "^3.20.2" diff --git a/public/locales/en/app.json b/public/locales/en/app.json index 4d47780fa..d26fe0921 100644 --- a/public/locales/en/app.json +++ b/public/locales/en/app.json @@ -67,7 +67,10 @@ "name": "Name", "namePlaceholder": "Jessie Smith", "new": "New", + "adminPollTitle": "{{title}}: Admin", "newPoll": "New poll", + "createNew": "Create new", + "home": "Home", "next": "Next", "nextMonth": "Next month", "no": "No", diff --git a/src/components/admin-control.tsx b/src/components/admin-control.tsx new file mode 100644 index 000000000..d505c133c --- /dev/null +++ b/src/components/admin-control.tsx @@ -0,0 +1,134 @@ +import { AnimatePresence, motion } from "framer-motion"; +import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; +import posthog from "posthog-js"; +import React from "react"; +import toast from "react-hot-toast"; +import { useMount } from "react-use"; + +import { Button } from "@/components/button"; +import Share from "@/components/icons/share.svg"; + +import { trpc, trpcNext } from "../utils/trpc"; +import { useParticipants } from "./participants-provider"; +import ManagePoll from "./poll/manage-poll"; +import { useUpdatePollMutation } from "./poll/mutations"; +import NotificationsToggle from "./poll/notifications-toggle"; +import { UnverifiedPollNotice } from "./poll/unverified-poll-notice"; +import { usePoll } from "./poll-context"; +import Sharing from "./sharing"; +import { useUser } from "./user-provider"; + +export const AdminControls = (props: { children?: React.ReactNode }) => { + const { poll, urlId } = usePoll(); + const { t } = useTranslation("app"); + + const router = useRouter(); + + const queryClient = trpcNext.useContext(); + + const session = useUser(); + + const { mutate: updatePollMutation } = useUpdatePollMutation(); + + React.useEffect(() => { + if (router.query.unsubscribe) { + updatePollMutation( + { urlId: urlId, notifications: false }, + { + onSuccess: () => { + toast.success(t("notificationsDisabled")); + posthog.capture("unsubscribed from notifications"); + }, + }, + ); + router.replace(`/admin/${router.query.urlId}`, undefined, { + shallow: true, + }); + } + }, [urlId, router, updatePollMutation, t]); + + const verifyEmail = trpc.useMutation(["polls.verification.verify"], { + onSuccess: () => { + toast.success(t("pollHasBeenVerified")); + queryClient.poll.invalidate(); + session.refresh(); + posthog.capture("verified email"); + }, + onError: () => { + toast.error(t("linkHasExpired")); + }, + onSettled: () => { + router.replace(`/admin/${router.query.urlId}`, undefined, { + shallow: true, + }); + }, + }); + + const { participants } = useParticipants(); + + const [isSharingVisible, setIsSharingVisible] = React.useState( + participants.length === 0, + ); + + useMount(() => { + const { code } = router.query; + if (typeof code === "string" && !poll.verified) { + verifyEmail.mutate({ code, pollId: poll.id }); + } + }); + + return ( +
+
+
+ + + +
+
+ + {isSharingVisible ? ( + + { + setIsSharingVisible(false); + }} + /> + + ) : null} + + + {poll.verified === false ? : null} + {props.children} + +
+ ); +}; diff --git a/src/components/auth/auth-layout.tsx b/src/components/auth/auth-layout.tsx index 81c5c0a23..4f5620e9e 100644 --- a/src/components/auth/auth-layout.tsx +++ b/src/components/auth/auth-layout.tsx @@ -4,11 +4,11 @@ import Logo from "~/public/logo.svg"; export const AuthLayout = ({ children }: { children?: React.ReactNode }) => { return ( -
+
- +
{children}
diff --git a/src/components/auth/login-modal.tsx b/src/components/auth/login-modal.tsx index 35bbc6946..7f2994092 100644 --- a/src/components/auth/login-modal.tsx +++ b/src/components/auth/login-modal.tsx @@ -1,8 +1,7 @@ import Link from "next/link"; import React from "react"; -import Logo from "~/public/logo.svg"; - +import { Logo } from "../logo"; import { useModalContext } from "../modal/modal-provider"; import { useUser } from "../user-provider"; import { LoginForm, RegisterForm } from "./login-form"; @@ -16,7 +15,7 @@ export const LoginModal: React.VoidFunctionComponent<{ return (
- +
{hasAccount ? ( diff --git a/src/components/button.tsx b/src/components/button.tsx index cf39ca394..f1d649bfc 100644 --- a/src/components/button.tsx +++ b/src/components/button.tsx @@ -56,6 +56,7 @@ export const Button = React.forwardRef( className, )} {...passThroughProps} + role="button" disabled={disabled || loading} > {loading ? ( diff --git a/src/components/create-poll.tsx b/src/components/create-poll.tsx index 3f9444ea7..9b5a6bbc6 100644 --- a/src/components/create-poll.tsx +++ b/src/components/create-poll.tsx @@ -1,5 +1,4 @@ import { NextPage } from "next"; -import Head from "next/head"; import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; import posthog from "posthog-js"; @@ -100,7 +99,7 @@ const Page: NextPage = ({ optionsView: formData?.options?.view, }); setPersistedFormData(initialNewEventData); - router.replace(`/admin/${res.urlId}?sharing=true`); + router.replace(`/admin/${res.urlId}`); }, }); @@ -146,76 +145,76 @@ const Page: NextPage = ({ return ( - - {formData?.eventDetails?.title ?? t("newPoll")} - - -
-
-
-

{t("newPoll")}

- -
-
- {(() => { - switch (currentStepName) { - case "eventDetails": - return ( - - ); - case "options": - return ( - - ); - case "userDetails": - return ( - - ); - } - })()} -
- {currentStepIndex > 0 ? ( +
+
+
+
+

+ {t("createNew")} +

+ +
+
+ {(() => { + switch (currentStepName) { + case "eventDetails": + return ( + + ); + case "options": + return ( + + ); + case "userDetails": + return ( + + ); + } + })()} +
+ {currentStepIndex > 0 ? ( + + ) : null} - ) : null} - +
diff --git a/src/components/date-card.tsx b/src/components/date-card.tsx index 5ac7820e2..eb59bdb46 100644 --- a/src/components/date-card.tsx +++ b/src/components/date-card.tsx @@ -5,7 +5,6 @@ export interface DateCardProps { annotation?: React.ReactNode; day: string; month: string; - dow: string; className?: string; } @@ -13,27 +12,23 @@ const DateCard: React.VoidFunctionComponent = ({ annotation, className, day, - dow, month, }) => { return (
{annotation ? (
{annotation}
) : null} -
- - {dow.substring(0, 3)} - -
-
{day}
-
- {month} +
+
+ {month} +
+
{day}
); diff --git a/src/components/discussion/discussion.tsx b/src/components/discussion/discussion.tsx index 6100b60c2..787514670 100644 --- a/src/components/discussion/discussion.tsx +++ b/src/components/discussion/discussion.tsx @@ -1,5 +1,4 @@ import clsx from "clsx"; -import { AnimatePresence, motion } from "framer-motion"; import { useTranslation } from "next-i18next"; import posthog from "posthog-js"; import * as React from "react"; @@ -81,77 +80,64 @@ const Discussion: React.VoidFunctionComponent = () => { } return ( -
-
+
+
{t("comments")}
0, + "bg-pattern space-y-3 border-b p-3": comments.length > 0, })} > - - {comments.map((comment) => { - const canDelete = - admin || session.ownsObject(comment) || isUnclaimed(comment); + {comments.map((comment) => { + const canDelete = + admin || session.ownsObject(comment) || isUnclaimed(comment); - return ( - +
- -
- + +
+ + + {dayjs(new Date(comment.createdAt)).fromNow()} + +
+ } + > + { + deleteComment.mutate({ + commentId: comment.id, + pollId, + }); + }} /> -
- - - {dayjs(new Date(comment.createdAt)).fromNow()} - -
- } - > - { - deleteComment.mutate({ - commentId: comment.id, - pollId, - }); - }} - /> - -
-
- {comment.content} -
-
- - ); - })} - + +
+
+ {comment.content} +
+
+
+ ); + })}
{ await addComment.mutateAsync({ authorName, content, pollId }); reset({ authorName, content: "" }); diff --git a/src/components/dropdown.tsx b/src/components/dropdown.tsx index 875080f1e..1b42f6155 100644 --- a/src/components/dropdown.tsx +++ b/src/components/dropdown.tsx @@ -8,14 +8,11 @@ import { } from "@floating-ui/react-dom-interactions"; import { Menu } from "@headlessui/react"; import clsx from "clsx"; -import { motion } from "framer-motion"; import * as React from "react"; import { transformOriginByPlacement } from "@/utils/constants"; import { stopPropagation } from "@/utils/stop-propagation"; -const MotionMenuItems = motion(Menu.Items); - export interface DropdownProps { trigger?: React.ReactNode; children?: React.ReactNode; @@ -55,13 +52,9 @@ const Dropdown: React.VoidFunctionComponent = ({ {open ? ( - = ({ }} > {children} - + ) : null} diff --git a/src/components/error-page.tsx b/src/components/error-page.tsx index 652372394..b35899d58 100644 --- a/src/components/error-page.tsx +++ b/src/components/error-page.tsx @@ -35,12 +35,7 @@ const ErrorPage: React.VoidFunctionComponent = ({

{description}

- + {t("goToHome")}