Vectrum
Sign In
126 lines · 5.1 KB · TypeScript
"use client";

import { useActionState, useState } from "react";
import { Checkbox, Fieldset, FormField, Select } from "@unlocalhosted/metalui";
import { createTokenAction } from "@/app/actions/settings";
import { PlusIcon } from "@/components/icons";
import { ChoiceCards, CopyIconButton, TextField } from "@/components/settings-fields";
import { PlateBody, PlateFooter } from "@/components/settings-plate";
import { Alert, Button } from "@/components/ui";

type RepoOption = { id: string; fullName: string; writable: boolean };

const expiries = [
  { value: "7", label: "7 days" },
  { value: "30", label: "30 days" },
  { value: "90", label: "90 days" },
  { value: "365", label: "1 year" },
  { value: "never", label: "No expiration" },
];

export function NewTokenForm({ repos, origin, username }: { repos: RepoOption[]; origin: string; username: string }) {
  const [state, action, pending] = useActionState(createTokenAction, undefined);
  const [scope, setScope] = useState<"all" | "selected">("all");
  const [permission, setPermission] = useState<"read" | "write">("write");
  const example = repos[0]?.fullName ?? `${username}/repo`;

  if (state?.token) {
    return (
      <>
        <PlateBody className="max-w-2xl">
          <Alert kind="success">Token created. Copy it now — you won’t be able to see it again.</Alert>
          <div className="flex items-center gap-2 rounded-row recipe-well-field py-1.5 pr-1.5 pl-3.5">
            <code className="flex-1 font-mono text-[12.5px] leading-5 break-all text-ink">{state.token}</code>
            <CopyIconButton text={state.token} label="Copy token" toast="Copied token" />
          </div>
          <div className="flex flex-col gap-1.5">
            <p className="type-title text-ink">Use it with git</p>
            <p className="type-body text-ink2">When git asks for a password, paste the token. Or put it in the remote URL:</p>
            <pre className="well mt-1 overflow-x-auto rounded-row px-3.5 py-3 font-mono text-[12px] leading-5 text-ink">
              git clone {origin.replace("://", `://${username}:${state.token}@`)}/{example}.git
            </pre>
          </div>
        </PlateBody>
        <PlateFooter>
          <Button type="button" onClick={() => window.location.reload()}>
            Done
          </Button>
        </PlateFooter>
      </>
    );
  }

  return (
    <form action={action}>
      <PlateBody className="max-w-2xl gap-6">
        <TextField
          label="Token name"
          description="What’s this token for? e.g. “CI deploy” or “Laptop”."
          name="name"
          required
          maxLength={100}
        />

        <FormField>
          <FormField.Label>Expiration</FormField.Label>
          <Select name="expiry" defaultValue="30" options={expiries} aria-label="Expiration" className="w-44" />
        </FormField>

        <Fieldset>
          <Fieldset.Legend>Permissions</Fieldset.Legend>
          <ChoiceCards
            name="permission"
            aria-label="Permissions"
            value={permission}
            onValueChange={setPermission}
            choices={[
              { value: "write", title: "Read & write", description: "Clone, fetch and push." },
              { value: "read", title: "Read-only", description: "Clone and fetch only." },
            ]}
          />
        </Fieldset>

        <Fieldset>
          <Fieldset.Legend>Repository access</Fieldset.Legend>
          <ChoiceCards
            name="scope"
            aria-label="Repository access"
            value={scope}
            onValueChange={setScope}
            choices={[
              { value: "all", title: "All repositories", description: "Including ones you get access to later." },
              { value: "selected", title: "Only select repositories" },
            ]}
          />
          {scope === "selected" && (
            <div className="max-h-64 overflow-y-auto rounded-plate recipe-well-field p-1" role="group" aria-label="Repositories">
              {repos.length === 0 && <p className="px-3 py-3 type-body text-ink3">You don’t have any repositories yet.</p>}
              {repos.map((r) => {
                const disabled = permission === "write" && !r.writable;
                return (
                  <label
                    key={r.id}
                    className={`flex min-h-8 items-center gap-2.5 rounded-row px-3 py-1.5 ${
                      disabled ? "cursor-default opacity-50" : "cursor-pointer hover:recipe-row-list-hover"
                    }`}
                  >
                    <Checkbox size="row" name="repos" value={String(r.id)} disabled={disabled} />
                    <span className="font-mono text-[12px] text-ink">{r.fullName}</span>
                    {disabled && <span className="ml-auto type-label engraved">No push access</span>}
                  </label>
                );
              })}
            </div>
          )}
        </Fieldset>

        {state?.error && <Alert>{state.error}</Alert>}
      </PlateBody>
      <PlateFooter>
        <Button variant="primary" icon={<PlusIcon />} disabled={pending}>
          {pending ? "Generating…" : "Generate Token"}
        </Button>
      </PlateFooter>
    </form>
  );
}