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
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
import { Button } from "ui";
import { Button, Input } from "ui";
// types
import type { IProject } from "types";
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
data?: IProject;
onClose: () => void;
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 [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 { setToastAlert } = useToast();
@ -29,13 +36,18 @@ const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
const cancelButtonRef = useRef(null);
const handleClose = () => {
setIsOpen(false);
setIsDeleteLoading(false);
const timer = setTimeout(() => {
setConfirmProjectName("");
setConfirmDeleteMyProject(false);
clearTimeout(timer);
}, 350);
onClose();
};
const handleDeletion = async () => {
setIsDeleteLoading(true);
if (!data || !activeWorkspace) return;
if (!data || !activeWorkspace || !canDelete) return;
await projectService
.deleteProject(activeWorkspace.slug, data.id)
.then(() => {
@ -54,8 +66,14 @@ const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
};
useEffect(() => {
data && setIsOpen(true);
}, [data, setIsOpen]);
if (data) setSelectedProject(data);
else {
const timer = setTimeout(() => {
setSelectedProject(null);
clearTimeout(timer);
}, 300);
}
}, [data]);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
@ -104,11 +122,48 @@ const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
<div className="mt-2">
<p className="text-sm text-gray-500">
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
removed. This action cannot be undone.
</p>
</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>
@ -117,7 +172,7 @@ const ConfirmProjectDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) =>
type="button"
onClick={handleDeletion}
theme="danger"
disabled={isDeleteLoading}
disabled={isDeleteLoading || !canDelete}
className="inline-flex sm:ml-3"
>
{isDeleteLoading ? "Deleting..." : "Delete"}

View File

@ -28,7 +28,7 @@ type Props = {
slug: string;
invitationsRespond: string[];
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> = ({
@ -100,7 +100,7 @@ const ProjectMemberInvitations: React.FC<Props> = ({
<button
type="button"
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" />
</button>

View File

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