mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-02 03:36:33 +02:00
Add option to delete poll (#174)
This commit is contained in:
parent
2c4157ea24
commit
c170e03b6a
25 changed files with 271 additions and 104 deletions
|
@ -53,5 +53,8 @@
|
||||||
"24h": "24-hour",
|
"24h": "24-hour",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"ifNeedBe": "If need be"
|
"ifNeedBe": "If need be",
|
||||||
|
"areYouSure": "Are you sure?",
|
||||||
|
"deletePollDescription": "All data related to this poll will be deleted. This action <b>cannot be undone</b>. To confirm, please type <s>“{{confirmText}}”</s> in to the input below:",
|
||||||
|
"deletePoll": "Delete poll"
|
||||||
}
|
}
|
||||||
|
|
69
src/components/button.tsx
Normal file
69
src/components/button.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import clsx from "clsx";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import SpinnerIcon from "@/components/icons/spinner.svg";
|
||||||
|
|
||||||
|
export interface ButtonProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
icon?: React.ReactElement;
|
||||||
|
htmlType?: React.ButtonHTMLAttributes<HTMLButtonElement>["type"];
|
||||||
|
type?: "default" | "primary" | "danger" | "link";
|
||||||
|
form?: string;
|
||||||
|
rounded?: boolean;
|
||||||
|
title?: string;
|
||||||
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
function Button(
|
||||||
|
{
|
||||||
|
children,
|
||||||
|
loading,
|
||||||
|
type = "default",
|
||||||
|
htmlType = "button",
|
||||||
|
className,
|
||||||
|
icon,
|
||||||
|
disabled,
|
||||||
|
rounded,
|
||||||
|
...passThroughProps
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
type={htmlType}
|
||||||
|
className={clsx(
|
||||||
|
{
|
||||||
|
"btn-default": type === "default",
|
||||||
|
"btn-primary": type === "primary",
|
||||||
|
"btn-danger": type === "danger",
|
||||||
|
"btn-link": type === "link",
|
||||||
|
"btn-disabled": disabled,
|
||||||
|
"h-auto rounded-full p-2": rounded,
|
||||||
|
"w-10 p-0": !children,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...passThroughProps}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<SpinnerIcon
|
||||||
|
className={clsx("inline-block w-5 animate-spin", {
|
||||||
|
"mr-2": !!children,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : icon ? (
|
||||||
|
React.cloneElement(icon, {
|
||||||
|
className: clsx("w-5 h-5", { "-ml-1 mr-2": !!children }),
|
||||||
|
})
|
||||||
|
) : null}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
|
@ -1,69 +0,0 @@
|
||||||
import clsx from "clsx";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import SpinnerIcon from "../icons/spinner.svg";
|
|
||||||
|
|
||||||
export interface ButtonProps {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
loading?: boolean;
|
|
||||||
icon?: React.ReactElement;
|
|
||||||
htmlType?: React.ButtonHTMLAttributes<HTMLButtonElement>["type"];
|
|
||||||
type?: "default" | "primary" | "danger" | "link";
|
|
||||||
form?: string;
|
|
||||||
rounded?: boolean;
|
|
||||||
title?: string;
|
|
||||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Button: React.ForwardRefRenderFunction<HTMLButtonElement, ButtonProps> = (
|
|
||||||
{
|
|
||||||
children,
|
|
||||||
loading,
|
|
||||||
type = "default",
|
|
||||||
htmlType = "button",
|
|
||||||
className,
|
|
||||||
icon,
|
|
||||||
disabled,
|
|
||||||
rounded,
|
|
||||||
...passThroughProps
|
|
||||||
},
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
ref={ref}
|
|
||||||
type={htmlType}
|
|
||||||
className={clsx(
|
|
||||||
{
|
|
||||||
"btn-default": type === "default",
|
|
||||||
"btn-primary": type === "primary",
|
|
||||||
"btn-danger": type === "danger",
|
|
||||||
"btn-link": type === "link",
|
|
||||||
"btn-disabled": disabled,
|
|
||||||
"h-auto rounded-full p-2": rounded,
|
|
||||||
"w-10 p-0": !children,
|
|
||||||
},
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...passThroughProps}
|
|
||||||
disabled={disabled || loading}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<SpinnerIcon
|
|
||||||
className={clsx("inline-block w-5 animate-spin", {
|
|
||||||
"mr-2": !!children,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
) : icon ? (
|
|
||||||
React.cloneElement(icon, {
|
|
||||||
className: clsx("w-5 h-5", { "-ml-1 mr-2": !!children }),
|
|
||||||
})
|
|
||||||
) : null}
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.forwardRef(Button);
|
|
|
@ -1,2 +0,0 @@
|
||||||
export type { ButtonProps } from "./button";
|
|
||||||
export { default } from "./button";
|
|
|
@ -8,7 +8,7 @@ import { useSessionStorage } from "react-use";
|
||||||
|
|
||||||
import { encodeDateOption } from "../utils/date-time-utils";
|
import { encodeDateOption } from "../utils/date-time-utils";
|
||||||
import { trpc } from "../utils/trpc";
|
import { trpc } from "../utils/trpc";
|
||||||
import Button from "./button";
|
import { Button } from "./button";
|
||||||
import {
|
import {
|
||||||
NewEventData,
|
NewEventData,
|
||||||
PollDetailsData,
|
PollDetailsData,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
||||||
|
|
||||||
import Chat from "@/components/icons/chat.svg";
|
import Chat from "@/components/icons/chat.svg";
|
||||||
|
|
||||||
import Button from "./button";
|
import { Button } from "./button";
|
||||||
|
|
||||||
const crispWebsiteId = process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID;
|
const crispWebsiteId = process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID;
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { requiredString } from "../../utils/form-validation";
|
import { requiredString } from "../../utils/form-validation";
|
||||||
import { trpc } from "../../utils/trpc";
|
import { trpc } from "../../utils/trpc";
|
||||||
import Button from "../button";
|
import { Button } from "../button";
|
||||||
import CompactButton from "../compact-button";
|
import CompactButton from "../compact-button";
|
||||||
import Dropdown, { DropdownItem } from "../dropdown";
|
import Dropdown, { DropdownItem } from "../dropdown";
|
||||||
import DotsHorizontal from "../icons/dots-horizontal.svg";
|
import DotsHorizontal from "../icons/dots-horizontal.svg";
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import Button from "@/components/button";
|
import { Button } from "@/components/button";
|
||||||
import Chat from "@/components/icons/chat.svg";
|
import Chat from "@/components/icons/chat.svg";
|
||||||
import EmojiSad from "@/components/icons/emoji-sad.svg";
|
import EmojiSad from "@/components/icons/emoji-sad.svg";
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ const ErrorPage: React.VoidFunctionComponent<ComponentProps> = ({
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Icon className="mb-4 inline-block w-24 text-slate-400" />
|
<Icon className="mb-4 inline-block w-24 text-slate-400" />
|
||||||
<div className="text-3xl font-bold uppercase text-indigo-500 ">
|
<div className="mb-2 text-3xl font-bold text-indigo-500 ">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
<p>{description}</p>
|
<p>{description}</p>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
getDateProps,
|
getDateProps,
|
||||||
removeAllOptionsForDay,
|
removeAllOptionsForDay,
|
||||||
} from "../../../../utils/date-time-utils";
|
} from "../../../../utils/date-time-utils";
|
||||||
import Button from "../../../button";
|
import { Button } from "../../../button";
|
||||||
import CompactButton from "../../../compact-button";
|
import CompactButton from "../../../compact-button";
|
||||||
import DateCard from "../../../date-card";
|
import DateCard from "../../../date-card";
|
||||||
import Dropdown, { DropdownItem } from "../../../dropdown";
|
import Dropdown, { DropdownItem } from "../../../dropdown";
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 272 B After Width: | Height: | Size: 329 B |
|
@ -4,7 +4,7 @@ import { usePlausible } from "next-plausible";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import Button from "@/components/button";
|
import { Button } from "@/components/button";
|
||||||
import Magic from "@/components/icons/magic.svg";
|
import Magic from "@/components/icons/magic.svg";
|
||||||
import { validEmail } from "@/utils/form-validation";
|
import { validEmail } from "@/utils/form-validation";
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,15 @@ export interface ModalProviderProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ModalContentProps = { close: () => void };
|
||||||
|
|
||||||
|
interface ModalConfig extends ModalProps {
|
||||||
|
content?: React.ReactNode | ((props: ModalContentProps) => React.ReactNode);
|
||||||
|
}
|
||||||
|
|
||||||
const ModalContext =
|
const ModalContext =
|
||||||
React.createContext<{
|
React.createContext<{
|
||||||
render: (el: ModalProps) => void;
|
render: (el: ModalConfig) => void;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
ModalContext.displayName = "<ModalProvider />";
|
ModalContext.displayName = "<ModalProvider />";
|
||||||
|
@ -22,7 +28,7 @@ export const useModalContext = () => {
|
||||||
const ModalProvider: React.VoidFunctionComponent<ModalProviderProps> = ({
|
const ModalProvider: React.VoidFunctionComponent<ModalProviderProps> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const [modals, { push, removeAt, updateAt }] = useList<ModalProps>([]);
|
const [modals, { push, removeAt, updateAt }] = useList<ModalConfig>([]);
|
||||||
|
|
||||||
const removeModalAt = (index: number) => {
|
const removeModalAt = (index: number) => {
|
||||||
updateAt(index, { ...modals[index], visible: false });
|
updateAt(index, { ...modals[index], visible: false });
|
||||||
|
@ -44,6 +50,11 @@ const ModalProvider: React.VoidFunctionComponent<ModalProviderProps> = ({
|
||||||
key={i}
|
key={i}
|
||||||
visible={true}
|
visible={true}
|
||||||
{...props}
|
{...props}
|
||||||
|
content={
|
||||||
|
typeof props.content === "function"
|
||||||
|
? props.content({ close: () => removeModalAt(i) })
|
||||||
|
: props.content
|
||||||
|
}
|
||||||
onOk={() => {
|
onOk={() => {
|
||||||
props.onOk?.();
|
props.onOk?.();
|
||||||
removeModalAt(i);
|
removeModalAt(i);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as React from "react";
|
||||||
|
|
||||||
import X from "@/components/icons/x.svg";
|
import X from "@/components/icons/x.svg";
|
||||||
|
|
||||||
import Button, { ButtonProps } from "../button";
|
import { Button, ButtonProps } from "../button";
|
||||||
|
|
||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Participant, Vote, VoteType } from "@prisma/client";
|
||||||
import { keyBy } from "lodash";
|
import { keyBy } from "lodash";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import Trash from "@/components/icons/trash.svg";
|
||||||
import {
|
import {
|
||||||
decodeOptions,
|
decodeOptions,
|
||||||
getBrowserTimeZone,
|
getBrowserTimeZone,
|
||||||
|
@ -10,6 +11,7 @@ import {
|
||||||
} from "@/utils/date-time-utils";
|
} from "@/utils/date-time-utils";
|
||||||
import { GetPollApiResponse } from "@/utils/trpc/types";
|
import { GetPollApiResponse } from "@/utils/trpc/types";
|
||||||
|
|
||||||
|
import ErrorPage from "./error-page";
|
||||||
import { useParticipants } from "./participants-provider";
|
import { useParticipants } from "./participants-provider";
|
||||||
import { usePreferences } from "./preferences/use-preferences";
|
import { usePreferences } from "./preferences/use-preferences";
|
||||||
import { useSession } from "./session";
|
import { useSession } from "./session";
|
||||||
|
@ -22,6 +24,8 @@ type PollContextValue = {
|
||||||
setTargetTimeZone: (timeZone: string) => void;
|
setTargetTimeZone: (timeZone: string) => void;
|
||||||
pollType: "date" | "timeSlot";
|
pollType: "date" | "timeSlot";
|
||||||
highScore: number;
|
highScore: number;
|
||||||
|
isDeleted: boolean;
|
||||||
|
setDeleted: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
optionIds: string[];
|
optionIds: string[];
|
||||||
// TODO (Luke Vella) [2022-05-18]: Move this stuff to participants provider
|
// TODO (Luke Vella) [2022-05-18]: Move this stuff to participants provider
|
||||||
getParticipantsWhoVotedForOption: (optionId: string) => Participant[]; // maybe just attach votes to parsed options
|
getParticipantsWhoVotedForOption: (optionId: string) => Participant[]; // maybe just attach votes to parsed options
|
||||||
|
@ -49,7 +53,7 @@ export const PollContextProvider: React.VoidFunctionComponent<{
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}> = ({ value: poll, children }) => {
|
}> = ({ value: poll, children }) => {
|
||||||
const { participants } = useParticipants();
|
const { participants } = useParticipants();
|
||||||
|
const [isDeleted, setDeleted] = React.useState(false);
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
const [targetTimeZone, setTargetTimeZone] =
|
const [targetTimeZone, setTargetTimeZone] =
|
||||||
React.useState(getBrowserTimeZone);
|
React.useState(getBrowserTimeZone);
|
||||||
|
@ -145,9 +149,20 @@ export const PollContextProvider: React.VoidFunctionComponent<{
|
||||||
...parsedOptions,
|
...parsedOptions,
|
||||||
targetTimeZone,
|
targetTimeZone,
|
||||||
setTargetTimeZone,
|
setTargetTimeZone,
|
||||||
|
isDeleted,
|
||||||
|
setDeleted,
|
||||||
};
|
};
|
||||||
}, [getScore, locale, participants, poll, targetTimeZone, user]);
|
}, [getScore, isDeleted, locale, participants, poll, targetTimeZone, user]);
|
||||||
|
|
||||||
|
if (isDeleted) {
|
||||||
|
return (
|
||||||
|
<ErrorPage
|
||||||
|
icon={Trash}
|
||||||
|
title="Deleted poll"
|
||||||
|
description="This poll doesn't exist anymore."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<PollContext.Provider value={contextValue}>{children}</PollContext.Provider>
|
<PollContext.Provider value={contextValue}>{children}</PollContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,7 +6,7 @@ import React from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useMount } from "react-use";
|
import { useMount } from "react-use";
|
||||||
|
|
||||||
import Button from "@/components/button";
|
import { Button } from "@/components/button";
|
||||||
import LocationMarker from "@/components/icons/location-marker.svg";
|
import LocationMarker from "@/components/icons/location-marker.svg";
|
||||||
import LockClosed from "@/components/icons/lock-closed.svg";
|
import LockClosed from "@/components/icons/lock-closed.svg";
|
||||||
import Share from "@/components/icons/share.svg";
|
import Share from "@/components/icons/share.svg";
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as React from "react";
|
||||||
import { useMeasure } from "react-use";
|
import { useMeasure } from "react-use";
|
||||||
import smoothscroll from "smoothscroll-polyfill";
|
import smoothscroll from "smoothscroll-polyfill";
|
||||||
|
|
||||||
import Button from "../button";
|
import { Button } from "../button";
|
||||||
import ArrowLeft from "../icons/arrow-left.svg";
|
import ArrowLeft from "../icons/arrow-left.svg";
|
||||||
import ArrowRight from "../icons/arrow-right.svg";
|
import ArrowRight from "../icons/arrow-right.svg";
|
||||||
import PlusCircle from "../icons/plus-circle.svg";
|
import PlusCircle from "../icons/plus-circle.svg";
|
||||||
|
|
|
@ -7,7 +7,7 @@ import Check from "@/components/icons/check.svg";
|
||||||
import X from "@/components/icons/x.svg";
|
import X from "@/components/icons/x.svg";
|
||||||
|
|
||||||
import { requiredString } from "../../../utils/form-validation";
|
import { requiredString } from "../../../utils/form-validation";
|
||||||
import Button from "../../button";
|
import { Button } from "../../button";
|
||||||
import NameInput from "../../name-input";
|
import NameInput from "../../name-input";
|
||||||
import { usePoll } from "../../poll-context";
|
import { usePoll } from "../../poll-context";
|
||||||
import { normalizeVotes } from "../mutations";
|
import { normalizeVotes } from "../mutations";
|
||||||
|
|
|
@ -2,13 +2,14 @@ import { Placement } from "@floating-ui/react-dom-interactions";
|
||||||
import { Trans, useTranslation } from "next-i18next";
|
import { Trans, useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import Button from "@/components/button";
|
import { Button } from "@/components/button";
|
||||||
import Cog from "@/components/icons/cog.svg";
|
import Cog from "@/components/icons/cog.svg";
|
||||||
import LockClosed from "@/components/icons/lock-closed.svg";
|
import LockClosed from "@/components/icons/lock-closed.svg";
|
||||||
import LockOpen from "@/components/icons/lock-open.svg";
|
import LockOpen from "@/components/icons/lock-open.svg";
|
||||||
import Pencil from "@/components/icons/pencil-alt.svg";
|
import Pencil from "@/components/icons/pencil-alt.svg";
|
||||||
import Save from "@/components/icons/save.svg";
|
import Save from "@/components/icons/save.svg";
|
||||||
import Table from "@/components/icons/table.svg";
|
import Table from "@/components/icons/table.svg";
|
||||||
|
import Trash from "@/components/icons/trash.svg";
|
||||||
import { encodeDateOption } from "@/utils/date-time-utils";
|
import { encodeDateOption } from "@/utils/date-time-utils";
|
||||||
|
|
||||||
import Dropdown, { DropdownItem } from "../dropdown";
|
import Dropdown, { DropdownItem } from "../dropdown";
|
||||||
|
@ -16,6 +17,7 @@ import { PollDetailsForm } from "../forms";
|
||||||
import { useModal } from "../modal";
|
import { useModal } from "../modal";
|
||||||
import { useModalContext } from "../modal/modal-provider";
|
import { useModalContext } from "../modal/modal-provider";
|
||||||
import { usePoll } from "../poll-context";
|
import { usePoll } from "../poll-context";
|
||||||
|
import { DeletePollForm } from "./manage-poll/delete-poll-form";
|
||||||
import { useCsvExporter } from "./manage-poll/use-csv-exporter";
|
import { useCsvExporter } from "./manage-poll/use-csv-exporter";
|
||||||
import { useUpdatePollMutation } from "./mutations";
|
import { useUpdatePollMutation } from "./mutations";
|
||||||
|
|
||||||
|
@ -25,7 +27,7 @@ const ManagePoll: React.VoidFunctionComponent<{
|
||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
}> = ({ placement }) => {
|
}> = ({ placement }) => {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const { poll, getParticipantsWhoVotedForOption } = usePoll();
|
const { poll, getParticipantsWhoVotedForOption, setDeleted } = usePoll();
|
||||||
|
|
||||||
const { exportToCsv } = useCsvExporter();
|
const { exportToCsv } = useCsvExporter();
|
||||||
|
|
||||||
|
@ -206,6 +208,26 @@ const ManagePoll: React.VoidFunctionComponent<{
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<DropdownItem
|
||||||
|
icon={Trash}
|
||||||
|
label="Delete poll"
|
||||||
|
onClick={() => {
|
||||||
|
modalContext.render({
|
||||||
|
overlayClosable: true,
|
||||||
|
content: ({ close }) => (
|
||||||
|
<DeletePollForm
|
||||||
|
onConfirm={async () => {
|
||||||
|
close();
|
||||||
|
setDeleted(true);
|
||||||
|
}}
|
||||||
|
onCancel={close}
|
||||||
|
urlId={poll.urlId}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
footer: null,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
89
src/components/poll/manage-poll/delete-poll-form.tsx
Normal file
89
src/components/poll/manage-poll/delete-poll-form.tsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { Trans, useTranslation } from "next-i18next";
|
||||||
|
import { usePlausible } from "next-plausible";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Button } from "@/components/button";
|
||||||
|
import Exclamation from "@/components/icons/exclamation.svg";
|
||||||
|
|
||||||
|
import { trpc } from "../../../utils/trpc";
|
||||||
|
|
||||||
|
const confirmText = "delete-me";
|
||||||
|
|
||||||
|
export const DeletePollForm: React.VoidFunctionComponent<{
|
||||||
|
onCancel: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
urlId: string;
|
||||||
|
}> = ({ onCancel, onConfirm, urlId }) => {
|
||||||
|
const { register, handleSubmit, formState, watch } =
|
||||||
|
useForm<{ confirmation: string }>();
|
||||||
|
|
||||||
|
const plausible = usePlausible();
|
||||||
|
|
||||||
|
const confirmationText = watch("confirmation");
|
||||||
|
const canDelete = confirmationText === confirmText;
|
||||||
|
const deletePoll = trpc.useMutation("polls.delete", {
|
||||||
|
onSuccess: () => {
|
||||||
|
plausible("Deleted poll");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation("app");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex max-w-lg space-x-6 p-5">
|
||||||
|
<div className="">
|
||||||
|
<div className="rounded-full bg-rose-100 p-3">
|
||||||
|
<Exclamation className="w-8 text-rose-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
data-testid="delete-poll-form"
|
||||||
|
onSubmit={handleSubmit(async () => {
|
||||||
|
await deletePoll.mutateAsync({ urlId });
|
||||||
|
onConfirm();
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="mb-3 text-xl font-medium text-slate-800">
|
||||||
|
{t("areYouSure")}
|
||||||
|
</div>
|
||||||
|
<p className="text-slate-500">
|
||||||
|
<Trans
|
||||||
|
t={t}
|
||||||
|
i18nKey="deletePollDescription"
|
||||||
|
values={{ confirmText }}
|
||||||
|
components={{
|
||||||
|
b: <strong />,
|
||||||
|
s: <span className="whitespace-nowrap" />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<div className="mb-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={clsx("input w-full", {
|
||||||
|
"input-error": formState.errors.confirmation,
|
||||||
|
})}
|
||||||
|
placeholder={confirmText}
|
||||||
|
{...register("confirmation", {
|
||||||
|
validate: (value) => value === confirmText,
|
||||||
|
})}
|
||||||
|
readOnly={formState.isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<Button onClick={onCancel}>{t("cancel")}</Button>
|
||||||
|
<Button
|
||||||
|
disabled={!canDelete}
|
||||||
|
htmlType="submit"
|
||||||
|
type="danger"
|
||||||
|
loading={formState.isSubmitting}
|
||||||
|
>
|
||||||
|
{t("deletePoll")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -15,7 +15,7 @@ import Trash from "@/components/icons/trash.svg";
|
||||||
import { usePoll } from "@/components/poll-context";
|
import { usePoll } from "@/components/poll-context";
|
||||||
|
|
||||||
import { requiredString } from "../../utils/form-validation";
|
import { requiredString } from "../../utils/form-validation";
|
||||||
import Button from "../button";
|
import { Button } from "../button";
|
||||||
import { styleMenuItem } from "../menu-styles";
|
import { styleMenuItem } from "../menu-styles";
|
||||||
import NameInput from "../name-input";
|
import NameInput from "../name-input";
|
||||||
import { useParticipants } from "../participants-provider";
|
import { useParticipants } from "../participants-provider";
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Trans, useTranslation } from "next-i18next";
|
||||||
import { usePlausible } from "next-plausible";
|
import { usePlausible } from "next-plausible";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import Button from "@/components/button";
|
import { Button } from "@/components/button";
|
||||||
import Bell from "@/components/icons/bell.svg";
|
import Bell from "@/components/icons/bell.svg";
|
||||||
import BellCrossed from "@/components/icons/bell-crossed.svg";
|
import BellCrossed from "@/components/icons/bell-crossed.svg";
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as React from "react";
|
||||||
|
|
||||||
import { trpc } from "../../utils/trpc";
|
import { trpc } from "../../utils/trpc";
|
||||||
import Badge from "../badge";
|
import Badge from "../badge";
|
||||||
import Button from "../button";
|
import { Button } from "../button";
|
||||||
import { usePoll } from "../poll-context";
|
import { usePoll } from "../poll-context";
|
||||||
import Popover from "../popover";
|
import Popover from "../popover";
|
||||||
import { usePreferences } from "../preferences/use-preferences";
|
import { usePreferences } from "../preferences/use-preferences";
|
||||||
|
|
|
@ -6,7 +6,7 @@ import * as React from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useCopyToClipboard } from "react-use";
|
import { useCopyToClipboard } from "react-use";
|
||||||
|
|
||||||
import Button from "./button";
|
import { Button } from "./button";
|
||||||
|
|
||||||
export interface SharingProps {
|
export interface SharingProps {
|
||||||
links: Link[];
|
links: Link[];
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { prisma } from "~/prisma/db";
|
import { prisma } from "~/prisma/db";
|
||||||
|
|
||||||
import { absoluteUrl } from "../../utils/absolute-url";
|
import { absoluteUrl } from "../../utils/absolute-url";
|
||||||
import { sendEmailTemplate } from "../../utils/api-utils";
|
import { sendEmailTemplate } from "../../utils/api-utils";
|
||||||
import { createToken } from "../../utils/auth";
|
import { createToken } from "../../utils/auth";
|
||||||
|
@ -199,4 +201,20 @@ export const polls = createRouter()
|
||||||
|
|
||||||
return createPollResponse(poll, link);
|
return createPollResponse(poll, link);
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
.mutation("delete", {
|
||||||
|
input: z.object({
|
||||||
|
urlId: z.string(),
|
||||||
|
}),
|
||||||
|
resolve: async ({ input: { urlId } }) => {
|
||||||
|
const link = await getLink(urlId);
|
||||||
|
if (link.role !== "admin") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "Tried to delete poll using participant url",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.poll.delete({ where: { urlId: link.pollId } });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,7 @@
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
test("should be able to create a new poll", async ({ page, context }) => {
|
test("should be able to create a new poll and delete it", async ({ page }) => {
|
||||||
await context.addCookies([
|
|
||||||
{
|
|
||||||
name: "rallly_cookie_consent",
|
|
||||||
value: "1",
|
|
||||||
url: "http://localhost",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
await page.goto("/new");
|
await page.goto("/new");
|
||||||
// // // Find an element with the text 'About Page' and click on it
|
|
||||||
await page.type('[placeholder="Monthly Meetup"]', "Monthly Meetup");
|
await page.type('[placeholder="Monthly Meetup"]', "Monthly Meetup");
|
||||||
// click on label to focus on input
|
// click on label to focus on input
|
||||||
await page.click('text="Location"');
|
await page.click('text="Location"');
|
||||||
|
@ -23,6 +15,7 @@ test("should be able to create a new poll", async ({ page, context }) => {
|
||||||
|
|
||||||
await page.click('[title="Next month"]');
|
await page.click('[title="Next month"]');
|
||||||
|
|
||||||
|
// Select a few days
|
||||||
await page.click("text=/^5$/");
|
await page.click("text=/^5$/");
|
||||||
await page.click("text=/^7$/");
|
await page.click("text=/^7$/");
|
||||||
await page.click("text=/^10$/");
|
await page.click("text=/^10$/");
|
||||||
|
@ -38,4 +31,22 @@ test("should be able to create a new poll", async ({ page, context }) => {
|
||||||
await expect(page.locator("data-testid=poll-title")).toHaveText(
|
await expect(page.locator("data-testid=poll-title")).toHaveText(
|
||||||
"Monthly Meetup",
|
"Monthly Meetup",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// let's delete the poll we just created
|
||||||
|
await page.click("text=Manage");
|
||||||
|
await page.click("text=Delete poll");
|
||||||
|
|
||||||
|
const deletePollForm = page.locator("data-testid=delete-poll-form");
|
||||||
|
|
||||||
|
// button should be disabled
|
||||||
|
await expect(deletePollForm.locator("text=Delete poll")).toBeDisabled();
|
||||||
|
|
||||||
|
// enter confirmation text
|
||||||
|
await page.type("[placeholder=delete-me]", "delete-me");
|
||||||
|
|
||||||
|
// button should now be enabled
|
||||||
|
await deletePollForm.locator("text=Delete poll").click();
|
||||||
|
|
||||||
|
// expect delete message to appear
|
||||||
|
await expect(page.locator("text=Deleted poll")).toBeVisible();
|
||||||
});
|
});
|
Loading…
Add table
Reference in a new issue