coder/site/src/pages/IconsPage/IconsPage.tsx

206 lines
6.2 KiB
TypeScript

import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import Tooltip from "@mui/material/Tooltip";
import IconButton from "@mui/material/IconButton";
import Link from "@mui/material/Link";
import SearchIcon from "@mui/icons-material/SearchOutlined";
import ClearIcon from "@mui/icons-material/CloseOutlined";
import { useTheme } from "@emotion/react";
import { type FC, type ReactNode, useMemo, useState } from "react";
import { Helmet } from "react-helmet-async";
import uFuzzy from "ufuzzy";
import { CopyableValue } from "components/CopyableValue/CopyableValue";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Margins } from "components/Margins/Margins";
import {
PageHeader,
PageHeaderSubtitle,
PageHeaderTitle,
} from "components/PageHeader/PageHeader";
import { Stack } from "components/Stack/Stack";
import icons from "theme/icons.json";
import {
defaultParametersForBuiltinIcons,
parseImageParameters,
} from "theme/externalImages";
import { pageTitle } from "utils/page";
const iconsWithoutSuffix = icons.map((icon) => icon.split(".")[0]);
const fuzzyFinder = new uFuzzy({
intraMode: 1,
intraIns: 1,
intraSub: 1,
intraTrn: 1,
intraDel: 1,
});
export const IconsPage: FC = () => {
const theme = useTheme();
const [searchInputText, setSearchInputText] = useState("");
const searchText = searchInputText.trim();
const searchedIcons = useMemo(() => {
if (!searchText) {
return icons.map((icon) => ({ url: `/icon/${icon}`, description: icon }));
}
const [map, info, sorted] = fuzzyFinder.search(
iconsWithoutSuffix,
searchText,
);
// We hit an invalid state somehow
if (!map || !info || !sorted) {
return [];
}
return sorted.map((i) => {
const iconName = icons[info.idx[i]];
const ranges = info.ranges[i];
const nodes: ReactNode[] = [];
let cursor = 0;
for (let j = 0; j < ranges.length; j += 2) {
nodes.push(iconName.slice(cursor, ranges[j]));
nodes.push(
<mark key={j + 1}>{iconName.slice(ranges[j], ranges[j + 1])}</mark>,
);
cursor = ranges[j + 1];
}
nodes.push(iconName.slice(cursor));
return { url: `/icon/${iconName}`, description: nodes };
});
}, [searchText]);
return (
<>
<Helmet>
<title>{pageTitle("Icons")}</title>
</Helmet>
<Margins>
<PageHeader
actions={
<Tooltip
placement="bottom-end"
title={
<p
css={{
padding: 8,
fontSize: 13,
lineHeight: 1.5,
}}
>
You can suggest a new icon by submitting a Pull Request to our
public GitHub repository. Just keep in mind that it should be
relevant to many Coder users, and redistributable under a
permissive license.
</p>
}
>
<Link href="https://github.com/coder/coder/tree/main/site/static/icon">
Suggest an icon
</Link>
</Tooltip>
}
>
<PageHeaderTitle>Icons</PageHeaderTitle>
<PageHeaderSubtitle>
All of the icons included with Coder
</PageHeaderSubtitle>
</PageHeader>
<TextField
size="small"
InputProps={{
"aria-label": "Filter",
name: "query",
placeholder: "Search…",
value: searchInputText,
onChange: (event) => setSearchInputText(event.target.value),
sx: {
borderRadius: "6px",
marginLeft: "-1px",
"& input::placeholder": {
color: theme.palette.text.secondary,
},
"& .MuiInputAdornment-root": {
marginLeft: 0,
},
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon
css={{
fontSize: 14,
color: theme.palette.text.secondary,
}}
/>
</InputAdornment>
),
endAdornment: searchInputText && (
<InputAdornment position="end">
<Tooltip title="Clear filter">
<IconButton
size="small"
onClick={() => setSearchInputText("")}
>
<ClearIcon css={{ fontSize: 14 }} />
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
/>
<Stack
direction="row"
wrap="wrap"
spacing={1}
justifyContent="center"
css={{ marginTop: 32 }}
>
{searchedIcons.length === 0 && (
<EmptyState message="No results matched your search" />
)}
{searchedIcons.map((icon) => (
<CopyableValue key={icon.url} value={icon.url} placement="bottom">
<Stack alignItems="center" css={{ margin: 12 }}>
<img
alt={icon.url}
src={icon.url}
css={[
{
width: 60,
height: 60,
objectFit: "contain",
pointerEvents: "none",
padding: 12,
},
parseImageParameters(
theme.externalImages,
defaultParametersForBuiltinIcons.get(icon.url) ?? "",
),
]}
/>
<figcaption
css={{
width: 88,
height: 48,
fontSize: 13,
textOverflow: "ellipsis",
textAlign: "center",
overflow: "hidden",
}}
>
{icon.description}
</figcaption>
</Stack>
</CopyableValue>
))}
</Stack>
</Margins>
</>
);
};
export default IconsPage;