/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { setupI18n } from "@gnu-taler/taler-util";
import {
ComponentChild,
ComponentChildren,
Fragment,
FunctionalComponent,
FunctionComponent,
h,
JSX,
render,
VNode,
} from "preact";
import { useEffect, useErrorBoundary, useState } from "preact/hooks";
import { ExampleItemSetup } from "./tests/hook.js";
const Page: FunctionalComponent = ({ children }): VNode => {
return (
{children}
);
};
const SideBar: FunctionalComponent<{ width: number }> = ({
width,
children,
}): VNode => {
return (
{children}
);
};
const ResizeHandleDiv: FunctionalComponent<
JSX.HTMLAttributes
> = ({ children, ...props }): VNode => {
return (
{children}
);
};
const Content: FunctionalComponent = ({ children }): VNode => {
return (
{children}
);
};
function findByGroupComponentName(
allExamples: Group[],
group: string,
component: string,
name: string,
): ExampleItem | undefined {
const gl = allExamples.filter((e) => e.title === group);
if (gl.length === 0) {
return undefined;
}
const cl = gl[0].list.filter((l) => l.name === component);
if (cl.length === 0) {
return undefined;
}
const el = cl[0].examples.filter((c) => c.name === name);
if (el.length === 0) {
return undefined;
}
return el[0];
}
function getContentForExample(
item: ExampleItem | undefined,
allExamples: Group[],
): FunctionalComponent {
if (!item)
return function SelectExampleMessage() {
return select example from the list on the left
;
};
const example = findByGroupComponentName(
allExamples,
item.group,
item.component,
item.name,
);
if (!example) {
return function ExampleNotFoundMessage() {
return example not found
;
};
}
return () => example.render.component(example.render.props);
}
function ExampleList({
name,
list,
selected,
onSelectStory,
}: {
name: string;
list: {
name: string;
examples: ExampleItem[];
}[];
selected: ExampleItem | undefined;
onSelectStory: (i: ExampleItem, id: string) => void;
}): VNode {
const [isOpen, setOpen] = useState(selected && selected.group === name);
return (
setOpen(!isOpen)}
>
{name}
);
}
/**
* Prevents the UI from redirecting and inform the dev
* where the should have redirected
* @returns
*/
function PreventLinkNavigation({
children,
}: {
children: ComponentChildren;
}): VNode {
return (
{
let t: any = e.target;
do {
if (t.localName === "a" && t.getAttribute("href")) {
alert(`should navigate to: ${t.attributes.href.value}`);
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
return false;
}
} while ((t = t.parentNode));
return true;
}}
>
{children}
);
}
function ErrorReport({
children,
selected,
}: {
children: ComponentChild;
selected: ExampleItem | undefined;
}): VNode {
const [error, resetError] = useErrorBoundary();
//if there is an error, reset when unloading this component
useEffect(() => (error ? resetError : undefined));
if (error) {
return (
Error was thrown trying to render
{selected && (
)}
{error.message}
{error.stack}
);
}
return {children} ;
}
function getSelectionFromLocationHash(
hash: string,
allExamples: Group[],
): ExampleItem | undefined {
if (!hash) return undefined;
const parts = hash.substring(1).split("-");
if (parts.length < 3) return undefined;
return findByGroupComponentName(
allExamples,
decodeURIComponent(parts[0]),
decodeURIComponent(parts[1]),
decodeURIComponent(parts[2]),
);
}
function parseExampleImport(
group: string,
componentName: string,
im: MaybeComponent,
): ComponentItem {
const examples: ExampleItem[] = Object.entries(im)
.filter(([k]) => k !== "default")
.map(([exampleName, exampleValue]): ExampleItem => {
if (!exampleValue) {
throw Error(
`example "${exampleName}" from component "${componentName}" in group "${group}" is undefined`,
);
}
if (typeof exampleValue === "function") {
return {
group,
component: componentName,
name: exampleName,
render: {
component: exampleValue as FunctionComponent,
props: {},
contextProps: {},
},
};
}
const v: any = exampleValue;
if (
"component" in v &&
typeof v.component === "function" &&
"props" in v
) {
return {
group,
component: componentName,
name: exampleName,
render: v,
};
}
throw Error(
`example "${exampleName}" from component "${componentName}" in group "${group}" doesn't follow one of the two ways of example`,
);
});
return {
name: componentName,
examples,
};
}
export function parseGroupImport(
groups: Record,
): Group[] {
return Object.entries(groups).map(([groupName, value]) => {
return {
title: groupName,
list: Object.entries(value).flatMap(([key, value]) =>
folder(groupName, value),
),
};
});
}
export interface Group {
title: string;
list: ComponentItem[];
}
export interface ComponentItem {
name: string;
examples: ExampleItem[];
}
export interface ExampleItem {
group: string;
component: string;
name: string;
render: ExampleItemSetup;
}
type ComponentOrFolder = MaybeComponent | MaybeFolder;
interface MaybeFolder {
default?: { title: string };
// [exampleName: string]: FunctionalComponent;
}
interface MaybeComponent {
// default?: undefined;
[exampleName: string]: undefined | object;
}
function folder(groupName: string, value: ComponentOrFolder): ComponentItem[] {
let title: string | undefined = undefined;
try {
title =
typeof value === "object" &&
typeof value.default === "object" &&
value.default !== undefined &&
"title" in value.default &&
typeof value.default.title === "string"
? value.default.title
: undefined;
} catch (e) {
throw Error(
`Could not defined if it is component or folder ${groupName}: ${JSON.stringify(
value,
undefined,
2,
)}`,
);
}
if (title) {
const c = parseExampleImport(groupName, title, value as MaybeComponent);
return [c];
}
return Object.entries(value).flatMap(([subkey, value]) =>
folder(groupName, value),
);
}
interface Props {
getWrapperForGroup: (name: string) => FunctionComponent;
examplesInGroups: Group[];
langs: Record;
}
function Application({
langs,
examplesInGroups,
getWrapperForGroup,
}: Props): VNode {
const url = new URL(window.location.href);
const initialSelection = getSelectionFromLocationHash(
url.hash,
examplesInGroups,
);
const currentLang = url.searchParams.get("lang") || "en";
if (!langs["en"]) {
langs["en"] = {};
}
setupI18n(currentLang, langs);
const [selected, updateSelected] = useState(
initialSelection,
);
const [sidebarWidth, setSidebarWidth] = useState(200);
useEffect(() => {
if (url.hash) {
const hash = url.hash.substring(1);
const found = document.getElementById(hash);
if (found) {
setTimeout(() => {
found.scrollIntoView({
block: "center",
});
}, 50);
}
}
}, []);
const GroupWrapper = getWrapperForGroup(selected?.group || "default");
const ExampleContent = getContentForExample(selected, examplesInGroups);
//style={{ "--with-size": `${sidebarWidth}px` }}
return (
{/* */}
Language:
{
const url = new URL(window.location.href);
url.searchParams.set("lang", e.currentTarget.value);
window.location.href = url.href;
}}
>
{Object.keys(langs).map((l) => (
{l}
))}
{examplesInGroups.map((group) => (
{
document.getElementById(htmlId)?.scrollIntoView({
block: "center",
});
updateSelected(item);
}}
/>
))}
{/* {
setSidebarWidth((s) => s + x);
}}
/> */}
);
}
export interface Options {
id?: string;
strings?: any;
getWrapperForGroup?: (name: string) => FunctionComponent;
}
export function renderStories(
groups: Record,
options: Options = {},
): void {
const examples = parseGroupImport(groups);
try {
const cid = options.id ?? "container";
const container = document.getElementById(cid);
if (!container) {
throw Error(
`container with id ${cid} not found, can't mount page contents`,
);
}
render(
Fragment)}
langs={options.strings ?? { en: {} }}
/>,
container,
);
} catch (e) {
console.error("got error", e);
if (e instanceof Error) {
document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
}
}
}
function ResizeHandle({ onUpdate }: { onUpdate: (x: number) => void }): VNode {
const [start, setStart] = useState(undefined);
return (
{
setStart(e.pageX);
console.log("active", e.pageX);
return false;
}}
onMouseMove={(e: any) => {
if (start !== undefined) {
onUpdate(e.pageX - start);
}
return false;
}}
onMouseUp={() => {
setStart(undefined);
return false;
}}
/>
);
}