Skip to content

Commit

Permalink
feat(team): implement editing team name, kicking team member and leav…
Browse files Browse the repository at this point in the history
…ing team
  • Loading branch information
Matej Tarca authored and matejtarca committed Oct 28, 2023
1 parent fd13d75 commit 3109cf0
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 43 deletions.
2 changes: 1 addition & 1 deletion src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const buttonVariants = cva(
secondary:
"bg-slate-100 text-slate-900 hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80",
ghost:
"hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50",
"text-hkOrange hover:bg-slate-100 dark:hover:bg-slate-800 dark:hover:text-slate-50",
link: "text-slate-900 underline-offset-4 hover:underline dark:text-slate-50",
combobox:
"w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-hkOrange disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus:ring-slate-300",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,24 @@ import {
import { Input } from "@/components/ui/input";
import createTeam from "@/server/actions/team/createTeam";
import { PlusIcon } from "@heroicons/react/24/outline";
import editTeamName from "@/server/actions/team/editTeamName";
import { PencilIcon } from "@heroicons/react/24/solid";

const newTeamFormSchema = z.object({
name: z.string().min(1),
});

type NewTeamForm = z.infer<typeof newTeamFormSchema>;

const NewTeamDialog = () => {
type NewTeamDialogProps = {
mode?: "create" | "edit";
initialData?: NewTeamForm;
};

const NewTeamDialog = ({
mode = "create",
initialData,
}: NewTeamDialogProps) => {
const [isOpened, setIsOpened] = useState(false);
const form = useForm<NewTeamForm>({
resolver: zodResolver(newTeamFormSchema),
Expand All @@ -41,25 +51,43 @@ const NewTeamDialog = () => {
});

const onNewTeamCreation = async (data: NewTeamForm) => {
await createTeam(data);
if (mode === "edit") {
await editTeamName({ newName: data.name });
} else if (mode === "create") {
await createTeam(data);
}

setIsOpened(false);
};

useEffect(() => {
if (isOpened) form.reset();
}, [form, isOpened]);
if (mode === "edit" && initialData && isOpened) {
form.reset(initialData);
} else if (mode === "create" && isOpened) {
form.reset();
}
}, [form, initialData, isOpened, mode]);

return (
<Dialog onOpenChange={setIsOpened} open={isOpened}>
<DialogTrigger asChild>
<Button>
<PlusIcon className="w-4 h-4 mr-1 text-white inline" />
Create new team
</Button>
{mode === "create" ? (
<Button>
<PlusIcon className="w-4 h-4 mr-1 text-white inline" />
Create new team
</Button>
) : (
<Button variant="ghost" size="small">
<PencilIcon className="w-4 h-4 mr-1 inline" />
Edit team name
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create new team</DialogTitle>
<DialogTitle>
{mode === "create" ? "Create new team" : "Edit team name"}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onNewTeamCreation)}>
Expand All @@ -81,7 +109,9 @@ const NewTeamDialog = () => {
)}
/>
<DialogFooter className="mt-5">
<Button type="submit">Create</Button>
<Button type="submit">
{mode === "create" ? "Create" : "Save"}
</Button>
</DialogFooter>
</form>
</Form>
Expand Down
118 changes: 86 additions & 32 deletions src/scenes/Application/components/TeamManager/components/TeamInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
"use client";

import React, { useMemo } from "react";
import React, { useMemo, useState } from "react";
import { TeamData, TeamMemberData } from "@/server/getters/team";
import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import { Stack } from "@/components/ui/stack";
import { DocumentDuplicateIcon } from "@heroicons/react/24/outline";
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@/components/ui/data-table";
import NewTeamDialog from "@/scenes/Application/components/TeamManager/components/NewTeamDialog";
import ConfirmationDialog from "@/components/common/ConfirmationDialog";
import kickTeamMember from "@/server/actions/team/kickTeamMember";
import { ArrowLeftOnRectangleIcon } from "@heroicons/react/24/solid";
import leaveTeam from "@/server/actions/team/leaveTeam";

type TeamInfoProps = {
team: TeamData;
isOwnerSession: boolean;
};

const ActionsCell = ({ member }: { member: TeamMemberData }) => {
const [isKickConfirmationDialogOpened, setIsKickConfirmationDialogOpened] =
useState(false);

if (member.isCurrentUser) {
return null;
}

return (
<>
<ConfirmationDialog
question={`Are you sure you want to kick ${member.email}?`}
onAnswer={async (answer) => {
if (answer) {
await kickTeamMember({ memberId: member.id });
}
setIsKickConfirmationDialogOpened(false);
}}
isManuallyOpened={isKickConfirmationDialogOpened}
/>
<Button
onClick={() => {
setIsKickConfirmationDialogOpened(true);
}}
variant="ghost"
size="small"
>
Kick
</Button>
</>
);
};

const getTeamMembersColumns = (
isOwnerSession: boolean
): ColumnDef<TeamMemberData>[] => {
Expand All @@ -33,50 +71,66 @@ const getTeamMembersColumns = (
columns.push({
header: "Actions",
cell: ({ row }) => {
return (
<Stack direction="row" spacing="none">
<Button
onClick={() => {
console.log(`KICK member ${row.original.email}`);
}}
variant="ghost"
size="icon"
>
Kick
</Button>
</Stack>
);
return <ActionsCell member={row.original} />;
},
});
}

return columns;
};
const TeamInfo = ({ team, isOwnerSession }: TeamInfoProps) => {
const TeamInfo = ({
team: { name, code, members },
isOwnerSession,
}: TeamInfoProps) => {
const teamMembersColumns = useMemo(
() => getTeamMembersColumns(isOwnerSession),
[isOwnerSession]
);
const onLeaveTeamClick = async () => {
await leaveTeam();
};
return (
<>
<Text>
Team name: <span className="font-bold">{team.name}</span>
</Text>
<Stack direction="row" alignItems="center" spacing="none">
<Text>Team&apos;s code: {team.code}</Text>
<Button
onClick={() => {
navigator.clipboard.writeText(team.code);
<Stack direction="column" spacing="medium">
<div>
<Stack direction="row" alignItems="center">
<Text>
Team name: <span className="font-bold">{name}</span>
</Text>
{isOwnerSession && (
<NewTeamDialog mode="edit" initialData={{ name }} />
)}
</Stack>
<Stack direction="row" alignItems="center" spacing="none">
<Text>Team&apos;s code: {code}</Text>
<Button
onClick={() => {
navigator.clipboard.writeText(code);
}}
variant="ghost"
size="icon"
>
<DocumentDuplicateIcon className="w-4 h-4 mr-1 inline" />
</Button>
</Stack>
<Text>Team members ({members.length}/4):</Text>
<DataTable columns={teamMembersColumns} data={members} />
</div>
{!isOwnerSession && (
<ConfirmationDialog
question="Are you sure you want to leave this team?"
onAnswer={async (answer) => {
if (answer) {
await onLeaveTeamClick();
}
}}
variant="ghost"
size="icon"
>
<DocumentDuplicateIcon className="w-4 h-4 mr-1 inline" />
</Button>
</Stack>
<Text>Team members:</Text>
<DataTable columns={teamMembersColumns} data={team.members} />
</>
<Button variant="outline" className="text-red-500">
<ArrowLeftOnRectangleIcon className="h-4 w-4 mr-1" />
Leave team
</Button>
</ConfirmationDialog>
)}
</Stack>
);
};

Expand Down
25 changes: 25 additions & 0 deletions src/server/actions/team/editTeamName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use server";

import { prisma } from "@/services/prisma";
import { revalidatePath } from "next/cache";
import requireTeamOwnerSession from "@/server/services/helpers/requireTeamOwnerSession";

type EditTeamNameInput = {
newName: string;
};
const editTeamName = async ({ newName }: EditTeamNameInput) => {
const { team } = await requireTeamOwnerSession();

await prisma.team.update({
where: {
id: team.id,
},
data: {
name: newName,
},
});

revalidatePath("/application", "page");
};

export default editTeamName;
29 changes: 29 additions & 0 deletions src/server/actions/team/kickTeamMember.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use server";

import { prisma } from "@/services/prisma";
import { revalidatePath } from "next/cache";
import requireTeamOwnerSession from "@/server/services/helpers/requireTeamOwnerSession";

type KickTeamMemberInput = {
memberId: number;
};
const kickTeamMember = async ({ memberId }: KickTeamMemberInput) => {
const { hacker } = await requireTeamOwnerSession();

if (memberId === hacker.id) {
throw new Error("You cannot kick yourself");
}

await prisma.hacker.update({
where: {
id: memberId,
},
data: {
teamId: null,
},
});

revalidatePath("/application", "page");
};

export default kickTeamMember;
46 changes: 46 additions & 0 deletions src/server/actions/team/leaveTeam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use server";

import { prisma } from "@/services/prisma";
import requireHackerSession from "@/server/services/helpers/requireHackerSession";
import { revalidatePath } from "next/cache";

const leaveTeam = async () => {
const hacker = await requireHackerSession();

if (!hacker.teamId) {
throw new Error("Hacker is not in a team");
}

const teamId = hacker.teamId;

const team = await prisma.team.findUnique({
where: {
id: teamId,
},
select: {
id: true,
ownerId: true,
},
});

if (!team) {
throw new Error("Team not found");
}

if (team.ownerId === hacker.id) {
throw new Error("Owner cannot leave team");
}

await prisma.hacker.update({
where: {
id: hacker.id,
},
data: {
teamId: null,
},
});

revalidatePath("/application", "page");
};

export default leaveTeam;
2 changes: 2 additions & 0 deletions src/server/getters/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type TeamMemberData = {
id: number;
email: string;
isOwner: boolean;
isCurrentUser: boolean;
};

export type TeamData = {
Expand Down Expand Up @@ -78,6 +79,7 @@ const getTeam = async (): Promise<GetTeamData> => {
id: member.id,
email: member.user.email,
isOwner: member.id === ownerId,
isCurrentUser: member.id === hacker.id,
})),
};

Expand Down
Loading

0 comments on commit 3109cf0

Please sign in to comment.