302 lines
12 KiB
TypeScript
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'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'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>
|
|
);
|
|
}
|