mAAdhaTTah/brookjs

View on GitHub
packages/brookjs-cli/src/commands/NewCommand/View.tsx

Summary

Maintainability
A
0 mins
Test Coverage
import React, { useEffect } from 'react';
import { Color, Box, Static, useStdin } from 'ink';
import { toJunction } from 'brookjs-silt';
import TextInput from 'ink-text-input';
import SelectInput from 'ink-select-input';
import Spinner from 'ink-spinner';
import { Observable } from 'kefir';
import { useExit } from '../../cli';
import {
  State,
  Configurable,
  unreachable,
  ConfiguredState,
  ConfiguringState,
  Log,
} from './types';

type Props = State & {
  onChange: (value: string) => void;
  onSubmit: () => void;
  onConfirm: (value: boolean) => void;
};

const useOnSubmit = (onSubmit: () => void) => {
  const { stdin, setRawMode } = useStdin();

  useEffect(() => {
    const onData = (data: Buffer) => {
      const s = data.toString();

      if (s === '\r') {
        onSubmit();
      }
    };

    setRawMode!(true);
    stdin.on('data', onData);

    return () => {
      // @TODO(mAAdhaTTah) This fails in node-pty – not sure why
      // stdin.off is not a function
      stdin.off?.('data', onData);
      setRawMode?.(false);
    };
  }, [stdin, setRawMode, onSubmit]);
};

type Question = {
  text: string;
  placeholder: string;
};

const questions: { [key in Configurable]: Question } = {
  version: {
    text: 'What is the application version?',
    placeholder: '0.0.0',
  },
  description: {
    text: 'What is the application description?',
    placeholder: 'A brookjs application',
  },
  dir: {
    text: 'Relative to the project, where will the source code live?',
    placeholder: 'src',
  },
  license: {
    text: 'Choose a license',
    placeholder: 'MIT',
  },
};

const ConfigureStep: React.FC<
  ConfiguringState & {
    onChange: (value: string) => void;
    onSubmit: () => void;
  }
> = ({ configuring, config, onChange, onSubmit }) => {
  useOnSubmit(onSubmit);

  return (
    <Box flexDirection="column">
      <Color green>App name: {config.name}</Color>
      <Box>
        {questions[configuring].text}:{' '}
        <TextInput
          value={config[configuring] ?? ''}
          placeholder={questions[configuring].placeholder}
          focus={true}
          onChange={onChange}
        />
      </Box>
    </Box>
  );
};

const ConfirmStep: React.FC<
  ConfiguredState & {
    onConfirm: (value: boolean) => void;
  }
> = ({ config: { name, version, description, dir, license }, onConfirm }) => (
  <Box flexDirection="column">
    <Box>Configuration:</Box>
    <Box>
      name: <Color green>{name}</Color>
    </Box>
    <Box>
      version: <Color green>{version}</Color>
    </Box>
    <Box>
      description: <Color green>{description}</Color>
    </Box>
    <Box>
      dir: <Color green>{dir}</Color>
    </Box>
    <Box>
      license: <Color green>{license}</Color>
    </Box>
    <Box>
      <SelectInput
        items={[
          {
            label: 'Yes',
            value: 1,
          },
          {
            label: 'No',
            value: 0,
          },
        ]}
        onSelect={({ value }) => onConfirm(!!value)}
      />
    </Box>
  </Box>
);

const CancelledStep: React.FC<{}> = () => {
  useExit();

  return <Box>Cancelled!</Box>;
};

const LogDisplay: React.FC<{ logs: Log[] }> = ({ logs }) => (
  <Box paddingBottom={2}>
    <Static>
      {logs.map((log, i) => (
        <Color key={i} green={log.level === 'ok'}>
          {log.msg}
        </Color>
      ))}
    </Static>
  </Box>
);

const CreatingStep: React.FC<{ name: string; logs: Log[] }> = ({
  name,
  logs,
}) => (
  <Box flexDirection="column">
    <LogDisplay logs={logs} />
    <Box flexDirection="row">
      <Color green>
        <Spinner type="arrow3" />
      </Color>
      <Box marginLeft={1}>Creating app {name}</Box>
    </Box>
  </Box>
);

const ErrorStep: React.FC<{ error: Error }> = ({ error }) => {
  useExit(error);

  return (
    <Box flexDirection="column" paddingTop={2}>
      <Box>
        <Color red>Error running new</Color>
      </Box>
      <Box>
        <Color red>Message:</Color> {error.message}
      </Box>
    </Box>
  );
};

const CompleteStep: React.FC<{ name: string }> = ({ name }) => {
  useExit();

  return (
    <Box flexDirection="column" paddingTop={2}>
      <Box>
        <Color green>New brookjs application created at {name}</Color>
      </Box>
      <Box>
        Run <Color blue>npm i</Color> or <Color blue>yarn</Color> to bootstrap
      </Box>
    </Box>
  );
};

const View: React.FC<Props> = props => {
  if (props.config.name == null) {
    return (
      <Color red>
        Called <Color yellow>beaver new</Color> with no name.
      </Color>
    );
  }

  switch (props.step) {
    case 'configure':
      return <ConfigureStep {...props} />;
    case 'confirm':
      return <ConfirmStep {...props} />;
    case 'cancelled':
      return <CancelledStep />;
    case 'creating':
      return <CreatingStep name={props.config.name} logs={props.logs} />;
    case 'error':
      return <ErrorStep error={props.error} />;
    case 'complete':
      return <CompleteStep name={props.config.name} />;
    default:
      return unreachable(props);
  }
};

const events = {
  onChange: (e$: Observable<string, never>) =>
    e$.map(value => ({ type: 'INPUT', payload: { value } })),
  onSubmit: (e$: Observable<void, never>) => e$.map(() => ({ type: 'SUBMIT' })),
  onConfirm: (e$: Observable<boolean, never>) =>
    e$.map(value => ({ type: 'CONFIRM', payload: { value } })),
};

export default toJunction(events)(View);