cal.pub0.org/components/team/EditTeam.tsx

302 lines
12 KiB
TypeScript

import { ArrowLeftIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline";
import React, { useEffect, useRef, useState } from "react";
import { Member } from "@lib/member";
import { Team } from "@lib/team";
import { Dialog, DialogTrigger } from "@components/Dialog";
import ImageUploader from "@components/ImageUploader";
import Modal from "@components/Modal";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import MemberInvitationModal from "@components/team/MemberInvitationModal";
import Avatar from "@components/ui/Avatar";
import Button from "@components/ui/Button";
import { UsernameInput } from "@components/ui/UsernameInput";
import ErrorAlert from "@components/ui/alerts/Error";
import MemberList from "./MemberList";
export default function EditTeam(props: { team: Team | undefined | null; onCloseEdit: () => void }) {
const [members, setMembers] = useState([]);
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const teamUrlRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const descriptionRef = useRef<HTMLTextAreaElement>() as React.MutableRefObject<HTMLTextAreaElement>;
const hideBrandingRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const logoRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const [hasErrors, setHasErrors] = useState(false);
const [successModalOpen, setSuccessModalOpen] = useState(false);
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false);
const [inviteModalTeam, setInviteModalTeam] = useState<Team | null | undefined>();
const [errorMessage, setErrorMessage] = useState("");
const [imageSrc, setImageSrc] = useState<string>("");
const loadMembers = () =>
fetch("/api/teams/" + props.team?.id + "/membership")
.then((res) => res.json())
.then((data) => setMembers(data.members));
useEffect(() => {
loadMembers();
}, []);
const deleteTeam = () => {
return fetch("/api/teams/" + props.team?.id, {
method: "DELETE",
}).then(props.onCloseEdit());
};
const onRemoveMember = (member: Member) => {
return fetch("/api/teams/" + props.team?.id + "/membership", {
method: "DELETE",
body: JSON.stringify({ userId: member.id }),
headers: {
"Content-Type": "application/json",
},
}).then(loadMembers);
};
const onInviteMember = (team: Team | null | undefined) => {
setShowMemberInvitationModal(true);
setInviteModalTeam(team);
};
const handleError = async (resp: Response) => {
if (!resp.ok) {
const error = await resp.json();
throw new Error(error.message);
}
};
async function updateTeamHandler(event) {
event.preventDefault();
const enteredUsername = teamUrlRef?.current?.value.toLowerCase();
const enteredName = nameRef?.current?.value;
const enteredDescription = descriptionRef?.current?.value;
const enteredLogo = logoRef?.current?.value;
const enteredHideBranding = hideBrandingRef?.current?.checked;
// TODO: Add validation
await fetch("/api/teams/" + props.team?.id + "/profile", {
method: "PATCH",
body: JSON.stringify({
username: enteredUsername,
name: enteredName,
description: enteredDescription,
logo: enteredLogo,
hideBranding: enteredHideBranding,
}),
headers: {
"Content-Type": "application/json",
},
})
.then(handleError)
.then(() => {
setSuccessModalOpen(true);
setHasErrors(false); // dismiss any open errors
})
.catch((err) => {
setHasErrors(true);
setErrorMessage(err.message);
});
}
const onMemberInvitationModalExit = () => {
loadMembers();
setShowMemberInvitationModal(false);
};
const closeSuccessModal = () => {
setSuccessModalOpen(false);
};
const handleLogoChange = (newLogo: string) => {
logoRef.current.value = newLogo;
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement?.prototype, "value").set;
nativeInputValueSetter?.call(logoRef.current, newLogo);
const ev2 = new Event("input", { bubbles: true });
logoRef?.current?.dispatchEvent(ev2);
updateTeamHandler(ev2);
setImageSrc(newLogo);
};
return (
<div className="divide-y divide-gray-200 lg:col-span-9">
<div className="py-6 lg:pb-8">
<div className="mb-4">
<Button
type="button"
color="secondary"
size="sm"
StartIcon={ArrowLeftIcon}
onClick={() => props.onCloseEdit()}>
Back
</Button>
</div>
<div>
<div className="pb-5 pr-4 sm:pb-6">
<h3 className="text-lg font-bold leading-6 text-gray-900">{props.team?.name}</h3>
<div className="max-w-xl mt-2 text-sm text-gray-500">
<p>Manage your team</p>
</div>
</div>
</div>
<hr className="mt-2" />
<h3 className="font-cal font-bold leading-6 text-gray-900 mt-7 text-md">Profile</h3>
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateTeamHandler}>
{hasErrors && <ErrorAlert message={errorMessage} />}
<div className="py-6 lg:pb-8">
<div className="flex flex-col lg:flex-row">
<div className="flex-grow space-y-6">
<div className="block sm:flex">
<div className="w-full mb-6 sm:w-1/2 sm:mr-2">
<UsernameInput ref={teamUrlRef} defaultValue={props.team?.slug} label={"My team URL"} />
</div>
<div className="w-full sm:w-1/2 sm:ml-2">
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Team name
</label>
<input
ref={nameRef}
type="text"
name="name"
id="name"
placeholder="Your team name"
required
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
defaultValue={props.team?.name}
/>
</div>
</div>
<div>
<label htmlFor="about" className="block text-sm font-medium text-gray-700">
About
</label>
<div className="mt-1">
<textarea
ref={descriptionRef}
id="about"
name="about"
rows={3}
defaultValue={props.team?.bio}
className="block w-full mt-1 border-gray-300 rounded-sm shadow-sm focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"></textarea>
<p className="mt-2 text-sm text-gray-500">
A few sentences about your team. This will appear on your team&apos;s URL page.
</p>
</div>
</div>
<div>
<div className="flex mt-1">
<Avatar
className="relative w-10 h-10 rounded-full"
imageSrc={imageSrc ? imageSrc : props.team?.logo}
displayName="Logo"
/>
<input
ref={logoRef}
type="hidden"
name="avatar"
id="avatar"
placeholder="URL"
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
defaultValue={imageSrc ? imageSrc : props.team?.logo}
/>
<ImageUploader
target="logo"
id="logo-upload"
buttonMsg={imageSrc !== "" ? "Edit logo" : "Upload a logo"}
handleAvatarChange={handleLogoChange}
imageRef={imageSrc ? imageSrc : props.team?.logo}
/>
</div>
<hr className="mt-6" />
</div>
<div className="flex justify-between mt-7">
<h3 className="font-cal font-bold leading-6 text-gray-900 text-md">Members</h3>
<div className="relative flex items-center">
<Button
type="button"
color="secondary"
StartIcon={PlusIcon}
onClick={() => onInviteMember(props.team)}>
New Member
</Button>
</div>
</div>
<div>
{!!members.length && (
<MemberList members={members} onRemoveMember={onRemoveMember} onChange={loadMembers} />
)}
<hr className="mt-6" />
</div>
<div>
<div className="relative flex items-start">
<div className="flex items-center h-5">
<input
id="hide-branding"
name="hide-branding"
type="checkbox"
ref={hideBrandingRef}
defaultChecked={props.team?.hideBranding}
className="w-4 h-4 border-gray-300 rounded-sm focus:ring-neutral-500 text-neutral-900"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="hide-branding" className="font-medium text-gray-700">
Disable Cal.com branding
</label>
<p className="text-gray-500">Hide all Cal.com branding from your public pages.</p>
</div>
</div>
<hr className="mt-6" />
</div>
<h3 className="font-bold leading-6 text-gray-900 mt-7 text-md">Danger Zone</h3>
<div>
<div className="relative flex items-start">
<Dialog>
<DialogTrigger
onClick={(e) => {
e.stopPropagation();
}}
className="btn-sm btn-white">
<TrashIcon className="group-hover:text-red text-gray-700 w-3.5 h-3.5 mr-2 inline-block" />
Disband Team
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title="Disband Team"
confirmBtnText="Yes, disband team"
cancelBtnText="Cancel"
onConfirm={() => deleteTeam()}>
Are you sure you want to disband this team? Anyone who you&apos;ve shared this team
link with will no longer be able to book using it.
</ConfirmationDialogContent>
</Dialog>
</div>
</div>
</div>
</div>
<hr className="mt-8" />
<div className="flex justify-end py-4">
<Button type="submit" color="primary">
Save
</Button>
</div>
</div>
</form>
<Modal
heading="Team updated successfully"
description="Your team has been updated successfully."
open={successModalOpen}
handleClose={closeSuccessModal}
/>
{showMemberInvitationModal && (
<MemberInvitationModal team={inviteModalTeam} onExit={onMemberInvitationModalExit} />
)}
</div>
</div>
);
}