diff --git a/components/confirm-prompt.tsx b/components/confirm-prompt.tsx new file mode 100644 index 000000000..eb66f07a1 --- /dev/null +++ b/components/confirm-prompt.tsx @@ -0,0 +1,23 @@ +import ReactDOM from "react-dom"; +import Modal, { ModalProps } from "./modal/modal"; + +export const confirmPrompt = (props: ModalProps) => { + const div = document.createElement("div"); + document.body.appendChild(div); + ReactDOM.render( + { + props.onOk?.(); + document.body.removeChild(div); + }} + onCancel={() => { + document.body.removeChild(div); + }} + />, + div, + ); +}; diff --git a/components/modal/modal-provider.tsx b/components/modal/modal-provider.tsx new file mode 100644 index 000000000..826ee917b --- /dev/null +++ b/components/modal/modal-provider.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; +import { useList } from "react-use"; +import { useRequiredContext } from "../use-required-context"; +import Modal, { ModalProps } from "./modal"; + +export interface ModalProviderProps { + children?: React.ReactNode; +} + +const ModalContext = + React.createContext<{ + render: (el: ModalProps) => void; + } | null>(null); + +ModalContext.displayName = ""; + +export const useModalContext = () => { + return useRequiredContext(ModalContext); +}; + +const ModalProvider: React.VoidFunctionComponent = ({ + children, +}) => { + const [modals, { push, removeAt, updateAt }] = useList([]); + + const removeModalAt = (index: number) => { + updateAt(index, { ...modals[index], visible: false }); + setTimeout(() => { + removeAt(index); + }, 500); + }; + return ( + { + push(props); + }, + }} + > + {children} + {modals.map((props, i) => ( + { + props.onOk?.(); + removeModalAt(i); + }} + onCancel={() => { + props.onCancel?.(); + removeModalAt(i); + }} + /> + ))} + + ); +}; + +export default ModalProvider; diff --git a/components/poll/manage-poll.tsx b/components/poll/manage-poll.tsx index 234797be1..95e811027 100644 --- a/components/poll/manage-poll.tsx +++ b/components/poll/manage-poll.tsx @@ -13,8 +13,10 @@ import { decodeDateOption, encodeDateOption } from "utils/date-time-utils"; import Dropdown, { DropdownItem } from "../dropdown"; import { PollDetailsForm } from "../forms"; import { useModal } from "../modal"; +import { useModalContext } from "../modal/modal-provider"; import { usePoll } from "../use-poll"; import { useUpdatePollMutation } from "./mutations"; +import { Trans } from "next-i18next"; const PollOptionsForm = React.lazy(() => import("../forms/poll-options-form")); @@ -25,6 +27,8 @@ const ManagePoll: React.VoidFunctionComponent<{ const { t } = useTranslation("app"); const poll = usePoll(); + const modalContext = useModalContext(); + const { mutate: updatePollMutation, isLoading: isUpdating } = useUpdatePollMutation(); const [ @@ -68,26 +72,52 @@ const ManagePoll: React.VoidFunctionComponent<{ }} onSubmit={(data) => { const encodedOptions = data.options.map(encodeDateOption); - const optionsToDelete = poll.options - .filter((option) => { - return !encodedOptions.includes(option.value); - }) - .map((option) => option.id); + const optionsToDelete = poll.options.filter((option) => { + return !encodedOptions.includes(option.value); + }); const optionsToAdd = encodedOptions.filter( (encodedOption) => !poll.options.find((o) => o.value === encodedOption), ); - updatePollMutation( - { - timeZone: data.timeZone, - optionsToDelete, - optionsToAdd, - }, - { - onSuccess: () => closeChangeOptionsModal(), - }, + + const onOk = () => { + updatePollMutation( + { + timeZone: data.timeZone, + optionsToDelete: optionsToDelete.map(({ id }) => id), + optionsToAdd, + }, + { + onSuccess: () => closeChangeOptionsModal(), + }, + ); + }; + + const optionsToDeleteThatHaveVotes = optionsToDelete.filter( + (option) => option.votes.length > 0, ); + + if (optionsToDeleteThatHaveVotes.length > 0) { + modalContext.render({ + title: "Are you sure?", + description: ( + }} + /> + ), + onOk, + okButtonProps: { + type: "danger", + }, + okText: "Delete", + cancelText: "Cancel", + }); + } else { + onOk(); + } }} /> diff --git a/components/use-required-context.ts b/components/use-required-context.ts new file mode 100644 index 000000000..264dffd33 --- /dev/null +++ b/components/use-required-context.ts @@ -0,0 +1,14 @@ +import React from "react"; + +export const useRequiredContext = ( + context: React.Context, + errorMessage?: string, +) => { + const contextValue = React.useContext(context); + if (contextValue === null) { + throw new Error( + errorMessage ?? `Missing context provider: ${context.displayName}`, + ); + } + return contextValue; +}; diff --git a/pages/_app.tsx b/pages/_app.tsx index 01e2bd8dd..2c1967b0d 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,3 +1,4 @@ +import ModalProvider from "@/components/modal/modal-provider"; import { NextPage } from "next"; import { appWithTranslation } from "next-i18next"; import PlausibleProvider from "next-plausible"; @@ -42,9 +43,11 @@ const MyApp: NextPage = ({ Component, pageProps }) => { - - - + + + + + ); diff --git a/pages/poll.tsx b/pages/poll.tsx index 830e2ab26..0ba7e0adc 100644 --- a/pages/poll.tsx +++ b/pages/poll.tsx @@ -9,6 +9,7 @@ import MobilePoll from "@/components/poll/mobile-poll"; import { useUpdatePollMutation } from "@/components/poll/mutations"; import NotificationsToggle from "@/components/poll/notifications-toggle"; import PollSubheader from "@/components/poll/poll-subheader"; +import TruncatedLinkify from "@/components/poll/truncated-linkify"; import { UserAvatarProvider } from "@/components/poll/user-avatar"; import Popover from "@/components/popover"; import Sharing from "@/components/sharing"; @@ -30,8 +31,6 @@ import { preventWidows } from "utils/prevent-widows"; import { GetPollResponse } from "../api-client/get-poll"; import { getBrowserTimeZone } from "../utils/date-time-utils"; import Custom404 from "./404"; -import Linkify from "react-linkify"; -import TruncatedLinkify from "@/components/poll/truncated-linkify"; const Discussion = React.lazy(() => import("@/components/discussion")); diff --git a/public/locales/en/app.json b/public/locales/en/app.json index 2d0097361..6422f9d54 100644 --- a/public/locales/en/app.json +++ b/public/locales/en/app.json @@ -41,5 +41,6 @@ "participant": "Participant", "participantDescription": "Partial access to vote and comment on this poll.", "unverifiedMessage": "An email has been sent to {{email}} with a link to verify the email address.", - "notificationsOnDescription": "An email will be sent to {{email}} when there is activity on this poll." + "notificationsOnDescription": "An email will be sent to {{email}} when there is activity on this poll.", + "deletingOptionsWarning": "You are deleting options that participants have voted for. Their votes will be also be deleted." }