🏡 index : ~doyle/chartered.git

import React = require("react");
import { useState, useEffect, useRef } from "react";
import { useLocation } from "react-router-dom";

import { useAuth } from "../useAuth";
import { useUnauthenticatedRequest } from "../util";

interface OAuthProviders {
  providers: string[];
}

export default function Login() {
  const location = useLocation();
  const auth = useAuth();

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const [loading, setLoading] = useState<string | null>(null);
  const isMountedRef = useRef(null);

  const { response: oauthProviders } =
    useUnauthenticatedRequest<OAuthProviders>({
      endpoint: "login/oauth/providers",
    });

  useEffect(() => {
    if (location.state?.error) {
      setError(location.state.error);
    }

    isMountedRef.current = true;
    return () => (isMountedRef.current = false);
  });

  const handleSubmit = async (evt) => {
    evt.preventDefault();

    setError("");
    setLoading("password");

    try {
      await auth.login(username, password);
    } catch (e) {
      setError(e.message);
    } finally {
      if (isMountedRef.current) {
        setLoading(null);
      }
    }
  };

  const handleOAuthLogin = async (provider) => {
    setError("");
    setLoading(provider);

    try {
      await auth.oauthLogin(provider);
    } catch (e) {
      setError(e.message);
    }
  };

  return (
    <div className="bg-primary p-4 text-white min-vh-100 d-flex justify-content-center align-items-center">
      <div>
        <h1>chartered ✈️</h1>
        <h6>a private, authenticated cargo registry</h6>

        <div
          className="card border-0 shadow-sm text-black p-2"
          style={{ width: "40rem" }}
        >
          <div className="card-body">
            <div
              className="alert alert-danger alert-dismissible"
              role="alert"
              style={{ display: error ? "block" : "none" }}
            >
              {error}

              <button
                type="button"
                className="btn-close"
                aria-label="Close"
                onClick={() => setError("")}
              ></button>
            </div>

            <form onSubmit={handleSubmit}>
              <div className="form-floating">
                <input
                  type="text"
                  className="form-control"
                  placeholder="john.smith"
                  id="username"
                  disabled={!!loading}
                  value={username}
                  onChange={(e) => setUsername(e.target.value)}
                />

                <label htmlFor="email" className="form-label">
                  Username
                </label>
              </div>

              <div className="form-floating mt-2">
                <input
                  type="password"
                  className="form-control"
                  placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;"
                  id="password"
                  disabled={!!loading}
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                />

                <label htmlFor="password" className="form-label">
                  Password
                </label>
              </div>

              <ButtonOrSpinner
                type="submit"
                variant="primary"
                disabled={!!loading}
                showSpinner={loading === "password"}
                text={`Login`}
                onClick={handleSubmit}
              />
            </form>

            {oauthProviders?.providers.length > 0 ? (
              <>
                <div className="side-lines mt-3">or</div>

                {oauthProviders.providers.map((v, i) => (
                  <ButtonOrSpinner
                    key={i}
                    type="button"
                    variant="dark"
                    disabled={!!loading}
                    showSpinner={loading === v}
                    text={`Login with ${v}`}
                    onClick={(evt) => {
                      evt.preventDefault();
                      handleOAuthLogin(v);
                    }}
                  />
                ))}
              </>
            ) : (
              <></>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

function ButtonOrSpinner({
  type,
  variant,
  disabled,
  showSpinner,
  text,
  onClick,
}: {
  type: "button" | "submit";
  variant: string;
  disabled: boolean;
  showSpinner: boolean;
  text: string;
  onClick: (evt) => any;
}) {
  if (showSpinner) {
    return (
      <div
        className="spinner-border text-primary mt-3 m-auto d-block"
        role="status"
      >
        <span className="visually-hidden">Logging in...</span>
      </div>
    );
  }

  if (type) {
    return (
      <button
        type={type}
        disabled={disabled}
        onClick={onClick}
        className={`btn btn-lg mt-2 btn-${variant} w-100`}
      >
        {text}
      </button>
    );
  }
}