diff --git a/.eslintrc.json b/.eslintrc.json index 6c0958f7b..d746c1644 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,6 +14,8 @@ "simple-import-sort/exports": "error", "import/first": "error", "import/newline-after-import": "error", - "import/no-duplicates": "error" + "import/no-duplicates": "error", + "@typescript-eslint/no-unused-vars": "error", + "no-console": ["error", { "allow": ["warn", "error", "info"] }] } } diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index 7a1e28605..3af47757f 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -1,11 +1,9 @@ { "12h": "12-hour", "24h": "24-hour", - "addParticipant": "Add participant", "addTimeOption": "Add time option", "adminPollTitle": "{{title}}: Admin", "alreadyRegistered": "Already registered? Login →", - "alreadyVoted": "You have already voted", "applyToAllDates": "Apply to all dates", "areYouSure": "Are you sure?", "back": "Back", @@ -15,15 +13,10 @@ "changeName": "Change name", "changeNameDescription": "Enter a new name for this participant.", "changeNameInfo": "This will not affect any votes you have already made.", + "close": "Close", "comment": "Comment", "commentPlaceholder": "Leave a comment on this poll (visible to everyone)", "comments": "Comments", - "feedbackSent": "Thank you! Your feedback has been sent.", - "feedbackFormLabel": "How can we improve ?", - "feedbackFormPlaceholder": "Share your thoughts…", - "feedbackFormFooter": "Need help? Visit the support page.", - "feedbackFormTitle": "Feedback Form", - "close": "Close", "continue": "Continue", "copied": "Copied", "copyLink": "Copy link", @@ -45,7 +38,6 @@ "demoPollNotice": "Demo polls are automatically deleted after 1 day", "description": "Description", "descriptionPlaceholder": "Hey everyone, please choose the dates that work for you!", - "donate": "Donate", "edit": "Edit", "editDetails": "Edit details", "editOptions": "Edit options", @@ -57,6 +49,11 @@ "endSession": "End session", "expiredOrInvalidLink": "This link is expired or invalid. Please request a new link.", "exportToCsv": "Export to CSV", + "feedbackFormFooter": "Need help? Visit the support page.", + "feedbackFormLabel": "How can we improve ?", + "feedbackFormPlaceholder": "Share your thoughts…", + "feedbackFormTitle": "Feedback Form", + "feedbackSent": "Thank you! Your feedback has been sent.", "forgetMe": "Forget me", "goToAdmin": "Go to Admin", "guest": "Guest", @@ -74,7 +71,6 @@ "loginSuccessful": "You're logged in! Please wait while you are redirected…", "logout": "Logout", "manage": "Manage", - "menu": "Menu", "mixedOptionsDescription": "You can't have both time and date options in the same poll. Which would you like to keep?", "mixedOptionsKeepDates": "Keep date options", "mixedOptionsKeepTimes": "Keep time options", @@ -91,12 +87,10 @@ "nextMonth": "Next month", "no": "No", "noDatesSelected": "No dates selected", - "notificationsDisabled": "Notifications have been disabled", - "notificationsEnabled": "Notifications have been enabled for {{title}}", - "notificationsOff": "Get notified when participants respond to your poll", + "notificationsDisabled": "Notifications have been disabled for {{title}}", + "notificationsGuest": "Log in to turn on notifications", + "notificationsOff": "Notifications are off", "notificationsOn": "Notifications are on", - "notificationsOnDescription": "An email will be sent to {{email}} when there is activity on this poll.", - "notificationsVerifyEmail": "You need to verify your email to turn on notifications", "notRegistered": "Create a new account →", "noVotes": "No one has voted for this option", "ok": "Ok", @@ -115,7 +109,6 @@ "participantCount_two": "{{count}} participants", "participantCount_zero": "{{count}} participants", "pollHasBeenLocked": "This poll has been locked", - "pollOwnerNotice": "Hey {{name}}, looks like you are the owner of this poll.", "pollsEmpty": "No polls created", "possibleAnswers": "Possible answers", "preferences": "Preferences", @@ -145,7 +138,6 @@ "titlePlaceholder": "Monthly Meetup", "today": "Today", "unlockPoll": "Unlock poll", - "unverifiedMessage": "An email has been sent to {{email}} with a link to verify the email address.", "user": "User", "userAlreadyExists": "A user with that email already exists", "userDoesNotExist": "The requested user was not found", @@ -154,7 +146,6 @@ "verificationCodeHelp": "Didn't get the email? Check your spam/junk.", "verificationCodePlaceholder": "Enter your 6-digit code", "verificationCodeSent": "A verification code has been sent to {{email}} Change", - "verificationEmailSent": "An email has been sent to {{email}} with a link to enable notifications", "verifyYourEmail": "Verify your email", "weekStartsOn": "Week starts on", "weekView": "Week view", diff --git a/apps/web/public/locales/en/common.json b/apps/web/public/locales/en/common.json index 97fb5a642..87eb94b6b 100644 --- a/apps/web/public/locales/en/common.json +++ b/apps/web/public/locales/en/common.json @@ -1,7 +1,6 @@ { "blog": "Blog", "discussions": "Discussions", - "donate": "Donate", "footerCredit": "Made by @imlukevella", "footerSponsor": "This project is user-funded. Please consider supporting it by donating.", "home": "Home", diff --git a/apps/web/src/components/admin-control.tsx b/apps/web/src/components/admin-control.tsx index 3c055168c..0b17c0a70 100644 --- a/apps/web/src/components/admin-control.tsx +++ b/apps/web/src/components/admin-control.tsx @@ -1,46 +1,18 @@ import { AnimatePresence, m } from "framer-motion"; -import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; import React from "react"; -import toast from "react-hot-toast"; import { Button } from "@/components/button"; import Share from "@/components/icons/share.svg"; -import { usePostHog } from "@/utils/posthog"; import { useParticipants } from "./participants-provider"; import ManagePoll from "./poll/manage-poll"; -import { useUpdatePollMutation } from "./poll/mutations"; import NotificationsToggle from "./poll/notifications-toggle"; -import { usePoll } from "./poll-context"; import Sharing from "./sharing"; export const AdminControls = (props: { children?: React.ReactNode }) => { - const { urlId } = usePoll(); const { t } = useTranslation("app"); - const router = useRouter(); - - const { mutate: updatePollMutation } = useUpdatePollMutation(); - const posthog = usePostHog(); - - 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, posthog]); - const { participants } = useParticipants(); const [isSharingVisible, setIsSharingVisible] = React.useState( diff --git a/apps/web/src/components/auth/login-modal.tsx b/apps/web/src/components/auth/login-modal.tsx index 045fa7630..ca2962c5e 100644 --- a/apps/web/src/components/auth/login-modal.tsx +++ b/apps/web/src/components/auth/login-modal.tsx @@ -13,7 +13,10 @@ export const LoginModal: React.FunctionComponent<{ const [defaultEmail, setDefaultEmail] = React.useState(""); return ( -
+
diff --git a/apps/web/src/components/create-poll.tsx b/apps/web/src/components/create-poll.tsx index 8f5aef1fe..d5581e508 100644 --- a/apps/web/src/components/create-poll.tsx +++ b/apps/web/src/components/create-poll.tsx @@ -1,7 +1,6 @@ import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; import React from "react"; -import { useSessionStorage } from "react-use"; import { usePostHog } from "@/utils/posthog"; @@ -22,8 +21,6 @@ import { useUser } from "./user-provider"; type StepName = "eventDetails" | "options" | "userDetails"; -const steps: StepName[] = ["eventDetails", "options", "userDetails"]; - const required = (v: T | undefined): T => { if (!v) { throw new Error("Required value is missing"); @@ -32,9 +29,6 @@ const required = (v: T | undefined): T => { return v; }; -const initialNewEventData: NewEventData = { currentStep: 0 }; -const sessionStorageKey = "newEventFormData"; - export interface CreatePollPageProps { title?: string; location?: string; @@ -42,48 +36,35 @@ export interface CreatePollPageProps { view?: "week" | "month"; } -const Page: React.FunctionComponent = ({ - title, - location, - description, - view, -}) => { +const Page: React.FunctionComponent = () => { const { t } = useTranslation("app"); const router = useRouter(); const session = useUser(); - const [persistedFormData, setPersistedFormData] = - useSessionStorage(sessionStorageKey, { - currentStep: 0, - eventDetails: { - title, - location, - description, - }, - options: { - view, - }, - userDetails: - session.user?.isGuest === false - ? { - name: session.user.name, - contact: session.user.email, - } - : undefined, - }); - - const [formData, setTransientFormData] = React.useState(persistedFormData); - - const setFormData = React.useCallback( - (newEventData: NewEventData) => { - setTransientFormData(newEventData); - setPersistedFormData(newEventData); - }, - [setPersistedFormData], + const steps: StepName[] = React.useMemo( + () => + session.user.isGuest + ? ["eventDetails", "options", "userDetails"] + : ["eventDetails", "options"], + [session.user.isGuest], ); + const [formData, setFormData] = React.useState({ + currentStep: 0, + }); + + React.useEffect(() => { + const newStep = Math.min(steps.length - 1, formData.currentStep); + if (newStep !== formData.currentStep) { + setFormData((prevData) => ({ + ...prevData, + currentStep: newStep, + })); + } + }, [formData.currentStep, steps.length]); + const currentStepIndex = formData?.currentStep ?? 0; const currentStepName = steps[currentStepIndex]; @@ -100,7 +81,6 @@ const Page: React.FunctionComponent = ({ numberOfOptions: formData.options?.options?.length, optionsView: formData?.options?.view, }); - setPersistedFormData(initialNewEventData); router.replace(`/admin/${res.urlId}`); }, }); @@ -125,10 +105,12 @@ const Page: React.FunctionComponent = ({ type: "date", location: formData?.eventDetails?.location, description: formData?.eventDetails?.description, - user: { - name: required(formData?.userDetails?.name), - email: required(formData?.userDetails?.contact), - }, + user: session.user.isGuest + ? { + name: required(formData?.userDetails?.name), + email: required(formData?.userDetails?.contact), + } + : undefined, timeZone: formData?.options?.timeZone, options: required(formData?.options?.options).map(encodeDateOption), }); @@ -197,7 +179,7 @@ const Page: React.FunctionComponent = ({ disabled={isBusy} onClick={() => { setFormData({ - ...persistedFormData, + ...formData, currentStep: currentStepIndex - 1, }); }} diff --git a/apps/web/src/components/discussion/discussion.tsx b/apps/web/src/components/discussion/discussion.tsx index 4fab945f3..78d1b3b46 100644 --- a/apps/web/src/components/discussion/discussion.tsx +++ b/apps/web/src/components/discussion/discussion.tsx @@ -42,15 +42,8 @@ const Discussion: React.FunctionComponent = () => { const posthog = usePostHog(); const addComment = trpc.polls.comments.add.useMutation({ - onSuccess: (newComment) => { + onSuccess: () => { posthog?.capture("created comment"); - - queryClient.polls.comments.list.setData( - { pollId }, - (existingComments = []) => { - return [...existingComments, newComment]; - }, - ); }, }); diff --git a/apps/web/src/components/poll/mobile-poll/poll-option.tsx b/apps/web/src/components/poll/mobile-poll/poll-option.tsx index cd3190ac6..5bd6e65bf 100644 --- a/apps/web/src/components/poll/mobile-poll/poll-option.tsx +++ b/apps/web/src/components/poll/mobile-poll/poll-option.tsx @@ -7,7 +7,6 @@ import * as React from "react"; import ChevronDown from "@/components/icons/chevron-down.svg"; import { useParticipants } from "../../participants-provider"; -import { usePoll } from "../../poll-context"; import { ConnectedScoreSummary } from "../score-summary"; import UserAvatar from "../user-avatar"; import VoteIcon from "../vote-icon"; @@ -181,37 +180,13 @@ const PollOption: React.FunctionComponent = ({ onChange, participants, editable = false, - yesScore, optionId, }) => { - const { getVote } = usePoll(); const showVotes = !!(selectedParticipantId || editable); const [expanded, setExpanded] = React.useState(false); const selectorRef = React.useRef(null); const [active, setActive] = React.useState(false); - const score = React.useMemo(() => { - if (!editable) { - return yesScore; - } - - if (selectedParticipantId) { - const currentVote = getVote(selectedParticipantId, optionId); - return ( - yesScore + - (currentVote === "yes" - ? vote === "yes" - ? 0 - : -1 - : vote === "yes" - ? 1 - : 0) - ); - } - - return yesScore + (vote === "yes" ? 1 : 0); - }, [editable, getVote, optionId, selectedParticipantId, vote, yesScore]); - return (
{ const queryClient = trpc.useContext(); const posthog = usePostHog(); return trpc.polls.update.useMutation({ - onSuccess: (data) => { + onSuccess: (_data, { urlId }) => { queryClient.polls.invalidate(); posthog?.capture("updated poll", { - id: data.id, + id: urlId, }); }, }); diff --git a/apps/web/src/components/poll/notifications-toggle.tsx b/apps/web/src/components/poll/notifications-toggle.tsx index 50c0090f3..d2cb1c419 100644 --- a/apps/web/src/components/poll/notifications-toggle.tsx +++ b/apps/web/src/components/poll/notifications-toggle.tsx @@ -1,95 +1,73 @@ -import { Trans, useTranslation } from "next-i18next"; +import { useTranslation } from "next-i18next"; import * as React from "react"; +import { useLoginModal } from "@/components/auth/login-modal"; import { Button } from "@/components/button"; import Bell from "@/components/icons/bell.svg"; import BellCrossed from "@/components/icons/bell-crossed.svg"; +import { useUser } from "@/components/user-provider"; +import { usePostHog } from "@/utils/posthog"; import { trpc } from "@/utils/trpc"; +import { usePollByAdmin } from "@/utils/trpc/hooks"; import { usePoll } from "../poll-context"; import Tooltip from "../tooltip"; -import { useUpdatePollMutation } from "./mutations"; - -const Email = (props: { children?: React.ReactNode }) => { - return ( - - {props.children} - - ); -}; const NotificationsToggle: React.FunctionComponent = () => { - const { poll, urlId } = usePoll(); + const { poll } = usePoll(); const { t } = useTranslation("app"); - const [isUpdatingNotifications, setIsUpdatingNotifications] = - React.useState(false); - const { mutate: updatePollMutation } = useUpdatePollMutation(); - const requestEnableNotifications = - trpc.polls.enableNotifications.useMutation(); + const { data } = usePollByAdmin(); + const watchers = data.watchers ?? []; + + const { user } = useUser(); + const isWatching = watchers.some(({ userId }) => userId === user.id); + + const posthog = usePostHog(); + + const watch = trpc.polls.watch.useMutation({ + onSuccess: () => { + posthog?.capture("turned notifications on"); + }, + }); + + const unwatch = trpc.polls.unwatch.useMutation({ + onSuccess: () => { + posthog?.capture("turned notifications off"); + }, + }); + + const isUpdating = watch.isLoading || unwatch.isLoading; + const { openLoginModal } = useLoginModal(); return ( - {requestEnableNotifications.isSuccess ? ( - }} - /> - ) : poll.notifications ? ( -
-
- {t("notificationsOn")} -
-
- , - }} - /> -
-
- ) : ( - t("notificationsOff") - )} + {user.isGuest + ? t("notificationsGuest") + : isWatching + ? t("notificationsOn") + : t("notificationsOff")}
} > - - The link will expire in 15 minutes. - - ); -}; - -export default EnableNotificationsEmail; diff --git a/turbo.json b/turbo.json index c25dffba1..2754a79a5 100644 --- a/turbo.json +++ b/turbo.json @@ -46,12 +46,6 @@ "db:generate": { "dependsOn": ["^db:generate"] }, - "db:push": { - "cache": false - }, - "db:deploy": { - "cache": false - }, "lint": { "outputs": [] },