Skip to content

Commit

Permalink
Add route-to-module map to dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Jun 13, 2024
1 parent 7253c62 commit 36aef47
Showing 1 changed file with 78 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Card, CardContent } from '@hypothesis/frontend-shared';
import type { ComponentChildren } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import { useParams, useRoute } from 'wouter-preact';
import type { ComponentChildren, FunctionalComponent } from 'preact';
import { useEffect, useMemo, useState } from 'preact/hooks';
import type { Parser } from 'wouter-preact';
import { useLocation, useParams, useRouter } from 'wouter-preact';

import type { ConfigObject, Ensure } from '../config';
import { useConfig } from '../config';
Expand All @@ -14,40 +15,93 @@ export type LoaderOptions = {
signal: AbortSignal;
};

type RouteModule = {
loader: (opts: LoaderOptions) => unknown;
default: FunctionalComponent;
};

const matchRoute = (parser: Parser, route: string, path: string) => {
const { pattern, keys } = parser(route);

// array destructuring loses keys, so this is done in two steps
const result = pattern.exec(path) || [];

// when parser is in "loose" mode, `$base` is equal to the
// first part of the route that matches the pattern
// (e.g. for pattern `/a/:b` and path `/a/1/2/3` the `$base` is `a/1`)
// we use this for route nesting
const [$base, ...matches] = result;

if ($base === undefined) {
return [false, null];
}

// an object with parameters matched, e.g. { foo: "bar" } for "/:foo"
// we "zip" two arrays here to construct the object
// ["foo"], ["bar"] → { foo: "bar" }
const groups = Object.fromEntries(keys.map((key, i) => [key, matches[i]]));

// convert the array to an instance of object
// this makes it easier to integrate with the existing param implementation
const obj = { ...matches };
// merge named capture groups with matches array
Object.assign(obj, groups);

return [true, obj];
};

function useRouteModule(
routeToModuleMap: Map<string, () => Promise<RouteModule>>,
) {
const { parser } = useRouter();
const [location] = useLocation();

return useMemo(() => {
for (const [route, moduleResolver] of routeToModuleMap) {
const [matches, params] = matchRoute(parser, route, location);
if (matches) {
return { module: moduleResolver(), params: params ?? {} };
}
}

return undefined;
}, [location, parser, routeToModuleMap]);
}

const routesMap = new Map([
[
'/assignments/:assignmentId',
() => import('./dashboard/AssignmentActivity') as Promise<RouteModule>,
],
[
'/courses/:courseId',
() => import('./dashboard/CourseActivity') as Promise<RouteModule>,
],
[
'',
() => import('./dashboard/OrganizationActivity') as Promise<RouteModule>,
],
]);

export default function ComponentWithLoaderWrapper() {
const config = useConfig(['dashboard', 'api']);
const [component, setComponent] = useState<ComponentChildren>();
const [loading, setLoading] = useState(true);
const [fatalError, setFatalError] = useState<ErrorLike>();

const [isAssignment, assignmentParams] = useRoute(
'/assignments/:assignmentId',
);
const [isCourse, courseParams] = useRoute('/courses/:courseId');
const [isHome] = useRoute('');
const globalParams = useParams();
const assignmentId = assignmentParams?.assignmentId ?? '';
const courseId = courseParams?.courseId ?? '';
const organizationId = globalParams.organizationId ?? '';
const { organizationId } = useParams<{ organizationId: string }>();
const routeModule = useRouteModule(routesMap);

useEffect(() => {
const loaderModule = isAssignment
? import('./dashboard/AssignmentActivity')
: isCourse
? import('./dashboard/CourseActivity')
: import('./dashboard/OrganizationActivity');
const params = { assignmentId, courseId, organizationId };

const abortController = new AbortController();
loaderModule.then(async ({ loader, default: Component }) => {
routeModule?.module.then(async ({ loader, default: Component }) => {
setLoading(true);
try {
const loaderResult = await loader({
config,
params,
params: { ...routeModule.params, organizationId },
signal: abortController.signal,
});
setComponent(<Component loaderResult={loaderResult} params={params} />);
setComponent(<Component loaderResult={loaderResult} />);
} catch (e) {
setFatalError(e);
} finally {
Expand All @@ -56,7 +110,7 @@ export default function ComponentWithLoaderWrapper() {
});

return () => abortController.abort();
}, [assignmentId, courseId, organizationId, config, isAssignment, isCourse]);
}, [config, organizationId, routeModule]);

if (fatalError) {
return (
Expand Down

0 comments on commit 36aef47

Please sign in to comment.