Skip to content
This repository was archived by the owner on May 3, 2025. It is now read-only.

Commit f39ea47

Browse files
Help dialog route (#130)
* Port over logic to load Usage markdown in new UsagePage * Add timeout render hook to usage page to ensure page doesn't get stuck * Implement simple hook to scroll to hash in route if present * Add 'CopyableHeading' component used in markdown renderer for copy-to-clipboard * Close ProfileMenu when About or Help dialogs are clicked * Reinstate scrolling to hash - needs to be called on UsagePage I believe since the components haven't been rendered yet in ApplicationLayout * Add button to open help page in new tab at top of dialog * Remove unused import
1 parent 273d4bc commit f39ea47

File tree

12 files changed

+325
-29
lines changed

12 files changed

+325
-29
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Flex } from "components/flex";
2+
import {
3+
Heading,
4+
HeadingProps,
5+
IconButton,
6+
LinkIcon,
7+
majorScale,
8+
TickIcon,
9+
toaster,
10+
} from "evergreen-ui";
11+
import { isEmpty, isString, kebabCase } from "lodash";
12+
import { useCallback, useMemo, useState } from "react";
13+
14+
interface CopyableHeadingProps extends HeadingProps {}
15+
16+
const CopyableHeading: React.FC<CopyableHeadingProps> = (
17+
props: CopyableHeadingProps
18+
) => {
19+
const { children } = props;
20+
const [copied, setCopied] = useState<boolean>(false);
21+
22+
const hash: string | undefined = useMemo(() => {
23+
let hash: string | undefined = isString(children)
24+
? children
25+
: undefined;
26+
27+
if (Array.isArray(children) && children.some(isString)) {
28+
hash = children.find(isString) as string;
29+
}
30+
31+
return isEmpty(hash) ? undefined : kebabCase(hash);
32+
}, [children]);
33+
34+
const handleClick = useCallback(() => {
35+
if (isEmpty(hash)) {
36+
toaster.danger("Failed to copy link!");
37+
return;
38+
}
39+
40+
const { hash: existingHash } = window.location;
41+
const currentPath = window.location
42+
.toString()
43+
.replace(existingHash, "");
44+
45+
const link = `${currentPath}#${hash}`;
46+
setCopied(true);
47+
navigator.clipboard.writeText(link);
48+
setTimeout(() => setCopied(false), 1500);
49+
}, [hash]);
50+
51+
return (
52+
<Flex.Row alignItems="center">
53+
<IconButton
54+
appearance="minimal"
55+
icon={copied ? TickIcon : LinkIcon}
56+
intent={copied ? "success" : "none"}
57+
marginRight={majorScale(1)}
58+
onClick={handleClick}
59+
size="small"
60+
/>
61+
<Heading {...props} />
62+
</Flex.Row>
63+
);
64+
};
65+
66+
export { CopyableHeading };

src/components/layouts/application-layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { SidebarNavigation } from "components/sidebar/sidebar-navigation";
33
import { Pane } from "evergreen-ui";
44
import { RouteProps } from "interfaces/route-props";
55
import { useSubscribeToAuthStatus } from "utils/hooks/supabase/use-subscribe-to-auth-status";
6+
import { useScrollToHash } from "utils/hooks/use-scroll-to-hash";
67

78
interface ApplicationLayoutProps extends RouteProps {}
89

@@ -11,6 +12,7 @@ const ApplicationLayout: React.FC<ApplicationLayoutProps> = (
1112
) => {
1213
const { route } = props;
1314
useSubscribeToAuthStatus();
15+
useScrollToHash();
1416

1517
return (
1618
<Pane display="flex" flexDirection="row" height="100%" width="100%">
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { majorScale, Pane, Tab, TabNavigation } from "evergreen-ui";
2+
import { RouteProps } from "interfaces/route-props";
3+
import { NestedRoutes } from "components/nested-routes";
4+
import { useHistory, useLocation } from "react-router";
5+
import { useCallback } from "react";
6+
import { Sitemap } from "sitemap";
7+
import upath from "upath";
8+
import { HelpResource } from "enums/help-resource";
9+
10+
interface HelpLayoutProps extends RouteProps {}
11+
12+
const tabs = Object.values(HelpResource);
13+
14+
const HelpLayout: React.FC<HelpLayoutProps> = (props: HelpLayoutProps) => {
15+
const { route } = props;
16+
const location = useLocation();
17+
const history = useHistory();
18+
const isTabSelected = useCallback(
19+
(tab: HelpResource): boolean =>
20+
location.pathname.endsWith(tab.toLowerCase()),
21+
[location]
22+
);
23+
const handleClick = useCallback(
24+
(tab: HelpResource) => () => {
25+
history.push(upath.join(Sitemap.help.home, tab.toLowerCase()));
26+
},
27+
[history]
28+
);
29+
30+
return (
31+
<Pane height="100%" overflow="auto" width="100%">
32+
<Pane marginLeft={majorScale(2)} marginTop={majorScale(2)}>
33+
<TabNavigation>
34+
{tabs.map((tab) => (
35+
<Tab
36+
isSelected={isTabSelected(tab)}
37+
key={tab}
38+
onSelect={handleClick(tab)}>
39+
{tab}
40+
</Tab>
41+
))}
42+
</TabNavigation>
43+
<Pane margin={majorScale(2)}>
44+
<NestedRoutes route={route} />
45+
</Pane>
46+
</Pane>
47+
</Pane>
48+
);
49+
};
50+
51+
export { HelpLayout };

src/components/markdown.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
Heading,
32
Link,
43
Image,
54
Pane,
@@ -20,6 +19,7 @@ import {
2019
} from "react-markdown/lib/ast-to-react";
2120
import ReactMarkdown from "react-markdown";
2221
import { useMemo } from "react";
22+
import { CopyableHeading } from "components/copyable-heading";
2323

2424
export type MarkdownComponentMap = Partial<
2525
Omit<NormalComponents, keyof SpecialComponents> & SpecialComponents
@@ -39,10 +39,18 @@ const defaultComponents: MarkdownComponentMap = {
3939
/>
4040
),
4141
h3: (props) => (
42-
<Heading {...omitIs(props)} marginY={majorScale(2)} size={600} />
42+
<CopyableHeading
43+
{...omitIs(props)}
44+
marginY={majorScale(2)}
45+
size={600}
46+
/>
4347
),
4448
h4: (props) => (
45-
<Heading {...omitIs(props)} marginY={majorScale(2)} size={500} />
49+
<CopyableHeading
50+
{...omitIs(props)}
51+
marginY={majorScale(2)}
52+
size={500}
53+
/>
4654
),
4755
img: (props) => (
4856
<Pane marginY={majorScale(2)}>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Markdown } from "components/markdown";
2+
import { HelpResource } from "enums/help-resource";
3+
import { Spinner } from "evergreen-ui";
4+
import React from "react";
5+
import { useHelpDocs } from "utils/hooks/use-help-docs";
6+
import { useScrollToHash } from "utils/hooks/use-scroll-to-hash";
7+
import { useTimeoutRender } from "utils/hooks/use-timeout-render";
8+
9+
interface UsagePageProps {}
10+
11+
const UsagePage: React.FC<UsagePageProps> = (props: UsagePageProps) => {
12+
const { isLoading, content } = useHelpDocs({
13+
resource: HelpResource.Usage,
14+
});
15+
useTimeoutRender();
16+
useScrollToHash();
17+
18+
return (
19+
<React.Fragment>
20+
{isLoading && <Spinner />}
21+
{!isLoading && <Markdown>{content}</Markdown>}
22+
</React.Fragment>
23+
);
24+
};
25+
26+
export { UsagePage };
Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,55 @@
11
import React, { useCallback, useState } from "react";
22
import { Dialog, DialogProps } from "components/dialog";
3-
import UsageMarkdown from "docs/usage.md";
4-
import { useQuery } from "utils/hooks/use-query";
5-
import { Spinner, Tablist, Tab, IconButton, CrossIcon } from "evergreen-ui";
3+
import {
4+
Spinner,
5+
Tablist,
6+
Tab,
7+
IconButton,
8+
CrossIcon,
9+
MinimizeIcon,
10+
MaximizeIcon,
11+
majorScale,
12+
ShareIcon,
13+
} from "evergreen-ui";
614
import { Markdown } from "components/markdown";
715
import { Flex } from "components/flex";
16+
import { HelpResource } from "enums/help-resource";
17+
import { useHelpDocs } from "utils/hooks/use-help-docs";
18+
import { useBoolean } from "utils/hooks/use-boolean";
19+
import { Sitemap } from "sitemap";
20+
import upath from "upath";
821

922
interface HelpDialogProps extends Pick<DialogProps, "onCloseComplete"> {}
1023

11-
enum HelpTab {
12-
Usage = "Usage",
13-
}
14-
15-
const tabs = [HelpTab.Usage];
24+
const tabs = [HelpResource.Usage];
1625

1726
const HelpDialog: React.FC<HelpDialogProps> = (props: HelpDialogProps) => {
1827
const { onCloseComplete } = props;
19-
const [selectedTab, setSelectedTab] = useState<HelpTab>(HelpTab.Usage);
20-
const { resultObject: markdownContent, isLoading } = useQuery<string>({
21-
fn: async () => {
22-
const response = await fetch(UsageMarkdown);
23-
const content = await response.text();
24-
return sanitizeContent(content);
25-
},
26-
});
28+
const [selectedTab, setSelectedTab] = useState<HelpResource>(
29+
HelpResource.Usage
30+
);
31+
const { value: isFullscreen, toggle: handleFullscreenClick } = useBoolean();
32+
const { isLoading, content } = useHelpDocs({ resource: selectedTab });
33+
34+
const handleShareClick = useCallback(() => {
35+
window.open(upath.join(Sitemap.help.home, selectedTab.toLowerCase()));
36+
}, [selectedTab]);
2737

2838
const handleTabSelected = useCallback(
29-
(tab: HelpTab) => () => setSelectedTab(tab),
39+
(tab: HelpResource) => () => setSelectedTab(tab),
3040
[]
3141
);
3242

3343
return (
3444
<Dialog
45+
containerProps={
46+
isFullscreen
47+
? {
48+
marginY: majorScale(2),
49+
maxHeight: `calc(100% - ${majorScale(4)}px)`,
50+
}
51+
: undefined
52+
}
3553
hasFooter={false}
3654
header={({ close }) => (
3755
<Flex.Row alignItems="center" width="100%">
@@ -47,23 +65,32 @@ const HelpDialog: React.FC<HelpDialogProps> = (props: HelpDialogProps) => {
4765
</Tablist>
4866
<IconButton
4967
appearance="minimal"
50-
icon={CrossIcon}
68+
icon={ShareIcon}
5169
marginLeft="auto"
70+
onClick={handleShareClick}
71+
/>
72+
<IconButton
73+
appearance="minimal"
74+
icon={isFullscreen ? MinimizeIcon : MaximizeIcon}
75+
marginLeft={majorScale(1)}
76+
onClick={handleFullscreenClick}
77+
/>
78+
<IconButton
79+
appearance="minimal"
80+
icon={CrossIcon}
81+
marginLeft={majorScale(1)}
5282
onClick={close}
5383
/>
5484
</Flex.Row>
5585
)}
5686
isShown={true}
5787
onCloseComplete={onCloseComplete}
5888
title="Usage"
59-
width="60%">
89+
width={isFullscreen ? "100%" : undefined}>
6090
{isLoading && <Spinner />}
61-
{!isLoading && <Markdown>{markdownContent!}</Markdown>}
91+
{!isLoading && <Markdown>{content}</Markdown>}
6292
</Dialog>
6393
);
6494
};
6595

66-
const sanitizeContent = (content: string): string =>
67-
content.replace("# Usage", "");
68-
6996
export { HelpDialog };

src/components/sidebar/profile-menu.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ const ProfileMenu: React.FC<ProfileMenuProps> = (props: ProfileMenuProps) => {
3535
const { mutate: logout } = useLogout({ onSettled: handleLogoutsettled });
3636
const history = useHistory();
3737

38+
const handleAboutDialogClick = useCallback(() => {
39+
onClose();
40+
onAboutDialogClick();
41+
}, [onAboutDialogClick, onClose]);
42+
43+
const handleHelpDialogClick = useCallback(() => {
44+
onClose();
45+
onHelpDialogClick();
46+
}, [onClose, onHelpDialogClick]);
47+
3848
const handleLogoutSelect = useCallback(() => {
3949
onClose();
4050
setTimeout(logout, 25); // Slight delay to allow popover to close
@@ -52,10 +62,10 @@ const ProfileMenu: React.FC<ProfileMenuProps> = (props: ProfileMenuProps) => {
5262

5363
return (
5464
<Menu>
55-
<Menu.Item icon={InfoSignIcon} onSelect={onAboutDialogClick}>
65+
<Menu.Item icon={InfoSignIcon} onSelect={handleAboutDialogClick}>
5666
About
5767
</Menu.Item>
58-
<Menu.Item icon={HelpIcon} onSelect={onHelpDialogClick}>
68+
<Menu.Item icon={HelpIcon} onSelect={handleHelpDialogClick}>
5969
Help
6070
</Menu.Item>
6171
{isAuthenticated && (

src/enums/help-resource.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
enum HelpResource {
2+
Usage = "Usage",
3+
}
4+
5+
export { HelpResource };

src/routes.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,29 @@ import { LoginPage } from "components/pages/login-page";
66
import { LogoutPage } from "components/pages/logout-page";
77
import { RegisterPage } from "components/pages/register-page";
88
import { WorkstationPage } from "components/pages/workstation-page";
9-
import { HomeIcon, LogInIcon, LogOutIcon, MusicIcon } from "evergreen-ui";
9+
import {
10+
HelpIcon,
11+
HomeIcon,
12+
LogInIcon,
13+
LogOutIcon,
14+
MusicIcon,
15+
} from "evergreen-ui";
1016
import { Sitemap } from "sitemap";
1117
import { RouteDefinition } from "interfaces/route-definition";
1218
import { RouteMap as GenericRouteMap } from "interfaces/route-map";
1319
import { InstrumentsPage } from "components/pages/instruments-page";
20+
import { HelpLayout } from "components/layouts/help-layout";
21+
import { UsagePage } from "components/pages/usage-page";
22+
import { HelpResource } from "enums/help-resource";
1423

1524
export interface RouteMap extends GenericRouteMap {
1625
root: RouteDefinition & {
1726
routes: {
27+
help: RouteDefinition & {
28+
routes: {
29+
usage: RouteDefinition;
30+
};
31+
};
1832
library: RouteDefinition & {
1933
routes: {
2034
files: RouteDefinition;
@@ -40,6 +54,21 @@ const Routes: RouteMap = {
4054
name: "ApplicationLayout",
4155
path: Sitemap.home,
4256
routes: {
57+
help: {
58+
component: HelpLayout,
59+
exact: false,
60+
icon: HelpIcon,
61+
name: "Help",
62+
path: Sitemap.help.home,
63+
routes: {
64+
usage: {
65+
component: UsagePage,
66+
exact: true,
67+
name: HelpResource.Usage,
68+
path: Sitemap.help.usage,
69+
},
70+
},
71+
},
4372
library: {
4473
component: LibraryLayout,
4574
exact: false,

0 commit comments

Comments
 (0)