Skip to content

Commit bda68b1

Browse files
authored
feat: add /icons page (#10093)
1 parent 236e84c commit bda68b1

File tree

5 files changed

+226
-12
lines changed

5 files changed

+226
-12
lines changed

site/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"ts-prune": "0.10.3",
9292
"tzdata": "1.0.30",
9393
"ua-parser-js": "1.0.33",
94+
"ufuzzy": "npm:@leeoniya/ufuzzy@1.0.10",
9495
"unique-names-generator": "4.7.1",
9596
"uuid": "9.0.0",
9697
"vite": "4.4.2",

site/pnpm-lock.yaml

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/AppRouter.tsx

+10-8
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ const TemplateInsightsPage = lazy(
193193
);
194194
const HealthPage = lazy(() => import("./pages/HealthPage/HealthPage"));
195195
const GroupsPage = lazy(() => import("./pages/GroupsPage/GroupsPage"));
196+
const IconsPage = lazy(() => import("./pages/IconsPage/IconsPage"));
196197

197198
export const AppRouter: FC = () => {
198199
return (
@@ -207,21 +208,21 @@ export const AppRouter: FC = () => {
207208
<Route element={<DashboardLayout />}>
208209
<Route index element={<Navigate to="/workspaces" replace />} />
209210

210-
<Route path="health" element={<HealthPage />} />
211+
<Route path="/health" element={<HealthPage />} />
211212

212213
<Route
213-
path="external-auth/:provider"
214+
path="/external-auth/:provider"
214215
element={<ExternalAuthPage />}
215216
/>
216217

217-
<Route path="workspaces" element={<WorkspacesPage />} />
218+
<Route path="/workspaces" element={<WorkspacesPage />} />
218219

219-
<Route path="starter-templates">
220+
<Route path="/starter-templates">
220221
<Route index element={<StarterTemplatesPage />} />
221222
<Route path=":exampleId" element={<StarterTemplatePage />} />
222223
</Route>
223224

224-
<Route path="templates">
225+
<Route path="/templates">
225226
<Route index element={<TemplatesPage />} />
226227
<Route path="new" element={<CreateTemplatePage />} />
227228
<Route path=":template">
@@ -261,7 +262,7 @@ export const AppRouter: FC = () => {
261262
</Route>
262263
</Route>
263264

264-
<Route path="users">
265+
<Route path="/users">
265266
<Route element={<UsersLayout />}>
266267
<Route index element={<UsersPage />} />
267268
</Route>
@@ -302,7 +303,7 @@ export const AppRouter: FC = () => {
302303
/>
303304
</Route>
304305

305-
<Route path="settings" element={<SettingsLayout />}>
306+
<Route path="/settings" element={<SettingsLayout />}>
306307
<Route path="account" element={<AccountPage />} />
307308
<Route path="schedule" element={<SchedulePage />} />
308309
<Route path="security" element={<SecurityPage />} />
@@ -340,7 +341,8 @@ export const AppRouter: FC = () => {
340341
path="/:username/:workspace/terminal"
341342
element={<TerminalPage renderer="webgl" />}
342343
/>
343-
<Route path="cli-auth" element={<CliAuthenticationPage />} />
344+
<Route path="/cli-auth" element={<CliAuthenticationPage />} />
345+
<Route path="/icons" element={<IconsPage />} />
344346
</Route>
345347

346348
{/* Using path="*"" means "match anything", so this route

site/src/components/CopyableValue/CopyableValue.tsx

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1-
import Tooltip from "@mui/material/Tooltip";
1+
import Tooltip, { type TooltipProps } from "@mui/material/Tooltip";
22
import { useClickable } from "hooks/useClickable";
33
import { useClipboard } from "hooks/useClipboard";
4-
import { FC, HTMLProps } from "react";
4+
import { type FC, type HTMLProps } from "react";
55

66
interface CopyableValueProps extends HTMLProps<HTMLDivElement> {
77
value: string;
8+
placement?: TooltipProps["placement"];
9+
PopperProps?: TooltipProps["PopperProps"];
810
}
911

10-
export const CopyableValue: FC<CopyableValueProps> = ({ value, ...props }) => {
12+
export const CopyableValue: FC<CopyableValueProps> = ({
13+
value,
14+
placement = "bottom-start",
15+
PopperProps,
16+
...props
17+
}) => {
1118
const { isCopied, copy } = useClipboard(value);
1219
const clickableProps = useClickable<HTMLSpanElement>(copy);
1320

1421
return (
1522
<Tooltip
1623
title={isCopied ? "Copied!" : "Click to copy"}
17-
placement="bottom-start"
24+
placement={placement}
25+
PopperProps={PopperProps}
1826
>
1927
<span {...props} {...clickableProps} css={{ cursor: "pointer" }} />
2028
</Tooltip>
+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import TextField from "@mui/material/TextField";
2+
import InputAdornment from "@mui/material/InputAdornment";
3+
import Tooltip from "@mui/material/Tooltip";
4+
import IconButton from "@mui/material/IconButton";
5+
import Box from "@mui/material/Box";
6+
import Link from "@mui/material/Link";
7+
import SearchIcon from "@mui/icons-material/SearchOutlined";
8+
import ClearIcon from "@mui/icons-material/CloseOutlined";
9+
import { useTheme } from "@emotion/react";
10+
import { type FC, type ReactNode, useMemo, useState } from "react";
11+
import { Helmet } from "react-helmet-async";
12+
import uFuzzy from "ufuzzy";
13+
import { CopyableValue } from "components/CopyableValue/CopyableValue";
14+
import { EmptyState } from "components/EmptyState/EmptyState";
15+
import { Margins } from "components/Margins/Margins";
16+
import {
17+
PageHeader,
18+
PageHeaderSubtitle,
19+
PageHeaderTitle,
20+
} from "components/PageHeader/PageHeader";
21+
import { Stack } from "components/Stack/Stack";
22+
import icons from "theme/icons.json";
23+
import { pageTitle } from "utils/page";
24+
25+
const iconsWithoutSuffix = icons.map((icon) => icon.split(".")[0]);
26+
const fuzzyFinder = new uFuzzy({
27+
intraMode: 1,
28+
intraIns: 1,
29+
intraSub: 1,
30+
intraTrn: 1,
31+
intraDel: 1,
32+
});
33+
34+
export const IconsPage: FC = () => {
35+
const theme = useTheme();
36+
const [searchInputText, setSearchInputText] = useState("");
37+
const searchText = searchInputText.trim();
38+
39+
const searchedIcons = useMemo(() => {
40+
if (!searchText) {
41+
return icons.map((icon) => ({ url: `/icon/${icon}`, description: icon }));
42+
}
43+
44+
const [map, info, sorted] = fuzzyFinder.search(
45+
iconsWithoutSuffix,
46+
searchText,
47+
);
48+
49+
// We hit an invalid state somehow
50+
if (!map || !info || !sorted) {
51+
return [];
52+
}
53+
54+
return sorted.map((i) => {
55+
const iconName = icons[info.idx[i]];
56+
const ranges = info.ranges[i];
57+
58+
const nodes: ReactNode[] = [];
59+
let cursor = 0;
60+
for (let j = 0; j < ranges.length; j += 2) {
61+
nodes.push(iconName.slice(cursor, ranges[j]));
62+
nodes.push(
63+
<mark key={j + 1}>{iconName.slice(ranges[j], ranges[j + 1])}</mark>,
64+
);
65+
cursor = ranges[j + 1];
66+
}
67+
nodes.push(iconName.slice(cursor));
68+
return { url: `/icon/${iconName}`, description: nodes };
69+
});
70+
}, [searchText]);
71+
72+
return (
73+
<>
74+
<Helmet>
75+
<title>{pageTitle("Icons")}</title>
76+
</Helmet>
77+
<Margins>
78+
<PageHeader
79+
actions={
80+
<Tooltip
81+
placement="bottom-end"
82+
title={
83+
<Box
84+
css={{
85+
padding: theme.spacing(1),
86+
fontSize: 13,
87+
lineHeight: 1.5,
88+
}}
89+
>
90+
You can suggest a new icon by submitting a Pull Request to our
91+
public GitHub repository. Just keep in mind that it should be
92+
relevant to many Coder users, and redistributable under a
93+
permissive license.
94+
</Box>
95+
}
96+
>
97+
<Link href="https://github.com/coder/coder/tree/main/site/static/icon">
98+
Suggest an icon
99+
</Link>
100+
</Tooltip>
101+
}
102+
>
103+
<PageHeaderTitle>Icons</PageHeaderTitle>
104+
<PageHeaderSubtitle>
105+
All of the icons included with Coder
106+
</PageHeaderSubtitle>
107+
</PageHeader>
108+
<TextField
109+
size="small"
110+
InputProps={{
111+
"aria-label": "Filter",
112+
name: "query",
113+
placeholder: "Search…",
114+
value: searchInputText,
115+
onChange: (event) => setSearchInputText(event.target.value),
116+
sx: {
117+
borderRadius: "6px",
118+
marginLeft: "-1px",
119+
"& input::placeholder": {
120+
color: theme.palette.text.secondary,
121+
},
122+
"& .MuiInputAdornment-root": {
123+
marginLeft: 0,
124+
},
125+
},
126+
startAdornment: (
127+
<InputAdornment position="start">
128+
<SearchIcon
129+
sx={{
130+
fontSize: 14,
131+
color: theme.palette.text.secondary,
132+
}}
133+
/>
134+
</InputAdornment>
135+
),
136+
endAdornment: searchInputText && (
137+
<InputAdornment position="end">
138+
<Tooltip title="Clear filter">
139+
<IconButton
140+
size="small"
141+
onClick={() => setSearchInputText("")}
142+
>
143+
<ClearIcon sx={{ fontSize: 14 }} />
144+
</IconButton>
145+
</Tooltip>
146+
</InputAdornment>
147+
),
148+
}}
149+
/>
150+
151+
<Stack
152+
direction="row"
153+
wrap="wrap"
154+
spacing={1}
155+
justifyContent="center"
156+
css={(theme) => ({ marginTop: theme.spacing(4) })}
157+
>
158+
{searchedIcons.length === 0 && (
159+
<EmptyState message="No results matched your search" />
160+
)}
161+
{searchedIcons.map((icon) => (
162+
<CopyableValue key={icon.url} value={icon.url} placement="bottom">
163+
<Stack alignItems="center" css={{ margin: theme.spacing(1.5) }}>
164+
<img
165+
alt={icon.url}
166+
src={icon.url}
167+
css={{
168+
width: 60,
169+
height: 60,
170+
objectFit: "contain",
171+
pointerEvents: "none",
172+
padding: theme.spacing(1.5),
173+
}}
174+
/>
175+
<figcaption
176+
css={{
177+
width: 88,
178+
height: 48,
179+
fontSize: 13,
180+
textOverflow: "ellipsis",
181+
textAlign: "center",
182+
overflow: "hidden",
183+
}}
184+
>
185+
{icon.description}
186+
</figcaption>
187+
</Stack>
188+
</CopyableValue>
189+
))}
190+
</Stack>
191+
</Margins>
192+
</>
193+
);
194+
};
195+
196+
export default IconsPage;

0 commit comments

Comments
 (0)