Skip to content

Commit

Permalink
Add team listing and editing
Browse files Browse the repository at this point in the history
  • Loading branch information
th0th committed Jan 20, 2025
1 parent 9049727 commit 1350256
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 61 deletions.
12 changes: 12 additions & 0 deletions frontend/src/components/Portal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ReactNode } from "react";
import { createPortal } from "react-dom";

export type PortalProps = {
children: ReactNode;
};

export default function Portal({ children }: PortalProps) {
const rootElement = document.getElementById("root");

return rootElement !== null ? createPortal(children, rootElement) : null;
}
46 changes: 46 additions & 0 deletions frontend/src/components/Result/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { IconCircleCheck, TablerIcon } from "@tabler/icons-react";
import classNames from "classnames";
import { JSX, PropsWithoutRef, ReactNode } from "react";
import { Link, LinkProps } from "wouter";

export type ResultProps = Overwrite<Omit<PropsWithoutRef<JSX.IntrinsicElements["div"]>, "children">, {
description: ReactNode;
icon?: TablerIcon;
title: ReactNode;
to: Exclude<LinkProps["to"], undefined>;
toTitle: string;
variant?: ResultVariant;
}>;

type ResultVariant = "success";

const iconClassNames: Record<ResultVariant, string> = {
success: "text-success",
};

export default function Result(
{
className,
description,
icon: Icon = IconCircleCheck,
title,
to,
toTitle,
variant = "success",
...props
}: ResultProps,
) {
return (
<div {...props} className={classNames("align-items-center d-flex flex-column", className)}>
<Icon className={classNames(iconClassNames[variant])} size="6rem" />

<div className="mt-6 mw-32rem text-center">
<h2 className="fs-2">{title}</h2>

<div className="fs-5_5 text-body-emphasis">{description}</div>

<Link className="btn btn-primary mt-8" to={to}>{toTitle}</Link>
</div>
</div>
);
}
158 changes: 158 additions & 0 deletions frontend/src/components/Team/DeleteModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { useEffect, useMemo, useState } from "react";
import { Modal } from "react-bootstrap";
import { useErrorBoundary } from "react-error-boundary";
import Portal from "~/components/Portal";
import useUsers from "~/hooks/api/useUsers";
import useSearchParams from "~/hooks/useSearchParams";
import { api } from "~/lib/api";

type State = {
isInProgress: boolean;
isShown: boolean;
user: HydratedUser | null;
};

export default function DeleteModal() {
const { showBoundary } = useErrorBoundary();
const { navigateWithSearchParams, searchParams } = useSearchParams();
const [state, setState] = useState<State>({ isInProgress: false, isShown: false, user: null });
const isEnabled = useMemo(() => searchParams.get("action") === "delete", [searchParams]);
const userID = useMemo(() => Number(searchParams.get("userID")) || undefined, [searchParams]);
const { mutate } = useUsers();

async function confirmDeletion() {
try {
setState((s) => ({ ...s, isInProgress: true }));
const response = await api.delete(`/users/${userID}`);

if (response.ok) {
await mutate();

hide();
}
} catch (e) {
showBoundary(e);
} finally {
setState((s) => ({ ...s, isInProgress: false }));
}
}

function handleExited() {
navigateWithSearchParams({ action: null, userID: null });
}

function hide() {
setState((s) => ({ ...s, isShown: false }));
}

useEffect(() => {
if (isEnabled && userID !== undefined) {
setState((s) => ({ ...s, isShown: true }));
}
}, [isEnabled, userID]);

useEffect(() => {
async function getUser() {
try {
if (userID === null) {
return;
}

const response = await api.get(`/users/${userID}`);

if (response.ok) {
const user = await response.json();
setState((s) => ({ ...s, user }));
}
} catch (e) {
showBoundary(e);
}
}

getUser().catch((e) => {
showBoundary(e);
});
}, [showBoundary, userID]);

return (
<Portal>
<Modal centered onExited={handleExited} onHide={hide} show={state.isShown}>
<Modal.Header closeButton>
<Modal.Title>Delete user</Modal.Title>
</Modal.Header>

<Modal.Body>
Are you sure you want to delete team member
{" "}
<span className="fw-semi-bold">{state.user?.name}</span>
?
</Modal.Body>

<Modal.Footer as="fieldset" disabled={state.isInProgress}>
<button className="btn btn-secondary" onClick={hide} type="button">Cancel</button>

<button className="align-items-center btn btn-danger d-flex gap-4" onClick={confirmDeletion} type="button">
{state.isInProgress ? (
<span className="spinner-border spinner-border-sm" />
) : null}

Delete
</button>
</Modal.Footer>
</Modal>
</Portal>
);

// const { data: user } = useUser(userID || undefined);
// const { mutate: mutateUsers } = useUsers();
//
// async function confirmDeletion() {
// try {
// const response = await api.delete(`/users/${userID}`);
//
// if (response.ok) {
// await mutateUsers();
//
// hide();
// }
// } catch (e) {
// showBoundary(e);
// }
// }
//
// function hide() {
// navigateWithSearchParams({ action: null, userID: null });
// }
//
// return (
// <Portal>
// <Modal centered onHide={hide} show={user !== undefined}>
// <Modal.Header closeButton>
// <Modal.Title>Delete user</Modal.Title>
// </Modal.Header>
//
// <Modal.Body>
// Are you sure you want to delete team member
// {" "}
// <span className="fw-semi-bold">{user?.name}</span>
// ?
// </Modal.Body>
//
// <Modal.Footer>
// <button className="btn btn-secondary" onClick={hide} type="button">Cancel</button>
//
// <button className="align-items-center btn btn-danger d-flex gap-4" onClick={confirmDeletion} type="button">
// {/*{state.isInProgress ? (*/}
// {/* <>*/}
// {/* <span className="spinner-border spinner-border-sm" />*/}
// {/* {" "}*/}
// {/* </>*/}
// {/*) : null}*/}
//
// Delete
// </button>
// </Modal.Footer>
// </Modal>
// </Portal>
// );
}
4 changes: 2 additions & 2 deletions frontend/src/components/Team/User/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function User({ user, ...props }: UserProps) {
<div className="d-flex flex-column flex-sm-row gap-4">
<Link className="btn btn-primary btn-sm" to={`/team/edit?userID=${user.id}`}>Edit</Link>

<Link className="btn btn-danger btn-sm" to={`/team/${setSearchParams({ action: "delete", userID: user.id.toString() })}`}>
<Link className="btn btn-danger btn-sm" to={`/team${setSearchParams({ action: "delete", userID: user.id.toString() })}`}>
Delete
</Link>

Expand All @@ -40,7 +40,7 @@ export default function User({ user, ...props }: UserProps) {

<Link
className="btn btn-outline-secondary btn-sm"
to={`/team/${setSearchParams({ action: "transferOwnership", userID: user.id.toString() })}`}
to={`/team${setSearchParams({ action: "transferOwnership", userID: user.id.toString() })}`}
>
Transfer ownership
</Link>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/Team/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Title from "~/components/Title";
import withAuthorization from "~/components/withAuthorization";
import useUsers from "~/hooks/api/useUsers";
import useListFilters from "~/hooks/useListFilters";
import DeleteModal from "./DeleteModal";

function Team() {
const { data: users } = useUsers();
Expand Down Expand Up @@ -49,6 +50,8 @@ function Team() {
</div>
</div>
</div>

<DeleteModal />
</>
);
}
Expand Down
Loading

0 comments on commit 1350256

Please sign in to comment.