feat: confirm project delete by typing

refractor: organised imports, changed create-project-model filename to kabab-case, and made modal more bug free
This commit is contained in:
Dakshesh Jain 2022-12-12 14:08:11 +05:30
parent fe7284b9b0
commit 74a27edb09
3 changed files with 96 additions and 38 deletions

View File

@ -9,19 +9,26 @@ import useToast from "lib/hooks/useToast";
// icons // icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui // ui
import { Button } from "ui"; import { Button, Input } from "ui";
// types // types
import type { IProject } from "types"; import type { IProject } from "types";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; onClose: () => void;
data?: IProject; data: IProject | null;
}; };
const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) => { const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, data, onClose }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [selectedProject, setSelectedProject] = useState<IProject | null>(null);
const [confirmProjectName, setConfirmProjectName] = useState("");
const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false);
const canDelete = confirmProjectName === data?.name && confirmDeleteMyProject;
const { activeWorkspace, mutateProjects } = useUser(); const { activeWorkspace, mutateProjects } = useUser();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -29,13 +36,18 @@ const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
const cancelButtonRef = useRef(null); const cancelButtonRef = useRef(null);
const handleClose = () => { const handleClose = () => {
setIsOpen(false);
setIsDeleteLoading(false); setIsDeleteLoading(false);
const timer = setTimeout(() => {
setConfirmProjectName("");
setConfirmDeleteMyProject(false);
clearTimeout(timer);
}, 350);
onClose();
}; };
const handleDeletion = async () => { const handleDeletion = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
if (!data || !activeWorkspace) return; if (!data || !activeWorkspace || !canDelete) return;
await projectService await projectService
.deleteProject(activeWorkspace.slug, data.id) .deleteProject(activeWorkspace.slug, data.id)
.then(() => { .then(() => {
@ -54,8 +66,14 @@ const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
}; };
useEffect(() => { useEffect(() => {
data && setIsOpen(true); if (data) setSelectedProject(data);
}, [data, setIsOpen]); else {
const timer = setTimeout(() => {
setSelectedProject(null);
clearTimeout(timer);
}, 300);
}
}, [data]);
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <Transition.Root show={isOpen} as={React.Fragment}>
@ -104,11 +122,48 @@ const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Are you sure you want to delete project - {`"`} Are you sure you want to delete project - {`"`}
<span className="italic">{data?.name}</span> <span className="italic">{selectedProject?.name}</span>
{`"`} ? All of the data related to the project will be permanently {`"`} ? All of the data related to the project will be permanently
removed. This action cannot be undone. removed. This action cannot be undone.
</p> </p>
</div> </div>
<div className="h-0.5 bg-gray-200 my-3" />
<div className="mt-3">
<p className="text-sm">
Enter the project name{" "}
<span className="font-semibold">{selectedProject?.name}</span> to
continue:
</p>
<Input
type="text"
placeholder="Project name"
className="mt-2"
value={confirmProjectName}
onChange={(e) => {
setConfirmProjectName(e.target.value);
}}
name="projectName"
/>
</div>
<div className="mt-3">
<p className="text-sm">
To confirm, type <span className="font-semibold">delete my project</span>{" "}
below:
</p>
<Input
type="text"
placeholder="Enter 'delete my project'"
className="mt-2"
onChange={(e) => {
if (e.target.value === "delete my project") {
setConfirmDeleteMyProject(true);
} else {
setConfirmDeleteMyProject(false);
}
}}
name="projectName"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -117,7 +172,7 @@ const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
type="button" type="button"
onClick={handleDeletion} onClick={handleDeletion}
theme="danger" theme="danger"
disabled={isDeleteLoading} disabled={isDeleteLoading || !canDelete}
className="inline-flex sm:ml-3" className="inline-flex sm:ml-3"
> >
{isDeleteLoading ? "Deleting..." : "Delete"} {isDeleteLoading ? "Deleting..." : "Delete"}

View File

@ -28,7 +28,7 @@ type Props = {
slug: string; slug: string;
invitationsRespond: string[]; invitationsRespond: string[];
handleInvitation: (project_invitation: any, action: "accepted" | "withdraw") => void; handleInvitation: (project_invitation: any, action: "accepted" | "withdraw") => void;
setDeleteProject: React.Dispatch<React.SetStateAction<IProject | undefined>>; setDeleteProject: (id: string | null) => void;
}; };
const ProjectMemberInvitations: React.FC<Props> = ({ const ProjectMemberInvitations: React.FC<Props> = ({
@ -100,7 +100,7 @@ const ProjectMemberInvitations: React.FC<Props> = ({
<button <button
type="button" type="button"
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none" className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
onClick={() => setDeleteProject(project)} onClick={() => setDeleteProject(project.id)}
> >
<TrashIcon className="h-4 w-4 text-red-500" /> <TrashIcon className="h-4 w-4 text-red-500" />
</button> </button>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useState } from "react";
// next // next
import type { NextPage } from "next"; import type { NextPage } from "next";
// hooks // hooks
@ -8,23 +8,25 @@ import withAuth from "lib/hoc/withAuthWrapper";
// layouts // layouts
import AppLayout from "layouts/AppLayout"; import AppLayout from "layouts/AppLayout";
// components // components
import CreateProjectModal from "components/project/create-project-modal"; import ProjectMemberInvitations from "components/project/memberInvitations";
import ConfirmProjectDeletion from "components/project/ConfirmProjectDeletion"; import ConfirmProjectDeletion from "components/project/confirm-project-deletion";
// ui // ui
import { Button, Spinner } from "ui"; import {
// types Button,
import { IProject } from "types"; Spinner,
HeaderButton,
Breadcrumbs,
BreadcrumbItem,
EmptySpace,
EmptySpaceItem,
} from "ui";
// services // services
import projectService from "lib/services/project.service"; import projectService from "lib/services/project.service";
import ProjectMemberInvitations from "components/project/memberInvitations"; // icons
import { ClipboardDocumentListIcon, PlusIcon } from "@heroicons/react/24/outline"; import { ClipboardDocumentListIcon, PlusIcon } from "@heroicons/react/24/outline";
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace";
import HeaderButton from "ui/HeaderButton";
const Projects: NextPage = () => { const Projects: NextPage = () => {
const [isOpen, setIsOpen] = useState(false); const [deleteProject, setDeleteProject] = useState<string | null>(null);
const [deleteProject, setDeleteProject] = useState<IProject | undefined>();
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]); const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
const { projects, activeWorkspace, mutateProjects } = useUser(); const { projects, activeWorkspace, mutateProjects } = useUser();
@ -54,21 +56,12 @@ const Projects: NextPage = () => {
}); });
}; };
useEffect(() => {
if (isOpen) return;
const timer = setTimeout(() => {
setDeleteProject(undefined);
clearTimeout(timer);
}, 300);
}, [isOpen]);
return ( return (
<AppLayout> <AppLayout>
<CreateProjectModal isOpen={isOpen && !deleteProject} setIsOpen={setIsOpen} />
<ConfirmProjectDeletion <ConfirmProjectDeletion
isOpen={isOpen && !!deleteProject} isOpen={!!deleteProject}
setIsOpen={setIsOpen} onClose={() => setDeleteProject(null)}
data={deleteProject} data={projects?.find((item) => item.id === deleteProject) ?? null}
/> />
{projects ? ( {projects ? (
<> <>
@ -89,7 +82,10 @@ const Projects: NextPage = () => {
</span> </span>
} }
Icon={PlusIcon} Icon={PlusIcon}
action={() => setIsOpen(true)} action={() => {
const e = new KeyboardEvent("keydown", { key: "p", ctrlKey: true });
document.dispatchEvent(e);
}}
/> />
</EmptySpace> </EmptySpace>
</div> </div>
@ -100,7 +96,14 @@ const Projects: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full"> <div className="flex items-center justify-between cursor-pointer w-full">
<h2 className="text-2xl font-medium">Projects</h2> <h2 className="text-2xl font-medium">Projects</h2>
<HeaderButton Icon={PlusIcon} label="Add Project" onClick={() => setIsOpen(true)} /> <HeaderButton
Icon={PlusIcon}
label="Add Project"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p", ctrlKey: true });
document.dispatchEvent(e);
}}
/>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{projects.map((item) => ( {projects.map((item) => (