Skip to content

Commit b787093

Browse files
committed
feat: add /icons page
1 parent 71ad590 commit b787093

File tree

5 files changed

+216
-12
lines changed

5 files changed

+216
-12
lines changed

site/package.json

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

0 commit comments

Comments
 (0)