import React, { useEffect, useState, useRef } from "react"; import { ArrowLeftIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline"; import ErrorAlert from "@components/ui/alerts/Error"; import { UsernameInput } from "@components/ui/UsernameInput"; import MemberList from "./MemberList"; import Avatar from "@components/ui/Avatar"; import ImageUploader from "@components/ImageUploader"; import { Dialog, DialogTrigger } from "@components/Dialog"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import Modal from "@components/Modal"; import MemberInvitationModal from "@components/team/MemberInvitationModal"; import Button from "@components/ui/Button"; import { Member } from "@lib/member"; import { Team } from "@lib/team"; 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 className=""> <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-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-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> ); }