summaryrefslogtreecommitdiff
path: root/packages/anastasis-webui/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/anastasis-webui/src/components')
-rw-r--r--packages/anastasis-webui/src/components/AsyncButton.tsx49
-rw-r--r--packages/anastasis-webui/src/components/Notifications.tsx59
-rw-r--r--packages/anastasis-webui/src/components/QR.tsx35
-rw-r--r--packages/anastasis-webui/src/components/fields/DateInput.tsx74
-rw-r--r--packages/anastasis-webui/src/components/fields/EmailInput.tsx44
-rw-r--r--packages/anastasis-webui/src/components/fields/FileInput.tsx81
-rw-r--r--packages/anastasis-webui/src/components/fields/ImageInput.tsx81
-rw-r--r--packages/anastasis-webui/src/components/fields/NumberInput.tsx43
-rw-r--r--packages/anastasis-webui/src/components/fields/TextInput.tsx42
-rw-r--r--packages/anastasis-webui/src/components/menu/NavigationBar.tsx2
-rw-r--r--packages/anastasis-webui/src/components/menu/SideBar.tsx66
-rw-r--r--packages/anastasis-webui/src/components/picker/DatePicker.tsx326
-rw-r--r--packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx50
-rw-r--r--packages/anastasis-webui/src/components/picker/DurationPicker.tsx154
14 files changed, 1066 insertions, 40 deletions
diff --git a/packages/anastasis-webui/src/components/AsyncButton.tsx b/packages/anastasis-webui/src/components/AsyncButton.tsx
new file mode 100644
index 000000000..92bef2219
--- /dev/null
+++ b/packages/anastasis-webui/src/components/AsyncButton.tsx
@@ -0,0 +1,49 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { ComponentChildren, h, VNode } from "preact";
+// import { LoadingModal } from "../modal";
+import { useAsync } from "../hooks/async";
+// import { Translate } from "../../i18n";
+
+type Props = {
+ children: ComponentChildren;
+ disabled?: boolean;
+ onClick?: () => Promise<void>;
+ [rest: string]: any;
+};
+
+export function AsyncButton({ onClick, disabled, children, ...rest }: Props): VNode {
+ const { isLoading, request } = useAsync(onClick);
+
+ // if (isSlow) {
+ // return <LoadingModal onCancel={cancel} />;
+ // }
+ if (isLoading) {
+ return <button class="button">Loading...</button>;
+ }
+
+ return <span data-tooltip={rest['data-tooltip']} style={{marginLeft: 5}}>
+ <button {...rest} onClick={request} disabled={disabled}>
+ {children}
+ </button>
+ </span>;
+}
diff --git a/packages/anastasis-webui/src/components/Notifications.tsx b/packages/anastasis-webui/src/components/Notifications.tsx
new file mode 100644
index 000000000..c916020d7
--- /dev/null
+++ b/packages/anastasis-webui/src/components/Notifications.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { h, VNode } from "preact";
+
+export interface Notification {
+ message: string;
+ description?: string | VNode;
+ type: MessageType;
+}
+
+export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS'
+
+interface Props {
+ notifications: Notification[];
+ removeNotification?: (n: Notification) => void;
+}
+
+function messageStyle(type: MessageType): string {
+ switch (type) {
+ case "INFO": return "message is-info";
+ case "WARN": return "message is-warning";
+ case "ERROR": return "message is-danger";
+ case "SUCCESS": return "message is-success";
+ default: return "message"
+ }
+}
+
+export function Notifications({ notifications, removeNotification }: Props): VNode {
+ return <div class="block">
+ {notifications.map((n,i) => <article key={i} class={messageStyle(n.type)}>
+ <div class="message-header">
+ <p>{n.message}</p>
+ <button class="delete" onClick={() => removeNotification && removeNotification(n)} />
+ </div>
+ {n.description && <div class="message-body">
+ {n.description}
+ </div>}
+ </article>)}
+ </div>
+} \ No newline at end of file
diff --git a/packages/anastasis-webui/src/components/QR.tsx b/packages/anastasis-webui/src/components/QR.tsx
new file mode 100644
index 000000000..48f1a7c12
--- /dev/null
+++ b/packages/anastasis-webui/src/components/QR.tsx
@@ -0,0 +1,35 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 <http://www.gnu.org/licenses/>
+ */
+
+import { h, VNode } from "preact";
+import { useEffect, useRef } from "preact/hooks";
+import qrcode from "qrcode-generator";
+
+export function QR({ text }: { text: string }): VNode {
+ const divRef = useRef<HTMLDivElement>(null);
+ useEffect(() => {
+ const qr = qrcode(0, 'L');
+ qr.addData(text);
+ qr.make();
+ if (divRef.current) divRef.current.innerHTML = qr.createSvgTag({
+ scalable: true,
+ });
+ });
+
+ return <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+ <div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} />
+ </div>;
+}
diff --git a/packages/anastasis-webui/src/components/fields/DateInput.tsx b/packages/anastasis-webui/src/components/fields/DateInput.tsx
new file mode 100644
index 000000000..3148c953f
--- /dev/null
+++ b/packages/anastasis-webui/src/components/fields/DateInput.tsx
@@ -0,0 +1,74 @@
+import { format, isAfter, parse, sub, subYears } from "date-fns";
+import { h, VNode } from "preact";
+import { useLayoutEffect, useRef, useState } from "preact/hooks";
+import { DatePicker } from "../picker/DatePicker";
+
+export interface DateInputProps {
+ label: string;
+ grabFocus?: boolean;
+ tooltip?: string;
+ error?: string;
+ years?: Array<number>;
+ bind: [string, (x: string) => void];
+}
+
+export function DateInput(props: DateInputProps): VNode {
+ const inputRef = useRef<HTMLInputElement>(null);
+ useLayoutEffect(() => {
+ if (props.grabFocus) {
+ inputRef.current?.focus();
+ }
+ }, [props.grabFocus]);
+ const [opened, setOpened] = useState(false)
+
+ const value = props.bind[0] || "";
+ const [dirty, setDirty] = useState(false)
+ const showError = dirty && props.error
+
+ const calendar = subYears(new Date(), 30)
+
+ return <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ <div class="control">
+ <div class="field has-addons">
+ <p class="control">
+ <input
+ type="text"
+ class={showError ? 'input is-danger' : 'input'}
+ value={value}
+ onInput={(e) => {
+ const text = e.currentTarget.value
+ setDirty(true)
+ props.bind[1](text);
+ }}
+ ref={inputRef} />
+ </p>
+ <p class="control">
+ <a class="button" onClick={() => { setOpened(true) }}>
+ <span class="icon"><i class="mdi mdi-calendar" /></span>
+ </a>
+ </p>
+ </div>
+ </div>
+ <p class="help">Using the format yyyy-mm-dd</p>
+ {showError && <p class="help is-danger">{props.error}</p>}
+ <DatePicker
+ opened={opened}
+ initialDate={calendar}
+ years={props.years}
+ closeFunction={() => setOpened(false)}
+ dateReceiver={(d) => {
+ setDirty(true)
+ const v = format(d, 'yyyy-MM-dd')
+ props.bind[1](v);
+ }}
+ />
+ </div>
+ ;
+
+}
diff --git a/packages/anastasis-webui/src/components/fields/EmailInput.tsx b/packages/anastasis-webui/src/components/fields/EmailInput.tsx
new file mode 100644
index 000000000..e21418fea
--- /dev/null
+++ b/packages/anastasis-webui/src/components/fields/EmailInput.tsx
@@ -0,0 +1,44 @@
+import { h, VNode } from "preact";
+import { useLayoutEffect, useRef, useState } from "preact/hooks";
+
+export interface TextInputProps {
+ label: string;
+ grabFocus?: boolean;
+ error?: string;
+ placeholder?: string;
+ tooltip?: string;
+ bind: [string, (x: string) => void];
+}
+
+export function EmailInput(props: TextInputProps): VNode {
+ const inputRef = useRef<HTMLInputElement>(null);
+ useLayoutEffect(() => {
+ if (props.grabFocus) {
+ inputRef.current?.focus();
+ }
+ }, [props.grabFocus]);
+ const value = props.bind[0];
+ const [dirty, setDirty] = useState(false)
+ const showError = dirty && props.error
+ return (<div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ <div class="control has-icons-right">
+ <input
+ value={value}
+ required
+ placeholder={props.placeholder}
+ type="email"
+ class={showError ? 'input is-danger' : 'input'}
+ onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
+ ref={inputRef}
+ style={{ display: "block" }} />
+ </div>
+ {showError && <p class="help is-danger">{props.error}</p>}
+ </div>
+ );
+}
diff --git a/packages/anastasis-webui/src/components/fields/FileInput.tsx b/packages/anastasis-webui/src/components/fields/FileInput.tsx
new file mode 100644
index 000000000..8b144ea43
--- /dev/null
+++ b/packages/anastasis-webui/src/components/fields/FileInput.tsx
@@ -0,0 +1,81 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { h, VNode } from "preact";
+import { useLayoutEffect, useRef, useState } from "preact/hooks";
+import { TextInputProps } from "./TextInput";
+
+const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024
+
+export function FileInput(props: TextInputProps): VNode {
+ const inputRef = useRef<HTMLInputElement>(null);
+ useLayoutEffect(() => {
+ if (props.grabFocus) {
+ inputRef.current?.focus();
+ }
+ }, [props.grabFocus]);
+
+ const value = props.bind[0];
+ // const [dirty, setDirty] = useState(false)
+ const image = useRef<HTMLInputElement>(null)
+ const [sizeError, setSizeError] = useState(false)
+ function onChange(v: string): void {
+ // setDirty(true);
+ props.bind[1](v);
+ }
+ return <div class="field">
+ <label class="label">
+ <a onClick={() => image.current?.click()}>
+ {props.label}
+ </a>
+ {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ <div class="control">
+ <input
+ ref={image} style={{ display: 'none' }}
+ type="file" name={String(name)}
+ onChange={e => {
+ const f: FileList | null = e.currentTarget.files
+ if (!f || f.length != 1) {
+ return onChange("")
+ }
+ if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
+ setSizeError(true)
+ return onChange("")
+ }
+ setSizeError(false)
+ return f[0].arrayBuffer().then(b => {
+ const b64 = btoa(
+ new Uint8Array(b)
+ .reduce((data, byte) => data + String.fromCharCode(byte), '')
+ )
+ return onChange(`data:${f[0].type};base64,${b64}` as any)
+ })
+ }} />
+ {props.error && <p class="help is-danger">{props.error}</p>}
+ {sizeError && <p class="help is-danger">
+ File should be smaller than 1 MB
+ </p>}
+ </div>
+ </div>
+}
+
diff --git a/packages/anastasis-webui/src/components/fields/ImageInput.tsx b/packages/anastasis-webui/src/components/fields/ImageInput.tsx
new file mode 100644
index 000000000..d5bf643d4
--- /dev/null
+++ b/packages/anastasis-webui/src/components/fields/ImageInput.tsx
@@ -0,0 +1,81 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { h, VNode } from "preact";
+import { useLayoutEffect, useRef, useState } from "preact/hooks";
+import emptyImage from "../../assets/empty.png";
+import { TextInputProps } from "./TextInput";
+
+const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024
+
+export function ImageInput(props: TextInputProps): VNode {
+ const inputRef = useRef<HTMLInputElement>(null);
+ useLayoutEffect(() => {
+ if (props.grabFocus) {
+ inputRef.current?.focus();
+ }
+ }, [props.grabFocus]);
+
+ const value = props.bind[0];
+ // const [dirty, setDirty] = useState(false)
+ const image = useRef<HTMLInputElement>(null)
+ const [sizeError, setSizeError] = useState(false)
+ function onChange(v: string): void {
+ // setDirty(true);
+ props.bind[1](v);
+ }
+ return <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ <div class="control">
+ <img src={!value ? emptyImage : value} style={{ width: 200, height: 200 }} onClick={() => image.current?.click()} />
+ <input
+ ref={image} style={{ display: 'none' }}
+ type="file" name={String(name)}
+ onChange={e => {
+ const f: FileList | null = e.currentTarget.files
+ if (!f || f.length != 1) {
+ return onChange(emptyImage)
+ }
+ if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
+ setSizeError(true)
+ return onChange(emptyImage)
+ }
+ setSizeError(false)
+ return f[0].arrayBuffer().then(b => {
+ const b64 = btoa(
+ new Uint8Array(b)
+ .reduce((data, byte) => data + String.fromCharCode(byte), '')
+ )
+ return onChange(`data:${f[0].type};base64,${b64}` as any)
+ })
+ }} />
+ {props.error && <p class="help is-danger">{props.error}</p>}
+ {sizeError && <p class="help is-danger">
+ Image should be smaller than 1 MB
+ </p>}
+ </div>
+ </div>
+}
+
diff --git a/packages/anastasis-webui/src/components/fields/NumberInput.tsx b/packages/anastasis-webui/src/components/fields/NumberInput.tsx
new file mode 100644
index 000000000..2afb242b8
--- /dev/null
+++ b/packages/anastasis-webui/src/components/fields/NumberInput.tsx
@@ -0,0 +1,43 @@
+import { h, VNode } from "preact";
+import { useLayoutEffect, useRef, useState } from "preact/hooks";
+
+export interface TextInputProps {
+ label: string;
+ grabFocus?: boolean;
+ error?: string;
+ placeholder?: string;
+ tooltip?: string;
+ bind: [string, (x: string) => void];
+}
+
+export function NumberInput(props: TextInputProps): VNode {
+ const inputRef = useRef<HTMLInputElement>(null);
+ useLayoutEffect(() => {
+ if (props.grabFocus) {
+ inputRef.current?.focus();
+ }
+ }, [props.grabFocus]);
+ const value = props.bind[0];
+ const [dirty, setDirty] = useState(false)
+ const showError = dirty && props.error
+ return (<div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ <div class="control has-icons-right">
+ <input
+ value={value}
+ type="number"
+ placeholder={props.placeholder}
+ class={showError ? 'input is-danger' : 'input'}
+ onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
+ ref={inputRef}
+ style={{ display: "block" }} />
+ </div>
+ {showError && <p class="help is-danger">{props.error}</p>}
+ </div>
+ );
+}
diff --git a/packages/anastasis-webui/src/components/fields/TextInput.tsx b/packages/anastasis-webui/src/components/fields/TextInput.tsx
new file mode 100644
index 000000000..c093689c5
--- /dev/null
+++ b/packages/anastasis-webui/src/components/fields/TextInput.tsx
@@ -0,0 +1,42 @@
+import { h, VNode } from "preact";
+import { useLayoutEffect, useRef, useState } from "preact/hooks";
+
+export interface TextInputProps {
+ label: string;
+ grabFocus?: boolean;
+ error?: string;
+ placeholder?: string;
+ tooltip?: string;
+ bind: [string, (x: string) => void];
+}
+
+export function TextInput(props: TextInputProps): VNode {
+ const inputRef = useRef<HTMLInputElement>(null);
+ useLayoutEffect(() => {
+ if (props.grabFocus) {
+ inputRef.current?.focus();
+ }
+ }, [props.grabFocus]);
+ const value = props.bind[0];
+ const [dirty, setDirty] = useState(false)
+ const showError = dirty && props.error
+ return (<div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ <div class="control has-icons-right">
+ <input
+ value={value}
+ placeholder={props.placeholder}
+ class={showError ? 'input is-danger' : 'input'}
+ onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
+ ref={inputRef}
+ style={{ display: "block" }} />
+ </div>
+ {showError && <p class="help is-danger">{props.error}</p>}
+ </div>
+ );
+}
diff --git a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
index e1bb4c7c0..935951ab9 100644
--- a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
@@ -49,7 +49,7 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode {
</a>
<div class="navbar-end">
<div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
- <LangSelector />
+ {/* <LangSelector /> */}
</div>
</div>
</div>
diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx b/packages/anastasis-webui/src/components/menu/SideBar.tsx
index df582a5d0..72655662f 100644
--- a/packages/anastasis-webui/src/components/menu/SideBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx
@@ -33,14 +33,15 @@ interface Props {
export function Sidebar({ mobile }: Props): VNode {
// const config = useConfigContext();
const config = { version: 'none' }
+ // FIXME: add replacement for __VERSION__ with the current version
const process = { env: { __VERSION__: '0.0.0' } }
const reducer = useAnastasisContext()!
return (
<aside class="aside is-placed-left is-expanded">
- {mobile && <div class="footer" onClick={(e) => { return e.stopImmediatePropagation() }}>
+ {/* {mobile && <div class="footer" onClick={(e) => { return e.stopImmediatePropagation() }}>
<LangSelector />
- </div>}
+ </div>} */}
<div class="aside-tools">
<div class="aside-tools-label">
<div><b>Anastasis</b> Reducer</div>
@@ -59,97 +60,84 @@ export function Sidebar({ mobile }: Props): VNode {
{!reducer.currentReducerState &&
<li>
<div class="ml-4">
- <span class="menu-item-label"><Translate>Start one options</Translate></span>
+ <span class="menu-item-label"><Translate>Select one option</Translate></span>
</div>
</li>
}
{reducer.currentReducerState && reducer.currentReducerState.backup_state ? <Fragment>
- <li class={reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ? 'is-active' : ''}>
+ <li class={reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
+ reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>Continent selection</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Country selection</Translate></span>
+ <span class="menu-item-label"><Translate>Location</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.UserAttributesCollecting ? 'is-active' : ''}>
<div class="ml-4">
-
- <span class="menu-item-label"><Translate>User attributes</Translate></span>
+ <span class="menu-item-label"><Translate>Personal information</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.AuthenticationsEditing ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>Auth methods</Translate></span>
+ <span class="menu-item-label"><Translate>Authorization methods</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>PoliciesReviewing</Translate></span>
+ <span class="menu-item-label"><Translate>Policies</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>SecretEditing</Translate></span>
+ <span class="menu-item-label"><Translate>Secret input</Translate></span>
</div>
</li>
- <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
+ {/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>PoliciesPaying</Translate></span>
+ <span class="menu-item-label"><Translate>Payment (optional)</Translate></span>
</div>
- </li>
+ </li> */}
<li class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>BackupFinished</Translate></span>
+ <span class="menu-item-label"><Translate>Backup completed</Translate></span>
</div>
</li>
- <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
+ {/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>TruthsPaying</Translate></span>
+ <span class="menu-item-label"><Translate>Truth Paying</Translate></span>
</div>
- </li>
+ </li> */}
</Fragment> : (reducer.currentReducerState && reducer.currentReducerState?.recovery_state && <Fragment>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting ? 'is-active' : ''}>
+ <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting ||
+ reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>TruthsPaying</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>CountrySelecting</Translate></span>
+ <span class="menu-item-label"><Translate>Location</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>UserAttributesCollecting</Translate></span>
+ <span class="menu-item-label"><Translate>Personal information</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.SecretSelecting ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>SecretSelecting</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSelecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>ChallengeSelecting</Translate></span>
+ <span class="menu-item-label"><Translate>Secret selection</Translate></span>
</div>
</li>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSolving ? 'is-active' : ''}>
+ <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSelecting ||
+ reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSolving ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>ChallengeSolving</Translate></span>
+ <span class="menu-item-label"><Translate>Solve Challenges</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.RecoveryFinished ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>RecoveryFinished</Translate></span>
+ <span class="menu-item-label"><Translate>Secret recovered</Translate></span>
</div>
</li>
</Fragment>)}
diff --git a/packages/anastasis-webui/src/components/picker/DatePicker.tsx b/packages/anastasis-webui/src/components/picker/DatePicker.tsx
new file mode 100644
index 000000000..eb5d8145d
--- /dev/null
+++ b/packages/anastasis-webui/src/components/picker/DatePicker.tsx
@@ -0,0 +1,326 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { h, Component } from "preact";
+
+interface Props {
+ closeFunction?: () => void;
+ dateReceiver?: (d: Date) => void;
+ initialDate?: Date;
+ years?: Array<number>;
+ opened?: boolean;
+}
+interface State {
+ displayedMonth: number;
+ displayedYear: number;
+ selectYearMode: boolean;
+ currentDate: Date;
+}
+const now = new Date()
+
+const monthArrShortFull = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December'
+]
+
+const monthArrShort = [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec'
+]
+
+const dayArr = [
+ 'Sun',
+ 'Mon',
+ 'Tue',
+ 'Wed',
+ 'Thu',
+ 'Fri',
+ 'Sat'
+]
+
+const yearArr: number[] = []
+
+
+// inspired by https://codepen.io/m4r1vs/pen/MOOxyE
+export class DatePicker extends Component<Props, State> {
+
+ closeDatePicker() {
+ this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent
+ }
+
+ /**
+ * Gets fired when a day gets clicked.
+ * @param {object} e The event thrown by the <span /> element clicked
+ */
+ dayClicked(e: any) {
+
+ const element = e.target; // the actual element clicked
+
+ if (element.innerHTML === '') return false; // don't continue if <span /> empty
+
+ // get date from clicked element (gets attached when rendered)
+ const date = new Date(element.getAttribute('data-value'));
+
+ // update the state
+ this.setState({ currentDate: date });
+ this.passDateToParent(date)
+ }
+
+ /**
+ * returns days in month as array
+ * @param {number} month the month to display
+ * @param {number} year the year to display
+ */
+ getDaysByMonth(month: number, year: number) {
+
+ const calendar = [];
+
+ const date = new Date(year, month, 1); // month to display
+
+ const firstDay = new Date(year, month, 1).getDay(); // first weekday of month
+ const lastDate = new Date(year, month + 1, 0).getDate(); // last date of month
+
+ let day: number | null = 0;
+
+ // the calendar is 7*6 fields big, so 42 loops
+ for (let i = 0; i < 42; i++) {
+
+ if (i >= firstDay && day !== null) day = day + 1;
+ if (day !== null && day > lastDate) day = null;
+
+ // append the calendar Array
+ calendar.push({
+ day: (day === 0 || day === null) ? null : day, // null or number
+ date: (day === 0 || day === null) ? null : new Date(year, month, day), // null or Date()
+ today: (day === now.getDate() && month === now.getMonth() && year === now.getFullYear()) // boolean
+ });
+ }
+
+ return calendar;
+ }
+
+ /**
+ * Display previous month by updating state
+ */
+ displayPrevMonth() {
+ if (this.state.displayedMonth <= 0) {
+ this.setState({
+ displayedMonth: 11,
+ displayedYear: this.state.displayedYear - 1
+ });
+ }
+ else {
+ this.setState({
+ displayedMonth: this.state.displayedMonth - 1
+ });
+ }
+ }
+
+ /**
+ * Display next month by updating state
+ */
+ displayNextMonth() {
+ if (this.state.displayedMonth >= 11) {
+ this.setState({
+ displayedMonth: 0,
+ displayedYear: this.state.displayedYear + 1
+ });
+ }
+ else {
+ this.setState({
+ displayedMonth: this.state.displayedMonth + 1
+ });
+ }
+ }
+
+ /**
+ * Display the selected month (gets fired when clicking on the date string)
+ */
+ displaySelectedMonth() {
+ if (this.state.selectYearMode) {
+ this.toggleYearSelector();
+ }
+ else {
+ if (!this.state.currentDate) return false;
+ this.setState({
+ displayedMonth: this.state.currentDate.getMonth(),
+ displayedYear: this.state.currentDate.getFullYear()
+ });
+ }
+ }
+
+ toggleYearSelector() {
+ this.setState({ selectYearMode: !this.state.selectYearMode });
+ }
+
+ changeDisplayedYear(e: any) {
+ const element = e.target;
+ this.toggleYearSelector();
+ this.setState({ displayedYear: parseInt(element.innerHTML, 10), displayedMonth: 0 });
+ }
+
+ /**
+ * Pass the selected date to parent when 'OK' is clicked
+ */
+ passSavedDateDateToParent() {
+ this.passDateToParent(this.state.currentDate)
+ }
+ passDateToParent(date: Date) {
+ if (typeof this.props.dateReceiver === 'function') this.props.dateReceiver(date);
+ this.closeDatePicker();
+ }
+
+ componentDidUpdate() {
+ // if (this.state.selectYearMode) {
+ // document.getElementsByClassName('selected')[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it
+ // }
+ }
+
+ constructor(props: any) {
+ super(props);
+
+ this.closeDatePicker = this.closeDatePicker.bind(this);
+ this.dayClicked = this.dayClicked.bind(this);
+ this.displayNextMonth = this.displayNextMonth.bind(this);
+ this.displayPrevMonth = this.displayPrevMonth.bind(this);
+ this.getDaysByMonth = this.getDaysByMonth.bind(this);
+ this.changeDisplayedYear = this.changeDisplayedYear.bind(this);
+ this.passDateToParent = this.passDateToParent.bind(this);
+ this.toggleYearSelector = this.toggleYearSelector.bind(this);
+ this.displaySelectedMonth = this.displaySelectedMonth.bind(this);
+
+ const initial = props.initialDate || now;
+
+ this.state = {
+ currentDate: initial,
+ displayedMonth: initial.getMonth(),
+ displayedYear: initial.getFullYear(),
+ selectYearMode: false
+ }
+ }
+
+ render() {
+
+ const { currentDate, displayedMonth, displayedYear, selectYearMode } = this.state;
+
+ return (
+ <div>
+ <div class={`datePicker ${ this.props.opened && "datePicker--opened"}`}>
+
+ <div class="datePicker--titles">
+ <h3 style={{
+ color: selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)'
+ }} onClick={this.toggleYearSelector}>{currentDate.getFullYear()}</h3>
+ <h2 style={{
+ color: !selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)'
+ }} onClick={this.displaySelectedMonth}>
+ {dayArr[currentDate.getDay()]}, {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()}
+ </h2>
+ </div>
+
+ {!selectYearMode && <nav>
+ <span onClick={this.displayPrevMonth} class="icon"><i style={{ transform: 'rotate(180deg)' }} class="mdi mdi-forward" /></span>
+ <h4>{monthArrShortFull[displayedMonth]} {displayedYear}</h4>
+ <span onClick={this.displayNextMonth} class="icon"><i class="mdi mdi-forward" /></span>
+ </nav>}
+
+ <div class="datePicker--scroll">
+
+ {!selectYearMode && <div class="datePicker--calendar" >
+
+ <div class="datePicker--dayNames">
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day,i) => <span key={i}>{day}</span>)}
+ </div>
+
+ <div onClick={this.dayClicked} class="datePicker--days">
+
+ {/*
+ Loop through the calendar object returned by getDaysByMonth().
+ */}
+
+ {this.getDaysByMonth(this.state.displayedMonth, this.state.displayedYear)
+ .map(
+ day => {
+ let selected = false;
+
+ if (currentDate && day.date) selected = (currentDate.toLocaleDateString() === day.date.toLocaleDateString());
+
+ return (<span key={day.day}
+ class={(day.today ? 'datePicker--today ' : '') + (selected ? 'datePicker--selected' : '')}
+ disabled={!day.date}
+ data-value={day.date}
+ >
+ {day.day}
+ </span>)
+ }
+ )
+ }
+
+ </div>
+
+ </div>}
+
+ {selectYearMode && <div class="datePicker--selectYear">
+ {(this.props.years || yearArr).map(year => (
+ <span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}>
+ {year}
+ </span>
+ ))}
+
+ </div>}
+
+ </div>
+ </div>
+
+ <div class="datePicker--background" onClick={this.closeDatePicker} style={{
+ display: this.props.opened ? 'block' : 'none',
+ }}
+ />
+
+ </div>
+ )
+ }
+}
+
+
+for (let i = 2010; i <= now.getFullYear() + 10; i++) {
+ yearArr.push(i);
+}
diff --git a/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx b/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx
new file mode 100644
index 000000000..275c80fa6
--- /dev/null
+++ b/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx
@@ -0,0 +1,50 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { h, FunctionalComponent } from 'preact';
+import { useState } from 'preact/hooks';
+import { DurationPicker as TestedComponent } from './DurationPicker';
+
+
+export default {
+ title: 'Components/Picker/Duration',
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: 'onCreate' },
+ goBack: { action: 'goBack' },
+ }
+};
+
+function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) {
+ const r = (args: any) => <Component {...args} />
+ r.args = props
+ return r
+}
+
+export const Example = createExample(TestedComponent, {
+ days: true, minutes: true, hours: true, seconds: true,
+ value: 10000000
+});
+
+export const WithState = () => {
+ const [v,s] = useState<number>(1000000)
+ return <TestedComponent value={v} onChange={s} days minutes hours seconds />
+}
diff --git a/packages/anastasis-webui/src/components/picker/DurationPicker.tsx b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
new file mode 100644
index 000000000..235a63e2d
--- /dev/null
+++ b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
@@ -0,0 +1,154 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useTranslator } from "../../i18n";
+import "../../scss/DurationPicker.scss";
+
+export interface Props {
+ hours?: boolean;
+ minutes?: boolean;
+ seconds?: boolean;
+ days?: boolean;
+ onChange: (value: number) => void;
+ value: number
+}
+
+// inspiration taken from https://github.com/flurmbo/react-duration-picker
+export function DurationPicker({ days, hours, minutes, seconds, onChange, value }: Props): VNode {
+ const ss = 1000
+ const ms = ss * 60
+ const hs = ms * 60
+ const ds = hs * 24
+ const i18n = useTranslator()
+
+ return <div class="rdp-picker">
+ {days && <DurationColumn unit={i18n`days`} max={99}
+ value={Math.floor(value / ds)}
+ onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined}
+ onChange={diff => onChange(value + diff * ds)}
+ />}
+ {hours && <DurationColumn unit={i18n`hours`} max={23} min={1}
+ value={Math.floor(value / hs) % 24}
+ onDecrease={value >= hs ? () => onChange(value - hs) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined}
+ onChange={diff => onChange(value + diff * hs)}
+ />}
+ {minutes && <DurationColumn unit={i18n`minutes`} max={59} min={1}
+ value={Math.floor(value / ms) % 60}
+ onDecrease={value >= ms ? () => onChange(value - ms) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined}
+ onChange={diff => onChange(value + diff * ms)}
+ />}
+ {seconds && <DurationColumn unit={i18n`seconds`} max={59}
+ value={Math.floor(value / ss) % 60}
+ onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined}
+ onChange={diff => onChange(value + diff * ss)}
+ />}
+ </div>
+}
+
+interface ColProps {
+ unit: string,
+ min?: number,
+ max: number,
+ value: number,
+ onIncrease?: () => void;
+ onDecrease?: () => void;
+ onChange?: (diff: number) => void;
+}
+
+function InputNumber({ initial, onChange }: { initial: number, onChange: (n: number) => void }) {
+ const [value, handler] = useState<{v:string}>({
+ v: toTwoDigitString(initial)
+ })
+
+ return <input
+ value={value.v}
+ onBlur={(e) => onChange(parseInt(value.v, 10))}
+ onInput={(e) => {
+ e.preventDefault()
+ const n = Number.parseInt(e.currentTarget.value, 10);
+ if (isNaN(n)) return handler({v:toTwoDigitString(initial)})
+ return handler({v:toTwoDigitString(n)})
+ }}
+ style={{ width: 50, border: 'none', fontSize: 'inherit', background: 'inherit' }} />
+}
+
+function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, onChange }: ColProps): VNode {
+
+ const cellHeight = 35
+ return (
+ <div class="rdp-column-container">
+ <div class="rdp-masked-div">
+ <hr class="rdp-reticule" style={{ top: cellHeight * 2 - 1 }} />
+ <hr class="rdp-reticule" style={{ top: cellHeight * 3 - 1 }} />
+
+ <div class="rdp-column" style={{ top: 0 }}>
+
+ <div class="rdp-cell" key={value - 2}>
+ {onDecrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }}
+ onClick={onDecrease}>
+ <span class="icon">
+ <i class="mdi mdi-chevron-up" />
+ </span>
+ </button>}
+ </div>
+ <div class="rdp-cell" key={value - 1}>
+ {value > min ? toTwoDigitString(value - 1) : ''}
+ </div>
+ <div class="rdp-cell rdp-center" key={value}>
+ {onChange ?
+ <InputNumber initial={value} onChange={(n) => onChange(n - value)} /> :
+ toTwoDigitString(value)
+ }
+ <div>{unit}</div>
+ </div>
+
+ <div class="rdp-cell" key={value + 1}>
+ {value < max ? toTwoDigitString(value + 1) : ''}
+ </div>
+
+ <div class="rdp-cell" key={value + 2}>
+ {onIncrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }}
+ onClick={onIncrease}>
+ <span class="icon">
+ <i class="mdi mdi-chevron-down" />
+ </span>
+ </button>}
+ </div>
+
+ </div>
+ </div>
+ </div>
+ );
+}
+
+
+function toTwoDigitString(n: number) {
+ if (n < 10) {
+ return `0${n}`;
+ }
+ return `${n}`;
+} \ No newline at end of file