Skip to content

Commit 0c6e055

Browse files
authored
chore: Improves test pages (copied from chart-components) (#81)
* chore: Improves test pages (copied from chart-components) * remove unused code and employ page settings
1 parent ea1951d commit 0c6e055

18 files changed

+561
-281
lines changed

pages/app/app-context.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { createContext, useEffect } from "react";
5+
import { useSearchParams } from "react-router-dom";
6+
import mapValues from "lodash/mapValues";
7+
8+
import { applyDensity, applyMode, Density, disableMotion, Mode } from "@cloudscape-design/global-styles";
9+
10+
interface AppUrlParams {
11+
mode: Mode;
12+
density: Density;
13+
direction: "ltr" | "rtl";
14+
motionDisabled: boolean;
15+
i18n: boolean;
16+
}
17+
18+
export interface AppContextType<T = unknown> {
19+
pageId?: string;
20+
urlParams: AppUrlParams & T;
21+
setUrlParams: (newParams: Partial<AppUrlParams & T>) => void;
22+
}
23+
24+
const appContextDefaults: AppContextType = {
25+
pageId: undefined,
26+
urlParams: {
27+
mode: Mode.Light,
28+
density: Density.Comfortable,
29+
direction: "ltr",
30+
motionDisabled: false,
31+
i18n: true,
32+
},
33+
setUrlParams: () => {},
34+
};
35+
36+
const AppContext = createContext<AppContextType>(appContextDefaults);
37+
38+
export default AppContext;
39+
40+
function parseQuery(urlParams: URLSearchParams) {
41+
const queryParams: Record<string, any> = { ...appContextDefaults.urlParams };
42+
urlParams.forEach((value, key) => (queryParams[key] = value));
43+
44+
return mapValues(queryParams, (value) => {
45+
if (value === "true" || value === "false") {
46+
return value === "true";
47+
}
48+
return value;
49+
});
50+
}
51+
52+
function formatQuery(params: AppUrlParams) {
53+
const query: Record<string, string> = {};
54+
for (const [key, value] of Object.entries(params)) {
55+
if (value === appContextDefaults.urlParams[key as keyof AppUrlParams]) {
56+
continue;
57+
}
58+
query[key as keyof AppUrlParams] = String(value);
59+
}
60+
return query;
61+
}
62+
63+
export function AppContextProvider({ children }: { children: React.ReactNode }) {
64+
const [searchParams, setSearchParams] = useSearchParams();
65+
const urlParams = parseQuery(searchParams) as AppUrlParams;
66+
67+
function setUrlParams(newParams: Partial<AppUrlParams>) {
68+
setSearchParams(formatQuery({ ...urlParams, ...newParams }));
69+
70+
if ((newParams.direction ?? "ltr") !== (urlParams.direction ?? "ltr")) {
71+
window.location.reload();
72+
}
73+
}
74+
75+
useEffect(() => {
76+
applyMode(urlParams.mode);
77+
}, [urlParams.mode]);
78+
79+
useEffect(() => {
80+
applyDensity(urlParams.density);
81+
}, [urlParams.density]);
82+
83+
useEffect(() => {
84+
disableMotion(urlParams.motionDisabled);
85+
}, [urlParams.motionDisabled]);
86+
87+
document.documentElement.setAttribute("dir", urlParams.direction);
88+
89+
return <AppContext.Provider value={{ urlParams, setUrlParams: setUrlParams }}>{children}</AppContext.Provider>;
90+
}

pages/app/index.tsx

Lines changed: 182 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,198 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
import { HashRouter, Link, Route, Routes, useLocation } from "react-router-dom";
43

5-
import { pages } from "../pages";
6-
import Page from "./page";
4+
import { Suspense, useContext } from "react";
5+
import { HashRouter, Route, Routes, useHref, useLocation, useNavigate, useSearchParams } from "react-router-dom";
6+
7+
import Alert from "@cloudscape-design/components/alert";
8+
import AppLayout from "@cloudscape-design/components/app-layout";
9+
import Box from "@cloudscape-design/components/box";
10+
import { I18nProvider } from "@cloudscape-design/components/i18n";
11+
import enMessages from "@cloudscape-design/components/i18n/messages/all.en";
12+
import Link, { LinkProps } from "@cloudscape-design/components/link";
13+
import Spinner from "@cloudscape-design/components/spinner";
14+
import TopNavigation from "@cloudscape-design/components/top-navigation";
15+
import { Density, Mode } from "@cloudscape-design/global-styles";
16+
17+
import AppContext, { AppContextProvider } from "./app-context";
18+
import { pages, pagesMap } from "./pages";
19+
20+
import "@cloudscape-design/global-styles/index.css";
721

822
export default function App() {
923
return (
1024
<HashRouter>
25+
<AppContextProvider>
26+
<AppBody />
27+
</AppContextProvider>
28+
</HashRouter>
29+
);
30+
}
31+
32+
function AppBody() {
33+
const { urlParams } = useContext(AppContext);
34+
const routes = (
35+
<>
36+
<Navigation />
1137
<Routes>
12-
<Route path="/" element={<Start />} />
38+
<Route path="/" element={<IndexPage />} />
1339
<Route path="/*" element={<PageWithFallback />} />
1440
</Routes>
15-
</HashRouter>
41+
</>
42+
);
43+
return urlParams.i18n ? (
44+
<I18nProvider locale="en" messages={[enMessages]}>
45+
{routes}
46+
</I18nProvider>
47+
) : (
48+
routes
49+
);
50+
}
51+
52+
function Page({ pageId }: { pageId: string }) {
53+
const Component = pagesMap[pageId];
54+
return (
55+
<Suspense fallback={<Spinner />}>
56+
{Component ? (
57+
<Component />
58+
) : (
59+
<AppLayout
60+
headerSelector="#h"
61+
navigationHide={true}
62+
toolsHide={true}
63+
content={<Alert type="error">Page not found</Alert>}
64+
/>
65+
)}
66+
</Suspense>
67+
);
68+
}
69+
70+
function Navigation() {
71+
const navigate = useNavigate();
72+
const [searchParams] = useSearchParams();
73+
const { urlParams, setUrlParams } = useContext(AppContext);
74+
const isDarkMode = urlParams.mode === Mode.Dark;
75+
const isCompactMode = urlParams.density === Density.Compact;
76+
const isRtl = urlParams.direction === "rtl";
77+
return (
78+
<header id="h" style={{ position: "sticky", insetBlockStart: 0, zIndex: 1002 }}>
79+
<TopNavigation
80+
identity={{
81+
title: "Chat components - dev pages",
82+
href: "#",
83+
onFollow: () => navigate(`/?${searchParams.toString()}`),
84+
}}
85+
utilities={[
86+
{
87+
type: "menu-dropdown",
88+
text: "Settings",
89+
iconName: "settings",
90+
items: [
91+
{ id: "dark-mode", text: isDarkMode ? "Set light mode" : "Set dark mode" },
92+
{ id: "compact-mode", text: isCompactMode ? "Set comfortable mode" : "Set compact mode" },
93+
{ id: "rtl", text: isRtl ? "Set left to right text" : "Set right to left text" },
94+
{ id: "motion", text: urlParams.motionDisabled ? "Enable motion" : "Disable motion" },
95+
{ id: "i18n", text: urlParams.i18n ? "Disable built-in i18n" : "Enable built-in i18n" },
96+
],
97+
onItemClick({ detail }) {
98+
switch (detail.id) {
99+
case "dark-mode":
100+
return setUrlParams({ mode: isDarkMode ? Mode.Light : Mode.Dark });
101+
case "compact-mode":
102+
return setUrlParams({ density: isCompactMode ? Density.Comfortable : Density.Compact });
103+
case "rtl":
104+
return setUrlParams({ direction: isRtl ? "ltr" : "rtl" });
105+
case "motion":
106+
return setUrlParams({ motionDisabled: !urlParams.motionDisabled });
107+
case "i18n":
108+
return setUrlParams({ i18n: !urlParams.i18n });
109+
}
110+
},
111+
},
112+
]}
113+
/>
114+
</header>
16115
);
17116
}
18117

19-
const Start = () => (
20-
<>
21-
<h1>Pages</h1>
22-
<main>
23-
<Index />
24-
</main>
25-
</>
26-
);
27-
28-
const Index = () => (
29-
<ul className="list">
30-
{pages.map((page) => (
31-
<li key={page}>
32-
<Link to={`${page}`}>{page}</Link>
33-
</li>
34-
))}
35-
</ul>
36-
);
118+
interface TreeItem {
119+
name: string;
120+
href?: string;
121+
items: TreeItem[];
122+
level: number;
123+
}
124+
125+
function IndexPage() {
126+
const tree = createPagesTree(pages);
127+
return (
128+
<AppLayout
129+
headerSelector="#h"
130+
navigationHide={true}
131+
toolsHide={true}
132+
content={
133+
<Box>
134+
<h1>Welcome!</h1>
135+
<p>Select a page:</p>
136+
<ul>
137+
{tree.items.map((item) => (
138+
<TreeItemView key={item.name} item={item} />
139+
))}
140+
</ul>
141+
</Box>
142+
}
143+
/>
144+
);
145+
}
146+
147+
function createPagesTree(pages: string[]) {
148+
const tree: TreeItem = { name: ".", items: [], level: 0 };
149+
function putInTree(segments: string[], node: TreeItem, item: string, level = 1) {
150+
if (segments.length === 0) {
151+
node.href = item;
152+
} else {
153+
let match = node.items.filter((item) => item.name === segments[0])[0];
154+
if (!match) {
155+
match = { name: segments[0], items: [], level };
156+
node.items.push(match);
157+
}
158+
putInTree(segments.slice(1), match, item, level + 1);
159+
// make directories to be displayed above files
160+
node.items.sort((a, b) => Math.min(b.items.length, 1) - Math.min(a.items.length, 1));
161+
}
162+
}
163+
for (const page of pages) {
164+
const segments = page.slice(1).split("/");
165+
putInTree(segments, tree, page);
166+
}
167+
return tree;
168+
}
169+
170+
function TreeItemView({ item }: { item: TreeItem }) {
171+
return (
172+
<li>
173+
{item.href ? (
174+
<RouterLink to={item.href}>{item.name}</RouterLink>
175+
) : (
176+
<Box variant={item.level === 1 ? "h2" : "h3"}>{item.name}</Box>
177+
)}
178+
<ul style={{ marginBlock: 0, marginInline: 0 }}>
179+
{item.items.map((item) => (
180+
<TreeItemView key={item.name} item={item} />
181+
))}
182+
</ul>
183+
</li>
184+
);
185+
}
186+
187+
function RouterLink({ to, children, ...rest }: LinkProps & { to: string }) {
188+
const [searchParams] = useSearchParams();
189+
const href = useHref(to);
190+
return (
191+
<Link href={`${href}?${searchParams.toString()}`} {...rest}>
192+
{children}
193+
</Link>
194+
);
195+
}
37196

38197
const PageWithFallback = () => {
39198
const { pathname: page } = useLocation();

pages/app/page.tsx

Lines changed: 0 additions & 56 deletions
This file was deleted.

pages/app/pages.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { lazy } from "react";
5+
6+
const pagesRaw = import.meta.glob("../**/*.page.tsx");
7+
const pageIdRegex = /([\w-/]+)\.page\.tsx/;
8+
const getPage = (path: string) => path.match(pageIdRegex)![1];
9+
10+
export const pages = Object.keys(pagesRaw).map(getPage);
11+
12+
type ComponentFactory = Parameters<typeof lazy>[0];
13+
14+
export const pagesMap = Object.fromEntries(
15+
Object.entries(pagesRaw).map(([path, dynamicImport]) => {
16+
const match = getPage(path);
17+
return [match, lazy(dynamicImport as ComponentFactory)];
18+
}),
19+
);

0 commit comments

Comments
 (0)