🏡 index : ~doyle/chartered.git

import { useState } from "react";
import {Link, Redirect, useParams} from "react-router-dom";

import Nav from "../../sections/Nav";
import { useAuth } from "../../useAuth";
import {
  useAuthenticatedRequest,
  authenticatedEndpoint,
  RoundedPicture,
} from "../../util";

import { BoxSeam } from "react-bootstrap-icons";
import ErrorPage from "../ErrorPage";
import Members from "./Members";
import ReactPlaceholder from "react-placeholder";

interface OrganisationDetails {
  possible_permissions?: string[];
  crates: Crate[];
  members: Member[];
  description: string;
}

interface Crate {
  name: string;
  description?: string;
}

interface Member {
  uuid: string;
  display_name: string;
  permissions: string[];
}

interface UrlParams {
  organisation: string;
}

export default function ShowOrganisation() {
  const tabs = {
    crates: "Crates",
    members: "Members",
  };

  const { organisation } = useParams<UrlParams>();
  const auth = useAuth();
  const [activeTab, setActiveTab] = useState(Object.keys(tabs)[0]);

  if (!auth) {
    return <Redirect to="/login" />;
  }

  const [reload, setReload] = useState(0);
  const { response: organisationDetails, error } =
    useAuthenticatedRequest<OrganisationDetails>(
      {
        auth,
        endpoint: `organisations/${organisation}`,
      },
      [reload]
    );

  if (error) {
    return <ErrorPage message={error} />;
  }

  const ready = !!organisationDetails;

  return (
    <div className="text-white">
      <Nav />

      <div className="container mt-4 pb-4">
        <div className="row align-items-stretch">
          <div className="col-12 mb-3">
            <div className="card border-0 shadow-sm text-black h-100">
              <div className="card-body">
                <div className="d-flex flex-row align-items-center">
                  <RoundedPicture
                    src="http://placekitten.com/96/96"
                    height="96px"
                    width="96px"
                  />

                  <div className="px-2">
                    <h1 className="text-primary my-0">{organisation}</h1>
                    <ReactPlaceholder
                      showLoadingAnimation
                      type="text"
                      rows={1}
                      ready={ready}
                      style={{ height: "1.4rem" }}
                    >
                      <p className="m-0">
                        {organisationDetails?.description || (
                          <i>No description given.</i>
                        )}
                      </p>
                    </ReactPlaceholder>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <div className="col-12 mb-3">
            <div className="card border-0 shadow-sm text-black h-100">
              <div className="card-header">
                <ul className="nav nav-pills card-header-pills">
                  {Object.entries(tabs).map(([key, name]) => (
                    <li key={key} className="nav-item">
                      <a
                        href="#"
                        className={`nav-link ${
                          activeTab == key
                            ? "bg-primary bg-gradient active"
                            : ""
                        }`}
                        onClick={(e) => {
                          e.preventDefault();
                          setActiveTab(key);
                        }}
                      >
                        {name}
                      </a>
                    </li>
                  ))}
                </ul>
              </div>

              {!ready ? (
                <div className="card-body">
                  <div className="d-flex justify-content-center align-items-center">
                    <div className="spinner-border text-primary" role="status">
                      <span className="visually-hidden">Loading...</span>
                    </div>
                  </div>
                </div>
              ) : (
                <>
                  {activeTab == "crates" ? (
                    <ListCrates
                      organisation={organisation}
                      crates={organisationDetails.crates}
                    />
                  ) : (
                    <></>
                  )}
                  {activeTab == "members" ? (
                    <ListMembers
                      organisation={organisation}
                      members={organisationDetails.members}
                      possiblePermissions={
                        organisationDetails.possible_permissions
                      }
                      reload={() => setReload(reload + 1)}
                    />
                  ) : (
                    <></>
                  )}
                </>
              )}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function ListCrates({
  organisation,
  crates,
}: {
  organisation: string;
  crates: Crate[];
}) {
  if (crates.length === 0) {
    return (
      <div className="card-body">
        This organisation doesn't have any crates yet.
      </div>
    );
  }

  return (
    <div className="table-responsive w-100">
      <table className="table table-striped">
        <tbody>
          {crates.map((v, i) => (
            <tr key={i}>
              <td className="align-middle fit">
                <div
                  className="text-white circle bg-primary bg-gradient d-inline rounded-circle d-inline-flex justify-content-center align-items-center"
                  style={{ width: "48px", height: "48px" }}
                >
                  <BoxSeam />
                </div>
              </td>

              <td className="align-middle">
                <div>
                  <Link to={`/crates/${organisation}/${v.name}`}>
                    <span className="text-secondary">{organisation}/</span>
                    {v.name}
                  </Link>
                </div>
                <div className="text-muted">{v.description}</div>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

interface ListMemberParams {
  organisation: string;
  members: Member[];
  possiblePermissions?: string[];
  reload: () => any;
}

function ListMembers({
  organisation,
  members,
  possiblePermissions,
  reload,
}: ListMemberParams) {
  const auth = useAuth();

  if (!auth) {
    return <></>;
  }

  const saveMemberPermissions = async (
    prospectiveMember: boolean,
    uuid: string,
    selectedPermissions: string[]
  ) => {
    let res = await fetch(
      authenticatedEndpoint(auth, `organisations/${organisation}/members`),
      {
        method: prospectiveMember ? "PUT" : "PATCH",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          user_uuid: uuid,
          permissions: selectedPermissions,
        }),
      }
    );
    let json = await res.json();

    if (json.error) {
      throw new Error(json.error);
    }

    reload();
  };

  const deleteMember = async (uuid: string) => {
    let res = await fetch(
      authenticatedEndpoint(auth, `organisations/${organisation}/members`),
      {
        method: "DELETE",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          user_uuid: uuid,
        }),
      }
    );
    let json = await res.json();

    if (json.error) {
      throw new Error(json.error);
    }

    reload();
  };

  return (
    <Members
      members={members}
      possiblePermissions={possiblePermissions}
      saveMemberPermissions={saveMemberPermissions}
      deleteMember={deleteMember}
    />
  );
}