code:
import { classes } from 'common/react';
import { multiline } from 'common/string';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, ColorBox, LabeledList, Section, Stack, Tabs } from '../components';
import { Window } from '../layouts';

const ROOT_CATEGORIES = [
  'Atmospherics',
  'Disposals',
  'Transit Tubes',
];

const ICON_BY_CATEGORY_NAME = {
  'Atmospherics': 'wrench',
  'Disposals': 'trash-alt',
  'Transit Tubes': 'bus',
  'Pipes': 'grip-lines',
  'Disposal Pipes': 'grip-lines',
  'Devices': 'microchip',
  'Heat Exchange': 'thermometer-half',
  'Station Equipment': 'microchip',
};

const TOOLS = [
  {
    name: 'Dispense',
    bitmask: 1,
  },
  {
    name: 'Connect',
    bitmask: 2,
  },
  {
    name: 'Destroy',
    bitmask: 4,
  },
  {
    name: 'Reprogram',
    bitmask: 8,
  },
];

const SelectionSection = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    category: rootCategoryIndex,
    selected_color,
    mode,
  } = data;
  return (

      <LabeledList>
        <LabeledList.Item
          label="Category">
          {ROOT_CATEGORIES.map((categoryName, i) => (
            <Button
              key={categoryName}
              selected={rootCategoryIndex === i}
              icon={ICON_BY_CATEGORY_NAME[categoryName]}
              color="transparent"
              onClick={() => act('category', { category: i })} >
              {categoryName}
            </Button>
          ))}
        </LabeledList.Item>
        <LabeledList.Item label="Modes">
          <Stack fill>
            {TOOLS.map(tool => (
              <Stack.Item grow key={tool.bitmask}>
                <Button.Checkbox
                  checked={mode & tool.bitmask}
                  fluid
                  content={tool.name}
                  onClick={() => act('mode', {
                    mode: tool.bitmask,
                  })} />
              </Stack.Item>
            ))}
          </Stack>
        </LabeledList.Item>
        <LabeledList.Item
          label="Color">
          <Box
            inline
            width="64px"
            color={data.paint_colors[selected_color]}>
            {selected_color}
          </Box>
          {Object.keys(data.paint_colors)
            .map(colorName => (
              <ColorBox
                key={colorName}
                ml={1}
                color={data.paint_colors[colorName]}
                onClick={() => act('color', {
                  paint_color: colorName,
                })} />
            ))}
        </LabeledList.Item>
      </LabeledList>

  );
};

const LayerSection = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    category: rootCategoryIndex,
    piping_layer,
  } = data;
  const previews = data.preview_rows.flatMap(row => row.previews);
  return (

      {rootCategoryIndex === 0 && (
        <Stack vertical mb={1}>
          {[1, 2, 3, 4, 5].map(layer => (
            <Stack.Item my={0} key={layer}>
              <Button.Checkbox
                checked={layer === piping_layer}
                content={'Layer ' + layer}
                onClick={() => act('piping_layer', {
                  piping_layer: layer,
                })} />
            </Stack.Item>
          ))}
        </Stack>
      )}
      <Box width="120px">
        {previews.map(preview => (
          <Button
            ml={0}
            key={preview.dir}
            title={preview.dir_name}
            selected={preview.selected}
            style={{
              width: '40px',
              height: '40px',
              padding: 0,
            }}
            onClick={() => act('setdir', {
              dir: preview.dir,
              flipped: preview.flipped,
            })}>
            <Box
              className={classes([
                'pipes32x32',
                preview.dir + '-' + preview.icon_state,
              ])}
              style={{
                transform: 'scale(1.5) translate(9.5%, 9.5%)',
              }} />
          </Button>
        ))}
      </Box>

  );
};

const PipeTypeSection = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    categories = [],
  } = data;
  const [
    categoryName,
    setCategoryName,
  ] = useLocalState(context, 'categoryName');
  const shownCategory = categories
    .find(category => category.cat_name === categoryName)
    || categories[0];
  return (

      <Tabs>
        {categories.map((category, i) => (
          <Tabs.Tab
            fluid
            key={category.cat_name}
            icon={ICON_BY_CATEGORY_NAME[category.cat_name]}
            selected={category.cat_name === shownCategory.cat_name}
            onClick={() => setCategoryName(category.cat_name)}>
            {category.cat_name}
          </Tabs.Tab>
        ))}
      </Tabs>
      {shownCategory?.recipes.map(recipe => (
        <Button.Checkbox
          key={recipe.pipe_index}
          fluid
          ellipsis
          checked={recipe.selected}
          content={recipe.pipe_name}
          title={recipe.pipe_name}
          onClick={() => act('pipe_type', {
            pipe_type: recipe.pipe_index,
            category: shownCategory.cat_name,
          })} />
      ))}

  );
};

const SmartPipeBlockSection = (props, context) => {
  const { act, data } = useBackend(context);
  const init_directions = data.init_directions || [];
  const {
    category: rootCategoryIndex,
  } = data;
  return (

      <Stack fill vertical textAlign="center">
        <Stack.Item basis={1.5}>
          <Stack>
            <Stack.Item>
              <Button
                color="transparent"
                icon="info"
                tooltipPosition="right"
                tooltip={multiline`
                This is a panel for blocking certain connection
                directions for the smart pipes.
                The button in the center resets to
                default (all directions can connect)`} />
            </Stack.Item>
            <Stack.Item>
              <Button icon="arrow-up"
                disabled={!!data.smart_pipe}
                selected={init_directions["north"]}
                onClick={() => act('init_dir_setting', {
                  dir_flag: "north",
                })} />
            </Stack.Item>
          </Stack>
        </Stack.Item>
        <Stack.Item basis={1.5}>
          <Stack fill>
            <Stack.Item>
              <Button icon="arrow-left"
                selected={init_directions["west"]}
                onClick={() => act('init_dir_setting', {
                  dir_flag: "west",
                })} />
            </Stack.Item>
            <Stack.Item grow>
              <Button icon="circle"
                onClick={() => act('init_reset', {
                })} />
            </Stack.Item>
            <Stack.Item>
              <Button icon="arrow-right"
                selected={init_directions["east"]}
                onClick={() => act('init_dir_setting', {
                  dir_flag: "east",
                })} />
            </Stack.Item>
          </Stack>
        </Stack.Item>
        <Stack.Item grow>
          <Button icon="arrow-down"
            selected={init_directions["south"]}
            onClick={() => act('init_dir_setting', {
              dir_flag: "south",
            })} />
        </Stack.Item>
      </Stack>

  );
};

export const RapidPipeDispenser = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    category: rootCategoryIndex,
  } = data;
  return (
    <Window
      width={450}
      height={575}>
      <Window.Content>
        <Stack fill vertical>
          <Stack.Item>
            <SelectionSection />
          </Stack.Item>
          <Stack.Item grow>
            <Stack fill>
              <Stack.Item>
                <Stack vertical fill>
                  {rootCategoryIndex === 0 && (
                    <Stack.Item>
                      <SmartPipeBlockSection />
                    </Stack.Item>
                  )}
                  <Stack.Item grow>
                    <LayerSection />
                  </Stack.Item>
                </Stack>
              </Stack.Item>
              <Stack.Item grow>
                <PipeTypeSection />
              </Stack.Item>
            </Stack>
          </Stack.Item>
        </Stack>
      </Window.Content>
    </Window>
  );
};


code:
import { useBackend, useLocalState } from '../backend';
import { Button, Icon, LabeledList, NumberInput, Section, Stack, Table } from '../components';
import { Window } from '../layouts';
import { ReagentLookup } from './common/ReagentLookup';
import { RecipeLookup } from './common/RecipeLookup';

const bookmarkedReactions = new Set();

const matchBitflag = (a, b) => (a & b) && (a | b) === b;

export const Reagents = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    beakerSync,
    reagent_mode_recipe,
    reagent_mode_reagent,
    bitflags = {},
  } = data;

  const flagIcons = [
    { flag: bitflags.BRUTE, icon: "gavel" },
    { flag: bitflags.BURN, icon: "burn" },
    { flag: bitflags.TOXIN, icon: "biohazard" },
    { flag: bitflags.OXY, icon: "wind" },
    { flag: bitflags.CLONE, icon: "male" },
    { flag: bitflags.HEALING, icon: "medkit" },
    { flag: bitflags.DAMAGING, icon: "skull-crossbones" },
    { flag: bitflags.EXPLOSIVE, icon: "bomb" },
    { flag: bitflags.OTHER, icon: "question" },
    { flag: bitflags.DANGEROUS, icon: "exclamation-triangle" },
    { flag: bitflags.EASY, icon: "chess-pawn" },
    { flag: bitflags.MODERATE, icon: "chess-knight" },
    { flag: bitflags.HARD, icon: "chess-queen" },
    { flag: bitflags.ORGAN, icon: "brain" },
    { flag: bitflags.DRINK, icon: "cocktail" },
    { flag: bitflags.FOOD, icon: "drumstick-bite" },
    { flag: bitflags.SLIME, icon: "microscope" },
    { flag: bitflags.DRUG, icon: "pills" },
    { flag: bitflags.UNIQUE, icon: "puzzle-piece" },
    { flag: bitflags.CHEMICAL, icon: "flask" },
    { flag: bitflags.PLANT, icon: "seedling" },
    { flag: bitflags.COMPETITIVE, icon: "recycle" },
  ];

  const [page, setPage] = useLocalState(context, "page", 1);

  return (
    <Window
      width={720}
      height={850}>
      <Window.Content>
        <Stack fill vertical>
          <Stack.Item>
            <Stack fill>
              <Stack.Item grow basis={0}>
                <Section
                  title="Recipe lookup"
                  minWidth="353px"
                  buttons={(
                    <>
                      <Button
                        content="Beaker Sync"
                        icon="atom"
                        color={beakerSync ? "green" : "red"}
                        tooltip="When enabled the displayed reaction will automatically display ongoing reactions in the associated beaker."
                        onClick={() => act('beaker_sync')} />
                      <Button
                        content="Search"
                        icon="search"
                        color="purple"
                        tooltip="Search for a recipe by product name"
                        onClick={() => act('search_recipe')} />
                      <Button
                        icon="times"
                        color="red"
                        disabled={!reagent_mode_recipe}
                        onClick={() => act('recipe_click', {
                          id: null,
                        })} />
                    </>
                  )}>
                  <RecipeLookup
                    recipe={reagent_mode_recipe}
                    bookmarkedReactions={bookmarkedReactions} />

              </Stack.Item>
              <Stack.Item grow basis={0}>
                <Section
                  title="Reagent lookup"
                  minWidth="300px"
                  buttons={(
                    <>
                      <Button
                        content="Search"
                        icon="search"
                        tooltip="Search for a reagent by name"
                        tooltipPosition="left"
                        onClick={() => act('search_reagents')} />
                      <Button
                        icon="times"
                        color="red"
                        disabled={!reagent_mode_reagent}
                        onClick={() => act('reagent_click', {
                          id: null,
                        })} />
                    </>
                  )}>
                  <ReagentLookup reagent={reagent_mode_reagent} />

              </Stack.Item>
            </Stack>
          </Stack.Item>
          <Stack.Item>

              <TagBox
                bitflags={bitflags} />

          </Stack.Item>
          <Stack.Item grow={2} basis={0}>
            <RecipeLibrary
              flagIcons={flagIcons} />
          </Stack.Item>
        </Stack>
      </Window.Content>
    </Window>
  );
};

const TagBox = (props, context) => {
  const { act, data } = useBackend(context);
  const [page, setPage] = useLocalState(context, "page", 1);
  const { bitflags } = props;
  const { selectedBitflags } = data;
  return (
    <LabeledList>
      <LabeledList.Item label="Affects">
        <Button
          color={selectedBitflags & bitflags.BRUTE ? "green" : "red"}
          icon="gavel"
          onClick={() => {
            act('toggle_tag_brute');
            setPage(1);
          }}>
          Brute
        </Button>
        <Button
          color={selectedBitflags & bitflags.BURN ? "green" : "red"}
          icon="burn"
          onClick={() => {
            act('toggle_tag_burn');
            setPage(1);
          }}>
          Burn
        </Button>
        <Button
          color={selectedBitflags & bitflags.TOXIN ? "green" : "red"}
          icon="biohazard"
          onClick={() => {
            act('toggle_tag_toxin');
            setPage(1);
          }}>
          Toxin
        </Button>
        <Button
          color={selectedBitflags & bitflags.OXY ? "green" : "red"}
          icon="wind"
          onClick={() => {
            act('toggle_tag_oxy');
            setPage(1);
          }}>
          Suffocation
        </Button>
        <Button
          color={selectedBitflags & bitflags.CLONE ? "green" : "red"}
          icon="male"
          onClick={() => {
            act('toggle_tag_clone');
            setPage(1);
          }}>
          Clone
        </Button>
        <Button
          color={selectedBitflags & bitflags.ORGAN ? "green" : "red"}
          icon="brain"
          onClick={() => {
            act('toggle_tag_organ');
            setPage(1);
          }}>
          Organ
        </Button>
        <Button
          icon="flask"
          color={selectedBitflags & bitflags.CHEMICAL ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_chemical');
            setPage(1);
          }}>
          Chemical
        </Button>
        <Button
          icon="seedling"
          color={selectedBitflags & bitflags.PLANT ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_plant');
            setPage(1);
          }}>
          Plants
        </Button>
        <Button
          icon="question"
          color={selectedBitflags & bitflags.OTHER ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_other');
            setPage(1);
          }}>
          Other
        </Button>
      </LabeledList.Item>
      <LabeledList.Item label="Type">
        <Button
          color={selectedBitflags & bitflags.DRINK ? "green" : "red"}
          icon="cocktail"
          onClick={() => {
            act('toggle_tag_drink');
            setPage(1);
          }}>
          Drink
        </Button>
        <Button
          color={selectedBitflags & bitflags.FOOD ? "green" : "red"}
          icon="drumstick-bite"
          onClick={() => {
            act('toggle_tag_food');
            setPage(1);
          }}>
          Food
        </Button>
        <Button
          color={selectedBitflags & bitflags.HEALING ? "green" : "red"}
          icon="medkit"
          onClick={() => {
            act('toggle_tag_healing');
            setPage(1);
          }}>
          Healing
        </Button>
        <Button
          icon="skull-crossbones"
          color={selectedBitflags & bitflags.DAMAGING ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_damaging');
            setPage(1);
          }}>
          Toxic
        </Button>
        <Button
          icon="pills"
          color={selectedBitflags & bitflags.DRUG ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_drug');
            setPage(1);
          }}>
          Drugs
        </Button>
        <Button
          icon="microscope"
          color={selectedBitflags & bitflags.SLIME ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_slime');
            setPage(1);
          }}>
          Slime
        </Button>
        <Button
          icon="bomb"
          color={selectedBitflags & bitflags.EXPLOSIVE ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_explosive');
            setPage(1);
          }}>
          Explosive
        </Button>
        <Button
          icon="puzzle-piece"
          color={selectedBitflags & bitflags.UNIQUE ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_unique');
            setPage(1);
          }}>
          Unique
        </Button>
      </LabeledList.Item>
      <LabeledList.Item label="Difficulty">
        <Button
          icon="chess-pawn"
          color={selectedBitflags & bitflags.EASY ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_easy');
            setPage(1);
          }}>
          Easy
        </Button>
        <Button
          icon="chess-knight"
          color={selectedBitflags & bitflags.MODERATE ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_moderate');
            setPage(1);
          }}>
          Moderate
        </Button>
        <Button
          icon="chess-queen"
          color={selectedBitflags & bitflags.HARD ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_hard');
            setPage(1);
          }}>
          Hard
        </Button>
        <Button
          icon="exclamation-triangle"
          color={selectedBitflags & bitflags.DANGEROUS ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_dangerous');
            setPage(1);
          }}>
          Dangerous
        </Button>
        <Button
          icon="recycle"
          color={selectedBitflags & bitflags.COMPETITIVE ? "green" : "red"}
          onClick={() => {
            act('toggle_tag_competitive');
            setPage(1);
          }}>
          Competitive
        </Button>
      </LabeledList.Item>
    </LabeledList>
  );
};

const RecipeLibrary = (props, context) => {
  const { act, data } = useBackend(context);
  const [page, setPage] = useLocalState(context, "page", 1);
  const { flagIcons } = props;
  const {
    selectedBitflags,
    currentReagents = [],
    master_reaction_list = [],
    linkedBeaker,
  } = data;

  const [reagentFilter, setReagentFilter] = useLocalState(
    context, 'reagentFilter', true);
  const [bookmarkMode, setBookmarkMode] = useLocalState(
    context, 'bookmarkMode', false);

  const matchReagents = reaction => {
    if (!reagentFilter || currentReagents === null) {
      return true;
    }
    let matches = reaction.reactants
      .filter(reactant => currentReagents.includes(reactant.id))
      .length;
    return matches === currentReagents.length;
  };

  const bookmarkArray = Array.from(bookmarkedReactions);

  const startIndex = 50 * (page-1);

  const endIndex = 50 * page;

  const visibleReactions = bookmarkMode
    ? bookmarkArray
    : master_reaction_list.filter(reaction => (
      (selectedBitflags
        ? matchBitflag(selectedBitflags, reaction.bitflags)
        : true)
      && matchReagents(reaction)
    ));

  const pageIndexMax = Math.ceil(visibleReactions.length/50);

  const addBookmark = bookmark => {
    bookmarkedReactions.add(bookmark);
  };

  const removeBookmark = bookmark => {
    bookmarkedReactions.delete(bookmark);
  };

  return (
    <Section
      fill
      scrollable
      title={bookmarkMode ? "Bookmarked recipes" : "Possible recipes"}
      buttons={(
        <>
          Beaker: {linkedBeaker+"  "}
          <Button
            content="Filter by reagents in beaker"
            icon="search"
            disabled={bookmarkMode}
            color={reagentFilter ? "green" : "red"}
            onClick={() => {
              setReagentFilter(!reagentFilter);
              setPage(1);
            }} />
          <Button
            content="Bookmarks"
            icon="book"
            color={bookmarkMode ? "green" : "red"}
            onClick={() => {
              setBookmarkMode(!bookmarkMode);
              setPage(1);
            }} />
          <Button
            icon="minus"
            disabled={page === 1}
            onClick={() => setPage(Math.max(page - 1, 1))} />
          <NumberInput
            width="25px"
            step={1}
            stepPixelSize={3}
            value={page}
            minValue={1}
            maxValue={pageIndexMax}
            onDrag={(e, value) => setPage(value)} />
          <Button
            icon="plus"
            disabled={page === pageIndexMax}
            onClick={() => setPage(Math.min(page + 1, pageIndexMax))} />
        </>
      )}>

            Reaction

            Required reagents

            Tags

            {!bookmarkMode ? "Save" : "Del"}

        {visibleReactions.slice(startIndex, endIndex).map(reaction => (

              <Button
                mt={0.5}
                icon="flask"
                color="purple"
                content={reaction.name}
                onClick={() => act('recipe_click', {
                  id: reaction.id,
                })} />

              {reaction.reactants.map(reactant => (
                <Button
                  key={reactant.id}
                  mt={0.1}
                  icon="vial"
                  textColor="white"
                  color={currentReagents?.includes(reactant.id) && "green"} // check here
                  content={reactant.name}
                  onClick={() => act('reagent_click', {
                    id: reactant.id,
                  })} />
              ))}

              {flagIcons
                .filter(meta => reaction.bitflags & meta.flag)
                .map(meta => (
                  <Icon
                    key={meta.flag}
                    name={meta.icon}
                    mr={1} />
                ))}

              {!bookmarkMode && (
                <Button
                  icon="book"
                  color="green"
                  disabled={bookmarkedReactions.has(reaction)}
                  onClick={() => {
                    addBookmark(reaction);
                    act('update_ui');
                  }} />
              ) || (
                <Button
                  icon="trash"
                  color="red"
                  onClick={() => removeBookmark(reaction)} />
              )}

        ))}

  );
};

repo_name: AnalWerewolf/sundown

license: mit


code:
import { useBackend, useSharedState } from '../backend';
import { BlockQuote, Box, Button, Collapsible, Dimmer, Icon, Section, Stack, Tabs } from '../components';
import { Window } from '../layouts';

const ALIGNMENT2COLOR = {
  "good": "yellow",
  "neutral": "white",
  "evil": "red",
};

export const ReligiousTool = (props, context) => {
  const { act, data } = useBackend(context);
  const [tab, setTab] = useSharedState(context, 'tab', 1);
  const {
    sects,
    alignment,
    toolname,
  } = data;
  return (
    <Window
      title={toolname}
      width={560}
      height={500}>
      <Window.Content scrollable>
        <Stack vertical fill>
          <Stack.Item>
            <Tabs textAlign="center" fluid>
              <Tabs.Tab
                selected={tab === 1}
                onClick={() => setTab(1)}>
                Sect <Icon name="place-of-worship" color={ALIGNMENT2COLOR[alignment]} />
              </Tabs.Tab>
              {!sects && (
                <Tabs.Tab
                  selected={tab === 2}
                  onClick={() => setTab(2)}>
                  Rites <Icon name="pray" color={ALIGNMENT2COLOR[alignment]} />
                </Tabs.Tab>
              )}
            </Tabs>
          </Stack.Item>
          <Stack.Item grow={1}>
            {tab === 1 && (
              !!sects && (
                <SectSelectTab />
              ) || (
                <SectTab />
              )
            )}
            {tab === 2 && (
              <RiteTab />
            )}
          </Stack.Item>
        </Stack>
      </Window.Content>
    </Window>
  );
};

const SectTab = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    name,
    quote,
    desc,
    icon,
    favordesc,
    favor,
    wanted,
    deity,
    alignment,
  } = data;
  return (

      <Stack fill vertical fontSize="15px" textAlign="center">
        <Stack.Item mt={2} fontSize="32px">
          <Icon name={icon} color={ALIGNMENT2COLOR[alignment]} />
          {" " + name + " "}
          <Icon name={icon} color={ALIGNMENT2COLOR[alignment]} />
        </Stack.Item>
        <Stack.Item grow mb={2} color="grey">
          {"\""+quote+"\""}
        </Stack.Item>
        <Stack.Item color={favor === 0 ? "white" : "green"}>
          {favordesc}
        </Stack.Item>
        <Stack.Item mb={2} textAlign="left">

            {desc}

        </Stack.Item>
        <Stack.Item>

            {!wanted && (
              deity + " doesn't want any sacrifices."
            ) || (
              deity + " wishes for " + wanted + "."
            )}

        </Stack.Item>
      </Stack>

  );
};

const SectSelectTab = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    sects,
  } = data;
  return (

      <Stack vertical>
        {sects.map(sect => (
          <>
            <Collapsible
              title={(
                <Stack mt={-3.3} ml={3}>
                  <Stack.Item>
                    <Icon
                      name={sect.icon}
                      color={ALIGNMENT2COLOR[sect.alignment]} />
                  </Stack.Item>
                  <Stack.Item grow>
                    {sect.name}
                  </Stack.Item>
                  <Stack.Item italic >
                    {"\""+sect.quote+"\""}
                  </Stack.Item>
                </Stack>
              )}
              color="transparent">
              <Stack.Item key={sect} >
                {sect.desc}

                <Button
                  mt={0.25}
                  textAlign="center"
                  icon="plus"
                  fluid
                  onClick={() => act('sect_select', {
                    path: sect.path,
                  })} >
                  Select {sect.name}
                </Button>
              </Stack.Item>
            </Collapsible>
            <Stack.Divider mt={-0.5} mb={0.5} />
          </>
        ))}
      </Stack>

  );
};

const RiteTab = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    rites,
    deity,
    icon,
    alignment,
    favor,
  } = data;
  return (
    <>
      {!rites.length && (

          <Dimmer>
            <Stack vertical>
              <Stack.Item textAlign="center">
                <Icon
                  color={ALIGNMENT2COLOR[alignment]}
                  name={icon}
                  size={10}
                />
              </Stack.Item>
              <Stack.Item fontSize="18px" color={ALIGNMENT2COLOR[alignment]}>
                {deity} does not have any invocations.
              </Stack.Item>
            </Stack>
          </Dimmer>

      )}
      <Stack vertical>
        {rites.map(rite => (
          <Stack.Item key={rite}>
            <Section
              title={rite.name}
              buttons={(
                <Button
                  fontColor="white"
                  iconColor={ALIGNMENT2COLOR[alignment]}
                  disabled={favor < rite.favor}
                  color="transparent"
                  icon="arrow-right"
                  onClick={() => act('perform_rite', {
                    path: rite.path,
                  })} >
                  Invoke
                </Button>
              )} >
              <Box
                color={favor < rite.favor ? "red" : "grey"}
                mb={0.5}>
                <Icon name="star" color={ALIGNMENT2COLOR[alignment]} /> Costs {rite.favor} favor.
              </Box>

                {rite.desc}

          </Stack.Item>
        ))}
      </Stack>
    </>
  );
};


code:
import { decodeHtmlEntities } from 'common/string';
import { useBackend } from '../backend';
import { Box, Button, NoticeBox, Section, LabeledList } from '../components';
import { Window } from '../layouts';

export const RemoteRobotControl = (props, context) => {
  return (
    <Window
      title="Remote Robot Control"
      width={500}
      height={500}>
      <Window.Content scrollable>
        <RemoteRobotControlContent />
      </Window.Content>
    </Window>
  );
};

export const RemoteRobotControlContent = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    robots = [],
  } = data;
  if (!robots.length) {
    return (
      <Section>
        <NoticeBox textAlign="center">
          No robots detected
        </NoticeBox>
      </Section>
    );
  }
  return robots.map(robot => {
    return (
      <Section
        key={robot.ref}
        title={robot.name + " (" + robot.model + ")"}
        buttons={(
          <>
            <Button
              icon="tools"
              content="Interface"
              onClick={() => act('interface', {
                ref: robot.ref,
              })} />
            <Button
              icon="phone-alt"
              content="Call"
              onClick={() => act('callbot', {
                ref: robot.ref,
              })} />
          </>
        )}>
        <LabeledList>
          <LabeledList.Item label="Status">
            <Box inline color={decodeHtmlEntities(robot.mode) === "Inactive"
              ? 'bad'
              : decodeHtmlEntities(robot.mode) === "Idle"
                ? 'average'
                : 'good'}>
              {decodeHtmlEntities(robot.mode)}
            </Box>
            {' '}
            {robot.hacked && (
              <Box inline color="bad">
                (HACKED)
              </Box>
            ) || "" }
          </LabeledList.Item>
          <LabeledList.Item label="Location">
            {robot.location}
          </LabeledList.Item>
        </LabeledList>
      </Section>
    );
  });
};


code:
import { useBackend } from '../backend';
import { Box, Button, Collapsible, Flex, LabeledList, NumberInput, Section, Stack, TextArea } from '../components';
import { formatMoney } from '../format';
import { Window } from '../layouts';

export const RequestKiosk = (props, context) => {
  return (
    <Window
      width={550}
      height={600}>
      <Window.Content scrollable>
        <RequestKioskContent />
      </Window.Content>
    </Window>
  );
};

export const RequestKioskContent = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    accountName,
    requests = [],
    applicants = [],
    bountyValue,
    bountyText,
  } = data;
  const color = 'rgba(13, 13, 213, 0.7)';
  const backColor = 'rgba(0, 0, 69, 0.5)';
  return (
    <>

        <LabeledList>
          <LabeledList.Item
            label="Current Account"
            buttons={(
              <Button
                icon="power-off"
                content="Log out"
                onClick={() => act('clear')} />
            )}>
            {accountName || 'N/A'}
          </LabeledList.Item>
        </LabeledList>

      <Flex mb={1}>
        <Flex.Item grow={1} basis={0}>
          {requests?.map(request => (
            <Collapsible
              key={request.name}
              title={request.owner}
              width="300px">

                <Stack align="baseline">
                  <Stack.Item bold width="310px">
                    {request.owner}
                  </Stack.Item>
                  <Stack.Item width="100px">
                    {formatMoney(request.value) + ' cr'}
                  </Stack.Item>
                  <Stack.Item>
                    <Button
                      fluid
                      icon="pen-fancy"
                      content="Apply"
                      onClick={() => act('apply', {
                        request: request.acc_number,
                      })} />
                    <Button
                      fluid
                      icon="trash-alt"
                      content="Delete"
                      color="red"
                      onClick={() => act('deleteRequest', {
                        request: request.acc_number,
                      })} />
                  </Stack.Item>
                </Stack>

                  "{request.description}"

                  {applicants?.map(applicant => (
                    applicant.request_id === request.acc_number && (
                      <Flex>
                        <Flex.Item
                          grow={1}
                          p={0.5}
                          backgroundColor={backColor}
                          width="510px"
                          style={{
                            border: `2px solid ${color}`,
                          }}>
                          {applicant.name}
                        </Flex.Item>
                        <Flex.Item
                          align="end">
                          <Button
                            fluid
                            icon="cash-register"
                            onClick={() => act('payApplicant', {
                              applicant: applicant.requestee_id,
                              request: request.acc_number,
                            })} />
                        </Flex.Item>
                      </Flex>
                    )
                  ))}

            </Collapsible>
          ))}
        </Flex.Item>
        <Flex.Item>
          <Collapsible
            title="New Bounty"
            width="220px"
            color="green">

              <TextArea
                fluid
                height="250px"
                width="200px"
                backgroundColor="black"
                textColor="white"
                onChange={(e, value) => act('bountyText', {
                  bountytext: value,
                })} />
              <Box>
                <NumberInput
                  animate
                  unit="cr"
                  minValue={1}
                  maxValue={1000}
                  value={bountyValue}
                  width="80px"
                  onChange={(e, value) => act('bountyVal', {
                    bountyval: value,
                  })} />
              </Box>
              <Button
                icon="print"
                content="Submit bounty"
                onClick={() => act('createBounty')} />

          </Collapsible>
        </Flex.Item>
      </Flex>
    </>
  );
};


code:
/**
 * @file
 * @copyright 2021 bobbahbrown (https://github.com/bobbahbrown)
 * @license MIT
 */

import { decodeHtmlEntities } from 'common/string';
import { useBackend, useLocalState } from '../backend';
import { Button, Input, Section, Table } from '../components';
import { Popper } from '../components/Popper';
import { Window } from '../layouts';

export const RequestManager = (props, context) => {
  const { act, data } = useBackend(context);
  const { requests } = data;
  const [filteredTypes, _] = useLocalState(
    context,
    'filteredTypes',
    Object.fromEntries(
      Object.entries(displayTypeMap).map(([type, _]) => [type, true])
    )
  );
  const [searchText, setSearchText] = useLocalState(context, 'searchText');

  // Handle filtering
  let displayedRequests = requests.filter(
    (request) => filteredTypes[request.req_type]
  );
  if (searchText) {
    const filterText = searchText.toLowerCase();
    displayedRequests = displayedRequests.filter(
      (request) =>
        decodeHtmlEntities(request.message)
          .toLowerCase()
          .includes(filterText)
        || request.owner_name.toLowerCase().includes(filterText)
    );
  }

  return (
    <Window title="Request Manager" width={575} height={600} theme="admin">
      <Window.Content scrollable>
        <Section
          title="Requests"
          buttons={
            <>
              <Input
                value={searchText}
                onInput={(_, value) => setSearchText(value)}
                placeholder={'Search...'}
                mr={1}
              />
              <FilterPanel />
            </>
          }>
          {displayedRequests.map((request) => (

                    {request.owner_name}
                    {request.owner === null && ' [DC]'}

                    {request.timestamp_str}

                  <RequestType requestType={request.req_type} />
                  {decodeHtmlEntities(request.message)}

              {request.owner !== null && <RequestControls request={request} />}

          ))}

      </Window.Content>
    </Window>
  );
};

const displayTypeMap = {
  'request_prayer': 'PRAYER',
  'request_centcom': 'CENTCOM',
  'request_syndicate': 'SYNDICATE',
  'request_nuke': 'NUKE CODE',
};

const RequestType = (props) => {
  const { requestType } = props;

  return (
    <b className={`RequestManager__${requestType}`}>
      {displayTypeMap[requestType]}:

  );
};

const RequestControls = (props, context) => {
  const { act, _ } = useBackend(context);
  const { request } = props;

  return (

      <Button onClick={() => act('pp', { id: request.id })}>PP</Button>
      <Button onClick={() => act('vv', { id: request.id })}>VV</Button>
      <Button onClick={() => act('sm', { id: request.id })}>SM</Button>
      <Button onClick={() => act('flw', { id: request.id })}>FLW</Button>
      <Button onClick={() => act('tp', { id: request.id })}>TP</Button>
      <Button onClick={() => act('logs', { id: request.id })}>LOGS</Button>
      <Button onClick={() => act('smite', { id: request.id })}>SMITE</Button>
      {request.req_type !== 'request_prayer' && (
        <Button onClick={() => act('rply', { id: request.id })}>RPLY</Button>
      )}
      {request.req_type === 'request_nuke' && (
        <Button onClick={() => act('setcode', { id: request.id })}>
          SETCODE
        </Button>
      )}

  );
};

const FilterPanel = (_, context) => {
  const [filterVisible, setFilterVisible] = useLocalState(
    context,
    'filterVisible',
    false
  );
  const [filteredTypes, setFilteredTypes] = useLocalState(
    context,
    'filteredTypes',
    Object.fromEntries(
      Object.entries(displayTypeMap).map(([type, _]) => [type, true])
    )
  );

  return (
    <Popper
      options={{
        placement: 'bottom-start',
      }}
      popperContent={

            {Object.keys(displayTypeMap).map((type) => {
              return (

                    <RequestType requestType={type} />

                    <Button.Checkbox
                      checked={filteredTypes[type]}
                      onClick={() => {
                        filteredTypes[type] = !filteredTypes[type];
                        setFilteredTypes(filteredTypes);
                      }}
                      my={0.25}
                    />

              );
            })}

      }>
      <Button icon="cog" onClick={() => setFilterVisible(!filterVisible)}>
        Type Filter
      </Button>
    </Popper>
  );
};


code:
import { useBackend, useSharedState } from '../backend';
import { Box, Button, LabeledList, NoticeBox, Section, Tabs } from '../components';
import { Window } from '../layouts';

export const RoboticsControlConsole = (props, context) => {
  const { act, data } = useBackend(context);
  const [tab, setTab] = useSharedState(context, 'tab', 1);
  const {
    can_hack,
    cyborgs = [],
    drones = [],
  } = data;
  return (
    <Window
      width={500}
      height={460}>
      <Window.Content scrollable>
        <Tabs>
          <Tabs.Tab
            icon="list"
            lineHeight="23px"
            selected={tab === 1}
            onClick={() => setTab(1)}>
            Cyborgs ({cyborgs.length})
          </Tabs.Tab>
          <Tabs.Tab
            icon="list"
            lineHeight="23px"
            selected={tab === 2}
            onClick={() => setTab(2)}>
            Drones ({drones.length})
          </Tabs.Tab>
        </Tabs>
        {tab === 1 && (
          <Cyborgs cyborgs={cyborgs} can_hack={can_hack} />
        )}
        {tab === 2 && (
          <Drones drones={drones} />
        )}
      </Window.Content>
    </Window>
  );
};

const Cyborgs = (props, context) => {
  const { cyborgs, can_hack } = props;
  const { act, data } = useBackend(context);
  if (!cyborgs.length) {
    return (
      <NoticeBox>
        No cyborg units detected within access parameters
      </NoticeBox>
    );
  }
  return cyborgs.map(cyborg => {
    return (
      <Section
        key={cyborg.ref}
        title={cyborg.name}
        buttons={(
          <>
            {!!can_hack && !cyborg.emagged && (
              <Button
                icon="terminal"
                content="Hack"
                color="bad"
                onClick={() => act('magbot', {
                  ref: cyborg.ref,
                })} />
            )}
            <Button.Confirm
              icon={cyborg.locked_down ? 'unlock' : 'lock'}
              color={cyborg.locked_down ? 'good' : 'default'}
              content={cyborg.locked_down ? "Release" : "Lockdown"}
              onClick={() => act('stopbot', {
                ref: cyborg.ref,
              })} />
            <Button.Confirm
              icon="bomb"
              content="Detonate"
              color="bad"
              onClick={() => act('killbot', {
                ref: cyborg.ref,
              })} />
          </>
        )}>
        <LabeledList>
          <LabeledList.Item label="Status">
            <Box color={cyborg.status
              ? 'bad'
              : cyborg.locked_down
                ? 'average'
                : 'good'}>
              {cyborg.status
                ? "Not Responding"
                : cyborg.locked_down
                  ? "Locked Down"
                  : "Nominal"}
            </Box>
          </LabeledList.Item>
          <LabeledList.Item label="Charge">
            <Box color={cyborg.charge <= 30
              ? 'bad'
              : cyborg.charge <= 70
                ? 'average'
                : 'good'}>
              {typeof cyborg.charge === 'number'
                ? cyborg.charge + "%"
                : "Not Found"}
            </Box>
          </LabeledList.Item>
          <LabeledList.Item label="Model">
            {cyborg.module}
          </LabeledList.Item>
          <LabeledList.Item label="Master AI">
            <Box color={cyborg.synchronization ? 'default' : 'average'}>
              {cyborg.synchronization || "None"}
            </Box>
          </LabeledList.Item>
        </LabeledList>
      </Section>
    );
  });
};

const Drones = (props, context) => {
  const { drones } = props;
  const { act } = useBackend(context);

  if (!drones.length) {
    return (
      <NoticeBox>
        No drone units detected within access parameters
      </NoticeBox>
    );
  }

  return drones.map(drone => {
    return (
      <Section
        key={drone.ref}
        title={drone.name}
        buttons={(
          <Button.Confirm
            icon="bomb"
            content="Detonate"
            color="bad"
            onClick={() => act('killdrone', {
              ref: drone.ref,
            })} />
        )}>
        <LabeledList>
          <LabeledList.Item label="Status">
            <Box color={drone.status
              ? 'bad'
              : 'good'}>
              {drone.status
                ? "Not Responding"
                : 'Nominal'}
            </Box>
          </LabeledList.Item>
        </LabeledList>
      </Section>
    );
  });
};


code:
import { classes } from 'common/react';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Grid, NumberInput, Table } from '../components';
import { Window } from '../layouts';

const getNumberColor = number => {
  const inRedOddRange = (
    (number >= 1 && number <= 10)
    || (number >= 19 && number <= 28)
  );

  if (number % 2 === 1) {
    return inRedOddRange ? 'red' : 'black';
  }
  return inRedOddRange ? 'black' : 'red';
};

export const RouletteNumberCell = (props, context) => {
  const {
    buttonClass = null,
    cellClass = null,
    color,
    colspan = "1",
    rowspan = "1",
    text,
    value,
  } = props;
  const { act } = useBackend(context);

  return (

      <Button
        color={color}
        className={classes([
          "Roulette__board-button",
          buttonClass,
        ])}
        onClick={() => act('ChangeBetType', { type: value })}
      >
        {text}
      </Button>

  );
};

export const RouletteBoard = () => {
  const firstRow = [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36];
  const secondRow = [2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35];
  const thirdRow = [1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34];
  const fourthRow = {
    "s1-12": "1st 12",
    "s13-24": "2nd 12",
    "s25-36": "3rd 12",
  };
  const fifthRow = [
    { color: "transparent", text: "1-18", value: "s1-18" },
    { color: "transparent", text: "Even", value: "even" },
    { color: "black", text: "Black", value: "black" },
    { color: "red", text: "Red", value: "red" },
    { color: "transparent", text: "Odd", value: "odd" },
    { color: "transparent", text: "19-36", value: "s19-36" },
  ];

  return (
    <Box className="Roulette__container">

          <RouletteNumberCell
            buttonClass="Roulette__board-button--rowspan-3"
            color="transparent"
            rowspan="3"
            text="0"
            value="0"
          />
          {firstRow.map(number => (
            <RouletteNumberCell
              color={getNumberColor(number)}
              key={number}
              text={number.toString()}
              value={number.toString()}
            />
          ))}
          <RouletteNumberCell
            color="transparent"
            text="2 to 1"
            value="s3rd col"
          />

          {secondRow.map(number => (
            <RouletteNumberCell
              color={getNumberColor(number)}
              key={number}
              text={number.toString()}
              value={number.toString()}
            />
          ))}
          <RouletteNumberCell
            color="transparent"
            text="2 to 1"
            value="s2nd col"
          />

          {thirdRow.map(number => (
            <RouletteNumberCell
              color={getNumberColor(number)}
              key={number}
              text={number.toString()}
              value={number.toString()}
            />
          ))}
          <RouletteNumberCell
            color="transparent"
            text="2 to 1"
            value="s1st col"
          />

          {Object.entries(fourthRow).map(([value, text]) => (
            <RouletteNumberCell
              cellClass="Roulette__board-cell-number--colspan-4"
              color="transparent"
              colspan="4"
              key={value}
              text={text}
              value={value}
            />
          ))}

          {fifthRow.map(cell => (
            <RouletteNumberCell
              cellClass="Roulette__board-cell-number--colspan-2"
              color={cell.color}
              colspan="2"
              key={cell.value}
              text={cell.text}
              value={cell.value}
            />
          ))}

    </Box>
  );
};

export const RouletteBetTable = (props, context) => {
  const { act, data } = useBackend(context);

  const [
    customBet,
    setCustomBet,
  ] = useLocalState(context, 'customBet', 500);

  let {
    BetType,
  } = data;

  if (BetType.startsWith('s')) {
    BetType = BetType.substring(1, BetType.length);
  }

  return (

          Last Spin:

          Current Bet:

          {data.LastSpin}

          <Box
            bold
            mt={1}
            mb={1}
            fontSize="20px"
            textAlign="center">
            {data.BetAmount} cr on {BetType}
          </Box>
          <Box ml={1} mr={1}>
            <Button
              fluid
              content="Bet 10 cr"
              onClick={() => act('ChangeBetAmount', {
                amount: 10,
              })}
            />
            <Button
              fluid
              content="Bet 50 cr"
              onClick={() => act('ChangeBetAmount', {
                amount: 50,
              })}
            />
            <Button
              fluid
              content="Bet 100 cr"
              onClick={() => act('ChangeBetAmount', {
                amount: 100,
              })}
            />
            <Button
              fluid
              content="Bet 500 cr"
              onClick={() => act('ChangeBetAmount', {
                amount: 500,
              })}
            />
            <Grid>
              <Grid.Column>
                <Button
                  fluid
                  content="Bet custom amount..."
                  onClick={() => act('ChangeBetAmount', {
                    amount: customBet,
                  })}
                />
              </Grid.Column>
              <Grid.Column size={0.1}>
                <NumberInput
                  value={customBet}
                  minValue={0}
                  maxValue={1000}
                  step={10}
                  stepPixelSize={4}
                  width="40px"
                  onChange={(e, value) => setCustomBet(value)}
                />
              </Grid.Column>
            </Grid>
          </Box>

          <Box
            bold
            m={1}
            fontSize="14px"
            textAlign="center">
            Swipe an ID card with a connected account to spin!
          </Box>

          <Box inline bold mr={1}>
            House Balance:
          </Box>
          <Box inline>
            {data.HouseBalance ? data.HouseBalance + ' cr': "None"}
          </Box>

          <Button
            fluid
            content={data.IsAnchored ? "Bolted" : "Unbolted"}
            m={1}
            color="transparent"
            textAlign="center"
            onClick={() => act('anchor')}
          />

  );
};

export const Roulette = (props, context) => {
  return (
    <Window
      width={570}
      height={520}
      theme="cardtable">
      <Window.Content>
        <RouletteBoard />
        <RouletteBetTable />
      </Window.Content>
    </Window>
  );
};


code:
import { useBackend } from "../backend";
import { Button, Table } from "../components";
import { Window } from "../layouts";

type SDQLSpellAdminPanelData = {
  spells: {
    ref: string;
    name: string;
    ownerRef: string;
    owner: string;
    creator: string;
  }[]
}

export const SDQLSpellAdminPanel = (props, context) => {
  const { act, data } = useBackend<SDQLSpellAdminPanelData>(context);

  return (
    <Window title="SDQL Spell Admin Panel" width={1200} height={500} theme="admin" resizable>
      <Window.Content>
        <Table>
          <Table.Row header>
            <Table.Cell>
              Spell Name
            </Table.Cell>

            <Table.Cell>
              Spell Owner
            </Table.Cell>

            <Table.Cell>
              Spell Creator
            </Table.Cell>

            <Table.Cell>
              Actions
            </Table.Cell>
          </Table.Row>

          {data.spells.map(spell => {
            const createSpellAct = (action: string) => () => {
              act(action, { spell: spell.ref });
            };

            const createOwnerAct = (action: string) => () => {
              act(action, { owner: spell.ownerRef });
            };

            return (
              <Table.Row key={spell.ref}>
                <Table.Cell>
                  {spell.name}
                </Table.Cell>

                <Table.Cell>
                  {spell.owner}
                </Table.Cell>

                <Table.Cell>
                  {spell.creator}
                </Table.Cell>

                <Table.Cell>
                  <Button onClick={createSpellAct("edit_spell")}>
                    Edit
                  </Button>

                  <Button onClick={createOwnerAct("follow_owner")}>
                    Follow Owner
                  </Button>

                  <Button onClick={createSpellAct("vv_spell")}>
                    VV
                  </Button>

                  <Button onClick={createOwnerAct("open_player_panel")}>
                    Player Panel
                  </Button>
                </Table.Cell>
              </Table.Row>
            );
          })}
        </Table>
      </Window.Content>
    </Window>
  );
};


code:
import { useBackend } from '../backend';
import { Box, Button, Collapsible, Dropdown, Input, LabeledList, Modal, NumberInput, Section, Stack, Tooltip } from '../components';
import { Window } from '../layouts';

/**
 * Gets a list of objects that encode the parameters for the variables relevant
 * to the passed spell type.
 * @param type The type of the spell.
 * @returns A list of objects. Each object contains the name of the variable,
 * the variable's data type,
 * what options are valid (if the variable is an enum),
 * and what the variable's default value should be.
 */
const typevars = (type) => {
  let ret = [
    { name: 'name', type: 'string', options: null, default_value: '' },
    { name: 'desc', type: 'string', options: null, default_value: '' },
    { name: 'query', type: 'string', options: null, default_value: '' },
    {
      name: 'suppress_message_admins',
      type: 'bool',
      options: null,
      default_value: false,
    },
    { name: 'action_icon', type: 'string', options: null, default_value: '' },
    {
      name: 'action_icon_state',
      type: 'string',
      options: null,
      default_value: '',
    },
    {
      name: 'action_background_icon_state',
      type: 'string',
      options: null,
      default_value: '',
    },
    { name: 'sound', type: 'string', options: null, default_value: '' },
    {
      name: 'charge_type',
      type: 'string_enum',
      options: ['recharge', 'charges', 'holder_var'],
      default_value: 'recharge',
    },
    { name: 'charge_max', type: 'int', options: null, default_value: 100 },
    {
      name: 'still_recharging_msg',
      type: 'string',
      options: null,
      default_value: '',
    },
    {
      name: 'holder_var_type',
      type: 'string',
      options: null,
      default_value: '',
    },
    {
      name: 'holder_var_amount',
      type: 'int',
      options: null,
      default_value: '',
    },
    { name: 'clothes_req', type: 'bool', options: null, default_value: false },
    { name: 'cult_req', type: 'bool', options: null, default_value: false },
    { name: 'human_req', type: 'bool', options: null, default_value: false },
    {
      name: 'nonabstract_req',
      type: 'bool',
      options: null,
      default_value: false,
    },
    { name: 'stat_allowed', type: 'bool', options: null, default_value: false },
    {
      name: 'phase_allowed',
      type: 'bool',
      options: null,
      default_value: false,
    },
    {
      name: 'antimagic_allowed',
      type: 'bool',
      options: null,
      default_value: false,
    },
    {
      name: 'invocation_type',
      type: 'string_enum',
      options: ['none', 'whisper', 'emote', 'shout'],
      default_value: 'none',
    },
    { name: 'invocation', type: 'string', options: null, default_value: '' },
    {
      name: 'invocation_emote_self',
      type: 'string',
      options: null,
      default_value: '',
    },
    {
      name: 'selection_type',
      type: 'string_enum',
      options: ['view', 'range'],
      default_value: 'view',
    },
    { name: 'range', type: 'int', options: null, default_value: 7 },
    { name: 'message', type: 'string', options: null, default_value: '' },
    { name: 'player_lock', type: 'bool', options: null, default_value: true },
    {
      name: 'sparks_spread',
      type: 'bool',
      options: null,
      default_value: false,
    },
    { name: 'sparks_amt', type: 'int', options: null, default_value: 0 },
    {
      name: 'smoke_spread',
      type: 'int_enum',
      options: ['none', 'harmless', 'harmful', 'sleeping'],
      default_value: 'none',
    },
    { name: 'smoke_amt', type: 'int', options: null, default_value: 0 },
    {
      name: 'centcom_cancast',
      type: 'bool',
      options: null,
      default_value: false,
    },
  ];
  switch (type) {
    case 'targeted':
      ret.push(
        { name: 'overlay', type: 'bool', options: null, default_value: false },
        {
          name: 'overlay_icon',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'overlay_icon_state',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'overlay_lifespan',
          type: 'int',
          options: null,
          default_value: 0,
        },
        {
          name: 'max_targets',
          type: 'int',
          options: null,
          default_value: false,
        },
        {
          name: 'target_ignore_prev',
          type: 'bool',
          options: null,
          default_value: true,
        },
        {
          name: 'include_user',
          type: 'bool',
          options: null,
          default_value: false,
        },
        {
          name: 'random_target',
          type: 'bool',
          options: null,
          default_value: false,
        },
        {
          name: 'random_target_priority',
          type: 'int_enum',
          options: ['closest', 'random'],
          default_value: 'closest',
        }
      );
      break;
    case 'aoe_turf':
      ret = ret.filter((variable) => variable.name !== 'selection_type');
      ret.push(
        { name: 'inner_radius', type: 'int', options: null, default_value: -1 },
        { name: 'overlay', type: 'bool', options: null, default_value: false },
        {
          name: 'overlay_icon',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'overlay_icon_state',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'overlay_lifespan',
          type: 'int',
          options: null,
          default_value: 0,
        }
      );
      break;
    case 'self':
      ret = ret.filter(
        (variable) =>
          variable.name !== 'range' && variable.name !== 'selection_type'
      );
      break;
    case 'aimed':
      ret.push(
        {
          name: 'base_icon_state',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'ranged_mousepointer',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'deactive_msg',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'active_msg',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'projectile_type',
          type: 'path',
          options: null,
          default_value: '/obj/projectile',
        },
        {
          name: 'projectile_amount',
          type: 'int',
          options: null,
          default_value: 1,
        },
        {
          name: 'projectiles_per_fire',
          type: 'int',
          options: null,
          default_value: 1,
        },
        {
          name: 'projectile_var_overrides',
          type: 'list',
          options: null,
          default_value: {},
        }
      );
      break;
    case 'cone':
    case 'cone/staggered':
      ret = ret.filter(
        (variable) =>
          variable.name !== 'range' && variable.name !== 'selection_type'
      );
      ret.push(
        { name: 'cone_level', type: 'int', options: null, default_value: 3 },
        {
          name: 'respect_density',
          type: 'bool',
          options: null,
          default_value: false,
        }
      );
      break;
    case 'pointed':
      ret.push(
        { name: 'overlay', type: 'bool', options: null, default_value: false },
        {
          name: 'overlay_icon',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'overlay_icon_state',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'overlay_lifespan',
          type: 'int',
          options: null,
          default_value: 0,
        },
        {
          name: 'ranged_mousepointer',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'deactive_msg',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'active_msg',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'self_castable',
          type: 'bool',
          options: null,
          default_value: false,
        },
        { name: 'aim_assist', type: 'bool', options: null, default_value: true }
      );
      break;
    case 'targeted/touch':
      ret = ret.filter(
        (variable) =>
          variable.name !== 'range'
          && variable.name !== 'invocation_type'
          && variable.name !== 'selection_type'
      );
      ret.push(
        {
          name: 'drawmessage',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'dropmessage',
          type: 'string',
          options: null,
          default_value: '',
        },
        {
          name: 'hand_path',
          type: 'path',
          options: null,
          default_value: '/obj/item/melee/touch_attack',
        },
        {
          name: 'hand_var_overrides',
          type: 'list',
          options: null,
          default_value: {},
        }
      );
      break;
    default:
      return [];
  }
  ret.push({
    name: 'scratchpad',
    type: 'list',
    options: null,
    default_value: [],
  });
  return ret;
};

export const SDQLSpellMenu = (props, context) => {
  const { act, data } = useBackend(context);
  const { type, types, fixed_type, alert, parse_errors } = data;

  return (
    <Window width={800} height={600} theme="admin">
      <Window.Content>
        <Stack fill>
          <Stack.Item grow={1} basis={0}>
            <Stack fill vertical>
              <Stack.Item>
                <Dropdown
                  width="100%"
                  options={types}
                  disabled={fixed_type}
                  displayText={type || 'Select a Spell Type'}
                  onSelected={(value) => act('type', { path: value })}
                />
              </Stack.Item>
              <Stack.Item grow={1} basis={0}>
                <SDQLSpellOptions />
              </Stack.Item>
              <Stack.Item>
                <Stack fill>
                  <Stack.Item>
                    <Button.Confirm
                      disabled={!type}
                      content="Confirm"
                      confirmContent="Are you sure?"
                      onClick={() => act('confirm')}
                    />
                    <Button
                      disabled={!type}
                      tooltip="Save the spell to a json file on your local system."
                      onClick={() => act('save')}>
                      Save Spell
                    </Button>
                    <Button
                      tooltip="Load a spell from a json file on your local system."
                      onClick={() => act('load')}>
                      Load Spell
                    </Button>
                  </Stack.Item>
                  <Stack.Item grow basis={0} />
                  <Stack.Item textColor="bad">{alert}</Stack.Item>
                </Stack>
              </Stack.Item>
            </Stack>
          </Stack.Item>
          <Stack.Item minWidth="128px">
            <SDQLSpellIcons />
          </Stack.Item>
        </Stack>
      </Window.Content>
      {parse_errors && <SDQLSpellParseErrorModal />}
    </Window>
  );
};

/**
 * Used to determine whether or not to show a UI element corresponding to a
 * variable.
 * @param entry An object, from the list of objects returned by typevars(),
 * corresponding to the variable to be shown or hidden.
 * @param saved_vars The list of currently stored variable values.
 * @returns Whether or not to show the UI element corresponding to the variable
 * represented by the passed entry.
 */
const varCondition = (entry, saved_vars) => {
  switch (entry.name) {
    case 'charge_max':
      return saved_vars['charge_type'] !== 'holder_var';
    case 'holder_var_type':
    case 'holder_var_amount':
      return saved_vars['charge_type'] === 'holder_var';
    case 'human_req':
      return !saved_vars['clothes_req'];
    case 'invocation':
      return saved_vars['invocation_type'] !== 'none';
    case 'invocation_emote_self':
      return saved_vars['invocation_type'] === 'emote';
    case 'overlay_icon':
    case 'overlay_icon_state':
    case 'overlay_lifespan':
      return !!saved_vars['overlay'];
    case 'sparks_amt':
      return !!saved_vars['sparks_spread'];
    case 'smoke_amt':
      return !!saved_vars['smoke_spread'];
    case 'random_target_priority':
      return !!saved_vars['random_target'];
    default:
      return true;
  }
};

/**
 * A React component that wraps its contents in a tooltip object,
 * if one exists for the variable described by the object passed through the
 * entry property.
 *
 * @param entry An object, from the list of objects returned by typevars(),
 * corresponding to the variable whose tooltip is to be shown.
 */
const WrapInTooltip = (props, context) => {
  const { data } = useBackend(context);
  const { entry, children } = props;
  const { type, tooltips } = data;
  const tip = tooltips[entry.name]?.replace(
    '$type',
    tooltips[entry.name + '_' + type]
  );
  // TODO: Uncomment this block when tooltips no longer suck.
  if (tip) {
    return (
      <Tooltip position="bottom" content={tip}>
        {children}
      </Tooltip>
    );
  } else {
    return children;
  }
};

/**
 * A React component that contains a list of the meaningfully-editable variables
 * of the spell being edited.
 */
const SDQLSpellOptions = (props, context) => {
  const { data } = useBackend(context);
  const { type, saved_vars } = data;

  const vars = typevars(type);

  return (
    <Section fill scrollable>
      {vars
        .filter((entry) => varCondition(entry, saved_vars))
        .map((entry) => (
          <Stack key={entry.name} mb="6px">
            <Stack.Item>
              <WrapInTooltip entry={entry}>
                <Box inline bold color="label" mr="6px">
                  {entry.name}:
                </Box>
              </WrapInTooltip>
            </Stack.Item>
            <Stack.Item shrink basis="100%">
              <SDQLSpellInput entry={entry} />
            </Stack.Item>
          </Stack>
        ))}
    </Section>
  );
};

/**
 * A React component that contains the appropriate input element for the
 * variable described by the object passed through the entry property.
 * @param entry An object, from the list of objects returned by typevars(),
 * corresponding to the variable to provide an input element for.
 */
const SDQLSpellInput = (props, context) => {
  const { act, data } = useBackend(context);
  const { saved_vars } = data;
  const { entry } = props;
  const { name, type, options, default_value } = entry;
  switch (type) {
    case 'string':
      return (
        <Input
          width="100%"
          fluid
          value={saved_vars[name] ?? default_value}
          onChange={(e, value) => act('variable', { name, value })}
        />
      );
    case 'int':
      return (
        <NumberInput
          value={saved_vars[name] ?? default_value}
          onChange={(e, value) => act('variable', { name, value })}
        />
      );
    case 'bool':
      return (
        <Button.Checkbox
          checked={saved_vars[name] ?? default_value}
          onClick={() => act('bool_variable', { name })}
        />
      );
    case 'string_enum':
      return (
        <Dropdown
          options={options}
          displayText={saved_vars[name] ?? default_value}
          onSelected={(value) => act('variable', { name, value })}
        />
      );
    case 'int_enum':
      return (
        <Dropdown
          options={options}
          displayText={options[saved_vars[name]] ?? default_value}
          onSelected={(value) =>
            act('variable', { name, value: options.indexOf(value) })}
        />
      );
    case 'path':
      return (
        <Button
          content={saved_vars[name] ?? default_value}
          onClick={() => act('path_variable', { name, root_path: default_value })}
        />
      );
    case 'list':
      return <SDQLSpellListEntry list={name} default_value={default_value} />;
  }
};

/**
 * A React component containing the appropriate input fields for editing a list
 * variable.
 * @param {string} list The name of the list to show variables for.
 */
const SDQLSpellListEntry = (props, context) => {
  const { act, data } = useBackend(context);
  const { list_vars } = data;
  const { list, default_value, is_input } = props;
  return (
    <Collapsible>
      {Object.entries(list_vars[list] || default_value)
        .map(([name, { type, value, flags }]) => (
          <Stack key={name} fill mb="6px">
            <Stack.Item grow>
              {
              // Can be renamed?
                (flags & 2) === 0 ? (
                  <Input
                    value={name}
                    onChange={(e, value) =>
                      act('list_variable_rename', { list, name, new_name: value })}
                  />
                ) : (
                  <Box inline bold color="label" mr="6px">
                    {name}:
                  </Box>
                )
              }
            </Stack.Item>
            <Stack.Item>
              {
              // Can type be changed?
                (flags & 1) === 0 && (
                  <Dropdown
                    options={['num', 'bool', 'string', 'path', 'ref', 'icon', 'list']}
                    displayText={type}
                    onSelected={(value) =>
                      act('list_variable_change_type', { list, name, value })}
                  />
                )
              }
            </Stack.Item>
            <Stack.Item shrink basis="100%">
              <SDQLSpellListVarInput
                list={list}
                name={name}
                type={type}
                value={value}
              />
              <Button
                icon="minus-circle"
                color="red"
                title="remove"
                onClick={() => act('list_variable_remove', { list, name })}
              />
            </Stack.Item>
          </Stack>
        ))}
      <Button
        icon="plus-circle"
        color="blue"
        title="add variable"
        onClick={() => act('list_variable_add', { list })}
      />
    </Collapsible>
  );
};

/**
 * A React component that contains the appropriate input element for the
 * variable of a given name and type within a list.
 * @param list The name of the list containing the variable
 * @param name The name of the variable
 * @param type The type of the variable
 * @param value The current value of the variable
 */
const SDQLSpellListVarInput = (props, context) => {
  const { act } = useBackend(context);
  const { list, name, type, value } = props;
  switch (type) {
    case 'num':
      return (
        <NumberInput
          value={value}
          onChange={(e, value) =>
            act('list_variable_change_value', { list, name, value })}
        />
      );
    case 'bool':
      return (
        <Button.Checkbox
          checked={value === 1}
          onClick={() => act('list_variable_change_bool', { list, name })}
        />
      );
    case 'string':
    case 'path':
    case 'icon':
      return (
        <Input
          width="75%"
          fluid
          value={value}
          onChange={(e, value) =>
            act('list_variable_change_value', { list, name, value })}
        />
      );
    case 'list':
      return <SDQLSpellListEntry list={list + '/' + name} />;
    case 'ref':
      return (
        <>
          <Box inline bold color="label" mr="6px">
            {value}
          </Box>
          <Button
            icon="upload"
            color="blue"
            title="set to marked datum"
            onClick={() => act('list_variable_set_ref', { list, name })} />
        </>
      );
    default:
      return (
        <Box bold textColor="bad">
          {"You shouldn't be seeing this!"}
        </Box>
      );
  }
};

const SDQLSpellIcons = (props, context) => {
  const { data } = useBackend(context);
  const {
    saved_vars,
    type,
    action_icon,
    hand_icon,
    projectile_icon,
    overlay_icon,
    mouse_icon,
  } = data;

  const vars = typevars(type);

  return (
    <Section fill>
      <Stack vertical>
        {type && (
          <Section title="Action Button Icon">
            <Box
              as="img"
              height="64px"
              width="auto"
              m={0}
              src={`data:image/jpeg;base64,${action_icon}`}
              style={{
                '-ms-interpolation-mode': 'nearest-neighbor',
              }}
            />
          </Section>
        )}
        {type === 'targeted/touch' && (
          <Section title="Touch Attack Icon">
            <Box
              as="img"
              height="64px"
              width="auto"
              m={0}
              src={`data:image/jpeg;base64,${hand_icon}`}
              style={{
                '-ms-interpolation-mode': 'nearest-neighbor',
              }}
            />
          </Section>
        )}
        {type === 'aimed' && (
          <Section title="Projectile Icon">
            <Box
              as="img"
              height="64px"
              width="auto"
              m={0}
              src={`data:image/jpeg;base64,${projectile_icon}`}
              style={{
                '-ms-interpolation-mode': 'nearest-neighbor',
              }}
            />
          </Section>
        )}
        {type
          && vars.some((entry) => entry.name === 'ranged_mousepointer')
          && saved_vars['ranged_mousepointer'] && (
          <Section title="Mouse Cursor">
            <Box
              as="img"
              height="64px"
              width="auto"
              m={0}
              src={`data:image/jpeg;base64,${mouse_icon}`}
              style={{
                '-ms-interpolation-mode': 'nearest-neighbor',
              }}
            />
          </Section>
        )}
        {type && 'overlay' in saved_vars && saved_vars['overlay'] === 1 && (
          <Section title="Overlay Icon">
            <Box
              as="img"
              height="64px"
              width="auto"
              m={0}
              src={`data:image/jpeg;base64,${overlay_icon}`}
              style={{
                '-ms-interpolation-mode': 'nearest-neighbor',
              }}
            />
          </Section>
        )}
      </Stack>
    </Section>
  );
};

const SDQLSpellParseErrorModal = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    parse_errors,
    parsed_type,
    parsed_vars,
    parsed_list_vars } = data;
  const multiple_errors = parse_errors.length > 1;
  return (
    <Modal>
      <Stack fill vertical>
        <Stack.Item>
          Parse error{multiple_errors ? "s" : ""} occured while loading from file.
        </Stack.Item>
        <Stack.Item>
          <Section scrollable maxWidth="600px" maxHeight="200px">
            {parse_errors.map((error, i) => {
              return (
                <>
                  <Box key={`error_${i}`}>
                    {error}
                  </Box>
                  {i<parse_errors.length-1 && (
                    <hr />
                  )}
                </>
              );
            })}
          </Section>
        </Stack.Item>
        {(parsed_type || parsed_vars || parsed_list_vars) && (
          <>
            <Stack.Item>
              <Box>
                Some data could be correctly parsed. Click below to view.
              </Box>
            </Stack.Item>
            <Stack.Item>
              <Collapsible title="Parsed Data">
                <Section scrollable maxWidth="600px" maxHeight="200px">
                  <LabeledList>
                    <LabeledList.Item label="type">
                      {parsed_type}
                    </LabeledList.Item>
                    {Object.entries(parsed_vars).map(([name, value]) => {
                      return (
                        <LabeledList.Item key={name} label={name}>
                          {value}
                        </LabeledList.Item>
                      );
                    })}
                  </LabeledList>
                  {Object.entries(parsed_list_vars).filter(([name]) => {
                    return !/\//.test(name);
                  }).map(([name]) => {
                    return (
                      <SDQLSpellParsedList
                        key={name}
                        list={name}
                        name={name}
                        recursive_props={{
                          mx: "10px",
                        }} />
                    );
                  })}
                  <Button.Confirm
                    content="Load Anyways"
                    confirmContent="Are you sure?"
                    onClick={() => act('load_despite_error')} />
                </Section>
              </Collapsible>
            </Stack.Item>
          </>
        )}
        <Stack.Item>
          <Button onClick={() => act('close_error')}>
            Ok
          </Button>
        </Stack.Item>
      </Stack>
    </Modal>
  );
};

const SDQLSpellParsedList = (props, context) => {
  const { list, name, recursive_props } = props;
  const { act, data } = useBackend(context);
  const { parsed_list_vars } = data;
  return (
    <Collapsible title={name}>
      <Box {...recursive_props}>
        <LabeledList>
          {Object.entries(parsed_list_vars[list])
            .sort(([, { type_1 }], [, { type_2 }]) => {
              return (type_1 === "list" ? 1 : 0) - (type_2 === "list" ? 1 : 0);
            }).map(([name, { type, value }]) => {
              return type === "list" ? (
                <SDQLSpellParsedList
                  key={name}
                  list={`${list}/${name}`}
                  name={name}
                  recursive_props={recursive_props} />
              ) : (
                <LabeledList.Item key={name} label={name}>
                  {value}
                </LabeledList.Item>
              );
            })}
        </LabeledList>
      </Box>
    </Collapsible>
  );
};


code:
import { Fragment } from 'inferno';
import { resolveAsset } from '../assets';
import { useBackend } from '../backend';
import { Box, Button, Icon, Section } from '../components';
import { Window } from "../layouts";

export const Safe = (properties, context) => {
  const { act, data } = useBackend(context);
  const {
    dial,
    open,
  } = data;
  return (
    <Window
      width={625}
      height={800}
      theme="ntos">
      <Window.Content>
        <Box className="Safe__engraving">
          <Dialer />
          <Box>
            <Box
              className="Safe__engraving-hinge"
              top="25%" />
            <Box
              className="Safe__engraving-hinge"
              top="75%" />
          </Box>
          <Icon
            className="Safe__engraving-arrow"
            name="long-arrow-alt-down"
            size="5"
          />

          {open ? (
            <Contents />
          ) : (
            <Box
              as="img"
              className="Safe__dial"
              src={resolveAsset('safe_dial.png')}
              style={{
                "transform": "rotate(-" + (3.6 * dial) + "deg)",
              }}
            />
          )}
        </Box>
        {!open && (
          <Help />
        )}
      </Window.Content>
    </Window>
  );
};

const Dialer = (properties, context) => {
  const { act, data } = useBackend(context);
  const {
    dial,
    open,
    locked,
    broken,
  } = data;
  const dialButton = (amount, right) => {
    return (
      <Button
        disabled={open || (right && !locked) || broken}
        icon={"arrow-" + (right ? "right" : "left")}
        content={(right ? "Right" : "Left") + " " + amount}
        iconPosition={right ? "right" : "left"}
        onClick={() => act(!right ? "turnright" : "turnleft", {
          num: amount,
        })}
      />
    );
  };
  return (
    <Box className="Safe__dialer">
      <Button
        disabled={locked && !broken}
        icon={open ? "lock" : "lock-open"}
        content={open ? "Close" : "Open"}
        mb="0.5rem"
        onClick={() => act('open')}
      />

      <Box position="absolute">
        {[dialButton(50), dialButton(10), dialButton(1)]}
      </Box>
      <Box
        className="Safe__dialer-right"
        position="absolute" right="5px">
        {[dialButton(1, true), dialButton(10, true), dialButton(50, true)]}
      </Box>
      <Box className="Safe__dialer-number">
        {dial}
      </Box>
    </Box>
  );
};

const Contents = (properties, context) => {
  const { act, data } = useBackend(context);
  const {
    contents,
  } = data;
  return (
    <Box
      className="Safe__contents"
      overflow="auto">
      {contents.map((item, index) => (
        <Fragment key={item}>
          <Button
            mb="0.5rem"
            onClick={() => act("retrieve", {
              index: index + 1,
            })}>
            <Box
              as="img"
              src={item.sprite + ".png"}
              verticalAlign="middle"
              ml="-6px"
              mr="0.5rem"
            />
            {item.name}
          </Button>

        </Fragment>
      ))}
    </Box>
  );
};

const Help = (properties, context) => {
  return (

      <Box>
        1. Turn the dial left to the first number.

        2. Turn the dial right to the second number.

        3. Continue repeating this process for each number,
        switching between left and right each time.

        4. Open the safe.
      </Box>
      <Box bold>
        To lock fully, turn the dial to the left after closing the safe.
      </Box>

  );
};


code:
import { useBackend } from '../backend';
import { Box, Button, LabeledList, ProgressBar, Section } from '../components';
import { Window } from '../layouts';

export const SatelliteControl = (props, context) => {
  const { act, data } = useBackend(context);
  const satellites = data.satellites || [];
  return (
    <Window
      width={400}
      height={305}>
      <Window.Content>
        {data.meteor_shield && (

            <LabeledList>
              <LabeledList.Item label="Coverage">
                <ProgressBar
                  value={data.meteor_shield_coverage
                    / data.meteor_shield_coverage_max}
                  content={100 * data.meteor_shield_coverage
                    / data.meteor_shield_coverage_max + '%'}
                  ranges={{
                    good: [1, Infinity],
                    average: [0.30, 1],
                    bad: [-Infinity, 0.30],
                  }} />
              </LabeledList.Item>
            </LabeledList>

        )}

          <Box mr={-1}>
            {satellites.map(satellite => (
              <Button.Checkbox
                key={satellite.id}
                checked={satellite.active}
                content={"#" + satellite.id + " " + satellite.mode}
                onClick={() => act('toggle', {
                  id: satellite.id,
                })}
              />
            ))}
          </Box>

      </Window.Content>
    </Window>
  );
};


code:
import { useBackend } from '../backend';
import { Box, Button, LabeledList, Section } from '../components';
import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox';
import { Window } from '../layouts';

const DISEASE_THEASHOLD_LIST = [
  'Positive',
  'Harmless',
  'Minor',
  'Medium',
  'Harmful',
  'Dangerous',
  'BIOHAZARD',
];

const TARGET_SPECIES_LIST = [
  {
    name: 'Human',
    value: 'human',
  },
  {
    name: 'Lizardperson',
    value: 'lizard',
  },
  {
    name: 'Flyperson',
    value: 'fly',
  },
  {
    name: 'Felinid',
    value: 'felinid',
  },
  {
    name: 'Plasmaman',
    value: 'plasma',
  },
  {
    name: 'Mothperson',
    value: 'moth',
  },
  {
    name: 'Jellyperson',
    value: 'jelly',
  },
  {
    name: 'Podperson',
    value: 'pod',
  },
  {
    name: 'Golem',
    value: 'golem',
  },
  {
    name: 'Zombie',
    value: 'zombie',
  },
];

const TARGET_NUTRITION_LIST = [
  {
    name: 'Starving',
    value: 150,
  },
  {
    name: 'Obese',
    value: 600,
  },
];

export const ScannerGate = (props, context) => {
  const { act, data } = useBackend(context);
  return (
    <Window
      width={400}
      height={300}>
      <Window.Content scrollable>
        <InterfaceLockNoticeBox
          onLockedStatusChange={() => act('toggle_lock')} />
        {!data.locked && (
          <ScannerGateControl />
        )}
      </Window.Content>
    </Window>
  );
};

const SCANNER_GATE_ROUTES = {
  Off: {
    title: 'Scanner Mode: Off',
    component: () => ScannerGateOff,
  },
  Wanted: {
    title: 'Scanner Mode: Wanted',
    component: () => ScannerGateWanted,
  },
  Guns: {
    title: 'Scanner Mode: Guns',
    component: () => ScannerGateGuns,
  },
  Mindshield: {
    title: 'Scanner Mode: Mindshield',
    component: () => ScannerGateMindshield,
  },
  Disease: {
    title: 'Scanner Mode: Disease',
    component: () => ScannerGateDisease,
  },
  Species: {
    title: 'Scanner Mode: Species',
    component: () => ScannerGateSpecies,
  },
  Nutrition: {
    title: 'Scanner Mode: Nutrition',
    component: () => ScannerGateNutrition,
  },
};

const ScannerGateControl = (props, context) => {
  const { act, data } = useBackend(context);
  const { scan_mode } = data;
  const route = SCANNER_GATE_ROUTES[scan_mode]
    || SCANNER_GATE_ROUTES.off;
  const Component = route.component();
  return (
    <Section
      title={route.title}
      buttons={scan_mode !== 'Off' && (
        <Button
          icon="arrow-left"
          content="back"
          onClick={() => act('set_mode', { new_mode: 'Off' })} />
      )}>
      <Component />
    </Section>
  );
};

const ScannerGateOff = (props, context) => {
  const { act } = useBackend(context);
  return (
    <>
      <Box mb={2}>
        Select a scanning mode below.
      </Box>
      <Box>
        <Button
          content="Wanted"
          onClick={() => act('set_mode', { new_mode: 'Wanted' })} />
        <Button
          content="Guns"
          onClick={() => act('set_mode', { new_mode: 'Guns' })} />
        <Button
          content="Mindshield"
          onClick={() => act('set_mode', { new_mode: 'Mindshield' })} />
        <Button
          content="Disease"
          onClick={() => act('set_mode', { new_mode: 'Disease' })} />
        <Button
          content="Species"
          onClick={() => act('set_mode', { new_mode: 'Species' })} />
        <Button
          content="Nutrition"
          onClick={() => act('set_mode', { new_mode: 'Nutrition' })} />
      </Box>
    </>
  );
};

const ScannerGateWanted = (props, context) => {
  const { data } = useBackend(context);
  const { reverse } = data;
  return (
    <>
      <Box mb={2}>
        Trigger if the person scanned {reverse ? 'does not have' : 'has'}
        {' '}any warrants for their arrest.
      </Box>
      <ScannerGateMode />
    </>
  );
};

const ScannerGateGuns = (props, context) => {
  const { data } = useBackend(context);
  const { reverse } = data;
  return (
    <>
      <Box mb={2}>
        Trigger if the person scanned {reverse ? 'does not have' : 'has'}
        {' '}any guns.
      </Box>
      <ScannerGateMode />
    </>
  );
};

const ScannerGateMindshield = (props, context) => {
  const { data } = useBackend(context);
  const { reverse } = data;
  return (
    <>
      <Box mb={2}>
        Trigger if the person scanned {reverse ? 'does not have' : 'has'}
        {' '}a mindshield.
      </Box>
      <ScannerGateMode />
    </>
  );
};

const ScannerGateDisease = (props, context) => {
  const { act, data } = useBackend(context);
  const { reverse, disease_threshold } = data;
  return (
    <>
      <Box mb={2}>
        Trigger if the person scanned {reverse ? 'does not have' : 'has'}
        {' '}a disease equal or worse than {disease_threshold}.
      </Box>
      <Box mb={2}>
        {DISEASE_THEASHOLD_LIST.map(threshold => (
          <Button.Checkbox
            key={threshold}
            checked={threshold === disease_threshold}
            content={threshold}
            onClick={() => act('set_disease_threshold', {
              new_threshold: threshold,
            })} />
        ))}
      </Box>
      <ScannerGateMode />
    </>
  );
};

const ScannerGateSpecies = (props, context) => {
  const { act, data } = useBackend(context);
  const { reverse, target_species } = data;
  const species = TARGET_SPECIES_LIST.find(species => {
    return species.value === target_species;
  });
  return (
    <>
      <Box mb={2}>
        Trigger if the person scanned is {reverse ? 'not' : ''}
        {' '}of the {species.name} species.
        {target_species === 'zombie' && (
          ' All zombie types will be detected, including dormant zombies.'
        )}
      </Box>
      <Box mb={2}>
        {TARGET_SPECIES_LIST.map(species => (
          <Button.Checkbox
            key={species.value}
            checked={species.value === target_species}
            content={species.name}
            onClick={() => act('set_target_species', {
              new_species: species.value,
            })} />
        ))}
      </Box>
      <ScannerGateMode />
    </>
  );
};

const ScannerGateNutrition = (props, context) => {
  const { act, data } = useBackend(context);
  const { reverse, target_nutrition } = data;
  const nutrition = TARGET_NUTRITION_LIST.find(nutrition => {
    return nutrition.value === target_nutrition;
  });
  return (
    <>
      <Box mb={2}>
        Trigger if the person scanned {reverse ? 'does not have' : 'has'}
        {' '}the {nutrition.name} nutrition level.
      </Box>
      <Box mb={2}>
        {TARGET_NUTRITION_LIST.map(nutrition => (
          <Button.Checkbox
            key={nutrition.name}
            checked={nutrition.value === target_nutrition}
            content={nutrition.name}
            onClick={() => act('set_target_nutrition', {
              new_nutrition: nutrition.name,
            })} />
        ))}
      </Box>
      <ScannerGateMode />
    </>
  );
};

const ScannerGateMode = (props, context) => {
  const { act, data } = useBackend(context);
  const { reverse } = data;
  return (
    <LabeledList>
      <LabeledList.Item label="Scanning Mode">
        <Button
          content={reverse ? 'Inverted' : 'Default'}
          icon={reverse ? 'random' : 'long-arrow-alt-right'}
          onClick={() => act('toggle_reverse')}
          color={reverse ? 'bad' : 'good'} />
      </LabeledList.Item>
    </LabeledList>
  );
};